Skip to main content

crypto_core/
hsm.rs

1//! HSM (Hardware Security Module) integration
2//!
3//! Provides abstraction layer for Thales Luna HSM and other PKCS#11 compatible HSMs
4
5use crate::{CryptoError, KeyMetadata, Result};
6use ed25519_dalek::{Signature as Ed25519Signature, Verifier as _, VerifyingKey};
7use rand::RngCore;
8use serde::{Deserialize, Serialize};
9
10/// HSM configuration
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct HsmConfig {
13    /// HSM type
14    pub hsm_type: HsmType,
15    /// Connection string
16    pub connection: String,
17    /// Slot number
18    pub slot: u32,
19    /// Key label prefix
20    pub key_label_prefix: String,
21}
22
23/// HSM type
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
25#[serde(rename_all = "lowercase")]
26pub enum HsmType {
27    /// Thales Luna HSM
28    #[default]
29    ThalesLuna,
30    /// Utimaco HSM
31    Utimaco,
32    /// AWS CloudHSM
33    AwsCloudHsm,
34    /// Azure Key Vault HSM
35    AzureKeyVault,
36    /// Software simulation (testing)
37    SoftHSM,
38}
39
40/// HSM key handle
41#[derive(Debug, Clone)]
42pub struct HsmKeyHandle {
43    pub key_id: String,
44    pub slot: u32,
45    pub algorithm: String,
46}
47
48/// HSM session
49pub trait HsmSignerOps {
50    /// Sign data with key
51    fn sign(&mut self, key_handle: &HsmKeyHandle, data: &[u8]) -> Result<Vec<u8>>;
52}
53
54pub trait HsmVerifierOps {
55    /// Verify signature
56    fn verify(&mut self, key_handle: &HsmKeyHandle, data: &[u8], signature: &[u8]) -> Result<bool>;
57}
58
59/// HSM session
60pub trait HsmSession: Send + HsmSignerOps + HsmVerifierOps {
61    /// Generate key pair
62    fn generate_key_pair(&mut self, algorithm: &str, key_id: &str) -> Result<HsmKeyHandle>;
63
64    /// Import key
65    fn import_key(&mut self, key_id: &str, key_data: &[u8]) -> Result<HsmKeyHandle>;
66
67    /// Close session
68    fn close(&mut self);
69}
70
71/// SoftHSM implementation for testing
72pub 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
182/// Create HSM session based on configuration
183pub 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
198/// HSM-backed signer
199pub struct HsmSigner {
200    session: Box<dyn HsmSession>,
201    key_handle: HsmKeyHandle,
202    config: HsmConfig,
203}
204
205impl HsmSigner {
206    /// Create new HSM signer
207    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    /// Sign data
219    pub fn sign(&mut self, data: &[u8]) -> Result<Vec<u8>> {
220        self.session.sign(&self.key_handle, data)
221    }
222
223    /// Verify signature (best-effort depending on HSM backend support)
224    pub fn verify(&mut self, data: &[u8], signature: &[u8]) -> Result<bool> {
225        self.session.verify(&self.key_handle, data, signature)
226    }
227
228    /// Get key metadata
229    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}