libeufin

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

commit ad8c3edb0dd866376e411fea1fecc3d4a773e5fa
parent c4d6004bfa395140a228591147128405c0bdb557
Author: Antoine A <>
Date:   Wed, 27 May 2026 14:25:24 +0200

nexus: add PDF gen

Diffstat:
MCargo.lock | 106++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
MCargo.toml | 1+
Mcrates/libeufin-bank/Cargo.toml | 2+-
Mcrates/libeufin-bank/src/db.rs | 2+-
Mcrates/libeufin-ebics/Cargo.toml | 2++
Mcrates/libeufin-ebics/src/config.rs | 8++++++++
Mcrates/libeufin-ebics/src/keys.rs | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Mcrates/libeufin-ebics/src/lib.rs | 1+
Acrates/libeufin-ebics/src/pdf.rs | 125+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/libeufin-ebics/src/setup.rs | 65+++++++++++++++++++++++++++++++++++++++++++++++++----------------
Mcrates/libeufin-ebics/src/utils.rs | 2+-
Mcrates/libeufin-nexus/Cargo.toml | 3++-
Mcrates/libeufin-nexus/src/config.rs | 39++++++++++++++++++++++++++++++++++++++-
Mcrates/libeufin-nexus/src/lib.rs | 8++++----
Mcrates/libeufin-nexus/src/testing.rs | 9+++++++--
15 files changed, 404 insertions(+), 48 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -326,6 +326,17 @@ dependencies = [ ] [[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + +[[package]] name = "bumpalo" version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -502,9 +513,9 @@ dependencies = [ [[package]] name = "compact_str" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" +checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" dependencies = [ "castaway", "cfg-if", @@ -851,9 +862,9 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", @@ -978,6 +989,7 @@ version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ + "crc32fast", "miniz_oxide", "zlib-rs", ] @@ -1748,10 +1760,12 @@ dependencies = [ "calamine", "clap", "compact_str", + "dialoguer", "flate2", "futures-util", "jiff", "pretty_assertions", + "printpdf", "rand 0.10.1", "rcgen", "reqwest", @@ -1842,6 +1856,12 @@ dependencies = [ ] [[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] name = "linux-raw-sys" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1886,6 +1906,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" [[package]] +name = "lopdf" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07c8e1b6184b1b32ea5f72f572ebdc40e5da1d2921fa469947ff7c480ad1f85a" +dependencies = [ + "encoding_rs", + "flate2", + "itoa", + "linked-hash-map", + "log", + "md5", + "pom", + "time", + "weezl", +] + +[[package]] name = "lru-slab" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1908,10 +1945,16 @@ dependencies = [ ] [[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + +[[package]] name = "memchr" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" [[package]] name = "mimalloc" @@ -2075,6 +2118,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] +name = "owned_ttf_parser" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "706de7e2214113d63a8238d1910463cfce781129a6f263d13fdb09ff64355ba4" +dependencies = [ + "ttf-parser", +] + +[[package]] name = "owo-colors" version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2174,6 +2226,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] +name = "pom" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c972d8f86e943ad532d0b04e8965a749ad1d18bb981a9c7b3ae72fe7fd7744b" +dependencies = [ + "bstr", +] + +[[package]] name = "portable-atomic" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2233,6 +2294,17 @@ dependencies = [ ] [[package]] +name = "printpdf" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c30a4cc87c3ca9a98f4970db158a7153f8d1ec8076e005751173c57836380b1d" +dependencies = [ + "lopdf", + "owned_ttf_parser", + "time", +] + +[[package]] name = "proc-macro2" version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3319,7 +3391,7 @@ dependencies = [ [[package]] name = "taler-api" version = "1.5.0" -source = "git+git://git.taler.net/taler-rust.git/#c1de21a6fed81c424564aaee60e6ecc82fc9572b" +source = "git+git://git.taler.net/taler-rust.git/#664cd180f65c314bae731743dbd481e4314a8ed7" dependencies = [ "aws-lc-rs", "axum", @@ -3346,12 +3418,12 @@ dependencies = [ [[package]] name = "taler-build" version = "1.5.0" -source = "git+git://git.taler.net/taler-rust.git/#c1de21a6fed81c424564aaee60e6ecc82fc9572b" +source = "git+git://git.taler.net/taler-rust.git/#664cd180f65c314bae731743dbd481e4314a8ed7" [[package]] name = "taler-common" version = "1.5.0" -source = "git+git://git.taler.net/taler-rust.git/#c1de21a6fed81c424564aaee60e6ecc82fc9572b" +source = "git+git://git.taler.net/taler-rust.git/#664cd180f65c314bae731743dbd481e4314a8ed7" dependencies = [ "anyhow", "aws-lc-rs", @@ -3380,7 +3452,7 @@ dependencies = [ [[package]] name = "taler-macros" version = "1.5.0" -source = "git+git://git.taler.net/taler-rust.git/#c1de21a6fed81c424564aaee60e6ecc82fc9572b" +source = "git+git://git.taler.net/taler-rust.git/#664cd180f65c314bae731743dbd481e4314a8ed7" dependencies = [ "proc-macro2", "quote", @@ -3390,7 +3462,7 @@ dependencies = [ [[package]] name = "taler-test-utils" version = "1.5.0" -source = "git+git://git.taler.net/taler-rust.git/#c1de21a6fed81c424564aaee60e6ecc82fc9572b" +source = "git+git://git.taler.net/taler-rust.git/#664cd180f65c314bae731743dbd481e4314a8ed7" dependencies = [ "aws-lc-rs", "axum", @@ -3702,6 +3774,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] +name = "ttf-parser" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49d64318d8311fc2668e48b63969f4343e0a85c4a109aa8460d6672e364b8bd1" + +[[package]] name = "tungstenite" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4066,6 +4144,12 @@ dependencies = [ ] [[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] name = "whoami" version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml @@ -41,6 +41,7 @@ clap = { version = "4.5", features = ["derive"] } uuid = { version = "1.0", features = ["v4", "fast-rng", "serde"] } rand = "0.10" pretty_assertions = "1" +dialoguer = "0.12" #taler-common = { path = "../taler-rust/common/taler-common" } #taler-api = { path = "../taler-rust/common/taler-api" } #taler-build = { path = "../taler-rust/common/taler-build" } diff --git a/crates/libeufin-bank/Cargo.toml b/crates/libeufin-bank/Cargo.toml @@ -26,12 +26,12 @@ compact_str.workspace = true uuid.workspace = true axum.workspace = true rand.workspace = true +dialoguer.workspace = true tower-http = { version = "0.6", features = ["fs"]} futures = "0.3" url = "2.5" regex = "1.12" bcrypt = "0.19.0" -dialoguer = "0.12" [dev-dependencies] pretty_assertions.workspace = true diff --git a/crates/libeufin-bank/src/db.rs b/crates/libeufin-bank/src/db.rs @@ -456,7 +456,7 @@ mod test { ]; for (mode, rounding) in checks { for (rounded, amounts) in *rounding { - for amount in amounts.clone().into_iter() { + for amount in amounts.clone() { // Check euro assert_eq!( decimal(format!("0.0{rounded}")), diff --git a/crates/libeufin-ebics/Cargo.toml b/crates/libeufin-ebics/Cargo.toml @@ -28,6 +28,7 @@ clap.workspace = true uuid.workspace = true rand.workspace = true pretty_assertions.workspace = true +dialoguer.workspace = true tempfile = "3" flate2 = { version = "1.0", features = ["zlib-rs"], default-features = false } zip = { version = "8.5", default-features = false, features = [ @@ -42,3 +43,4 @@ rcgen = { version = "0.14.7", features = [ ], default-features = false } x509-parser = { version = "0.18.1", features = ["verify-aws"] } calamine = { version = "0.35.0" } +printpdf = { version = "0.7", default-features = false } diff --git a/crates/libeufin-ebics/src/config.rs b/crates/libeufin-ebics/src/config.rs @@ -31,3 +31,11 @@ pub struct EbicsHostCfg<'a> { pub user_id: &'a str, pub partner_id: &'a str, } + +#[derive(Debug, Clone, Copy)] +pub struct EbicsSetupCfg<'a> { + pub keys: EbicsKeysCfg<'a>, + pub host: EbicsHostCfg<'a>, + pub enc: Option<&'a [u8]>, + pub auth: Option<&'a [u8]>, +} diff --git a/crates/libeufin-ebics/src/keys.rs b/crates/libeufin-ebics/src/keys.rs @@ -23,8 +23,10 @@ use anyhow::bail; use aws_lc_rs::{ encoding::{AsDer, Pkcs8V1Der}, error::KeyRejected, - rsa::{KeySize, PrivateDecryptingKey, PublicEncryptingKey, PublicKey, PublicKeyComponents}, - signature::RsaKeyPair, + rsa::{ + KeyPair, KeySize, PrivateDecryptingKey, PublicEncryptingKey, PublicKey, PublicKeyComponents, + }, + signature::KeyPair as _, }; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use taler_common::{ @@ -34,14 +36,14 @@ use taler_common::{ use crate::config::EbicsKeysCfg; -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct ClientKeys { #[serde( rename = "signature_private_key", serialize_with = "ser_pkcs8", deserialize_with = "de_ras_sign_base32" )] - pub sign: RsaKeyPair, + pub sign: KeyPair, #[serde( rename = "encryption_private_key", serialize_with = "ser_pkcs8", @@ -53,7 +55,7 @@ pub struct ClientKeys { serialize_with = "ser_pkcs8", deserialize_with = "de_ras_sign_base32" )] - pub auth: RsaKeyPair, + pub auth: KeyPair, pub submitted_ini: bool, pub submitted_hia: bool, } @@ -61,9 +63,9 @@ pub struct ClientKeys { impl ClientKeys { pub fn generate() -> anyhow::Result<Self> { Ok(Self { - sign: RsaKeyPair::generate(KeySize::Rsa2048)?, + sign: KeyPair::generate(KeySize::Rsa2048)?, enc: PrivateDecryptingKey::generate(KeySize::Rsa2048)?, - auth: RsaKeyPair::generate(KeySize::Rsa2048)?, + auth: KeyPair::generate(KeySize::Rsa2048)?, submitted_ini: false, submitted_hia: false, }) @@ -77,6 +79,11 @@ pub struct RsaPub { } impl RsaPub { + pub fn generate() -> Self { + let key = KeyPair::generate(KeySize::Rsa2048).unwrap(); + Self::from_der(key.public_key().as_der().unwrap().as_ref()).unwrap() + } + pub fn from_der(der: &[u8]) -> Result<Self, KeyRejected> { let key = PublicKey::from_der(der)?; let component = PublicKeyComponents { @@ -125,7 +132,7 @@ impl PartialEq for RsaPub { impl Eq for RsaPub {} -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct BankKeys { #[serde(rename = "bank_encryption_public_key")] pub enc: RsaPub, @@ -158,14 +165,14 @@ where Ok(key) } -fn de_ras_sign_base32<'de, D>(deserializer: D) -> Result<RsaKeyPair, D::Error> +fn de_ras_sign_base32<'de, D>(deserializer: D) -> Result<KeyPair, D::Error> where D: Deserializer<'de>, { let base32 = Cow::<str>::deserialize(deserializer)?; let der = base32::decode(base32.as_bytes()).map_err(|e| serde::de::Error::custom(e.to_string()))?; - let key = RsaKeyPair::from_pkcs8(&der).map_err(|e| serde::de::Error::custom(e.to_string()))?; + let key = KeyPair::from_pkcs8(&der).map_err(|e| serde::de::Error::custom(e.to_string()))?; Ok(key) } @@ -233,3 +240,55 @@ pub fn expect_full_keys(cfg: &EbicsKeysCfg) -> anyhow::Result<(ClientKeys, BankK } Ok((client_keys, bank_keys)) } + +#[cfg(test)] +mod test { + use std::path::Path; + + use serde_json::json; + + use crate::keys::{ + BankKeys, ClientKeys, RsaPub, load_bank_keys, load_client_keys, persist_bank_keys, + persist_client_keys, + }; + + /// Loading bank public keys from disk + #[test] + fn bank() { + let path: &Path = "/tmp/nexus-tests-bank-keys.json".as_ref(); + let keys = BankKeys { + enc: RsaPub::generate(), + auth: RsaPub::generate(), + accepted: true, + }; + if std::fs::exists(path).unwrap() { + std::fs::remove_file(path).unwrap() + } + persist_bank_keys(&keys, path).unwrap(); + let loaded = load_bank_keys(path).unwrap().unwrap(); + assert_eq!(loaded, keys); + persist_bank_keys(&loaded, path).unwrap(); + + assert!( + load_bank_keys("/tmp/highly-unlikely-to-be-found.json".as_ref()) + .unwrap() + .is_none() + ); + } + + /// Loading client private keys from disk + #[test] + fn client() { + let path: &Path = "/tmp/nexus-tests-client-keys.json".as_ref(); + let keys = ClientKeys::generate().unwrap(); + persist_client_keys(&keys, path).unwrap(); + let loaded = load_client_keys(path).unwrap().unwrap(); + assert_eq!(json!(loaded), json!(keys)); + + assert!( + load_client_keys("/tmp/highly-unlikely-to-be-found.json".as_ref()) + .unwrap() + .is_none() + ); + } +} diff --git a/crates/libeufin-ebics/src/lib.rs b/crates/libeufin-ebics/src/lib.rs @@ -25,6 +25,7 @@ pub mod dialect; pub mod ebics; pub mod iso20022; pub mod keys; +mod pdf; pub mod setup; pub mod test; pub mod utils; diff --git a/crates/libeufin-ebics/src/pdf.rs b/crates/libeufin-ebics/src/pdf.rs @@ -0,0 +1,125 @@ +/* +* This file is part of LibEuFin. +* Copyright (C) 2026 Taler Systems S.A. + +* LibEuFin is free software; you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation; either version 3, or +* (at your option) any later version. + +* LibEuFin is distributed in the hope that it will be useful, but +* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General +* Public License for more details. + +* You should have received a copy of the GNU Affero General Public +* License along with LibEuFin; see the file COPYING. If not, see +* <http://www.gnu.org/licenses/> +*/ + +use aws_lc_rs::{encoding::AsDer as _, rsa::PublicKey, signature::KeyPair}; +use jiff::Zoned; +use printpdf::{BuiltinFont, Mm, PdfDocument}; + +use crate::{ + config::EbicsHostCfg, crypto::ebics_pub_key_hash, keys::ClientKeys, utils::hex_chunk_by_two, +}; + +pub fn generate_keys_pdf(keys: &ClientKeys, cfg: &EbicsHostCfg) -> anyhow::Result<Vec<u8>> { + let date = Zoned::now().date().to_string(); + + let (doc, p, l) = PdfDocument::new("EBICS Client Keys", Mm(210.0), Mm(297.0), "Content"); + let reg = doc.add_builtin_font(BuiltinFont::Helvetica)?; + let bld = doc.add_builtin_font(BuiltinFont::HelveticaBold)?; + + let pages_data = [ + ( + "Signaturschlüssel", + "Öffentlicher Schlüssel (Public signature key)", + keys.sign.public_key(), + ), + ( + "Authentifikationsschlüssel", + "Öffentlicher Schlüssel (Public identification and authentication key)", + keys.auth.public_key(), + ), + ( + "Verschlüsselungsschlüssel", + "Öffentlicher Schlüssel (Public encryption key)", + &PublicKey::from_der(keys.enc.public_key().as_der()?.as_ref())?, + ), + ]; + + for (i, (title, label, pub_key)) in pages_data.into_iter().enumerate() { + let layer = if i == 0 { + doc.get_page(p).get_layer(l) + } else { + let (p, l) = doc.add_page(Mm(210.0), Mm(297.0), "Content"); + doc.get_page(p).get_layer(l) + }; + + let mut y = 270.0; + let line = |font, y: &mut f32, size, text: &str| { + layer.use_text(text, size, Mm(15.0), Mm(*y), font); + *y -= size * 0.45; + }; + + // Write Header + line(&bld, &mut y, 24.0, title); + y -= 2.2; + line(&reg, &mut y, 13.0, &format!("Datum: {date}")); + line(&reg, &mut y, 13.0, &format!("Host-ID: {}", cfg.host_id)); + line(&reg, &mut y, 13.0, &format!("User-ID: {}", cfg.user_id)); + line( + &reg, + &mut y, + 11.0, + &format!("Partner-ID: {}", cfg.partner_id), + ); + line(&reg, &mut y, 13.0, "ES version: A006"); + y -= 5.0; + line(&bld, &mut y, 13.0, label); + y -= 5.0; + + for (hdr, bytes) in [ + ( + "Exponent:", + pub_key.exponent().big_endian_without_leading_zero(), + ), + ( + "Modulus:", + pub_key.modulus().big_endian_without_leading_zero(), + ), + ("SHA-256 hash:", ebics_pub_key_hash(&pub_key).as_ref()), + ] { + line(&bld, &mut y, 13.0, hdr); + for chunk in bytes.chunks(16) { + line(&reg, &mut y, 11.0, &hex_chunk_by_two(chunk).to_string()); + } + y -= 5.0; + } + + // Write Footer + line(&reg, &mut y, 13.0, "Ort / Datum: ________________"); + line(&reg, &mut y, 13.0, "Firma / Name: ________________"); + line(&reg, &mut y, 13.0, "Unterschrift: ________________"); + } + + Ok(doc.save_to_bytes()?) +} + +#[test] +fn test() { + let bytes = generate_keys_pdf( + &ClientKeys::generate().unwrap(), + &EbicsHostCfg { + base_url: "https://isotest.postfinance.ch/ebicsweb/ebicsweb", + unix_path: None, + host_id: "PFEBICS", + user_id: "PFC00563", + partner_id: "PFC00563", + }, + ) + .unwrap(); + std::fs::write("tmp12.pdf", bytes).unwrap() +} diff --git a/crates/libeufin-ebics/src/setup.rs b/crates/libeufin-ebics/src/setup.rs @@ -19,17 +19,18 @@ use std::path::Path; -use anyhow::bail; +use anyhow::{anyhow, bail}; use tracing::{debug, info}; use crate::{ - config::EbicsKeysCfg, + config::EbicsSetupCfg, crypto::ebics_pub_key_hash, ebics::{EbicsClient, administrative::VersionNumber, order::Order}, keys::{ BankKeys, ClientKeys, load_bank_keys, load_client_keys, persist_bank_keys, persist_client_keys, }, + pdf::generate_keys_pdf, utils::hex_chunk_by_two, }; @@ -52,13 +53,13 @@ pub fn load_or_generate_client_keys(path: &Path) -> anyhow::Result<ClientKeys> { pub async fn ebics_setup( ebics: &EbicsClient<'_>, - cfg: &EbicsKeysCfg<'_>, + cfg: &EbicsSetupCfg<'_>, force_keys_resubmission: bool, generate_registration_pdf: bool, auto_accept_keys: bool, ) -> anyhow::Result<(ClientKeys, BankKeys)> { - let mut client = load_or_generate_client_keys(cfg.client.as_ref())?; - let bank = load_bank_keys(cfg.bank.as_ref())?; + let mut client = load_or_generate_client_keys(cfg.keys.client.as_ref())?; + let bank = load_bank_keys(cfg.keys.bank.as_ref())?; // Check EBICS 3 support let versions = ebics.hev().await?; @@ -84,14 +85,20 @@ pub async fn ebics_setup( let keys_not_sub = !client.submitted_ini; if !client.submitted_ini || force_keys_resubmission { ebics - .submit_client_keys(cfg, &mut client, Order::INI) + .submit_client_keys(&cfg.keys, &mut client, Order::INI) .await?; } // Eject PDF if the keys were submitted for the first time, or the user asked. - // TODO if (keysNotSub || generateRegistrationPdf) makePdf(clientKeys, hostCfg) + if keys_not_sub || generate_registration_pdf { + let pdf = generate_keys_pdf(&client, &cfg.host)?; + let path = "/tmp/libeufin-ebics-keys.pdf"; + std::fs::write("/tmp/libeufin-ebics-keys.pdf", &pdf) + .map_err(|e| anyhow!("Could not write PDF to '{path}': {}", e.kind()))?; + info!(target: "setup", "PDF file with keys created at '{path}'"); + } if !client.submitted_hia || force_keys_resubmission { ebics - .submit_client_keys(cfg, &mut client, Order::HIA) + .submit_client_keys(&cfg.keys, &mut client, Order::HIA) .await?; } @@ -101,31 +108,57 @@ pub async fn ebics_setup( if current.enc != new.enc { bail!( "On disk bank encryption key stored at {} doesn't match server key\nDisk: {}\nServer: {}", - cfg.bank, + cfg.keys.bank, hex_chunk_by_two(ebics_pub_key_hash(&current.enc.key)), hex_chunk_by_two(ebics_pub_key_hash(&new.enc.key)) ) } else if current.auth != new.auth { bail!( "On disk bank authentication key stored at {} doesn't match server key\nDisk: {}\nServer: {}", - cfg.bank, + cfg.keys.bank, hex_chunk_by_two(ebics_pub_key_hash(&current.auth.key)), hex_chunk_by_two(ebics_pub_key_hash(&new.auth.key)) ) } } else { // Accept bank keys - info!("Bank keys stored at {}", cfg.bank); - persist_bank_keys(&new, cfg.bank.as_ref())?; + info!("Bank keys stored at {}", cfg.keys.bank); + persist_bank_keys(&new, cfg.keys.bank.as_ref())?; }; let mut bank = new; if !bank.accepted { // Finishing the setup by accepting the bank keys. - if !auto_accept_keys { - panic!("Cannot successfully finish the setup without accepting the bank keys"); + let enc_hash = ebics_pub_key_hash(&bank.enc.key); + let auth_hash = ebics_pub_key_hash(&bank.auth.key); + if auto_accept_keys { + bank.accepted = true + } else if let Some(enc) = cfg.enc + && let Some(auth) = cfg.auth + { + if enc == enc_hash.as_ref() && auth == auth_hash.as_ref() { + info!(target: "setup", "Accepting bank keys matching config hashes"); + bank.accepted = true + } else { + bail!( + "Bank keys does not match config hashes\nBank encryption key: {}\nConfig encryption key: {}\nBank authentication key: {}\nConfig authentication key: {}", + hex_chunk_by_two(enc_hash), + hex_chunk_by_two(enc), + hex_chunk_by_two(auth_hash), + hex_chunk_by_two(auth), + ) + } + } else { + println!( + "The bank has the following keys:\nEncryption key: {}\nAuthentication key: {}", + hex_chunk_by_two(enc_hash), + hex_chunk_by_two(auth_hash) + ); + bank.accepted = dialoguer::Confirm::new().interact()? + } + if !bank.accepted { + bail!("Cannot successfully finish the setup without accepting the bank keys"); } - bank.accepted = true; - persist_bank_keys(&bank, cfg.bank.as_ref())?; + persist_bank_keys(&bank, cfg.keys.bank.as_ref())?; } Ok((client, bank)) diff --git a/crates/libeufin-ebics/src/utils.rs b/crates/libeufin-ebics/src/utils.rs @@ -39,7 +39,7 @@ pub fn inflate(bytes: &[u8]) -> Vec<u8> { pub fn hex_chunk_by_two<'a>(bytes: impl AsRef<[u8]> + 'a) -> impl Display + 'a { std::fmt::from_fn(move |f| { for b in bytes.as_ref() { - write!(f, "{b:X} ")?; + write!(f, "{b:02X} ")?; } Ok(()) }) diff --git a/crates/libeufin-nexus/Cargo.toml b/crates/libeufin-nexus/Cargo.toml @@ -34,4 +34,4 @@ zip = { version = "8.5", default-features = false, features = [ ] } tracing-subscriber = "0.3" owo-colors = "4.3" -shlex = "2.0" +shlex = "2.0" +\ No newline at end of file diff --git a/crates/libeufin-nexus/src/config.rs b/crates/libeufin-nexus/src/config.rs @@ -24,7 +24,7 @@ use jiff::{ civil::{Date, Time}, }; use libeufin_ebics::{ - config::{EbicsHostCfg, EbicsKeysCfg}, + config::{EbicsHostCfg, EbicsKeysCfg, EbicsSetupCfg}, dialect::Dialect, }; use regex::Regex; @@ -205,6 +205,21 @@ impl NexusEbicsConfig { } } +pub struct NexusSetupConfig { + pub enc: Option<Vec<u8>>, + pub auth: Option<Vec<u8>>, +} + +impl NexusSetupConfig { + pub fn parse(cfg: &Config) -> Result<Self, ValueErr> { + let s = cfg.section("nexus-setup"); + Ok(Self { + enc: s.hex("bank_encryption_pub_key_hash").opt()?, + auth: s.hex("bank_authentication_pub_key_hash").opt()?, + }) + } +} + pub struct NexusCfg { pub cfg: Config, pub currency: Currency, @@ -214,6 +229,7 @@ pub struct NexusCfg { pub fetch: OnceCell<NexusFetchCfg>, pub submit: OnceCell<NexusSubmitCfg>, pub ebics: OnceCell<NexusEbicsConfig>, + pub setup: OnceCell<NexusSetupConfig>, pub wire_cfg: Option<ApiCfg>, pub revenue_cfg: Option<ApiCfg>, pub serve_cfg: Serve, @@ -233,6 +249,7 @@ impl NexusCfg { fetch: OnceCell::new(), submit: OnceCell::new(), ebics: OnceCell::new(), + setup: OnceCell::new(), cfg, }) } @@ -287,6 +304,16 @@ impl NexusCfg { Ok(self.ebics.get().unwrap()) } + pub fn setup(&self) -> Result<&NexusSetupConfig, ValueErr> { + // TODO use get_or_try_init when stable + if let Some(setup) = self.setup.get() { + return Ok(setup); + } + let setup = NexusSetupConfig::parse(&self.cfg)?; + self.setup.set(setup).ok(); + Ok(self.setup.get().unwrap()) + } + pub fn ingest(&self) -> Result<NexusIngestCfg, ValueErr> { let fetch = self.fetch()?; Ok(NexusIngestCfg { @@ -299,4 +326,14 @@ impl NexusCfg { currency: self.currency, }) } + + pub fn ebics_setup<'a>(&'a self) -> Result<EbicsSetupCfg<'a>, ValueErr> { + let setup = self.setup()?; + Ok(EbicsSetupCfg { + keys: self.keys()?.ebics(), + host: self.host()?.ebics(), + enc: setup.enc.as_deref(), + auth: setup.auth.as_deref(), + }) + } } diff --git a/crates/libeufin-nexus/src/lib.rs b/crates/libeufin-nexus/src/lib.rs @@ -50,7 +50,7 @@ use tracing::{debug, error, info, warn}; use crate::{ api::NexusApi, - config::{NexusCfg, NexusKeysCfg}, + config::NexusCfg, db::{ dbinit, initiated::{ @@ -311,14 +311,14 @@ pub async fn ebics_submit( pub async fn ebics_setup( ebics: &EbicsClient<'_>, - cfg: &NexusKeysCfg, + cfg: &NexusCfg, force_keys_resubmission: bool, generate_registration_pdf: bool, auto_accept_keys: bool, ) -> anyhow::Result<()> { let (client, bank) = libeufin_ebics::setup::ebics_setup( ebics, - &cfg.ebics(), + &cfg.ebics_setup()?, force_keys_resubmission, generate_registration_pdf, auto_accept_keys, @@ -348,7 +348,7 @@ pub async fn run(cfg: Config, cmd: Cmd) -> anyhow::Result<()> { let ebics = EbicsClient::new(cfg.host()?.ebics(), ebics_logs)?; ebics_setup( &ebics, - cfg.keys()?, + &cfg, force_keys_resubmission, generate_registration_pdf, auto_accept_keys, diff --git a/crates/libeufin-nexus/src/testing.rs b/crates/libeufin-nexus/src/testing.rs @@ -19,7 +19,7 @@ use anyhow::{anyhow, bail}; use compact_str::CompactString; -use jiff::{Timestamp, civil::Date}; +use jiff::{Timestamp, civil::Date, tz::TimeZone}; use libeufin_ebics::{ ebics::{ EbicsErrKind, @@ -210,7 +210,12 @@ impl TestingCmd { &client, &bank, &order, - &None, // TODO + &pinned_start.map(|date| { + ( + date.to_zoned(TimeZone::UTC).unwrap().timestamp(), + Timestamp::now(), + ) + }), peek, async |_| { if dry_run {