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 }