commit b3043819e7e2af8f18b2c434b7266c25dcace578
parent 3294a1ff9b7cd43b9ec72ea4a9bfaae04931a914
Author: Antoine A <>
Date: Tue, 8 Sep 2026 11:06:46 +0200
ebics: many fixes
Diffstat:
8 files changed, 109 insertions(+), 54 deletions(-)
diff --git a/libeufin-ebics/sample/platform/postfinance_camt054.xml b/libeufin-ebics/sample/platform/postfinance_camt054.xml
@@ -111,7 +111,7 @@
<Refs>
<AcctSvcrRef>231121CH0AZWCVR1</AcctSvcrRef>
<EndToEndId>NOTPROVIDED</EndToEndId>
- <UETR>62e2b511-7313-4ccd-8d40-c9d8e612cd71</UETR>
+ <UETR>62e2b511-7313-4ccd-8d40-c9d8e612cd72</UETR>
</Refs>
<Amt Ccy="CHF">2.53</Amt>
<CdtDbtInd>CRDT</CdtDbtInd>
diff --git a/libeufin-ebics/src/crypto.rs b/libeufin-ebics/src/crypto.rs
@@ -91,7 +91,6 @@ pub fn rsa_private_from_b64_x509_certificate(encoded: &str) -> anyhow::Result<Pu
let der = base64::decode(encoded)?;
let (_, cert) = X509Certificate::from_der(&der)?;
let issuer_public_key = cert.public_key();
- cert.verify_signature(Some(issuer_public_key))?;
Ok(PublicKey::from_der(issuer_public_key.raw)?)
}
diff --git a/libeufin-ebics/src/ebics.rs b/libeufin-ebics/src/ebics.rs
@@ -22,7 +22,9 @@ use std::{borrow::Cow, io::Write as _};
use aws_lc_rs::{
digest::Digest,
encoding::AsDer,
+ error::KeyRejected,
rsa::{self, PrivateDecryptingKey, PublicKey},
+ signature::KeyPair,
};
use compact_str::CompactString;
use flate2::write::ZlibDecoder;
@@ -36,20 +38,21 @@ use roxmltree::Document;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use sqlx::PgPool;
use taler_common::encoding::base64;
+use thiserror::Error;
use tracing::{debug, info, trace, warn};
use crate::{
cli::EbicsLogs,
config::EbicsHostCfg,
crypto::{
- decrypt_ebics_e002, decrypt_ebics_e002_key, digest_ebics_order_a006, encrypt_ebics_e002,
- gen_ebics_e002_key, sign_ebics_a006,
+ decrypt_ebics_e002, decrypt_ebics_e002_key, digest_ebics_order_a006, ebics_pub_key_hash,
+ encrypt_ebics_e002, gen_ebics_e002_key, sign_ebics_a006,
},
db::{ebics_first, ebics_register, ebics_remove},
ebics::{
administrative::{HAA, HKD, VersionNumber, hev_msg, parse_haa, parse_hev, parse_hkd},
bts::{
- DInit, DTransfer, DataEncryptionInfo, U, d_init, d_transfer, parse_d_init,
+ DInit, DTransfer, DataEncryptionInfo, UInit, d_init, d_transfer, parse_d_init,
parse_d_transfer, parse_receipt, parse_u_init, parse_u_transfer, receipt, u_init,
u_transfer,
},
@@ -215,7 +218,7 @@ fn fmt_code(
if technical.is_error() {
write!(f, "technical error: {technical}")
} else {
- write!(f, "technical error: {bank}")
+ write!(f, "bank error: {bank}")
}
}
@@ -258,6 +261,9 @@ pub enum EbicsErrKind {
#[error(transparent)]
Signature(#[from] VerifyError),
+ #[error(transparent)]
+ DecDe(#[from] DecDeErr),
+
#[error("{0}")]
Custom(Cow<'static, str>),
}
@@ -278,7 +284,7 @@ impl EbicsErrKind {
pub struct EbicsResponse<T> {
pub technical_code: EbicsReturnCode,
pub bank_code: EbicsReturnCode,
- pub technical_text: String,
+ pub technical_text: CompactString,
pub content: Option<T>,
}
@@ -434,7 +440,7 @@ impl<'a> EbicsClient<'a> {
while let Some(tx_id) = ebics_first(db).await.ctx(&ctx)? {
let ctx = EbicsCtx::new(order).interrupt(&tx_id);
let xml = receipt(&self.cfg, client, order, &tx_id, false);
- if let Err(e) = self.post_bts(xml, &ctx, &bank.auth, parse_d_init).await {
+ if let Err(e) = self.post_bts(xml, &ctx, &bank.auth, parse_receipt).await {
if !matches!(
e.kind,
// Transaction already closed or expired - EBICS protocol error
@@ -465,19 +471,22 @@ impl<'a> EbicsClient<'a> {
ebics_register(db, &tx_id).await.ctx(&ctx)?;
// Transfer phase
- let mut segments = vec![segment];
+ let mut payload = segment;
for segment_nb in 2..=nb_segments {
ctx = ctx.transfer(&tx_id, segment_nb);
let xml = d_transfer(&self.cfg, client, order, nb_segments, segment_nb, &tx_id);
let DTransfer { segment, .. } = self
.post_bts(xml, &ctx, &bank.auth, parse_d_transfer)
.await?;
- segments.push(segment);
+ payload.extend(segment);
}
// Processing phase
ctx = ctx.process(&tx_id);
- let payload = decrypt_and_decompress_payload(&client.enc, data_encryption_info, segments);
+ let payload = decrypt_and_decompress_payload(&client.enc, data_encryption_info, payload)
+ .map_err(|e| {
+ EbicsErrKind::Custom(Cow::Owned(format!("decrypt & decompress: {e}"))).ctx(&ctx)
+ })?;
self.logger.log_payload(&ctx, &payload, order.file_type())?;
let res = processing(payload).await.ctx(&ctx);
@@ -519,7 +528,7 @@ impl<'a> EbicsClient<'a> {
// Init phase
ctx = ctx.init();
let xml = u_init(&self.cfg, bank, client, order, &payload);
- let U { tx_id, order_id } = self.post_bts(xml, &ctx, &bank.auth, parse_u_init).await?;
+ let UInit { tx_id, order_id } = self.post_bts(xml, &ctx, &bank.auth, parse_u_init).await?;
// Transfer phase
for segment_nb in 1..=payload.nb_segments() {
@@ -554,22 +563,28 @@ impl PreparedUploadData {
}
}
+#[derive(Debug, Error)]
+pub enum DecDeErr {
+ #[error(transparent)]
+ Zlib(#[from] std::io::Error),
+ #[error("bank E002 encryption public-key digest mismatch")]
+ Digest,
+ #[error(transparent)]
+ Key(#[from] KeyRejected),
+}
+
/** Decrypts and decompresses EBICS BTS payload */
fn decrypt_and_decompress_payload(
key_pair: &rsa::KeyPair,
encryption_info: DataEncryptionInfo,
- segments: Vec<Vec<u8>>,
-) -> Vec<u8> {
- let client_encryption_key =
- PrivateDecryptingKey::from_pkcs8(key_pair.as_der().unwrap().as_ref()).unwrap();
- // TODO check bank_pub_digest
- let tx_key = decrypt_ebics_e002_key(client_encryption_key, &encryption_info.tx_key);
+ payload: Vec<u8>,
+) -> Result<Vec<u8>, DecDeErr> {
+ let enc_key = PrivateDecryptingKey::from_pkcs8(key_pair.as_der().unwrap().as_ref())?;
+ let tx_key = decrypt_ebics_e002_key(enc_key, &encryption_info.tx_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()
+ let decrypted = decrypt_ebics_e002(&tx_key, payload);
+ decoder.write_all(&decrypted)?;
+ Ok(decoder.finish()?)
}
/** Signs, encrypts and format EBICS BTS payload */
@@ -708,7 +723,7 @@ pub async fn tx_check(
)
.await
{
- Ok(U { tx_id, .. }) => {
+ Ok(UInit { tx_id, .. }) => {
result.concurrent_fetch_and_submit = true;
let ctx = ctx.transfer(&tx_id, 1);
ebics
diff --git a/libeufin-ebics/src/ebics/bts.rs b/libeufin-ebics/src/ebics/bts.rs
@@ -285,7 +285,7 @@ pub fn u_transfer(
pub struct DataEncryptionInfo {
pub tx_key: Vec<u8>,
- pub bank_pub_digest: Vec<u8>,
+ pub enc_pub_digest: Vec<u8>,
}
fn expect_phase(n: Xml<'_>, phase: &str) -> xml::Result<()> {
@@ -336,7 +336,7 @@ pub fn parse_d_init(xml: Document) -> xml::Result<EbicsResponse<DInit>> {
tx_id: st.one("TransactionID").parse()?,
data_encryption_info: DataEncryptionInfo {
tx_key: enc_info.one("TransactionKey").b64()?,
- bank_pub_digest: enc_info.one("EncryptionPubKeyDigest").b64()?,
+ enc_pub_digest: enc_info.one("EncryptionPubKeyDigest").b64()?,
},
segment: data.one("OrderData").b64()?,
nb_segments: st.one("NumSegments").parse()?,
@@ -348,7 +348,6 @@ pub fn parse_d_init(xml: Document) -> xml::Result<EbicsResponse<DInit>> {
pub struct DTransfer {
pub tx_id: CompactString,
pub segment: Vec<u8>,
- pub nb_segments: usize,
}
pub fn parse_d_transfer(xml: Document) -> xml::Result<EbicsResponse<DTransfer>> {
@@ -380,7 +379,6 @@ pub fn parse_d_transfer(xml: Document) -> xml::Result<EbicsResponse<DTransfer>>
content: Some(DTransfer {
tx_id: st.one("TransactionID").parse()?,
segment: body.one("DataTransfer").one("OrderData").b64()?,
- nb_segments: st.one("NumSegments").parse()?,
}),
})
})
@@ -423,12 +421,12 @@ pub fn parse_receipt(xml: Document) -> xml::Result<EbicsResponse<Receipt>> {
})
}
-pub struct U {
+pub struct UInit {
pub tx_id: CompactString,
pub order_id: CompactString,
}
-pub fn parse_u_init(xml: Document) -> xml::Result<EbicsResponse<U>> {
+pub fn parse_u_init(xml: Document) -> xml::Result<EbicsResponse<UInit>> {
Xml::doc(xml, "ebicsResponse", |root| {
let header = root.one_signed("header")?;
let st = header.one("static")?;
@@ -454,7 +452,7 @@ pub fn parse_u_init(xml: Document) -> xml::Result<EbicsResponse<U>> {
technical_code,
bank_code,
technical_text,
- content: Some(U {
+ content: Some(UInit {
order_id: mutable.one("OrderID").parse()?,
tx_id: st.one("TransactionID").parse()?,
}),
@@ -462,7 +460,11 @@ pub fn parse_u_init(xml: Document) -> xml::Result<EbicsResponse<U>> {
})
}
-pub fn parse_u_transfer(xml: Document) -> xml::Result<EbicsResponse<U>> {
+pub struct UTransfer {
+ pub tx_id: CompactString,
+}
+
+pub fn parse_u_transfer(xml: Document) -> xml::Result<EbicsResponse<UTransfer>> {
Xml::doc(xml, "ebicsResponse", |root| {
let header = root.one_signed("header")?;
let st = header.one("static")?;
@@ -488,8 +490,7 @@ pub fn parse_u_transfer(xml: Document) -> xml::Result<EbicsResponse<U>> {
technical_code,
bank_code,
technical_text,
- content: Some(U {
- order_id: mutable.one("OrderID").parse()?,
+ content: Some(UTransfer {
tx_id: st.one("TransactionID").parse()?,
}),
})
diff --git a/libeufin-ebics/src/ebics/key_management.rs b/libeufin-ebics/src/ebics/key_management.rs
@@ -229,7 +229,7 @@ impl EbicsClient<'_> {
msg
};
let res = self.post_to_bank(signed, ctx).await?;
- Xml::parse(&res, "ebicsKeyManagementResponse", |root| {
+ let parsed = Xml::parse(&res, "ebicsKeyManagementResponse", |root| {
let body = root.one("body")?;
let mutable = root.one_signed("header").one("mutable")?;
Ok(EbicsResponse {
@@ -240,17 +240,29 @@ impl EbicsClient<'_> {
let info = data.one_signed("DataEncryptionInfo")?;
let info = DataEncryptionInfo {
tx_key: info.one("TransactionKey").b64()?,
- bank_pub_digest: info.one("EncryptionPubKeyDigest").b64()?,
+ enc_pub_digest: info.one("EncryptionPubKeyDigest").b64()?,
};
let chunk = data.one("OrderData").b64()?;
- let decoded = decrypt_and_decompress_payload(&client.enc, info, vec![chunk]);
- Some(decoded)
+ Some((info, chunk))
} else {
None
}),
})
})
- .ctx(ctx)
+ .ctx(ctx)?;
+ let decoded = match parsed.content {
+ Some(Some((info, chunk))) => Some(Some(
+ decrypt_and_decompress_payload(&client.enc, info, chunk).ctx(ctx)?,
+ )),
+ Some(None) => Some(None),
+ None => None,
+ };
+ Ok(EbicsResponse {
+ content: decoded,
+ technical_code: parsed.technical_code,
+ bank_code: parsed.bank_code,
+ technical_text: parsed.technical_text,
+ })
}
}
diff --git a/libeufin-ebics/src/iso20022/camt.rs b/libeufin-ebics/src/iso20022/camt.rs
@@ -215,8 +215,28 @@ fn incoming_id(n: Xml, sref: Option<&str>) -> xml::Result<InId> {
/** Parse transaction wire transfer subject */
fn wire_transfer_subject(n: Xml) -> xml::Result<Option<String>> {
- Ok(n.opt("RmtInf")?
- .map(|n| n.many("Ustrd").map(|n| n.text()).collect::<String>()))
+ Ok(n.opt("RmtInf")?.map(|n| {
+ // Concat all lines
+ let mut s = n.many("Ustrd").map(|n| n.text()).collect::<String>();
+ // Merge whitespace
+ let mut pending_space = false;
+ s.retain(|c| {
+ if c.is_ascii_whitespace() {
+ if pending_space {
+ false
+ } else {
+ pending_space = true;
+ true
+ }
+ } else if pending_space {
+ pending_space = false;
+ true
+ } else {
+ true
+ }
+ });
+ s
+ }))
}
/** Parse and format transaction return reasons */
@@ -677,7 +697,7 @@ pub mod test {
),
tx_in(
(
- Some("62e2b511-7313-4ccd-8d40-c9d8e612cd71"),
+ Some("62e2b511-7313-4ccd-8d40-c9d8e612cd72"),
None,
Some("231121CH0AZWCVR1"),
),
diff --git a/libeufin-ebics/src/ws.rs b/libeufin-ebics/src/ws.rs
@@ -91,6 +91,7 @@ pub enum WssNotification {
partnerid: String,
userid: Option<String>,
btf: Vec<WssNotificationBTF>,
+ #[serde(default)]
ordertype: Vec<String>,
},
}
@@ -171,7 +172,7 @@ pub async fn listen_for_notification(
) {
let mut backoff = ExpoBackoffDecorr::new(Duration::from_secs(30), Duration::from_mins(30), 2.5);
loop {
- let res: Result<(), anyhow::Error> = async {
+ let res: Result<bool, anyhow::Error> = async {
let res = ebics
.download(
db,
@@ -201,7 +202,7 @@ pub async fn listen_for_notification(
) {
// Failure is expected if this wss is not supported
info!(target: "ws", "Real-time EBICS notifications is not supported");
- return Ok(());
+ return Ok(true);
} else {
return Err(e.into());
}
@@ -238,14 +239,19 @@ pub async fn listen_for_notification(
}
})
.await?;
- Ok(())
+ Ok(false)
}
.await;
- if let Err(e) = res {
- error!(target: "ws", "{e}");
- tokio::time::sleep(backoff.backoff()).await;
- } else {
- return;
+ match res {
+ Ok(stop) => {
+ if stop {
+ return;
+ }
+ }
+ Err(e) => {
+ error!(target: "ws", "{e}");
+ tokio::time::sleep(backoff.backoff()).await;
+ }
}
}
}
diff --git a/testbench/src/main.rs b/testbench/src/main.rs
@@ -306,8 +306,8 @@ async fn main() -> anyhow::Result<()> {
match cmd.component {
Component::Nexus => {
let mut repl = Repl::new(".nexus_history");
+ auto_setup().await;
loop {
- auto_setup().await;
if let Some(cmd) = repl.read_line(&prompt, "") {
match cmd {
NexusCmd::Setup => {
@@ -337,10 +337,12 @@ async fn main() -> anyhow::Result<()> {
run(&format!("list {}", raw_args.join(" "))).await;
}
NexusCmd::ResetKeys => {
- if test {
+ if test && std::fs::exists(&ebics.client).unwrap() {
std::fs::remove_file(&ebics.client).unwrap();
}
- std::fs::remove_file(&ebics.bank).unwrap();
+ if std::fs::exists(&ebics.bank).unwrap() {
+ std::fs::remove_file(&ebics.bank).unwrap();
+ }
}
NexusCmd::TxCheck => {
run(&format!("testing tx-check {ebics_log}")).await;
@@ -380,8 +382,8 @@ async fn main() -> anyhow::Result<()> {
}
Component::Ebisync => {
let mut repl = Repl::new(".ebisync_history");
+ auto_setup().await;
loop {
- auto_setup().await;
if let Some(cmd) = repl.read_line(&prompt, "") {
match cmd {
SyncCmd::Compact => {