commit 0800e6a51177dcfa6d15c4ff6dca7362603c10d4
parent e3307ec4f08ad31ac75baa22486d5d04bbdfdcfe
Author: Florian Dold <florian.dold@gmail.com>
Date: Mon, 4 Nov 2019 17:30:03 +0100
refactor package structure
Diffstat:
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