libeufin

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

key_management.rs (10985B)


      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::{borrow::Cow, io::Write as _};
     21 
     22 use anyhow::bail;
     23 use aws_lc_rs::{
     24     encoding::{AsDer, Pkcs8V1Der},
     25     rsa::PublicKey,
     26 };
     27 use compact_str::CompactStringExt;
     28 use flate2::{Compression, write::ZlibEncoder};
     29 use taler_common::encoding::base64;
     30 use tracing::info;
     31 
     32 use crate::{
     33     config::{EbicsHostCfg, EbicsKeysCfg},
     34     crypto::{rsa_private_from_b64_x509_certificate, x509_certificate_from_rsa_private},
     35     ebics::{
     36         EbicsClient, EbicsCtx, EbicsErrKind, EbicsError, EbicsErrorHelper, EbicsResponse,
     37         bts::DataEncryptionInfo, decrypt_and_decompress_payload, ebics_code::EbicsReturnCode,
     38         order::Order,
     39     },
     40     keys::{self, BankKeys, ClientKeys},
     41     xml,
     42     xml::{Xml, XmlAccess as _, XmlWriter},
     43     xml_sign::sign_ebics,
     44 };
     45 
     46 impl EbicsClient<'_> {
     47     /** Perform an EBICS public key management [order] using [client] and update on disk state */
     48     pub async fn submit_client_keys(
     49         &self,
     50         cfg: &EbicsKeysCfg<'_>,
     51         client: &mut ClientKeys,
     52         order: Order,
     53     ) -> Result<(), EbicsError> {
     54         let ctx = EbicsCtx::new(&order);
     55         if !matches!(order, Order::INI | Order::HIA) {
     56             unreachable!("Only INI & HIA are supported for client keys");
     57         }
     58         let res = self.key_management(client, &order, &ctx).await?;
     59 
     60         if res.technical_code == EbicsReturnCode::EBICS_INVALID_USER_STATE
     61             || res.technical_code == EbicsReturnCode::EBICS_INVALID_USER_OR_USER_STATE
     62         {
     63             return Err(EbicsErrKind::Custom(Cow::Owned(format!(
     64             "status code {}: either your IDs are incorrect, or you already have keys registered with this bank",
     65             res.technical_code
     66         ))).ctx(&ctx));
     67         }
     68         res.ok_or_fail().ctx(&ctx)?;
     69         match order {
     70             Order::INI => client.submitted_ini = true,
     71             Order::HIA => client.submitted_hia = true,
     72             _ => unreachable!("Only INI & HIA are supported for client keys"),
     73         }
     74         keys::persist_client_keys(client, cfg.client.as_ref())
     75             .map_err(|e| EbicsErrKind::Custom(e.to_string().into()))
     76             .ctx(&ctx)?;
     77         Ok(())
     78     }
     79 
     80     /** Perform an EBICS private key management HPB using [client] */
     81     pub async fn hpb(&self, client: &ClientKeys) -> anyhow::Result<BankKeys> {
     82         let order = Order::HPB;
     83         let ctx = EbicsCtx::new(&order);
     84         let res = self.key_management(client, &order, &ctx).await?;
     85         if res.technical_code == EbicsReturnCode::EBICS_AUTHENTICATION_FAILED {
     86             bail!(
     87                 "{order} status code {}: could not download bank keys, send client keys (and/or related PDF document with --generate-registration-pdf) to the bank",
     88                 res.technical_code
     89             )
     90         }
     91         let order_data = res.ok_or_fail()?.expect("{order}: missing order data");
     92         self.logger.log_payload(&ctx, &order_data, "xml")?;
     93         Ok(Xml::parse(&order_data, "HPBResponseOrderData", |root| {
     94             let auth_pub = root.one("AuthenticationPubKeyInfo")?;
     95             let version = auth_pub.one("AuthenticationVersion")?.text();
     96             assert_eq!(
     97                 version, "X002",
     98                 "Expected authentication version X002 got unsupported {version}"
     99             );
    100             let auth_pub = rsa_pub_key(auth_pub)?;
    101 
    102             let enc_pub = root.one("EncryptionPubKeyInfo")?;
    103             let version = enc_pub.one("EncryptionVersion")?.text();
    104             assert_eq!(
    105                 version, "E002",
    106                 "Expected encryption version E002 got unsupported {version}"
    107             );
    108             let enc_pub = rsa_pub_key(enc_pub)?;
    109 
    110             Ok(BankKeys {
    111                 auth: auth_pub,
    112                 enc: enc_pub,
    113                 accepted: false,
    114             })
    115         })?)
    116     }
    117 
    118     async fn key_management(
    119         &self,
    120         client: &ClientKeys,
    121         order: &Order,
    122         ctx: &EbicsCtx<'_>,
    123     ) -> Result<EbicsResponse<Option<Vec<u8>>>, EbicsError> {
    124         let EbicsHostCfg {
    125             host_id,
    126             user_id,
    127             partner_id,
    128             ..
    129         } = &self.cfg;
    130         info!(target: "ebics", "Doing key request {order}");
    131 
    132         let (name, security_medium) = match order {
    133             Order::INI | Order::HIA => ("ebicsUnsecuredRequest", "0200"),
    134             Order::HPB => ("ebicsNoPubKeyDigestsRequest", "0000"),
    135             _ => unreachable!(),
    136         };
    137 
    138         fn xml_order_data(
    139             cfg: &EbicsHostCfg,
    140             name: &str,
    141             schema: &str,
    142             build: impl FnOnce(&mut XmlWriter),
    143         ) -> String {
    144             let xml = xml!(name "xmlns"=schema "xmlns:ds"="http://www.w3.org/2000/09/xmldsig#" {
    145                 @ build,
    146                 "PartnerID": &cfg.partner_id,
    147                 "UserID": &cfg.user_id
    148             });
    149             let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
    150             encoder.write_all(xml.as_bytes()).unwrap();
    151             let compressed = encoder.finish().unwrap();
    152             base64::encode(&compressed)
    153         }
    154 
    155         let data = match order {
    156             Order::INI => Some(xml_order_data(
    157                 &self.cfg,
    158                 "SignaturePubKeyOrderData",
    159                 "http://www.ebics.org/S002",
    160                 |w| {
    161                     xml!(w => "SignaturePubKeyInfo" {
    162                         @ |w| rsa_key_xml(w, &client.sign),
    163                         "SignatureVersion": "A006"
    164                     })
    165                 },
    166             )),
    167             Order::HIA => Some(xml_order_data(
    168                 &self.cfg,
    169                 "HIARequestOrderData",
    170                 "urn:org:ebics:H005",
    171                 |w| {
    172                     xml!(w =>
    173                         "AuthenticationPubKeyInfo" {
    174                             @ |w| rsa_key_xml(w, &client.auth),
    175                             "AuthenticationVersion": "X002"
    176                         },
    177                         "EncryptionPubKeyInfo" {
    178                             @ |w| rsa_key_xml(w, &client.enc),
    179                             "EncryptionVersion": "E002"
    180                         }
    181                     )
    182                 },
    183             )),
    184             Order::HPB => None,
    185             _ => unreachable!(),
    186         };
    187         let sign = matches!(order, Order::HPB);
    188         let msg = xml!(
    189             name
    190                 "xmlns"="urn:org:ebics:H005"
    191                 "xmlns:ds"="http://www.w3.org/2000/09/xmldsig#"
    192                 "Version"="H005"
    193                 "Revision"="1"
    194             {
    195                 "header" "authenticate"="true" {
    196                     "static" {
    197                         "HostID": host_id,
    198                         @ |w: &mut XmlWriter| if *order == Order::HPB {
    199                             let nonce: u128 = rand::random();
    200                             xml!(w =>
    201                                 "Nonce": format_args!("{:032x}", nonce),
    202                                 "Timestamp": jiff::Timestamp::now()
    203                             )
    204                         },
    205                         "PartnerID": partner_id,
    206                         "UserID": user_id,
    207                         "OrderDetails" {
    208                             "AdminOrderType": order
    209                         },
    210                         "SecurityMedium": security_medium
    211                     },
    212                     "mutable"
    213                 },
    214                 @ |w: &mut XmlWriter| if sign {
    215                     xml!(w => "AuthSignature")
    216                 },
    217                 "body" {
    218                     @ |w: &mut XmlWriter| if let Some(data) = data {
    219                         xml!(w => "DataTransfer" {
    220                             "OrderData": data
    221                         })
    222                     }
    223                 }
    224             }
    225         );
    226         let signed = if sign {
    227             sign_ebics(msg, &client.auth).expect("EBICS signature never fails")
    228         } else {
    229             msg
    230         };
    231         let res = self.post_to_bank(signed, ctx).await?;
    232         let parsed = Xml::parse(&res, "ebicsKeyManagementResponse", |root| {
    233             let body = root.one("body")?;
    234             let mutable = root.one_signed("header").one("mutable")?;
    235             Ok(EbicsResponse {
    236                 technical_code: mutable.one("ReturnCode").parse()?,
    237                 technical_text: mutable.one("ReportText").parse()?,
    238                 bank_code: body.one_signed("ReturnCode").parse()?,
    239                 content: Some(if let Some(data) = body.opt("DataTransfer")? {
    240                     let info = data.one_signed("DataEncryptionInfo")?;
    241                     let info = DataEncryptionInfo {
    242                         tx_key: info.one("TransactionKey").b64()?,
    243                         enc_pub_digest: info.one("EncryptionPubKeyDigest").b64()?,
    244                     };
    245                     let chunk = data.one("OrderData").b64()?;
    246                     Some((info, chunk))
    247                 } else {
    248                     None
    249                 }),
    250             })
    251         })
    252         .ctx(ctx)?;
    253         let decoded = match parsed.content {
    254             Some(Some((info, chunk))) => Some(Some(
    255                 decrypt_and_decompress_payload(&client.enc, info, chunk).ctx(ctx)?,
    256             )),
    257             Some(None) => Some(None),
    258             None => None,
    259         };
    260         Ok(EbicsResponse {
    261             content: decoded,
    262             technical_code: parsed.technical_code,
    263             bank_code: parsed.bank_code,
    264             technical_text: parsed.technical_text,
    265         })
    266     }
    267 }
    268 
    269 pub fn rsa_pub_key(xml: Xml) -> xml::Result<PublicKey> {
    270     xml.one("X509Data")
    271         .one("X509Certificate")
    272         .decode(rsa_private_from_b64_x509_certificate)
    273 }
    274 
    275 pub fn rsa_key_xml<K>(w: &mut XmlWriter, key: &K)
    276 where
    277     K: AsDer<Pkcs8V1Der<'static>>,
    278 {
    279     let der = key.as_der().unwrap();
    280     let b64 = base64::encode(der.as_ref());
    281     let lines = b64
    282         .as_bytes()
    283         .chunks(64)
    284         .map(|c| std::str::from_utf8(c).unwrap())
    285         .join_compact("\n");
    286     let pem = format!("-----BEGIN RSA PRIVATE KEY-----\n{lines}\n-----END RSA PRIVATE KEY-----\n");
    287     let cert = x509_certificate_from_rsa_private(&pem, "LibEuFin EBICS").unwrap();
    288     let der = cert.der();
    289 
    290     xml!(w => "ds:X509Data" {
    291         "ds:X509Certificate": base64::fmt(der)
    292     })
    293 }