libeufin

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

test.rs (19784B)


      1 /*
      2 * This file is part of LibEuFin.
      3 * Copyright (C) 2026 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 use std::{
     21     fs::Permissions,
     22     os::unix::fs::PermissionsExt as _,
     23     sync::{Arc, Mutex},
     24     time::Duration,
     25 };
     26 
     27 use aws_lc_rs::rsa::{KeyPair, KeySize, PublicKey};
     28 use axum::{body::Bytes, response::IntoResponse as _, routing::post};
     29 use compact_str::CompactString;
     30 use jiff::{
     31     Timestamp, Zoned,
     32     civil::{Date, date},
     33     tz::TimeZone,
     34 };
     35 use reqwest::StatusCode;
     36 use taler_api::{Serve, api::TalerRouter as _};
     37 use taler_common::encoding::base64;
     38 use tempfile::{TempDir, tempdir};
     39 use tokio::net::UnixStream;
     40 
     41 use crate::{
     42     crypto::{ebics_pub_key_hash, encrypt_ebics_e002, gen_ebics_e002_key},
     43     ebics::{
     44         key_management::{rsa_key_xml, rsa_pub_key},
     45         rand_ebics_id,
     46     },
     47     utils::{deflate, inflate},
     48     xml,
     49     xml::{Xml, XmlAccess},
     50     xml_sign::sign_ebics,
     51 };
     52 
     53 pub async fn wait_for_unix_socket(path: &str) {
     54     for _ in 0..100 {
     55         if UnixStream::connect(path).await.is_ok() {
     56             return;
     57         }
     58         tokio::time::sleep(Duration::from_millis(10)).await;
     59     }
     60     panic!("{path} never becomed active")
     61 }
     62 
     63 pub type Sequence = fn(&mut EbicsState, body: &[u8]) -> EbicsRes;
     64 
     65 pub enum EbicsRes {
     66     Ok(String),
     67     BadRequest,
     68     Failure,
     69 }
     70 pub struct EbicsState {
     71     bank: KeyPair,
     72 
     73     client_sign: Option<PublicKey>,
     74     client_enc: Option<PublicKey>,
     75     client_auth: Option<PublicKey>,
     76 
     77     tx_id: Option<CompactString>,
     78     order_id: Option<CompactString>,
     79 }
     80 
     81 impl EbicsState {
     82     pub fn new() -> Self {
     83         Self {
     84             bank: KeyPair::generate(KeySize::Rsa2048).unwrap(),
     85             client_sign: None,
     86             client_enc: None,
     87             client_auth: None,
     88             tx_id: None,
     89             order_id: None,
     90         }
     91     }
     92 
     93     fn parse_unsecure_request(
     94         body: &[u8],
     95         order: &str,
     96         root: &str,
     97         parse: impl FnOnce(Xml) -> xml::Result<()>,
     98     ) {
     99         Xml::parse(body, "ebicsUnsecuredRequest", |n| {
    100             let admin_order = n
    101                 .one("header")
    102                 .one("static")
    103                 .one("OrderDetails")
    104                 .one("AdminOrderType")?
    105                 .text();
    106             assert_eq!(admin_order, order);
    107             let chunk = n.one("body").one("DataTransfer").one("OrderData").b64()?;
    108             let inflated = inflate(&chunk);
    109             Xml::parse(&inflated, root, parse)
    110         })
    111         .unwrap()
    112     }
    113 
    114     fn parse_download_init(body: &[u8], order: &str) {
    115         Xml::parse(body, "ebicsRequest", |root| {
    116             let header = root.one("header")?;
    117             let admin_order = header
    118                 .one("static")
    119                 .one("OrderDetails")
    120                 .one("AdminOrderType")?
    121                 .text();
    122             assert_eq!(admin_order, order);
    123             let phase = header.one("mutable").one("TransactionPhase")?.text();
    124             assert_eq!(phase, "Initialisation");
    125             Ok(())
    126         })
    127         .unwrap();
    128     }
    129 
    130     fn signed_response(&self, xml: String) -> EbicsRes {
    131         EbicsRes::Ok(sign_ebics(xml, &self.bank).expect("EBICS signature never fails"))
    132     }
    133 
    134     fn ebics_response_payload(&mut self, payload: &str, last: bool) -> EbicsRes {
    135         let tx_id = self.tx_id.insert(rand_ebics_id());
    136         let deflated = deflate(payload.as_bytes());
    137         let (tx_key, encrypted_key) = gen_ebics_e002_key(self.client_enc.as_ref().unwrap());
    138         let encrypted = encrypt_ebics_e002(&tx_key, deflated);
    139         let xml = xml!("ebicsResponse" "xmlns"="http://www.ebics.org/H005" "xmlns:ds"="http://www.w3.org/2000/09/xmldsig#" {
    140             "header" "authenticate"="true" {
    141                 "static" {
    142                     "TransactionID": tx_id,
    143                     "NumSegments": "1"
    144                 },
    145                 "mutable" {
    146                     "TransactionPhase": "Initialisation",
    147                     "SegmentNumber" "lastSegment"=last : 1,
    148                     "ReturnCode": "000000",
    149                     "ReportText": "[EBICS_OK] OK"
    150                 }
    151             },
    152             "AuthSignature",
    153             "body" {
    154                 "DataTransfer" {
    155                     "DataEncryptionInfo" "authenticate"="true" {
    156                         "EncryptionPubKeyDigest" "Version"="E002" "Algorithm"="http://www.w3.org/2001/04/xmlenc#sha256": base64::fmt(ebics_pub_key_hash(self.client_enc.as_ref().unwrap())),
    157                         "TransactionKey": base64::fmt(encrypted_key)
    158                     },
    159                     "OrderData": base64::fmt(encrypted)
    160                 },
    161                 "ReturnCode" "authenticate"="true": "000000"
    162             }
    163         });
    164         self.signed_response(xml)
    165     }
    166 
    167     fn ebics_response_no_data(&self) -> EbicsRes {
    168         let xml = xml!("ebicsResponse" "xmlns"="http://www.ebics.org/H005" "xmlns:ds"="http://www.w3.org/2000/09/xmldsig#" {
    169             "header" "authenticate"="true" {
    170                 "static",
    171                 "mutable" {
    172                     "TransactionPhase": "Initialisation",
    173                     "ReturnCode": "000000",
    174                     "ReportText": "[EBICS_OK] OK"
    175                 }
    176             },
    177             "AuthSignature",
    178             "body" {
    179                 "ReturnCode" "authenticate"="true": "090005"
    180             }
    181         });
    182         self.signed_response(xml)
    183     }
    184 
    185     pub fn hev(&mut self, body: &[u8]) -> EbicsRes {
    186         Xml::parse(body, "ebicsHEVRequest", |root| {
    187             root.one("HostID")?;
    188             Ok(())
    189         })
    190         .unwrap();
    191         EbicsRes::Ok(
    192             xml!("ebicsHEVResponse" "xmlns"="http://www.ebics.org/H000" {
    193                 "SystemReturnCode" {
    194                     "ReturnCode": "000000",
    195                     "ReportText": "[EBICS_OK] OK"
    196                 },
    197                 "VersionNumber" "ProtocolVersion"="H005" : "03.00"
    198             }),
    199         )
    200     }
    201 
    202     pub fn ini(&mut self, body: &[u8]) -> EbicsRes {
    203         Self::parse_unsecure_request(body, "INI", "SignaturePubKeyOrderData", |root| {
    204             let n = root.one("SignaturePubKeyInfo")?;
    205             assert_eq!(n.one("SignatureVersion")?.text(), "A006");
    206             self.client_sign = Some(rsa_pub_key(n)?);
    207             Ok(())
    208         });
    209         EbicsRes::Ok(
    210             xml!("ebicsKeyManagementResponse" "xmlns"="http://www.ebics.org/H000" {
    211                 "header" "authenticate"="true" {
    212                     "mutable" {
    213                         "ReturnCode": "000000",
    214                         "ReportText": "[EBICS_OK] OK"
    215                     }
    216                 },
    217                 "body" {
    218                     "ReturnCode" "authenticate"="true" : "000000"
    219                 }
    220             }),
    221         )
    222     }
    223 
    224     pub fn hia(&mut self, body: &[u8]) -> EbicsRes {
    225         Self::parse_unsecure_request(body, "HIA", "HIARequestOrderData", |root| {
    226             let n = root.one("AuthenticationPubKeyInfo")?;
    227             assert_eq!(n.one("AuthenticationVersion")?.text(), "X002");
    228             self.client_auth = Some(rsa_pub_key(n)?);
    229 
    230             let n = root.one("EncryptionPubKeyInfo")?;
    231             assert_eq!(n.one("EncryptionVersion")?.text(), "E002");
    232             self.client_enc = Some(rsa_pub_key(n)?);
    233             Ok(())
    234         });
    235         EbicsRes::Ok(
    236             xml!("ebicsKeyManagementResponse" "xmlns"="http://www.ebics.org/H000" {
    237                 "header" "authenticate"="true" {
    238                     "mutable" {
    239                         "ReturnCode": "000000",
    240                         "ReportText": "[EBICS_OK] OK"
    241                     }
    242                 },
    243                 "body" {
    244                     "ReturnCode" "authenticate"="true" : "000000"
    245                 }
    246             }),
    247         )
    248     }
    249 
    250     pub fn hpb(&mut self, body: &[u8]) -> EbicsRes {
    251         // Parse HPB request
    252         Xml::parse(body, "ebicsNoPubKeyDigestsRequest", |root| {
    253             let order = root
    254                 .one("header")
    255                 .one("static")
    256                 .one("OrderDetails")
    257                 .one("AdminOrderType")?
    258                 .text();
    259             assert_eq!(order, "HPB");
    260             Ok(())
    261         })
    262         .unwrap();
    263 
    264         let payload = xml!("HPBResponseOrderData" "xmlns:ds"="http://www.w3.org/2000/09/xmldsig#" {
    265             "AuthenticationPubKeyInfo" {
    266                 @ |w| rsa_key_xml(w, &self.bank),
    267                 "AuthenticationVersion": "X002"
    268             },
    269             "EncryptionPubKeyInfo" {
    270                 @ |w| rsa_key_xml(w, &self.bank),
    271                 "EncryptionVersion": "E002"
    272             }
    273         });
    274         let deflated = deflate(payload.as_bytes());
    275         let (tx_key, encrypted_key) = gen_ebics_e002_key(self.client_enc.as_ref().unwrap());
    276         let encrypted = encrypt_ebics_e002(&tx_key, deflated);
    277         EbicsRes::Ok(
    278             xml!("ebicsKeyManagementResponse" "xmlns:ds"="http://www.w3.org/2000/09/xmldsig#" "xmlns"="http://www.ebics.org/H005" {
    279                 "header" "authenticate"="true"{
    280                     "mutable" {
    281                         "ReturnCode": "000000",
    282                         "ReportText": "[EBICS_OK] OK"
    283                     }
    284                 },
    285                 "body" {
    286                     "DataTransfer" {
    287                         "DataEncryptionInfo" "authenticate"="true" {
    288                             "EncryptionPubKeyDigest" "Version"="E002" "Algorithm"="http://www.w3.org/2001/04/xmlenc#sha256": base64::fmt(ebics_pub_key_hash(self.client_enc.as_ref().unwrap())),
    289                             "TransactionKey": base64::fmt(encrypted_key)
    290                         },
    291                         "OrderData": base64::fmt(encrypted)
    292                     },
    293                     "ReturnCode" "authenticate"="true": "000000"
    294                 }
    295             }),
    296         )
    297     }
    298 
    299     pub fn hkd(&mut self, body: &[u8]) -> EbicsRes {
    300         Self::parse_download_init(body, "HKD");
    301         self.ebics_response_payload(
    302             &xml!("HKDResponseOrderData" {
    303                 "PartnerInfo" {
    304                     "AddressInfo",
    305                     "OrderInfo" {
    306                         "AdminOrderType": "BTD",
    307                         "Service" {
    308                             "ServiceName": "STM",
    309                             "Scope": "CH",
    310                             "Container" "containerType"="ZIP",
    311                             "MsgName" "version"="08": "camt.052"
    312                         },
    313                         "Description"
    314                     },
    315                     "OrderInfo" {
    316                         "AdminOrderType": "BTU",
    317                         "Service" {
    318                             "ServiceName": "SCT",
    319                             "MsgName": "pain.001"
    320                         },
    321                         "Description": "Direct Debit"
    322                     },
    323                     "OrderInfo" {
    324                         "AdminOrderType": "BTU",
    325                         "Service" {
    326                             "ServiceName": "SCI",
    327                             "Scope": "DE",
    328                             "MsgName": "pain.001"
    329                         },
    330                         "Description": "Instant Direct Debit"
    331                     }
    332                 }
    333             }),
    334             true,
    335         )
    336     }
    337 
    338     pub fn haa(&mut self, body: &[u8]) -> EbicsRes {
    339         Self::parse_download_init(body, "HAA");
    340         self.ebics_response_payload(
    341             &xml!("HAAResponseOrderData" {
    342                 "Service" {
    343                     "ServiceName": "STM",
    344                     "Scope": "CH",
    345                     "Container" "containerType"="ZIP",
    346                     "MsgName" "version"="08": "camt.052"
    347                 }
    348             }),
    349             true,
    350         )
    351     }
    352 
    353     fn receipt(&mut self, body: &[u8], ok: bool) -> EbicsRes {
    354         Xml::parse(body, "ebicsRequest", |root| {
    355             let header = root.one("header")?;
    356             let tx_id = header.one("static").one("TransactionID")?.text();
    357             assert_eq!(tx_id, self.tx_id.as_deref().unwrap());
    358             let phase = header.one("mutable").one("TransactionPhase")?.text();
    359             assert_eq!(phase, "Receipt");
    360             let code = root
    361                 .one("body")
    362                 .one("TransferReceipt")
    363                 .one("ReceiptCode")?
    364                 .text();
    365             if ok {
    366                 assert_eq!(code, "0")
    367             } else {
    368                 assert_eq!(code, "1")
    369             }
    370             Ok(())
    371         })
    372         .unwrap();
    373         let tx_id = self.tx_id.take().unwrap();
    374         self.signed_response(xml!("ebicsResponse" "xmlns"="http://www.ebics.org/H005" "xmlns:ds"="http://www.w3.org/2000/09/xmldsig#" {
    375                 "header" "authenticate"="true" {
    376                     "static" {
    377                         "TransactionID": tx_id
    378                     },
    379                     "mutable" {
    380                         "TransactionPhase": "Receipt",
    381                         "ReturnCode": "000000",
    382                         "ReportText": "[EBICS_OK] OK",
    383                     }
    384                 },
    385                 "AuthSignature",
    386                 "body" {
    387                     "ReturnCode" "authenticate"="true": "000000"
    388                 }
    389             }))
    390     }
    391 
    392     pub fn receipt_ok(&mut self, body: &[u8]) -> EbicsRes {
    393         self.receipt(body, true)
    394     }
    395 
    396     pub fn receipt_err(&mut self, body: &[u8]) -> EbicsRes {
    397         self.receipt(body, false)
    398     }
    399 
    400     fn btd_date_check(&self, body: &[u8], pinned: Option<Date>) -> EbicsRes {
    401         Xml::parse(body, "ebicsRequest", |root| {
    402             let header = root.one("header")?;
    403             let details = header.one("static").one("OrderDetails")?;
    404             let admin_order = details.one("AdminOrderType")?.text();
    405             assert_eq!(admin_order, "BTD");
    406             let start = details
    407                 .one("BTDOrderParams")
    408                 .opt("DateRange")
    409                 .opt("Start")
    410                 .parse()?;
    411             assert_eq!(start, pinned);
    412             let phase = header.one("mutable").one("TransactionPhase")?.text();
    413             assert_eq!(phase, "Initialisation");
    414             Ok(())
    415         })
    416         .unwrap();
    417         self.ebics_response_no_data()
    418     }
    419 
    420     pub fn btd_no_data(&mut self, body: &[u8]) -> EbicsRes {
    421         self.btd_date_check(body, None)
    422     }
    423 
    424     pub fn btd_no_data_now(&mut self, body: &[u8]) -> EbicsRes {
    425         self.btd_date_check(
    426             body,
    427             Some(Zoned::new(Timestamp::now(), TimeZone::UTC).date()),
    428         )
    429     }
    430 
    431     pub fn btd_no_data_pinned(&mut self, body: &[u8]) -> EbicsRes {
    432         self.btd_date_check(body, Some(date(2024, 6, 5)))
    433     }
    434 
    435     pub fn btu_init(&mut self, body: &[u8]) -> EbicsRes {
    436         Self::parse_download_init(body, "BTU");
    437         let tx_id = self.tx_id.insert(rand_ebics_id());
    438         let order_id = self.order_id.insert(rand_ebics_id());
    439         let xml = xml!("ebicsResponse" "xmlns"="http://www.ebics.org/H005" "xmlns:ds"="http://www.w3.org/2000/09/xmldsig#" {
    440             "header" "authenticate"="true" {
    441                 "static" {
    442                     "TransactionID": tx_id
    443                 },
    444                 "mutable" {
    445                     "TransactionPhase": "Initialisation",
    446                     "OrderID": order_id,
    447                     "ReturnCode": "000000",
    448                     "ReportText": "[EBICS_OK] OK",
    449                 }
    450             },
    451             "AuthSignature",
    452             "body" {
    453                 "ReturnCode" "authenticate"="true": "000000"
    454             }
    455         });
    456         self.signed_response(xml)
    457     }
    458 
    459     pub fn btu_payload(&mut self, body: &[u8]) -> EbicsRes {
    460         let tx_id = self.tx_id.as_ref().unwrap();
    461         let order_id = self.order_id.as_ref().unwrap();
    462         let segment_nb: CompactString = Xml::parse(body, "ebicsRequest", |root| {
    463             let header = root.one("header")?;
    464             let txid = header.one("static").one("TransactionID")?.text();
    465             assert_eq!(txid, tx_id);
    466             let mutable = header.one("mutable")?;
    467             let phase = mutable.one("TransactionPhase")?.text();
    468             assert_eq!(phase, "Transfer");
    469             mutable.one("SegmentNumber").parse()
    470         })
    471         .unwrap();
    472         self.signed_response(xml!("ebicsResponse" "xmlns"="http://www.ebics.org/H005" "xmlns:ds"="http://www.w3.org/2000/09/xmldsig#" {
    473                 "header" "authenticate"="true" {
    474                     "static" {
    475                         "TransactionID": tx_id
    476                     },
    477                     "mutable" {
    478                         "TransactionPhase": "Transfer",
    479                         "SegmentNumber": segment_nb,
    480                         "OrderID": order_id,
    481                         "ReturnCode": "000000",
    482                         "ReportText": "[EBICS_OK] OK",
    483                     }
    484                 },
    485                 "AuthSignature",
    486                 "body" {
    487                     "ReturnCode" "authenticate"="true": "000000"
    488                 }
    489             }))
    490     }
    491 
    492     pub fn init_tx(&mut self, _: &[u8]) -> EbicsRes {
    493         self.ebics_response_payload("", false)
    494     }
    495 
    496     pub fn failure(&mut self, _: &[u8]) -> EbicsRes {
    497         EbicsRes::Failure
    498     }
    499 
    500     pub fn bad_request(&mut self, _: &[u8]) -> EbicsRes {
    501         EbicsRes::BadRequest
    502     }
    503 }
    504 
    505 pub struct TestBank {
    506     pub dir: TempDir,
    507     pub sock_path: String,
    508     pub sequence: Arc<Mutex<Vec<Sequence>>>,
    509 }
    510 
    511 impl TestBank {
    512     pub async fn new() -> Self {
    513         let dir = tempdir().unwrap();
    514         let sock_path = dir.path().join("bank.sock").to_str().unwrap().to_string();
    515         let sequence = Arc::new(Mutex::new(Vec::new()));
    516         let server_sequence = sequence.clone();
    517         let bank = Arc::new(Mutex::new(EbicsState::new()));
    518         let serve = Serve::Unix {
    519             path: sock_path.clone(),
    520             permission: Permissions::from_mode(0o660),
    521         };
    522         tokio::spawn(async move {
    523             axum::Router::new()
    524                 .route(
    525                     "/",
    526                     post(async move |body: Bytes| {
    527                         let sequence: Sequence = server_sequence.lock().unwrap().pop().unwrap();
    528                         let mut bank = bank.lock().unwrap();
    529                         let res = sequence(&mut bank, &body);
    530                         match res {
    531                             EbicsRes::Ok(xml) => xml.into_response(),
    532                             EbicsRes::BadRequest => StatusCode::BAD_REQUEST.into_response(),
    533                             EbicsRes::Failure => StatusCode::SERVICE_UNAVAILABLE.into_response(),
    534                         }
    535                     }),
    536                 )
    537                 .serve(&serve, None)
    538                 .await
    539                 .unwrap()
    540         });
    541         wait_for_unix_socket(&sock_path).await;
    542         Self {
    543             dir,
    544             sock_path,
    545             sequence,
    546         }
    547     }
    548 
    549     pub fn sequences(&self, sequences: &[Sequence]) {
    550         let mut state = self.sequence.lock().unwrap();
    551         assert_eq!(state.len(), 0);
    552         state.extend(sequences.iter().rev());
    553     }
    554 }
    555 
    556 impl Drop for TestBank {
    557     fn drop(&mut self) {
    558         if let Ok(sequences) = self.sequence.lock() {
    559             assert_eq!(sequences.len(), 0);
    560         }
    561     }
    562 }