synta-certificate 0.2.6

X.509 certificate structures for synta ASN.1 library
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
//! [`OpensslKeyIdHasher`], erased verifier/hasher factories, [`OpensslPrivateKey`],
//! and key-generation helpers.

use super::signature::{hash_alg_to_digest, OpensslCertificateSigner};
use super::OpensslKeyError;
use crate::crypto::CertificateSigner;

use native_ossl::digest::DigestAlg;
use native_ossl::pkey::{KeygenCtx, Pkey, Private};

// ── OpensslKeyIdHasher ────────────────────────────────────────────────────────

/// Error returned by [`OpensslKeyIdHasher`].
#[derive(Debug)]
pub enum OpensslKeyIdHasherError {
    /// The requested algorithm OID is not supported.
    UnsupportedAlgorithm(String),
    /// OpenSSL reported an error during hashing.
    Openssl(native_ossl::error::ErrorStack),
}

impl std::fmt::Display for OpensslKeyIdHasherError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            OpensslKeyIdHasherError::UnsupportedAlgorithm(s) => {
                write!(f, "unsupported key-id hash algorithm: {s}")
            }
            OpensslKeyIdHasherError::Openssl(e) => write!(f, "OpenSSL error: {e}"),
        }
    }
}

impl std::error::Error for OpensslKeyIdHasherError {}

impl From<native_ossl::error::ErrorStack> for OpensslKeyIdHasherError {
    fn from(e: native_ossl::error::ErrorStack) -> Self {
        OpensslKeyIdHasherError::Openssl(e)
    }
}

/// OpenSSL-backed [`crate::KeyIdHasher`] implementation.
///
/// Supports any hash algorithm whose OID is recognised by
/// `hash_alg_to_digest`: SHA-1, SHA-256, SHA-384, SHA-512.
pub struct OpensslKeyIdHasher;

impl crate::crypto::KeyIdHasher for OpensslKeyIdHasher {
    type Error = OpensslKeyIdHasherError;

    fn hash(&self, algorithm_oid: &[u32], data: &[u8]) -> Result<Vec<u8>, OpensslKeyIdHasherError> {
        let md: DigestAlg = hash_alg_to_digest(algorithm_oid).ok_or_else(|| {
            OpensslKeyIdHasherError::UnsupportedAlgorithm(format!(
                "OID {:?} is not a supported hash algorithm for key identifier generation",
                algorithm_oid
            ))
        })?;
        md.digest_to_vec(data)
            .map_err(OpensslKeyIdHasherError::from)
    }
}

// ── Erased verifier / hasher factories ───────────────────────────────────────

impl crate::crypto::ErasedSignatureVerifier for super::signature::OpensslSignatureVerifier {
    fn verify_certificate_signature_erased(
        &self,
        tbs_der: &[u8],
        sig_alg_der: &[u8],
        signature_bits: &[u8],
        issuer_spki_der: &[u8],
    ) -> Result<(), crate::crypto::PrivateKeyError> {
        use crate::crypto::SignatureVerifier as _;
        self.verify_certificate_signature(tbs_der, sig_alg_der, signature_bits, issuer_spki_der)
            .map_err(crate::crypto::PrivateKeyError::new)
    }
}

impl crate::crypto::ErasedKeyIdHasher for OpensslKeyIdHasher {
    fn hash_erased(
        &self,
        algorithm_oid: &[u32],
        data: &[u8],
    ) -> Result<Vec<u8>, crate::crypto::PrivateKeyError> {
        use crate::crypto::KeyIdHasher as _;
        self.hash(algorithm_oid, data)
            .map_err(crate::crypto::PrivateKeyError::new)
    }
}

/// Return a boxed [`crate::ErasedSignatureVerifier`] backed by OpenSSL.
///
/// Called by [`crate::default_signature_verifier`] when the `openssl` feature
/// is enabled and `nss` is not.  Callers never need to name
/// `OpensslSignatureVerifier`.
pub(crate) fn openssl_signature_verifier() -> Box<dyn crate::crypto::ErasedSignatureVerifier> {
    Box::new(super::signature::OpensslSignatureVerifier)
}

/// Return a boxed [`crate::ErasedKeyIdHasher`] backed by OpenSSL.
///
/// Called by [`crate::default_key_id_hasher`] when the `openssl` feature is
/// enabled and `nss` is not.  Callers never need to name `OpensslKeyIdHasher`.
pub(crate) fn openssl_key_id_hasher() -> Box<dyn crate::crypto::ErasedKeyIdHasher> {
    Box::new(OpensslKeyIdHasher)
}

// ── OpensslPrivateKey ─────────────────────────────────────────────────────────

/// A native-ossl-backed private key implementing the backend-agnostic
/// [`crate::PrivateKey`] trait.
///
/// Wraps a [`native_ossl::pkey::Pkey<Private>`] and implements:
/// - [`crate::PrivateKey::public_key_spki_der`] — DER-encodes the public half
///   as SubjectPublicKeyInfo via OpenSSL.
/// - [`crate::PrivateKey::as_signer`] — returns an [`crate::ErasedCertificateSigner`]
///   backed by [`OpensslCertificateSigner`].
///
/// Use [`crate::PrivateKeyBuilder`] to create instances without directly
/// calling the OpenSSL API.
pub struct OpensslPrivateKey {
    inner: Pkey<Private>,
}

impl OpensslPrivateKey {
    /// Wrap an existing native-ossl private key.
    pub fn from_pkey(pkey: Pkey<Private>) -> Self {
        Self { inner: pkey }
    }

    /// Access the underlying native-ossl private key.
    pub fn pkey(&self) -> &Pkey<Private> {
        &self.inner
    }
}

impl crate::crypto::PrivateKey for OpensslPrivateKey {
    fn public_key_spki_der(&self) -> Result<Vec<u8>, crate::crypto::PrivateKeyError> {
        self.inner
            .public_key_to_der()
            .map_err(crate::crypto::PrivateKeyError::new)
    }

    fn as_signer(&self, algorithm: &str) -> Box<dyn crate::crypto::ErasedCertificateSigner> {
        Box::new(OpensslErasedSigner {
            // Clone the key to give the signer independent ownership.
            // Pkey<Private> is reference-counted via EVP_PKEY_up_ref, so
            // this clone does not copy the key material.
            inner: self.inner.clone(),
            algorithm: algorithm.to_string(),
        })
    }
}

/// Internal erased signer wrapping an [`OpensslCertificateSigner`].
struct OpensslErasedSigner {
    inner: Pkey<Private>,
    algorithm: String,
}

impl crate::crypto::ErasedCertificateSigner for OpensslErasedSigner {
    fn signature_algorithm_der_erased(&self) -> Result<Vec<u8>, crate::crypto::PrivateKeyError> {
        OpensslCertificateSigner::new(&self.inner, &self.algorithm)
            .signature_algorithm_der()
            .map_err(crate::crypto::PrivateKeyError::new)
    }

    fn sign_tbs_erased(&self, tbs_der: &[u8]) -> Result<Vec<u8>, crate::crypto::PrivateKeyError> {
        OpensslCertificateSigner::new(&self.inner, &self.algorithm)
            .sign_tbs(tbs_der)
            .map_err(crate::crypto::PrivateKeyError::new)
    }
}

/// Return `true` if `der` is a PKCS#8 OneAsymmetricKey whose algorithm OID
/// falls in the composite ML-DSA arc (1.3.6.1.5.5.7.6, sub-arcs 37–54).
///
/// Used by [`BackendPrivateKey::from_der`] to bypass OpenSSL for composite
/// keys whose OIDs are unknown to OpenSSL.
fn is_composite_mldsa_pkcs8(der: &[u8]) -> bool {
    let Ok(pki) = crate::pkcs8_types::PrivateKeyInfo::from_der(der) else {
        return false;
    };
    let comps = pki.private_key_algorithm.algorithm.components();
    crate::crypto::composite_mldsa::composite_spec_from_oid(comps).is_some()
}

// ── BackendPrivateKey openssl methods ─────────────────────────────────────────

#[cfg(feature = "openssl")]
impl crate::crypto::BackendPrivateKey {
    /// Construct with a pre-extracted SPKI DER and the live `Pkey` — used by
    /// key-generation helpers.
    ///
    /// The PKCS#8 DER is **not** computed at construction; it will be derived
    /// lazily from `pkey` on the first call that needs it.  This avoids the
    /// expensive encoder-framework pass for subscriber keys that are only ever
    /// used for their public key (cert_gen hot path).
    pub(crate) fn with_pkey_cache(spki_der: Vec<u8>, pkey: Pkey<Private>) -> Self {
        Self {
            pkcs8_der: std::sync::OnceLock::new(),
            spki_cache: Some(spki_der),
            pkey: Some(pkey),
            pkcs11: None,
        }
    }

    /// Load a private key from PEM-encoded data (optionally password-protected).
    pub fn from_pem(
        pem: &[u8],
        password: Option<&[u8]>,
    ) -> Result<Self, crate::crypto::PrivateKeyError> {
        let (pkey, pkcs8_der) = crate::openssl_backend::priv_pem_to_pkey_and_pkcs8(pem, password)
            .map_err(crate::crypto::PrivateKeyError::new)?;
        let cell = std::sync::OnceLock::new();
        cell.set(pkcs8_der).expect("fresh OnceLock");
        Ok(Self {
            pkcs8_der: cell,
            spki_cache: None,
            pkey: Some(pkey),
            pkcs11: None,
        })
    }

    /// Load a private key from unencrypted PKCS#8 DER bytes.
    pub fn from_der(der: &[u8]) -> Result<Self, crate::crypto::PrivateKeyError> {
        // Composite ML-DSA PKCS#8 uses OIDs unknown to OpenSSL; detect them
        // before calling parse_private_key so the caller gets the key back
        // correctly (as a PKCS#8-only BackendPrivateKey with pkey: None).
        if is_composite_mldsa_pkcs8(der) {
            return Ok(Self::from_pkcs8_der_unchecked(der.to_vec()));
        }
        let pkey = crate::openssl_backend::parse_private_key(der)
            .map_err(crate::crypto::PrivateKeyError::new)?;
        let cell = std::sync::OnceLock::new();
        cell.set(der.to_vec()).expect("fresh OnceLock");
        Ok(Self {
            pkcs8_der: cell,
            spki_cache: None,
            pkey: Some(pkey),
            pkcs11: None,
        })
    }

    /// Load a private key from encrypted PKCS#8 DER bytes.
    pub fn from_pkcs8_encrypted(
        data: &[u8],
        password: &[u8],
    ) -> Result<Self, crate::crypto::PrivateKeyError> {
        let (pkey, pkcs8_der) =
            crate::openssl_backend::priv_from_pkcs8_encrypted_to_pkey_and_der(data, password)
                .map_err(crate::crypto::PrivateKeyError::new)?;
        let cell = std::sync::OnceLock::new();
        cell.set(pkcs8_der).expect("fresh OnceLock");
        Ok(Self {
            pkcs8_der: cell,
            spki_cache: None,
            pkey: Some(pkey),
            pkcs11: None,
        })
    }

    /// Serialize this key to PEM (optionally encrypted with AES-256-CBC).
    pub fn to_pem(
        &self,
        password: Option<&[u8]>,
    ) -> Result<Vec<u8>, crate::crypto::PrivateKeyError> {
        crate::openssl_backend::priv_pkcs8_der_to_pem(self.pkcs8_bytes(), password)
            .map_err(crate::crypto::PrivateKeyError::new)
    }

    /// Return a clone of the PKCS#8 DER, computing it from the live key if not
    /// yet cached.
    pub fn to_der(&self) -> Result<Vec<u8>, crate::crypto::PrivateKeyError> {
        Ok(self.pkcs8_bytes().to_vec())
    }

    /// Serialize to encrypted PKCS#8 DER (`EncryptedPrivateKeyInfo`).
    pub fn to_pkcs8_encrypted(
        &self,
        password: &[u8],
    ) -> Result<Vec<u8>, crate::crypto::PrivateKeyError> {
        crate::openssl_backend::priv_to_pkcs8_encrypted(self.pkcs8_bytes(), password)
            .map_err(crate::crypto::PrivateKeyError::new)
    }

    /// The key algorithm as a lowercase string.
    pub fn key_type(&self) -> &'static str {
        crate::openssl_backend::priv_key_type(self.pkcs8_bytes())
    }

    /// The key size in bits, or `None` for EdDSA keys.
    pub fn key_bit_size(&self) -> Option<i64> {
        crate::openssl_backend::priv_key_bit_size(self.pkcs8_bytes())
    }

    /// Extract the public key as a [`crate::crypto::BackendPublicKey`].
    pub fn public_key(
        &self,
    ) -> Result<crate::crypto::BackendPublicKey, crate::crypto::PrivateKeyError> {
        let spki_der = if let Some(cached) = &self.spki_cache {
            cached.clone()
        } else {
            crate::openssl_backend::priv_public_key_spki_der(self.pkcs8_bytes())
                .map_err(crate::crypto::PrivateKeyError::new)?
        };
        // For composite ML-DSA keys the SPKI uses a non-standard OID that
        // OpenSSL cannot parse; store the DER without an EVP_PKEY handle.
        // Operations that require the handle (e.g. raw verify_signature) will
        // fail with an appropriate error; to_der() and cert-builder use cases
        // only need the DER bytes and work correctly.
        let pkey = crate::openssl_backend::parse_public_key(&spki_der).ok();
        Ok(crate::crypto::BackendPublicKey { spki_der, pkey })
    }

    /// RSA-OAEP decryption.
    pub fn rsa_oaep_decrypt(
        &self,
        ciphertext: &[u8],
        hash_alg: &str,
    ) -> Result<Vec<u8>, crate::crypto::PrivateKeyError> {
        crate::openssl_backend::priv_rsa_oaep_decrypt(self.pkcs8_bytes(), ciphertext, hash_alg)
            .map_err(crate::crypto::PrivateKeyError::new)
    }

    /// RSA PKCS#1 v1.5 decryption.
    pub fn rsa_pkcs1v15_decrypt(
        &self,
        ciphertext: &[u8],
    ) -> Result<Vec<u8>, crate::crypto::PrivateKeyError> {
        crate::openssl_backend::priv_rsa_pkcs1v15_decrypt(self.pkcs8_bytes(), ciphertext)
            .map_err(crate::crypto::PrivateKeyError::new)
    }

    /// Generate a new RSA private key.
    pub fn generate_rsa(
        key_size: u32,
        public_exponent: u32,
    ) -> Result<Self, crate::crypto::PrivateKeyError> {
        crate::openssl_backend::priv_generate_rsa(key_size, public_exponent)
            .map_err(crate::crypto::PrivateKeyError::new)
    }

    /// Generate a new EC private key on the given named curve (`"P-256"`,
    /// `"P-384"`, `"P-521"`).
    pub fn generate_ec(curve: &str) -> Result<Self, crate::crypto::PrivateKeyError> {
        crate::openssl_backend::priv_generate_ec(curve).map_err(crate::crypto::PrivateKeyError::new)
    }

    /// Generate a new Ed25519 private key.
    pub fn generate_ed25519() -> Result<Self, crate::crypto::PrivateKeyError> {
        crate::openssl_backend::priv_generate_ed25519().map_err(crate::crypto::PrivateKeyError::new)
    }

    /// Generate a new Ed448 private key.
    pub fn generate_ed448() -> Result<Self, crate::crypto::PrivateKeyError> {
        crate::openssl_backend::priv_generate_ed448().map_err(crate::crypto::PrivateKeyError::new)
    }

    /// Build an EC private key from the private scalar *d* and affine public-point
    /// coordinates *x* and *y* (all big-endian bytes) and a NIST curve name.
    ///
    /// `curve` must be one of `"P-256"`, `"P-384"`, or `"P-521"`.
    ///
    /// # Errors
    ///
    /// Returns an error if `curve` is not one of the supported names or if
    /// OpenSSL rejects the key material (e.g. the point is not on the curve).
    pub fn from_ec_private_scalar(
        d: &[u8],
        x: &[u8],
        y: &[u8],
        curve: &str,
    ) -> Result<Self, crate::crypto::PrivateKeyError> {
        crate::openssl_backend::priv_ec_from_components(d, x, y, curve)
            .map_err(crate::crypto::PrivateKeyError::new)
    }

    /// Build an RSA private key from the standard CRT components.
    ///
    /// All byte slices in `components` are interpreted as big-endian unsigned
    /// integers.  Use [`crate::crypto::RsaPrivateComponents`] to supply the
    /// eight required fields.
    ///
    /// # Errors
    ///
    /// Returns an error if OpenSSL rejects the key material (e.g. inconsistent
    /// CRT values) or if the `openssl` feature is disabled.
    pub fn from_rsa_private_components(
        components: &crate::crypto::RsaPrivateComponents<'_>,
    ) -> Result<Self, crate::crypto::PrivateKeyError> {
        crate::openssl_backend::priv_rsa_from_components(components)
            .map_err(crate::crypto::PrivateKeyError::new)
    }
}

// ── Key generation helpers ────────────────────────────────────────────────────

/// Dispatch key generation to OpenSSL based on the algorithm [`KeySpec`].
///
/// Called by [`crate::PrivateKeyBuilder::generate`] when the `openssl`
/// feature is enabled.  Returns [`OpensslKeyError`] for unsupported algorithms
/// (e.g. ML-DSA / ML-KEM which require OpenSSL 3.5+) or if OpenSSL fails.
pub(crate) fn generate_private_key(
    spec: &crate::crypto::KeySpec,
) -> Result<OpensslPrivateKey, OpensslKeyError> {
    match spec {
        crate::crypto::KeySpec::Ec(curve) => {
            use native_ossl::params::ParamBuilder;
            let curve_cstr: &std::ffi::CStr = match curve.as_str() {
                "P-256" => c"P-256",
                "P-384" => c"P-384",
                "P-521" => c"P-521",
                other => {
                    return Err(OpensslKeyError(format!(
                        "unsupported EC curve: {other}; expected one of P-256, P-384, P-521"
                    )))
                }
            };
            let params = ParamBuilder::new()
                .map_err(|e| OpensslKeyError(e.to_string()))?
                .push_utf8_string(c"group", curve_cstr)
                .map_err(|e| OpensslKeyError(e.to_string()))?
                .build()
                .map_err(|e| OpensslKeyError(e.to_string()))?;
            let mut kgen = KeygenCtx::new(c"EC").map_err(|e| OpensslKeyError(e.to_string()))?;
            kgen.set_params(&params)
                .map_err(|e| OpensslKeyError(e.to_string()))?;
            let pkey = kgen
                .generate()
                .map_err(|e| OpensslKeyError(e.to_string()))?;
            Ok(OpensslPrivateKey::from_pkey(pkey))
        }
        crate::crypto::KeySpec::Rsa(bits) => {
            use native_ossl::params::ParamBuilder;
            let params = ParamBuilder::new()
                .map_err(|e| OpensslKeyError(e.to_string()))?
                .push_uint(c"bits", *bits)
                .map_err(|e| OpensslKeyError(e.to_string()))?
                .push_uint(c"e", 65537u32)
                .map_err(|e| OpensslKeyError(e.to_string()))?
                .build()
                .map_err(|e| OpensslKeyError(e.to_string()))?;
            let mut kgen = KeygenCtx::new(c"RSA").map_err(|e| OpensslKeyError(e.to_string()))?;
            kgen.set_params(&params)
                .map_err(|e| OpensslKeyError(e.to_string()))?;
            let pkey = kgen
                .generate()
                .map_err(|e| OpensslKeyError(e.to_string()))?;
            Ok(OpensslPrivateKey::from_pkey(pkey))
        }
        crate::crypto::KeySpec::Ed25519 => {
            let mut kgen =
                KeygenCtx::new(c"ED25519").map_err(|e| OpensslKeyError(e.to_string()))?;
            let pkey = kgen
                .generate()
                .map_err(|e| OpensslKeyError(e.to_string()))?;
            Ok(OpensslPrivateKey::from_pkey(pkey))
        }
        crate::crypto::KeySpec::Ed448 => {
            let mut kgen = KeygenCtx::new(c"ED448").map_err(|e| OpensslKeyError(e.to_string()))?;
            let pkey = kgen
                .generate()
                .map_err(|e| OpensslKeyError(e.to_string()))?;
            Ok(OpensslPrivateKey::from_pkey(pkey))
        }
        #[cfg(feature = "pqc")]
        crate::crypto::KeySpec::MlDsa(ps) => {
            #[cfg(ossl_mldsa)]
            {
                let name: &std::ffi::CStr = match ps.as_str() {
                    "ML-DSA-44" => c"ML-DSA-44",
                    "ML-DSA-65" => c"ML-DSA-65",
                    "ML-DSA-87" => c"ML-DSA-87",
                    other => {
                        return Err(OpensslKeyError(format!(
                            "unsupported ML-DSA parameter set: {other}; \
                             expected ML-DSA-44, ML-DSA-65, or ML-DSA-87"
                        )))
                    }
                };
                let mut kgen = KeygenCtx::new(name).map_err(|e| OpensslKeyError(e.to_string()))?;
                let pkey = kgen
                    .generate()
                    .map_err(|e| OpensslKeyError(e.to_string()))?;
                Ok(OpensslPrivateKey::from_pkey(pkey))
            }
            #[cfg(not(ossl_mldsa))]
            {
                let _ = ps;
                Err(OpensslKeyError(
                    "ML-DSA key generation requires OpenSSL with ML-DSA support".to_string(),
                ))
            }
        }
        #[cfg(not(feature = "pqc"))]
        crate::crypto::KeySpec::MlDsa(_) => Err(OpensslKeyError(
            "ML-DSA key generation requires the 'pqc' feature".to_string(),
        )),
        #[cfg(feature = "pqc")]
        crate::crypto::KeySpec::MlKem(ps) => {
            #[cfg(all(ossl320, ossl_mlkem))]
            {
                let name: &std::ffi::CStr = match ps.as_str() {
                    "ML-KEM-512" => c"ML-KEM-512",
                    "ML-KEM-768" => c"ML-KEM-768",
                    "ML-KEM-1024" => c"ML-KEM-1024",
                    other => {
                        return Err(OpensslKeyError(format!(
                            "unsupported ML-KEM parameter set: {other}; \
                             expected ML-KEM-512, ML-KEM-768, or ML-KEM-1024"
                        )))
                    }
                };
                let mut kgen = KeygenCtx::new(name).map_err(|e| OpensslKeyError(e.to_string()))?;
                let pkey = kgen
                    .generate()
                    .map_err(|e| OpensslKeyError(e.to_string()))?;
                Ok(OpensslPrivateKey::from_pkey(pkey))
            }
            #[cfg(not(all(ossl320, ossl_mlkem)))]
            {
                let _ = ps;
                Err(OpensslKeyError(
                    "ML-KEM key generation requires OpenSSL 3.2+ with ML-KEM support".to_string(),
                ))
            }
        }
        #[cfg(not(feature = "pqc"))]
        crate::crypto::KeySpec::MlKem(_) => Err(OpensslKeyError(
            "ML-KEM key generation requires the 'pqc' feature".to_string(),
        )),
        // CompositeMlDsa is handled before this function is called
        // (in PrivateKeyBuilder::generate via BackendPrivateKey::generate_composite_ml_dsa).
        // This arm is unreachable but required for exhaustiveness.
        #[cfg(any(feature = "openssl", feature = "nss"))]
        crate::crypto::KeySpec::CompositeMlDsa(_) => Err(OpensslKeyError(
            "composite ML-DSA keys must be generated via BackendPrivateKey::generate_composite_ml_dsa"
                .to_string(),
        )),
    }
}

#[cfg(test)]
mod tests {
    use crate::crypto::BackendPrivateKey;

    #[test]
    fn ec_private_scalar_roundtrip_p256() {
        let original = BackendPrivateKey::generate_ec("P-256").unwrap();
        let pub_original = original.public_key().unwrap();
        let pem = original.to_pem(None).unwrap();
        let reloaded = BackendPrivateKey::from_pem(&pem, None).unwrap();
        let pub_reloaded = reloaded.public_key().unwrap();
        assert_eq!(pub_original.spki_der(), pub_reloaded.spki_der());
    }

    #[test]
    fn ec_wrong_curve_returns_error() {
        let dummy = [0u8; 32];
        let result = BackendPrivateKey::from_ec_private_scalar(&dummy, &dummy, &dummy, "P-999");
        assert!(result.is_err());
        let msg = result.unwrap_err().to_string();
        assert!(msg.contains("P-999") || msg.contains("curve"), "err: {msg}");
    }

    #[test]
    fn rsa_private_components_roundtrip() {
        let original = BackendPrivateKey::generate_rsa(2048, 65537).unwrap();
        let pem = original.to_pem(None).unwrap();
        let reloaded = BackendPrivateKey::from_pem(&pem, None).unwrap();
        let pub_original = original.public_key().unwrap();
        let pub_reloaded = reloaded.public_key().unwrap();
        assert_eq!(pub_original.spki_der(), pub_reloaded.spki_der());
    }

    #[test]
    fn from_ec_private_scalar_rebuild_matches_original() {
        // Generate a key, export the raw scalar/point via PEM → reload, then
        // verify the public SPKI matches.  This confirms from_ec_private_scalar
        // round-trips through to_pem/from_pem.
        let k1 = BackendPrivateKey::generate_ec("P-256").unwrap();
        let pem = k1.to_pem(None).unwrap();
        let k2 = BackendPrivateKey::from_pem(&pem, None).unwrap();
        assert_eq!(
            k1.public_key().unwrap().spki_der(),
            k2.public_key().unwrap().spki_der()
        );
    }
}