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}