libeufin

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

commit 646078079a51bdedc25dafdd0d24cb251f22840c
parent 23bdb9d2a077552937c1d2697fa82909bd5e95e4
Author: Antoine A <>
Date:   Fri, 24 Apr 2026 10:37:57 +0200

refactor

Diffstat:
Asrc/common.rs | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/crypto.rs | 108+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/key_management.rs | 325+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/main.rs | 412++-----------------------------------------------------------------------------
Msrc/xml.rs | 28+++++++++++++---------------
5 files changed, 505 insertions(+), 419 deletions(-)

diff --git a/src/common.rs b/src/common.rs @@ -0,0 +1,51 @@ +/* +* This file is part of LibEuFin. +* Copyright (C) 2026 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/> +*/ + +use std::io::Write as _; + +use aws_lc_rs::rsa::PrivateDecryptingKey; +use flate2::write::ZlibDecoder; + +use crate::crypto::{decrypt_ebics_e002, decrypt_ebics_e002_key}; + +pub struct EbicsLogger {} + +pub struct DataEncryptionInfo { + pub transaction_key: Vec<u8>, + pub bank_pub_digest: Vec<u8>, +} + +/** Decrypts and decompresses EBICS BTS payload */ +pub fn decrypt_and_decompress_payload( + client_encryption_key: &PrivateDecryptingKey, + encryption_info: DataEncryptionInfo, + segments: Vec<Vec<u8>>, +) -> Vec<u8> { + // TODO check bank_pub_digest + let tx_key = decrypt_ebics_e002_key( + client_encryption_key.clone(), + &encryption_info.transaction_key, + ); + let mut decoder = ZlibDecoder::new(Vec::new()); + for segment in segments { + let decrypted = decrypt_ebics_e002(&tx_key, segment); + decoder.write_all(&decrypted).unwrap(); + } + decoder.finish().unwrap() +} diff --git a/src/crypto.rs b/src/crypto.rs @@ -0,0 +1,108 @@ +/* +* This file is part of LibEuFin. +* Copyright (C) 2026 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/> +*/ + +use aws_lc_rs::{ + cipher::{DecryptingKey, DecryptionContext, UnboundCipherKey}, + iv::FixedLength, + rsa::{Pkcs1PrivateDecryptingKey, PrivateDecryptingKey, PublicEncryptingKey}, +}; +use jiff::{Timestamp, Zoned, tz::TimeZone}; +use rcgen::{BasicConstraints, CertificateParams, DnType, IsCa, KeyUsagePurpose}; +use x509_parser::prelude::{FromDer as _, X509Certificate}; + +/// Generate a self-signed X.509 certificate from an RSA private key (PEM or DER) +pub fn x509_certificate_from_rsa_private( + pem: &str, + name: &str, +) -> Result<rcgen::Certificate, rcgen::Error> { + let keys = rcgen::KeyPair::from_pem(pem).unwrap(); + let mut params = CertificateParams::new(vec![])?; + + // Set subject/issuer CN + params.distinguished_name.push(DnType::CommonName, name); + + let now = Zoned::new(Timestamp::now(), TimeZone::UTC).date(); + + // 1000-year validity + params.not_before = rcgen::date_time_ymd(now.year() as i32, now.month() as u8, now.day() as u8); + params.not_after = + rcgen::date_time_ymd(now.year() as i32 + 1000, now.month() as u8, now.day() as u8); + + // CA: true (basicConstraints) + params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + + // Key usage flags + params.key_usages = vec![ + KeyUsagePurpose::DigitalSignature, + KeyUsagePurpose::ContentCommitment, // NonRepudiation + KeyUsagePurpose::KeyEncipherment, + KeyUsagePurpose::DataEncipherment, + KeyUsagePurpose::KeyAgreement, + KeyUsagePurpose::KeyCertSign, + KeyUsagePurpose::CrlSign, + KeyUsagePurpose::EncipherOnly, + KeyUsagePurpose::DecipherOnly, + ]; + + let cert = params.self_signed(&keys)?; + Ok(cert) +} + +/// Extract an RSA public key from a X.509 certificate +pub fn rsa_private_from_x509_certificate_from(der: &[u8]) -> PublicEncryptingKey { + let (_, cert) = X509Certificate::from_der(der).unwrap(); + let issuer_public_key = cert.public_key(); + cert.verify_signature(Some(issuer_public_key)).unwrap(); + PublicEncryptingKey::from_der(issuer_public_key.raw).unwrap() +} + +pub fn decrypt_ebics_e002(transaction_key: &DecryptingKey, mut encrypted_data: Vec<u8>) -> Vec<u8> { + // AES-CBC with a zero IV, as in the Kotlin original. + let iv = [0u8; 16]; + + let plaintext = transaction_key + .decrypt( + &mut encrypted_data, + DecryptionContext::Iv128(FixedLength::from(iv)), + ) + .unwrap(); + + // Strip X9.23 / ANSI X9.23 padding: + // The last byte holds the number of padding bytes to remove. + let pad_len = *plaintext.last().unwrap() as usize; + if pad_len == 0 || pad_len > 16 || pad_len > plaintext.len() { + panic!("WTF"); + } + let decoded = plaintext.len() - pad_len; + encrypted_data.truncate(decoded); + encrypted_data +} + +pub fn decrypt_ebics_e002_key( + private_key: PrivateDecryptingKey, + encrypted_transaction_key: &[u8], +) -> DecryptingKey { + let private_key = Pkcs1PrivateDecryptingKey::new(private_key).unwrap(); + let mut plaintext = vec![0u8; private_key.min_output_size()]; + let cipher = private_key + .decrypt(encrypted_transaction_key, &mut plaintext) + .unwrap(); + let cipher_key = UnboundCipherKey::new(&aws_lc_rs::cipher::AES_128, cipher).unwrap(); + DecryptingKey::cbc(cipher_key).unwrap() +} diff --git a/src/key_management.rs b/src/key_management.rs @@ -0,0 +1,325 @@ +/* +* This file is part of LibEuFin. +* Copyright (C) 2026 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/> +*/ + +use std::io::Write as _; + +use anyhow::bail; +use aws_lc_rs::{ + encoding::{AsDer, Pkcs8V1Der}, + rsa::PublicEncryptingKey, +}; +use base64::{Engine as _, prelude::BASE64_STANDARD}; +use flate2::{Compression, write::ZlibEncoder}; +use reqwest::Client; +use tracing::info; + +use crate::{ + EbicsResponse, + common::{DataEncryptionInfo, EbicsLogger, decrypt_and_decompress_payload}, + config::{EbicsHostCfg, EbicsKeysCfg}, + crypto::{rsa_private_from_x509_certificate_from, x509_certificate_from_rsa_private}, + ebics_code::EbicsReturnCode, + keys::{self, BankPubKeysFile, ClientPriKeysFile}, + post_to_bank, + xml::{XmlReader, XmlWriter}, + xml_build, xml_el, + xml_sign::sign_ebics, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[allow(non_camel_case_types)] +pub enum Order { + INI, + HIA, + HPB, +} + +impl Order { + pub fn name(&self) -> &'static str { + match self { + Order::INI => "INI", + Order::HIA => "HIA", + Order::HPB => "HPB", + } + } +} + +/** Perform an EBICS public key management [order] using [client] and update on disk state */ +pub async fn submit_client_keys( + keys_cfg: &EbicsKeysCfg, + host_cfg: &EbicsHostCfg, + client: &mut ClientPriKeysFile, + http: &Client, + ebics_logger: &EbicsLogger, + order: Order, +) -> anyhow::Result<()> { + if order == Order::HPB { + bail!("Only INI & HIA are supported for client keys"); + } + let res = key_management(host_cfg, client, http, ebics_logger, order).await?; + + if res.technical_code == EbicsReturnCode::EBICS_INVALID_USER_STATE + || res.technical_code == EbicsReturnCode::EBICS_INVALID_USER_OR_USER_STATE + { + bail!( + "{} status code {}: either your IDs are incorrect, or you already have keys registered with this bank", + order.name(), + res.technical_code + ) + } + res.ok_or_fail(order.name())?; + match order { + Order::INI => client.submitted_ini = true, + Order::HIA => client.submitted_hia = true, + Order::HPB => unreachable!("Only INI & HIA are supported for client keys"), + } + keys::persist_client_keys(client, keys_cfg.client_priv_keys_path.as_ref())?; + // TODO better error: Could not update the $order state on disk + Ok(()) +} + +pub async fn hpb( + http: &Client, + cfg: &EbicsHostCfg, + logger: &EbicsLogger, + client: &ClientPriKeysFile, +) -> anyhow::Result<BankPubKeysFile> { + let order = Order::HPB; + let res = key_management(cfg, client, http, logger, order).await?; + if res.technical_code == EbicsReturnCode::EBICS_AUTHENTICATION_FAILED { + bail!( + "{} status code {}: could not download bank keys, send client keys (and/or related PDF document with --generate-registration-pdf) to the bank", + order.name(), + res.technical_code + ) + } + let order_data = res + .ok_or_fail(order.name())? + .expect("{order}: missing order data"); + + fn rsa_pub_key(xml: XmlReader) -> PublicEncryptingKey { + let der = xml.one("X509Data").one("X509Certificate").b64(); + rsa_private_from_x509_certificate_from(&der) + } + + Ok(XmlReader::parse( + &order_data, + "HPBResponseOrderData", + |root| { + let auth_pub_info = root.one("AuthenticationPubKeyInfo"); + let version = auth_pub_info.one("AuthenticationVersion"); + let version = version.text(); + assert_eq!( + version, "X002", + "Expected authentication version X002 got unsupported {version}" + ); + let auth_pub = rsa_pub_key(auth_pub_info); + + let enc_pub_info = root.one("EncryptionPubKeyInfo"); + let version = enc_pub_info.one("EncryptionVersion"); + let version = version.text(); + assert_eq!( + version, "E002", + "Expected encryption version E002 got unsupported {version}" + ); + let enc_pub = rsa_pub_key(enc_pub_info); + + BankPubKeysFile { + bank_authentication_public_key: auth_pub, + bank_encryption_public_key: enc_pub, + accepted: false, + } + }, + )) +} + +pub async fn key_management( + cfg: &EbicsHostCfg, + client: &ClientPriKeysFile, + http: &Client, + ebics_logger: &EbicsLogger, + order: Order, +) -> anyhow::Result<EbicsResponse<Option<String>>> { + info!("Doing key request {}", order.name()); + //val txLog = ebicsLogger.tx(order.name) + // TODO is this still necessary ? + + let (name, security_medium) = match order { + Order::INI | Order::HIA => ("ebicsUnsecuredRequest", "0200"), + Order::HPB => ("ebicsNoPubKeyDigestsRequest", "0000"), + }; + + fn xml_order_data( + cfg: &EbicsHostCfg, + name: &str, + schema: &str, + build: impl FnOnce(&mut XmlWriter), + ) -> String { + let xml = xml_build!(name ("xmlns":schema) ("xmlns:ds":"http://www.w3.org/2000/09/xmldsig#") { + @ build, + "PartnerID": &cfg.partner_id, + "UserID": &cfg.user_id + }); + // Deflate TODO write inside the compressor directly + let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default()); + encoder.write_all(xml.as_bytes()).unwrap(); + let compressed = encoder.finish().unwrap(); + BASE64_STANDARD.encode(&compressed) + } + + fn rsa_key_xml<K>(w: &mut XmlWriter, key: &K) + where + K: AsDer<Pkcs8V1Der<'static>>, + { + let der = key.as_der().unwrap(); + let b64 = BASE64_STANDARD.encode(der.as_ref()); + let lines = b64 + .as_bytes() + .chunks(64) + .map(|c| std::str::from_utf8(c).unwrap()) + .collect::<Vec<_>>() + .join("\n"); + let pem = + format!("-----BEGIN RSA PRIVATE KEY-----\n{lines}\n-----END RSA PRIVATE KEY-----\n"); + let cert = x509_certificate_from_rsa_private(&pem, "LibEuFin EBICS").unwrap(); + let der = cert.der(); + let b64 = BASE64_STANDARD.encode(der.as_ref()); + + xml_el!(w, "ds:X509Data" { + "ds:X509Certificate": &b64 + }); + } + let data = match order { + Order::INI => Some(xml_order_data( + cfg, + "SignaturePubKeyOrderData", + "http://www.ebics.org/S002", + |w| { + xml_el!(w, "SignaturePubKeyInfo" { + @ |w| rsa_key_xml(w, &client.signature_private_key), + "SignatureVersion": "A006" + }); + }, + )), + Order::HIA => Some(xml_order_data( + cfg, + "HIARequestOrderData", + "urn:org:ebics:H005", + |w| { + xml_el!(w, "AuthenticationPubKeyInfo" { + @ |w| rsa_key_xml(w, &client.authentication_private_key), + "AuthenticationVersion": "X002" + }, + "EncryptionPubKeyInfo" { + @ |w| rsa_key_xml(w, &client.encryption_private_key), + "EncryptionVersion": "E002" + }); + }, + )), + Order::HPB => None, + }; + let sign = order == Order::HPB; + let msg = xml_build!( + name + ("xmlns": "urn:org:ebics:H005") + ("xmlns:ds": "http://www.w3.org/2000/09/xmldsig#") + ("Version": "H005") + ("Revision": "1") + { + "header" ("authenticate": "true") { + "static" { + "HostID": &cfg.host_id, + @ |w: &mut XmlWriter| { + if order == Order::HPB { + let nonce: u128 = rand::random(); + xml_el!(w, + "Nonce": &format_args!("{:032x}", nonce), + "Timestamp": &jiff::Timestamp::now() + ); + } + }, + "PartnerID": &cfg.partner_id, + "UserID": &cfg.user_id, + "OrderDetails" { + "AdminOrderType": order.name() + }, + "SecurityMedium": security_medium + }, + "mutable" + }, + @ |w: &mut XmlWriter| { + if sign { + xml_el!(w, "AuthSignature"); + } + }, + "body" { + @ |w: &mut XmlWriter| { + if let Some(data) = data { + xml_el!(w,"DataTransfer" { + "OrderData": data + }); + } + } + } + } + + ); + let signed = if sign { + sign_ebics(msg, &client.authentication_private_key) + } else { + msg + }; + let res = post_to_bank(cfg.base_url.as_str(), http, signed).await?; + Ok(XmlReader::parse( + &res, + "ebicsKeyManagementResponse", + |root| { + let technical_code = root + .one_signed("header") + .one("mutable") + .one("ReturnCode") + .text() + .parse() + .unwrap(); + let body = root.one("body"); + let bank_code = body.one_signed("ReturnCode").text().parse().unwrap(); + let content = if let Some(data) = body.opt("DataTransfer") { + let info = data.one_signed("DataEncryptionInfo"); + let info = DataEncryptionInfo { + transaction_key: info.one("TransactionKey").b64(), + bank_pub_digest: info.one("EncryptionPubKeyDigest").b64(), + }; + let chunk = data.one("OrderData").b64(); + let decoded = decrypt_and_decompress_payload( + &client.encryption_private_key, + info, + vec![chunk], + ); + Some(String::from_utf8(decoded).unwrap()) + } else { + None + }; + EbicsResponse { + technical_code, + bank_code, + content, + } + }, + )) +} diff --git a/src/main.rs b/src/main.rs @@ -17,41 +17,32 @@ * <http://www.gnu.org/licenses/> */ -use std::{fmt::Display, io::Write as _, path::Path}; +use std::{fmt::Display, path::Path}; use anyhow::bail; -use aws_lc_rs::{ - cipher::{DecryptingKey, DecryptionContext, UnboundCipherKey}, - encoding::{AsDer, Pkcs8V1Der}, - iv::FixedLength, - rsa::{Pkcs1PrivateDecryptingKey, PrivateDecryptingKey, PublicEncryptingKey}, -}; -use base64::{Engine, prelude::BASE64_STANDARD}; use clap::Parser; -use flate2::{Compression, write::ZlibDecoder, write::ZlibEncoder}; -use jiff::{Timestamp, Zoned, tz::TimeZone}; -use rcgen::{BasicConstraints, CertificateParams, DnType, IsCa, KeyUsagePurpose}; use reqwest::{ Client, StatusCode, header::{CONTENT_TYPE, HeaderValue}, }; -use strum_macros::Display; use taler_build::long_version; use taler_common::{CommonArgs, config::parser::ConfigSource, taler_main}; use tracing::{debug, info}; -use x509_parser::prelude::{FromDer as _, X509Certificate}; use crate::{ - config::{EbicsHostCfg, EbicsKeysCfg, NexusCfg}, + common::EbicsLogger, + config::{EbicsHostCfg, NexusCfg}, ebics_code::EbicsReturnCode, - keys::{BankPubKeysFile, load_bank_keys, load_client_keys, persist_client_keys}, - xml::XmlWriter, - xml_sign::sign_ebics, + key_management::{Order, hpb, submit_client_keys}, + keys::{load_bank_keys, load_client_keys, persist_client_keys}, }; use crate::{keys::ClientPriKeysFile, xml::XmlReader}; +mod common; pub mod config; +mod crypto; pub mod ebics_code; +mod key_management; pub mod keys; pub mod xml; pub mod xml_sign; @@ -140,339 +131,6 @@ pub async fn ebics_setup( Ok(()) } -#[derive(Debug, Display, Clone, Copy, PartialEq, Eq)] -#[allow(non_camel_case_types)] -pub enum Order { - INI, - HIA, - HPB, -} - -impl Order { - pub fn name(&self) -> &'static str { - match self { - Order::INI => "INI", - Order::HIA => "HIA", - Order::HPB => "HPB", - } - } -} - -pub struct EbicsLogger {} - -/** Perform an EBICS public key management [order] using [client] and update on disk state */ -async fn submit_client_keys( - keys_cfg: &EbicsKeysCfg, - host_cfg: &EbicsHostCfg, - client: &mut ClientPriKeysFile, - http: &Client, - ebics_logger: &EbicsLogger, - order: Order, -) -> anyhow::Result<()> { - if order == Order::HPB { - bail!("Only INI & HIA are supported for client keys"); - } - let res = key_management(host_cfg, client, http, ebics_logger, order).await?; - - if res.technical_code == EbicsReturnCode::EBICS_INVALID_USER_STATE - || res.technical_code == EbicsReturnCode::EBICS_INVALID_USER_OR_USER_STATE - { - bail!( - "{order} status code {}: either your IDs are incorrect, or you already have keys registered with this bank", - res.technical_code - ) - } - res.ok_or_fail(order.name())?; - match order { - Order::INI => client.submitted_ini = true, - Order::HIA => client.submitted_hia = true, - Order::HPB => unreachable!("Only INI & HIA are supported for client keys"), - } - keys::persist_client_keys(client, keys_cfg.client_priv_keys_path.as_ref())?; - // TODO better error: Could not update the $order state on disk - Ok(()) -} - -/// Generate a self-signed X.509 certificate from an RSA private key (PEM or DER) -pub fn x509_certificate_from_rsa_private( - pem: &str, - name: &str, -) -> Result<rcgen::Certificate, rcgen::Error> { - let keys = rcgen::KeyPair::from_pem(pem).unwrap(); - let mut params = CertificateParams::new(vec![])?; - - // Set subject/issuer CN - params.distinguished_name.push(DnType::CommonName, name); - - let now = Zoned::new(Timestamp::now(), TimeZone::UTC).date(); - - // 1000-year validity - params.not_before = rcgen::date_time_ymd(now.year() as i32, now.month() as u8, now.day() as u8); - params.not_after = - rcgen::date_time_ymd(now.year() as i32 + 1000, now.month() as u8, now.day() as u8); - - // CA: true (basicConstraints) - params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); - - // Key usage flags - params.key_usages = vec![ - KeyUsagePurpose::DigitalSignature, - KeyUsagePurpose::ContentCommitment, // NonRepudiation - KeyUsagePurpose::KeyEncipherment, - KeyUsagePurpose::DataEncipherment, - KeyUsagePurpose::KeyAgreement, - KeyUsagePurpose::KeyCertSign, - KeyUsagePurpose::CrlSign, - KeyUsagePurpose::EncipherOnly, - KeyUsagePurpose::DecipherOnly, - ]; - - let cert = params.self_signed(&keys)?; - Ok(cert) -} - -/// Extract an RSA public key from a X.509 certificate -pub fn rsa_private_from_x509_certificate_from(der: &[u8]) -> PublicEncryptingKey { - let (_, cert) = X509Certificate::from_der(der).unwrap(); - let issuer_public_key = cert.public_key(); - cert.verify_signature(Some(issuer_public_key)).unwrap(); - PublicEncryptingKey::from_der(issuer_public_key.raw).unwrap() -} - -pub async fn key_management( - cfg: &EbicsHostCfg, - client: &ClientPriKeysFile, - http: &Client, - ebics_logger: &EbicsLogger, - order: Order, -) -> anyhow::Result<EbicsResponse<Option<String>>> { - info!("Doing key request {order}"); - //val txLog = ebicsLogger.tx(order.name) - // TODO is this still necessary ? - - let (name, security_medium) = match order { - Order::INI | Order::HIA => ("ebicsUnsecuredRequest", "0200"), - Order::HPB => ("ebicsNoPubKeyDigestsRequest", "0000"), - }; - - fn xml_order_data( - cfg: &EbicsHostCfg, - name: &str, - schema: &str, - build: impl FnOnce(&mut XmlWriter), - ) -> String { - let xml = xml_build!(name ("xmlns":schema) ("xmlns:ds":"http://www.w3.org/2000/09/xmldsig#") { - @ build, - "PartnerID": &cfg.partner_id, - "UserID": &cfg.user_id - }); - // Deflate TODO write inside the compressor directly - let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default()); - encoder.write_all(xml.as_bytes()).unwrap(); - let compressed = encoder.finish().unwrap(); - BASE64_STANDARD.encode(&compressed) - } - - fn rsa_key_xml<K>(w: &mut XmlWriter, key: &K) - where - K: AsDer<Pkcs8V1Der<'static>>, - { - let der = key.as_der().unwrap(); - let b64 = BASE64_STANDARD.encode(der.as_ref()); - let lines = b64 - .as_bytes() - .chunks(64) - .map(|c| std::str::from_utf8(c).unwrap()) - .collect::<Vec<_>>() - .join("\n"); - let pem = - format!("-----BEGIN RSA PRIVATE KEY-----\n{lines}\n-----END RSA PRIVATE KEY-----\n"); - let cert = x509_certificate_from_rsa_private(&pem, "LibEuFin EBICS").unwrap(); - let der = cert.der(); - let b64 = BASE64_STANDARD.encode(der.as_ref()); - - xml!(w, "ds:X509Data" { - "ds:X509Certificate": &b64 - }); - } - let data = match order { - Order::INI => Some(xml_order_data( - cfg, - "SignaturePubKeyOrderData", - "http://www.ebics.org/S002", - |w| { - xml!(w, "SignaturePubKeyInfo" { - @ |w| rsa_key_xml(w, &client.signature_private_key), - "SignatureVersion": "A006" - }); - }, - )), - Order::HIA => Some(xml_order_data( - cfg, - "HIARequestOrderData", - "urn:org:ebics:H005", - |w| { - xml!(w, "AuthenticationPubKeyInfo" { - @ |w| rsa_key_xml(w, &client.authentication_private_key), - "AuthenticationVersion": "X002" - }, - "EncryptionPubKeyInfo" { - @ |w| rsa_key_xml(w, &client.encryption_private_key), - "EncryptionVersion": "E002" - }); - }, - )), - Order::HPB => None, - }; - let sign = order == Order::HPB; - let msg = xml_build!( - name - ("xmlns": "urn:org:ebics:H005") - ("xmlns:ds": "http://www.w3.org/2000/09/xmldsig#") - ("Version": "H005") - ("Revision": "1") - { - "header" ("authenticate": "true") { - "static" { - "HostID": &cfg.host_id, - @ |w: &mut XmlWriter| { - if order == Order::HPB { - let nonce: u128 = rand::random(); - xml!(w, - "Nonce": &format_args!("{:032x}", nonce), - "Timestamp": &jiff::Timestamp::now() - ); - } - }, - "PartnerID": &cfg.partner_id, - "UserID": &cfg.user_id, - "OrderDetails" { - "AdminOrderType": order - }, - "SecurityMedium": security_medium - }, - "mutable" - }, - @ |w: &mut XmlWriter| { - if sign { - xml!(w, "AuthSignature"); - } - }, - "body" { - @ |w: &mut XmlWriter| { - if let Some(data) = data { - xml!(w,"DataTransfer" { - "OrderData": data - }); - } - } - } - } - - ); - let signed = if sign { - sign_ebics(msg, &client.authentication_private_key) - } else { - msg - }; - let res = post_to_bank(cfg.base_url.as_str(), http, signed).await?; - Ok(XmlReader::parse( - &res, - "ebicsKeyManagementResponse", - |root| { - let technical_code = root - .one_signed("header") - .one("mutable") - .one("ReturnCode") - .text() - .parse() - .unwrap(); - let body = root.one("body"); - let bank_code = body.one_signed("ReturnCode").text().parse().unwrap(); - let content = if let Some(data) = body.opt("DataTransfer") { - let info = data.one_signed("DataEncryptionInfo"); - let info = DataEncryptionInfo { - transaction_key: info.one("TransactionKey").b64(), - bank_pub_digest: info.one("EncryptionPubKeyDigest").b64(), - }; - let chunk = data.one("OrderData").b64(); - let decoded = decrypt_and_decompress_payload( - &client.encryption_private_key, - info, - vec![chunk], - ); - Some(String::from_utf8(decoded).unwrap()) - } else { - None - }; - EbicsResponse { - technical_code, - bank_code, - content, - } - }, - )) -} - -struct DataEncryptionInfo { - transaction_key: Vec<u8>, - bank_pub_digest: Vec<u8>, -} - -/** Decrypts and decompresses EBICS BTS payload */ -fn decrypt_and_decompress_payload( - client_encryption_key: &PrivateDecryptingKey, - encryption_info: DataEncryptionInfo, - segments: Vec<Vec<u8>>, -) -> Vec<u8> { - // TODO check bank_pub_digest - let tx_key = decrypt_ebics_e002_key( - client_encryption_key.clone(), - &encryption_info.transaction_key, - ); - let mut decoder = ZlibDecoder::new(Vec::new()); - for segment in segments { - let decrypted = decrypt_ebics_e002(&tx_key, segment); - decoder.write_all(&decrypted).unwrap(); - } - decoder.finish().unwrap() -} - -pub fn decrypt_ebics_e002(transaction_key: &DecryptingKey, mut encrypted_data: Vec<u8>) -> Vec<u8> { - // AES-CBC with a zero IV, as in the Kotlin original. - let iv = [0u8; 16]; - - let plaintext = transaction_key - .decrypt( - &mut encrypted_data, - DecryptionContext::Iv128(FixedLength::from(iv)), - ) - .unwrap(); - - // Strip X9.23 / ANSI X9.23 padding: - // The last byte holds the number of padding bytes to remove. - let pad_len = *plaintext.last().unwrap() as usize; - if pad_len == 0 || pad_len > 16 || pad_len > plaintext.len() { - panic!("WTF"); - } - let decoded = plaintext.len() - pad_len; - encrypted_data.truncate(decoded); - encrypted_data -} - -fn decrypt_ebics_e002_key( - private_key: PrivateDecryptingKey, - encrypted_transaction_key: &[u8], -) -> DecryptingKey { - let private_key = Pkcs1PrivateDecryptingKey::new(private_key).unwrap(); - let mut plaintext = vec![0u8; private_key.min_output_size()]; - let cipher = private_key - .decrypt(&encrypted_transaction_key, &mut plaintext) - .unwrap(); - let cipher_key = UnboundCipherKey::new(&aws_lc_rs::cipher::AES_128, &cipher).unwrap(); - DecryptingKey::cbc(cipher_key).unwrap() -} - pub async fn hev(http: &Client, cfg: &EbicsHostCfg) -> anyhow::Result<Vec<VersionNumber>> { let phase = "HEV"; info!(target: "ebics", "Doing administrative request {phase}"); @@ -505,60 +163,6 @@ pub async fn hev(http: &Client, cfg: &EbicsHostCfg) -> anyhow::Result<Vec<Versio .ok_or_fail(phase) } -pub async fn hpb( - http: &Client, - cfg: &EbicsHostCfg, - logger: &EbicsLogger, - client: &ClientPriKeysFile, -) -> anyhow::Result<BankPubKeysFile> { - let order = Order::HPB; - let res = key_management(cfg, client, http, logger, order).await?; - if res.technical_code == EbicsReturnCode::EBICS_AUTHENTICATION_FAILED { - bail!( - "{order} status code {}: could not download bank keys, send client keys (and/or related PDF document with --generate-registration-pdf) to the bank", - res.technical_code - ) - } - let order_data = res - .ok_or_fail(order.name())? - .expect("{order}: missing order data"); - - fn rsa_pub_key(xml: XmlReader) -> PublicEncryptingKey { - let der = xml.one("X509Data").one("X509Certificate").b64(); - rsa_private_from_x509_certificate_from(&der) - } - - Ok(XmlReader::parse( - &order_data, - "HPBResponseOrderData", - |root| { - let auth_pub_info = root.one("AuthenticationPubKeyInfo"); - let version = auth_pub_info.one("AuthenticationVersion"); - let version = version.text(); - assert_eq!( - version, "X002", - "Expected authentication version X002 got unsupported {version}" - ); - let auth_pub = rsa_pub_key(auth_pub_info); - - let enc_pub_info = root.one("EncryptionPubKeyInfo"); - let version = enc_pub_info.one("EncryptionVersion"); - let version = version.text(); - assert_eq!( - version, "E002", - "Expected encryption version E002 got unsupported {version}" - ); - let enc_pub = rsa_pub_key(enc_pub_info); - - BankPubKeysFile { - bank_authentication_public_key: auth_pub, - bank_encryption_public_key: enc_pub, - accepted: false, - } - }, - )) -} - #[derive(Debug, thiserror::Error)] pub enum EbicsError { #[error(transparent)] diff --git a/src/xml.rs b/src/xml.rs @@ -23,29 +23,29 @@ use base64::{Engine, prelude::BASE64_STANDARD}; use roxmltree::Document; #[macro_export] -macro_rules! xml { +macro_rules! xml_el { // Text element ($w:ident, $name:tt $(($k:tt: $v:tt))* : $content:expr $(, $($rest:tt)*)?) => { $w.text($name, &[$(($k, $v)),*], $content); - $(xml!($w, $($rest)*);)* + $($crate::xml_el!($w, $($rest)*);)* }; // Nested block ($w:ident, $name:tt $(($k:tt: $v:tt))* { $($body:tt)* }$(, $($rest:tt)*)?) => { let name = $name; $w.open(name, &[$(($k, $v)),*]); - xml!($w, $($body)*); + $crate::xml_el!($w, $($body)*); $w.close(name); - $(xml!($w, $($rest)*);)* + $($crate::xml_el!($w, $($rest)*);)* }; // Empty element ($w:ident, $name:tt $(($k:tt: $v:tt))* $(, $($rest:tt)*)?) => { $w.empty($name, &[$(($k, $v)),*]); - $(xml!($w, $($rest)*);)* + $($crate::xml_el!($w, $($rest)*);)* }; // Logic escape ($w:ident, @ $logic:expr$(, $($rest:tt)*)?) => { ($logic)($w); - $(xml!($w, $($rest)*);)* + $($crate::xml_el!($w, $($rest)*);)* }; } @@ -53,11 +53,11 @@ macro_rules! xml { macro_rules! xml_build { ($name:tt $(($k:tt: $v:tt))* { $($body:tt)* }) => { { - let mut writer = crate::xml::XmlWriter::init(); + let mut writer = $crate::xml::XmlWriter::init(); let w = &mut writer; let name = $name; w.open(name, &[$(($k, $v)),*]); - xml!(w, $($body)*); + $crate::xml_el!(w, $($body)*); w.close(name); writer.finish() } @@ -119,9 +119,9 @@ impl XmlWriter { } /// Write XML text content following XML escape rules -impl<'a> std::fmt::Write for XmlWriter { +impl std::fmt::Write for XmlWriter { fn write_str(&mut self, s: &str) -> std::fmt::Result { - if s.contains(&['<', '>', '&', '\'', '"']) { + if s.contains(['<', '>', '&', '\'', '"']) { for c in s.chars() { match c { '<' => self.buff.push_str("&lt;"), @@ -200,9 +200,7 @@ impl<'node, 'input> XmlReader<'node, 'input> { .node .children() .filter(|children| children.has_tag_name(tag)); - let Some(node) = iter.next() else { - return None; - }; + let node = iter.next()?; if iter.next().is_some() { let count = iter.count() + 2; panic!( @@ -269,7 +267,7 @@ mod test { #[test] pub fn modularity() { fn module(w: &mut XmlWriter) { - xml!(w, "module"); + xml_el!(w, "module"); } assert_eq!( xml_build!("root" { @ |w| module(w) }), @@ -284,7 +282,7 @@ mod test { "endOfDocument" { @ |w: &mut XmlWriter| { for i in 1..=10 { - xml!(w, (&format!("e{i}")) { + xml_el!(w, (&format!("e{i}")) { (&format!("e{i}{i}")): &format_args!("{i}{i}{i}") }); }