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