commit ad8c3edb0dd866376e411fea1fecc3d4a773e5fa
parent c4d6004bfa395140a228591147128405c0bdb557
Author: Antoine A <>
Date: Wed, 27 May 2026 14:25:24 +0200
nexus: add PDF gen
Diffstat:
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(®, &mut y, 13.0, &format!("Datum: {date}"));
+ line(®, &mut y, 13.0, &format!("Host-ID: {}", cfg.host_id));
+ line(®, &mut y, 13.0, &format!("User-ID: {}", cfg.user_id));
+ line(
+ ®,
+ &mut y,
+ 11.0,
+ &format!("Partner-ID: {}", cfg.partner_id),
+ );
+ line(®, &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(®, &mut y, 11.0, &hex_chunk_by_two(chunk).to_string());
+ }
+ y -= 5.0;
+ }
+
+ // Write Footer
+ line(®, &mut y, 13.0, "Ort / Datum: ________________");
+ line(®, &mut y, 13.0, "Firma / Name: ________________");
+ line(®, &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(¤t.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(¤t.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 {