summaryrefslogtreecommitdiff
path: root/nexus/src/main/kotlin/tech/libeufin/nexus/ebics
diff options
context:
space:
mode:
Diffstat (limited to 'nexus/src/main/kotlin/tech/libeufin/nexus/ebics')
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt63
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsConstants.kt100
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsKeyMng.kt148
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)
}