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 }