libeufin

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

commit 0800e6a51177dcfa6d15c4ff6dca7362603c10d4
parent e3307ec4f08ad31ac75baa22486d5d04bbdfdcfe
Author: Florian Dold <florian.dold@gmail.com>
Date:   Mon,  4 Nov 2019 17:30:03 +0100

refactor package structure

Diffstat:
Dsandbox/src/main/kotlin/CryptoUtil.kt | 179-------------------------------------------------------------------------------
Dsandbox/src/main/kotlin/DB.kt | 187-------------------------------------------------------------------------------
Dsandbox/src/main/kotlin/Main.kt | 467-------------------------------------------------------------------------------
Dsandbox/src/main/kotlin/XMLUtil.kt | 376-------------------------------------------------------------------------------
Asandbox/src/main/kotlin/tech/libeufin/sandbox/CryptoUtil.kt | 174+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt | 187+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rsandbox/src/main/kotlin/JSON.kt -> sandbox/src/main/kotlin/tech/libeufin/sandbox/JSON.kt | 0
Asandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt | 468+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asandbox/src/main/kotlin/tech/libeufin/sandbox/XMLUtil.kt | 386+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msandbox/src/main/kotlin/tech/libeufin/schema/ebics_h004/EbicsMessages.kt | 16++++++++++++++++
Msandbox/src/test/kotlin/XmlUtilTest.kt | 6++----
11 files changed, 1233 insertions(+), 1213 deletions(-)

diff --git a/sandbox/src/main/kotlin/CryptoUtil.kt b/sandbox/src/main/kotlin/CryptoUtil.kt @@ -1,179 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2019 Stanisci and Dold. - - * LibEuFin is free software; you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation; either version 3, or - * (at your option) any later version. - - * LibEuFin is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General - * Public License for more details. - - * You should have received a copy of the GNU Affero General Public - * License along with LibEuFin; see the file COPYING. If not, see - * <http://www.gnu.org/licenses/> - */ - -package tech.libeufin.sandbox - -import org.bouncycastle.jce.provider.BouncyCastleProvider -import java.io.ByteArrayOutputStream -import java.lang.Exception -import java.math.BigInteger -import java.security.KeyFactory -import java.security.KeyPairGenerator -import java.security.interfaces.RSAPrivateCrtKey -import java.security.interfaces.RSAPublicKey -import java.security.spec.PKCS8EncodedKeySpec -import java.security.spec.RSAPublicKeySpec -import java.security.spec.X509EncodedKeySpec -import javax.crypto.Cipher -import javax.crypto.KeyGenerator -import java.security.MessageDigest -import javax.crypto.spec.IvParameterSpec -import javax.crypto.spec.SecretKeySpec - - - - - - - -/** - * RSA key pair. - */ -data class RsaCrtKeyPair(val private: RSAPrivateCrtKey, val public: RSAPublicKey) - -/** - * Helpers for dealing with crypographic operations in EBICS / LibEuFin. - */ -class CryptoUtil { - - class EncryptionResult( - val encryptedTransactionKey: ByteArray, - val pubKeyDigest: ByteArray, - val encryptedData: ByteArray - ) - - companion object { - private val bouncyCastleProvider = BouncyCastleProvider() - - /** - * Load an RSA private key from its binary PKCS#8 encoding. - */ - fun loadRsaPrivateKey(encodedPrivateKey: ByteArray): RSAPrivateCrtKey { - val spec = PKCS8EncodedKeySpec(encodedPrivateKey) - val priv = KeyFactory.getInstance("RSA").generatePrivate(spec) - if (priv !is RSAPrivateCrtKey) - throw Exception("wrong encoding") - return priv - } - - /** - * Load an RSA public key from its binary X509 encoding. - */ - fun loadRsaPublicKey(encodedPublicKey: ByteArray): RSAPublicKey { - val spec = X509EncodedKeySpec(encodedPublicKey) - val pub = KeyFactory.getInstance("RSA").generatePublic(spec) - if (pub !is RSAPublicKey) - throw Exception("wrong encoding") - return pub - } - - /** - * Load an RSA public key from its binary X509 encoding. - */ - fun getRsaPublicFromPrivate(rsaPrivateCrtKey: RSAPrivateCrtKey): RSAPublicKey { - val spec = RSAPublicKeySpec(rsaPrivateCrtKey.modulus, rsaPrivateCrtKey.publicExponent) - val pub = KeyFactory.getInstance("RSA").generatePublic(spec) - if (pub !is RSAPublicKey) - throw Exception("wrong encoding") - return pub - } - - /** - * Generate a fresh RSA key pair. - * - * @param nbits size of the modulus in bits - */ - fun generateRsaKeyPair(nbits: Int): RsaCrtKeyPair { - val gen = KeyPairGenerator.getInstance("RSA") - gen.initialize(nbits) - val pair = gen.genKeyPair() - val priv = pair.private - val pub = pair.public - if (priv !is RSAPrivateCrtKey) - throw Exception("key generation failed") - if (pub !is RSAPublicKey) - throw Exception("key generation failed") - return RsaCrtKeyPair(priv, pub) - } - - /** - * Load an RSA public key from its components. - * - * @param exponent - * @param modulus - * @return key - */ - fun loadRsaPublicKeyFromComponents(modulus: ByteArray, exponent: ByteArray): RSAPublicKey { - val modulusBigInt = BigInteger(1, modulus) - val exponentBigInt = BigInteger(1, exponent) - - val keyFactory = KeyFactory.getInstance("RSA") - val tmp = RSAPublicKeySpec(modulusBigInt, exponentBigInt) - return keyFactory.generatePublic(tmp) as RSAPublicKey - } - - /** - * Hash an RSA public key according to the EBICS standard (EBICS 2.5: 4.4.1.2.3). - */ - fun getEbicsPublicKeyHash(publicKey: RSAPublicKey): ByteArray { - val keyBytes = ByteArrayOutputStream() - keyBytes.writeBytes(publicKey.publicExponent.toByteArray()) - keyBytes.write(' '.toInt()) - keyBytes.writeBytes(publicKey.modulus.toByteArray()) - val digest = MessageDigest.getInstance("SHA-256") - return digest.digest(keyBytes.toByteArray()) - } - - /** - * Encrypt data according to the EBICS E002 encryption process. - */ - fun encryptEbicsE002(data: ByteArray, encryptionPublicKey: RSAPublicKey): EncryptionResult { - val keygen = KeyGenerator.getInstance("AES", bouncyCastleProvider) - keygen.init(128) - val transactionKey = keygen.generateKey() - val symmetricCipher = Cipher.getInstance("AES/CBC/X9.23Padding", bouncyCastleProvider) - val ivParameterSpec = IvParameterSpec(ByteArray(16)) - symmetricCipher.init(Cipher.ENCRYPT_MODE, transactionKey, ivParameterSpec) - val encryptedData = symmetricCipher.doFinal(data) - val asymmetricCipher = Cipher.getInstance("RSA/None/PKCS1Padding", bouncyCastleProvider) - asymmetricCipher.init(Cipher.ENCRYPT_MODE, encryptionPublicKey) - val encryptedTransactionKey = asymmetricCipher.doFinal(transactionKey.encoded) - val pubKeyDigest = getEbicsPublicKeyHash(encryptionPublicKey) - return EncryptionResult(encryptedTransactionKey, pubKeyDigest, encryptedData) - } - - fun decryptEbicsE002(enc: EncryptionResult, privateKey: RSAPrivateCrtKey): ByteArray { - val asymmetricCipher = Cipher.getInstance("RSA/None/PKCS1Padding", bouncyCastleProvider) - asymmetricCipher.init(Cipher.DECRYPT_MODE, privateKey) - val transactionKeyBytes = asymmetricCipher.doFinal(enc.encryptedTransactionKey) - val secretKeySpec = SecretKeySpec(transactionKeyBytes, "AES") - val symmetricCipher = Cipher.getInstance("AES/CBC/X9.23Padding", bouncyCastleProvider) - val ivParameterSpec = IvParameterSpec(ByteArray(16)) - symmetricCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec) - 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/DB.kt b/sandbox/src/main/kotlin/DB.kt @@ -1,187 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2019 Stanisci and Dold. - - * LibEuFin is free software; you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation; either version 3, or - * (at your option) any later version. - - * LibEuFin is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General - * Public License for more details. - - * You should have received a copy of the GNU Affero General Public - * License along with LibEuFin; see the file COPYING. If not, see - * <http://www.gnu.org/licenses/> - */ - -package tech.libeufin.sandbox.db - -import org.jetbrains.exposed.dao.* -import org.jetbrains.exposed.sql.* -import org.jetbrains.exposed.sql.transactions.transaction -import java.sql.Blob - -const val CUSTOMER_NAME_MAX_LENGTH = 20 -const val EBICS_HOST_ID_MAX_LENGTH = 10 -const val EBICS_USER_ID_MAX_LENGTH = 10 -const val EBICS_PARTNER_ID_MAX_LENGTH = 10 -const val EBICS_SYSTEM_ID_MAX_LENGTH = 10 -/** - * All the states to give a subscriber. - */ -enum class SubscriberState { - /** - * No keys at all given to the bank. - */ - NEW, - - /** - * Only INI electronic message was successfully sent. - */ - PARTIALLY_INITIALIZED_INI, - - /**r - * Only HIA electronic message was successfully sent. - */ - PARTIALLY_INITIALIZED_HIA, - - /** - * Both INI and HIA were electronically sent with success. - */ - INITIALIZED, - - /** - * All the keys accounted in INI and HIA have been confirmed - * via physical mail. - */ - READY -} - -/** - * All the states that one key can be assigned. - */ -enum class KeyState { - - /** - * The key was never communicated. - */ - MISSING, - - /** - * The key has been electronically sent. - */ - NEW, - - /** - * The key has been confirmed (either via physical mail - * or electronically -- e.g. with certificates) - */ - RELEASED -} - -fun Blob.toByteArray(): ByteArray { - return this.binaryStream.readAllBytes() -} - -/** - * This table information *not* related to EBICS, for all - * its customers. - */ -object BankCustomers: IntIdTable() { - // Customer ID is the default 'id' field provided by the constructor. - val name = varchar("name", CUSTOMER_NAME_MAX_LENGTH).primaryKey() - val ebicsSubscriber = reference("ebicsSubscriber", EbicsSubscribers) -} - -class BankCustomer(id: EntityID<Int>) : IntEntity(id) { - companion object : IntEntityClass<BankCustomer>(BankCustomers) - - var name by BankCustomers.name - var ebicsSubscriber by EbicsSubscriber referencedOn BankCustomers.ebicsSubscriber -} - - -/** - * This table stores RSA public keys of subscribers. - */ -object EbicsPublicKeys : IntIdTable() { - val rsaPublicKey = blob("rsaPublicKey") - val state = enumeration("state", KeyState::class) -} - - -/** - * Definition of a row in the [EbicsPublicKey] table - */ -class EbicsPublicKey(id: EntityID<Int>) : IntEntity(id) { - companion object : IntEntityClass<EbicsPublicKey>(EbicsPublicKeys) - var rsaPublicKey by EbicsPublicKeys.rsaPublicKey - var state by EbicsPublicKeys.state -} - - -object EbicsHosts : IntIdTable() { - val hostID = text("hostID") - val ebicsVersion = text("ebicsVersion") - val signaturePrivateKey = blob("signaturePrivateKey") - val encryptionPrivateKey = blob("encryptionPrivateKey") - val authenticationPrivateKey = blob("authenticationPrivateKey") -} - - -class EbicsHost(id: EntityID<Int>) : IntEntity(id) { - companion object : IntEntityClass<EbicsHost>(EbicsHosts) - var hostId by EbicsHosts.hostID - var ebicsVersion by EbicsHosts.ebicsVersion - var signaturePrivateKey by EbicsHosts.signaturePrivateKey - var encryptionPrivateKey by EbicsHosts.encryptionPrivateKey - var authenticationPrivateKey by EbicsHosts.authenticationPrivateKey -} - -/** - * Subscribers table. This table associates users with partners - * and systems. Each value can appear multiple times in the same column. - */ -object EbicsSubscribers: IntIdTable() { - val userId = text("userID") - val partnerId = text("partnerID") - val systemId = text("systemID").nullable() - - val signatureKey = reference("signatureKey", EbicsPublicKeys).nullable() - val encryptionKey = reference("encryptionKey", EbicsPublicKeys).nullable() - val authenticationKey = reference("authorizationKey", EbicsPublicKeys).nullable() - - val state = enumeration("state", SubscriberState::class) -} - -class EbicsSubscriber(id: EntityID<Int>) : IntEntity(id) { - companion object : IntEntityClass<EbicsSubscriber>(EbicsSubscribers) - - var userId by EbicsSubscribers.userId - var partnerId by EbicsSubscribers.partnerId - var systemId by EbicsSubscribers.systemId - - var signatureKey by EbicsPublicKey optionalReferencedOn EbicsSubscribers.signatureKey - var encryptionKey by EbicsPublicKey optionalReferencedOn EbicsSubscribers.encryptionKey - var authenticationKey by EbicsPublicKey optionalReferencedOn EbicsSubscribers.authenticationKey - - var state by EbicsSubscribers.state -} - - -fun dbCreateTables() { - Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", driver = "org.h2.Driver") - - transaction { - // addLogger(StdOutSqlLogger) - - SchemaUtils.create( - BankCustomers, - EbicsSubscribers, - EbicsHosts - ) - } -} diff --git a/sandbox/src/main/kotlin/Main.kt b/sandbox/src/main/kotlin/Main.kt @@ -1,467 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2019 Stanisci and Dold. - - * LibEuFin is free software; you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation; either version 3, or - * (at your option) any later version. - - * LibEuFin is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General - * Public License for more details. - - * You should have received a copy of the GNU Affero General Public - * License along with LibEuFin; see the file COPYING. If not, see - * <http://www.gnu.org/licenses/> - */ - -package tech.libeufin.sandbox - -import io.ktor.application.ApplicationCall -import io.ktor.application.ApplicationCallPipeline -import io.ktor.application.call -import io.ktor.application.install -import io.ktor.features.CallLogging -import io.ktor.features.ContentNegotiation -import io.ktor.features.StatusPages -import io.ktor.gson.gson -import io.ktor.http.ContentType -import io.ktor.http.HttpStatusCode -import io.ktor.request.receive -import io.ktor.request.receiveText -import io.ktor.request.uri -import io.ktor.response.respond -import io.ktor.response.respondText -import io.ktor.routing.get -import io.ktor.routing.post -import io.ktor.routing.routing -import io.ktor.server.engine.embeddedServer -import io.ktor.server.netty.Netty -import org.jetbrains.exposed.sql.and -import org.jetbrains.exposed.sql.transactions.transaction -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import org.w3c.dom.Document -import tech.libeufin.sandbox.db.* -import tech.libeufin.schema.ebics_h004.EbicsKeyManagementResponse -import tech.libeufin.schema.ebics_h004.EbicsNoPubKeyDigestsRequest -import tech.libeufin.schema.ebics_h004.EbicsUnsecuredRequest -import tech.libeufin.schema.ebics_h004.HIARequestOrderDataType -import tech.libeufin.schema.ebics_hev.HEVResponse -import tech.libeufin.schema.ebics_hev.SystemReturnCodeType -import tech.libeufin.schema.ebics_s001.SignaturePubKeyOrderData -import java.nio.charset.StandardCharsets.US_ASCII -import java.nio.charset.StandardCharsets.UTF_8 -import java.security.interfaces.RSAPublicKey -import java.text.DateFormat -import java.util.zip.InflaterInputStream -import javax.sql.rowset.serial.SerialBlob - -val logger: Logger = LoggerFactory.getLogger("tech.libeufin.sandbox") - -val xmlProcess = XMLUtil() - -data class EbicsRequestError(val statusCode: HttpStatusCode) : Exception("Ebics request error") - -private suspend fun ApplicationCall.respondEbicsKeyManagement( - errorText: String, - errorCode: String, - statusCode: HttpStatusCode, - bankReturnCode: String, - orderId: String? = null -) { - val responseXml = EbicsKeyManagementResponse().apply { - version = "H004" - header = EbicsKeyManagementResponse.Header().apply { - authenticate = true - mutable = EbicsKeyManagementResponse.Header.KeyManagementResponseMutableHeaderType().apply { - reportText = errorText - returnCode = errorCode - if (orderId != null) { - this.orderID = orderId - } - } - _static = EbicsKeyManagementResponse.Header.EmptyStaticHeader() - } - body = EbicsKeyManagementResponse.Body().apply { - this.returnCode = EbicsKeyManagementResponse.Body.ReturnCode().apply { - this.authenticate = true - this.value = bankReturnCode - } - } - } - val text = XMLUtil.convertJaxbToString(responseXml) - logger.info("responding with:\n${text}") - respondText(text, ContentType.Application.Xml, statusCode) -} - - -private suspend fun ApplicationCall.respondEbicsInvalidXml() { - respondEbicsKeyManagement("[EBICS_INVALID_XML]", "091010", HttpStatusCode.BadRequest, "000000") -} - - -fun findEbicsSubscriber(partnerID: String, userID: String, systemID: String?): EbicsSubscriber? { - return if (systemID == null) { - EbicsSubscriber.find { - (EbicsSubscribers.partnerId eq partnerID) and (EbicsSubscribers.userId eq userID) - } - } else { - EbicsSubscriber.find { - (EbicsSubscribers.partnerId eq partnerID) and - (EbicsSubscribers.userId eq userID) and - (EbicsSubscribers.systemId eq systemID) - } - }.firstOrNull() -} - -data class SubscriberKeys( - val authenticationPublicKey: RSAPublicKey, - val encryptionPublicKey: RSAPublicKey, - val signaturePublicKey: RSAPublicKey -) - -private suspend fun ApplicationCall.ebicsweb() { - val body: String = receiveText() - logger.debug("Data received: $body") - - val bodyDocument: Document? = XMLUtil.parseStringIntoDom(body) - - if (bodyDocument == null || (!xmlProcess.validateFromDom(bodyDocument))) { - respondEbicsInvalidXml() - return - } - - logger.info("Processing ${bodyDocument.documentElement.localName}") - - when (bodyDocument.documentElement.localName) { - "ebicsUnsecuredRequest" -> { - - val bodyJaxb = XMLUtil.convertDomToJaxb( - EbicsUnsecuredRequest::class.java, - bodyDocument - ) - - val staticHeader = bodyJaxb.value.header.static - val requestHostID = bodyJaxb.value.header.static.hostID - - val ebicsHost = transaction { - EbicsHost.find { EbicsHosts.hostID eq requestHostID }.firstOrNull() - } - - if (ebicsHost == null) { - logger.warn("client requested unknown HostID") - respondEbicsKeyManagement("[EBICS_INVALID_HOST_ID]", "091011", HttpStatusCode.NotFound, "000000") - return - } - - logger.info("Serving a ${bodyJaxb.value.header.static.orderDetails.orderType} request") - - /** - * NOTE: the JAXB interface has some automagic mechanism that decodes - * the Base64 string into its byte[] form _at the same time_ it instantiates - * the object; in other words, there is no need to perform here the decoding. - */ - val zkey = bodyJaxb.value.body.dataTransfer.orderData.value - - /** - * The validation enforces zkey to be a base64 value, but does not check - * whether it is given _empty_ or not; will check explicitly here. FIXME: - * shall the schema be patched to avoid having this if-block here? - */ - if (zkey.isEmpty()) { - logger.info("0-length key element given, invalid request") - respondEbicsInvalidXml() - return - } - - /** - * This value holds the bytes[] of a XML "SignaturePubKeyOrderData" document - * and at this point is valid and _never_ empty. - */ - val inflater = InflaterInputStream(zkey.inputStream()) - - var payload = try { - ByteArray(1) { inflater.read().toByte() } - } catch (e: Exception) { - e.printStackTrace() - respondEbicsInvalidXml() - return - } - - while (inflater.available() == 1) { - payload += inflater.read().toByte() - } - - inflater.close() - - logger.debug("Found payload: ${payload.toString(US_ASCII)}") - - when (bodyJaxb.value.header.static.orderDetails.orderType) { - "INI" -> { - val keyObject = XMLUtil.convertStringToJaxb<SignaturePubKeyOrderData>(payload.toString(UTF_8)) - - val rsaPublicKey: RSAPublicKey = try { - CryptoUtil.loadRsaPublicKeyFromComponents( - keyObject.value.signaturePubKeyInfo.pubKeyValue.rsaKeyValue.modulus, - keyObject.value.signaturePubKeyInfo.pubKeyValue.rsaKeyValue.exponent - ) - } catch (e: Exception) { - logger.info("User gave bad key, not storing it") - e.printStackTrace() - respondEbicsInvalidXml() - return - } - - // put try-catch block here? (FIXME) - transaction { - val ebicsSubscriber = - findEbicsSubscriber(staticHeader.partnerID, staticHeader.userID, staticHeader.systemID) - if (ebicsSubscriber == null) { - logger.warn("ebics subscriber ('${staticHeader.partnerID}' / '${staticHeader.userID}' / '${staticHeader.systemID}') not found") - throw EbicsRequestError(HttpStatusCode.NotFound) - } - ebicsSubscriber.signatureKey = EbicsPublicKey.new { - this.rsaPublicKey = SerialBlob(rsaPublicKey.encoded) - state = KeyState.NEW - } - - if (ebicsSubscriber.state == SubscriberState.NEW) { - ebicsSubscriber.state = SubscriberState.PARTIALLY_INITIALIZED_INI - } - - if (ebicsSubscriber.state == SubscriberState.PARTIALLY_INITIALIZED_HIA) { - ebicsSubscriber.state = SubscriberState.INITIALIZED - } - } - - logger.info("Signature key inserted in database _and_ subscriber state changed accordingly") - respondEbicsKeyManagement( - "[EBICS_OK]", - "000000", - HttpStatusCode.OK, - bankReturnCode = "000000", - orderId = "OR01" - ) - return - } - - "HIA" -> { - val keyObject = XMLUtil.convertStringToJaxb<HIARequestOrderDataType>(payload.toString(US_ASCII)) - - val authenticationPublicKey = try { - CryptoUtil.loadRsaPublicKeyFromComponents( - keyObject.value.authenticationPubKeyInfo.pubKeyValue.rsaKeyValue.modulus, - keyObject.value.authenticationPubKeyInfo.pubKeyValue.rsaKeyValue.exponent - ) - } catch (e: Exception) { - logger.info("auth public key invalid") - e.printStackTrace() - respondEbicsInvalidXml() - return - } - - val encryptionPublicKey = try { - CryptoUtil.loadRsaPublicKeyFromComponents( - keyObject.value.encryptionPubKeyInfo.pubKeyValue.rsaKeyValue.modulus, - keyObject.value.encryptionPubKeyInfo.pubKeyValue.rsaKeyValue.exponent - ) - } catch (e: Exception) { - logger.info("auth public key invalid") - e.printStackTrace() - respondEbicsInvalidXml() - return - } - - transaction { - val ebicsSubscriber = - findEbicsSubscriber(staticHeader.partnerID, staticHeader.userID, staticHeader.systemID) - if (ebicsSubscriber == null) { - logger.warn("ebics subscriber not found") - throw EbicsRequestError(HttpStatusCode.NotFound) - } - ebicsSubscriber.authenticationKey = EbicsPublicKey.new { - this.rsaPublicKey = SerialBlob(authenticationPublicKey.encoded) - state = KeyState.NEW - } - ebicsSubscriber.encryptionKey = EbicsPublicKey.new { - this.rsaPublicKey = SerialBlob(encryptionPublicKey.encoded) - state = KeyState.NEW - } - - if (ebicsSubscriber.state == SubscriberState.NEW) { - ebicsSubscriber.state = SubscriberState.PARTIALLY_INITIALIZED_HIA - } - - if (ebicsSubscriber.state == SubscriberState.PARTIALLY_INITIALIZED_INI) { - ebicsSubscriber.state = SubscriberState.INITIALIZED - } - } - respondEbicsKeyManagement("[EBICS_OK]", "000000", HttpStatusCode.OK, "000000") - return - } - } - - throw AssertionError("not reached") - } - - "ebicsHEVRequest" -> { - val hevResponse = HEVResponse().apply { - this.systemReturnCode = SystemReturnCodeType().apply { - this.reportText = "[EBICS_OK]" - this.returnCode = "000000" - } - this.versionNumber = listOf(HEVResponse.VersionNumber.create("H004", "02.50")) - } - - val strResp = XMLUtil.convertJaxbToString(hevResponse) - respondText(strResp, ContentType.Application.Xml, HttpStatusCode.OK) - return - } - "ebicsNoPubKeyDigestsRequest" -> { - val requestJaxb = XMLUtil.convertDomToJaxb(EbicsNoPubKeyDigestsRequest::class.java, bodyDocument) - val staticHeader = requestJaxb.value.header.static - when (val orderType = staticHeader.orderDetails.orderType) { - "HPB" -> { - val subscriberKeys = transaction { - val ebicsSubscriber = - findEbicsSubscriber(staticHeader.partnerID, staticHeader.userID, staticHeader.systemID) - if (ebicsSubscriber == null) { - throw EbicsRequestError(HttpStatusCode.Unauthorized) - } - if (ebicsSubscriber.state != SubscriberState.INITIALIZED) { - throw EbicsRequestError(HttpStatusCode.Forbidden) - } - val authPubBlob = ebicsSubscriber.authenticationKey!!.rsaPublicKey - val encPubBlob = ebicsSubscriber.encryptionKey!!.rsaPublicKey - val sigPubBlob = ebicsSubscriber.signatureKey!!.rsaPublicKey - SubscriberKeys( - CryptoUtil.loadRsaPublicKey(authPubBlob.toByteArray()), - CryptoUtil.loadRsaPublicKey(encPubBlob.toByteArray()), - CryptoUtil.loadRsaPublicKey(sigPubBlob.toByteArray()) - ) - } - val validationResult = XMLUtil.verifyEbicsDocument(bodyDocument, subscriberKeys.authenticationPublicKey) - logger.info("validationResult: $validationResult") - } - else -> { - logger.warn("order type '${orderType}' not supported for ebicsNoPubKeyDigestsRequest") - respondEbicsInvalidXml() - } - } - } - else -> { - /* Log to console and return "unknown type" */ - logger.info("Unknown message, just logging it!") - respond( - HttpStatusCode.NotImplemented, - SandboxError("Not Implemented") - ) - return - } - } -} - - -fun main() { - dbCreateTables() - - transaction { - val pairA = CryptoUtil.generateRsaKeyPair(2048) - val pairB = CryptoUtil.generateRsaKeyPair(2048) - val pairC = CryptoUtil.generateRsaKeyPair(2048) - EbicsHost.new { - hostId = "host01" - ebicsVersion = "H004" - authenticationPrivateKey = SerialBlob(pairA.private.encoded) - encryptionPrivateKey = SerialBlob(pairB.private.encoded) - signaturePrivateKey = SerialBlob(pairC.private.encoded) - } - - EbicsSubscriber.new { - partnerId = "PARTNER1" - userId = "USER1" - systemId = null - state = SubscriberState.NEW - } - } - - val server = embeddedServer(Netty, port = 5000) { - install(CallLogging) - install(ContentNegotiation) { - gson { - setDateFormat(DateFormat.LONG) - setPrettyPrinting() - } - } - install(StatusPages) { - exception<Throwable> { cause -> - logger.error("Exception while handling '${call.request.uri.toString()}'", cause) - call.respondText("Internal server error.", ContentType.Text.Plain, HttpStatusCode.InternalServerError) - } - } - intercept(ApplicationCallPipeline.Fallback) { - if (this.call.response.status() == null) { - call.respondText("Not found (no route matched).\n", ContentType.Text.Plain, HttpStatusCode.NotFound) - return@intercept finish() - } - } - routing { - //trace { logger.info(it.buildText()) } - get("/") { - call.respondText("Hello LibEuFin!\n", ContentType.Text.Plain) - } - get("/ebics/hosts") { - val ebicsHosts = transaction { - EbicsHost.all().map { it.hostId } - } - call.respond(EbicsHostsResponse(ebicsHosts)) - } - post("/ebics/hosts") { - val req = call.receive<EbicsHostCreateRequest>() - transaction { - EbicsHost.new { - this.ebicsVersion = req.ebicsVersion - this.hostId = hostId - } - } - } - get("/ebics/hosts/{id}") { - val resp = transaction { - val host = EbicsHost.find { EbicsHosts.hostID eq call.parameters["id"]!! }.firstOrNull() - if (host == null) null - else EbicsHostResponse(host.hostId, host.ebicsVersion) - } - if (resp == null) call.respond(HttpStatusCode.NotFound, SandboxError("host not found")) - else call.respond(resp) - } - get("/ebics/subscribers") { - val subscribers = transaction { - EbicsSubscriber.all().map { it.id.value.toString() } - } - call.respond(EbicsSubscribersResponse(subscribers)) - } - get("/ebics/subscribers/{id}") { - val resp = transaction { - val id = call.parameters["id"]!! - val subscriber = EbicsSubscriber.findById(id.toInt())!! - EbicsSubscriberResponse( - id, - subscriber.partnerId, - subscriber.userId, - subscriber.systemId, - subscriber.state.name - ) - } - call.respond(resp) - } - post("/ebicsweb") { - call.ebicsweb() - } - } - } - logger.info("Up and running") - server.start(wait = true) -} diff --git a/sandbox/src/main/kotlin/XMLUtil.kt b/sandbox/src/main/kotlin/XMLUtil.kt @@ -1,376 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2019 Stanisci and Dold. - - * LibEuFin is free software; you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation; either version 3, or - * (at your option) any later version. - - * LibEuFin is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General - * Public License for more details. - - * You should have received a copy of the GNU Affero General Public - * License along with LibEuFin; see the file COPYING. If not, see - * <http://www.gnu.org/licenses/> - */ - -package tech.libeufin.sandbox - -import com.sun.org.apache.xerces.internal.dom.DOMInputImpl -import org.apache.xml.security.c14n.Canonicalizer -import org.w3c.dom.Document -import org.w3c.dom.Node -import org.w3c.dom.NodeList -import org.w3c.dom.ls.LSInput -import org.w3c.dom.ls.LSResourceResolver -import org.xml.sax.ErrorHandler -import org.xml.sax.InputSource -import org.xml.sax.SAXException -import org.xml.sax.SAXParseException -import java.io.* -import java.lang.UnsupportedOperationException -import java.security.MessageDigest -import java.security.PrivateKey -import java.security.PublicKey -import java.util.* -import javax.xml.XMLConstants -import javax.xml.bind.JAXBContext -import javax.xml.bind.JAXBElement -import javax.xml.bind.Marshaller -import javax.xml.crypto.* -import javax.xml.crypto.dom.DOMURIReference -import javax.xml.crypto.dsig.* -import javax.xml.crypto.dsig.dom.DOMSignContext -import javax.xml.crypto.dsig.dom.DOMValidateContext -import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec -import javax.xml.crypto.dsig.spec.TransformParameterSpec -import javax.xml.namespace.NamespaceContext -import javax.xml.parsers.DocumentBuilderFactory -import javax.xml.transform.OutputKeys -import javax.xml.transform.Source -import javax.xml.transform.TransformerFactory -import javax.xml.transform.dom.DOMSource -import javax.xml.transform.stream.StreamResult -import javax.xml.transform.stream.StreamSource -import javax.xml.validation.SchemaFactory -import javax.xml.xpath.XPath -import javax.xml.xpath.XPathConstants -import javax.xml.xpath.XPathFactory - -/** - * Helpers for dealing with XML in EBICS. - */ -class XMLUtil { - /** - * This URI dereferencer allows handling the resource reference used for - * XML signatures in EBICS. - */ - private class EbicsSigUriDereferencer : URIDereferencer { - override fun dereference(myRef: URIReference?, myCtx: XMLCryptoContext?): Data { - val ebicsXpathExpr = "//*[@authenticate='true']" - if (myRef !is DOMURIReference) - throw Exception("invalid type") - 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) - if (nodeSet !is NodeList) - throw Exception("invalid type") - if (nodeSet.length <= 0) { - throw Exception("no nodes to sign") - } - val bytes = ByteArrayOutputStream() - 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) - } - return OctetStreamData(ByteArrayInputStream(bytes.toByteArray())) - } - } - - /** - * Validator for EBICS messages. - */ - private val validator = try { - val classLoader = ClassLoader.getSystemClassLoader() - val sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI) - sf.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "file") - sf.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "") - sf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true) - sf.errorHandler = object : ErrorHandler { - override fun warning(p0: SAXParseException?) { - println("Warning: $p0") - } - - override fun error(p0: SAXParseException?) { - println("Error: $p0") - } - - override fun fatalError(p0: SAXParseException?) { - println("Fatal error: $p0") - } - } - sf.resourceResolver = object : LSResourceResolver { - override fun resolveResource( - type: String?, - namespaceURI: String?, - publicId: String?, - systemId: String?, - baseUri: String? - ): LSInput? { - if (type != "http://www.w3.org/2001/XMLSchema") { - return null - } - val res = classLoader.getResourceAsStream("xsd/$systemId") ?: return null - return DOMInputImpl(publicId, systemId, baseUri, res, "UTF-8") - } - } - val schemaInputs: Array<Source> = listOf("xsd/ebics_H004.xsd", "xsd/ebics_hev.xsd").map { - val resUrl = classLoader.getResource(it) ?: throw FileNotFoundException("Schema file $it not found.") - StreamSource(File(resUrl.toURI())) - }.toTypedArray() - val bundle = sf.newSchema(schemaInputs) - bundle.newValidator() - } catch (e: SAXException) { - e.printStackTrace() - throw e - } - - - /** - * - * @param xmlDoc the XML document to validate - * @return true when validation passes, false otherwise - */ - fun validate(xmlDoc: StreamSource): Boolean { - try { - validator?.validate(xmlDoc) - } catch (e: Exception) { - logger.warn("Validation failed: {}", e) - return false - } - return true; - } - - /** - * Validates the DOM against the Schema(s) of this object. - * @param domDocument DOM to validate - * @return true/false if the document is valid/invalid - */ - fun validateFromDom(domDocument: Document): Boolean { - try { - validator?.validate(DOMSource(domDocument)) - } catch (e: SAXException) { - e.printStackTrace() - return false - } - return true - } - - /** - * Craft object to be passed to the XML validator. - * @param xmlString XML body, as read from the POST body. - * @return InputStream object, as wanted by the validator. - */ - fun validateFromString(xmlString: String): Boolean { - val xmlInputStream: InputStream = ByteArrayInputStream(xmlString.toByteArray()) - val xmlSource = StreamSource(xmlInputStream) - return this.validate(xmlSource) - } - - companion object { - inline fun <reified T> convertJaxbToString(obj: T): String { - val sw = StringWriter() - val jc = JAXBContext.newInstance(T::class.java) - val m = jc.createMarshaller() - m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true) - m.marshal(obj, sw) - return sw.toString() - } - - /** - * Convert a XML string to the JAXB representation. - * - * @param documentString the string to convert into JAXB. - * @return the JAXB object reflecting the original XML document. - */ - inline fun <reified T> convertStringToJaxb(documentString: String): JAXBElement<T> { - val jc = JAXBContext.newInstance(T::class.java) - val u = jc.createUnmarshaller() - return u.unmarshal( /* Marshalling the object into the document. */ - StreamSource(StringReader(documentString)), - T::class.java - ) - } - - /** - * Extract String from DOM. - * - * @param document the DOM to extract the string from. - * @return the final String, or null if errors occur. - */ - fun convertDomToString(document: Document): String { - /* Make Transformer. */ - val tf = TransformerFactory.newInstance() - val t = tf.newTransformer() - - //t.setOutputProperty(OutputKeys.INDENT, "yes") - - /* Make string writer. */ - val sw = StringWriter() - - /* Extract string. */ - t.transform(DOMSource(document), StreamResult(sw)) - return sw.toString() - } - - /** - * Convert a node to a string without the XML declaration or - * indentation. - */ - fun convertNodeToString(node: Node): String { - /* Make Transformer. */ - val tf = TransformerFactory.newInstance() - val t = tf.newTransformer() - - t.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); - - /* Make string writer. */ - val sw = StringWriter() - - /* Extract string. */ - t.transform(DOMSource(node), StreamResult(sw)) - return sw.toString() - } - - /** - * Convert a DOM document - of a XML document - to the JAXB representation. - * - * @param finalType class type of the output - * @param document the document to convert into JAXB. - * @return the JAXB object reflecting the original XML document. - */ - fun <T> convertDomToJaxb(finalType: Class<T>, document: Document): JAXBElement<T> { - - val jc = JAXBContext.newInstance(finalType) - - /* Marshalling the object into the document. */ - val m = jc.createUnmarshaller() - return m.unmarshal(document, finalType) // document "went" into Jaxb - } - - /** - * Parse string into XML DOM. - * @param xmlString the string to parse. - * @return the DOM representing @a xmlString - */ - fun parseStringIntoDom(xmlString: String): Document { - val factory = DocumentBuilderFactory.newInstance().apply { - isNamespaceAware = true - } - val xmlInputStream = ByteArrayInputStream(xmlString.toByteArray()) - val builder = factory.newDocumentBuilder() - return builder.parse(InputSource(xmlInputStream)) - } - - - /** - * Sign an EBICS document with the authentication and identity signature. - */ - fun signEbicsDocument(doc: Document, signingPriv: PrivateKey): Unit { - val xpath = XPathFactory.newInstance().newXPath() - xpath.namespaceContext = object : NamespaceContext { - override fun getNamespaceURI(p0: String?): String { - return when (p0) { - "ebics" -> "urn:org:ebics:H004" - else -> throw IllegalArgumentException() - } - } - - override fun getPrefix(p0: String?): String { - throw UnsupportedOperationException() - } - - override fun getPrefixes(p0: String?): MutableIterator<String> { - throw UnsupportedOperationException() - } - } - val authSigNode = xpath.compile("/*[1]/ebics:AuthSignature").evaluate(doc, XPathConstants.NODE) - if (authSigNode !is Node) - throw java.lang.Exception("no AuthSignature") - val fac = XMLSignatureFactory.getInstance("DOM") - val c14n = fac.newTransform(CanonicalizationMethod.INCLUSIVE, null as TransformParameterSpec?) - val ref: Reference = - fac.newReference( - "#xpointer(//*[@authenticate='true'])", - fac.newDigestMethod(DigestMethod.SHA256, null), - listOf(c14n), - null, - null - ) - val canon: CanonicalizationMethod = - fac.newCanonicalizationMethod(CanonicalizationMethod.INCLUSIVE, null as C14NMethodParameterSpec?) - val signatureMethod = fac.newSignatureMethod("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", null) - val si: SignedInfo = fac.newSignedInfo(canon, signatureMethod, listOf(ref)) - val sig: XMLSignature = fac.newXMLSignature(si, null) - val dsc = DOMSignContext(signingPriv, authSigNode) - dsc.defaultNamespacePrefix = "ds" - dsc.uriDereferencer = EbicsSigUriDereferencer() - - sig.sign(dsc) - - val innerSig = authSigNode.firstChild - while (innerSig.hasChildNodes()) { - authSigNode.appendChild(innerSig.firstChild) - } - authSigNode.removeChild(innerSig) - } - - fun verifyEbicsDocument(doc: Document, signingPub: PublicKey): Boolean { - val xpath = XPathFactory.newInstance().newXPath() - xpath.namespaceContext = object : NamespaceContext { - override fun getNamespaceURI(p0: String?): String { - return when (p0) { - "ebics" -> "urn:org:ebics:H004" - else -> throw IllegalArgumentException() - } - } - - override fun getPrefix(p0: String?): String { - throw UnsupportedOperationException() - } - - override fun getPrefixes(p0: String?): MutableIterator<String> { - throw UnsupportedOperationException() - } - } - val doc2: Document = doc.cloneNode(true) as Document - val authSigNode = xpath.compile("/*[1]/ebics:AuthSignature").evaluate(doc2, XPathConstants.NODE) - if (authSigNode !is Node) - throw java.lang.Exception("no AuthSignature") - val sigEl = doc2.createElementNS("http://www.w3.org/2000/09/xmldsig#", "ds:Signature") - authSigNode.parentNode.insertBefore(sigEl, authSigNode) - while (authSigNode.hasChildNodes()) { - sigEl.appendChild(authSigNode.firstChild) - } - authSigNode.parentNode.removeChild(authSigNode) - val fac = XMLSignatureFactory.getInstance("DOM") - val dvc = DOMValidateContext(signingPub, sigEl) - dvc.uriDereferencer = EbicsSigUriDereferencer() - val sig = fac.unmarshalXMLSignature(dvc) - // FIXME: check that parameters are okay!s - val valResult = sig.validate(dvc) - sig.signedInfo.references[0].validate(dvc) - return valResult - } - } -} diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/CryptoUtil.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/CryptoUtil.kt @@ -0,0 +1,174 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2019 Stanisci and Dold. + + * LibEuFin is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation; either version 3, or + * (at your option) any later version. + + * LibEuFin is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General + * Public License for more details. + + * You should have received a copy of the GNU Affero General Public + * License along with LibEuFin; see the file COPYING. If not, see + * <http://www.gnu.org/licenses/> + */ + +package tech.libeufin.sandbox + +import org.bouncycastle.jce.provider.BouncyCastleProvider +import java.io.ByteArrayOutputStream +import java.lang.Exception +import java.math.BigInteger +import java.security.KeyFactory +import java.security.KeyPairGenerator +import java.security.interfaces.RSAPrivateCrtKey +import java.security.interfaces.RSAPublicKey +import java.security.spec.PKCS8EncodedKeySpec +import java.security.spec.RSAPublicKeySpec +import java.security.spec.X509EncodedKeySpec +import javax.crypto.Cipher +import javax.crypto.KeyGenerator +import java.security.MessageDigest +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + + +/** + * RSA key pair. + */ +data class RsaCrtKeyPair(val private: RSAPrivateCrtKey, val public: RSAPublicKey) + +/** + * Helpers for dealing with crypographic operations in EBICS / LibEuFin. + */ +class CryptoUtil { + + class EncryptionResult( + val encryptedTransactionKey: ByteArray, + val pubKeyDigest: ByteArray, + val encryptedData: ByteArray + ) + + companion object { + private val bouncyCastleProvider = BouncyCastleProvider() + + /** + * Load an RSA private key from its binary PKCS#8 encoding. + */ + fun loadRsaPrivateKey(encodedPrivateKey: ByteArray): RSAPrivateCrtKey { + val spec = PKCS8EncodedKeySpec(encodedPrivateKey) + val priv = KeyFactory.getInstance("RSA").generatePrivate(spec) + if (priv !is RSAPrivateCrtKey) + throw Exception("wrong encoding") + return priv + } + + /** + * Load an RSA public key from its binary X509 encoding. + */ + fun loadRsaPublicKey(encodedPublicKey: ByteArray): RSAPublicKey { + val spec = X509EncodedKeySpec(encodedPublicKey) + val pub = KeyFactory.getInstance("RSA").generatePublic(spec) + if (pub !is RSAPublicKey) + throw Exception("wrong encoding") + return pub + } + + /** + * Load an RSA public key from its binary X509 encoding. + */ + fun getRsaPublicFromPrivate(rsaPrivateCrtKey: RSAPrivateCrtKey): RSAPublicKey { + val spec = RSAPublicKeySpec(rsaPrivateCrtKey.modulus, rsaPrivateCrtKey.publicExponent) + val pub = KeyFactory.getInstance("RSA").generatePublic(spec) + if (pub !is RSAPublicKey) + throw Exception("wrong encoding") + return pub + } + + /** + * Generate a fresh RSA key pair. + * + * @param nbits size of the modulus in bits + */ + fun generateRsaKeyPair(nbits: Int): RsaCrtKeyPair { + val gen = KeyPairGenerator.getInstance("RSA") + gen.initialize(nbits) + val pair = gen.genKeyPair() + val priv = pair.private + val pub = pair.public + if (priv !is RSAPrivateCrtKey) + throw Exception("key generation failed") + if (pub !is RSAPublicKey) + throw Exception("key generation failed") + return RsaCrtKeyPair(priv, pub) + } + + /** + * Load an RSA public key from its components. + * + * @param exponent + * @param modulus + * @return key + */ + fun loadRsaPublicKeyFromComponents(modulus: ByteArray, exponent: ByteArray): RSAPublicKey { + val modulusBigInt = BigInteger(1, modulus) + val exponentBigInt = BigInteger(1, exponent) + + val keyFactory = KeyFactory.getInstance("RSA") + val tmp = RSAPublicKeySpec(modulusBigInt, exponentBigInt) + return keyFactory.generatePublic(tmp) as RSAPublicKey + } + + /** + * Hash an RSA public key according to the EBICS standard (EBICS 2.5: 4.4.1.2.3). + */ + fun getEbicsPublicKeyHash(publicKey: RSAPublicKey): ByteArray { + val keyBytes = ByteArrayOutputStream() + keyBytes.writeBytes(publicKey.publicExponent.toByteArray().toHexString().toByteArray()) + keyBytes.write(' '.toInt()) + keyBytes.writeBytes(publicKey.modulus.toByteArray().toHexString().toByteArray()) + val digest = MessageDigest.getInstance("SHA-256") + return digest.digest(keyBytes.toByteArray()) + } + + /** + * Encrypt data according to the EBICS E002 encryption process. + */ + fun encryptEbicsE002(data: ByteArray, encryptionPublicKey: RSAPublicKey): EncryptionResult { + val keygen = KeyGenerator.getInstance("AES", bouncyCastleProvider) + keygen.init(128) + val transactionKey = keygen.generateKey() + val symmetricCipher = Cipher.getInstance("AES/CBC/X9.23Padding", bouncyCastleProvider) + val ivParameterSpec = IvParameterSpec(ByteArray(16)) + symmetricCipher.init(Cipher.ENCRYPT_MODE, transactionKey, ivParameterSpec) + val encryptedData = symmetricCipher.doFinal(data) + val asymmetricCipher = Cipher.getInstance("RSA/None/PKCS1Padding", bouncyCastleProvider) + asymmetricCipher.init(Cipher.ENCRYPT_MODE, encryptionPublicKey) + val encryptedTransactionKey = asymmetricCipher.doFinal(transactionKey.encoded) + val pubKeyDigest = getEbicsPublicKeyHash(encryptionPublicKey) + return EncryptionResult(encryptedTransactionKey, pubKeyDigest, encryptedData) + } + + fun decryptEbicsE002(enc: EncryptionResult, privateKey: RSAPrivateCrtKey): ByteArray { + val asymmetricCipher = Cipher.getInstance("RSA/None/PKCS1Padding", bouncyCastleProvider) + asymmetricCipher.init(Cipher.DECRYPT_MODE, privateKey) + val transactionKeyBytes = asymmetricCipher.doFinal(enc.encryptedTransactionKey) + val secretKeySpec = SecretKeySpec(transactionKeyBytes, "AES") + val symmetricCipher = Cipher.getInstance("AES/CBC/X9.23Padding", bouncyCastleProvider) + val ivParameterSpec = IvParameterSpec(ByteArray(16)) + symmetricCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec) + 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 @@ -0,0 +1,187 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2019 Stanisci and Dold. + + * LibEuFin is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation; either version 3, or + * (at your option) any later version. + + * LibEuFin is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General + * Public License for more details. + + * You should have received a copy of the GNU Affero General Public + * License along with LibEuFin; see the file COPYING. If not, see + * <http://www.gnu.org/licenses/> + */ + +package tech.libeufin.sandbox + +import org.jetbrains.exposed.dao.* +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.transactions.transaction +import java.sql.Blob + +const val CUSTOMER_NAME_MAX_LENGTH = 20 +const val EBICS_HOST_ID_MAX_LENGTH = 10 +const val EBICS_USER_ID_MAX_LENGTH = 10 +const val EBICS_PARTNER_ID_MAX_LENGTH = 10 +const val EBICS_SYSTEM_ID_MAX_LENGTH = 10 +/** + * All the states to give a subscriber. + */ +enum class SubscriberState { + /** + * No keys at all given to the bank. + */ + NEW, + + /** + * Only INI electronic message was successfully sent. + */ + PARTIALLY_INITIALIZED_INI, + + /**r + * Only HIA electronic message was successfully sent. + */ + PARTIALLY_INITIALIZED_HIA, + + /** + * Both INI and HIA were electronically sent with success. + */ + INITIALIZED, + + /** + * All the keys accounted in INI and HIA have been confirmed + * via physical mail. + */ + READY +} + +/** + * All the states that one key can be assigned. + */ +enum class KeyState { + + /** + * The key was never communicated. + */ + MISSING, + + /** + * The key has been electronically sent. + */ + NEW, + + /** + * The key has been confirmed (either via physical mail + * or electronically -- e.g. with certificates) + */ + RELEASED +} + +fun Blob.toByteArray(): ByteArray { + return this.binaryStream.readAllBytes() +} + +/** + * This table information *not* related to EBICS, for all + * its customers. + */ +object BankCustomers: IntIdTable() { + // Customer ID is the default 'id' field provided by the constructor. + val name = varchar("name", CUSTOMER_NAME_MAX_LENGTH).primaryKey() + val ebicsSubscriber = reference("ebicsSubscriber", EbicsSubscribers) +} + +class BankCustomer(id: EntityID<Int>) : IntEntity(id) { + companion object : IntEntityClass<BankCustomer>(BankCustomers) + + var name by BankCustomers.name + var ebicsSubscriber by EbicsSubscriber referencedOn BankCustomers.ebicsSubscriber +} + + +/** + * This table stores RSA public keys of subscribers. + */ +object EbicsPublicKeys : IntIdTable() { + val rsaPublicKey = blob("rsaPublicKey") + val state = enumeration("state", KeyState::class) +} + + +/** + * Definition of a row in the [EbicsPublicKey] table + */ +class EbicsPublicKey(id: EntityID<Int>) : IntEntity(id) { + companion object : IntEntityClass<EbicsPublicKey>(EbicsPublicKeys) + var rsaPublicKey by EbicsPublicKeys.rsaPublicKey + var state by EbicsPublicKeys.state +} + + +object EbicsHosts : IntIdTable() { + val hostID = text("hostID") + val ebicsVersion = text("ebicsVersion") + val signaturePrivateKey = blob("signaturePrivateKey") + val encryptionPrivateKey = blob("encryptionPrivateKey") + val authenticationPrivateKey = blob("authenticationPrivateKey") +} + + +class EbicsHost(id: EntityID<Int>) : IntEntity(id) { + companion object : IntEntityClass<EbicsHost>(EbicsHosts) + var hostId by EbicsHosts.hostID + var ebicsVersion by EbicsHosts.ebicsVersion + var signaturePrivateKey by EbicsHosts.signaturePrivateKey + var encryptionPrivateKey by EbicsHosts.encryptionPrivateKey + var authenticationPrivateKey by EbicsHosts.authenticationPrivateKey +} + +/** + * Subscribers table. This table associates users with partners + * and systems. Each value can appear multiple times in the same column. + */ +object EbicsSubscribers: IntIdTable() { + val userId = text("userID") + val partnerId = text("partnerID") + val systemId = text("systemID").nullable() + + val signatureKey = reference("signatureKey", EbicsPublicKeys).nullable() + val encryptionKey = reference("encryptionKey", EbicsPublicKeys).nullable() + val authenticationKey = reference("authorizationKey", EbicsPublicKeys).nullable() + + val state = enumeration("state", SubscriberState::class) +} + +class EbicsSubscriber(id: EntityID<Int>) : IntEntity(id) { + companion object : IntEntityClass<EbicsSubscriber>(EbicsSubscribers) + + var userId by EbicsSubscribers.userId + var partnerId by EbicsSubscribers.partnerId + var systemId by EbicsSubscribers.systemId + + var signatureKey by EbicsPublicKey optionalReferencedOn EbicsSubscribers.signatureKey + var encryptionKey by EbicsPublicKey optionalReferencedOn EbicsSubscribers.encryptionKey + var authenticationKey by EbicsPublicKey optionalReferencedOn EbicsSubscribers.authenticationKey + + var state by EbicsSubscribers.state +} + + +fun dbCreateTables() { + Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", driver = "org.h2.Driver") + + transaction { + // addLogger(StdOutSqlLogger) + + SchemaUtils.create( + BankCustomers, + EbicsSubscribers, + EbicsHosts + ) + } +} diff --git a/sandbox/src/main/kotlin/JSON.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/JSON.kt diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt @@ -0,0 +1,468 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2019 Stanisci and Dold. + + * LibEuFin is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation; either version 3, or + * (at your option) any later version. + + * LibEuFin is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General + * Public License for more details. + + * You should have received a copy of the GNU Affero General Public + * License along with LibEuFin; see the file COPYING. If not, see + * <http://www.gnu.org/licenses/> + */ + +package tech.libeufin.sandbox + +import io.ktor.application.ApplicationCall +import io.ktor.application.ApplicationCallPipeline +import io.ktor.application.call +import io.ktor.application.install +import io.ktor.features.CallLogging +import io.ktor.features.ContentNegotiation +import io.ktor.features.StatusPages +import io.ktor.gson.gson +import io.ktor.http.ContentType +import io.ktor.http.HttpStatusCode +import io.ktor.request.receive +import io.ktor.request.receiveText +import io.ktor.request.uri +import io.ktor.response.respond +import io.ktor.response.respondText +import io.ktor.routing.get +import io.ktor.routing.post +import io.ktor.routing.routing +import io.ktor.server.engine.embeddedServer +import io.ktor.server.netty.Netty +import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.transactions.transaction +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.w3c.dom.Document +import tech.libeufin.schema.ebics_h004.EbicsKeyManagementResponse +import tech.libeufin.schema.ebics_h004.EbicsNoPubKeyDigestsRequest +import tech.libeufin.schema.ebics_h004.EbicsUnsecuredRequest +import tech.libeufin.schema.ebics_h004.HIARequestOrderDataType +import tech.libeufin.schema.ebics_hev.HEVResponse +import tech.libeufin.schema.ebics_hev.SystemReturnCodeType +import tech.libeufin.schema.ebics_s001.SignaturePubKeyOrderData +import java.nio.charset.StandardCharsets.US_ASCII +import java.nio.charset.StandardCharsets.UTF_8 +import java.security.interfaces.RSAPublicKey +import java.text.DateFormat +import java.util.zip.InflaterInputStream +import javax.sql.rowset.serial.SerialBlob + +val logger: Logger = LoggerFactory.getLogger("tech.libeufin.sandbox") + +data class EbicsRequestError(val statusCode: HttpStatusCode) : Exception("Ebics request error") + +private suspend fun ApplicationCall.respondEbicsKeyManagement( + errorText: String, + errorCode: String, + statusCode: HttpStatusCode, + bankReturnCode: String, + orderId: String? = null +) { + val responseXml = EbicsKeyManagementResponse().apply { + version = "H004" + header = EbicsKeyManagementResponse.Header().apply { + authenticate = true + mutable = EbicsKeyManagementResponse.Header.KeyManagementResponseMutableHeaderType().apply { + reportText = errorText + returnCode = errorCode + if (orderId != null) { + this.orderID = orderId + } + } + _static = EbicsKeyManagementResponse.Header.EmptyStaticHeader() + } + body = EbicsKeyManagementResponse.Body().apply { + this.returnCode = EbicsKeyManagementResponse.Body.ReturnCode().apply { + this.authenticate = true + this.value = bankReturnCode + } + } + } + val text = XMLUtil.convertJaxbToString(responseXml) + logger.info("responding with:\n${text}") + respondText(text, ContentType.Application.Xml, statusCode) +} + + +private suspend fun ApplicationCall.respondEbicsInvalidXml() { + respondEbicsKeyManagement("[EBICS_INVALID_XML]", "091010", HttpStatusCode.BadRequest, "000000") +} + + +fun findEbicsSubscriber(partnerID: String, userID: String, systemID: String?): EbicsSubscriber? { + return if (systemID == null) { + EbicsSubscriber.find { + (EbicsSubscribers.partnerId eq partnerID) and (EbicsSubscribers.userId eq userID) + } + } else { + EbicsSubscriber.find { + (EbicsSubscribers.partnerId eq partnerID) and + (EbicsSubscribers.userId eq userID) and + (EbicsSubscribers.systemId eq systemID) + } + }.firstOrNull() +} + +data class SubscriberKeys( + val authenticationPublicKey: RSAPublicKey, + val encryptionPublicKey: RSAPublicKey, + val signaturePublicKey: RSAPublicKey +) + +private suspend fun ApplicationCall.ebicsweb() { + val body: String = receiveText() + logger.debug("Data received: $body") + + val bodyDocument: Document? = XMLUtil.parseStringIntoDom(body) + + if (bodyDocument == null || (!XMLUtil.validateFromDom(bodyDocument))) { + respondEbicsInvalidXml() + return + } + + logger.info("Processing ${bodyDocument.documentElement.localName}") + + when (bodyDocument.documentElement.localName) { + "ebicsUnsecuredRequest" -> { + + val bodyJaxb = XMLUtil.convertDomToJaxb( + EbicsUnsecuredRequest::class.java, + bodyDocument + ) + + val staticHeader = bodyJaxb.value.header.static + val requestHostID = bodyJaxb.value.header.static.hostID + + val ebicsHost = transaction { + EbicsHost.find { EbicsHosts.hostID eq requestHostID }.firstOrNull() + } + + if (ebicsHost == null) { + logger.warn("client requested unknown HostID") + respondEbicsKeyManagement("[EBICS_INVALID_HOST_ID]", "091011", HttpStatusCode.NotFound, "000000") + return + } + + logger.info("Serving a ${bodyJaxb.value.header.static.orderDetails.orderType} request") + + /** + * NOTE: the JAXB interface has some automagic mechanism that decodes + * the Base64 string into its byte[] form _at the same time_ it instantiates + * the object; in other words, there is no need to perform here the decoding. + */ + val zkey = bodyJaxb.value.body.dataTransfer.orderData.value + + /** + * The validation enforces zkey to be a base64 value, but does not check + * whether it is given _empty_ or not; will check explicitly here. FIXME: + * shall the schema be patched to avoid having this if-block here? + */ + if (zkey.isEmpty()) { + logger.info("0-length key element given, invalid request") + respondEbicsInvalidXml() + return + } + + /** + * This value holds the bytes[] of a XML "SignaturePubKeyOrderData" document + * and at this point is valid and _never_ empty. + */ + val inflater = InflaterInputStream(zkey.inputStream()) + + var payload = try { + ByteArray(1) { inflater.read().toByte() } + } catch (e: Exception) { + e.printStackTrace() + respondEbicsInvalidXml() + return + } + + while (inflater.available() == 1) { + payload += inflater.read().toByte() + } + + inflater.close() + + logger.debug("Found payload: ${payload.toString(US_ASCII)}") + + when (bodyJaxb.value.header.static.orderDetails.orderType) { + "INI" -> { + val keyObject = XMLUtil.convertStringToJaxb<SignaturePubKeyOrderData>(payload.toString(UTF_8)) + + val rsaPublicKey: RSAPublicKey = try { + CryptoUtil.loadRsaPublicKeyFromComponents( + keyObject.value.signaturePubKeyInfo.pubKeyValue.rsaKeyValue.modulus, + keyObject.value.signaturePubKeyInfo.pubKeyValue.rsaKeyValue.exponent + ) + } catch (e: Exception) { + logger.info("User gave bad key, not storing it") + e.printStackTrace() + respondEbicsInvalidXml() + return + } + + // put try-catch block here? (FIXME) + transaction { + val ebicsSubscriber = + findEbicsSubscriber(staticHeader.partnerID, staticHeader.userID, staticHeader.systemID) + if (ebicsSubscriber == null) { + logger.warn("ebics subscriber ('${staticHeader.partnerID}' / '${staticHeader.userID}' / '${staticHeader.systemID}') not found") + throw EbicsRequestError(HttpStatusCode.NotFound) + } + ebicsSubscriber.signatureKey = EbicsPublicKey.new { + this.rsaPublicKey = SerialBlob(rsaPublicKey.encoded) + state = KeyState.NEW + } + + if (ebicsSubscriber.state == SubscriberState.NEW) { + ebicsSubscriber.state = SubscriberState.PARTIALLY_INITIALIZED_INI + } + + if (ebicsSubscriber.state == SubscriberState.PARTIALLY_INITIALIZED_HIA) { + ebicsSubscriber.state = SubscriberState.INITIALIZED + } + } + + logger.info("Signature key inserted in database _and_ subscriber state changed accordingly") + respondEbicsKeyManagement( + "[EBICS_OK]", + "000000", + HttpStatusCode.OK, + bankReturnCode = "000000", + orderId = "OR01" + ) + return + } + + "HIA" -> { + val keyObject = XMLUtil.convertStringToJaxb<HIARequestOrderDataType>(payload.toString(US_ASCII)) + + val authenticationPublicKey = try { + CryptoUtil.loadRsaPublicKeyFromComponents( + keyObject.value.authenticationPubKeyInfo.pubKeyValue.rsaKeyValue.modulus, + keyObject.value.authenticationPubKeyInfo.pubKeyValue.rsaKeyValue.exponent + ) + } catch (e: Exception) { + logger.info("auth public key invalid") + e.printStackTrace() + respondEbicsInvalidXml() + return + } + + val encryptionPublicKey = try { + CryptoUtil.loadRsaPublicKeyFromComponents( + keyObject.value.encryptionPubKeyInfo.pubKeyValue.rsaKeyValue.modulus, + keyObject.value.encryptionPubKeyInfo.pubKeyValue.rsaKeyValue.exponent + ) + } catch (e: Exception) { + logger.info("auth public key invalid") + e.printStackTrace() + respondEbicsInvalidXml() + return + } + + transaction { + val ebicsSubscriber = + findEbicsSubscriber(staticHeader.partnerID, staticHeader.userID, staticHeader.systemID) + if (ebicsSubscriber == null) { + logger.warn("ebics subscriber not found") + throw EbicsRequestError(HttpStatusCode.NotFound) + } + ebicsSubscriber.authenticationKey = EbicsPublicKey.new { + this.rsaPublicKey = SerialBlob(authenticationPublicKey.encoded) + state = KeyState.NEW + } + ebicsSubscriber.encryptionKey = EbicsPublicKey.new { + this.rsaPublicKey = SerialBlob(encryptionPublicKey.encoded) + state = KeyState.NEW + } + + if (ebicsSubscriber.state == SubscriberState.NEW) { + ebicsSubscriber.state = SubscriberState.PARTIALLY_INITIALIZED_HIA + } + + if (ebicsSubscriber.state == SubscriberState.PARTIALLY_INITIALIZED_INI) { + ebicsSubscriber.state = SubscriberState.INITIALIZED + } + } + respondEbicsKeyManagement("[EBICS_OK]", "000000", HttpStatusCode.OK, "000000") + return + } + } + + throw AssertionError("not reached") + } + + "ebicsHEVRequest" -> { + val hevResponse = HEVResponse().apply { + this.systemReturnCode = SystemReturnCodeType().apply { + this.reportText = "[EBICS_OK]" + this.returnCode = "000000" + } + this.versionNumber = listOf(HEVResponse.VersionNumber.create("H004", "02.50")) + } + + val strResp = XMLUtil.convertJaxbToString(hevResponse) + respondText(strResp, ContentType.Application.Xml, HttpStatusCode.OK) + return + } + "ebicsNoPubKeyDigestsRequest" -> { + val requestJaxb = XMLUtil.convertDomToJaxb(EbicsNoPubKeyDigestsRequest::class.java, bodyDocument) + val staticHeader = requestJaxb.value.header.static + when (val orderType = staticHeader.orderDetails.orderType) { + "HPB" -> { + val subscriberKeys = transaction { + val ebicsSubscriber = + findEbicsSubscriber(staticHeader.partnerID, staticHeader.userID, staticHeader.systemID) + if (ebicsSubscriber == null) { + throw EbicsRequestError(HttpStatusCode.Unauthorized) + } + if (ebicsSubscriber.state != SubscriberState.INITIALIZED) { + throw EbicsRequestError(HttpStatusCode.Forbidden) + } + val authPubBlob = ebicsSubscriber.authenticationKey!!.rsaPublicKey + val encPubBlob = ebicsSubscriber.encryptionKey!!.rsaPublicKey + val sigPubBlob = ebicsSubscriber.signatureKey!!.rsaPublicKey + SubscriberKeys( + CryptoUtil.loadRsaPublicKey(authPubBlob.toByteArray()), + CryptoUtil.loadRsaPublicKey(encPubBlob.toByteArray()), + CryptoUtil.loadRsaPublicKey(sigPubBlob.toByteArray()) + ) + } + val validationResult = + XMLUtil.verifyEbicsDocument(bodyDocument, subscriberKeys.authenticationPublicKey) + logger.info("validationResult: $validationResult") + } + else -> { + logger.warn("order type '${orderType}' not supported for ebicsNoPubKeyDigestsRequest") + respondEbicsInvalidXml() + } + } + } + else -> { + /* Log to console and return "unknown type" */ + logger.info("Unknown message, just logging it!") + respond( + HttpStatusCode.NotImplemented, + SandboxError("Not Implemented") + ) + return + } + } +} + + +fun main() { + dbCreateTables() + + transaction { + val pairA = CryptoUtil.generateRsaKeyPair(2048) + val pairB = CryptoUtil.generateRsaKeyPair(2048) + val pairC = CryptoUtil.generateRsaKeyPair(2048) + EbicsHost.new { + hostId = "host01" + ebicsVersion = "H004" + authenticationPrivateKey = SerialBlob(pairA.private.encoded) + encryptionPrivateKey = SerialBlob(pairB.private.encoded) + signaturePrivateKey = SerialBlob(pairC.private.encoded) + } + + EbicsSubscriber.new { + partnerId = "PARTNER1" + userId = "USER1" + systemId = null + state = SubscriberState.NEW + } + } + + val server = embeddedServer(Netty, port = 5000) { + install(CallLogging) + install(ContentNegotiation) { + gson { + setDateFormat(DateFormat.LONG) + setPrettyPrinting() + } + } + install(StatusPages) { + exception<Throwable> { cause -> + logger.error("Exception while handling '${call.request.uri.toString()}'", cause) + call.respondText("Internal server error.", ContentType.Text.Plain, HttpStatusCode.InternalServerError) + } + } + intercept(ApplicationCallPipeline.Fallback) { + if (this.call.response.status() == null) { + call.respondText("Not found (no route matched).\n", ContentType.Text.Plain, HttpStatusCode.NotFound) + return@intercept finish() + } + } + routing { + //trace { logger.info(it.buildText()) } + get("/") { + call.respondText("Hello LibEuFin!\n", ContentType.Text.Plain) + } + get("/ebics/hosts") { + val ebicsHosts = transaction { + EbicsHost.all().map { it.hostId } + } + call.respond(EbicsHostsResponse(ebicsHosts)) + } + post("/ebics/hosts") { + val req = call.receive<EbicsHostCreateRequest>() + transaction { + EbicsHost.new { + this.ebicsVersion = req.ebicsVersion + this.hostId = hostId + } + } + } + get("/ebics/hosts/{id}") { + val resp = transaction { + val host = EbicsHost.find { EbicsHosts.hostID eq call.parameters["id"]!! }.firstOrNull() + if (host == null) null + else EbicsHostResponse(host.hostId, host.ebicsVersion) + } + if (resp == null) call.respond( + HttpStatusCode.NotFound, + SandboxError("host not found") + ) + else call.respond(resp) + } + get("/ebics/subscribers") { + val subscribers = transaction { + EbicsSubscriber.all().map { it.id.value.toString() } + } + call.respond(EbicsSubscribersResponse(subscribers)) + } + get("/ebics/subscribers/{id}") { + val resp = transaction { + val id = call.parameters["id"]!! + val subscriber = EbicsSubscriber.findById(id.toInt())!! + EbicsSubscriberResponse( + id, + subscriber.partnerId, + subscriber.userId, + subscriber.systemId, + subscriber.state.name + ) + } + call.respond(resp) + } + post("/ebicsweb") { + call.ebicsweb() + } + } + } + logger.info("Up and running") + server.start(wait = true) +} diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/XMLUtil.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/XMLUtil.kt @@ -0,0 +1,386 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2019 Stanisci and Dold. + + * LibEuFin is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation; either version 3, or + * (at your option) any later version. + + * LibEuFin is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General + * Public License for more details. + + * You should have received a copy of the GNU Affero General Public + * License along with LibEuFin; see the file COPYING. If not, see + * <http://www.gnu.org/licenses/> + */ + +package tech.libeufin.sandbox + +import com.sun.org.apache.xerces.internal.dom.DOMInputImpl +import org.apache.xml.security.c14n.Canonicalizer +import org.w3c.dom.Document +import org.w3c.dom.Node +import org.w3c.dom.NodeList +import org.w3c.dom.ls.LSInput +import org.w3c.dom.ls.LSResourceResolver +import org.xml.sax.ErrorHandler +import org.xml.sax.InputSource +import org.xml.sax.SAXException +import org.xml.sax.SAXParseException +import java.io.* +import java.security.PrivateKey +import java.security.PublicKey +import javax.xml.XMLConstants +import javax.xml.bind.JAXBContext +import javax.xml.bind.JAXBElement +import javax.xml.bind.Marshaller +import javax.xml.crypto.* +import javax.xml.crypto.dom.DOMURIReference +import javax.xml.crypto.dsig.* +import javax.xml.crypto.dsig.dom.DOMSignContext +import javax.xml.crypto.dsig.dom.DOMValidateContext +import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec +import javax.xml.crypto.dsig.spec.TransformParameterSpec +import javax.xml.namespace.NamespaceContext +import javax.xml.parsers.DocumentBuilderFactory +import javax.xml.transform.OutputKeys +import javax.xml.transform.Source +import javax.xml.transform.TransformerFactory +import javax.xml.transform.dom.DOMSource +import javax.xml.transform.stream.StreamResult +import javax.xml.transform.stream.StreamSource +import javax.xml.validation.SchemaFactory +import javax.xml.validation.Validator +import javax.xml.xpath.XPath +import javax.xml.xpath.XPathConstants +import javax.xml.xpath.XPathFactory + +/** + * Helpers for dealing with XML in EBICS. + */ +class XMLUtil private constructor() { + /** + * This URI dereferencer allows handling the resource reference used for + * XML signatures in EBICS. + */ + private class EbicsSigUriDereferencer : URIDereferencer { + override fun dereference(myRef: URIReference?, myCtx: XMLCryptoContext?): Data { + val ebicsXpathExpr = "//*[@authenticate='true']" + if (myRef !is DOMURIReference) + throw Exception("invalid type") + 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) + if (nodeSet !is NodeList) + throw Exception("invalid type") + if (nodeSet.length <= 0) { + throw Exception("no nodes to sign") + } + val bytes = ByteArrayOutputStream() + 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) + } + return OctetStreamData(ByteArrayInputStream(bytes.toByteArray())) + } + } + + /** + * Validator for EBICS messages. + */ + private val validator = try { + + } catch (e: SAXException) { + e.printStackTrace() + throw e + } + + companion object { + + private var cachedEbicsValidator: Validator? = null + + private fun getEbicsValidator(): Validator { + val currentValidator = cachedEbicsValidator + if (currentValidator != null) + return currentValidator + val classLoader = ClassLoader.getSystemClassLoader() + val sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI) + sf.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "file") + sf.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "") + sf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true) + sf.errorHandler = object : ErrorHandler { + override fun warning(p0: SAXParseException?) { + println("Warning: $p0") + } + + override fun error(p0: SAXParseException?) { + println("Error: $p0") + } + + override fun fatalError(p0: SAXParseException?) { + println("Fatal error: $p0") + } + } + sf.resourceResolver = object : LSResourceResolver { + override fun resolveResource( + type: String?, + namespaceURI: String?, + publicId: String?, + systemId: String?, + baseUri: String? + ): LSInput? { + if (type != "http://www.w3.org/2001/XMLSchema") { + return null + } + val res = classLoader.getResourceAsStream("xsd/$systemId") ?: return null + return DOMInputImpl(publicId, systemId, baseUri, res, "UTF-8") + } + } + val schemaInputs: Array<Source> = listOf("xsd/ebics_H004.xsd", "xsd/ebics_hev.xsd").map { + val resUrl = classLoader.getResource(it) ?: throw FileNotFoundException("Schema file $it not found.") + StreamSource(File(resUrl.toURI())) + }.toTypedArray() + val bundle = sf.newSchema(schemaInputs) + val newValidator = bundle.newValidator() + cachedEbicsValidator = newValidator + return newValidator + } + + /** + * + * @param xmlDoc the XML document to validate + * @return true when validation passes, false otherwise + */ + fun validate(xmlDoc: StreamSource): Boolean { + try { + getEbicsValidator().validate(xmlDoc) + } catch (e: Exception) { + logger.warn("Validation failed: {}", e) + return false + } + return true; + } + + /** + * Validates the DOM against the Schema(s) of this object. + * @param domDocument DOM to validate + * @return true/false if the document is valid/invalid + */ + fun validateFromDom(domDocument: Document): Boolean { + try { + getEbicsValidator().validate(DOMSource(domDocument)) + } catch (e: SAXException) { + e.printStackTrace() + return false + } + return true + } + + /** + * Craft object to be passed to the XML validator. + * @param xmlString XML body, as read from the POST body. + * @return InputStream object, as wanted by the validator. + */ + fun validateFromString(xmlString: String): Boolean { + val xmlInputStream: InputStream = ByteArrayInputStream(xmlString.toByteArray()) + val xmlSource = StreamSource(xmlInputStream) + return validate(xmlSource) + } + + + inline fun <reified T> convertJaxbToString(obj: T): String { + val sw = StringWriter() + val jc = JAXBContext.newInstance(T::class.java) + val m = jc.createMarshaller() + m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true) + m.marshal(obj, sw) + return sw.toString() + } + + /** + * Convert a XML string to the JAXB representation. + * + * @param documentString the string to convert into JAXB. + * @return the JAXB object reflecting the original XML document. + */ + inline fun <reified T> convertStringToJaxb(documentString: String): JAXBElement<T> { + val jc = JAXBContext.newInstance(T::class.java) + val u = jc.createUnmarshaller() + return u.unmarshal( /* Marshalling the object into the document. */ + StreamSource(StringReader(documentString)), + T::class.java + ) + } + + /** + * Extract String from DOM. + * + * @param document the DOM to extract the string from. + * @return the final String, or null if errors occur. + */ + fun convertDomToString(document: Document): String { + /* Make Transformer. */ + val tf = TransformerFactory.newInstance() + val t = tf.newTransformer() + + //t.setOutputProperty(OutputKeys.INDENT, "yes") + + /* Make string writer. */ + val sw = StringWriter() + + /* Extract string. */ + t.transform(DOMSource(document), StreamResult(sw)) + return sw.toString() + } + + /** + * Convert a node to a string without the XML declaration or + * indentation. + */ + fun convertNodeToString(node: Node): String { + /* Make Transformer. */ + val tf = TransformerFactory.newInstance() + val t = tf.newTransformer() + + t.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); + + /* Make string writer. */ + val sw = StringWriter() + + /* Extract string. */ + t.transform(DOMSource(node), StreamResult(sw)) + return sw.toString() + } + + /** + * Convert a DOM document - of a XML document - to the JAXB representation. + * + * @param finalType class type of the output + * @param document the document to convert into JAXB. + * @return the JAXB object reflecting the original XML document. + */ + fun <T> convertDomToJaxb(finalType: Class<T>, document: Document): JAXBElement<T> { + + val jc = JAXBContext.newInstance(finalType) + + /* Marshalling the object into the document. */ + val m = jc.createUnmarshaller() + return m.unmarshal(document, finalType) // document "went" into Jaxb + } + + /** + * Parse string into XML DOM. + * @param xmlString the string to parse. + * @return the DOM representing @a xmlString + */ + fun parseStringIntoDom(xmlString: String): Document { + val factory = DocumentBuilderFactory.newInstance().apply { + isNamespaceAware = true + } + val xmlInputStream = ByteArrayInputStream(xmlString.toByteArray()) + val builder = factory.newDocumentBuilder() + return builder.parse(InputSource(xmlInputStream)) + } + + + /** + * Sign an EBICS document with the authentication and identity signature. + */ + fun signEbicsDocument(doc: Document, signingPriv: PrivateKey): Unit { + val xpath = XPathFactory.newInstance().newXPath() + xpath.namespaceContext = object : NamespaceContext { + override fun getNamespaceURI(p0: String?): String { + return when (p0) { + "ebics" -> "urn:org:ebics:H004" + else -> throw IllegalArgumentException() + } + } + + override fun getPrefix(p0: String?): String { + throw UnsupportedOperationException() + } + + override fun getPrefixes(p0: String?): MutableIterator<String> { + throw UnsupportedOperationException() + } + } + val authSigNode = xpath.compile("/*[1]/ebics:AuthSignature").evaluate(doc, XPathConstants.NODE) + if (authSigNode !is Node) + throw java.lang.Exception("no AuthSignature") + val fac = XMLSignatureFactory.getInstance("DOM") + val c14n = fac.newTransform(CanonicalizationMethod.INCLUSIVE, null as TransformParameterSpec?) + val ref: Reference = + fac.newReference( + "#xpointer(//*[@authenticate='true'])", + fac.newDigestMethod(DigestMethod.SHA256, null), + listOf(c14n), + null, + null + ) + val canon: CanonicalizationMethod = + fac.newCanonicalizationMethod(CanonicalizationMethod.INCLUSIVE, null as C14NMethodParameterSpec?) + val signatureMethod = fac.newSignatureMethod("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", null) + val si: SignedInfo = fac.newSignedInfo(canon, signatureMethod, listOf(ref)) + val sig: XMLSignature = fac.newXMLSignature(si, null) + val dsc = DOMSignContext(signingPriv, authSigNode) + dsc.defaultNamespacePrefix = "ds" + dsc.uriDereferencer = EbicsSigUriDereferencer() + + sig.sign(dsc) + + val innerSig = authSigNode.firstChild + while (innerSig.hasChildNodes()) { + authSigNode.appendChild(innerSig.firstChild) + } + authSigNode.removeChild(innerSig) + } + + fun verifyEbicsDocument(doc: Document, signingPub: PublicKey): Boolean { + val xpath = XPathFactory.newInstance().newXPath() + xpath.namespaceContext = object : NamespaceContext { + override fun getNamespaceURI(p0: String?): String { + return when (p0) { + "ebics" -> "urn:org:ebics:H004" + else -> throw IllegalArgumentException() + } + } + + override fun getPrefix(p0: String?): String { + throw UnsupportedOperationException() + } + + override fun getPrefixes(p0: String?): MutableIterator<String> { + throw UnsupportedOperationException() + } + } + val doc2: Document = doc.cloneNode(true) as Document + val authSigNode = xpath.compile("/*[1]/ebics:AuthSignature").evaluate(doc2, XPathConstants.NODE) + if (authSigNode !is Node) + throw java.lang.Exception("no AuthSignature") + val sigEl = doc2.createElementNS("http://www.w3.org/2000/09/xmldsig#", "ds:Signature") + authSigNode.parentNode.insertBefore(sigEl, authSigNode) + while (authSigNode.hasChildNodes()) { + sigEl.appendChild(authSigNode.firstChild) + } + authSigNode.parentNode.removeChild(authSigNode) + val fac = XMLSignatureFactory.getInstance("DOM") + val dvc = DOMValidateContext(signingPub, sigEl) + dvc.uriDereferencer = EbicsSigUriDereferencer() + val sig = fac.unmarshalXMLSignature(dvc) + // FIXME: check that parameters are okay!s + val valResult = sig.validate(dvc) + sig.signedInfo.references[0].validate(dvc) + return valResult + } + } +} diff --git a/sandbox/src/main/kotlin/tech/libeufin/schema/ebics_h004/EbicsMessages.kt b/sandbox/src/main/kotlin/tech/libeufin/schema/ebics_h004/EbicsMessages.kt @@ -581,4 +581,20 @@ class EbicsNoPubKeyDigestsRequest { @XmlAccessorType(XmlAccessType.NONE) class EmptyBody +} + + +@XmlAccessorType(XmlAccessType.NONE) +@XmlType(name = "", propOrder = ["authenticationPubKeyInfo", "encryptionPubKeyInfo", "hostID"]) +@XmlRootElement(name = "HPBResponseOrderData") +class HPBResponseOrderData { + @get:XmlElement(name = "AuthenticationPubKeyInfo", required = true) + lateinit var authenticationPubKeyInfo: AuthenticationPubKeyInfoType + + @get:XmlElement(name = "EncryptionPubKeyInfo", required = true) + lateinit var encryptionPubKeyInfo: EncryptionPubKeyInfoType + + @get:XmlElement(name = "HostID", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var hostID: String } \ No newline at end of file diff --git a/sandbox/src/test/kotlin/XmlUtilTest.kt b/sandbox/src/test/kotlin/XmlUtilTest.kt @@ -8,20 +8,18 @@ import javax.xml.transform.stream.StreamSource class XmlUtilTest { - val processor = tech.libeufin.sandbox.XMLUtil() - @Test fun hevValidation(){ val classLoader = ClassLoader.getSystemClassLoader() val hev = classLoader.getResourceAsStream("ebics_hev.xml") - assertTrue(processor.validate(StreamSource(hev))) + assertTrue(XMLUtil.validate(StreamSource(hev))) } @Test fun iniValidation(){ val classLoader = ClassLoader.getSystemClassLoader() val ini = classLoader.getResourceAsStream("ebics_ini_request_sample.xml") - assertTrue(processor.validate(StreamSource(ini))) + assertTrue(XMLUtil.validate(StreamSource(ini))) } @Test