libeufin

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

keys.rs (8504B)


      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::ErrorKind, path::Path};
     21 
     22 use anyhow::anyhow;
     23 use aws_lc_rs::{
     24     encoding::{AsDer, Pkcs8V1Der, PublicKeyX509Der},
     25     rsa::{KeyPair, KeySize, PublicKey},
     26     signature::KeyPair as _,
     27 };
     28 use serde::{Deserialize, Deserializer, Serialize, Serializer};
     29 use taler_common::{
     30     config::CfgErr,
     31     config_bail,
     32     encoding::base32::{self},
     33     json_file,
     34 };
     35 
     36 use crate::config::EbicsKeysCfg;
     37 
     38 #[derive(Debug, Serialize, Deserialize)]
     39 pub struct ClientKeys {
     40     #[serde(
     41         rename = "signature_private_key",
     42         serialize_with = "ser_pkcs8",
     43         deserialize_with = "de_ras_sign_base32"
     44     )]
     45     pub sign: KeyPair,
     46     #[serde(
     47         rename = "encryption_private_key",
     48         serialize_with = "ser_pkcs8",
     49         deserialize_with = "de_ras_sign_base32"
     50     )]
     51     pub enc: KeyPair,
     52     #[serde(
     53         rename = "authentication_private_key",
     54         serialize_with = "ser_pkcs8",
     55         deserialize_with = "de_ras_sign_base32"
     56     )]
     57     pub auth: KeyPair,
     58     pub submitted_ini: bool,
     59     pub submitted_hia: bool,
     60 }
     61 
     62 impl ClientKeys {
     63     pub fn generate() -> anyhow::Result<Self> {
     64         Ok(Self {
     65             sign: KeyPair::generate(KeySize::Rsa2048)?,
     66             enc: KeyPair::generate(KeySize::Rsa2048)?,
     67             auth: KeyPair::generate(KeySize::Rsa2048)?,
     68             submitted_ini: false,
     69             submitted_hia: false,
     70         })
     71     }
     72 }
     73 
     74 #[derive(Debug, Serialize, Deserialize)]
     75 pub struct BankKeys {
     76     #[serde(
     77         rename = "bank_encryption_public_key",
     78         serialize_with = "ser_der",
     79         deserialize_with = "de_ras_priv_base32"
     80     )]
     81     pub enc: PublicKey,
     82     #[serde(
     83         rename = "bank_authentication_public_key",
     84         serialize_with = "ser_der",
     85         deserialize_with = "de_ras_priv_base32"
     86     )]
     87     pub auth: PublicKey,
     88     pub accepted: bool,
     89 }
     90 
     91 impl PartialEq for BankKeys {
     92     fn eq(&self, other: &Self) -> bool {
     93         self.enc.as_ref() == other.enc.as_ref() && self.auth.as_ref() == other.auth.as_ref()
     94     }
     95 }
     96 
     97 impl Eq for BankKeys {}
     98 
     99 impl BankKeys {
    100     pub fn generate() -> Self {
    101         Self {
    102             enc: KeyPair::generate(KeySize::Rsa2048)
    103                 .unwrap()
    104                 .public_key()
    105                 .clone(),
    106             auth: KeyPair::generate(KeySize::Rsa2048)
    107                 .unwrap()
    108                 .public_key()
    109                 .clone(),
    110             accepted: false,
    111         }
    112     }
    113 }
    114 
    115 fn ser_pkcs8<S, K>(key: &K, serializer: S) -> Result<S::Ok, S::Error>
    116 where
    117     K: AsDer<Pkcs8V1Der<'static>>,
    118     S: Serializer,
    119 {
    120     let der = key
    121         .as_der()
    122         .map_err(|e| serde::ser::Error::custom(e.to_string()))?;
    123     let base32 = base32::encode(der.as_ref());
    124     base32.serialize(serializer)
    125 }
    126 
    127 fn ser_der<S, K>(key: &K, serializer: S) -> Result<S::Ok, S::Error>
    128 where
    129     K: AsDer<PublicKeyX509Der<'static>>,
    130     S: Serializer,
    131 {
    132     let der = key
    133         .as_der()
    134         .map_err(|e| serde::ser::Error::custom(e.to_string()))?;
    135     let base32 = base32::encode(der.as_ref());
    136     base32.serialize(serializer)
    137 }
    138 
    139 fn de_ras_priv_base32<'de, D>(deserializer: D) -> Result<PublicKey, D::Error>
    140 where
    141     D: Deserializer<'de>,
    142 {
    143     let base32 = Cow::<str>::deserialize(deserializer)?;
    144     let der =
    145         base32::decode(base32.as_bytes()).map_err(|e| serde::de::Error::custom(e.to_string()))?;
    146     let key = PublicKey::from_der(&der).map_err(|e| serde::de::Error::custom(e.to_string()))?;
    147     Ok(key)
    148 }
    149 
    150 fn de_ras_sign_base32<'de, D>(deserializer: D) -> Result<KeyPair, D::Error>
    151 where
    152     D: Deserializer<'de>,
    153 {
    154     let base32 = Cow::<str>::deserialize(deserializer)?;
    155     let der =
    156         base32::decode(base32.as_bytes()).map_err(|e| serde::de::Error::custom(e.to_string()))?;
    157     let key = KeyPair::from_pkcs8(&der).map_err(|e| serde::de::Error::custom(e.to_string()))?;
    158     Ok(key)
    159 }
    160 
    161 /// Persist the bank keys file to disk
    162 pub fn persist_bank_keys(keys: &BankKeys, path: &Path) -> anyhow::Result<()> {
    163     json_file::persist(path, keys).map_err(|e| {
    164         anyhow!(
    165             "Could not write bank public keys at '{}': {}",
    166             path.to_string_lossy(),
    167             e.kind()
    168         )
    169     })
    170 }
    171 
    172 pub fn persist_client_keys(keys: &ClientKeys, path: &Path) -> anyhow::Result<()> {
    173     json_file::persist(path, keys).map_err(|e| {
    174         anyhow!(
    175             "Could not write client private keys at '{}': {}",
    176             path.to_string_lossy(),
    177             e.kind()
    178         )
    179     })
    180 }
    181 
    182 /// Load the bank keys file from disk
    183 pub fn load_bank_keys(path: &Path) -> anyhow::Result<Option<BankKeys>> {
    184     match json_file::load(path) {
    185         Ok(existing) => Ok(Some(existing)),
    186         Err(e) if e.kind() == ErrorKind::NotFound => Ok(None),
    187         Err(e) => anyhow::bail!(
    188             "Could not read bank public keys at '{}': {}",
    189             path.to_string_lossy(),
    190             e.kind()
    191         ),
    192     }
    193 }
    194 
    195 /// Load the client keys file from disk
    196 pub fn load_client_keys(path: &Path) -> anyhow::Result<Option<ClientKeys>> {
    197     match json_file::load(path) {
    198         Ok(existing) => Ok(Some(existing)),
    199         Err(e) if e.kind() == ErrorKind::NotFound => Ok(None),
    200         Err(e) => anyhow::bail!(
    201             "Could not read client private keys at '{}': {}",
    202             path.to_string_lossy(),
    203             e.kind()
    204         ),
    205     }
    206 }
    207 
    208 /// Load client and bank keys from disk and checks that the keying process has been fully completed
    209 pub fn expect_full_keys(
    210     cfg: &EbicsKeysCfg,
    211     setup_cmd: &str,
    212 ) -> Result<(ClientKeys, BankKeys), CfgErr> {
    213     let client_keys = load_client_keys(cfg.client.as_ref()).map_err(CfgErr::custom)?;
    214     let Some(client_keys) = client_keys else {
    215         config_bail!(
    216             "Missing client private keys file at '{}', run '{setup_cmd}' first",
    217             cfg.client
    218         )
    219     };
    220     if !client_keys.submitted_ini || !client_keys.submitted_hia {
    221         config_bail!("Unsubmitted client private keys, run '{setup_cmd}' first")
    222     }
    223     let bank_keys = load_bank_keys(cfg.bank.as_ref()).map_err(CfgErr::custom)?;
    224     let Some(bank_keys) = bank_keys else {
    225         config_bail!(
    226             "Missing bank public keys file at '{}', run '{setup_cmd}' first",
    227             cfg.bank
    228         )
    229     };
    230     if !bank_keys.accepted {
    231         config_bail!("Unaccepted bank public keys, run '{setup_cmd}' until accepting the bank keys")
    232     }
    233     Ok((client_keys, bank_keys))
    234 }
    235 
    236 #[cfg(test)]
    237 mod test {
    238     use std::path::Path;
    239 
    240     use serde_json::json;
    241 
    242     use crate::keys::{
    243         BankKeys, ClientKeys, load_bank_keys, load_client_keys, persist_bank_keys,
    244         persist_client_keys,
    245     };
    246 
    247     /// Loading bank public keys from disk
    248     #[test]
    249     fn bank() {
    250         let path: &Path = "/tmp/nexus-tests-bank-keys.json".as_ref();
    251         let keys = BankKeys::generate();
    252         if std::fs::exists(path).unwrap() {
    253             std::fs::remove_file(path).unwrap()
    254         }
    255         persist_bank_keys(&keys, path).unwrap();
    256         let loaded = load_bank_keys(path).unwrap().unwrap();
    257         assert_eq!(loaded, keys);
    258         persist_bank_keys(&loaded, path).unwrap();
    259 
    260         assert!(
    261             load_bank_keys("/tmp/highly-unlikely-to-be-found.json".as_ref())
    262                 .unwrap()
    263                 .is_none()
    264         );
    265     }
    266 
    267     /// Loading client private keys from disk
    268     #[test]
    269     fn client() {
    270         let path: &Path = "/tmp/nexus-tests-client-keys.json".as_ref();
    271         let keys = ClientKeys::generate().unwrap();
    272         persist_client_keys(&keys, path).unwrap();
    273         let loaded = load_client_keys(path).unwrap().unwrap();
    274         assert_eq!(json!(loaded), json!(keys));
    275 
    276         assert!(
    277             load_client_keys("/tmp/highly-unlikely-to-be-found.json".as_ref())
    278                 .unwrap()
    279                 .is_none()
    280         );
    281     }
    282 }