Skip to main content

kipuka_hsm/
sign.rs

1//! HSM signing operations with PQC support.
2
3use crate::error::{HsmError, HsmResult};
4use crate::key::{HsmKeyPair, KeyAlgorithm, MlDsaLevel, MlKemLevel};
5use cryptoki::mechanism::dsa::{HedgeType, SignAdditionalContext};
6use cryptoki::mechanism::rsa::{PkcsMgfType, PkcsOaepParams, PkcsOaepSource, PkcsPssParams};
7use cryptoki::mechanism::{Mechanism, MechanismType};
8use cryptoki::object::{Attribute, ObjectHandle};
9use cryptoki::session::Session;
10use cryptoki::types::Ulong;
11
12/// HSM signer trait.
13pub trait HsmSigner {
14    /// Sign a message digest.
15    ///
16    /// # Arguments
17    ///
18    /// * `key` - Key pair to sign with
19    /// * `digest` - Pre-computed message digest
20    ///
21    /// # Returns
22    ///
23    /// The signature bytes.
24    fn sign(&self, key: &HsmKeyPair, digest: &[u8]) -> HsmResult<Vec<u8>>;
25
26    /// Sign a message digest with a specific mechanism.
27    fn sign_with_mechanism(
28        &self,
29        key: &HsmKeyPair,
30        digest: &[u8],
31        mechanism: &Mechanism,
32    ) -> HsmResult<Vec<u8>>;
33
34    /// Wrap a key using AES Key Wrap (RFC 3394).
35    ///
36    /// Used for wrapping ML-KEM private keys during /serverkeygen.
37    fn wrap_key_aes(
38        &self,
39        session: &Session,
40        wrapping_key: ObjectHandle,
41        key_to_wrap: ObjectHandle,
42    ) -> HsmResult<Vec<u8>>;
43
44    /// Wrap a key using RSAES-OAEP with SHA-256.
45    fn wrap_key_rsa_oaep(
46        &self,
47        session: &Session,
48        wrapping_key: ObjectHandle,
49        key_to_wrap: ObjectHandle,
50    ) -> HsmResult<Vec<u8>>;
51
52    /// ML-KEM encapsulate operation via PKCS#11 v3.2 C_EncapsulateKey.
53    ///
54    /// # Arguments
55    ///
56    /// * `session` - PKCS#11 session
57    /// * `public_key` - ML-KEM public key
58    ///
59    /// # Returns
60    ///
61    /// `(ciphertext, shared_secret_handle)` — the shared secret is returned
62    /// as a CKK_GENERIC_SECRET object handle inside the token.  The caller
63    /// must extract or use it via PKCS#11 operations.
64    fn ml_kem_encapsulate(
65        &self,
66        session: &Session,
67        public_key: ObjectHandle,
68    ) -> HsmResult<(Vec<u8>, Vec<u8>)>;
69
70    /// ML-KEM decapsulate operation via PKCS#11 v3.2 C_DecapsulateKey.
71    ///
72    /// # Arguments
73    ///
74    /// * `session` - PKCS#11 session
75    /// * `private_key` - ML-KEM private key
76    /// * `ciphertext` - Ciphertext from encapsulate
77    ///
78    /// # Returns
79    ///
80    /// The shared secret bytes (extracted from the derived key object).
81    fn ml_kem_decapsulate(
82        &self,
83        session: &Session,
84        private_key: ObjectHandle,
85        ciphertext: &[u8],
86    ) -> HsmResult<Vec<u8>>;
87}
88
89/// Default HSM signer implementation.
90pub struct DefaultHsmSigner;
91
92impl HsmSigner for DefaultHsmSigner {
93    fn sign(&self, key: &HsmKeyPair, digest: &[u8]) -> HsmResult<Vec<u8>> {
94        let mechanism = match key.algorithm() {
95            KeyAlgorithm::Rsa(_bits) => {
96                // Default to PKCS#1 v1.5 with SHA-256
97                Mechanism::Sha256RsaPkcs
98            }
99            KeyAlgorithm::Ecdsa(_curve) => {
100                // ECDSA with pre-computed digest
101                Mechanism::Ecdsa
102            }
103            KeyAlgorithm::MlDsa(_level) => {
104                // ML-DSA requires explicit mechanism ID
105                return Err(HsmError::PqcNotSupported(
106                    "ML-DSA signing requires explicit mechanism ID".to_string(),
107                ));
108            }
109            KeyAlgorithm::MlKem(_level) => {
110                return Err(HsmError::UnsupportedMechanism(
111                    "ML-KEM is for encapsulation, not signing".to_string(),
112                ));
113            }
114        };
115
116        self.sign_with_mechanism(key, digest, &mechanism)
117    }
118
119    fn sign_with_mechanism(
120        &self,
121        key: &HsmKeyPair,
122        digest: &[u8],
123        mechanism: &Mechanism,
124    ) -> HsmResult<Vec<u8>> {
125        let session = key.session();
126        let private_key = key.private_key();
127
128        session
129            .sign(mechanism, private_key, digest)
130            .map_err(|e| HsmError::SigningFailure(format!("Sign operation failed: {e}")))
131    }
132
133    fn wrap_key_aes(
134        &self,
135        session: &Session,
136        wrapping_key: ObjectHandle,
137        key_to_wrap: ObjectHandle,
138    ) -> HsmResult<Vec<u8>> {
139        let mechanism = Mechanism::AesKeyWrap;
140
141        session
142            .wrap_key(&mechanism, wrapping_key, key_to_wrap)
143            .map_err(|e| HsmError::KeyWrap(format!("AES key wrap failed: {e}")))
144    }
145
146    fn wrap_key_rsa_oaep(
147        &self,
148        session: &Session,
149        wrapping_key: ObjectHandle,
150        key_to_wrap: ObjectHandle,
151    ) -> HsmResult<Vec<u8>> {
152        // RSA-OAEP with SHA-256 and MGF1-SHA-256 (PKCS#11 v3.2 standard parameters)
153        let oaep_params = PkcsOaepParams::new(
154            MechanismType::SHA256,
155            PkcsMgfType::MGF1_SHA256,
156            PkcsOaepSource::empty(),
157        );
158        let mechanism = Mechanism::RsaPkcsOaep(oaep_params);
159
160        session
161            .wrap_key(&mechanism, wrapping_key, key_to_wrap)
162            .map_err(|e| HsmError::KeyWrap(format!("RSA-OAEP key wrap failed: {e}")))
163    }
164
165    fn ml_kem_encapsulate(
166        &self,
167        session: &Session,
168        public_key: ObjectHandle,
169    ) -> HsmResult<(Vec<u8>, Vec<u8>)> {
170        // Use the standard PKCS#11 v3.2 CKM_ML_KEM mechanism for encapsulation.
171        // C_EncapsulateKey produces a ciphertext and derives a CKK_GENERIC_SECRET
172        // key object.  We request an extractable key so that we can read the
173        // shared secret bytes back to the caller.
174        let mechanism = Mechanism::MlKem;
175
176        // Template for the derived shared-secret key object
177        let template = vec![
178            Attribute::Token(false),      // session object, not persistent
179            Attribute::Extractable(true), // allow CKA_VALUE extraction
180            Attribute::Sensitive(false),  // needed for extraction
181        ];
182
183        let (ciphertext, secret_handle) = session
184            .encapsulate_key(&mechanism, public_key, &template)
185            .map_err(|e| HsmError::PqcNotSupported(format!("ML-KEM encapsulate failed: {e}")))?;
186
187        // Extract the shared secret value from the derived key object.
188        // The returned handle is a CKK_GENERIC_SECRET; read its CKA_VALUE.
189        let attrs = match session
190            .get_attributes(secret_handle, &[cryptoki::object::AttributeType::Value])
191        {
192            Ok(a) => a,
193            Err(e) => {
194                if let Err(de) = session.destroy_object(secret_handle) {
195                    tracing::warn!(error = %de, "failed to destroy temporary KEM shared secret");
196                }
197                return Err(HsmError::PqcNotSupported(format!(
198                    "ML-KEM: failed to extract shared secret: {e}"
199                )));
200            }
201        };
202
203        let shared_secret = match attrs.into_iter().find_map(|a| {
204            if let Attribute::Value(v) = a {
205                Some(v)
206            } else {
207                None
208            }
209        }) {
210            Some(v) => v,
211            None => {
212                if let Err(de) = session.destroy_object(secret_handle) {
213                    tracing::warn!(error = %de, "failed to destroy temporary KEM shared secret");
214                }
215                return Err(HsmError::PqcNotSupported(
216                    "ML-KEM: derived key has no CKA_VALUE attribute".to_string(),
217                ));
218            }
219        };
220
221        // Destroy the temporary session object
222        if let Err(e) = session.destroy_object(secret_handle) {
223            tracing::warn!(error = %e, "failed to destroy temporary KEM shared secret");
224        }
225
226        Ok((ciphertext, shared_secret))
227    }
228
229    fn ml_kem_decapsulate(
230        &self,
231        session: &Session,
232        private_key: ObjectHandle,
233        ciphertext: &[u8],
234    ) -> HsmResult<Vec<u8>> {
235        // Use the standard PKCS#11 v3.2 CKM_ML_KEM mechanism for decapsulation.
236        // C_DecapsulateKey takes the ciphertext and private key, and derives
237        // a CKK_GENERIC_SECRET key object containing the shared secret.
238        let mechanism = Mechanism::MlKem;
239
240        // Template for the derived shared-secret key object
241        let template = vec![
242            Attribute::Token(false),
243            Attribute::Extractable(true),
244            Attribute::Sensitive(false),
245        ];
246
247        let secret_handle = session
248            .decapsulate_key(&mechanism, private_key, &template, ciphertext)
249            .map_err(|e| HsmError::PqcNotSupported(format!("ML-KEM decapsulate failed: {e}")))?;
250
251        // Extract the shared secret value
252        let attrs = match session
253            .get_attributes(secret_handle, &[cryptoki::object::AttributeType::Value])
254        {
255            Ok(a) => a,
256            Err(e) => {
257                if let Err(de) = session.destroy_object(secret_handle) {
258                    tracing::warn!(error = %de, "failed to destroy temporary KEM shared secret");
259                }
260                return Err(HsmError::PqcNotSupported(format!(
261                    "ML-KEM: failed to extract shared secret: {e}"
262                )));
263            }
264        };
265
266        let shared_secret = match attrs.into_iter().find_map(|a| {
267            if let Attribute::Value(v) = a {
268                Some(v)
269            } else {
270                None
271            }
272        }) {
273            Some(v) => v,
274            None => {
275                if let Err(de) = session.destroy_object(secret_handle) {
276                    tracing::warn!(error = %de, "failed to destroy temporary KEM shared secret");
277                }
278                return Err(HsmError::PqcNotSupported(
279                    "ML-KEM: derived key has no CKA_VALUE attribute".to_string(),
280                ));
281            }
282        };
283
284        // Destroy the temporary session object
285        if let Err(e) = session.destroy_object(secret_handle) {
286            tracing::warn!(error = %e, "failed to destroy temporary KEM shared secret");
287        }
288
289        Ok(shared_secret)
290    }
291}
292
293/// Sign with RSA PKCS#1 v1.5.
294pub fn sign_rsa_pkcs1(
295    key: &HsmKeyPair,
296    digest: &[u8],
297    hash_algorithm: RsaHashAlgorithm,
298) -> HsmResult<Vec<u8>> {
299    let mechanism = match hash_algorithm {
300        RsaHashAlgorithm::Sha256 => Mechanism::Sha256RsaPkcs,
301        RsaHashAlgorithm::Sha384 => Mechanism::Sha384RsaPkcs,
302        RsaHashAlgorithm::Sha512 => Mechanism::Sha512RsaPkcs,
303    };
304
305    let signer = DefaultHsmSigner;
306    signer.sign_with_mechanism(key, digest, &mechanism)
307}
308
309/// Sign with RSA-PSS.
310///
311/// Uses the standard PKCS#11 CKM_SHA*_RSA_PKCS_PSS mechanism which
312/// performs hashing and PSS padding internally.  The `digest` parameter
313/// is the raw TBS (to-be-signed) data — the HSM hashes it.
314///
315/// The salt length is set equal to the hash output length, matching the
316/// CA/B Forum Baseline Requirements recommendation.
317pub fn sign_rsa_pss(
318    key: &HsmKeyPair,
319    digest: &[u8],
320    hash_algorithm: RsaHashAlgorithm,
321) -> HsmResult<Vec<u8>> {
322    let (hash_mech, mgf, salt_len) = match hash_algorithm {
323        RsaHashAlgorithm::Sha256 => (MechanismType::SHA256, PkcsMgfType::MGF1_SHA256, 32u64),
324        RsaHashAlgorithm::Sha384 => (MechanismType::SHA384, PkcsMgfType::MGF1_SHA384, 48u64),
325        RsaHashAlgorithm::Sha512 => (MechanismType::SHA512, PkcsMgfType::MGF1_SHA512, 64u64),
326    };
327
328    let pss_params = PkcsPssParams {
329        hash_alg: hash_mech,
330        mgf,
331        s_len: Ulong::from(salt_len),
332    };
333
334    let mechanism = match hash_algorithm {
335        RsaHashAlgorithm::Sha256 => Mechanism::Sha256RsaPkcsPss(pss_params),
336        RsaHashAlgorithm::Sha384 => Mechanism::Sha384RsaPkcsPss(pss_params),
337        RsaHashAlgorithm::Sha512 => Mechanism::Sha512RsaPkcsPss(pss_params),
338    };
339
340    let signer = DefaultHsmSigner;
341    signer.sign_with_mechanism(key, digest, &mechanism)
342}
343
344/// Sign with ECDSA.
345pub fn sign_ecdsa(key: &HsmKeyPair, digest: &[u8]) -> HsmResult<Vec<u8>> {
346    let mechanism = Mechanism::Ecdsa;
347    let signer = DefaultHsmSigner;
348    signer.sign_with_mechanism(key, digest, &mechanism)
349}
350
351/// Sign with ML-DSA (FIPS 204) using the standard PKCS#11 v3.2 CKM_ML_DSA mechanism.
352///
353/// ML-DSA signing internally hashes the message before signing.
354/// The HSM performs both hashing and signing.  The standard
355/// `Mechanism::MlDsa` variant from cryptoki 0.12+ is used directly.
356pub fn sign_ml_dsa(key: &HsmKeyPair, message: &[u8], _level: MlDsaLevel) -> HsmResult<Vec<u8>> {
357    // Use the standard PKCS#11 v3.2 CKM_ML_DSA mechanism with default
358    // hedging (HedgeType::Preferred — the token may create either a
359    // hedged or deterministic signature).
360    let ctx = SignAdditionalContext::new(HedgeType::Preferred, None);
361    let mechanism = Mechanism::MlDsa(ctx);
362
363    let signer = DefaultHsmSigner;
364    signer.sign_with_mechanism(key, message, &mechanism)
365}
366
367/// RSA hash algorithms.
368#[derive(Debug, Clone, Copy, PartialEq, Eq)]
369pub enum RsaHashAlgorithm {
370    Sha256,
371    Sha384,
372    Sha512,
373}
374
375/// Software fallback for PQC operations.
376///
377/// When HSM does not support ML-DSA or ML-KEM, fall back to synta-certificate
378/// software implementations.
379pub struct SoftwarePqcFallback;
380
381impl SoftwarePqcFallback {
382    /// Check if HSM supports the required PQC mechanism.
383    pub fn is_hsm_supported(
384        mechanism_type: MechanismType,
385        provider_mechanisms: &[MechanismType],
386    ) -> bool {
387        provider_mechanisms.contains(&mechanism_type)
388    }
389
390    /// Sign with ML-DSA using software implementation via synta-certificate.
391    ///
392    /// Falls back to `synta_certificate::BackendPrivateKey::generate_ml_dsa()`
393    /// when the HSM does not support ML-DSA natively.
394    pub fn sign_ml_dsa_software(
395        _message: &[u8],
396        _private_key_bytes: &[u8],
397        _level: MlDsaLevel,
398    ) -> HsmResult<Vec<u8>> {
399        // synta-certificate ML-DSA software signing:
400        //
401        // In a real deployment, the caller would:
402        // 1. Deserialize the private key bytes into a BackendPrivateKey
403        // 2. Call BackendPrivateKey::sign_ml_dsa(message, level)
404        // 3. Return the signature bytes
405        //
406        // This requires synta-certificate to be compiled with the `pqc` feature.
407        // For now we return a clear error indicating the integration point.
408        Err(HsmError::PqcNotSupported(
409            "Software ML-DSA fallback requires synta-certificate 'pqc' feature. \
410             Use BackendPrivateKey::generate_ml_dsa() to create and sign."
411                .to_string(),
412        ))
413    }
414
415    /// ML-KEM encapsulate using software implementation via synta-certificate.
416    ///
417    /// Falls back to `synta_certificate::BackendPrivateKey::generate_ml_kem()`
418    /// when the HSM does not support ML-KEM natively.
419    pub fn ml_kem_encapsulate_software(
420        _public_key_bytes: &[u8],
421        _level: MlKemLevel,
422    ) -> HsmResult<(Vec<u8>, Vec<u8>)> {
423        // synta-certificate ML-KEM software encapsulation:
424        //
425        // In a real deployment, the caller would:
426        // 1. Deserialize the public key bytes
427        // 2. Call ml_kem_encapsulate(public_key, level)
428        // 3. Return (ciphertext, shared_secret)
429        //
430        // This requires synta-certificate to be compiled with the `pqc` feature.
431        Err(HsmError::PqcNotSupported(
432            "Software ML-KEM encapsulate fallback requires synta-certificate 'pqc' feature. \
433             Use BackendPrivateKey::generate_ml_kem() to create keys, then encapsulate."
434                .to_string(),
435        ))
436    }
437
438    /// ML-KEM decapsulate using software implementation via synta-certificate.
439    ///
440    /// Falls back to `synta_certificate::BackendPrivateKey::generate_ml_kem()`
441    /// when the HSM does not support ML-KEM natively.
442    pub fn ml_kem_decapsulate_software(
443        _private_key_bytes: &[u8],
444        _ciphertext: &[u8],
445        _level: MlKemLevel,
446    ) -> HsmResult<Vec<u8>> {
447        // synta-certificate ML-KEM software decapsulation:
448        //
449        // In a real deployment, the caller would:
450        // 1. Deserialize the private key bytes
451        // 2. Call ml_kem_decapsulate(private_key, ciphertext, level)
452        // 3. Return shared_secret
453        //
454        // This requires synta-certificate to be compiled with the `pqc` feature.
455        Err(HsmError::PqcNotSupported(
456            "Software ML-KEM decapsulate fallback requires synta-certificate 'pqc' feature. \
457             Use BackendPrivateKey::generate_ml_kem() to create keys, then decapsulate."
458                .to_string(),
459        ))
460    }
461}
462
463#[cfg(test)]
464mod tests {
465    use super::*;
466
467    #[test]
468    fn test_software_fallback_detection() {
469        let mechanisms = vec![
470            MechanismType::RSA_PKCS,
471            MechanismType::ECDSA,
472            MechanismType::AES_KEY_WRAP,
473        ];
474
475        assert!(SoftwarePqcFallback::is_hsm_supported(
476            MechanismType::RSA_PKCS,
477            &mechanisms
478        ));
479        assert!(!SoftwarePqcFallback::is_hsm_supported(
480            MechanismType::ECDSA_SHA256,
481            &mechanisms
482        ));
483    }
484
485    #[test]
486    fn test_pqc_mechanism_detection() {
487        // Verify we can check for standard PKCS#11 v3.2 PQC mechanisms
488        let mechanisms = vec![
489            MechanismType::RSA_PKCS,
490            MechanismType::ML_DSA_KEY_PAIR_GEN,
491            MechanismType::ML_DSA,
492            MechanismType::ML_KEM_KEY_PAIR_GEN,
493            MechanismType::ML_KEM,
494        ];
495
496        assert!(SoftwarePqcFallback::is_hsm_supported(
497            MechanismType::ML_DSA,
498            &mechanisms
499        ));
500        assert!(SoftwarePqcFallback::is_hsm_supported(
501            MechanismType::ML_KEM,
502            &mechanisms
503        ));
504        assert!(!SoftwarePqcFallback::is_hsm_supported(
505            MechanismType::HASH_ML_DSA_SHA256,
506            &mechanisms
507        ));
508    }
509
510    #[test]
511    fn test_software_fallback_errors_are_descriptive() {
512        let err = SoftwarePqcFallback::sign_ml_dsa_software(&[], &[], MlDsaLevel::L3);
513        assert!(err.is_err());
514        let msg = err.unwrap_err().to_string();
515        assert!(msg.contains("synta-certificate"));
516
517        let err = SoftwarePqcFallback::ml_kem_encapsulate_software(&[], MlKemLevel::L3);
518        assert!(err.is_err());
519        let msg = err.unwrap_err().to_string();
520        assert!(msg.contains("synta-certificate"));
521
522        let err = SoftwarePqcFallback::ml_kem_decapsulate_software(&[], &[], MlKemLevel::L3);
523        assert!(err.is_err());
524        let msg = err.unwrap_err().to_string();
525        assert!(msg.contains("synta-certificate"));
526    }
527}