libeufin

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

ebics.kt (18737B)


      1 /*
      2  * This file is part of LibEuFin.
      3  * Copyright (C) 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.test
     21 
     22 import org.w3c.dom.Document
     23 import com.github.ajalt.clikt.core.CliktCommand
     24 import com.github.ajalt.clikt.testing.test
     25 import io.ktor.http.*
     26 import io.ktor.http.content.*
     27 import tech.libeufin.common.*
     28 import tech.libeufin.common.crypto.CryptoUtil
     29 import tech.libeufin.ebics.*
     30 import kotlin.io.path.*
     31 import kotlin.test.*
     32 import java.security.interfaces.RSAPrivateCrtKey
     33 import java.security.interfaces.RSAPublicKey
     34 import java.time.LocalDate
     35 
     36 class EbicsState {
     37     private val bankSignKey: RSAPrivateCrtKey = CryptoUtil.genRSAPrivate(2048)
     38     private val bankEncKey: RSAPrivateCrtKey = CryptoUtil.genRSAPrivate(2048)
     39     private val bankAuthKey: RSAPrivateCrtKey = CryptoUtil.genRSAPrivate(2048)
     40 
     41     private var clientSignPub: RSAPublicKey? = null
     42     private var clientEncrPub: RSAPublicKey? = null
     43     private var clientAuthPub: RSAPublicKey? = null
     44 
     45     private var transactionId: String? = null
     46     private var orderId: String? = null
     47 
     48     companion object {
     49         private val HEV_OK = XmlBuilder.toBytes("ebicsHEVResponse") {
     50             attr("xmlns", "http://www.ebics.org/H000")
     51             el("SystemReturnCode") {
     52                 el("ReturnCode", "000000")
     53                 el("ReportText", "[EBICS_OK] OK")
     54             }
     55             el("VersionNumber") {
     56                 attr("ProtocolVersion", "H005")
     57                 text("03.00")
     58             }
     59         }
     60         private val KEY_OK = XmlBuilder.toBytes("ebicsKeyManagementResponse") {
     61             attr("xmlns", "http://www.ebics.org/H005")
     62             el("header") {
     63                 attr("authenticate", "true")
     64                 el("mutable") {
     65                     el("ReturnCode", "000000")
     66                     el("ReportText", "[EBICS_OK] OK")
     67                 }
     68             }
     69             el("body") {
     70                 el("ReturnCode") {
     71                     attr("authenticate", "true")
     72                     text("000000")
     73                 }
     74             }
     75         }
     76 
     77         private fun parseUnsecureRequest(body: String, order: String, root: String, parse: XmlDestructor.() -> Unit) {
     78             XmlDestructor.parse(body, "ebicsUnsecuredRequest") {
     79                 val adminOrder = one("header").one("static").one("OrderDetails").one("AdminOrderType").text()
     80                 assertEquals(adminOrder, order)
     81                 val chunk = one("body").one("DataTransfer").one("OrderData").base64()
     82                 val deflated = chunk.inputStream().inflate()
     83                 XmlDestructor.parse(deflated, root) { parse() }
     84             }
     85         }
     86     }
     87 
     88     private fun signedResponse(doc: Document): ByteArray {
     89         XMLUtil.signEbicsDocument(doc, bankAuthKey)
     90         return XMLUtil.convertDomToBytes(doc)
     91     }
     92 
     93     private fun ebicsResponsePayload(payload: ByteArray, last: Boolean = true): ByteArray {
     94         transactionId = randEbicsId()
     95         val deflated = payload.inputStream().deflate()
     96         val (transactionKey, encryptedTransactionKey) = CryptoUtil.genEbicsE002Key(clientEncrPub!!)
     97         val encrypted = CryptoUtil.encryptEbicsE002(transactionKey, deflated)
     98         val doc = XmlBuilder.toDom("ebicsResponse", "http://www.ebics.org/H005") {
     99             attr("http://www.w3.org/2000/xmlns/", "xmlns", "http://www.ebics.org/H005")
    100             el("header") {
    101                 attr("authenticate", "true")
    102                 el("static") {
    103                     el("TransactionID", transactionId!!)
    104                     el("NumSegments", "1")
    105                 }
    106                 el("mutable") {
    107                     el("TransactionPhase", "Initialisation")
    108                     el("SegmentNumber") {
    109                         attr("lastSegment", last.toString())
    110                         text("1")
    111                     }
    112                     el("ReturnCode", "000000")
    113                     el("ReportText", "[EBICS_OK] OK")
    114                 }
    115             }
    116             el("AuthSignature")
    117             el("body") {
    118                 el("DataTransfer") {
    119                     el("DataEncryptionInfo") {
    120                         attr("authenticate", "true")
    121                         el("EncryptionPubKeyDigest") {
    122                             attr("Version", "E002")
    123                             attr("Algorithm", "http://www.w3.org/2001/04/xmlenc#sha256")
    124                             text(CryptoUtil.getEbicsPublicKeyHash(clientEncrPub!!).encodeBase64())
    125                         }
    126                         el("TransactionKey", encryptedTransactionKey.encodeBase64())
    127                     }
    128                     el("OrderData", encrypted.encodeBase64())
    129                 }
    130                 el("ReturnCode") {
    131                     attr("authenticate", "true")
    132                     text("000000")
    133                 }
    134             }
    135         }
    136         return signedResponse(doc)
    137     }
    138 
    139     private fun ebicsResponseNoData(): ByteArray {
    140         val doc = XmlBuilder.toDom("ebicsResponse", "http://www.ebics.org/H005") {
    141             attr("http://www.w3.org/2000/xmlns/", "xmlns", "http://www.ebics.org/H005")
    142             el("header") {
    143                 attr("authenticate", "true")
    144                 el("static")
    145                 el("mutable") {
    146                     el("TransactionPhase", "Initialisation")
    147                     el("ReturnCode", "000000")
    148                     el("ReportText", "[EBICS_OK] OK")
    149                 }
    150             }
    151             el("AuthSignature")
    152             el("body") {
    153                 el("ReturnCode") {
    154                     attr("authenticate", "true")
    155                     text("090005")
    156                 }
    157             }
    158         }
    159         return signedResponse(doc)
    160     }
    161 
    162     fun hev(body: String): ByteArray {
    163         val hostId = XmlDestructor.parse(body, "ebicsHEVRequest") {
    164             one("HostID").text()
    165         }
    166         return HEV_OK
    167     }
    168 
    169     fun ini(body: String): ByteArray {
    170         parseUnsecureRequest(body, "INI", "SignaturePubKeyOrderData") {
    171             clientSignPub = one("SignaturePubKeyInfo") {
    172                 val version = one("SignatureVersion").text()
    173                 assertEquals(version, "A006")
    174                 rsaPubKey()
    175             }
    176         }
    177         return KEY_OK
    178     }
    179 
    180     fun hia(body: String): ByteArray {
    181         parseUnsecureRequest(body, "HIA", "HIARequestOrderData") {
    182             clientAuthPub = one("AuthenticationPubKeyInfo") {
    183                 val version = one("AuthenticationVersion").text()
    184                 assertEquals(version, "X002")
    185                 rsaPubKey()
    186             }
    187             clientEncrPub = one("EncryptionPubKeyInfo") {
    188                 val version = one("EncryptionVersion").text()
    189                 assertEquals(version, "E002")
    190                 rsaPubKey()
    191             }
    192         }
    193         return KEY_OK
    194     }
    195 
    196     fun hpb(body: String): ByteArray {
    197         // Parse HPB request
    198         XmlDestructor.parse(body, "ebicsNoPubKeyDigestsRequest") {
    199             val order = one("header").one("static").one("OrderDetails").one("AdminOrderType").text()
    200             assertEquals(order, "HPB")
    201         }
    202 
    203         val payload = XmlBuilder.toBytes("HPBResponseOrderData") {
    204             el("AuthenticationPubKeyInfo") {
    205                 el("PubKeyValue") {
    206                     el("RSAKeyValue") {
    207                         el("Modulus", bankAuthKey.modulus.encodeBase64())
    208                         el("Exponent", bankAuthKey.publicExponent.encodeBase64())
    209                     }
    210                 }
    211                 el("AuthenticationVersion", "X002")
    212             }
    213             el("EncryptionPubKeyInfo") {
    214                 el("PubKeyValue") {
    215                     el("RSAKeyValue") {
    216                         el("Modulus", bankEncKey.modulus.encodeBase64())
    217                         el("Exponent", bankEncKey.publicExponent.encodeBase64())
    218                     }
    219                 }
    220                 el("EncryptionVersion", "E002")
    221             }
    222         }.inputStream().deflate()
    223 
    224         val (transactionKey, encryptedTransactionKey) = CryptoUtil.genEbicsE002Key(clientEncrPub!!)
    225         val encrypted = CryptoUtil.encryptEbicsE002(transactionKey, payload)
    226         
    227         return XmlBuilder.toBytes("ebicsKeyManagementResponse") {
    228             attr("xmlns:ds", "http://www.w3.org/2000/09/xmldsig#")
    229             attr("xmlns", "http://www.ebics.org/H005")
    230             el("header") {
    231                 attr("authenticate", "true")
    232                 el("mutable") {
    233                     el("ReturnCode", "000000")
    234                     el("ReportText", "[EBICS_OK] OK")
    235                 }
    236             }
    237             el("body") {
    238                 el("DataTransfer") {
    239                     el("DataEncryptionInfo") {
    240                         attr("authenticate", "true")
    241                         el("EncryptionPubKeyDigest") {
    242                             attr("Version", "E002")
    243                             attr("Algorithm", "http://www.w3.org/2001/04/xmlenc#sha256")
    244                             text(CryptoUtil.getEbicsPublicKeyHash(clientEncrPub!!).encodeBase64())
    245                         }
    246                         el("TransactionKey", encryptedTransactionKey.encodeBase64())
    247                     }
    248                     el("OrderData", encrypted.encodeBase64())
    249                 }
    250                 el("ReturnCode") {
    251                     attr("authenticate", "true")
    252                     text("000000")
    253                 }
    254             }
    255         }
    256     }
    257 
    258     private fun receipt(body: String, ok: Boolean): ByteArray {
    259         XmlDestructor.parse(body, "ebicsRequest") {
    260             one("header") {
    261                 val id = one("static").one("TransactionID").text()
    262                 assertEquals(id, transactionId)
    263                 val phase = one("mutable").one("TransactionPhase").text()
    264                 assertEquals(phase, "Receipt")
    265             }
    266             val code = one("body").one("TransferReceipt").one("ReceiptCode").text()
    267             assertEquals(code, if (ok) { "0" } else { "1" })
    268         }
    269         val response = signedResponse(XmlBuilder.toDom("ebicsResponse", "http://www.ebics.org/H005") {
    270             attr("http://www.w3.org/2000/xmlns/", "xmlns", "http://www.ebics.org/H005")
    271             el("header") {
    272                 attr("authenticate", "true")
    273                 el("static") {
    274                     el("TransactionID", transactionId!!)
    275                 }
    276                 el("mutable") {
    277                     el("TransactionPhase", "Receipt")
    278                     el("ReturnCode", "000000")
    279                     el("ReportText", "[EBICS_OK] OK")
    280                 }
    281             }
    282             el("AuthSignature")
    283             el("body") {
    284                 el("ReturnCode") {
    285                     attr("authenticate", "true")
    286                     text("000000")
    287                 }
    288             }
    289         })
    290         transactionId = null
    291         return response
    292     }
    293 
    294     fun receiptOk(body: String): ByteArray = receipt(body, true)
    295     fun receiptErr(body: String): ByteArray = receipt(body, false)
    296 
    297     fun hkd(body: String): ByteArray {
    298         XmlDestructor.parse(body, "ebicsRequest") {
    299             one("header") {
    300                 val adminOrder = one("static").one("OrderDetails").one("AdminOrderType").text()
    301                 assertEquals(adminOrder, "HKD")
    302                 val phase = one("mutable").one("TransactionPhase").text()
    303                 assertEquals(phase, "Initialisation")
    304             }
    305         }
    306         return ebicsResponsePayload(
    307             XmlBuilder.toBytes("HKDResponseOrderData") {
    308                 el("PartnerInfo") {
    309                     el("AddressInfo")
    310                     el("OrderInfo") {
    311                         el("AdminOrderType", "BTD")
    312                         el("Service") {
    313                             el("ServiceName", "STM")
    314                             el("Scope", "CH")
    315                             el("Container") {
    316                                 attr("containerType", "ZIP")
    317                             }
    318                             el("MsgName") {
    319                                 attr("version", "08")
    320                                 text("camt.052")
    321                             }
    322                         }
    323                         el("Description")
    324                     }
    325                     el("OrderInfo") {
    326                         el("AdminOrderType", "BTU")
    327                         el("Service") {
    328                             el("ServiceName", "SCT")
    329                             el("MsgName") {
    330                                 text("pain.001")
    331                             }
    332                         }
    333                         el("Description", "Direct Debit")
    334                     }
    335                     el("OrderInfo") {
    336                         el("AdminOrderType", "BTU")
    337                         el("Service") {
    338                             el("ServiceName", "SCI")
    339                             el("Scope", "DE")
    340                             el("MsgName") {
    341                                 text("pain.001")
    342                             }
    343                         }
    344                         el("Description", "Instant Direct Debit")
    345                     }
    346                 }
    347             }
    348         )
    349     }
    350 
    351     fun haa(body: String): ByteArray {
    352         XmlDestructor.parse(body, "ebicsRequest") {
    353             one("header") {
    354                 val adminOrder = one("static").one("OrderDetails").one("AdminOrderType").text()
    355                 assertEquals(adminOrder, "HAA")
    356                 val phase = one("mutable").one("TransactionPhase").text()
    357                 assertEquals(phase, "Initialisation")
    358             }
    359         }
    360         return ebicsResponsePayload(
    361             XmlBuilder.toBytes("HAAResponseOrderData") {
    362                 el("Service") {
    363                     el("ServiceName", "STM")
    364                     el("Scope", "CH")
    365                     el("Container") {
    366                         attr("containerType", "ZIP")
    367                     }
    368                     el("MsgName") {
    369                         attr("version", "08")
    370                         text("camt.052")
    371                     }
    372                 }
    373             }
    374         )
    375     }
    376 
    377     private fun btdDateCheck(body: String, pinned: LocalDate?): ByteArray {
    378         XmlDestructor.parse(body, "ebicsRequest") {
    379             one("header") {
    380                 one("static").one("OrderDetails") {
    381                     val adminOrder = one("AdminOrderType").text()
    382                     assertEquals(adminOrder, "BTD")
    383                     val start = one("BTDOrderParams").opt("DateRange")?.opt("Start")?.date()
    384                     assertEquals(start, pinned)
    385                 }
    386                 val phase = one("mutable").one("TransactionPhase").text()
    387                 assertEquals(phase, "Initialisation")
    388             }
    389         }
    390         return ebicsResponseNoData()
    391     }
    392 
    393     fun btdNoData(body: String): ByteArray = btdDateCheck(body, null)
    394     fun btdNoDataNow(body: String): ByteArray = btdDateCheck(body, LocalDate.now())
    395     fun btdNoDataPinned(body: String): ByteArray = btdDateCheck(body, LocalDate.parse("2024-06-05"))
    396 
    397     fun btuInit(body: String): ByteArray {
    398         XmlDestructor.parse(body, "ebicsRequest") {
    399             one("header") {
    400                 one("static").one("OrderDetails") {
    401                     val adminOrder = one("AdminOrderType").text()
    402                     assertEquals(adminOrder, "BTU")
    403                 }
    404                 val phase = one("mutable").one("TransactionPhase").text()
    405                 assertEquals(phase, "Initialisation")
    406             }
    407         }
    408         transactionId = randEbicsId()
    409         orderId = randEbicsId()
    410         val doc = XmlBuilder.toDom("ebicsResponse", "http://www.ebics.org/H005") {
    411             attr("http://www.w3.org/2000/xmlns/", "xmlns", "http://www.ebics.org/H005")
    412             el("header") {
    413                 attr("authenticate", "true")
    414                 el("static") {
    415                     el("TransactionID", transactionId!!)
    416                 }
    417                 el("mutable") {
    418                     el("TransactionPhase", "Initialisation")
    419                     el("OrderID", orderId!!)
    420                     el("ReturnCode", "000000")
    421                     el("ReportText", "[EBICS_OK] OK")
    422                 }
    423             }
    424             el("AuthSignature")
    425             el("body") {
    426                 el("ReturnCode") {
    427                     attr("authenticate", "true")
    428                     text("000000")
    429                 }
    430             }
    431         }
    432         return signedResponse(doc)
    433     }
    434 
    435     fun btuPayload(body: String): ByteArray {
    436         lateinit var segment: String
    437         XmlDestructor.parse(body, "ebicsRequest") {
    438             one("header") {
    439                 one("static") {
    440                     val txid = one("TransactionID").text()
    441                     assertEquals(txid, transactionId)
    442                 }
    443                 one("mutable") {
    444                     val phase = one("TransactionPhase").text()
    445                     assertEquals(phase, "Transfer")
    446                     segment = one("SegmentNumber").text()
    447                 }
    448             }
    449         }
    450         val doc = XmlBuilder.toDom("ebicsResponse", "http://www.ebics.org/H005") {
    451             attr("http://www.w3.org/2000/xmlns/", "xmlns", "http://www.ebics.org/H005")
    452             el("header") {
    453                 attr("authenticate", "true")
    454                 el("static") {
    455                     el("TransactionID", transactionId!!)
    456                 }
    457                 el("mutable") {
    458                     el("TransactionPhase", "Transfer")
    459                     el("SegmentNumber", segment)
    460                     el("OrderID", orderId!!)
    461                     el("ReturnCode", "000000")
    462                     el("ReportText", "[EBICS_OK] OK")
    463                 }
    464             }
    465             el("AuthSignature")
    466             el("body") {
    467                 el("ReturnCode") {
    468                     attr("authenticate", "true")
    469                     text("000000")
    470                 }
    471             }
    472         }
    473         return signedResponse(doc)
    474     }
    475 
    476     fun badRequest(body: String): ByteArray {
    477         throw BadRequest
    478     }
    479 
    480     fun initializeTx(body: String): ByteArray = ebicsResponsePayload(ByteArray(0), false)
    481 
    482     fun failure(body: String): ByteArray {
    483         throw Exception("Not reachable")
    484     }
    485 }