Skip to main content

kipuka_hsm/
key.rs

1//! HSM key pair generation and management with PQC support.
2
3use crate::error::{HsmError, HsmResult};
4use crate::providers::HsmProviderConfig;
5use crate::slot::HsmSlot;
6use cryptoki::mechanism::{Mechanism, MechanismType};
7use cryptoki::object::{
8    Attribute, KeyType, MlDsaParameterSetType, MlKemParameterSetType, ObjectClass, ObjectHandle,
9};
10use cryptoki::session::Session;
11use cryptoki::types::Ulong;
12use std::collections::HashMap;
13use url::Url;
14
15/// Key algorithm.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum KeyAlgorithm {
18    /// RSA with specified bit length.
19    Rsa(u32),
20    /// ECDSA with named curve.
21    Ecdsa(EcdsaCurve),
22    /// ML-DSA (FIPS 204) with security level.
23    MlDsa(MlDsaLevel),
24    /// ML-KEM (FIPS 203) with security level.
25    MlKem(MlKemLevel),
26}
27
28/// ECDSA curves.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum EcdsaCurve {
31    P256,
32    P384,
33    P521,
34}
35
36impl EcdsaCurve {
37    /// Get the OID for this curve.
38    pub fn oid(&self) -> &[u8] {
39        match self {
40            Self::P256 => &[0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07],
41            Self::P384 => &[0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22],
42            Self::P521 => &[0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x23],
43        }
44    }
45}
46
47/// ML-DSA security levels (FIPS 204).
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum MlDsaLevel {
50    /// ML-DSA-44 (Category 2, ~128-bit security).
51    L2,
52    /// ML-DSA-65 (Category 3, ~192-bit security).
53    L3,
54    /// ML-DSA-87 (Category 5, ~256-bit security).
55    L5,
56}
57
58/// ML-KEM security levels (FIPS 203).
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum MlKemLevel {
61    /// ML-KEM-512 (Category 1, ~128-bit security).
62    L1,
63    /// ML-KEM-768 (Category 3, ~192-bit security).
64    L3,
65    /// ML-KEM-1024 (Category 5, ~256-bit security).
66    L5,
67}
68
69/// HSM key pair reference.
70pub struct HsmKeyPair {
71    /// PKCS#11 session.
72    session: Session,
73    /// Private key handle.
74    private_key: ObjectHandle,
75    /// Public key handle.
76    public_key: ObjectHandle,
77    /// Key algorithm.
78    algorithm: KeyAlgorithm,
79}
80
81impl HsmKeyPair {
82    /// Generate a new key pair.
83    ///
84    /// # Arguments
85    ///
86    /// * `slot` - HSM slot
87    /// * `algorithm` - Key algorithm
88    /// * `label` - Key label (CKA_LABEL)
89    /// * `id` - Key ID (CKA_ID), typically SHA-1 hash of public key
90    /// * `provider_config` - Provider configuration
91    ///
92    /// # NIAP CA PP Compliance
93    ///
94    /// Generated keys MUST have:
95    /// - `CKA_EXTRACTABLE = false` (FCS_CKM.1)
96    /// - `CKA_SENSITIVE = true` (FCS_CKM.1)
97    ///
98    /// # Errors
99    ///
100    /// Returns `HsmError::PqcNotSupported` if the HSM does not support the requested
101    /// PQC algorithm and fallback to software is not enabled.
102    pub fn generate(
103        slot: &HsmSlot,
104        algorithm: KeyAlgorithm,
105        label: &str,
106        id: &[u8],
107        provider_config: &HsmProviderConfig,
108    ) -> HsmResult<Self> {
109        let session = slot.open_rw_session()?;
110
111        let (public_key, private_key) = match algorithm {
112            KeyAlgorithm::Rsa(bits) => {
113                Self::generate_rsa(&session, bits, label, id, provider_config)?
114            }
115            KeyAlgorithm::Ecdsa(curve) => {
116                Self::generate_ecdsa(&session, curve, label, id, provider_config)?
117            }
118            KeyAlgorithm::MlDsa(level) => {
119                Self::generate_ml_dsa(&session, level, label, id, provider_config)?
120            }
121            KeyAlgorithm::MlKem(level) => {
122                Self::generate_ml_kem(&session, level, label, id, provider_config)?
123            }
124        };
125
126        Ok(Self {
127            session,
128            private_key,
129            public_key,
130            algorithm,
131        })
132    }
133
134    /// Generate RSA key pair.
135    fn generate_rsa(
136        session: &Session,
137        bits: u32,
138        label: &str,
139        id: &[u8],
140        config: &HsmProviderConfig,
141    ) -> HsmResult<(ObjectHandle, ObjectHandle)> {
142        if !config
143            .supported_mechanisms
144            .contains(&MechanismType::RSA_PKCS_KEY_PAIR_GEN)
145        {
146            return Err(HsmError::UnsupportedMechanism(
147                "RSA key generation not supported by HSM".to_string(),
148            ));
149        }
150
151        let mechanism = Mechanism::RsaPkcsKeyPairGen;
152
153        let public_key_template = vec![
154            Attribute::Token(true),
155            Attribute::Label(label.as_bytes().to_vec()),
156            Attribute::Id(id.to_vec()),
157            Attribute::Encrypt(true),
158            Attribute::Verify(true),
159            Attribute::ModulusBits(Ulong::from(bits as u64)),
160            Attribute::PublicExponent(vec![0x01, 0x00, 0x01]), // 65537
161        ];
162
163        let private_key_template = vec![
164            Attribute::Token(true),
165            Attribute::Label(label.as_bytes().to_vec()),
166            Attribute::Id(id.to_vec()),
167            Attribute::Private(true),
168            Attribute::Sensitive(true),    // NIAP CA PP FCS_CKM.1
169            Attribute::Extractable(false), // NIAP CA PP FCS_CKM.1
170            Attribute::Decrypt(true),
171            Attribute::Sign(true),
172        ];
173
174        session
175            .generate_key_pair(&mechanism, &public_key_template, &private_key_template)
176            .map_err(|e| HsmError::KeyGeneration(format!("RSA key generation failed: {e}")))
177    }
178
179    /// Generate ECDSA key pair.
180    fn generate_ecdsa(
181        session: &Session,
182        curve: EcdsaCurve,
183        label: &str,
184        id: &[u8],
185        config: &HsmProviderConfig,
186    ) -> HsmResult<(ObjectHandle, ObjectHandle)> {
187        if !config
188            .supported_mechanisms
189            .contains(&MechanismType::ECC_KEY_PAIR_GEN)
190        {
191            return Err(HsmError::UnsupportedMechanism(
192                "ECDSA key generation not supported by HSM".to_string(),
193            ));
194        }
195
196        let mechanism = Mechanism::EccKeyPairGen;
197
198        let public_key_template = vec![
199            Attribute::Token(true),
200            Attribute::Label(label.as_bytes().to_vec()),
201            Attribute::Id(id.to_vec()),
202            Attribute::Verify(true),
203            Attribute::EcParams(curve.oid().to_vec()),
204        ];
205
206        let private_key_template = vec![
207            Attribute::Token(true),
208            Attribute::Label(label.as_bytes().to_vec()),
209            Attribute::Id(id.to_vec()),
210            Attribute::Private(true),
211            Attribute::Sensitive(true),    // NIAP CA PP FCS_CKM.1
212            Attribute::Extractable(false), // NIAP CA PP FCS_CKM.1
213            Attribute::Sign(true),
214        ];
215
216        session
217            .generate_key_pair(&mechanism, &public_key_template, &private_key_template)
218            .map_err(|e| HsmError::KeyGeneration(format!("ECDSA key generation failed: {e}")))
219    }
220
221    /// Generate ML-DSA key pair (FIPS 204) using PKCS#11 v3.2 CKM_ML_DSA_KEY_PAIR_GEN.
222    ///
223    /// The parameter set (ML-DSA-44, ML-DSA-65, ML-DSA-87) is specified via
224    /// the CKA_PARAMETER_SET attribute in the key templates.
225    fn generate_ml_dsa(
226        session: &Session,
227        level: MlDsaLevel,
228        label: &str,
229        id: &[u8],
230        config: &HsmProviderConfig,
231    ) -> HsmResult<(ObjectHandle, ObjectHandle)> {
232        // Check if HSM supports the standard ML-DSA mechanism
233        if !config
234            .supported_mechanisms
235            .contains(&MechanismType::ML_DSA_KEY_PAIR_GEN)
236        {
237            return Err(HsmError::PqcNotSupported(
238                "ML-DSA key generation (CKM_ML_DSA_KEY_PAIR_GEN) not supported by HSM. \
239                 Consider using SoftwarePqcFallback."
240                    .to_string(),
241            ));
242        }
243
244        let mechanism = Mechanism::MlDsaKeyPairGen;
245
246        // Map our level enum to the standard PKCS#11 v3.2 parameter set type
247        let param_set = match level {
248            MlDsaLevel::L2 => MlDsaParameterSetType::ML_DSA_44,
249            MlDsaLevel::L3 => MlDsaParameterSetType::ML_DSA_65,
250            MlDsaLevel::L5 => MlDsaParameterSetType::ML_DSA_87,
251        };
252
253        let public_key_template = vec![
254            Attribute::Token(true),
255            Attribute::Label(label.as_bytes().to_vec()),
256            Attribute::Id(id.to_vec()),
257            Attribute::Verify(true),
258            Attribute::KeyType(KeyType::ML_DSA),
259            Attribute::ParameterSet(param_set.into()),
260        ];
261
262        let private_key_template = vec![
263            Attribute::Token(true),
264            Attribute::Label(label.as_bytes().to_vec()),
265            Attribute::Id(id.to_vec()),
266            Attribute::Private(true),
267            Attribute::Sensitive(true),    // NIAP CA PP FCS_CKM.1
268            Attribute::Extractable(false), // NIAP CA PP FCS_CKM.1
269            Attribute::Sign(true),
270            Attribute::KeyType(KeyType::ML_DSA),
271            Attribute::ParameterSet(param_set.into()),
272        ];
273
274        tracing::info!("Generating ML-DSA key pair with parameter set {:?}", level);
275
276        session
277            .generate_key_pair(&mechanism, &public_key_template, &private_key_template)
278            .map_err(|e| HsmError::KeyGeneration(format!("ML-DSA key generation failed: {e}")))
279    }
280
281    /// Generate ML-KEM key pair (FIPS 203) using PKCS#11 v3.2 CKM_ML_KEM_KEY_PAIR_GEN.
282    ///
283    /// The parameter set (ML-KEM-512, ML-KEM-768, ML-KEM-1024) is specified via
284    /// the CKA_PARAMETER_SET attribute in the key templates.
285    fn generate_ml_kem(
286        session: &Session,
287        level: MlKemLevel,
288        label: &str,
289        id: &[u8],
290        config: &HsmProviderConfig,
291    ) -> HsmResult<(ObjectHandle, ObjectHandle)> {
292        // Check if HSM supports the standard ML-KEM mechanism
293        if !config
294            .supported_mechanisms
295            .contains(&MechanismType::ML_KEM_KEY_PAIR_GEN)
296        {
297            return Err(HsmError::PqcNotSupported(
298                "ML-KEM key generation (CKM_ML_KEM_KEY_PAIR_GEN) not supported by HSM. \
299                 Consider using SoftwarePqcFallback."
300                    .to_string(),
301            ));
302        }
303
304        let mechanism = Mechanism::MlKemKeyPairGen;
305
306        // Map our level enum to the standard PKCS#11 v3.2 parameter set type
307        let param_set = match level {
308            MlKemLevel::L1 => MlKemParameterSetType::ML_KEM_512,
309            MlKemLevel::L3 => MlKemParameterSetType::ML_KEM_768,
310            MlKemLevel::L5 => MlKemParameterSetType::ML_KEM_1024,
311        };
312
313        let public_key_template = vec![
314            Attribute::Token(true),
315            Attribute::Label(label.as_bytes().to_vec()),
316            Attribute::Id(id.to_vec()),
317            Attribute::KeyType(KeyType::ML_KEM),
318            Attribute::ParameterSet(param_set.into()),
319        ];
320
321        let private_key_template = vec![
322            Attribute::Token(true),
323            Attribute::Label(label.as_bytes().to_vec()),
324            Attribute::Id(id.to_vec()),
325            Attribute::Private(true),
326            Attribute::Sensitive(true),    // NIAP CA PP FCS_CKM.1
327            Attribute::Extractable(false), // NIAP CA PP FCS_CKM.1
328            Attribute::KeyType(KeyType::ML_KEM),
329            Attribute::ParameterSet(param_set.into()),
330        ];
331
332        tracing::info!("Generating ML-KEM key pair with parameter set {:?}", level);
333
334        session
335            .generate_key_pair(&mechanism, &public_key_template, &private_key_template)
336            .map_err(|e| HsmError::KeyGeneration(format!("ML-KEM key generation failed: {e}")))
337    }
338
339    /// Find a key pair by label.
340    pub fn find_by_label(slot: &HsmSlot, label: &str, algorithm: KeyAlgorithm) -> HsmResult<Self> {
341        Self::find_selected(
342            slot,
343            vec![Attribute::Label(label.as_bytes().to_vec())],
344            algorithm,
345        )
346    }
347
348    pub fn find_by_id(slot: &HsmSlot, id: &[u8], algorithm: KeyAlgorithm) -> HsmResult<Self> {
349        Self::find_selected(slot, vec![Attribute::Id(id.to_vec())], algorithm)
350    }
351
352    fn find_selected(
353        slot: &HsmSlot,
354        selector: Vec<Attribute>,
355        algorithm: KeyAlgorithm,
356    ) -> HsmResult<Self> {
357        let session = slot.open_ro_session()?;
358        let mut private = selector.clone();
359        private.push(Attribute::Class(ObjectClass::PRIVATE_KEY));
360        let mut public = selector;
361        public.push(Attribute::Class(ObjectClass::PUBLIC_KEY));
362        let private_keys = session.find_objects(&private)?;
363        let public_keys = session.find_objects(&public)?;
364        if private_keys.len() != 1 || public_keys.len() != 1 {
365            return Err(HsmError::KeyNotFound(
366                "key selector must match exactly one key pair".into(),
367            ));
368        }
369        Ok(Self {
370            session,
371            private_key: private_keys[0],
372            public_key: public_keys[0],
373            algorithm,
374        })
375    }
376
377    /// Parse a PKCS#11 URI and find the corresponding key.
378    ///
379    /// # URI Format
380    ///
381    /// `pkcs11:token=MyToken;object=MyKey;type=private`
382    ///
383    /// Supported attributes:
384    /// - `token` - Token label
385    /// - `object` - Key label (CKA_LABEL)
386    /// - `id` - Key ID (CKA_ID, hex-encoded)
387    /// - `type` - Object type (private, public, cert)
388    pub fn from_uri(slot: &HsmSlot, uri: &str, algorithm: KeyAlgorithm) -> HsmResult<Self> {
389        let params = parse_uri(uri)?;
390        if let Some(token) = params.get("token")
391            && token != slot.token_label()?.as_bytes()
392        {
393            return Err(HsmError::UriParse(
394                "URI token does not match selected slot".into(),
395            ));
396        }
397        if params.get("type").is_some_and(|kind| kind != b"private") {
398            return Err(HsmError::UriParse("a private key URI is required".into()));
399        }
400        let mut selector = Vec::new();
401        if let Some(id) = params.get("id") {
402            selector.push(Attribute::Id(id.clone()));
403        }
404        if let Some(label) = params.get("object") {
405            selector.push(Attribute::Label(label.clone()));
406        }
407        if selector.is_empty() {
408            return Err(HsmError::UriParse("URI must contain id or object".into()));
409        }
410        Self::find_selected(slot, selector, algorithm)
411    }
412
413    /// Get the private key handle.
414    pub fn private_key(&self) -> ObjectHandle {
415        self.private_key
416    }
417
418    /// Get the public key handle.
419    pub fn public_key(&self) -> ObjectHandle {
420        self.public_key
421    }
422
423    /// Get the session.
424    pub fn session(&self) -> &Session {
425        &self.session
426    }
427
428    /// Get the key algorithm.
429    pub fn algorithm(&self) -> KeyAlgorithm {
430        self.algorithm
431    }
432}
433
434#[cfg(test)]
435mod tests {
436    use super::*;
437
438    #[test]
439    fn test_ecdsa_curve_oids() {
440        assert_eq!(EcdsaCurve::P256.oid().len(), 10);
441        assert_eq!(EcdsaCurve::P384.oid().len(), 7);
442        assert_eq!(EcdsaCurve::P521.oid().len(), 7);
443    }
444
445    #[test]
446    fn test_pkcs11_uri_parsing() {
447        let uri = "pkcs11:token=MyToken;object=MyKey;type=private";
448        let url = Url::parse(uri).unwrap();
449        assert_eq!(url.scheme(), "pkcs11");
450    }
451}
452
453/// Parse the supported RFC 7512 selectors without normalizing binary IDs.
454pub fn parse_uri(uri: &str) -> HsmResult<HashMap<String, Vec<u8>>> {
455    let url = Url::parse(uri).map_err(|e| HsmError::UriParse(e.to_string()))?;
456
457    if url.scheme() != "pkcs11" {
458        return Err(HsmError::UriParse(format!(
459            "Invalid scheme '{}', expected 'pkcs11'",
460            url.scheme()
461        )));
462    }
463
464    if url.query().is_some() || url.fragment().is_some() || url.has_host() {
465        return Err(HsmError::UriParse(
466            "PKCS#11 URI query, fragment, and authority are unsupported".into(),
467        ));
468    }
469    // RFC 7512 attributes live in the opaque path, separated by ';'.
470    let mut params: HashMap<String, Vec<u8>> = HashMap::new();
471    for part in url.path().split(';') {
472        let (name, value) = part
473            .split_once('=')
474            .ok_or_else(|| HsmError::UriParse("invalid PKCS#11 attribute".into()))?;
475        if !matches!(name, "token" | "object" | "id" | "type") {
476            return Err(HsmError::UriParse("unsupported PKCS#11 selector".into()));
477        }
478        let raw = value.as_bytes();
479        for (i, b) in raw.iter().enumerate() {
480            if *b == b'%'
481                && (i + 2 >= raw.len()
482                    || !raw[i + 1].is_ascii_hexdigit()
483                    || !raw[i + 2].is_ascii_hexdigit())
484            {
485                return Err(HsmError::UriParse("invalid percent escape".into()));
486            }
487        }
488        let bytes = percent_encoding::percent_decode_str(value).collect::<Vec<_>>();
489        if params.insert(name.to_string(), bytes).is_some() {
490            return Err(HsmError::UriParse("duplicate PKCS#11 attribute".into()));
491        }
492    }
493    Ok(params)
494}
495
496#[cfg(test)]
497mod uri_regressions {
498    use super::*;
499    #[test]
500    fn standard_binary_id_and_percent_encoded_label() {
501        let parsed =
502            parse_uri("pkcs11:token=Synthetic%20token;object=TLS%3Bkey;id=%00%ff;type=private")
503                .unwrap();
504        assert_eq!(parsed["token"], b"Synthetic token");
505        assert_eq!(parsed["object"], b"TLS;key");
506        assert_eq!(parsed["id"], [0, 255]);
507    }
508    #[test]
509    fn ambiguous_or_ignored_selectors_are_rejected() {
510        for uri in [
511            "pkcs11:object=a;object=b",
512            "pkcs11:object=a%2",
513            "pkcs11:object=a;unknown=x",
514            "pkcs11:object=a?pin-value=synthetic",
515        ] {
516            assert!(parse_uri(uri).is_err(), "{uri}");
517        }
518    }
519}