libeufin

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

EbicsKeyMng.kt (7983B)


      1 /*
      2  * This file is part of LibEuFin.
      3  * Copyright (C) 2024-2025 Taler Systems S.A.
      4 
      5  * LibEuFin is free software; you can redistribute it and/or modify
      6  * it under the terms of the GNU Affero General Public License as
      7  * published by the Free Software Foundation; either version 3, or
      8  * (at your option) any later version.
      9 
     10  * LibEuFin is distributed in the hope that it will be useful, but
     11  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
     12  * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General
     13  * Public License for more details.
     14 
     15  * You should have received a copy of the GNU Affero General Public
     16  * License along with LibEuFin; see the file COPYING.  If not, see
     17  * <http://www.gnu.org/licenses/>
     18  */
     19 
     20 package tech.libeufin.ebics
     21 
     22 import org.w3c.dom.Document
     23 import tech.libeufin.common.crypto.CryptoUtil
     24 import tech.libeufin.common.decodeBase64
     25 import tech.libeufin.common.deflate
     26 import tech.libeufin.common.encodeBase64
     27 import tech.libeufin.common.encodeUpHex
     28 import tech.libeufin.ebics.EbicsKeyMng.Order.*
     29 import java.io.InputStream
     30 import java.security.interfaces.RSAPrivateCrtKey
     31 import java.security.interfaces.RSAPublicKey
     32 import java.time.Instant
     33 
     34 /** EBICS protocol for key management */
     35 class EbicsKeyMng(
     36     private val cfg: EbicsHostConfig,
     37     private val clientKeys: ClientPrivateKeysFile,
     38     private val ebics3: Boolean
     39 ) {
     40     private val schema = if (ebics3) "H005" else "H004"
     41 
     42     enum class Order {
     43         INI,
     44         HIA,
     45         HPB
     46     }
     47 
     48     fun request(order: Order): ByteArray {
     49         val (name, securityMedium, orderAttribute) = when (order) {
     50             INI, HIA -> Triple("ebicsUnsecuredRequest", "0200", "DZNNN")
     51             HPB -> Triple("ebicsNoPubKeyDigestsRequest", "0000", "DZHNN")
     52         }
     53         val data = when (order) {
     54             INI -> XMLOrderData("SignaturePubKeyOrderData", "http://www.ebics.org/S00${if (ebics3) 2 else 1}") {
     55                 el("SignaturePubKeyInfo") {
     56                     RSAKeyXml(clientKeys.signature_private_key)
     57                     el("SignatureVersion", "A006")
     58                 }
     59             }
     60             HIA -> XMLOrderData("HIARequestOrderData", "urn:org:ebics:$schema") {
     61                 el("AuthenticationPubKeyInfo") {
     62                     RSAKeyXml(clientKeys.authentication_private_key)
     63                     el("AuthenticationVersion", "X002")
     64                 }
     65                 el("EncryptionPubKeyInfo") {
     66                     RSAKeyXml(clientKeys.encryption_private_key)
     67                     el("EncryptionVersion", "E002")
     68                 }
     69             }
     70             HPB -> null
     71         }
     72         val sign = order == HPB
     73         val doc = XmlBuilder.toDom(name, "urn:org:ebics:$schema") {
     74             attr("http://www.w3.org/2000/xmlns/", "xmlns", "urn:org:ebics:$schema")
     75             attr("http://www.w3.org/2000/xmlns/", "xmlns:ds", "http://www.w3.org/2000/09/xmldsig#")
     76             attr("Version", schema)
     77             attr("Revision", "1")
     78             el("header") {
     79                 attr("authenticate", "true")
     80                 el("static") {
     81                     el("HostID", cfg.hostId)
     82                     if (order == HPB) {
     83                         el("Nonce", getNonce(128).encodeUpHex())
     84                         el("Timestamp", Instant.now().xmlDateTime())
     85                     }
     86                     el("PartnerID", cfg.partnerId)
     87                     el("UserID", cfg.userId)
     88                     el("OrderDetails") {
     89                         if (ebics3) {
     90                             el("AdminOrderType", order.name)
     91                         } else {
     92                             el("OrderType", order.name)
     93                             el("OrderAttribute", orderAttribute)
     94                         }
     95                     }
     96                     el("SecurityMedium", securityMedium)
     97                 }
     98                 el("mutable")
     99             }
    100             if (sign) el("AuthSignature")
    101             el("body") {
    102                 if (data != null) el("DataTransfer/OrderData", data)
    103             }
    104         }
    105         if (sign) XMLUtil.signEbicsDocument(doc, clientKeys.authentication_private_key)
    106         return XMLUtil.convertDomToBytes(doc)
    107     }
    108 
    109     private fun XmlBuilder.RSAKeyXml(key: RSAPrivateCrtKey) {
    110         if (ebics3) {
    111             val cert = CryptoUtil.X509CertificateFromRSAPrivate(key, "LibEuFin EBICS")
    112             el("ds:X509Data") {
    113                 el("ds:X509Certificate", cert.encoded.encodeBase64())
    114             }
    115         } else {
    116             el("PubKeyValue") {
    117                 el("ds:RSAKeyValue") {
    118                     el("ds:Modulus", key.modulus.encodeBase64())
    119                     el("ds:Exponent", key.publicExponent.encodeBase64())
    120                 }
    121             }
    122         }
    123     }
    124     
    125     private fun XMLOrderData(name: String, schema: String, build: XmlBuilder.() -> Unit): String {
    126         return XmlBuilder.toBytes(name) {
    127             attr("xmlns:ds", "http://www.w3.org/2000/09/xmldsig#")
    128             attr("xmlns", schema)
    129             build()
    130             el("PartnerID", cfg.partnerId)
    131             el("UserID", cfg.userId)
    132         }.inputStream().deflate().encodeBase64()
    133     }
    134 
    135     companion object {
    136         fun parseResponse(doc: Document, clientEncryptionKey: RSAPrivateCrtKey): EbicsResponse<InputStream?> {
    137             return XmlDestructor.parse(doc, "ebicsKeyManagementResponse") {
    138                 lateinit var technicalCode: EbicsReturnCode
    139                 lateinit var bankCode: EbicsReturnCode
    140                 var payload: InputStream? = null
    141                 one("header", signed = true) {
    142                     one("mutable") {
    143                         technicalCode = EbicsReturnCode.lookup(one("ReturnCode").text())
    144                     }
    145                 }
    146                 one("body") {
    147                     bankCode = EbicsReturnCode.lookup(one("ReturnCode", signed = true).text())
    148                     payload = opt("DataTransfer") {
    149                         val descriptionInfo = one("DataEncryptionInfo", signed = true) {
    150                             DataEncryptionInfo(
    151                                 one("TransactionKey").base64(),
    152                                 one("EncryptionPubKeyDigest").base64()
    153                             )
    154                         }
    155                         val chunk = one("OrderData").base64()
    156                         decryptAndDecompressPayload(
    157                             clientEncryptionKey,
    158                             descriptionInfo,
    159                             listOf(chunk)
    160                         )
    161                     }
    162                 }
    163                 EbicsResponse(
    164                     technicalCode = technicalCode, 
    165                     bankCode, 
    166                     content = payload
    167                 )
    168             }
    169         }
    170 
    171         fun parseHpbOrder(data: InputStream): Pair<RSAPublicKey, RSAPublicKey> {
    172             return XmlDestructor.parse(data, "HPBResponseOrderData") {
    173                 val authPub = one("AuthenticationPubKeyInfo") {
    174                     val version = one("AuthenticationVersion").text()
    175                     require(version == "X002") { "Expected authentication version X002 got unsupported $version" }
    176                     rsaPubKey()
    177                 }
    178                 val encPub = one("EncryptionPubKeyInfo") {
    179                     val version = one("EncryptionVersion").text()
    180                     require(version == "E002") { "Expected encryption version E002 got unsupported $version" }
    181                     rsaPubKey()
    182                 }
    183                 Pair(authPub, encPub)
    184             }
    185         }
    186     }
    187 }
    188 
    189 fun XmlDestructor.rsaPubKey(): RSAPublicKey {
    190     val cert = opt("X509Data")?.one("X509Certificate")?.text()?.decodeBase64()
    191     return if (cert != null) {
    192         CryptoUtil.RSAPublicFromCertificate(cert)
    193     } else {
    194         one("PubKeyValue").one("RSAKeyValue") {
    195             CryptoUtil.RSAPublicFromComponents(
    196                 one("Modulus").base64(),
    197                 one("Exponent").base64(),
    198             )
    199         }
    200     }
    201 }