Skip to main content

kipuka_hsm/
lib.rs

1//! PKCS#11 HSM abstraction with ML-DSA and ML-KEM support.
2//!
3//! This crate provides a high-level interface to PKCS#11 Hardware Security Modules (HSMs)
4//! with full support for:
5//!
6//! - **Classical algorithms**: RSA (2048/3072/4096), ECDSA (P-256/P-384/P-521)
7//! - **Post-quantum algorithms**: ML-DSA (FIPS 204), ML-KEM (FIPS 203)
8//! - **Key wrapping**: AES Key Wrap (RFC 3394), RSAES-OAEP
9//! - **NIAP CA PP compliance**: FCS_CKM.1 key generation requirements
10//!
11//! # Supported Providers
12//!
13//! - Entrust nShield
14//! - Utimaco CryptoServer
15//! - Kryoptic (software token)
16//! - Thales Luna Cloud HSM (CSP)
17//! - Thales Luna Tactical (TCT)
18//!
19//! # Post-Quantum Cryptography
20//!
21//! PQC mechanisms (ML-DSA, ML-KEM) use the standard PKCS#11 v3.2 mechanism IDs
22//! provided by the `cryptoki` crate (e.g. `MechanismType::ML_DSA`, `MechanismType::ML_KEM`).
23//!
24//! When HSM does not support PQC, the library can fall back to software implementations
25//! using `synta-certificate`.
26//!
27//! # Architecture
28//!
29//! ```text
30//! ┌─────────────────────────────────────────────────────────┐
31//! │                     Application Layer                    │
32//! │                  (kipuka EST server)                     │
33//! └─────────────────────────────────────────────────────────┘
34//!                           │
35//!                           ▼
36//! ┌─────────────────────────────────────────────────────────┐
37//! │                    kipuka-hsm crate                      │
38//! │  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐  │
39//! │  │  HsmKeyPair  │  │  HsmSigner   │  │   HsmSlot    │  │
40//! │  └──────────────┘  └──────────────┘  └──────────────┘  │
41//! │            │              │                  │           │
42//! │            └──────────────┴──────────────────┘           │
43//! │                           │                              │
44//! │                  ┌────────▼────────┐                     │
45//! │                  │ Pkcs11Context   │                     │
46//! │                  └────────┬────────┘                     │
47//! └───────────────────────────┼──────────────────────────────┘
48//!                             │
49//!                             ▼
50//! ┌─────────────────────────────────────────────────────────┐
51//! │                     cryptoki crate                       │
52//! │              (Rust PKCS#11 bindings)                     │
53//! └─────────────────────────────────────────────────────────┘
54//!                             │
55//!                             ▼
56//! ┌─────────────────────────────────────────────────────────┐
57//! │            Vendor PKCS#11 Library (.so/.dll)             │
58//! │   (Entrust, Utimaco, Kryoptic, Thales CSP/TCT)          │
59//! └─────────────────────────────────────────────────────────┘
60//!                             │
61//!                             ▼
62//! ┌─────────────────────────────────────────────────────────┐
63//! │              Hardware Security Module                    │
64//! │           (Physical HSM or software token)               │
65//! └─────────────────────────────────────────────────────────┘
66//! ```
67//!
68//! # Example Usage
69//!
70//! ```rust,no_run
71//! use kipuka_hsm::{
72//!     HsmSlot, HsmKeyPair, KeyAlgorithm, EcdsaCurve,
73//!     Pkcs11Context, sign_ecdsa,
74//!     providers::HsmProvider,
75//! };
76//!
77//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
78//! // Initialize PKCS#11 library
79//! let provider = HsmProvider::Kryoptic;
80//! let config = provider.config();
81//! let context = Pkcs11Context::new(&config.library_path)?;
82//!
83//! // Find HSM slot
84//! let slot = HsmSlot::find_first_slot(&context)?;
85//!
86//! // Generate ECDSA P-256 key pair
87//! let key = HsmKeyPair::generate(
88//!     &slot,
89//!     KeyAlgorithm::Ecdsa(EcdsaCurve::P256),
90//!     "my-signing-key",
91//!     &[0x01, 0x02, 0x03], // CKA_ID
92//!     &config,
93//! )?;
94//!
95//! // Sign a message digest
96//! let digest = [0u8; 32]; // SHA-256 digest
97//! let signature = sign_ecdsa(&key, &digest)?;
98//! # Ok(())
99//! # }
100//! ```
101//!
102//! # ML-DSA Example
103//!
104//! ```rust,no_run
105//! use kipuka_hsm::{
106//!     HsmSlot, HsmKeyPair, KeyAlgorithm, MlDsaLevel,
107//!     Pkcs11Context, sign_ml_dsa,
108//!     providers::HsmProvider,
109//! };
110//!
111//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
112//! let provider = HsmProvider::Kryoptic;
113//! let config = provider.config();
114//! let context = Pkcs11Context::new(&config.library_path)?;
115//! let slot = HsmSlot::find_first_slot(&context)?;
116//!
117//! // Generate ML-DSA-65 key pair
118//! let key = HsmKeyPair::generate(
119//!     &slot,
120//!     KeyAlgorithm::MlDsa(MlDsaLevel::L3),
121//!     "ml-dsa-signing-key",
122//!     &[0x04, 0x05, 0x06],
123//!     &config,
124//! )?;
125//!
126//! // Sign a message (ML-DSA hashes internally)
127//! let message = b"Hello, post-quantum world!";
128//! let signature = sign_ml_dsa(&key, message, MlDsaLevel::L3)?;
129//! # Ok(())
130//! # }
131//! ```
132
133// Core modules
134pub mod error;
135pub mod key;
136pub mod pkcs11;
137pub mod sign;
138pub mod slot;
139
140// Provider registry
141pub mod providers;
142
143// rustls PKCS#11 signing key (TLS server/client key backed by HSM)
144pub mod rustls_signer;
145
146// Re-exports for convenience
147pub use error::{HsmError, HsmResult};
148pub use key::{EcdsaCurve, HsmKeyPair, KeyAlgorithm, MlDsaLevel, MlKemLevel};
149pub use pkcs11::Pkcs11Context;
150pub use providers::HsmProvider;
151pub use rustls_signer::Pkcs11SigningKey;
152pub use sign::{
153    DefaultHsmSigner, HsmSigner, RsaHashAlgorithm, SoftwarePqcFallback, sign_ecdsa, sign_ml_dsa,
154    sign_rsa_pkcs1, sign_rsa_pss,
155};
156pub use slot::HsmSlot;
157
158/// High-level HSM context wrapping PKCS#11 initialization and provider config.
159///
160/// Used by `AppState` to hold the HSM connection for the server lifetime.
161/// When fully initialized, holds a logged-in PKCS#11 session for signing.
162pub struct HsmContext {
163    pub context: Pkcs11Context,
164    pub provider: HsmProvider,
165    /// Active logged-in session for signing operations.
166    ///
167    /// Wrapped in a `Mutex` because `Session` is not `Send`+`Sync` and
168    /// signing requires `&Session` (which takes a lock internally).
169    session: std::sync::Mutex<Option<cryptoki::session::Session>>,
170    /// The slot used for this context (needed for opening new sessions).
171    #[allow(dead_code)]
172    slot: Option<HsmSlot>,
173}
174
175impl std::fmt::Debug for HsmContext {
176    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
177        f.debug_struct("HsmContext")
178            .field("provider", &self.provider)
179            .field("session", &"<Mutex<Session>>")
180            .finish()
181    }
182}
183
184// Safety: The `Session` inside `Mutex` is only accessed while locked.
185// The cryptoki `Session` is `!Send` but we only use it from within a
186// synchronous `Mutex::lock()` guard, which is safe for `Send`+`Sync`.
187unsafe impl Send for HsmContext {}
188unsafe impl Sync for HsmContext {}
189
190impl HsmContext {
191    /// Create a new HSM context with a logged-in session ready for signing.
192    pub fn new(
193        context: Pkcs11Context,
194        provider: HsmProvider,
195        slot: HsmSlot,
196        session: cryptoki::session::Session,
197    ) -> Self {
198        Self {
199            context,
200            provider,
201            session: std::sync::Mutex::new(Some(session)),
202            slot: Some(slot),
203        }
204    }
205
206    pub fn placeholder() -> Self {
207        Self {
208            context: Pkcs11Context::placeholder(),
209            provider: HsmProvider::Kryoptic,
210            session: std::sync::Mutex::new(None),
211            slot: None,
212        }
213    }
214
215    /// Check that the HSM session is initialized and the mutex is healthy.
216    ///
217    /// Returns `Ok(())` if a PKCS#11 session exists and the lock is
218    /// acquirable.  Used by health probes to verify HSM availability
219    /// without performing a signing operation.
220    pub fn health_check(&self) -> HsmResult<()> {
221        let guard = self
222            .session
223            .lock()
224            .map_err(|_| HsmError::LibraryLoad("HSM session mutex poisoned".into()))?;
225        if guard.is_some() {
226            Ok(())
227        } else {
228            Err(HsmError::LibraryLoad(
229                "HSM session not initialized (placeholder context)".into(),
230            ))
231        }
232    }
233
234    /// Resolve a strict RFC 7512 key selector against the configured token.
235    pub fn tls_key_label(&self, uri: &str) -> HsmResult<String> {
236        use cryptoki::object::{Attribute, AttributeType, ObjectClass};
237        let params = key::parse_uri(uri)?;
238        if params.get("type").is_some_and(|kind| kind != b"private") {
239            return Err(HsmError::UriParse("private key URI required".into()));
240        }
241        if let Some(token) = params.get("token") {
242            let slot = self
243                .slot
244                .as_ref()
245                .ok_or_else(|| HsmError::SlotAccess("no active slot".into()))?;
246            if token != slot.token_label()?.as_bytes() {
247                return Err(HsmError::UriParse(
248                    "token differs from configured slot".into(),
249                ));
250            }
251        }
252        let mut selector = vec![Attribute::Class(ObjectClass::PRIVATE_KEY)];
253        if let Some(label) = params.get("object") {
254            selector.push(Attribute::Label(label.clone()));
255        }
256        if let Some(id) = params.get("id") {
257            selector.push(Attribute::Id(id.clone()));
258        }
259        if selector.len() == 1 {
260            return Err(HsmError::UriParse("object or id required".into()));
261        }
262        let guard = self
263            .session
264            .lock()
265            .map_err(|_| HsmError::SigningFailure("session mutex poisoned".into()))?;
266        let session = guard
267            .as_ref()
268            .ok_or_else(|| HsmError::SigningFailure("HSM session unavailable".into()))?;
269        let objects = session.find_objects(&selector)?;
270        if objects.len() != 1 {
271            return Err(HsmError::KeyNotFound(
272                "URI must match exactly one private key".into(),
273            ));
274        }
275        let attributes = session.get_attributes(objects[0], &[AttributeType::Label])?;
276        let label = attributes
277            .into_iter()
278            .find_map(|a| {
279                if let Attribute::Label(label) = a {
280                    Some(label)
281                } else {
282                    None
283                }
284            })
285            .ok_or_else(|| HsmError::KeyNotFound("TLS key has no label".into()))?;
286        String::from_utf8(label)
287            .map_err(|_| HsmError::UriParse("TLS key label must be UTF-8".into()))
288    }
289
290    /// Sign the TLS transcript with the selected scheme and wire encoding.
291    pub fn sign_tls(
292        &self,
293        key_label: &str,
294        data: &[u8],
295        scheme: rustls::SignatureScheme,
296    ) -> HsmResult<Vec<u8>> {
297        use cryptoki::mechanism::rsa::{PkcsMgfType, PkcsPssParams};
298        use cryptoki::mechanism::{Mechanism, MechanismType};
299        use cryptoki::object::{Attribute, ObjectClass};
300        use rustls::SignatureScheme as S;
301        let guard = self
302            .session
303            .lock()
304            .map_err(|_| HsmError::SigningFailure("session mutex poisoned".into()))?;
305        let session = guard
306            .as_ref()
307            .ok_or_else(|| HsmError::SigningFailure("HSM not initialized".into()))?;
308        let objects = session.find_objects(&[
309            Attribute::Label(key_label.as_bytes().to_vec()),
310            Attribute::Class(ObjectClass::PRIVATE_KEY),
311        ])?;
312        if objects.len() != 1 {
313            return Err(HsmError::KeyNotFound(
314                "signing key label must match exactly one object".into(),
315            ));
316        }
317        let (md, hash, mgf, len) = match scheme {
318            S::RSA_PSS_SHA256 | S::RSA_PKCS1_SHA256 | S::ECDSA_NISTP256_SHA256 => (
319                openssl::hash::MessageDigest::sha256(),
320                MechanismType::SHA256,
321                PkcsMgfType::MGF1_SHA256,
322                32u64,
323            ),
324            S::RSA_PSS_SHA384 | S::RSA_PKCS1_SHA384 | S::ECDSA_NISTP384_SHA384 => (
325                openssl::hash::MessageDigest::sha384(),
326                MechanismType::SHA384,
327                PkcsMgfType::MGF1_SHA384,
328                48,
329            ),
330            S::RSA_PSS_SHA512 | S::RSA_PKCS1_SHA512 | S::ECDSA_NISTP521_SHA512 => (
331                openssl::hash::MessageDigest::sha512(),
332                MechanismType::SHA512,
333                PkcsMgfType::MGF1_SHA512,
334                64,
335            ),
336            _ => {
337                return Err(HsmError::UnsupportedMechanism(
338                    "unsupported TLS scheme".into(),
339                ));
340            }
341        };
342        let pss = PkcsPssParams {
343            hash_alg: hash,
344            mgf,
345            s_len: cryptoki::types::Ulong::from(len),
346        };
347        let mech = match scheme {
348            S::RSA_PSS_SHA256 => Mechanism::Sha256RsaPkcsPss(pss),
349            S::RSA_PSS_SHA384 => Mechanism::Sha384RsaPkcsPss(pss),
350            S::RSA_PSS_SHA512 => Mechanism::Sha512RsaPkcsPss(pss),
351            S::RSA_PKCS1_SHA256 => Mechanism::Sha256RsaPkcs,
352            S::RSA_PKCS1_SHA384 => Mechanism::Sha384RsaPkcs,
353            S::RSA_PKCS1_SHA512 => Mechanism::Sha512RsaPkcs,
354            _ => Mechanism::Ecdsa,
355        };
356        let ec = matches!(mech, Mechanism::Ecdsa);
357        let digest =
358            openssl::hash::hash(md, data).map_err(|e| HsmError::SigningFailure(e.to_string()))?;
359        let sig = session.sign(&mech, objects[0], if ec { &digest } else { data })?;
360        if !ec {
361            return Ok(sig);
362        }
363        if sig.is_empty() || sig.len() % 2 != 0 {
364            return Err(HsmError::SigningFailure(
365                "invalid raw ECDSA signature".into(),
366            ));
367        }
368        let half = sig.len() / 2;
369        let encode = || -> Result<Vec<u8>, openssl::error::ErrorStack> {
370            openssl::ecdsa::EcdsaSig::from_private_components(
371                openssl::bn::BigNum::from_slice(&sig[..half])?,
372                openssl::bn::BigNum::from_slice(&sig[half..])?,
373            )?
374            .to_der()
375        };
376        encode().map_err(|e| HsmError::SigningFailure(e.to_string()))
377    }
378
379    pub fn sign_data(
380        &self,
381        key_label: &str,
382        data: &[u8],
383        hash_algorithm: &str,
384    ) -> HsmResult<Vec<u8>> {
385        use cryptoki::mechanism::Mechanism;
386        use cryptoki::object::{Attribute, ObjectClass};
387
388        let guard = self
389            .session
390            .lock()
391            .map_err(|_| HsmError::LibraryLoad("HSM session mutex poisoned".into()))?;
392        let session = guard.as_ref().ok_or_else(|| {
393            HsmError::LibraryLoad("HSM session not initialized (placeholder context)".into())
394        })?;
395
396        // Find the private key by label.
397        let template = vec![
398            Attribute::Label(key_label.as_bytes().to_vec()),
399            Attribute::Class(ObjectClass::PRIVATE_KEY),
400        ];
401
402        let objects = session
403            .find_objects(&template)
404            .map_err(|e| HsmError::KeyNotFound(format!("Failed to find key '{key_label}': {e}")))?;
405
406        let private_key = objects.into_iter().next().ok_or_else(|| {
407            HsmError::KeyNotFound(format!("Private key '{key_label}' not found in token"))
408        })?;
409
410        // Select the combined hash+sign mechanism based on algorithm.
411        // These mechanisms hash the data internally then sign.
412        let mechanism = match hash_algorithm {
413            "sha256" => Mechanism::Sha256RsaPkcs,
414            "sha384" => Mechanism::Sha384RsaPkcs,
415            "sha512" => Mechanism::Sha512RsaPkcs,
416            other => {
417                return Err(HsmError::UnsupportedMechanism(format!(
418                    "Unsupported hash algorithm for RSA signing: {other}"
419                )));
420            }
421        };
422
423        // Sign the data.
424        session.sign(&mechanism, private_key, data).map_err(|e| {
425            HsmError::SigningFailure(format!("C_Sign failed for key '{key_label}': {e}"))
426        })
427    }
428}
429
430#[cfg(test)]
431mod tests {
432    use super::*;
433
434    #[test]
435    fn test_module_structure() {
436        // Smoke test to ensure all modules compile and link
437        let _provider = HsmProvider::Kryoptic;
438        let _config = _provider.config();
439        assert!(!_config.library_path.is_empty());
440    }
441
442    #[test]
443    fn test_key_algorithms() {
444        let rsa = KeyAlgorithm::Rsa(2048);
445        let ecdsa = KeyAlgorithm::Ecdsa(EcdsaCurve::P256);
446        let ml_dsa = KeyAlgorithm::MlDsa(MlDsaLevel::L3);
447        let ml_kem = KeyAlgorithm::MlKem(MlKemLevel::L3);
448
449        // Just verify they construct
450        assert!(matches!(rsa, KeyAlgorithm::Rsa(2048)));
451        assert!(matches!(ecdsa, KeyAlgorithm::Ecdsa(_)));
452        assert!(matches!(ml_dsa, KeyAlgorithm::MlDsa(_)));
453        assert!(matches!(ml_kem, KeyAlgorithm::MlKem(_)));
454    }
455}