diff options
Diffstat (limited to 'nexus/src/main/kotlin/tech/libeufin/nexus/ebics')
3 files changed, 174 insertions, 137 deletions
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) } |