libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit d1e60b16cb9fef51729a5d0fed9d09c56b5f8836
parent b8a428ce9541d9ede965183ea447d266ad217aad
Author: Florian Dold <florian.dold@gmail.com>
Date:   Thu,  7 Nov 2019 14:37:47 +0100

ebics transactions WIP

Diffstat:
A.idea/uiDesigner.xml | 125+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msandbox/src/main/kotlin/tech/libeufin/sandbox/CryptoUtil.kt | 6------
Msandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt | 17+++++++++++------
Msandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsOrderUtil.kt | 17+++++++++++++++++
Msandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt | 284++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msandbox/src/main/kotlin/tech/libeufin/sandbox/XMLUtil.kt | 21+++++++++++----------
Asandbox/src/main/kotlin/tech/libeufin/sandbox/XmlBinding.kt | 24++++++++++++++++++++++++
Asandbox/src/main/kotlin/tech/libeufin/sandbox/hex.kt | 7+++++++
Msandbox/src/main/kotlin/tech/libeufin/schema/ebics_h004/EbicsResponse.kt | 11+++--------
Msandbox/src/main/kotlin/tech/libeufin/schema/ebics_h004/EbicsTypes.kt | 5++---
Msandbox/src/test/kotlin/XmlUtilTest.kt | 16++++++++++++++++
11 files changed, 492 insertions(+), 41 deletions(-)

diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="Palette2"> + <group name="Swing"> + <item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.png" removable="false" auto-create-binding="false" can-attach-label="false"> + <default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" /> + </item> + <item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.png" removable="false" auto-create-binding="false" can-attach-label="false"> + <default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" /> + </item> + <item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.png" removable="false" auto-create-binding="false" can-attach-label="false"> + <default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" /> + </item> + <item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.png" removable="false" auto-create-binding="false" can-attach-label="true"> + <default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" /> + </item> + <item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.png" removable="false" auto-create-binding="true" can-attach-label="false"> + <default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" /> + <initial-values> + <property name="text" value="Button" /> + </initial-values> + </item> + <item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.png" removable="false" auto-create-binding="true" can-attach-label="false"> + <default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" /> + <initial-values> + <property name="text" value="RadioButton" /> + </initial-values> + </item> + <item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.png" removable="false" auto-create-binding="true" can-attach-label="false"> + <default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" /> + <initial-values> + <property name="text" value="CheckBox" /> + </initial-values> + </item> + <item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.png" removable="false" auto-create-binding="false" can-attach-label="false"> + <default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" /> + <initial-values> + <property name="text" value="Label" /> + </initial-values> + </item> + <item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.png" removable="false" auto-create-binding="true" can-attach-label="true"> + <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1"> + <preferred-size width="150" height="-1" /> + </default-constraints> + </item> + <item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.png" removable="false" auto-create-binding="true" can-attach-label="true"> + <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1"> + <preferred-size width="150" height="-1" /> + </default-constraints> + </item> + <item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.png" removable="false" auto-create-binding="true" can-attach-label="true"> + <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1"> + <preferred-size width="150" height="-1" /> + </default-constraints> + </item> + <item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.png" removable="false" auto-create-binding="true" can-attach-label="true"> + <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3"> + <preferred-size width="150" height="50" /> + </default-constraints> + </item> + <item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.png" removable="false" auto-create-binding="true" can-attach-label="true"> + <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3"> + <preferred-size width="150" height="50" /> + </default-constraints> + </item> + <item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.png" removable="false" auto-create-binding="true" can-attach-label="true"> + <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3"> + <preferred-size width="150" height="50" /> + </default-constraints> + </item> + <item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.png" removable="false" auto-create-binding="true" can-attach-label="true"> + <default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" /> + </item> + <item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.png" removable="false" auto-create-binding="true" can-attach-label="false"> + <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3"> + <preferred-size width="150" height="50" /> + </default-constraints> + </item> + <item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.png" removable="false" auto-create-binding="true" can-attach-label="false"> + <default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3"> + <preferred-size width="150" height="50" /> + </default-constraints> + </item> + <item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.png" removable="false" auto-create-binding="true" can-attach-label="false"> + <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3"> + <preferred-size width="150" height="50" /> + </default-constraints> + </item> + <item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.png" removable="false" auto-create-binding="true" can-attach-label="false"> + <default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3"> + <preferred-size width="200" height="200" /> + </default-constraints> + </item> + <item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.png" removable="false" auto-create-binding="false" can-attach-label="false"> + <default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3"> + <preferred-size width="200" height="200" /> + </default-constraints> + </item> + <item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.png" removable="false" auto-create-binding="true" can-attach-label="true"> + <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" /> + </item> + <item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.png" removable="false" auto-create-binding="true" can-attach-label="false"> + <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" /> + </item> + <item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.png" removable="false" auto-create-binding="false" can-attach-label="false"> + <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" /> + </item> + <item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.png" removable="false" auto-create-binding="true" can-attach-label="false"> + <default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" /> + </item> + <item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.png" removable="false" auto-create-binding="false" can-attach-label="false"> + <default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1"> + <preferred-size width="-1" height="20" /> + </default-constraints> + </item> + <item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.png" removable="false" auto-create-binding="false" can-attach-label="false"> + <default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" /> + </item> + <item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.png" removable="false" auto-create-binding="true" can-attach-label="false"> + <default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" /> + </item> + </group> + </component> +</project> +\ No newline at end of file diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/CryptoUtil.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/CryptoUtil.kt @@ -164,11 +164,5 @@ class CryptoUtil { val data = symmetricCipher.doFinal(enc.encryptedData) return data } - - fun ByteArray.toHexString() : String { - return this.joinToString("") { - java.lang.String.format("%02x", it) - } - } } } diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt @@ -154,6 +154,8 @@ object EbicsSubscribersTable : IntIdTable() { val encryptionKey = reference("encryptionKey", EbicsSubscriberPublicKeysTable).nullable() val authenticationKey = reference("authorizationKey", EbicsSubscriberPublicKeysTable).nullable() + val nextOrderID = integer("nextOrderID") + val state = enumeration("state", SubscriberState::class) } @@ -168,6 +170,8 @@ class EbicsSubscriberEntity(id: EntityID<Int>) : IntEntity(id) { var encryptionKey by EbicsSubscriberPublicKeyEntity optionalReferencedOn EbicsSubscribersTable.encryptionKey var authenticationKey by EbicsSubscriberPublicKeyEntity optionalReferencedOn EbicsSubscribersTable.authenticationKey + var nextOrderID by EbicsSubscribersTable.nextOrderID + var state by EbicsSubscribersTable.state } @@ -177,8 +181,8 @@ object EbicsDownloadTransactionsTable : IdTable<String>() { val orderType = text("orderType") val host = reference("host", EbicsHostsTable) val subscriber = reference("subscriber", EbicsSubscribersTable) - val encodedResponse = blob("encodedResponse") - val orderID = text("orderID") + val encodedResponse = text("encodedResponse") + val transactionKeyEnc = blob("transactionKeyEnc") val numSegments = integer("numSegments") val segmentSize = integer("segmentSize") val receiptReceived = bool("receiptReceived") @@ -189,11 +193,11 @@ class EbicsDownloadTransactionEntity(id: EntityID<String>) : Entity<String>(id) companion object : EntityClass<String, EbicsDownloadTransactionEntity>(EbicsDownloadTransactionsTable) var orderType by EbicsDownloadTransactionsTable.orderType - var host by EbicsDownloadTransactionsTable.host - var subscriber by EbicsDownloadTransactionsTable.host + var host by EbicsHostEntity referencedOn EbicsDownloadTransactionsTable.host + var subscriber by EbicsSubscriberEntity referencedOn EbicsDownloadTransactionsTable.subscriber var encodedResponse by EbicsDownloadTransactionsTable.encodedResponse - var orderID by EbicsDownloadTransactionsTable.orderID var numSegments by EbicsDownloadTransactionsTable.numSegments + var transactionKeyEnc by EbicsDownloadTransactionsTable.transactionKeyEnc var segmentSize by EbicsDownloadTransactionsTable.segmentSize var receiptReceived by EbicsDownloadTransactionsTable.receiptReceived } @@ -208,7 +212,8 @@ fun dbCreateTables() { SchemaUtils.create( BankCustomersTable, EbicsSubscribersTable, - EbicsHostsTable + EbicsHostsTable, + EbicsDownloadTransactionsTable ) } } diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsOrderUtil.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsOrderUtil.kt @@ -19,6 +19,7 @@ package tech.libeufin.sandbox +import java.security.SecureRandom import java.util.zip.DeflaterInputStream import java.util.zip.InflaterInputStream @@ -40,5 +41,21 @@ class EbicsOrderUtil private constructor() { it.readAllBytes() } } + + fun generateTransactionId(): String { + val rng = SecureRandom() + val res = ByteArray(16) + rng.nextBytes(res) + return res.toHexString() + } + + /** + * Calculate the resulting size of base64-encoding data of the given length, + * including padding. + */ + fun calculateBase64EncodedLength(dataLength: Int): Int { + val blocks = (dataLength + 3 - 1) / 3 + return blocks * 4 + } } } diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt @@ -40,6 +40,7 @@ import io.ktor.routing.routing import io.ktor.server.engine.embeddedServer import io.ktor.server.netty.Netty import org.apache.xml.security.binding.xmldsig.RSAKeyValueType +import org.apache.xml.security.binding.xmldsig.SignatureType import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.transactions.transaction import org.slf4j.Logger @@ -49,20 +50,28 @@ import tech.libeufin.schema.ebics_h004.* import tech.libeufin.schema.ebics_hev.HEVResponse import tech.libeufin.schema.ebics_hev.SystemReturnCodeType import tech.libeufin.schema.ebics_s001.SignaturePubKeyOrderData +import java.math.BigInteger import java.security.interfaces.RSAPublicKey import java.text.DateFormat +import java.util.* +import java.util.zip.DeflaterInputStream import javax.sql.rowset.serial.SerialBlob import javax.xml.bind.JAXBContext val logger: Logger = LoggerFactory.getLogger("tech.libeufin.sandbox") -data class EbicsRequestError(val statusCode: HttpStatusCode) : Exception("Ebics request error") +open class EbicsRequestError(val errorText: String, val errorCode: String) : + Exception("EBICS request management error: $errorText ($errorCode)") + +class EbicsInvalidRequestError : EbicsRequestError("[EBICS_INVALID_REQUEST] Invalid request", "060102") open class EbicsKeyManagementError(val errorText: String, val errorCode: String) : Exception("EBICS key management error: $errorText ($errorCode)") class EbicsInvalidXmlError : EbicsKeyManagementError("[EBICS_INVALID_XML]", "091010") +class EbicsInvalidOrderType : EbicsRequestError("[EBICS_UNSUPPORTED_ORDER_TYPE] Order type not supported", "091005") + private suspend fun ApplicationCall.respondEbicsKeyManagement( errorText: String, errorCode: String, @@ -158,7 +167,7 @@ private suspend fun ApplicationCall.handleEbicsHia(header: EbicsUnsecuredRequest val ebicsSubscriber = findEbicsSubscriber(header.static.partnerID, header.static.userID, header.static.systemID) if (ebicsSubscriber == null) { logger.warn("ebics subscriber not found") - throw EbicsRequestError(HttpStatusCode.NotFound) + throw EbicsInvalidRequestError() } ebicsSubscriber.authenticationKey = EbicsSubscriberPublicKeyEntity.new { this.rsaPublicKey = SerialBlob(authPub.encoded) @@ -188,7 +197,7 @@ private suspend fun ApplicationCall.handleEbicsIni(header: EbicsUnsecuredRequest findEbicsSubscriber(header.static.partnerID, header.static.userID, header.static.systemID) if (ebicsSubscriber == null) { logger.warn("ebics subscriber ('${header.static.partnerID}' / '${header.static.userID}' / '${header.static.systemID}') not found") - throw EbicsRequestError(HttpStatusCode.NotFound) + throw EbicsInvalidRequestError() } ebicsSubscriber.signatureKey = EbicsSubscriberPublicKeyEntity.new { this.rsaPublicKey = SerialBlob(sigPub.encoded) @@ -213,10 +222,10 @@ private suspend fun ApplicationCall.handleEbicsHpb( val ebicsSubscriber = findEbicsSubscriber(header.static.partnerID, header.static.userID, header.static.systemID) if (ebicsSubscriber == null) { - throw EbicsRequestError(HttpStatusCode.Unauthorized) + throw EbicsInvalidRequestError() } if (ebicsSubscriber.state != SubscriberState.INITIALIZED) { - throw EbicsRequestError(HttpStatusCode.Forbidden) + throw EbicsInvalidRequestError() } val authPubBlob = ebicsSubscriber.authenticationKey!!.rsaPublicKey val encPubBlob = ebicsSubscriber.encryptionKey!!.rsaPublicKey @@ -301,6 +310,176 @@ inline fun <reified T> Document.toObject(): T { } +fun handleEbicsHtd(): ByteArray { + val htd = HTDResponseOrderData().apply { + this.partnerInfo = HTDResponseOrderData.PartnerInfo().apply { + this.accountInfoList = listOf( + HTDResponseOrderData.AccountInfo().apply { + this.id = "acctid1" + this.accountHolder = "Mina Musterfrau" + this.accountNumberList = listOf( + HTDResponseOrderData.GeneralAccountNumber().apply { + this.international = true + this.value = "DE21500105174751659277" + } + ) + this.currency = "EUR" + this.description = "ACCT" + this.bankCodeList = listOf( + HTDResponseOrderData.GeneralBankCode().apply { + this.international = true + this.value = "INGDDEFFXXX" + } + ) + } + ) + this.addressInfo = HTDResponseOrderData.AddressInfo().apply { + this.name = "Foo" + } + this.bankInfo = HTDResponseOrderData.BankInfo().apply { + this.hostID = "host01" + } + this.orderInfoList = listOf( + HTDResponseOrderData.AuthOrderInfoType().apply { + this.description = "foo" + this.orderType = "C53" + this.transferType = "Download" + }, + HTDResponseOrderData.AuthOrderInfoType().apply { + this.description = "foo" + this.orderType = "C52" + this.transferType = "Download" + }, + HTDResponseOrderData.AuthOrderInfoType().apply { + this.description = "foo" + this.orderType = "CCC" + this.transferType = "Upload" + } + ) + } + this.userInfo = HTDResponseOrderData.UserInfo().apply { + this.name = "Some User" + this.userID = HTDResponseOrderData.UserIDType().apply { + this.status = 5 + this.value = "USER1" + } + this.permissionList = listOf( + HTDResponseOrderData.UserPermission().apply { + this.orderTypes = "C54 C53 C52 CCC" + } + ) + } + } + + val str = XMLUtil.convertJaxbToString(htd) + return str.toByteArray() +} + + +fun createEbicsResponseForDownloadInitializationPhase( + transactionID: String, + numSegments: Int, + segmentSize: Int, + enc: CryptoUtil.EncryptionResult, + encodedData: String +): EbicsResponse { + return EbicsResponse().apply { + this.version = "H004" + this.revision = 1 + this.header = EbicsResponse.Header().apply { + this.authenticate = true + this._static = EbicsResponse.StaticHeaderType().apply { + this.transactionID = transactionID + this.numSegments = BigInteger.valueOf(numSegments.toLong()) + } + this.mutable = EbicsResponse.MutableHeaderType().apply { + this.transactionPhase = EbicsTypes.TransactionPhaseType.INITIALISATION + this.segmentNumber = EbicsResponse.SegmentNumber().apply { + this.lastSegment = (numSegments == 1) + this.value = BigInteger.valueOf(1) + } + this.reportText = "[EBICS_OK] OK" + this.returnCode = "000000" + } + } + this.authSignature = SignatureType() + this.body = EbicsResponse.Body().apply { + this.returnCode = EbicsResponse.ReturnCode().apply { + this.authenticate = true + this.value = "000000" + } + this.dataTransfer = EbicsResponse.DataTransferResponseType().apply { + this.dataEncryptionInfo = EbicsTypes.DataEncryptionInfo().apply { + this.authenticate = true + this.encryptionPubKeyDigest = EbicsTypes.PubKeyDigest().apply { + this.algorithm = "http://www.w3.org/2001/04/xmlenc#sha256" + this.version = "E002" + this.value = enc.pubKeyDigest + } + this.transactionKey = enc.encryptedTransactionKey + } + this.orderData = EbicsResponse.OrderData().apply { + this.value = encodedData.substring(0, Math.min(segmentSize, encodedData.length)) + } + } + } + } +} + + +fun createEbicsResponseForDownloadTransferPhase() { + +} + + +fun createEbicsResponseForDownloadReceiptPhase(transactionID: String, positiveAck: Boolean): EbicsResponse { + return EbicsResponse().apply { + this.version = "H004" + this.revision = 1 + this.header = EbicsResponse.Header().apply { + this.authenticate = true + this._static = EbicsResponse.StaticHeaderType().apply { + this.transactionID = transactionID + } + this.mutable = EbicsResponse.MutableHeaderType().apply { + this.transactionPhase = EbicsTypes.TransactionPhaseType.RECEIPT + if (positiveAck) { + this.reportText = "[EBICS_DOWNLOAD_POSTPROCESS_DONE] Received positive receipt" + this.returnCode = "011000" + } else { + this.reportText = "[EBICS_DOWNLOAD_POSTPROCESS_DONE] Received negative receipt" + this.returnCode = "011001" + } + } + } + this.authSignature = SignatureType() + this.body = EbicsResponse.Body().apply { + this.returnCode = EbicsResponse.ReturnCode().apply { + this.authenticate = true + this.value = "000000" + } + } + } +} + + +private suspend fun ApplicationCall.handleEbicsDownloadInitialization() { + +} + +private suspend fun ApplicationCall.handleEbicsDownloadTransfer() { + +} + +private suspend fun ApplicationCall.handleEbicsDownloadReceipt() { + +} + +private suspend fun ApplicationCall.handleEbicsUploadInitialization() { + +} + + private suspend fun ApplicationCall.ebicsweb() { val requestDocument = receiveEbicsXml() @@ -344,24 +523,111 @@ private suspend fun ApplicationCall.ebicsweb() { println("ebicsRequest ${XMLUtil.convertDomToString(requestDocument)}") val requestObject = requestDocument.toObject<EbicsRequest>() val staticHeader = requestObject.header.static + when (requestObject.header.mutable.transactionPhase) { EbicsTypes.TransactionPhaseType.INITIALISATION -> { val partnerID = staticHeader.partnerID ?: throw EbicsInvalidXmlError() val userID = staticHeader.userID ?: throw EbicsInvalidXmlError() - transaction { + val respText = transaction { val subscriber = findEbicsSubscriber(partnerID, userID, staticHeader.systemID) ?: throw EbicsInvalidXmlError() - val authPub = + val requestedHostId = requestObject.header.static.hostID + val ebicsHost = EbicsHostEntity.find { EbicsHostsTable.hostID eq requestedHostId }.firstOrNull() + if (ebicsHost == null) + throw EbicsInvalidRequestError() + val hostAuthPriv = CryptoUtil.loadRsaPrivateKey( + ebicsHost.authenticationPrivateKey + .toByteArray() + ) + val clientAuthPub = CryptoUtil.loadRsaPublicKey(subscriber.authenticationKey!!.rsaPublicKey.toByteArray()) - val verifyResult = XMLUtil.verifyEbicsDocument(requestDocument, authPub) + val clientEncPub = + CryptoUtil.loadRsaPublicKey(subscriber.encryptionKey!!.rsaPublicKey.toByteArray()) + val verifyResult = XMLUtil.verifyEbicsDocument(requestDocument, clientAuthPub) println("ebicsRequest verification result: $verifyResult") + val transactionID = EbicsOrderUtil.generateTransactionId() + val orderType = requestObject.header.static.orderDetails?.orderType + + val response = when (orderType) { + "HTD" -> handleEbicsHtd() + else -> throw EbicsInvalidXmlError() + } + + val compressedResponse = DeflaterInputStream(response.inputStream()).use { + it.readAllBytes() + } + + val enc = CryptoUtil.encryptEbicsE002(compressedResponse, clientEncPub) + val encodedResponse = Base64.getEncoder().encodeToString(enc.encryptedData) + + val segmentSize = 4096 + val totalSize = encodedResponse.length + val numSegments = ((totalSize + segmentSize - 1) / segmentSize) + + println("inner response: " + response.toString(Charsets.UTF_8)) + + println("total size: $totalSize") + println("num segments: $numSegments") + + EbicsDownloadTransactionEntity.new(transactionID) { + this.subscriber = subscriber + this.host = ebicsHost + this.orderType = orderType + this.segmentSize = segmentSize + this.transactionKeyEnc = SerialBlob(enc.encryptedTransactionKey) + this.encodedResponse = encodedResponse + this.numSegments = numSegments + this.receiptReceived = false + } + + val ebicsResponse = createEbicsResponseForDownloadInitializationPhase( + transactionID, + numSegments, segmentSize, enc, encodedResponse + ) + val docText = XMLUtil.convertJaxbToString(ebicsResponse) + val doc = XMLUtil.parseStringIntoDom(docText) + XMLUtil.signEbicsDocument(doc, hostAuthPriv) + val signedDoc = XMLUtil.convertDomToString(doc) + println("response: $signedDoc") + docText } + respondText(respText, ContentType.Application.Xml, HttpStatusCode.OK) + return } EbicsTypes.TransactionPhaseType.TRANSFER -> { } EbicsTypes.TransactionPhaseType.RECEIPT -> { + val respText = transaction { + val requestedHostId = requestObject.header.static.hostID + val ebicsHost = EbicsHostEntity.find { EbicsHostsTable.hostID eq requestedHostId }.firstOrNull() + if (ebicsHost == null) + throw EbicsInvalidRequestError() + val hostAuthPriv = CryptoUtil.loadRsaPrivateKey( + ebicsHost.authenticationPrivateKey + .toByteArray() + ) + val transactionID = requestObject.header.static.transactionID + if (transactionID == null) + throw EbicsInvalidRequestError() + val downloadTransaction = EbicsDownloadTransactionEntity.findById(transactionID) + if (downloadTransaction == null) + throw EbicsInvalidRequestError() + println("sending receipt for transaction ID $transactionID") + val receiptCode = requestObject.body.transferReceipt?.receiptCode + if (receiptCode == null) + throw EbicsInvalidRequestError() + val ebicsResponse = createEbicsResponseForDownloadReceiptPhase(transactionID, receiptCode == 0) + val docText = XMLUtil.convertJaxbToString(ebicsResponse) + val doc = XMLUtil.parseStringIntoDom(docText) + XMLUtil.signEbicsDocument(doc, hostAuthPriv) + val signedDoc = XMLUtil.convertDomToString(doc) + println("response: $signedDoc") + docText + } + respondText(respText, ContentType.Application.Xml, HttpStatusCode.OK) + return } } @@ -397,6 +663,7 @@ fun main() { userId = "USER1" systemId = null state = SubscriberState.NEW + nextOrderID = 1 } } @@ -414,6 +681,7 @@ fun main() { call.respondText("Internal server error.", ContentType.Text.Plain, HttpStatusCode.InternalServerError) } } + // TODO: add another intercept call that adds schema validation before the response is sent intercept(ApplicationCallPipeline.Fallback) { if (this.call.response.status() == null) { call.respondText("Not found (no route matched).\n", ContentType.Text.Plain, HttpStatusCode.NotFound) diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/XMLUtil.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/XMLUtil.kt @@ -74,24 +74,20 @@ class XMLUtil private constructor() { if (myRef.uri != "#xpointer($ebicsXpathExpr)") throw Exception("invalid EBICS XML signature URI: '${myRef.uri}'") val xp: XPath = XPathFactory.newInstance().newXPath() - val nodeSet = xp.compile(ebicsXpathExpr).evaluate(myRef.here.ownerDocument, XPathConstants.NODESET) + val nodeSet = xp.compile("//*[@authenticate='true']/descendant-or-self::node()").evaluate(myRef.here + .ownerDocument, XPathConstants + .NODESET) if (nodeSet !is NodeList) throw Exception("invalid type") if (nodeSet.length <= 0) { throw Exception("no nodes to sign") } - val bytes = ByteArrayOutputStream() + val nodeList = ArrayList<Node>() for (i in 0 until nodeSet.length) { val node = nodeSet.item(i) - org.apache.xml.security.Init.init() - // Despite the transform later, this canonicalization step is absolutely necessary, - // as the canonicalizeSubtree method preserves namespaces that are not in the subtree - // being canonicalized, but in the parent hierarchy of the document. - val canon: Canonicalizer = Canonicalizer.getInstance(Canonicalizer.ALGO_ID_C14N11_OMIT_COMMENTS) - val cxml = canon.canonicalizeSubtree(node) - bytes.writeBytes(cxml) + nodeList.add(node) } - return OctetStreamData(ByteArrayInputStream(bytes.toByteArray())) + return NodeSetData { nodeList.iterator() } } } @@ -336,8 +332,12 @@ class XMLUtil private constructor() { dsc.defaultNamespacePrefix = "ds" dsc.uriDereferencer = EbicsSigUriDereferencer() + dsc.setProperty("javax.xml.crypto.dsig.cacheReference", true) + sig.sign(dsc) + println("canon data: " + sig.signedInfo.canonicalizedData.readAllBytes().toString(Charsets.UTF_8)) + val innerSig = authSigNode.firstChild while (innerSig.hasChildNodes()) { authSigNode.appendChild(innerSig.firstChild) @@ -375,6 +375,7 @@ class XMLUtil private constructor() { authSigNode.parentNode.removeChild(authSigNode) val fac = XMLSignatureFactory.getInstance("DOM") val dvc = DOMValidateContext(signingPub, sigEl) + dvc.setProperty("javax.xml.crypto.dsig.cacheReference", true) dvc.uriDereferencer = EbicsSigUriDereferencer() val sig = fac.unmarshalXMLSignature(dvc) // FIXME: check that parameters are okay!s diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/XmlBinding.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/XmlBinding.kt @@ -0,0 +1,23 @@ +package tech.libeufin.sandbox + +@Retention(AnnotationRetention.RUNTIME) +annotation class XmlSchemaContext + +@Retention(AnnotationRetention.RUNTIME) +annotation class XmlElement + +@Retention(AnnotationRetention.RUNTIME) +annotation class XmlAttribute + +@Retention(AnnotationRetention.RUNTIME) +annotation class XmlValue + +@Retention(AnnotationRetention.RUNTIME) +annotation class XmlWrapper + +@Retention(AnnotationRetention.RUNTIME) +annotation class XmlAdapter + +class XmlBinding<T> { + +} +\ No newline at end of file diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/hex.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/hex.kt @@ -0,0 +1,7 @@ +package tech.libeufin.sandbox + +fun ByteArray.toHexString() : String { + return this.joinToString("") { + java.lang.String.format("%02x", it) + } +} diff --git a/sandbox/src/main/kotlin/tech/libeufin/schema/ebics_h004/EbicsResponse.kt b/sandbox/src/main/kotlin/tech/libeufin/schema/ebics_h004/EbicsResponse.kt @@ -29,9 +29,6 @@ class EbicsResponse { @get:XmlElement(required = true) lateinit var body: Body - @get:XmlAnyAttribute - var otherAttributes = HashMap<QName, String>() - @XmlAccessorType(XmlAccessType.NONE) @XmlType(name = "", propOrder = ["_static", "mutable"]) class Header { @@ -101,7 +98,7 @@ class EbicsResponse { @XmlAccessorType(XmlAccessType.NONE) class OrderData { @get:XmlValue - lateinit var value: ByteArray + lateinit var value: String } @XmlAccessorType(XmlAccessType.NONE) @@ -128,10 +125,8 @@ class EbicsResponse { @XmlAccessorType(XmlAccessType.NONE) @XmlType(name = "ResponseStaticHeaderType", propOrder = ["transactionID", "numSegments"]) class StaticHeaderType { - @get:XmlElement(name = "TransactionID", type = String::class) - @get:XmlJavaTypeAdapter(HexBinaryAdapter::class) - @get:XmlSchemaType(name = "hexBinary") - var transactionID: ByteArray? = null + @get:XmlElement(name = "TransactionID") + var transactionID: String? = null @get:XmlElement(name = "NumSegments") @get:XmlSchemaType(name = "positiveInteger") diff --git a/sandbox/src/main/kotlin/tech/libeufin/schema/ebics_h004/EbicsTypes.kt b/sandbox/src/main/kotlin/tech/libeufin/schema/ebics_h004/EbicsTypes.kt @@ -52,6 +52,7 @@ class EbicsTypes private constructor() { } + @XmlType(name = "", propOrder = ["encryptionPubKeyDigest", "transactionKey"]) @XmlAccessorType(XmlAccessType.NONE) class DataEncryptionInfo { @get:XmlAttribute(name = "authenticate", required = true) @@ -62,12 +63,10 @@ class EbicsTypes private constructor() { @get:XmlElement(name = "TransactionKey", required = true) lateinit var transactionKey: ByteArray - - @get:XmlAnyElement(lax = true) - var any: List<Any>? = null } @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["value"]) class PubKeyDigest { /** * Version of the *digest* of the public key. diff --git a/sandbox/src/test/kotlin/XmlUtilTest.kt b/sandbox/src/test/kotlin/XmlUtilTest.kt @@ -57,6 +57,22 @@ class XmlUtilTest { } @Test + fun multiAuthSigningTest() { + val doc = XMLUtil.parseStringIntoDom(""" + <myMessage xmlns:ebics="urn:org:ebics:H004"> + <ebics:AuthSignature /> + <foo authenticate="true">Hello World</foo> + <bar authenticate="true">Another one!</bar> + </myMessage> + """.trimIndent()) + val kpg = KeyPairGenerator.getInstance("RSA") + kpg.initialize(2048) + val pair = kpg.genKeyPair() + XMLUtil.signEbicsDocument(doc, pair.private) + kotlin.test.assertTrue(XMLUtil.verifyEbicsDocument(doc, pair.public)) + } + + @Test fun testRefSignature() { val classLoader = ClassLoader.getSystemClassLoader() val docText = classLoader.getResourceAsStream("signature1/doc.xml")!!.readAllBytes().toString(Charsets.UTF_8)