libeufin

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

setup.rs (6178B)


      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::path::Path;
     21 
     22 use anyhow::{anyhow, bail};
     23 use compact_str::{CompactString, CompactStringExt};
     24 use tracing::{debug, info};
     25 
     26 use crate::{
     27     config::EbicsSetupCfg,
     28     crypto::ebics_pub_key_hash,
     29     ebics::{EbicsClient, administrative::VersionNumber, order::Order},
     30     keys::{
     31         BankKeys, ClientKeys, load_bank_keys, load_client_keys, persist_bank_keys,
     32         persist_client_keys,
     33     },
     34     pdf::generate_keys_pdf,
     35     utils::hex_chunk_by_two,
     36 };
     37 
     38 /** Load client private keys at or create new ones if missing */
     39 pub fn load_or_generate_client_keys(path: &Path) -> anyhow::Result<ClientKeys> {
     40     // If exists load from disk
     41     let current = load_client_keys(path)?;
     42     if let Some(current) = current {
     43         return Ok(current);
     44     }
     45     // Else create new keys
     46     let new = ClientKeys::generate()?;
     47     persist_client_keys(&new, path)?;
     48     info!(target: "setup",
     49         "New client private keys created at '{}'",
     50         path.to_string_lossy()
     51     );
     52     Ok(new)
     53 }
     54 
     55 pub async fn ebics_setup(
     56     ebics: &EbicsClient<'_>,
     57     cfg: &EbicsSetupCfg<'_>,
     58     force_keys_resubmission: bool,
     59     generate_registration_pdf: bool,
     60     auto_accept_keys: bool,
     61 ) -> anyhow::Result<(ClientKeys, BankKeys)> {
     62     let mut client = load_or_generate_client_keys(cfg.keys.client.as_ref())?;
     63     let bank = load_bank_keys(cfg.keys.bank.as_ref())?;
     64 
     65     // Check EBICS 3 support
     66     let versions = ebics.hev().await?;
     67     debug!(target: "setup",
     68         "HEV: {}",
     69         versions
     70             .iter()
     71             .map(|v| v.to_string())
     72             .join_compact(", ")
     73     );
     74     if !versions.contains(&VersionNumber {
     75         number: "03.00".into(),
     76         schema: "H005".into(),
     77     }) && !versions.contains(&VersionNumber {
     78         number: "03.02".into(),
     79         schema: "H005".into(),
     80     }) {
     81         bail!("EBICS 3 is not supported by your bank");
     82     }
     83 
     84     // Privs exist.  Upload their pubs
     85     let keys_not_sub = !client.submitted_ini;
     86     if !client.submitted_ini || force_keys_resubmission {
     87         ebics
     88             .submit_client_keys(&cfg.keys, &mut client, Order::INI)
     89             .await?;
     90     }
     91     // Eject PDF if the keys were submitted for the first time, or the user asked.
     92     if keys_not_sub || generate_registration_pdf {
     93         let pdf = generate_keys_pdf(&client, &cfg.host)?;
     94         let path = "/tmp/libeufin-ebics-keys.pdf";
     95         std::fs::write("/tmp/libeufin-ebics-keys.pdf", &pdf)
     96             .map_err(|e| anyhow!("Could not write PDF to '{path}': {}", e.kind()))?;
     97         println!("PDF file with keys created at '{path}'");
     98     }
     99     if !client.submitted_hia || force_keys_resubmission {
    100         ebics
    101             .submit_client_keys(&cfg.keys, &mut client, Order::HIA)
    102             .await?;
    103     }
    104 
    105     let new = ebics.hpb(&client).await?;
    106     let mut bank = if let Some(current) = bank {
    107         // Check current bank keys
    108         if current.enc.as_ref() != new.enc.as_ref() {
    109             bail!(
    110                 "On disk bank encryption key stored at {} doesn't match server key\nDisk:   {}\nServer: {}",
    111                 cfg.keys.bank,
    112                 hex_chunk_by_two(ebics_pub_key_hash(&current.enc)),
    113                 hex_chunk_by_two(ebics_pub_key_hash(&new.enc))
    114             )
    115         } else if current.auth.as_ref() != new.auth.as_ref() {
    116             bail!(
    117                 "On disk bank authentication key stored at {} doesn't match server key\nDisk:   {}\nServer: {}",
    118                 cfg.keys.bank,
    119                 hex_chunk_by_two(ebics_pub_key_hash(&current.auth)),
    120                 hex_chunk_by_two(ebics_pub_key_hash(&new.auth))
    121             )
    122         }
    123         current
    124     } else {
    125         // Accept bank keys
    126         info!("Bank keys stored at {}", cfg.keys.bank);
    127         persist_bank_keys(&new, cfg.keys.bank.as_ref())?;
    128         new
    129     };
    130     if !bank.accepted {
    131         // Finishing the setup by accepting the bank keys.
    132         let enc_hash = ebics_pub_key_hash(&bank.enc);
    133         let auth_hash = ebics_pub_key_hash(&bank.auth);
    134         if auto_accept_keys {
    135             bank.accepted = true
    136         } else if let Some(enc) = cfg.enc
    137             && let Some(auth) = cfg.auth
    138         {
    139             if enc == enc_hash.as_ref() && auth == auth_hash.as_ref() {
    140                 info!(target: "setup", "Accepting bank keys matching config hashes");
    141                 bank.accepted = true
    142             } else {
    143                 bail!(
    144                     "Bank keys does not match config hashes\nBank encryption key: {}\nConfig encryption key: {}\nBank authentication key: {}\nConfig authentication key: {}",
    145                     hex_chunk_by_two(enc_hash),
    146                     hex_chunk_by_two(enc),
    147                     hex_chunk_by_two(auth_hash),
    148                     hex_chunk_by_two(auth),
    149                 )
    150             }
    151         } else {
    152             println!(
    153                 "The bank has the following keys:\nEncryption key: {}\nAuthentication key: {}",
    154                 hex_chunk_by_two(enc_hash),
    155                 hex_chunk_by_two(auth_hash)
    156             );
    157             bank.accepted = dialoguer::Input::<CompactString>::new()
    158                 .with_prompt("type 'yes, accept' to accept them")
    159                 .interact()?
    160                 == "yes, accept";
    161         }
    162         if !bank.accepted {
    163             bail!("Cannot successfully finish the setup without accepting the bank keys");
    164         }
    165         persist_bank_keys(&bank, cfg.keys.bank.as_ref())?;
    166     }
    167 
    168     Ok((client, bank))
    169 }