libeufin

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

commit d9e44a79da332e58d33f29544d6dd4a8762d4c58
parent ebeb47583ae9f5c612bf3412545a4cddda574826
Author: Marcello Stanisci <stanisci.m@gmail.com>
Date:   Fri,  8 Nov 2019 12:56:52 +0100

prevent http client from throwing exceptions..

.. on != 200 responses.

Diffstat:
Mnexus/src/main/kotlin/DB.kt | 4++++
Mnexus/src/main/kotlin/Main.kt | 78++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Msandbox/src/main/python/libeufin-cli | 211++++++++++++++-----------------------------------------------------------------
3 files changed, 89 insertions(+), 204 deletions(-)

diff --git a/nexus/src/main/kotlin/DB.kt b/nexus/src/main/kotlin/DB.kt @@ -17,6 +17,8 @@ object EbicsSubscribersTable : IntIdTable() { val signaturePrivateKey = blob("signaturePrivateKey") val encryptionPrivateKey = blob("encryptionPrivateKey") val authenticationPrivateKey = blob("authenticationPrivateKey") + val bankEncryptionPublicKey = blob("bankEncryptionPublicKey").nullable() + val bankAuthenticationPublicKey = blob("bankAuthenticationPublicKey").nullable() } class EbicsSubscriberEntity(id: EntityID<Int>) : IntEntity(id) { @@ -30,6 +32,8 @@ class EbicsSubscriberEntity(id: EntityID<Int>) : IntEntity(id) { var signaturePrivateKey by EbicsSubscribersTable.signaturePrivateKey var encryptionPrivateKey by EbicsSubscribersTable.encryptionPrivateKey var authenticationPrivateKey by EbicsSubscribersTable.authenticationPrivateKey + var bankEncryptionPublicKey by EbicsSubscribersTable.bankEncryptionPublicKey + var bankAuthenticationPublicKey by EbicsSubscribersTable.bankAuthenticationPublicKey } fun dbCreateTables() { diff --git a/nexus/src/main/kotlin/Main.kt b/nexus/src/main/kotlin/Main.kt @@ -38,6 +38,7 @@ import io.ktor.routing.post import io.ktor.routing.routing import io.ktor.server.engine.embeddedServer import io.ktor.server.netty.Netty +import org.apache.commons.codec.digest.Crypt import org.apache.xml.security.binding.xmldsig.RSAKeyValueType import org.apache.xml.security.binding.xmldsig.SignatureType import org.jetbrains.exposed.sql.transactions.transaction @@ -90,8 +91,7 @@ fun expectId(param: String?) : Int { * @return null when the bank could not be reached, otherwise returns the * response already converted in JAXB. */ -// suspend inline fun <reified S, reified T>HttpClient.postToBank(url: String, body: JAXBElement<T>) : JAXBElement<S>? { -suspend inline fun <reified S>HttpClient.postToBank(url: String, body: String): JAXBElement<S>? { +suspend inline fun <reified S>HttpClient.postToBank(url: String, body: String): JAXBElement<S> { val response = try { this.post<String>( @@ -101,21 +101,21 @@ suspend inline fun <reified S>HttpClient.postToBank(url: String, body: String): } ) } catch (e: Exception) { - e.printStackTrace() - return null + throw UnreachableBankError(HttpStatusCode.InternalServerError) } - // note: not checking status code, as EBICS mandates to return "200 OK" for ANY outcome. - return XMLUtil.convertStringToJaxb(response) + try { + return XMLUtil.convertStringToJaxb(response) + } catch (e: Exception) { + throw UnparsableResponse(HttpStatusCode.BadRequest) + } } -// takes JAXB -suspend inline fun <reified T, reified S>HttpClient.postToBank(url: String, body: T): JAXBElement<S>? { +suspend inline fun <reified T, reified S>HttpClient.postToBank(url: String, body: T): JAXBElement<S> { return this.postToBank<S>(url, XMLUtil.convertJaxbToString(body)) } -// takes DOM -suspend inline fun <reified S>HttpClient.postToBank(url: String, body: Document): JAXBElement<S>? { +suspend inline fun <reified S>HttpClient.postToBank(url: String, body: Document): JAXBElement<S> { return this.postToBank<S>(url, XMLUtil.convertDomToString(body)) } @@ -138,6 +138,8 @@ fun getGregorianDate(): XMLGregorianCalendar { data class NotAnIdError(val statusCode: HttpStatusCode) : Exception("String ID not convertible in number") data class SubscriberNotFoundError(val statusCode: HttpStatusCode) : Exception("Subscriber not found in database") data class UnreachableBankError(val statusCode: HttpStatusCode) : Exception("Could not reach the bank") +data class UnparsableResponse(val statusCode: HttpStatusCode) : Exception("Bank responded with non-XML / non-EBICS " + + "content") data class EbicsError(val codeError: String) : Exception("Bank did not accepted EBICS request, error is: " + codeError ) @@ -145,7 +147,9 @@ data class EbicsError(val codeError: String) : Exception("Bank did not accepted fun main() { dbCreateTables() testData() // gets always id == 1 - val client = HttpClient() + val client = HttpClient(){ + expectSuccess = false // this way, does not throw exceptions on != 200 responses + } val logger = LoggerFactory.getLogger("tech.libeufin.nexus") @@ -169,6 +173,12 @@ fun main() { call.respondText("Bad request\n", ContentType.Text.Plain, HttpStatusCode.BadRequest) } + exception<UnparsableResponse> { cause -> + logger.error("Exception while handling '${call.request.uri}'", cause) + call.respondText("Could not parse bank response\n", ContentType.Text.Plain, HttpStatusCode + .InternalServerError) + } + exception<UnreachableBankError> { cause -> logger.error("Exception while handling '${call.request.uri}'", cause) call.respondText("Could not reach the bank\n", ContentType.Text.Plain, HttpStatusCode.InternalServerError) @@ -308,19 +318,11 @@ fun main() { throw EbicsError(responseJaxb.value.body.returnCode.value) } - call.respond( - HttpStatusCode.OK, - NexusError("Sandbox accepted the key!") - ) + call.respondText("Bank accepted signature key\n", ContentType.Text.Plain, HttpStatusCode.OK) return@post } post("/ebics/subscribers/{id}/sync") { - // fetch sub's EBICS URL, done - // prepare message, done - // send it out, done - // _parse_ response! - // respond to client val id = expectId(call.parameters["id"]) val (url, body, encPrivBlob) = transaction { val subscriber = EbicsSubscriberEntity.findById(id) ?: throw SubscriberNotFoundError(HttpStatusCode.NotFound) @@ -354,9 +356,7 @@ fun main() { Triple(subscriber.ebicsURL, hpbDoc, subscriber.encryptionPrivateKey.toByteArray()) } - val response = client.postToBank<EbicsKeyManagementResponse>(url, body) ?: throw UnreachableBankError( - HttpStatusCode.InternalServerError - ) + val response = client.postToBank<EbicsKeyManagementResponse>(url, body) if (response.value.body.returnCode.value != "000000") { throw EbicsError(response.value.body.returnCode.value) @@ -369,10 +369,27 @@ fun main() { response.value.body.dataTransfer!!.orderData.value ) - var dataCompr = CryptoUtil.decryptEbicsE002(er, CryptoUtil.loadRsaPrivateKey(encPrivBlob)) - var data = EbicsOrderUtil.decodeOrderDataXml<HPBResponseOrderData>(dataCompr) + val dataCompr = CryptoUtil.decryptEbicsE002(er, CryptoUtil.loadRsaPrivateKey(encPrivBlob)) + val data = EbicsOrderUtil.decodeOrderDataXml<HPBResponseOrderData>(dataCompr) - call.respond(HttpStatusCode.NotImplemented, NexusError("work in progress")) + val bankAuthPubBlob = CryptoUtil.loadRsaPublicKeyFromComponents( + data.authenticationPubKeyInfo.pubKeyValue.rsaKeyValue.modulus, + data.authenticationPubKeyInfo.pubKeyValue.rsaKeyValue.exponent + ) + + val bankEncPubBlob = CryptoUtil.loadRsaPublicKeyFromComponents( + data.encryptionPubKeyInfo.pubKeyValue.rsaKeyValue.modulus, + data.encryptionPubKeyInfo.pubKeyValue.rsaKeyValue.exponent + ) + + // put bank's keys into database. + transaction { + val subscriber = EbicsSubscriberEntity.findById(id) + subscriber!!.bankAuthenticationPublicKey = SerialBlob(bankAuthPubBlob.encoded) + subscriber!!.bankEncryptionPublicKey = SerialBlob(bankEncPubBlob.encoded) + } + + call.respondText("Bank keys stored in database\n", ContentType.Text.Plain, HttpStatusCode.OK) return@post } @@ -448,10 +465,11 @@ fun main() { throw EbicsError(responseJaxb.value.body.returnCode.value) } - call.respond( - HttpStatusCode.OK, - NexusError("Sandbox accepted the keys!") - ) + call.respondText( + "Bank accepted authentication and encryption keys\n", + ContentType.Text.Plain, + HttpStatusCode.OK) + return@post } } diff --git a/sandbox/src/main/python/libeufin-cli b/sandbox/src/main/python/libeufin-cli @@ -9,211 +9,74 @@ from requests import post, get from Crypto.PublicKey import RSA from urllib.parse import urljoin -CUSTOMERS_PATH = "/tmp/libeufindata/customers" -RECIPIENT_BANK = "LibEuBank" -RSA_LENGTH = 2048 # key "length" -IA_VERSION = "X002" -ENC_VERSION = "E002" -ES_VERSION = "A005" - @click.group() @click.option( - "--base-url", default="http://localhost:5000/", - help="Base URL of the bank (defaults to http://localhost:5000/)") + "--base-url", default="http://localhost:5001/", + help="Base URL of the nexus (defaults to http://localhost:5001/)") @click.pass_context def cli(ctx, base_url): ctx.obj = dict(base_url=base_url) @cli.group() -def admin(): +def ebics(): pass -@admin.command(help="Create a new customer (generating name)") +@ebics.command(help="send INI message") @click.pass_obj -def customers(obj): - - from faker import Faker - name = Faker().name() +@click.option( + "--customer-id", + help="numerical ID of the customer at the Nexus", + required=False, + default=1) +def ini(obj, customer_id): - url = urljoin(obj["base_url"], "/admin/customers") - print("Submitting '{}' to {}".format(name, url)) + url = urljoin(obj["base_url"], "/ebics/subscribers/{}/sendIni".format(customer_id)) try: - resp = post(url, json=dict(name=name)) + resp = post(url) except Exception: print("Could not reach the bank") return assert(resp.status_code == 200) - # use the customer id contained in the response to - # query for your details. - customer_id = resp.json().get("id") - assert(customer_id != None) - - customer_path = "{}/{}/".format(CUSTOMERS_PATH, customer_id) - try: - os.makedirs(customer_path) - except OSError as e: - # For now, just overwrite all is found under existing directory. - assert(e.errno == errno.EEXIST) + print(resp.content.decode("utf-8")) - # Generate keys for new user. - for keytype in ("eskey", "iakey", "enckey"): - key = RSA.generate(RSA_LENGTH) - pem = key.exportKey("PEM").decode("ascii") - keyfile = open("{}/{}.pem".format(customer_path, keytype), "w") - keyfile.write(pem) - keyfile.write("\n") - keyfile.close() - print( - "Customer (id == {}) and private keys ({}) correctly generated.".format( - customer_id, customer_path - ) - ) -@admin.command(help="Ask details about a customer") +@ebics.command(help="send HIA message") +@click.pass_obj @click.option( "--customer-id", - help="bank non-EBICS identifier of the customer", - required=True) -@click.pass_obj -def customer_info(obj, customer_id): - - url = urljoin( - obj["base_url"], "/admin/customers/{}".format(customer_id) - ) - + help="numerical ID of the customer at the Nexus", + required=False, + default=1) +def hia(obj, customer_id): + + url = urljoin(obj["base_url"], "/ebics/subscribers/{}/sendHia".format(customer_id)) try: - resp = get(url) + resp = post(url) except Exception: - print("Could not reach the bank, aborting") - return - - if resp.status_code != 200: - print("Failed request, status: {}".format(resp.status_code)) + print("Could not reach the bank") return - print(resp.json()) + assert(resp.status_code == 200) + print(resp.content.decode("utf-8")) -@admin.command( - help="Confirm INI and HIA messages via JSON API" -) +@ebics.command(help="send HPB message") +@click.pass_obj @click.option( "--customer-id", - required=True, - help="id of the customer at the bank (used to pick keyset on disk)" -) -@click.pass_obj -def keyletter(obj, customer_id): - - url = urljoin( - obj["base_url"], "/admin/customers/{}".format(customer_id) - ) - - try: - resp = get(url) - except Exception: - print("Could not connect to the bank, aborting") - return - - if resp.status_code != 200: - print("Couldn't query info about the customer: {}".format(resp.status_code)) - return - - - user_id = resp.json().get("ebicsInfo", {}).get("userId") - name = resp.json().get("name") - assert(user_id) - assert(name) - - # Take timestamp. - ts = datetime.now() - - # Get keys from disk. - try: - eskey = RSA.importKey( - open("{}/{}/eskey.pem".format( - CUSTOMERS_PATH, customer_id), "r").read() - ) - - enckey = RSA.importKey( - open("{}/{}/enckey.pem".format( - CUSTOMERS_PATH, customer_id), "r").read() - ) - - iakey = RSA.importKey( - open("{}/{}/iakey.pem".format( - CUSTOMERS_PATH, customer_id), "r").read() - ) - - except FileNotFoundError: - print("Could not find all the keys; now generating them all on the fly..") - eskey = RSA.generate(RSA_LENGTH) - enckey = RSA.generate(RSA_LENGTH) - iakey = RSA.generate(RSA_LENGTH) - - es_exponent = format(eskey.e, "x") - es_modulus = format(eskey.n, "x") - - ia_exponent = format(iakey.e, "x") - ia_modulus = format(iakey.n, "x") - - enc_exponent = format(enckey.e, "x") - enc_modulus = format(enckey.n, "x") - - # Make the request body. - body = dict( - - ini=dict( - userId=user_id, - customerId=customer_id, - name=name, - date=ts.strftime("%d.%m.%Y"), - time=ts.strftime("%H.%M.%S"), - recipient=RECIPIENT_BANK, - version=ES_VERSION, - public_exponent_length=eskey.n.bit_length(), - public_exponent=es_exponent, - public_modulus_length=eskey.e.bit_length(), - public_modulus=es_modulus, - hash=hashlib.sha256("{} {}".format(es_exponent, es_modulus).encode()).hexdigest() - ), - - hia=dict( - userId=user_id, - customerId=customer_id, - name=name, - date=ts.strftime("%d.%m.%Y"), - time=ts.strftime("%H.%M.%S"), - recipient=RECIPIENT_BANK, - ia_version=IA_VERSION, - ia_public_exponent_length=iakey.e.bit_length(), - ia_public_exponent=ia_exponent, - ia_public_modulus_length=iakey.n.bit_length(), - ia_public_modulus=ia_modulus, - ia_hash=hashlib.sha256("{} {}".format(ia_exponent, ia_modulus).encode()).hexdigest(), - enc_version=ENC_VERSION, - enc_public_exponent_length=enckey.e.bit_length(), - enc_public_exponent=enc_exponent, - enc_public_modulus_length=enckey.n.bit_length(), - enc_public_modulus=enc_modulus, - enc_hash=hashlib.sha256("{} {}".format(enc_exponent, enc_modulus).encode()).hexdigest() - ) - ) - - url = urljoin( - obj["base_url"], "/admin/customers/{}/ebics/keyletter".format(customer_id) - ) - + help="numerical ID of the customer at the Nexus", + required=False, + default=1) +def sync(obj, customer_id): + + url = urljoin(obj["base_url"], "/ebics/subscribers/{}/sync".format(customer_id)) try: - resp = post(url, json=body) + resp = post(url) except Exception: - print("Could not reach the bank, aborting now") - return - - if resp.status_code != 200: - print("Bank did not accept this letter: {}.".format(resp.status_code)) + print("Could not reach the bank") return - print("Letter accepted by the bank!") + assert(resp.status_code == 200) + print(resp.content.decode("utf-8")) cli()