1use crate::{CryptoError, KeyMetadata, Result};
6use ed25519_dalek::{Signature as Ed25519Signature, Verifier as _, VerifyingKey};
7use rand::RngCore;
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct HsmConfig {
13 pub hsm_type: HsmType,
15 pub connection: String,
17 pub slot: u32,
19 pub key_label_prefix: String,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
25#[serde(rename_all = "lowercase")]
26pub enum HsmType {
27 #[default]
29 ThalesLuna,
30 Utimaco,
32 AwsCloudHsm,
34 AzureKeyVault,
36 SoftHSM,
38}
39
40#[derive(Debug, Clone)]
42pub struct HsmKeyHandle {
43 pub key_id: String,
44 pub slot: u32,
45 pub algorithm: String,
46}
47
48pub trait HsmSignerOps {
50 fn sign(&mut self, key_handle: &HsmKeyHandle, data: &[u8]) -> Result<Vec<u8>>;
52}
53
54pub trait HsmVerifierOps {
55 fn verify(&mut self, key_handle: &HsmKeyHandle, data: &[u8], signature: &[u8]) -> Result<bool>;
57}
58
59pub trait HsmSession: Send + HsmSignerOps + HsmVerifierOps {
61 fn generate_key_pair(&mut self, algorithm: &str, key_id: &str) -> Result<HsmKeyHandle>;
63
64 fn import_key(&mut self, key_id: &str, key_data: &[u8]) -> Result<HsmKeyHandle>;
66
67 fn close(&mut self);
69}
70
71pub struct SoftHsm {
73 config: HsmConfig,
74 keys: std::collections::HashMap<String, Vec<u8>>,
75 public_keys: std::collections::HashMap<String, Vec<u8>>,
76}
77
78impl SoftHsm {
79 pub fn new(config: HsmConfig) -> Self {
80 SoftHsm {
81 config,
82 keys: std::collections::HashMap::new(),
83 public_keys: std::collections::HashMap::new(),
84 }
85 }
86}
87
88impl HsmSignerOps for SoftHsm {
89 fn sign(&mut self, key_handle: &HsmKeyHandle, data: &[u8]) -> Result<Vec<u8>> {
90 let key_data = self
91 .keys
92 .get(&key_handle.key_id)
93 .ok_or_else(|| CryptoError::HsmError("Key not found".to_string()))?;
94 if key_data.len() != 32 {
95 return Err(CryptoError::HsmError(
96 "Invalid SoftHSM key size (expected 32-byte Ed25519 seed)".to_string(),
97 ));
98 }
99 let mut seed = [0u8; 32];
100 seed.copy_from_slice(key_data);
101 let key =
102 crate::signature::Ed25519KeyPair::from_seed(seed, Some(key_handle.key_id.clone()));
103 Ok(key.sign(data))
104 }
105}
106
107impl HsmVerifierOps for SoftHsm {
108 fn verify(&mut self, key_handle: &HsmKeyHandle, data: &[u8], signature: &[u8]) -> Result<bool> {
109 if signature.len() != 64 {
110 return Ok(false);
111 }
112 let public_key = match self.public_keys.get(&key_handle.key_id) {
113 Some(pk) => pk,
114 None => {
115 return Err(CryptoError::HsmError(format!(
116 "Public key not found for key_id {}",
117 key_handle.key_id
118 )));
119 }
120 };
121 if public_key.len() != 32 {
122 return Err(CryptoError::HsmError(
123 "Invalid SoftHSM public key size (expected 32-byte Ed25519 public key)".to_string(),
124 ));
125 }
126 let mut pk = [0u8; 32];
127 pk.copy_from_slice(public_key);
128 let verifying_key = VerifyingKey::from_bytes(&pk).map_err(|e| {
129 CryptoError::HsmError(format!("Invalid SoftHSM Ed25519 public key bytes: {e}"))
130 })?;
131 let mut sig = [0u8; 64];
132 sig.copy_from_slice(signature);
133 let signature = Ed25519Signature::from_bytes(&sig);
134 Ok(verifying_key.verify(data, &signature).is_ok())
135 }
136}
137
138impl HsmSession for SoftHsm {
139 fn generate_key_pair(&mut self, algorithm: &str, key_id: &str) -> Result<HsmKeyHandle> {
140 let mut key_data = vec![0u8; 32];
141 rand::rngs::OsRng.fill_bytes(&mut key_data);
142 let mut seed = [0u8; 32];
143 seed.copy_from_slice(&key_data);
144 let key = crate::signature::Ed25519KeyPair::from_seed(seed, Some(key_id.to_string()));
145 self.public_keys
146 .insert(key_id.to_string(), key.verifying_key());
147 self.keys.insert(key_id.to_string(), key_data);
148
149 Ok(HsmKeyHandle {
150 key_id: key_id.to_string(),
151 slot: self.config.slot,
152 algorithm: algorithm.to_string(),
153 })
154 }
155
156 fn import_key(&mut self, key_id: &str, key_data: &[u8]) -> Result<HsmKeyHandle> {
157 if key_data.len() != 32 {
158 return Err(CryptoError::HsmError(
159 "SoftHSM import expects a 32-byte Ed25519 seed".to_string(),
160 ));
161 }
162 let mut seed = [0u8; 32];
163 seed.copy_from_slice(key_data);
164 let key = crate::signature::Ed25519KeyPair::from_seed(seed, Some(key_id.to_string()));
165 self.public_keys
166 .insert(key_id.to_string(), key.verifying_key());
167 self.keys.insert(key_id.to_string(), key_data.to_vec());
168
169 Ok(HsmKeyHandle {
170 key_id: key_id.to_string(),
171 slot: self.config.slot,
172 algorithm: "ED25519".to_string(),
173 })
174 }
175
176 fn close(&mut self) {
177 self.keys.clear();
178 self.public_keys.clear();
179 }
180}
181
182pub fn create_hsm_session(config: &HsmConfig) -> Result<Box<dyn HsmSession>> {
184 if matches!(config.hsm_type, HsmType::SoftHSM) && is_production_environment() {
185 return Err(CryptoError::HsmError(
186 "SoftHSM is forbidden in production environment".to_string(),
187 ));
188 }
189 match config.hsm_type {
190 HsmType::SoftHSM => Ok(Box::new(SoftHsm::new(config.clone()))),
191 _ => Err(CryptoError::HsmError(format!(
192 "HSM type {:?} not implemented",
193 config.hsm_type
194 ))),
195 }
196}
197
198pub struct HsmSigner {
200 session: Box<dyn HsmSession>,
201 key_handle: HsmKeyHandle,
202 config: HsmConfig,
203}
204
205impl HsmSigner {
206 pub fn new(config: HsmConfig, key_id: &str) -> Result<Self> {
208 let mut session = create_hsm_session(&config)?;
209 let key_handle = session.generate_key_pair("ED25519", key_id)?;
210
211 Ok(HsmSigner {
212 session,
213 key_handle,
214 config,
215 })
216 }
217
218 pub fn sign(&mut self, data: &[u8]) -> Result<Vec<u8>> {
220 self.session.sign(&self.key_handle, data)
221 }
222
223 pub fn verify(&mut self, data: &[u8], signature: &[u8]) -> Result<bool> {
225 self.session.verify(&self.key_handle, data, signature)
226 }
227
228 pub fn metadata(&self) -> KeyMetadata {
230 let algorithm = match self.key_handle.algorithm.to_ascii_uppercase().as_str() {
231 "ED25519" => crate::SignatureAlgorithm::Ed25519,
232 "RSA" | "RSA-PSS" | "RSA-PSS-4096" => crate::SignatureAlgorithm::RsaPss4096,
233 _ => crate::SignatureAlgorithm::Ed25519,
234 };
235 KeyMetadata {
236 key_id: self.key_handle.key_id.clone(),
237 algorithm,
238 created_at: chrono::Utc::now().timestamp(),
239 key_type: crate::KeyType::HsmBacked,
240 hsm_slot: Some(self.config.slot.to_string()),
241 }
242 }
243}
244
245fn is_production_environment() -> bool {
246 [
247 std::env::var("ENV").ok(),
248 std::env::var("APP_ENV").ok(),
249 std::env::var("RUST_ENV").ok(),
250 ]
251 .into_iter()
252 .flatten()
253 .any(|v| matches!(v.to_ascii_lowercase().as_str(), "prod" | "production"))
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 #[test]
261 fn test_soft_hsm() {
262 let config = HsmConfig {
263 hsm_type: HsmType::SoftHSM,
264 connection: "localhost".to_string(),
265 slot: 0,
266 key_label_prefix: "test".to_string(),
267 };
268
269 let mut session = create_hsm_session(&config).unwrap();
270 let handle = session.generate_key_pair("RSA", "test-key").unwrap();
271
272 let data = b"test data";
273 let signature = session.sign(&handle, data).unwrap();
274
275 assert!(session.verify(&handle, data, &signature).unwrap());
276 }
277
278 #[test]
279 fn test_hsm_signer_soft_hsm_roundtrip() {
280 let config = HsmConfig {
281 hsm_type: HsmType::SoftHSM,
282 connection: "local://softhsm".to_string(),
283 slot: 0,
284 key_label_prefix: "audit".to_string(),
285 };
286
287 let mut signer = HsmSigner::new(config, "audit-key").unwrap();
288 let data = b"daily publication payload";
289 let sig = signer.sign(data).unwrap();
290
291 assert!(signer.verify(data, &sig).unwrap());
292 assert!(!signer.verify(b"tampered", &sig).unwrap());
293 }
294
295 #[test]
296 fn test_soft_hsm_verify_rejects_signature_from_other_key() {
297 let config = HsmConfig {
298 hsm_type: HsmType::SoftHSM,
299 connection: "local://softhsm".to_string(),
300 slot: 0,
301 key_label_prefix: "audit".to_string(),
302 };
303
304 let mut s1 = HsmSigner::new(config.clone(), "k1").unwrap();
305 let mut s2 = HsmSigner::new(config, "k2").unwrap();
306 let data = b"daily publication payload";
307 let sig = s1.sign(data).unwrap();
308
309 assert!(!s2.verify(data, &sig).unwrap());
310 }
311}