libeufin

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

commit 0a5fec853dac05f66dce1ea857ff1579fe0a935e
parent 367d56d01f4b88b7642c026f1110ba774569d0df
Author: Marcello Stanisci <stanisci.m@gmail.com>
Date:   Thu, 28 Nov 2019 18:35:34 +0100

Sending C52 to the bank.

Diffstat:
Anexus/src/main/kotlin/Helpers.kt | 300+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mnexus/src/main/kotlin/JSON.kt | 6++++++
Mnexus/src/main/kotlin/Main.kt | 267++++++++++++-------------------------------------------------------------------
Msandbox/src/main/kotlin/tech/libeufin/schema/ebics_h004/EbicsRequest.kt | 35+++++++++++++++++++++++++++++++++++
4 files changed, 380 insertions(+), 228 deletions(-)

diff --git a/nexus/src/main/kotlin/Helpers.kt b/nexus/src/main/kotlin/Helpers.kt @@ -0,0 +1,300 @@ +package tech.libeufin.nexus + +import io.ktor.client.HttpClient +import io.ktor.client.request.post +import io.ktor.http.HttpStatusCode +import tech.libeufin.sandbox.CryptoUtil +import tech.libeufin.sandbox.XMLUtil +import tech.libeufin.sandbox.logger +import tech.libeufin.sandbox.toByteArray +import tech.libeufin.schema.ebics_h004.EbicsRequest +import tech.libeufin.schema.ebics_s001.UserSignatureData +import java.math.BigInteger +import java.security.PrivateKey +import java.security.SecureRandom +import java.security.interfaces.RSAPrivateCrtKey +import java.security.interfaces.RSAPublicKey +import java.util.* +import javax.xml.bind.JAXBElement +import javax.xml.datatype.DatatypeFactory +import javax.xml.datatype.XMLGregorianCalendar + +fun createDownloadInitializationPhase( + subscriberData: EbicsContainer, + orderType: String, + nonce: ByteArray, + date: XMLGregorianCalendar +): EbicsRequest { + + return EbicsRequest.createForDownloadInitializationPhase( + subscriberData.userId, + subscriberData.partnerId, + subscriberData.hostId, + nonce, + date, + subscriberData.bankEncPub ?: throw BankKeyMissing(HttpStatusCode.PreconditionFailed), + subscriberData.bankAuthPub ?: throw BankKeyMissing(HttpStatusCode.PreconditionFailed), + orderType + ) +} + + +fun createDownloadInitializationPhase( + subscriberData: EbicsContainer, + orderType: String, + nonce: ByteArray, + date: XMLGregorianCalendar, + dateStart: XMLGregorianCalendar, + dateEnd: XMLGregorianCalendar +): EbicsRequest { + + return EbicsRequest.createForDownloadInitializationPhase( + subscriberData.userId, + subscriberData.partnerId, + subscriberData.hostId, + nonce, + date, + subscriberData.bankEncPub ?: throw BankKeyMissing(HttpStatusCode.PreconditionFailed), + subscriberData.bankAuthPub ?: throw BankKeyMissing(HttpStatusCode.PreconditionFailed), + orderType, + dateStart, + dateEnd + ) +} + + +fun createUploadInitializationPhase( + subscriberData: EbicsContainer, + orderType: String, + cryptoBundle: CryptoUtil.EncryptionResult, + nonce: ByteArray, + date: XMLGregorianCalendar +): EbicsRequest { + + return EbicsRequest.createForUploadInitializationPhase( + cryptoBundle, + subscriberData.hostId, + getNonce(128), + subscriberData.partnerId, + subscriberData.userId, + getGregorianDate(), + subscriberData.bankAuthPub!!, + subscriberData.bankEncPub!!, + BigInteger.ONE, + orderType + ) +} + +fun containerInit(subscriber: EbicsSubscriberEntity): EbicsContainer { + + var bankAuthPubValue: RSAPublicKey? = null + if (subscriber.bankAuthenticationPublicKey != null) { + bankAuthPubValue = CryptoUtil.loadRsaPublicKey( + subscriber.bankAuthenticationPublicKey?.toByteArray()!! + ) + } + var bankEncPubValue: RSAPublicKey? = null + if (subscriber.bankEncryptionPublicKey != null) { + bankEncPubValue = CryptoUtil.loadRsaPublicKey( + subscriber.bankEncryptionPublicKey?.toByteArray()!! + ) + } + + return EbicsContainer( + bankAuthPub = bankAuthPubValue, + bankEncPub = bankEncPubValue, + + ebicsUrl = subscriber.ebicsURL, + hostId = subscriber.hostID, + userId = subscriber.userID, + partnerId = subscriber.partnerID, + + customerSignPriv = CryptoUtil.loadRsaPrivateKey(subscriber.signaturePrivateKey.toByteArray()), + customerAuthPriv = CryptoUtil.loadRsaPrivateKey(subscriber.authenticationPrivateKey.toByteArray()), + customerEncPriv = CryptoUtil.loadRsaPrivateKey(subscriber.authenticationPrivateKey.toByteArray()) + ) + +} + +/** + * Inserts spaces every 2 characters, and a newline after 8 pairs. + */ +fun chunkString(input: String): String { + + val ret = StringBuilder() + var columns = 0 + + for (i in input.indices) { + + if ((i + 1).rem(2) == 0) { + + if (columns == 7) { + ret.append(input[i] + "\n") + columns = 0 + continue + } + + ret.append(input[i] + " ") + columns++ + continue + } + ret.append(input[i]) + } + + return ret.toString() + +} + +fun expectId(param: String?): Int { + + try { + return param!!.toInt() + } catch (e: Exception) { + throw NotAnIdError(HttpStatusCode.BadRequest) + } +} + +fun signOrder( + orderBlob: ByteArray, + signKey: RSAPrivateCrtKey, + partnerId: String, + userId: String +): UserSignatureData { + + val ES_signature = CryptoUtil.signEbicsA006( + CryptoUtil.digestEbicsOrderA006(orderBlob), + signKey + ) + val userSignatureData = UserSignatureData().apply { + orderSignatureList = listOf( + UserSignatureData.OrderSignatureData().apply { + signatureVersion = "A006" + signatureValue = ES_signature + partnerID = partnerId + userID = userId + } + ) + } + + return userSignatureData +} + + +/** + * @return null when the bank could not be reached, otherwise returns the + * response already converted in JAXB. + */ +suspend inline fun HttpClient.postToBank(url: String, body: String): String { + + val response = try { + this.post<String>( + urlString = url, + block = { + this.body = body + } + ) + } catch (e: Exception) { + throw UnreachableBankError(HttpStatusCode.InternalServerError) + } + + return response +} + +/** + * DO verify the bank's signature + */ +suspend inline fun <reified T, reified S> HttpClient.postToBankSignedAndVerify( + url: String, + body: T, + pub: RSAPublicKey, + priv: RSAPrivateCrtKey +): JAXBElement<S> { + + val doc = XMLUtil.convertJaxbToDocument(body) + XMLUtil.signEbicsDocument(doc, priv) + + val response: String = this.postToBank(url, XMLUtil.convertDomToString(doc)) + logger.debug("About to verify: ${response}") + + val responseString = try { + + XMLUtil.parseStringIntoDom(response) + } catch (e: Exception) { + + throw UnparsableResponse(HttpStatusCode.BadRequest, response) + } + + if (!XMLUtil.verifyEbicsDocument(responseString, pub)) { + + throw BadSignature(HttpStatusCode.NotAcceptable) + } + + try { + + return XMLUtil.convertStringToJaxb(response) + } catch (e: Exception) { + + throw UnparsableResponse(HttpStatusCode.BadRequest, response) + } +} + +suspend inline fun <reified T, reified S> HttpClient.postToBankSigned( + url: String, + body: T, + priv: PrivateKey +): JAXBElement<S> { + + val doc = XMLUtil.convertJaxbToDocument(body) + XMLUtil.signEbicsDocument(doc, priv) + + val response: String = this.postToBank(url, XMLUtil.convertDomToString(doc)) + + try { + return XMLUtil.convertStringToJaxb(response) + } catch (e: Exception) { + throw UnparsableResponse(HttpStatusCode.BadRequest, response) + } +} + + + +/** + * do NOT verify the bank's signature + */ +suspend inline fun <reified T, reified S> HttpClient.postToBankUnsigned( + url: String, + body: T +): JAXBElement<S> { + + val response: String = this.postToBank(url, XMLUtil.convertJaxbToString(body)) + + try { + return XMLUtil.convertStringToJaxb(response) + } catch (e: Exception) { + throw UnparsableResponse(HttpStatusCode.BadRequest, response) + } +} + +/** + * @param size in bits + */ +fun getNonce(size: Int): ByteArray { + val sr = SecureRandom() + val ret = ByteArray(size / 8) + sr.nextBytes(ret) + return ret +} + +/* explicit point in time */ +fun getGregorianDate(year: Int, month: Int, day: Int): XMLGregorianCalendar { + val gregorianCalendar = GregorianCalendar(year, month, day) + val datatypeFactory = DatatypeFactory.newInstance() + return datatypeFactory.newXMLGregorianCalendar(gregorianCalendar) +} + +/* now */ +fun getGregorianDate(): XMLGregorianCalendar { + val gregorianCalendar = GregorianCalendar() + val datatypeFactory = DatatypeFactory.newInstance() + return datatypeFactory.newXMLGregorianCalendar(gregorianCalendar) +} diff --git a/nexus/src/main/kotlin/JSON.kt b/nexus/src/main/kotlin/JSON.kt @@ -9,6 +9,12 @@ data class EbicsBackupRequest( val passphrase: String ) + +data class EbicsDateRange( + val start: String, // ISO 8601 calendar date + val end: String // ISO 8601 calendar date +) + /** * This object is used twice: as a response to the backup request, * and as a request to the backup restore. Note: in the second case diff --git a/nexus/src/main/kotlin/Main.kt b/nexus/src/main/kotlin/Main.kt @@ -60,6 +60,7 @@ import javax.crypto.EncryptedPrivateKeyInfo import javax.xml.datatype.DatatypeFactory import javax.xml.datatype.XMLGregorianCalendar import java.security.interfaces.RSAPublicKey +import java.time.LocalDate fun testData() { @@ -82,212 +83,6 @@ fun testData() { } } -fun containerInit(subscriber: EbicsSubscriberEntity): EbicsContainer { - - var bankAuthPubValue: RSAPublicKey? = null - if (subscriber.bankAuthenticationPublicKey != null) { - bankAuthPubValue = CryptoUtil.loadRsaPublicKey( - subscriber.bankAuthenticationPublicKey?.toByteArray()!! - ) - } - var bankEncPubValue: RSAPublicKey? = null - if (subscriber.bankEncryptionPublicKey != null) { - bankEncPubValue = CryptoUtil.loadRsaPublicKey( - subscriber.bankEncryptionPublicKey?.toByteArray()!! - ) - } - - return EbicsContainer( - - - bankAuthPub = bankAuthPubValue, - bankEncPub = bankEncPubValue, - - ebicsUrl = subscriber.ebicsURL, - hostId = subscriber.hostID, - userId = subscriber.userID, - partnerId = subscriber.partnerID, - - customerSignPriv = CryptoUtil.loadRsaPrivateKey(subscriber.signaturePrivateKey.toByteArray()), - customerAuthPriv = CryptoUtil.loadRsaPrivateKey(subscriber.authenticationPrivateKey.toByteArray()), - customerEncPriv = CryptoUtil.loadRsaPrivateKey(subscriber.authenticationPrivateKey.toByteArray()) - ) - -} - -/** - * Inserts spaces every 2 characters, and a newline after 8 pairs. - */ -fun chunkString(input: String): String { - - val ret = StringBuilder() - var columns = 0 - - for (i in input.indices) { - - if ((i + 1).rem(2) == 0) { - - if (columns == 7) { - ret.append(input[i] + "\n") - columns = 0 - continue - } - - ret.append(input[i] + " ") - columns++ - continue - } - ret.append(input[i]) - } - - return ret.toString() - -} - -fun expectId(param: String?): Int { - - try { - return param!!.toInt() - } catch (e: Exception) { - throw NotAnIdError(HttpStatusCode.BadRequest) - } -} - -fun signOrder( - orderBlob: ByteArray, - signKey: RSAPrivateCrtKey, - partnerId: String, - userId: String -): UserSignatureData { - - val ES_signature = CryptoUtil.signEbicsA006( - CryptoUtil.digestEbicsOrderA006(orderBlob), - signKey - ) - val userSignatureData = UserSignatureData().apply { - orderSignatureList = listOf( - UserSignatureData.OrderSignatureData().apply { - signatureVersion = "A006" - signatureValue = ES_signature - partnerID = partnerId - userID = userId - } - ) - } - - return userSignatureData -} - - -/** - * @return null when the bank could not be reached, otherwise returns the - * response already converted in JAXB. - */ -suspend inline fun HttpClient.postToBank(url: String, body: String): String { - - val response = try { - this.post<String>( - urlString = url, - block = { - this.body = body - } - ) - } catch (e: Exception) { - throw UnreachableBankError(HttpStatusCode.InternalServerError) - } - - return response -} - -/** - * DO verify the bank's signature - */ -suspend inline fun <reified T, reified S>HttpClient.postToBankSignedAndVerify( - url: String, - body: T, - pub: RSAPublicKey, - priv: RSAPrivateCrtKey): JAXBElement<S> { - - val doc = XMLUtil.convertJaxbToDocument(body) - XMLUtil.signEbicsDocument(doc, priv) - - val response: String = this.postToBank(url, XMLUtil.convertDomToString(doc)) - logger.debug("About to verify: ${response}") - - val responseString = try { - - XMLUtil.parseStringIntoDom(response) - } catch (e: Exception) { - - throw UnparsableResponse(HttpStatusCode.BadRequest, response) - } - - if (!XMLUtil.verifyEbicsDocument(responseString, pub)) { - - throw BadSignature(HttpStatusCode.NotAcceptable) - } - - try { - - return XMLUtil.convertStringToJaxb(response) - } catch (e: Exception) { - - throw UnparsableResponse(HttpStatusCode.BadRequest, response) - } -} - -suspend inline fun <reified T, reified S>HttpClient.postToBankSigned( - url: String, - body: T, - priv: PrivateKey): JAXBElement<S> { - - val doc = XMLUtil.convertJaxbToDocument(body) - XMLUtil.signEbicsDocument(doc, priv) - - val response: String = this.postToBank(url, XMLUtil.convertDomToString(doc)) - - try { - return XMLUtil.convertStringToJaxb(response) - } catch (e: Exception) { - throw UnparsableResponse(HttpStatusCode.BadRequest, response) - } -} - - - -/** - * do NOT verify the bank's signature - */ -suspend inline fun <reified T, reified S>HttpClient.postToBankUnsigned( - url: String, - body: T -): JAXBElement<S> { - - val response: String = this.postToBank(url, XMLUtil.convertJaxbToString(body)) - - try { - return XMLUtil.convertStringToJaxb(response) - } catch (e: Exception) { - throw UnparsableResponse(HttpStatusCode.BadRequest, response) - } -} - -/** - * @param size in bits - */ -fun getNonce(size: Int): ByteArray { - val sr = SecureRandom() - val ret = ByteArray(size / 8) - sr.nextBytes(ret) - return ret -} - -fun getGregorianDate(): XMLGregorianCalendar { - val gregorianCalendar = GregorianCalendar() - val datatypeFactory = DatatypeFactory.newInstance() - return datatypeFactory.newXMLGregorianCalendar(gregorianCalendar) -} - data class NotAnIdError(val statusCode: HttpStatusCode) : Exception("String ID not convertible in number") data class BankKeyMissing(val statusCode: HttpStatusCode) : Exception("Impossible operation: bank keys are missing") data class SubscriberNotFoundError(val statusCode: HttpStatusCode) : Exception("Subscriber not found in database") @@ -398,27 +193,50 @@ fun main() { return@get } - get("/ebics/subscribers/{id}/sendHtd") { + post("/ebics/subscribers/{id}/sendC52") { val id = expectId(call.parameters["id"]) + val body = call.receive<EbicsDateRange>() + + val startDate = LocalDate.parse(body.start) + val endDate = LocalDate.parse(body.end) + // will throw DateTimeParseException if strings are malformed. + + val subscriberData = transaction { containerInit(EbicsSubscriberEntity.findById(id) ?: throw SubscriberNotFoundError(HttpStatusCode.NotFound)) } - val response = client.postToBankSigned<EbicsRequest, EbicsResponse>( subscriberData.ebicsUrl, - EbicsRequest.createForDownloadInitializationPhase( - subscriberData.userId, - subscriberData.partnerId, - subscriberData.hostId, + createDownloadInitializationPhase( + subscriberData, + "C52", getNonce(128), getGregorianDate(), - subscriberData.bankEncPub ?: throw BankKeyMissing(HttpStatusCode.PreconditionFailed), - subscriberData.bankAuthPub ?: throw BankKeyMissing(HttpStatusCode.PreconditionFailed), - "HTD" + getGregorianDate(startDate.year, startDate.monthValue, startDate.dayOfMonth), + getGregorianDate(endDate.year, endDate.monthValue, endDate.dayOfMonth) ), subscriberData.customerAuthPriv ) + } + + get("/ebics/subscribers/{id}/sendHtd") { + val id = expectId(call.parameters["id"]) + val subscriberData = transaction { + containerInit(EbicsSubscriberEntity.findById(id) ?: throw SubscriberNotFoundError(HttpStatusCode.NotFound)) + } + + val response = client.postToBankSigned<EbicsRequest, EbicsResponse>( + subscriberData.ebicsUrl, + createDownloadInitializationPhase( + subscriberData, + "HTD", + getNonce(128), + getGregorianDate() + ), + subscriberData.customerAuthPriv + ) + logger.debug("HTD response: " + XMLUtil.convertJaxbToString<EbicsResponse>(response.value)) if (response.value.body.returnCode.value != "000000") { @@ -790,7 +608,6 @@ fun main() { EbicsSubscriberEntity.findById(id) ?: throw SubscriberNotFoundError(HttpStatusCode.NotFound) ) } - val payload = "PAYLOAD" val usd_encrypted = CryptoUtil.encryptEbicsE002( EbicsOrderUtil.encodeOrderDataXml( @@ -807,20 +624,15 @@ fun main() { val response = client.postToBankSignedAndVerify<EbicsRequest, EbicsResponse>( subscriberData.ebicsUrl, - EbicsRequest.createForUploadInitializationPhase( + createUploadInitializationPhase( + subscriberData, + "TST", usd_encrypted, - subscriberData.hostId, getNonce(128), - subscriberData.partnerId, - subscriberData.userId, - getGregorianDate(), - subscriberData.bankAuthPub!!, - subscriberData.bankEncPub!!, - BigInteger.ONE, - "TST" + getGregorianDate() ), subscriberData.bankAuthPub!!, - subscriberData.customerAuthPriv + subscriberData.customerEncPriv ) if (response.value.body.returnCode.value != "000000") { @@ -865,8 +677,7 @@ fun main() { HttpStatusCode.OK ) } - - + post("/ebics/subscribers/{id}/sync") { val id = expectId(call.parameters["id"]) val bundle = transaction { diff --git a/sandbox/src/main/kotlin/tech/libeufin/schema/ebics_h004/EbicsRequest.kt b/sandbox/src/main/kotlin/tech/libeufin/schema/ebics_h004/EbicsRequest.kt @@ -305,6 +305,41 @@ class EbicsRequest { } + /* Take a time range (useful for C52 and C53) */ + fun createForDownloadInitializationPhase( + userId: String, + partnerId: String, + hostId: String, + nonceArg: ByteArray, + date: XMLGregorianCalendar, + bankEncPub: RSAPublicKey, + bankAuthPub: RSAPublicKey, + aOrderType: String, + dateStart: XMLGregorianCalendar, + dateEnd: XMLGregorianCalendar + ): EbicsRequest { + + val tmp = this.createForDownloadInitializationPhase( + userId, + partnerId, + hostId, + nonceArg, + date, + bankEncPub, + bankAuthPub, + aOrderType + ) + + (tmp.header.static.orderDetails?.orderParams as StandardOrderParams).apply { + dateRange?.apply { + start = dateStart + end = dateEnd + } + } + + return tmp + } + fun createForDownloadInitializationPhase( userId: String, partnerId: String,