commit 646078079a51bdedc25dafdd0d24cb251f22840c
parent 23bdb9d2a077552937c1d2697fa82909bd5e95e4
Author: Antoine A <>
Date: Fri, 24 Apr 2026 10:37:57 +0200
refactor
Diffstat:
| A | src/common.rs | | | 51 | +++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | src/crypto.rs | | | 108 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | src/key_management.rs | | | 325 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | src/main.rs | | | 412 | ++----------------------------------------------------------------------------- |
| M | src/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("<"),
@@ -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}")
});
}