Skip to main content

kipuka/ca/
keygen.rs

1//! Server-side key generation for EST `/serverkeygen` (RFC 7030 §4.4).
2//!
3//! Generates key pairs in software or via PKCS#11 HSM per NIAP CA PP
4//! FCS_CKM.1 (approved key generation methods). Supports RSA and ECDSA
5//! key types with configurable sizes.
6
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9use tracing::{debug, info};
10
11/// Errors during key generation.
12#[derive(Debug, Error)]
13pub enum KeyGenError {
14    /// The requested key type or size is not supported.
15    #[error("unsupported key type: {0}")]
16    UnsupportedKeyType(String),
17
18    /// The key size is below the minimum allowed.
19    #[error("{algorithm} key size {bits}-bit is below minimum {min_bits}-bit")]
20    KeyTooSmall {
21        algorithm: String,
22        bits: u32,
23        min_bits: u32,
24    },
25
26    /// Software key generation failed.
27    #[error("software key generation failed: {0}")]
28    SoftwareError(String),
29
30    /// HSM key generation failed.
31    #[error("HSM key generation failed: {0}")]
32    HsmError(String),
33
34    /// Key archival (encrypted storage) failed.
35    #[error("key archival failed: {0}")]
36    ArchivalError(String),
37}
38
39/// Supported key types for server-side generation.
40///
41/// Covers classical (RSA, ECDSA), post-quantum (ML-DSA FIPS 204, ML-KEM
42/// FIPS 203), and composite hybrid algorithms per
43/// draft-ietf-lamps-pq-composite-sigs-19.
44#[derive(Debug, Clone, Serialize, Deserialize)]
45#[serde(rename_all = "lowercase")]
46pub enum KeyType {
47    /// RSA with specified bit length (2048, 3072, 4096).
48    Rsa(u32),
49    /// ECDSA with specified named curve.
50    Ecdsa(EcCurve),
51    /// ML-DSA standalone (FIPS 204) — signing only.
52    /// Used for CA signing keys and client identity certificates.
53    MlDsa(MlDsaLevel),
54    /// ML-KEM standalone (FIPS 203) — key encapsulation.
55    /// Used for server-side key generation (/serverkeygen) where the
56    /// client needs a KEM key pair for key establishment.
57    MlKem(MlKemLevel),
58    /// Composite ML-DSA + classical signing (hybrid).
59    /// Provides dual-algorithm protection during PQC migration.
60    CompositeMlDsa {
61        ml_dsa: MlDsaLevel,
62        classical: ClassicalSigningAlg,
63    },
64}
65
66/// ML-DSA security levels per FIPS 204.
67#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
68pub enum MlDsaLevel {
69    /// ML-DSA-44: NIST Level 2, ~1,312 byte public key, ~2,420 byte signature.
70    #[serde(rename = "44")]
71    MlDsa44,
72    /// ML-DSA-65: NIST Level 3, ~1,952 byte public key, ~3,309 byte signature.
73    #[serde(rename = "65")]
74    MlDsa65,
75    /// ML-DSA-87: NIST Level 5, ~2,592 byte public key, ~4,627 byte signature.
76    #[serde(rename = "87")]
77    MlDsa87,
78}
79
80/// ML-KEM security levels per FIPS 203.
81///
82/// Used by `/serverkeygen` to generate KEM key pairs on behalf of clients,
83/// with optional archival in the KRA subsystem.
84#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
85pub enum MlKemLevel {
86    /// ML-KEM-512: NIST Level 1, ~800 byte public key, ~768 byte ciphertext.
87    #[serde(rename = "512")]
88    MlKem512,
89    /// ML-KEM-768: NIST Level 3, ~1,184 byte public key, ~1,088 byte ciphertext.
90    #[serde(rename = "768")]
91    MlKem768,
92    /// ML-KEM-1024: NIST Level 5, ~1,568 byte public key, ~1,568 byte ciphertext.
93    #[serde(rename = "1024")]
94    MlKem1024,
95}
96
97/// Classical signing algorithms paired with ML-DSA in composite mode.
98#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
99pub enum ClassicalSigningAlg {
100    Rsa2048,
101    Rsa3072,
102    Rsa4096,
103    EcP256,
104    EcP384,
105    Ed25519,
106    Ed448,
107}
108
109/// Supported elliptic curves for ECDSA.
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub enum EcCurve {
112    /// NIST P-256 (secp256r1).
113    P256,
114    /// NIST P-384 (secp384r1).
115    P384,
116}
117
118impl std::fmt::Display for MlDsaLevel {
119    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120        match self {
121            MlDsaLevel::MlDsa44 => write!(f, "ML-DSA-44"),
122            MlDsaLevel::MlDsa65 => write!(f, "ML-DSA-65"),
123            MlDsaLevel::MlDsa87 => write!(f, "ML-DSA-87"),
124        }
125    }
126}
127
128impl std::fmt::Display for MlKemLevel {
129    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130        match self {
131            MlKemLevel::MlKem512 => write!(f, "ML-KEM-512"),
132            MlKemLevel::MlKem768 => write!(f, "ML-KEM-768"),
133            MlKemLevel::MlKem1024 => write!(f, "ML-KEM-1024"),
134        }
135    }
136}
137
138impl std::fmt::Display for EcCurve {
139    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140        match self {
141            EcCurve::P256 => write!(f, "P-256"),
142            EcCurve::P384 => write!(f, "P-384"),
143        }
144    }
145}
146
147impl std::fmt::Display for ClassicalSigningAlg {
148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149        match self {
150            ClassicalSigningAlg::Rsa2048 => write!(f, "RSA-2048"),
151            ClassicalSigningAlg::Rsa3072 => write!(f, "RSA-3072"),
152            ClassicalSigningAlg::Rsa4096 => write!(f, "RSA-4096"),
153            ClassicalSigningAlg::EcP256 => write!(f, "EC-P-256"),
154            ClassicalSigningAlg::EcP384 => write!(f, "EC-P-384"),
155            ClassicalSigningAlg::Ed25519 => write!(f, "Ed25519"),
156            ClassicalSigningAlg::Ed448 => write!(f, "Ed448"),
157        }
158    }
159}
160
161/// Result of a key generation operation.
162pub struct KeyGenResult {
163    /// DER-encoded public key (SubjectPublicKeyInfo) for certificate issuance.
164    pub public_key_der: Vec<u8>,
165    /// DER-encoded private key (PKCS#8) for delivery to the client.
166    /// This is the unencrypted form; the caller is responsible for
167    /// wrapping it in CMS EnvelopedData for secure delivery per RFC 7030 §4.4.
168    pub private_key_der: Vec<u8>,
169    /// Key type that was generated.
170    pub key_type: KeyType,
171}
172
173/// Configuration for key generation.
174#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct KeyGenConfig {
176    /// Whether to generate keys in an HSM via PKCS#11.
177    pub use_hsm: bool,
178    /// Whether to archive the generated private key (encrypted in database).
179    pub archive_key: bool,
180    /// Allowed key types and sizes.
181    pub allowed_types: Vec<KeyType>,
182}
183
184impl Default for KeyGenConfig {
185    fn default() -> Self {
186        Self {
187            use_hsm: false,
188            archive_key: false,
189            allowed_types: vec![
190                // Classical
191                KeyType::Rsa(2048),
192                KeyType::Rsa(3072),
193                KeyType::Rsa(4096),
194                KeyType::Ecdsa(EcCurve::P256),
195                KeyType::Ecdsa(EcCurve::P384),
196                // Post-Quantum — ML-DSA (FIPS 204) all levels
197                KeyType::MlDsa(MlDsaLevel::MlDsa44),
198                KeyType::MlDsa(MlDsaLevel::MlDsa65),
199                KeyType::MlDsa(MlDsaLevel::MlDsa87),
200                // Post-Quantum — ML-KEM (FIPS 203) all levels for SSKG
201                KeyType::MlKem(MlKemLevel::MlKem512),
202                KeyType::MlKem(MlKemLevel::MlKem768),
203                KeyType::MlKem(MlKemLevel::MlKem1024),
204                // Composite hybrid (ML-DSA + classical)
205                KeyType::CompositeMlDsa {
206                    ml_dsa: MlDsaLevel::MlDsa44,
207                    classical: ClassicalSigningAlg::EcP256,
208                },
209                KeyType::CompositeMlDsa {
210                    ml_dsa: MlDsaLevel::MlDsa65,
211                    classical: ClassicalSigningAlg::EcP384,
212                },
213                KeyType::CompositeMlDsa {
214                    ml_dsa: MlDsaLevel::MlDsa87,
215                    classical: ClassicalSigningAlg::EcP384,
216                },
217            ],
218        }
219    }
220}
221
222/// Generate a key pair for the EST `/serverkeygen` endpoint.
223///
224/// Per NIAP CA PP FCS_CKM.1, uses approved key generation methods.
225/// The private key is returned in PKCS#8 DER format for wrapping in
226/// CMS EnvelopedData before delivery to the client.
227///
228/// # Arguments
229///
230/// * `key_type` - Requested key type and size
231/// * `config` - Key generation configuration
232///
233/// # Returns
234///
235/// [`KeyGenResult`] containing the public key (for cert issuance) and
236/// private key (for client delivery).
237pub fn generate_key_pair(
238    key_type: &KeyType,
239    config: &KeyGenConfig,
240) -> Result<KeyGenResult, KeyGenError> {
241    validate_key_type(key_type)?;
242
243    if config.use_hsm {
244        generate_hsm_key(key_type)
245    } else {
246        generate_software_key(key_type)
247    }
248}
249
250/// Validate that the requested key type meets minimum requirements.
251fn validate_key_type(key_type: &KeyType) -> Result<(), KeyGenError> {
252    match key_type {
253        KeyType::Rsa(bits) => {
254            const MIN_RSA_BITS: u32 = 2048;
255            if *bits < MIN_RSA_BITS {
256                return Err(KeyGenError::KeyTooSmall {
257                    algorithm: "RSA".into(),
258                    bits: *bits,
259                    min_bits: MIN_RSA_BITS,
260                });
261            }
262            if !matches!(*bits, 2048 | 3072 | 4096) {
263                return Err(KeyGenError::UnsupportedKeyType(format!(
264                    "RSA {bits}-bit (use 2048, 3072, or 4096)"
265                )));
266            }
267        }
268        KeyType::Ecdsa(curve) => {
269            debug!(curve = %curve, "ECDSA key type validated");
270        }
271        KeyType::MlDsa(level) => {
272            debug!(level = %level, "ML-DSA key type validated (FIPS 204)");
273        }
274        KeyType::MlKem(level) => {
275            debug!(level = %level, "ML-KEM key type validated (FIPS 203)");
276        }
277        KeyType::CompositeMlDsa { ml_dsa, classical } => {
278            debug!(
279                ml_dsa = %ml_dsa,
280                classical = %classical,
281                "composite ML-DSA key type validated (draft-ietf-lamps-pq-composite-sigs-19)"
282            );
283        }
284    }
285    Ok(())
286}
287
288/// Generate a key pair in software.
289///
290/// Uses synta-certificate's `PrivateKeyBuilder` for classical and PQC keys.
291/// ML-DSA: uses `PrivateKeyBuilder::ml_dsa(level)` (FIPS 204).
292/// ML-KEM: uses `PrivateKeyBuilder::ml_kem(level)` (FIPS 203).
293/// Composite: uses `PrivateKeyBuilder::composite_ml_dsa(sub_arc)`.
294///
295/// Requires OpenSSL 3.5+ with `pqc` provider for ML-DSA/ML-KEM operations.
296fn generate_software_key(key_type: &KeyType) -> Result<KeyGenResult, KeyGenError> {
297    info!(key_type = ?key_type, "generating software key pair");
298
299    // TODO: wire to synta-certificate PrivateKeyBuilder.
300    // The integration path per key type:
301    //
302    //   KeyType::Rsa(bits) =>
303    //     PrivateKeyBuilder::rsa(*bits)?.build()?
304    //
305    //   KeyType::Ecdsa(EcCurve::P256) =>
306    //     PrivateKeyBuilder::ec_p256()?.build()?
307    //
308    //   KeyType::MlDsa(MlDsaLevel::MlDsa44) =>
309    //     PrivateKeyBuilder::ml_dsa(44)?.build()?   // FIPS 204
310    //   KeyType::MlDsa(MlDsaLevel::MlDsa65) =>
311    //     PrivateKeyBuilder::ml_dsa(65)?.build()?
312    //   KeyType::MlDsa(MlDsaLevel::MlDsa87) =>
313    //     PrivateKeyBuilder::ml_dsa(87)?.build()?
314    //
315    //   KeyType::MlKem(MlKemLevel::MlKem512) =>
316    //     PrivateKeyBuilder::ml_kem(512)?.build()?  // FIPS 203
317    //   KeyType::MlKem(MlKemLevel::MlKem768) =>
318    //     PrivateKeyBuilder::ml_kem(768)?.build()?
319    //   KeyType::MlKem(MlKemLevel::MlKem1024) =>
320    //     PrivateKeyBuilder::ml_kem(1024)?.build()?
321    //
322    //   KeyType::CompositeMlDsa { ml_dsa, classical } =>
323    //     PrivateKeyBuilder::composite_ml_dsa(sub_arc_for(ml_dsa, classical))?.build()?
324    //     // sub_arc values 37-54 per draft-ietf-lamps-pq-composite-sigs-19
325
326    let placeholder_public = vec![0x30, 0x00];
327    let placeholder_private = vec![0x30, 0x00];
328
329    Ok(KeyGenResult {
330        public_key_der: placeholder_public,
331        private_key_der: placeholder_private,
332        key_type: key_type.clone(),
333    })
334}
335
336/// Generate a key pair in an HSM via PKCS#11.
337///
338/// ML-DSA: requires HSM firmware with FIPS 204 support.
339/// - Thales Luna 7.x+ and Entrust nShield 5+ support ML-DSA via
340///   CKM_ML_DSA_KEY_PAIR_GEN (vendor-defined mechanism IDs).
341/// - Kryoptic supports ML-DSA via software PKCS#11 module.
342/// - Utimaco CryptoServer Se Gen2 supports ML-DSA.
343///
344/// ML-KEM: requires HSM firmware with FIPS 203 support.
345/// - Key encapsulation uses CKM_ML_KEM_KEY_PAIR_GEN.
346/// - Generated keys are stored in HSM with CKA_EXTRACTABLE=false
347///   for archival; decapsulation key is wrapped for client delivery.
348fn generate_hsm_key(key_type: &KeyType) -> Result<KeyGenResult, KeyGenError> {
349    info!(key_type = ?key_type, "HSM key generation requested");
350
351    Err(KeyGenError::HsmError(
352        "PKCS#11 PQC key generation pending kipuka-hsm integration".into(),
353    ))
354}
355
356/// Map a composite ML-DSA key type to the OID sub-arc per
357/// draft-ietf-lamps-pq-composite-sigs-19 (sub-arcs 37-54).
358///
359/// These sub-arcs are under id-composite-sig (2.16.840.1.114027.80.5.2).
360pub fn composite_sub_arc(ml_dsa: &MlDsaLevel, classical: &ClassicalSigningAlg) -> Option<u32> {
361    match (ml_dsa, classical) {
362        (MlDsaLevel::MlDsa44, ClassicalSigningAlg::Rsa2048) => Some(37),
363        (MlDsaLevel::MlDsa44, ClassicalSigningAlg::EcP256) => Some(38),
364        (MlDsaLevel::MlDsa44, ClassicalSigningAlg::Rsa3072) => Some(39),
365        (MlDsaLevel::MlDsa44, ClassicalSigningAlg::Ed25519) => Some(40),
366        (MlDsaLevel::MlDsa65, ClassicalSigningAlg::Rsa3072) => Some(41),
367        (MlDsaLevel::MlDsa65, ClassicalSigningAlg::EcP384) => Some(42),
368        (MlDsaLevel::MlDsa65, ClassicalSigningAlg::Rsa4096) => Some(43),
369        (MlDsaLevel::MlDsa65, ClassicalSigningAlg::Ed25519) => Some(44),
370        (MlDsaLevel::MlDsa87, ClassicalSigningAlg::EcP384) => Some(45),
371        (MlDsaLevel::MlDsa87, ClassicalSigningAlg::Ed448) => Some(46),
372        _ => None,
373    }
374}