Skip to main content

aws_lc_rs/pqdsa/
key_pair.rs

1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0 OR ISC
3
4use crate::aws_lc::{
5    EVP_PKEY_CTX_pqdsa_set_params, EVP_PKEY_pqdsa_new_raw_private_key, EVP_PKEY, EVP_PKEY_PQDSA,
6};
7use crate::encoding::{AsDer, AsRawBytes, Pkcs8V1Der, PqdsaPrivateKeyRaw};
8use crate::error::{KeyRejected, Unspecified};
9use crate::evp_pkey::No_EVP_PKEY_CTX_consumer;
10use crate::pkcs8;
11use crate::pkcs8::{Document, Version};
12use crate::pqdsa::signature::{PqdsaSigningAlgorithm, PublicKey};
13use crate::pqdsa::validate_pqdsa_evp_key;
14use crate::ptr::LcPtr;
15use crate::signature::KeyPair;
16use core::fmt::{Debug, Formatter};
17use std::ffi::c_int;
18
19/// A PQDSA (Post-Quantum Digital Signature Algorithm) key pair, used for signing and verification.
20#[derive(Clone)]
21#[allow(clippy::module_name_repetitions)]
22pub struct PqdsaKeyPair {
23    algorithm: &'static PqdsaSigningAlgorithm,
24    evp_pkey: LcPtr<EVP_PKEY>,
25    pubkey: PublicKey,
26}
27
28#[allow(clippy::missing_fields_in_debug)]
29impl Debug for PqdsaKeyPair {
30    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
31        f.debug_struct("PqdsaKeyPair")
32            .field("algorithm", &self.algorithm)
33            .finish()
34    }
35}
36
37impl KeyPair for PqdsaKeyPair {
38    type PublicKey = PublicKey;
39
40    fn public_key(&self) -> &Self::PublicKey {
41        &self.pubkey
42    }
43}
44
45/// A PQDSA private key.
46pub struct PqdsaPrivateKey<'a>(pub(crate) &'a PqdsaKeyPair);
47
48impl Debug for PqdsaPrivateKey<'_> {
49    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
50        f.write_str(&format!("PqdsaPrivateKey({:?})", self.0.algorithm.0.id))
51    }
52}
53
54impl AsDer<Pkcs8V1Der<'static>> for PqdsaPrivateKey<'_> {
55    /// Serializes the key to PKCS#8 v1 DER.
56    ///
57    /// See [`PqdsaKeyPair::to_pkcs8v1`] for the chosen representation of the private key.
58    ///
59    /// # Errors
60    /// Returns `Unspecified` if serialization fails.
61    fn as_der(&self) -> Result<Pkcs8V1Der<'static>, Unspecified> {
62        Ok(Pkcs8V1Der::new(
63            self.0
64                .evp_pkey
65                .as_const()
66                .marshal_rfc5208_private_key(pkcs8::Version::V1)?,
67        ))
68    }
69}
70
71impl AsRawBytes<PqdsaPrivateKeyRaw<'static>> for PqdsaPrivateKey<'_> {
72    /// Serializes the expanded raw private key.
73    ///
74    /// This can be passed back to [`PqdsaKeyPair::from_raw_private_key`].
75    fn as_raw_bytes(&self) -> Result<PqdsaPrivateKeyRaw<'static>, Unspecified> {
76        Ok(PqdsaPrivateKeyRaw::new(
77            self.0.evp_pkey.as_const().marshal_raw_private_key()?,
78        ))
79    }
80}
81
82impl PqdsaKeyPair {
83    /// Generates a new PQDSA key pair for the specified algorithm.
84    ///
85    /// # Errors
86    /// Returns `Unspecified` if the key generation fails.
87    //
88    // # FIPS
89    // Approved for all supported algorithms: ML-DSA-44, ML-DSA-65, ML-DSA-87.
90    pub fn generate(algorithm: &'static PqdsaSigningAlgorithm) -> Result<Self, Unspecified> {
91        let evp_pkey = evp_key_pqdsa_generate(algorithm.0.id.nid())?;
92        let pubkey = PublicKey::from_private_evp_pkey(&evp_pkey)?;
93        Ok(Self {
94            algorithm,
95            evp_pkey,
96            pubkey,
97        })
98    }
99
100    /// Constructs a key pair from the parsing of PKCS#8.
101    ///
102    /// This accepts either the seed or expanded private key encodings. If both are present in the
103    /// input, they are validated to agree with each other.
104    ///
105    /// # Errors
106    /// Returns `Unspecified` if the key is not valid for the specified signing algorithm.
107    pub fn from_pkcs8(
108        algorithm: &'static PqdsaSigningAlgorithm,
109        pkcs8: &[u8],
110    ) -> Result<Self, KeyRejected> {
111        let evp_pkey = LcPtr::<EVP_PKEY>::parse_rfc5208_private_key(pkcs8, EVP_PKEY_PQDSA)?;
112        validate_pqdsa_evp_key(&evp_pkey, algorithm.0.id)?;
113        let pubkey = PublicKey::from_private_evp_pkey(&evp_pkey)?;
114        Ok(Self {
115            algorithm,
116            evp_pkey,
117            pubkey,
118        })
119    }
120
121    /// Constructs a key pair from raw private key bytes.
122    ///
123    /// This expects the expanded form of the raw private key bytes.
124    ///
125    /// # Errors
126    /// Returns `Unspecified` if the key is not valid for the specified signing algorithm.
127    pub fn from_raw_private_key(
128        algorithm: &'static PqdsaSigningAlgorithm,
129        raw_private_key: &[u8],
130    ) -> Result<Self, KeyRejected> {
131        let evp_pkey = LcPtr::<EVP_PKEY>::parse_raw_private_key(raw_private_key, EVP_PKEY_PQDSA)?;
132        validate_pqdsa_evp_key(&evp_pkey, algorithm.0.id)?;
133        let pubkey = PublicKey::from_private_evp_pkey(&evp_pkey)?;
134        Ok(Self {
135            algorithm,
136            evp_pkey,
137            pubkey,
138        })
139    }
140
141    /// Constructs a key pair deterministically from a 32-byte seed.
142    ///
143    /// Per FIPS 204, the same seed always produces the same key pair. This enables
144    /// reproducible key generation for testing, ACVP validation, and interoperability
145    /// with implementations that store seeds rather than expanded private keys.
146    ///
147    /// `algorithm` is the [`PqdsaSigningAlgorithm`] to be associated with the key pair.
148    ///
149    /// `seed` is the 32-byte seed from which the key pair is deterministically derived.
150    /// All ML-DSA variants (ML-DSA-44, ML-DSA-65, ML-DSA-87) use 32-byte seeds.
151    ///
152    /// # Security Considerations
153    ///
154    /// The seed is the root secret. Compromise of the seed is equivalent to compromise
155    /// of the private key. Callers are responsible for generating seeds from a
156    /// cryptographically secure random source and protecting them accordingly.
157    ///
158    /// The seed should be produced from random entropy such as through [`crate::rand::fill`].
159    /// However, for users requiring FIPS, the seed must be produced from
160    /// [`Self::generate`]. The [`Self::to_pkcs8v1`] method serializes the private key in seed form.
161    /// AWS-LC keeps the seed in the internal representation when possible, but if [`PqdsaKeyPair`]
162    /// is constructed from the expanded form (via [`Self::from_raw_private_key`]) the seed cannot
163    /// be obtained and [`Self::to_pkcs8v1`] will fail.
164    ///
165    /// This method expands the seed into the full private key internally. The expanded private key
166    /// can be retrieved via [`Self::private_key`] and serialized via
167    /// [`PqdsaPrivateKey::as_raw_bytes`].
168    ///
169    /// # Errors
170    ///
171    /// Returns `KeyRejected::too_small()` if `seed.len() < 32`.
172    ///
173    /// Returns `KeyRejected::too_large()` if `seed.len() > 32`.
174    ///
175    /// Returns `KeyRejected::unspecified()` if the underlying cryptographic operation fails.
176    pub fn from_seed(
177        algorithm: &'static PqdsaSigningAlgorithm,
178        seed: &[u8],
179    ) -> Result<Self, KeyRejected> {
180        let expected_seed_len = algorithm.0.id.seed_size_bytes();
181        match seed.len().cmp(&expected_seed_len) {
182            core::cmp::Ordering::Less => return Err(KeyRejected::too_small()),
183            core::cmp::Ordering::Greater => return Err(KeyRejected::too_large()),
184            core::cmp::Ordering::Equal => {}
185        }
186        let nid = algorithm.0.id.nid();
187        let evp_pkey = LcPtr::new(unsafe {
188            EVP_PKEY_pqdsa_new_raw_private_key(nid, seed.as_ptr(), seed.len())
189        })
190        .map_err(|()| KeyRejected::unspecified())?;
191        validate_pqdsa_evp_key(&evp_pkey, algorithm.0.id)?;
192        let pubkey =
193            PublicKey::from_private_evp_pkey(&evp_pkey).map_err(|_| KeyRejected::unspecified())?;
194        Ok(Self {
195            algorithm,
196            evp_pkey,
197            pubkey,
198        })
199    }
200
201    /// Serializes the private key to PKCS#8 v1 DER.
202    ///
203    /// This currently serializes the seed. If the seed is not available (for example when this
204    /// [`PqdsaKeyPair`] was constructed from the expanded private key via
205    /// [`Self::from_raw_private_key`]), serialization fails and this currently returns an error. A
206    /// future implementation may encode the expanded form of the key instead.
207    ///
208    /// # Errors
209    /// Returns `Unspecified` if serialization fails.
210    pub fn to_pkcs8v1(&self) -> Result<Document, Unspecified> {
211        Ok(Document::new(
212            self.evp_pkey
213                .as_const()
214                .marshal_rfc5208_private_key(Version::V1)?,
215        ))
216    }
217
218    /// Deprecated alias for [`Self::to_pkcs8v1`].
219    ///
220    /// This method predates the stabilization of the ML-DSA API and is retained for
221    /// consumers of the `unstable` feature; it will be removed in a future release.
222    ///
223    /// # Errors
224    /// Returns `Unspecified` if serialization fails.
225    #[cfg(feature = "unstable")]
226    #[deprecated(since = "1.18.0", note = "use `PqdsaKeyPair::to_pkcs8v1`")]
227    pub fn to_pkcs8(&self) -> Result<Document, Unspecified> {
228        self.to_pkcs8v1()
229    }
230
231    /// Uses this key to sign the message provided. The signature is written to the `signature`
232    /// slice provided, which must be at least [`PqdsaSigningAlgorithm::signature_len`] bytes
233    /// long. It returns the length of the signature on success.
234    ///
235    /// # Errors
236    /// Returns `Unspecified` if `signature` is too small or if signing fails.
237    //
238    // # FIPS
239    // Approved for all supported algorithms: ML-DSA-44, ML-DSA-65, ML-DSA-87.
240    pub fn sign(&self, msg: &[u8], signature: &mut [u8]) -> Result<usize, Unspecified> {
241        let sig_length = self.algorithm.signature_len();
242        if signature.len() < sig_length {
243            return Err(Unspecified);
244        }
245        let sig_bytes = self.evp_pkey.sign(msg, None, No_EVP_PKEY_CTX_consumer)?;
246        signature[0..sig_length].copy_from_slice(&sig_bytes);
247        Ok(sig_length)
248    }
249
250    /// Returns the signing algorithm associated with this key pair.
251    #[must_use]
252    pub fn algorithm(&self) -> &'static PqdsaSigningAlgorithm {
253        self.algorithm
254    }
255
256    /// Returns the private key associated with this key pair.
257    #[must_use]
258    pub fn private_key(&self) -> PqdsaPrivateKey<'_> {
259        PqdsaPrivateKey(self)
260    }
261}
262
263unsafe impl Send for PqdsaKeyPair {}
264
265unsafe impl Sync for PqdsaKeyPair {}
266
267pub(crate) fn evp_key_pqdsa_generate(nid: c_int) -> Result<LcPtr<EVP_PKEY>, Unspecified> {
268    let params_fn = |ctx| {
269        if 1 == unsafe { EVP_PKEY_CTX_pqdsa_set_params(ctx, nid) } {
270            Ok(())
271        } else {
272            Err(())
273        }
274    };
275    LcPtr::<EVP_PKEY>::generate(EVP_PKEY_PQDSA, Some(params_fn))
276}
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281
282    use crate::signature::{
283        UnparsedPublicKey, ML_DSA_44_SIGNING, ML_DSA_65_SIGNING, ML_DSA_87_SIGNING,
284    };
285
286    const TEST_ALGORITHMS: &[&PqdsaSigningAlgorithm] =
287        &[&ML_DSA_44_SIGNING, &ML_DSA_65_SIGNING, &ML_DSA_87_SIGNING];
288
289    #[test]
290    fn test_public_key_serialization() {
291        for &alg in TEST_ALGORITHMS {
292            // Generate a new key pair
293            let keypair = PqdsaKeyPair::generate(alg).unwrap();
294            let message = b"Test message";
295            let different_message = b"Different message";
296            let mut signature = vec![0; alg.signature_len()];
297            assert!(keypair
298                .sign(message, &mut signature[0..(alg.signature_len() - 1)])
299                .is_err());
300            let sig_len = keypair.sign(message, &mut signature).unwrap();
301            assert_eq!(sig_len, alg.signature_len());
302            let invalid_signature = vec![0u8; alg.signature_len()];
303
304            let original_public_key = keypair.public_key();
305
306            let x509_der = original_public_key.as_der().unwrap();
307            let x509_public_key = UnparsedPublicKey::new(alg.0, x509_der.as_ref());
308            assert!(x509_public_key.verify(message, signature.as_ref()).is_ok());
309            assert!(x509_public_key
310                .verify(different_message, signature.as_ref())
311                .is_err());
312            assert!(x509_public_key.verify(message, &invalid_signature).is_err());
313
314            let raw = original_public_key.as_ref();
315            let raw_public_key = UnparsedPublicKey::new(alg.0, raw);
316            assert!(raw_public_key.verify(message, signature.as_ref()).is_ok());
317            assert!(raw_public_key
318                .verify(different_message, signature.as_ref())
319                .is_err());
320            assert!(raw_public_key
321                .verify(different_message, &invalid_signature)
322                .is_err());
323
324            #[cfg(feature = "ring-sig-verify")]
325            #[allow(deprecated)]
326            {
327                use crate::signature::VerificationAlgorithm;
328                assert!(alg
329                    .0
330                    .verify(
331                        raw.into(),
332                        message.as_ref().into(),
333                        signature.as_slice().into()
334                    )
335                    .is_ok());
336            }
337        }
338    }
339
340    #[test]
341    fn test_private_key_serialization() {
342        for &alg in TEST_ALGORITHMS {
343            // Generate a new key pair
344            let keypair = PqdsaKeyPair::generate(alg).unwrap();
345            let message = b"Test message";
346            let mut original_signature = vec![0; alg.signature_len()];
347            let sig_len = keypair.sign(message, &mut original_signature).unwrap();
348            assert_eq!(sig_len, alg.signature_len());
349
350            let public_key = keypair.public_key();
351            let unparsed_public_key = UnparsedPublicKey::new(alg.0, public_key.as_ref());
352            unparsed_public_key
353                .verify(message, original_signature.as_ref())
354                .unwrap();
355
356            let pkcs8_1 = keypair.to_pkcs8v1().unwrap();
357            let pkcs8_2 = keypair.private_key().as_der().unwrap();
358            let raw = keypair.private_key().as_raw_bytes().unwrap();
359
360            assert_eq!(pkcs8_1.as_ref(), pkcs8_2.as_ref());
361
362            let pkcs8_keypair = PqdsaKeyPair::from_pkcs8(alg, pkcs8_1.as_ref()).unwrap();
363            let raw_keypair = PqdsaKeyPair::from_raw_private_key(alg, raw.as_ref()).unwrap();
364
365            assert_eq!(pkcs8_keypair.evp_pkey, raw_keypair.evp_pkey);
366        }
367    }
368
369    #[test]
370    fn test_from_seed() {
371        for &alg in TEST_ALGORITHMS {
372            let seed = [1u8; 32];
373            let kp = PqdsaKeyPair::from_seed(alg, &seed).unwrap();
374            assert_eq!(kp.algorithm(), alg);
375            // Verify key works for signing
376            let msg = b"seed test";
377            let mut sig = vec![0u8; alg.signature_len()];
378            let sig_len = kp.sign(msg, &mut sig).unwrap();
379            assert_eq!(sig_len, alg.signature_len());
380            let public_key = UnparsedPublicKey::new(alg.0, kp.public_key().as_ref());
381            public_key.verify(msg, &sig).unwrap();
382        }
383    }
384
385    #[test]
386    fn test_from_seed_deterministic() {
387        for &alg in TEST_ALGORITHMS {
388            let seed = [42u8; 32];
389            let kp1 = PqdsaKeyPair::from_seed(alg, &seed).unwrap();
390            let kp2 = PqdsaKeyPair::from_seed(alg, &seed).unwrap();
391            assert_eq!(kp1.public_key().as_ref(), kp2.public_key().as_ref());
392        }
393    }
394
395    #[test]
396    fn test_from_seed_wrong_size() {
397        use crate::error::KeyRejected;
398        for &alg in TEST_ALGORITHMS {
399            assert_eq!(
400                PqdsaKeyPair::from_seed(alg, &[0u8; 31]).err(),
401                Some(KeyRejected::too_small())
402            );
403            assert_eq!(
404                PqdsaKeyPair::from_seed(alg, &[0u8; 33]).err(),
405                Some(KeyRejected::too_large())
406            );
407            assert_eq!(
408                PqdsaKeyPair::from_seed(alg, &[]).err(),
409                Some(KeyRejected::too_small())
410            );
411        }
412    }
413
414    #[test]
415    fn test_from_seed_different_seeds_different_keys() {
416        for &alg in TEST_ALGORITHMS {
417            let kp1 = PqdsaKeyPair::from_seed(alg, &[1u8; 32]).unwrap();
418            let kp2 = PqdsaKeyPair::from_seed(alg, &[2u8; 32]).unwrap();
419            assert_ne!(kp1.public_key().as_ref(), kp2.public_key().as_ref());
420        }
421    }
422
423    #[test]
424    fn test_from_seed_raw_private_key_roundtrip() {
425        use crate::encoding::AsRawBytes;
426        for &alg in TEST_ALGORITHMS {
427            let seed = [55u8; 32];
428            let kp = PqdsaKeyPair::from_seed(alg, &seed).unwrap();
429            let raw_bytes = kp.private_key().as_raw_bytes().unwrap();
430            let kp2 = PqdsaKeyPair::from_raw_private_key(alg, raw_bytes.as_ref()).unwrap();
431            assert_eq!(kp.public_key().as_ref(), kp2.public_key().as_ref());
432        }
433    }
434
435    #[test]
436    fn test_from_seed_pkcs8_roundtrip() {
437        for &alg in TEST_ALGORITHMS {
438            let seed = [77u8; 32];
439            let kp = PqdsaKeyPair::from_seed(alg, &seed).unwrap();
440            let pkcs8 = kp.to_pkcs8v1().unwrap();
441            let kp2 = PqdsaKeyPair::from_pkcs8(alg, pkcs8.as_ref()).unwrap();
442            assert_eq!(kp.public_key().as_ref(), kp2.public_key().as_ref());
443        }
444    }
445
446    #[test]
447    fn test_from_seed_same_seed_different_algorithms() {
448        // Same seed with different algorithms should produce different keys
449        let seed = [42u8; 32];
450        let kp_44 = PqdsaKeyPair::from_seed(&ML_DSA_44_SIGNING, &seed).unwrap();
451        let kp_65 = PqdsaKeyPair::from_seed(&ML_DSA_65_SIGNING, &seed).unwrap();
452        let kp_87 = PqdsaKeyPair::from_seed(&ML_DSA_87_SIGNING, &seed).unwrap();
453        // Public keys have different sizes across algorithms, so they must differ
454        assert_ne!(
455            kp_44.public_key().as_ref().len(),
456            kp_65.public_key().as_ref().len()
457        );
458        assert_ne!(
459            kp_65.public_key().as_ref().len(),
460            kp_87.public_key().as_ref().len()
461        );
462    }
463
464    // `from_raw_private_key` documents that it expects the *expanded* form of the
465    // raw private key bytes, not the 32-byte seed. Feeding it a seed-sized input
466    // must be rejected rather than silently misinterpreted.
467    #[test]
468    fn test_from_raw_private_key_rejects_seed() {
469        for &alg in TEST_ALGORITHMS {
470            // A 32-byte seed is far smaller than any expanded private key, so it
471            // must not be accepted as an expanded key.
472            assert!(PqdsaKeyPair::from_raw_private_key(alg, &[7u8; 32]).is_err());
473        }
474    }
475
476    // `to_pkcs8v1` documents that it prefers serializing just the seed. When a key
477    // pair is created from a seed, the PKCS#8 output must therefore be the compact
478    // seed encoding, which is dramatically smaller than the expanded private key.
479    #[test]
480    fn test_from_seed_pkcs8_is_seed_form() {
481        for &alg in TEST_ALGORITHMS {
482            let kp = PqdsaKeyPair::from_seed(alg, &[3u8; 32]).unwrap();
483            let pkcs8 = kp.to_pkcs8v1().unwrap();
484            let raw_expanded = kp.private_key().as_raw_bytes().unwrap();
485            // The seed-form PKCS#8 wraps only the 32-byte seed (plus ASN.1
486            // framing), so it is far smaller than the expanded private key.
487            assert!(
488                pkcs8.as_ref().len() < raw_expanded.as_ref().len(),
489                "seed-form pkcs8 ({}) should be smaller than expanded key ({})",
490                pkcs8.as_ref().len(),
491                raw_expanded.as_ref().len(),
492            );
493            assert!(
494                pkcs8.as_ref().len() < 128,
495                "seed-form pkcs8 should be compact, got {}",
496                pkcs8.as_ref().len(),
497            );
498        }
499    }
500
501    // `from_seed` documents that if a `PqdsaKeyPair` is constructed from the
502    // expanded form the seed cannot be obtained. AWS-LC's PKCS#8 encoding only
503    // emits the seed, so a key pair with no seed available cannot be serialized to
504    // PKCS#8 at all -- `to_pkcs8v1` returns an error rather than falling back to the
505    // expanded encoding.
506    #[test]
507    fn test_expanded_key_pkcs8_unavailable() {
508        for &alg in TEST_ALGORITHMS {
509            // Round-trip through the raw (expanded) encoding to drop the seed.
510            let seed_kp = PqdsaKeyPair::from_seed(alg, &[4u8; 32]).unwrap();
511            let raw = seed_kp.private_key().as_raw_bytes().unwrap();
512            let expanded_kp = PqdsaKeyPair::from_raw_private_key(alg, raw.as_ref()).unwrap();
513
514            // The expanded-form key pair still signs and exposes the expanded raw
515            // bytes...
516            assert!(expanded_kp.private_key().as_raw_bytes().is_ok());
517            // ...but the seed is gone, so PKCS#8 serialization is unavailable.
518            assert!(
519                expanded_kp.to_pkcs8v1().is_err(),
520                "expanded-only key unexpectedly serialized to PKCS#8",
521            );
522        }
523    }
524
525    // `from_pkcs8` documents that it accepts the seed encoding, and `to_pkcs8v1`
526    // documents that it prefers the seed. A seed -> PKCS#8 -> key pair -> PKCS#8
527    // round-trip must therefore preserve the seed form byte-for-byte.
528    #[test]
529    fn test_from_seed_pkcs8_roundtrip_preserves_seed_form() {
530        for &alg in TEST_ALGORITHMS {
531            let kp = PqdsaKeyPair::from_seed(alg, &[8u8; 32]).unwrap();
532            let pkcs8 = kp.to_pkcs8v1().unwrap();
533            let reparsed = PqdsaKeyPair::from_pkcs8(alg, pkcs8.as_ref()).unwrap();
534            let pkcs8_again = reparsed.to_pkcs8v1().unwrap();
535            // Seed is retained through parsing, so re-serialization is identical.
536            assert_eq!(pkcs8.as_ref(), pkcs8_again.as_ref());
537            // And the reconstructed key pair is the same key.
538            assert_eq!(kp.public_key().as_ref(), reparsed.public_key().as_ref());
539        }
540    }
541
542    // `PqdsaPrivateKey::as_der` documents that it uses the representation chosen by
543    // `to_pkcs8v1`. For a seed-derived key that representation is the seed form, so
544    // `as_der` and `to_pkcs8v1` must agree.
545    #[test]
546    fn test_from_seed_as_der_matches_to_pkcs8v1() {
547        for &alg in TEST_ALGORITHMS {
548            let kp = PqdsaKeyPair::from_seed(alg, &[11u8; 32]).unwrap();
549            let pkcs8 = kp.to_pkcs8v1().unwrap();
550            let der = kp.private_key().as_der().unwrap();
551            assert_eq!(pkcs8.as_ref(), der.as_ref());
552        }
553    }
554
555    // Additional test for the algorithm getter
556    #[test]
557    fn test_algorithm_getter() {
558        for &alg in TEST_ALGORITHMS {
559            let keypair = PqdsaKeyPair::generate(alg).unwrap();
560            assert_eq!(keypair.algorithm(), alg);
561        }
562    }
563
564    #[test]
565    fn test_algorithm_lengths() {
566        for (alg, signature_len, public_key_len) in [
567            (&ML_DSA_44_SIGNING, 2420, 1312),
568            (&ML_DSA_65_SIGNING, 3309, 1952),
569            (&ML_DSA_87_SIGNING, 4627, 2592),
570        ] {
571            assert_eq!(alg.signature_len(), signature_len);
572            assert_eq!(alg.public_key_len(), public_key_len);
573            assert_eq!(alg.seed_len(), 32);
574        }
575    }
576
577    // The deprecated `to_pkcs8` alias is retained for consumers of the `unstable`
578    // feature; it must produce the same encoding as `to_pkcs8v1`.
579    #[cfg(feature = "unstable")]
580    #[allow(deprecated)]
581    #[test]
582    fn test_to_pkcs8_deprecated_alias() {
583        for &alg in TEST_ALGORITHMS {
584            let kp = PqdsaKeyPair::from_seed(alg, &[9u8; 32]).unwrap();
585            assert_eq!(
586                kp.to_pkcs8().unwrap().as_ref(),
587                kp.to_pkcs8v1().unwrap().as_ref()
588            );
589        }
590    }
591
592    #[test]
593    fn test_debug() {
594        for &alg in TEST_ALGORITHMS {
595            let keypair = PqdsaKeyPair::generate(alg).unwrap();
596            assert!(
597                format!("{keypair:?}").starts_with("PqdsaKeyPair { algorithm: PqdsaSigningAlgorithm(PqdsaVerificationAlgorithm { id:"),
598                "{keypair:?}"
599            );
600            let pubkey = keypair.public_key();
601            assert!(
602                format!("{pubkey:?}").starts_with("PqdsaPublicKey("),
603                "{pubkey:?}"
604            );
605            let privkey = keypair.private_key();
606            assert!(
607                format!("{privkey:?}").starts_with("PqdsaPrivateKey("),
608                "{privkey:?}"
609            );
610        }
611    }
612}