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}