diff options
author | Antoine A <> | 2024-03-26 23:20:58 +0100 |
---|---|---|
committer | Antoine A <> | 2024-03-26 23:20:58 +0100 |
commit | 3a0df963d964e5f1dc8c3105c4ad689525441408 (patch) | |
tree | 00339cd54e668cbc4b814843e52405fe5f8057ab | |
parent | 5ce5347e053d1222a9ac929de2f5373dd22337ad (diff) | |
download | libeufin-3a0df963d964e5f1dc8c3105c4ad689525441408.tar.gz libeufin-3a0df963d964e5f1dc8c3105c4ad689525441408.tar.bz2 libeufin-3a0df963d964e5f1dc8c3105c4ad689525441408.zip |
Support EBICS 3 key management
7 files changed, 237 insertions, 148 deletions
diff --git a/common/build.gradle b/common/build.gradle index 439950d7..446910ec 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -19,6 +19,7 @@ dependencies { implementation("ch.qos.logback:logback-classic:1.5.3") // Crypto implementation("org.bouncycastle:bcprov-jdk18on:1.77") + implementation("org.bouncycastle:bcpkix-jdk18on:1.77") // Database helper implementation("org.postgresql:postgresql:$postgres_version") implementation("com.zaxxer:HikariCP:5.1.0") diff --git a/common/src/main/kotlin/crypto/utils.kt b/common/src/main/kotlin/crypto/utils.kt index 2f98e064..1228eb91 100644 --- a/common/src/main/kotlin/crypto/utils.kt +++ b/common/src/main/kotlin/crypto/utils.kt @@ -20,12 +20,21 @@ package tech.libeufin.common.crypto import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder +import org.bouncycastle.operator.ContentSigner +import org.bouncycastle.cert.jcajce.* +import org.bouncycastle.asn1.x509.* +import org.bouncycastle.asn1.x509.Extension +import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.asn1.ASN1ObjectIdentifier import java.io.ByteArrayOutputStream import java.io.InputStream import java.math.BigInteger +import java.util.* import java.security.* import java.security.interfaces.RSAPrivateCrtKey import java.security.interfaces.RSAPublicKey +import java.security.cert.* import java.security.spec.* import javax.crypto.* import javax.crypto.spec.IvParameterSpec @@ -53,7 +62,7 @@ object CryptoUtil { val plainTransactionKey: SecretKey ) - private val bouncyCastleProvider = BouncyCastleProvider() + private val provider = BouncyCastleProvider() /** * Load an RSA private key from its binary PKCS#8 encoding. @@ -93,6 +102,12 @@ object CryptoUtil { return keyFactory.generatePublic(tmp) as RSAPublicKey } + fun loadRsaPublicKeyFromCertificate(certificate: ByteArray): RSAPublicKey { + val cf = CertificateFactory.getInstance("X.509"); + val c = cf.generateCertificate(certificate.inputStream()); + return c.getPublicKey() as RSAPublicKey + } + /** * Load an RSA public key from its binary X509 encoding. */ @@ -104,6 +119,42 @@ object CryptoUtil { return pub } + fun certificateFromPrivate(rsaPrivateCrtKey: RSAPrivateCrtKey): X509Certificate { + val now = System.currentTimeMillis() + val calendar = Calendar.getInstance() + calendar.time = Date(now) + calendar.add(Calendar.YEAR, 1) // TODO certificate validity + + val builder = JcaX509v3CertificateBuilder( + X500Name("CN=test"), // TODO certificate CN + BigInteger(now.toString()), // TODO certificate serial number + Date(now), + calendar.time, + X500Name("CN=test"), + getRsaPublicFromPrivate(rsaPrivateCrtKey) + ) + + + builder.addExtension(Extension.keyUsage, true, KeyUsage( + KeyUsage.digitalSignature + or KeyUsage.nonRepudiation + or KeyUsage.keyEncipherment + or KeyUsage.dataEncipherment + or KeyUsage.keyAgreement + or KeyUsage.keyCertSign + or KeyUsage.cRLSign + or KeyUsage.encipherOnly + or KeyUsage.decipherOnly + )) + builder.addExtension(Extension.basicConstraints, true, BasicConstraints(true)) + + val certificate = JcaContentSignerBuilder("SHA256WithRSA").build(rsaPrivateCrtKey) + return JcaX509CertificateConverter() + .setProvider(provider) + .getCertificate(builder.build(certificate)) + + } + /** * Generate a fresh RSA key pair. * @@ -135,7 +186,7 @@ object CryptoUtil { } fun encryptEbicsE002(data: InputStream, encryptionPublicKey: RSAPublicKey): EncryptionResult { - val keygen = KeyGenerator.getInstance("AES", bouncyCastleProvider) + val keygen = KeyGenerator.getInstance("AES", provider) keygen.init(128) val transactionKey = keygen.generateKey() return encryptEbicsE002withTransactionKey( @@ -154,14 +205,14 @@ object CryptoUtil { ): EncryptionResult { val symmetricCipher = Cipher.getInstance( "AES/CBC/X9.23Padding", - bouncyCastleProvider + provider ) val ivParameterSpec = IvParameterSpec(ByteArray(16)) symmetricCipher.init(Cipher.ENCRYPT_MODE, transactionKey, ivParameterSpec) val encryptedData = CipherInputStream(data, symmetricCipher).readAllBytes() val asymmetricCipher = Cipher.getInstance( "RSA/None/PKCS1Padding", - bouncyCastleProvider + provider ) asymmetricCipher.init(Cipher.ENCRYPT_MODE, encryptionPublicKey) val encryptedTransactionKey = asymmetricCipher.doFinal(transactionKey.encoded) @@ -189,14 +240,14 @@ object CryptoUtil { ): CipherInputStream { val asymmetricCipher = Cipher.getInstance( "RSA/None/PKCS1Padding", - bouncyCastleProvider + provider ) asymmetricCipher.init(Cipher.DECRYPT_MODE, privateKey) val transactionKeyBytes = asymmetricCipher.doFinal(encryptedTransactionKey) val secretKeySpec = SecretKeySpec(transactionKeyBytes, "AES") val symmetricCipher = Cipher.getInstance( "AES/CBC/X9.23Padding", - bouncyCastleProvider + provider ) val ivParameterSpec = IvParameterSpec(ByteArray(16)) symmetricCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec) @@ -211,7 +262,7 @@ object CryptoUtil { * uses a hash internally. */ fun signEbicsA006(data: ByteArray, privateKey: RSAPrivateCrtKey): ByteArray { - val signature = Signature.getInstance("SHA256withRSA/PSS", bouncyCastleProvider) + val signature = Signature.getInstance("SHA256withRSA/PSS", provider) signature.setParameter(PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 32, 1)) signature.initSign(privateKey) signature.update(data) @@ -219,7 +270,7 @@ object CryptoUtil { } fun verifyEbicsA006(sig: ByteArray, data: ByteArray, publicKey: RSAPublicKey): Boolean { - val signature = Signature.getInstance("SHA256withRSA/PSS", bouncyCastleProvider) + val signature = Signature.getInstance("SHA256withRSA/PSS", provider) signature.setParameter(PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 32, 1)) signature.initVerify(publicKey) signature.update(data) diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt index e9bb3713..f9c83eba 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt @@ -110,7 +110,7 @@ suspend fun doKeysRequestAndUpdateState( orderType: KeysOrderType ) { logger.info("Doing key request ${orderType.name}") - val impl = EbicsKeyMng(cfg, privs) + val impl = EbicsKeyMng(cfg, privs, true) val req = when(orderType) { KeysOrderType.INI -> impl.INI() KeysOrderType.HIA -> impl.HIA() diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/XmlCombinators.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/XmlCombinators.kt index cfb93782..fbed0b7b 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/XmlCombinators.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/XmlCombinators.kt @@ -191,8 +191,8 @@ class XmlDestructor internal constructor(private val el: Element) { } fun <T> fromDoc(doc: Document, root: String, f: XmlDestructor.() -> T): T { - if (doc.documentElement.tagName != root) { - throw DestructionError("expected root '$root' got '${doc.documentElement.tagName}'") + if (doc.documentElement.localName != root) { + throw DestructionError("expected root '$root' got '${doc.documentElement.localName}'") } val destr = XmlDestructor(doc.documentElement) return f(destr) diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt index 3c8120d9..30bcd8de 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt @@ -55,11 +55,6 @@ import org.w3c.dom.Document import org.xml.sax.SAXException /** - * Available EBICS versions. - */ -enum class EbicsVersion { two, three } - -/** * Which documents can be downloaded via EBICS. */ enum class SupportedDocument { @@ -392,62 +387,4 @@ class EbicsResponse<T>( } return content } -} - -// TODO import missing using a script -@Suppress("SpellCheckingInspection") -enum class EbicsReturnCode(val code: String) { - EBICS_OK("000000"), - EBICS_DOWNLOAD_POSTPROCESS_DONE("011000"), - EBICS_DOWNLOAD_POSTPROCESS_SKIPPED("011001"), - EBICS_TX_SEGMENT_NUMBER_UNDERRUN("011101"), - EBICS_AUTHENTICATION_FAILED("061001"), - EBICS_INVALID_REQUEST("061002"), - EBICS_INTERNAL_ERROR("061099"), - EBICS_TX_RECOVERY_SYNC("061101"), - EBICS_AUTHORISATION_ORDER_IDENTIFIER_FAILED("090003"), - EBICS_INVALID_ORDER_DATA_FORMAT("090004"), - EBICS_NO_DOWNLOAD_DATA_AVAILABLE("090005"), - EBICS_INVALID_USER_OR_USER_STATE("091002"), - EBICS_USER_UNKNOWN("091003"), - EBICS_INVALID_USER_STATE("091004"), - EBICS_INVALID_ORDER_IDENTIFIER("091005"), - EBICS_UNSUPPORTED_ORDER_TYPE("091006"), - EBICS_INVALID_XML("091010"), - EBICS_TX_MESSAGE_REPLAY("091103"), - EBICS_TX_SEGMENT_NUMBER_EXCEEDED("091104"), - EBICS_INVALID_REQUEST_CONTENT("091113"), - EBICS_PROCESSING_ERROR("091116"), - EBICS_ACCOUNT_AUTHORISATION_FAILED("091302"), - EBICS_AMOUNT_CHECK_FAILED("091303"); - - enum class Kind { - Information, - Note, - Warning, - Error - } - - fun kind(): Kind { - return when (val errorClass = code.substring(0..1)) { - "00" -> Kind.Information - "01" -> Kind.Note - "03" -> Kind.Warning - "06", "09" -> Kind.Error - else -> throw Exception("Unknown EBICS status code error class: $errorClass") - } - } - - companion object { - fun lookup(code: String): EbicsReturnCode { - for (x in entries) { - if (x.code == code) { - return x - } - } - throw Exception( - "Unknown EBICS status code: $code" - ) - } - } }
\ No newline at end of file diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsConstants.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsConstants.kt new file mode 100644 index 00000000..fc217c2a --- /dev/null +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsConstants.kt @@ -0,0 +1,100 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2024 Taler Systems S.A. + + * 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.nexus.ebics + + +// TODO import missing using a script +@Suppress("SpellCheckingInspection") +enum class EbicsReturnCode(val code: String) { + EBICS_OK("000000"), + EBICS_DOWNLOAD_POSTPROCESS_DONE("011000"), + EBICS_DOWNLOAD_POSTPROCESS_SKIPPED("011001"), + EBICS_TX_SEGMENT_NUMBER_UNDERRUN("011101"), + EBICS_AUTHENTICATION_FAILED("061001"), + EBICS_INVALID_REQUEST("061002"), + EBICS_INTERNAL_ERROR("061099"), + EBICS_TX_RECOVERY_SYNC("061101"), + EBICS_AUTHORISATION_ORDER_IDENTIFIER_FAILED("090003"), + EBICS_INVALID_ORDER_DATA_FORMAT("090004"), + EBICS_NO_DOWNLOAD_DATA_AVAILABLE("090005"), + + // Transaction administration + EBICS_INVALID_USER_OR_USER_STATE("091002"), + EBICS_USER_UNKNOWN("091003"), + EBICS_INVALID_USER_STATE("091004"), + EBICS_INVALID_ORDER_IDENTIFIER("091005"), + EBICS_UNSUPPORTED_ORDER_TYPE("091006"), + EBICS_INVALID_XML("091010"), + EBICS_TX_MESSAGE_REPLAY("091103"), + EBICS_TX_SEGMENT_NUMBER_EXCEEDED("091104"), + EBICS_INVALID_REQUEST_CONTENT("091113"), + EBICS_PROCESSING_ERROR("091116"), + + + // Key-Management errors + EBICS_X509_WRONG_KEY_USAGE("091210"), + EBICS_X509_WRONG_ALGORITHM("091211"), + EBICS_X509_INVALID_THUMBPRINT("091212"), + EBICS_X509_CTL_INVALID("091213"), + EBICS_X509_UNKNOWN_CERTIFICATE_AUTHORITY("091214"), + EBICS_X509_INVALID_POLICY("091215"), + EBICS_X509_INVALID_BASIC_CONSTRAINTS("091216"), + EBICS_ONLY_X509_SUPPORT("091217"), + EBICS_KEYMGMT_DUPLICATE_KEY("091218"), + EBICS_CERTIFICATE_VALIDATION_ERROR("091219"), + + // Pre-erification errors + EBICS_SIGNATURE_VERIFICATION_FAILED("091301"), + EBICS_ACCOUNT_AUTHORISATION_FAILED("091302"), + EBICS_AMOUNT_CHECK_FAILED("091303"), + EBICS_SIGNER_UNKNOWN("091304"), + EBICS_INVALID_SIGNER_STATE("091305"), + EBICS_DUPLICATE_SIGNATURE("091306"); + + enum class Kind { + Information, + Note, + Warning, + Error + } + + fun kind(): Kind { + return when (val errorClass = code.substring(0..1)) { + "00" -> Kind.Information + "01" -> Kind.Note + "03" -> Kind.Warning + "06", "09" -> Kind.Error + else -> throw Exception("Unknown EBICS status code error class: $errorClass") + } + } + + companion object { + fun lookup(code: String): EbicsReturnCode { + for (x in entries) { + if (x.code == code) { + return x + } + } + throw Exception( + "Unknown EBICS status code: $code" + ) + } + } +}
\ No newline at end of file diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsKeyMng.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsKeyMng.kt index 666ed2ed..48d10d18 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsKeyMng.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsKeyMng.kt @@ -36,37 +36,22 @@ import java.security.interfaces.* /** EBICS protocol for key management */ class EbicsKeyMng( private val cfg: EbicsSetupConfig, - private val clientKeys: ClientPrivateKeysFile + private val clientKeys: ClientPrivateKeysFile, + private val ebics3: Boolean ) { + private val schema = if (ebics3) "H005" else "H004" fun INI(): ByteArray { - val inner = XMLOrderData(cfg, "SignaturePubKeyOrderData", "http://www.ebics.org/S001") { + val data = XMLOrderData(cfg, "SignaturePubKeyOrderData", "http://www.ebics.org/S00${if (ebics3) 2 else 1}") { el("SignaturePubKeyInfo") { RSAKeyXml(clientKeys.signature_private_key) el("SignatureVersion", "A006") } } - val doc = request("ebicsUnsecuredRequest") { - el("header") { - attr("authenticate", "true") - el("static") { - el("HostID", cfg.ebicsHostId) - el("PartnerID", cfg.ebicsPartnerId) - el("UserID", cfg.ebicsUserId) - el("OrderDetails") { - el("OrderType", "INI") - el("OrderAttribute", "DZNNN") - } - el("SecurityMedium", "0200") - } - el("mutable") - } - el("body/DataTransfer/OrderData", inner) - } - return XMLUtil.convertDomToBytes(doc) + return request("ebicsUnsecuredRequest", "INI", "0200", data) } fun HIA(): ByteArray { - val inner = XMLOrderData(cfg, "HIARequestOrderData", "urn:org:ebics:H004") { + val data = XMLOrderData(cfg, "HIARequestOrderData", "urn:org:ebics:$schema") { el("AuthenticationPubKeyInfo") { RSAKeyXml(clientKeys.authentication_private_key) el("AuthenticationVersion", "X002") @@ -76,69 +61,72 @@ class EbicsKeyMng( el("EncryptionVersion", "E002") } } - val doc = request("ebicsUnsecuredRequest") { - el("header") { - attr("authenticate", "true") - el("static") { - el("HostID", cfg.ebicsHostId) - el("PartnerID", cfg.ebicsPartnerId) - el("UserID", cfg.ebicsUserId) - el("OrderDetails") { - el("OrderType", "HIA") - el("OrderAttribute", "DZNNN") - } - el("SecurityMedium", "0200") - } - el("mutable") - } - el("body/DataTransfer/OrderData", inner) - } - return XMLUtil.convertDomToBytes(doc) + return request("ebicsUnsecuredRequest", "HIA", "0200", data) } fun HPB(): ByteArray { val nonce = getNonce(128) - val doc = request("ebicsNoPubKeyDigestsRequest") { + return request("ebicsNoPubKeyDigestsRequest", "HPB", "0000", timestamp = Instant.now(), sign = true) + } + + /* ----- Helpers ----- */ + + private fun request( + name: String, + order: String, + securityMedium: String, + data: String? = null, + timestamp: Instant? = null, + sign: Boolean = false + ): ByteArray { + val doc = XmlBuilder.toDom(name, "urn:org:ebics:$schema") { + attr("http://www.w3.org/2000/xmlns/", "xmlns", "urn:org:ebics:$schema") + attr("http://www.w3.org/2000/xmlns/", "xmlns:ds", "http://www.w3.org/2000/09/xmldsig#") + attr("Version", "$schema") + attr("Revision", "1") el("header") { attr("authenticate", "true") el("static") { el("HostID", cfg.ebicsHostId) - el("Nonce", nonce.encodeUpHex()) - el("Timestamp", Instant.now().xmlDateTime()) + if (timestamp != null) { + el("Nonce", getNonce(128).encodeUpHex()) + el("Timestamp", timestamp.xmlDateTime()) + } el("PartnerID", cfg.ebicsPartnerId) el("UserID", cfg.ebicsUserId) el("OrderDetails") { - el("OrderType", "HPB") - el("OrderAttribute", "DZHNN") + if (ebics3) { + el("AdminOrderType", order) + } else { + el("OrderType", order) + el("OrderAttribute", if (order == "HPB") "DZHNN" else "DZNNN") + } } - el("SecurityMedium", "0000") + el("SecurityMedium", securityMedium) } el("mutable") } - el("AuthSignature") - el("body") + if (sign) el("AuthSignature") + el("body") { + if (data != null) el("DataTransfer/OrderData", data) + } } - XMLUtil.signEbicsDocument(doc, clientKeys.authentication_private_key, "H004") + if (sign) XMLUtil.signEbicsDocument(doc, clientKeys.authentication_private_key, schema) return XMLUtil.convertDomToBytes(doc) } - /* ----- Helpers ----- */ - - private fun request(name: String, build: XmlBuilder.() -> Unit): Document { - return XmlBuilder.toDom(name, "urn:org:ebics:H004") { - attr("http://www.w3.org/2000/xmlns/", "xmlns", "urn:org:ebics:H004") - attr("http://www.w3.org/2000/xmlns/", "xmlns:ds", "http://www.w3.org/2000/09/xmldsig#") - attr("Version", "H004") - attr("Revision", "1") - build() - } - } - private fun XmlBuilder.RSAKeyXml(key: RSAPrivateCrtKey) { - el("PubKeyValue") { - el("ds:RSAKeyValue") { - el("ds:Modulus", key.modulus.encodeBase64()) - el("ds:Exponent", key.publicExponent.encodeBase64()) + if (ebics3) { + val cert = CryptoUtil.certificateFromPrivate(key) + el("ds:X509Data") { + el("ds:X509Certificate", cert.encoded.encodeBase64()) + } + } else { + el("PubKeyValue") { + el("ds:RSAKeyValue") { + el("ds:Modulus", key.modulus.encodeBase64()) + el("ds:Exponent", key.publicExponent.encodeBase64()) + } } } } @@ -190,18 +178,30 @@ class EbicsKeyMng( } fun parseHpbOrder(data: InputStream): Pair<RSAPublicKey, RSAPublicKey> { + fun XmlDestructor.rsaPubKey(): RSAPublicKey { + val cert = opt("X509Data")?.one("X509Certificate")?.text()?.decodeBase64() + return if (cert != null) { + CryptoUtil.loadRsaPublicKeyFromCertificate(cert) + } else { + one("PubKeyValue").one("RSAKeyValue") { + CryptoUtil.loadRsaPublicKeyFromComponents( + one("Modulus").text().decodeBase64(), + one("Exponent").text().decodeBase64(), + ) + } + + } + } return XmlDestructor.fromStream(data, "HPBResponseOrderData") { - val authPub = one("AuthenticationPubKeyInfo").one("PubKeyValue").one("RSAKeyValue") { - CryptoUtil.loadRsaPublicKeyFromComponents( - one("Modulus").text().decodeBase64(), - one("Exponent").text().decodeBase64(), - ) + val authPub = one("AuthenticationPubKeyInfo") { + val version = one("AuthenticationVersion").text() + require(version == "X002") { "Expected authentication version X002 got unsupported $version" } + rsaPubKey() } - val encPub = one("EncryptionPubKeyInfo").one("PubKeyValue").one("RSAKeyValue") { - CryptoUtil.loadRsaPublicKeyFromComponents( - one("Modulus").text().decodeBase64(), - one("Exponent").text().decodeBase64(), - ) + val encPub = one("EncryptionPubKeyInfo") { + val version = one("EncryptionVersion").text() + require(version == "E002") { "Expected encryption version E002 got unsupported $version" } + rsaPubKey() } Pair(authPub, encPub) } |