Skip to main content

confium_tc_keys/
hsm_protection.rs

1//! HSM share protection interface — trait-based hardware integration.
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6/// A share sealed inside an HSM.
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct SealedShare {
9    /// HSM-internal key handle.
10    pub key_handle: String,
11    /// Encrypted share blob (opaque to the host).
12    pub encrypted_share: Vec<u8>,
13    /// HSM-attested public key.
14    pub attestation_pubkey_hex: String,
15}
16
17/// Trait for HSM share protection backends.
18pub trait HsmBackend: Send + Sync {
19    /// Seal (encrypt) a share inside the HSM.
20    fn seal(&self, share: &[u8], label: &str) -> Result<SealedShare, HsmError>;
21
22    /// Unseal (decrypt) a share from the HSM.
23    fn unseal(&self, sealed: &SealedShare) -> Result<Vec<u8>, HsmError>;
24
25    /// Generate a new key inside the HSM. Returns the handle.
26    fn generate_key(&self, label: &str) -> Result<String, HsmError>;
27
28    /// Delete a key from the HSM.
29    fn delete_key(&self, handle: &str) -> Result<(), HsmError>;
30
31    /// Attestation: prove the HSM is genuine.
32    fn attest(&self, challenge: &[u8]) -> Result<Vec<u8>, HsmError>;
33
34    /// Backend name.
35    fn name(&self) -> &str;
36}
37
38/// HSM errors.
39#[derive(Debug)]
40pub enum HsmError {
41    KeyNotFound(String),
42    AttestationFailed,
43    OperationFailed(String),
44    Unsupported,
45}
46
47impl std::fmt::Display for HsmError {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        match self {
50            Self::KeyNotFound(h) => write!(f, "key not found: {h}"),
51            Self::AttestationFailed => write!(f, "attestation failed"),
52            Self::OperationFailed(m) => write!(f, "operation failed: {m}"),
53            Self::Unsupported => write!(f, "unsupported operation"),
54        }
55    }
56}
57
58impl std::error::Error for HsmError {}
59
60/// Mock HSM backend (in-process, for development/testing).
61#[derive(Default)]
62pub struct MockHsmBackend {
63    keys: std::sync::Mutex<HashMap<String, Vec<u8>>>,
64    counter: std::sync::Mutex<u64>,
65}
66
67impl HsmBackend for MockHsmBackend {
68    fn seal(&self, share: &[u8], label: &str) -> Result<SealedShare, HsmError> {
69        let handle = format!("mock-key-{label}");
70        self.keys
71            .lock()
72            .unwrap()
73            .insert(handle.clone(), share.to_vec());
74        Ok(SealedShare {
75            key_handle: handle,
76            encrypted_share: share.iter().map(|b| b ^ 0x42).collect(),
77            attestation_pubkey_hex: "mock-attestation-pubkey".into(),
78        })
79    }
80
81    fn unseal(&self, sealed: &SealedShare) -> Result<Vec<u8>, HsmError> {
82        let keys = self.keys.lock().unwrap();
83        match keys.get(&sealed.key_handle) {
84            Some(share) => Ok(share.clone()),
85            None => Err(HsmError::KeyNotFound(sealed.key_handle.clone())),
86        }
87    }
88
89    fn generate_key(&self, label: &str) -> Result<String, HsmError> {
90        let mut counter = self.counter.lock().unwrap();
91        *counter += 1;
92        let handle = format!("mock-key-{label}-{}", *counter);
93        self.keys
94            .lock()
95            .unwrap()
96            .insert(handle.clone(), vec![0; 32]);
97        Ok(handle)
98    }
99
100    fn delete_key(&self, handle: &str) -> Result<(), HsmError> {
101        self.keys.lock().unwrap().remove(handle);
102        Ok(())
103    }
104
105    fn attest(&self, challenge: &[u8]) -> Result<Vec<u8>, HsmError> {
106        let mut result = vec![0u8; 32];
107        for (i, b) in challenge.iter().enumerate() {
108            result[i % 32] ^= b;
109        }
110        Ok(result)
111    }
112
113    fn name(&self) -> &str {
114        "mock-hsm"
115    }
116}
117
118/// A share vault that uses an HSM backend for protection.
119pub struct ShareVault {
120    backend: Box<dyn HsmBackend>,
121}
122
123impl ShareVault {
124    pub fn new(backend: Box<dyn HsmBackend>) -> Self {
125        Self { backend }
126    }
127
128    pub fn store(&self, party_idx: u32, share: &[u8]) -> Result<SealedShare, HsmError> {
129        self.backend.seal(share, &format!("party-{party_idx}"))
130    }
131
132    pub fn retrieve(&self, sealed: &SealedShare) -> Result<Vec<u8>, HsmError> {
133        self.backend.unseal(sealed)
134    }
135
136    pub fn attest(&self, challenge: &[u8]) -> Result<Vec<u8>, HsmError> {
137        self.backend.attest(challenge)
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn mock_backend_seal_unseal() {
147        let hsm = MockHsmBackend::default();
148        let sealed = hsm.seal(b"secret share", "test").unwrap();
149        let recovered = hsm.unseal(&sealed).unwrap();
150        assert_eq!(recovered, b"secret share");
151    }
152
153    #[test]
154    fn mock_backend_key_not_found() {
155        let hsm = MockHsmBackend::default();
156        let sealed = SealedShare {
157            key_handle: "nonexistent".into(),
158            encrypted_share: vec![],
159            attestation_pubkey_hex: "".into(),
160        };
161        assert!(matches!(hsm.unseal(&sealed), Err(HsmError::KeyNotFound(_))));
162    }
163
164    #[test]
165    fn mock_backend_generate_and_delete() {
166        let hsm = MockHsmBackend::default();
167        let handle = hsm.generate_key("test").unwrap();
168        hsm.delete_key(&handle).unwrap();
169    }
170
171    #[test]
172    fn mock_backend_attest() {
173        let hsm = MockHsmBackend::default();
174        let attestation = hsm.attest(b"challenge").unwrap();
175        assert_eq!(attestation.len(), 32);
176    }
177
178    #[test]
179    fn vault_store_retrieve() {
180        let vault = ShareVault::new(Box::new(MockHsmBackend::default()));
181        let sealed = vault.store(1, b"party-1-share").unwrap();
182        let recovered = vault.retrieve(&sealed).unwrap();
183        assert_eq!(recovered, b"party-1-share");
184    }
185
186    #[test]
187    fn vault_attest() {
188        let vault = ShareVault::new(Box::new(MockHsmBackend::default()));
189        let attestation = vault.attest(b"nonce").unwrap();
190        assert_eq!(attestation.len(), 32);
191    }
192
193    #[test]
194    fn different_labels_different_handles() {
195        let hsm = MockHsmBackend::default();
196        let s1 = hsm.seal(b"a", "label1").unwrap();
197        let s2 = hsm.seal(b"b", "label2").unwrap();
198        assert_ne!(s1.key_handle, s2.key_handle);
199    }
200
201    #[test]
202    fn backend_name() {
203        let hsm = MockHsmBackend::default();
204        assert_eq!(hsm.name(), "mock-hsm");
205    }
206}