libeufin

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

bts.rs (15763B)


      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 /*! EBICS protocol for business transactions */
     21 
     22 use compact_str::CompactString;
     23 use jiff::{Timestamp, Zoned, tz::TimeZone};
     24 use roxmltree::Document;
     25 use taler_common::encoding::base64;
     26 
     27 use crate::{
     28     config::EbicsHostCfg,
     29     crypto::ebics_pub_key_hash,
     30     ebics::{
     31         EbicsResponse, PreparedUploadData,
     32         ebics_code::EbicsReturnCode,
     33         order::{BTF, Order},
     34     },
     35     keys::{BankKeys, ClientKeys},
     36     xml,
     37     xml::{Xml, XmlAccess, XmlWriter},
     38     xml_sign::sign_ebics,
     39 };
     40 
     41 fn signed_request(
     42     order: &Order,
     43     client: &ClientKeys,
     44     lambda: impl FnOnce(&mut XmlWriter),
     45 ) -> String {
     46     let schema = order.schema();
     47     let doc = xml!(
     48         "ebicsRequest"
     49             "xmlns"=(format_args!("urn:org:ebics:{schema}"))
     50             "xmlns:ds"="http://www.w3.org/2000/09/xmldsig#"
     51             "Version"=schema
     52             "Revision"="1"
     53         {
     54             @ lambda
     55         }
     56     );
     57     sign_ebics(doc, &client.auth).expect("EBICS signature never fails")
     58 }
     59 
     60 fn bank_digest(w: &mut XmlWriter, bank: &BankKeys) {
     61     xml!(w =>
     62         "BankPubKeyDigests" {
     63             "Authentication" "Version"="X002" "Algorithm"="http://www.w3.org/2001/04/xmlenc#sha256" : base64::fmt(ebics_pub_key_hash(&bank.auth)),
     64             "Encryption" "Version"="E002" "Algorithm"="http://www.w3.org/2001/04/xmlenc#sha256" : base64::fmt(ebics_pub_key_hash(&bank.enc))
     65         },
     66         "SecurityMedium": "0000"
     67     )
     68 }
     69 
     70 fn service(w: &mut XmlWriter, service: &BTF) {
     71     let BTF {
     72         service: name,
     73         scope,
     74         msg,
     75         version,
     76         container,
     77         option,
     78     } = service;
     79     xml!(w =>
     80         "Service" {
     81             "ServiceName": name,
     82             @ |w: &mut XmlWriter| {
     83                 if let Some(scope) = scope {
     84                     xml!(w => "Scope": scope)
     85                 }
     86                 if let Some(option) = option {
     87                     xml!(w => "ServiceOption": option)
     88                 }
     89                 if let Some(container) = container {
     90                     xml!(w => "Container" "containerType"=container)
     91                 }
     92 
     93                 if let Some(version) = version {
     94                     xml!(w => "MsgName" "version"=version : msg)
     95                 } else {
     96                     xml!(w => "MsgName": msg)
     97                 }
     98             }
     99         }
    100     )
    101 }
    102 
    103 pub fn d_init(
    104     cfg: &EbicsHostCfg,
    105     bank: &BankKeys,
    106     client: &ClientKeys,
    107     order: &Order,
    108     range: &Option<(Timestamp, Timestamp)>,
    109 ) -> String {
    110     let nonce: u128 = rand::random();
    111     signed_request(order, client, |w| {
    112         xml!(w =>
    113             "header" "authenticate"="true" {
    114                 "static" {
    115                     "HostID": cfg.host_id,
    116                     "Nonce": format_args!("{:032x}", nonce),
    117                     "Timestamp": jiff::Timestamp::now(),
    118                     "PartnerID": cfg.partner_id,
    119                     "UserID": cfg.user_id,
    120                     "OrderDetails" {
    121                         "AdminOrderType": order.ty(),
    122                         @ |w: &mut XmlWriter| if let Order::BTD(s) = order {
    123                             xml!(w => "BTDOrderParams" {
    124                                 @ |w: &mut XmlWriter| {
    125                                     service(w, s);
    126                                     if let Some((start, end)) = range {
    127                                         xml!(w =>
    128                                             "DateRange" {
    129                                                 "Start": Zoned::new(*start, TimeZone::UTC).date(),
    130                                                 "End": Zoned::new(*end, TimeZone::UTC).date()
    131                                             }
    132                                         )
    133                                     }
    134                                 }
    135                             })
    136                         } else {
    137                             xml!(w => "StandardOrderParams")
    138                         }
    139                     },
    140                     @ |w: &mut XmlWriter| bank_digest(w, bank)
    141                 },
    142                 "mutable" {
    143                     "TransactionPhase": "Initialisation"
    144                 }
    145             },
    146             "AuthSignature",
    147             "body"
    148         )
    149     })
    150 }
    151 
    152 pub fn d_transfer(
    153     cfg: &EbicsHostCfg,
    154     client: &ClientKeys,
    155     order: &Order,
    156     nb_segment: usize,
    157     segment_nb: usize,
    158     tx_id: &str,
    159 ) -> String {
    160     signed_request(order, client, |w| {
    161         xml!(w =>
    162             "header" "authenticate"="true" {
    163                 "static" {
    164                     "HostID": cfg.host_id,
    165                     "TransactionID": tx_id
    166                 },
    167                 "mutable" {
    168                     "TransactionPhase": "Transfer",
    169                     "SegmentNumber" "lastSegment"=(nb_segment == segment_nb) : segment_nb
    170                 }
    171             },
    172             "AuthSignature",
    173             "body"
    174         )
    175     })
    176 }
    177 
    178 pub fn receipt(
    179     cfg: &EbicsHostCfg,
    180     client: &ClientKeys,
    181     order: &Order,
    182     tx_id: &str,
    183     success: bool,
    184 ) -> String {
    185     signed_request(order, client, |w| {
    186         xml!(w =>
    187             "header" "authenticate"="true" {
    188                 "static" {
    189                     "HostID": cfg.host_id,
    190                     "TransactionID": tx_id
    191                 },
    192                 "mutable" {
    193                     "TransactionPhase": "Receipt"
    194                 }
    195             },
    196             "AuthSignature",
    197             "body" {
    198                 "TransferReceipt" "authenticate"="true" {
    199                     "ReceiptCode": (if success { "0" } else { "1"})
    200                 }
    201             }
    202         )
    203     })
    204 }
    205 
    206 pub fn u_init(
    207     cfg: &EbicsHostCfg,
    208     bank: &BankKeys,
    209     client: &ClientKeys,
    210     order: &Order,
    211     data: &PreparedUploadData,
    212 ) -> String {
    213     let nonce: u128 = rand::random();
    214     signed_request(order, client, |w| {
    215         xml!(w =>
    216             "header" "authenticate"="true" {
    217                 "static" {
    218                     "HostID": cfg.host_id,
    219                     "Nonce": format_args!("{:032x}", nonce),
    220                     "Timestamp": jiff::Timestamp::now(),
    221                     "PartnerID": cfg.partner_id,
    222                     "UserID": cfg.user_id,
    223                     "OrderDetails" {
    224                         "AdminOrderType": order.ty(),
    225                         @ |w: &mut XmlWriter| if let Order::BTU(s) = order {
    226                             xml!(w => "BTUOrderParams" {
    227                                 @ |w: &mut XmlWriter| service(w, s),
    228                                 "SignatureFlag"
    229                             })
    230                         } else {
    231                             xml!(w => "StandardOrderParams")
    232                         }
    233                     },
    234                     @ |w: &mut XmlWriter| bank_digest(w, bank),
    235                     "NumSegments": data.nb_segments()
    236                 },
    237                 "mutable" {
    238                     "TransactionPhase": "Initialisation"
    239                 }
    240             },
    241             "AuthSignature",
    242             "body" {
    243                 "DataTransfer" {
    244                     "DataEncryptionInfo" "authenticate"="true" {
    245                         "EncryptionPubKeyDigest" "Version"="E002" "Algorithm"="http://www.w3.org/2001/04/xmlenc#sha256": base64::fmt(ebics_pub_key_hash(&bank.enc)),
    246                         "TransactionKey": base64::fmt(&data.encrypted_key)
    247                     },
    248                     "SignatureData" "authenticate"="true" : data.signature_data,
    249                     "DataDigest" "SignatureVersion"="A006" : base64::fmt(data.digest)
    250                 }
    251             }
    252         )
    253     })
    254 }
    255 
    256 pub fn u_transfer(
    257     cfg: &EbicsHostCfg,
    258     client: &ClientKeys,
    259     order: &Order,
    260     tx_id: &str,
    261     data: &PreparedUploadData,
    262     segment_nb: usize,
    263 ) -> String {
    264     signed_request(order, client, |w| {
    265         xml!(w =>
    266             "header" "authenticate"="true" {
    267                 "static" {
    268                     "HostID": cfg.host_id,
    269                     "TransactionID": tx_id
    270                 },
    271                 "mutable" {
    272                     "TransactionPhase": "Transfer",
    273                     "SegmentNumber" "lastSegment"=(data.nb_segments() == segment_nb) : segment_nb
    274                 }
    275             },
    276             "AuthSignature",
    277             "body" {
    278                 "DataTransfer" {
    279                     "OrderData": data.segment(segment_nb)
    280                 }
    281             }
    282         )
    283     })
    284 }
    285 
    286 pub struct DataEncryptionInfo {
    287     pub tx_key: Vec<u8>,
    288     pub enc_pub_digest: Vec<u8>,
    289 }
    290 
    291 fn expect_phase(n: Xml<'_>, phase: &str) -> xml::Result<()> {
    292     let n = n.one("TransactionPhase")?;
    293     if n.text() != phase {
    294         Err(n.parse_err(format_args!("Expected phase '{phase}' got '{}'", n.text())))
    295     } else {
    296         Ok(())
    297     }
    298 }
    299 
    300 pub struct DInit {
    301     pub tx_id: CompactString,
    302     pub data_encryption_info: DataEncryptionInfo,
    303     pub segment: Vec<u8>,
    304     pub nb_segments: usize,
    305 }
    306 
    307 pub fn parse_d_init(xml: Document) -> xml::Result<EbicsResponse<DInit>> {
    308     Xml::doc(xml, "ebicsResponse", |root| {
    309         let header = root.one_signed("header")?;
    310         let st = header.one("static")?;
    311         let mutable = header.one("mutable")?;
    312         let body = root.one("body")?;
    313 
    314         let bank_code: EbicsReturnCode = body.one_signed("ReturnCode").parse()?;
    315         let technical_code: EbicsReturnCode = mutable.one("ReturnCode").parse()?;
    316         let technical_text = mutable.one("ReportText").parse()?;
    317 
    318         if technical_code.is_error() || bank_code.is_error() {
    319             return Ok(EbicsResponse {
    320                 technical_code,
    321                 bank_code,
    322                 technical_text,
    323                 content: None,
    324             });
    325         }
    326 
    327         expect_phase(mutable, "Initialisation")?;
    328 
    329         let data: Xml<'_> = body.one("DataTransfer")?;
    330         let enc_info = data.one_signed("DataEncryptionInfo")?;
    331         Ok(EbicsResponse {
    332             technical_code,
    333             bank_code,
    334             technical_text,
    335             content: Some(DInit {
    336                 tx_id: st.one("TransactionID").parse()?,
    337                 data_encryption_info: DataEncryptionInfo {
    338                     tx_key: enc_info.one("TransactionKey").b64()?,
    339                     enc_pub_digest: enc_info.one("EncryptionPubKeyDigest").b64()?,
    340                 },
    341                 segment: data.one("OrderData").b64()?,
    342                 nb_segments: st.one("NumSegments").parse()?,
    343             }),
    344         })
    345     })
    346 }
    347 
    348 pub struct DTransfer {
    349     pub tx_id: CompactString,
    350     pub segment: Vec<u8>,
    351 }
    352 
    353 pub fn parse_d_transfer(xml: Document) -> xml::Result<EbicsResponse<DTransfer>> {
    354     Xml::doc(xml, "ebicsResponse", |root| {
    355         let header = root.one_signed("header")?;
    356         let st = header.one("static")?;
    357         let mutable = header.one("mutable")?;
    358         let body = root.one("body")?;
    359 
    360         let bank_code: EbicsReturnCode = body.one_signed("ReturnCode").parse()?;
    361         let technical_code: EbicsReturnCode = mutable.one("ReturnCode").parse()?;
    362         let technical_text = mutable.one("ReportText").parse()?;
    363 
    364         if technical_code.is_error() || bank_code.is_error() {
    365             return Ok(EbicsResponse {
    366                 technical_code,
    367                 bank_code,
    368                 technical_text,
    369                 content: None,
    370             });
    371         }
    372 
    373         expect_phase(mutable, "Transfer")?;
    374 
    375         Ok(EbicsResponse {
    376             technical_code,
    377             bank_code,
    378             technical_text,
    379             content: Some(DTransfer {
    380                 tx_id: st.one("TransactionID").parse()?,
    381                 segment: body.one("DataTransfer").one("OrderData").b64()?,
    382             }),
    383         })
    384     })
    385 }
    386 
    387 pub struct Receipt {
    388     pub tx_id: CompactString,
    389 }
    390 
    391 pub fn parse_receipt(xml: Document) -> xml::Result<EbicsResponse<Receipt>> {
    392     Xml::doc(xml, "ebicsResponse", |root| {
    393         let header = root.one_signed("header")?;
    394         let st = header.one("static")?;
    395         let mutable = header.one("mutable")?;
    396         let body = root.one("body")?;
    397 
    398         let bank_code: EbicsReturnCode = body.one_signed("ReturnCode").parse()?;
    399         let technical_code: EbicsReturnCode = mutable.one("ReturnCode").parse()?;
    400         let technical_text = mutable.one("ReportText").parse()?;
    401 
    402         if technical_code.is_error() || bank_code.is_error() {
    403             return Ok(EbicsResponse {
    404                 technical_code,
    405                 bank_code,
    406                 technical_text,
    407                 content: None,
    408             });
    409         }
    410 
    411         expect_phase(mutable, "Receipt")?;
    412 
    413         Ok(EbicsResponse {
    414             technical_code,
    415             bank_code,
    416             technical_text,
    417             content: Some(Receipt {
    418                 tx_id: st.one("TransactionID").parse()?,
    419             }),
    420         })
    421     })
    422 }
    423 
    424 pub struct UInit {
    425     pub tx_id: CompactString,
    426     pub order_id: CompactString,
    427 }
    428 
    429 pub fn parse_u_init(xml: Document) -> xml::Result<EbicsResponse<UInit>> {
    430     Xml::doc(xml, "ebicsResponse", |root| {
    431         let header = root.one_signed("header")?;
    432         let st = header.one("static")?;
    433         let mutable = header.one("mutable")?;
    434         let body = root.one("body")?;
    435 
    436         let bank_code: EbicsReturnCode = body.one_signed("ReturnCode").parse()?;
    437         let technical_code: EbicsReturnCode = mutable.one("ReturnCode").parse()?;
    438         let technical_text = mutable.one("ReportText").parse()?;
    439 
    440         if technical_code.is_error() || bank_code.is_error() {
    441             return Ok(EbicsResponse {
    442                 technical_code,
    443                 bank_code,
    444                 technical_text,
    445                 content: None,
    446             });
    447         }
    448 
    449         expect_phase(mutable, "Initialisation")?;
    450 
    451         Ok(EbicsResponse {
    452             technical_code,
    453             bank_code,
    454             technical_text,
    455             content: Some(UInit {
    456                 order_id: mutable.one("OrderID").parse()?,
    457                 tx_id: st.one("TransactionID").parse()?,
    458             }),
    459         })
    460     })
    461 }
    462 
    463 pub struct UTransfer {
    464     pub tx_id: CompactString,
    465 }
    466 
    467 pub fn parse_u_transfer(xml: Document) -> xml::Result<EbicsResponse<UTransfer>> {
    468     Xml::doc(xml, "ebicsResponse", |root| {
    469         let header = root.one_signed("header")?;
    470         let st = header.one("static")?;
    471         let mutable = header.one("mutable")?;
    472         let body = root.one("body")?;
    473 
    474         let bank_code: EbicsReturnCode = body.one_signed("ReturnCode").parse()?;
    475         let technical_code: EbicsReturnCode = mutable.one("ReturnCode").parse()?;
    476         let technical_text = mutable.one("ReportText").parse()?;
    477 
    478         if technical_code.is_error() || bank_code.is_error() {
    479             return Ok(EbicsResponse {
    480                 technical_code,
    481                 bank_code,
    482                 technical_text,
    483                 content: None,
    484             });
    485         }
    486 
    487         expect_phase(mutable, "Transfer")?;
    488 
    489         Ok(EbicsResponse {
    490             technical_code,
    491             bank_code,
    492             technical_text,
    493             content: Some(UTransfer {
    494                 tx_id: st.one("TransactionID").parse()?,
    495             }),
    496         })
    497     })
    498 }