diff --git a/Cargo.lock b/Cargo.lock
index 8743656..2b7b342 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -55,6 +55,12 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b59d472eab27ade8d770dcb11da7201c11234bef9f82ce7aa517be028d462b"
+[[package]]
+name = "base64ct"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
+
[[package]]
name = "bincode"
version = "1.3.3"
@@ -243,9 +249,9 @@ checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15"
[[package]]
name = "const-oid"
-version = "0.10.0"
+version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1cb3c4a0d3776f7535c32793be81d6d5fec0d48ac70955d9834e643aa249a52f"
+checksum = "0dabb6555f92fb9ee4140454eb5dcd14c7960e1225c6d1a6cc361f032947713e"
[[package]]
name = "cpufeatures"
@@ -401,9 +407,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7050e8041c28720851f7db83183195b6acf375bb7bb28e3b86f0fe6cbd69459d"
dependencies = [
"const-oid",
+ "der_derive",
+ "pem-rfc7468",
"zeroize",
]
+[[package]]
+name = "der_derive"
+version = "0.8.0-rc.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14bfffadecb79dfde429f5dcd7553780c2cea5f7d0e72ad7c37a74f1ef79230a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "dhkem"
version = "0.0.1-alpha"
@@ -854,13 +873,16 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
name = "ml-kem"
version = "0.3.0-pre"
dependencies = [
+ "const-oid",
"criterion",
"crypto-common",
+ "der",
"hex",
"hex-literal",
"hybrid-array",
"kem",
"num-rational",
+ "pkcs8",
"rand",
"rand_core",
"serde",
@@ -973,6 +995,15 @@ dependencies = [
"primeorder",
]
+[[package]]
+name = "pem-rfc7468"
+version = "1.0.0-rc.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8e58fab693c712c0d4e88f8eb3087b6521d060bcaf76aeb20cb192d809115ba"
+dependencies = [
+ "base64ct",
+]
+
[[package]]
name = "pin-project-lite"
version = "0.2.16"
@@ -985,6 +1016,16 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+[[package]]
+name = "pkcs8"
+version = "0.11.0-rc.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c53e5d0804fa4070b1b2a5b320102f2c1c094920a7533d5d87c2630609bcbd34"
+dependencies = [
+ "der",
+ "spki",
+]
+
[[package]]
name = "pkg-config"
version = "0.3.32"
@@ -1440,6 +1481,16 @@ dependencies = [
"lock_api",
]
+[[package]]
+name = "spki"
+version = "0.8.0-rc.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8baeff88f34ed0691978ec34440140e1572b68c7dd4a495fd14a3dc1944daa80"
+dependencies = [
+ "base64ct",
+ "der",
+]
+
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
diff --git a/ml-kem/Cargo.toml b/ml-kem/Cargo.toml
index 08a0e3b..9b8dc9e 100644
--- a/ml-kem/Cargo.toml
+++ b/ml-kem/Cargo.toml
@@ -17,7 +17,10 @@ exclude = ["tests/key-gen.rs", "tests/key-gen.json", "tests/encap-decap.rs", "te
[features]
deterministic = [] # Expose deterministic generation and encapsulation functions
+alloc = ["pkcs8?/alloc"]
zeroize = ["dep:zeroize"]
+pkcs8 = ["dep:const-oid", "dep:pkcs8"]
+pem = ["pkcs8?/pem"]
[dependencies]
kem = "0.3.0-pre.0"
@@ -25,6 +28,9 @@ hybrid-array = { version = "0.4", features = ["extra-sizes"] }
rand_core = "0.9"
sha3 = { version = "0.11.0-rc.0", default-features = false }
zeroize = { version = "1.8.1", optional = true, default-features = false }
+pkcs8 = { version = "0.11.0-rc.4", optional = true, default-features = false }
+const-oid = { version = "0.10.1", optional = true, features = ["db"], default-features = false }
+der = { version = "0.8.0-rc.0", features = ["derive"] }
[dev-dependencies]
criterion = "0.5.1"
diff --git a/ml-kem/src/kem.rs b/ml-kem/src/kem.rs
index 8c8edf7..3551f1f 100644
--- a/ml-kem/src/kem.rs
+++ b/ml-kem/src/kem.rs
@@ -18,6 +18,17 @@ pub use ::kem::{Decapsulate, Encapsulate};
/// A shared key resulting from an ML-KEM transaction
pub(crate) type SharedKey = B32;
+#[cfg(all(feature = "pkcs8", feature = "alloc"))]
+use pkcs8::der::{Encode, asn1::BitStringRef};
+#[cfg(feature = "pkcs8")]
+use {
+ hybrid_array::Array,
+ pkcs8::{
+ der::{AnyRef, asn1::OctetStringRef},
+ spki::AssociatedAlgorithmIdentifier,
+ },
+};
+
/// A `DecapsulationKey` provides the ability to generate a new key pair, and decapsulate an
/// encapsulated shared key.
#[derive(Clone, Debug, PartialEq)]
@@ -253,6 +264,189 @@ where
}
}
+/// The serialization of the private key is a choice between three different formats
+/// [according to PKCS#8](https://lamps-wg.github.io/kyber-certificates/draft-ietf-lamps-kyber-certificates.html#name-private-key-format).
+///
+/// “For ML-KEM private keys, the privateKey field in `OneAsymmetricKey`
+/// contains one of the following DER-encoded `CHOICE` structures.
+/// The seed format is a fixed 64-byte `OCTET STRING` (66 bytes total
+/// with the 0x8040 tag and length) for all security levels,
+/// while the expandedKey and both formats vary in size by security level”
+#[cfg(feature = "pkcs8")]
+#[derive(Clone, Debug, pkcs8::der::Choice)]
+pub(crate) enum PrivateKeyChoice<'o> {
+ /// FIPS 203 format for an ML-KEM private key: a 64-octet seed
+ #[asn1(tag_mode = "IMPLICIT", context_specific = "0")]
+ Seed(OctetStringRef<'o>),
+ /// FIPS 203 format for an ML-KEM private key: the decapsulation key resulting from PKE's `KeyGen` operation
+ Expanded(OctetStringRef<'o>),
+ /// In this setting both key formats are provided in a `PrivateKeyBothChoice` `struct`
+ Both(PrivateKeyBothChoice<'o>),
+}
+
+/// The private key's `Both` variant contains the seed as well as the expanded key.
+#[cfg(feature = "pkcs8")]
+#[derive(Clone, Debug, pkcs8::der::Sequence)]
+pub(crate) struct PrivateKeyBothChoice<'o> {
+ /// FIPS 203 format for an ML-KEM private key: a 64-octet seed
+ pub seed: OctetStringRef<'o>,
+ /// FIPS 203 format for an ML-KEM private key: the decapsulation key resulting from PKE's `KeyGen` operation
+ pub expanded: OctetStringRef<'o>,
+}
+
+#[cfg(feature = "pkcs8")]
+impl
AssociatedAlgorithmIdentifier for EncapsulationKey
+where
+ P: KemParams + AssociatedAlgorithmIdentifier>,
+{
+ type Params = P::Params;
+
+ const ALGORITHM_IDENTIFIER: pkcs8::spki::AlgorithmIdentifier =
+ P::ALGORITHM_IDENTIFIER;
+}
+
+#[cfg(all(feature = "pkcs8", feature = "alloc"))]
+impl pkcs8::EncodePublicKey for EncapsulationKey
+where
+ P: KemParams + AssociatedAlgorithmIdentifier>,
+{
+ /// Serialize the given `EncapsulationKey` into DER format.
+ /// Returns a `Document` which wraps the DER document in case of success.
+ fn to_public_key_der(&self) -> pkcs8::spki::Result {
+ let public_key = self.as_bytes();
+ let subject_public_key = BitStringRef::new(0, &public_key)?;
+
+ pkcs8::SubjectPublicKeyInfo {
+ algorithm: P::ALGORITHM_IDENTIFIER,
+ subject_public_key,
+ }
+ .try_into()
+ }
+}
+
+#[cfg(feature = "pkcs8")]
+impl TryFrom> for EncapsulationKey
+where
+ P: KemParams + AssociatedAlgorithmIdentifier>,
+{
+ type Error = pkcs8::spki::Error;
+
+ /// Deserialize the encapsulation key from DER format found in `spki.subject_public_key`.
+ /// Returns an `EncapsulationKey` containing `ek_{pke}` and `h` in case of success.
+ fn try_from(spki: pkcs8::SubjectPublicKeyInfoRef<'_>) -> Result {
+ if spki.algorithm.oid != P::ALGORITHM_IDENTIFIER.oid {
+ return Err(pkcs8::spki::Error::OidUnknown {
+ oid: P::ALGORITHM_IDENTIFIER.oid,
+ });
+ }
+
+ let bitstring_of_encapsulation_key = spki.subject_public_key;
+ let enc_key = match bitstring_of_encapsulation_key.as_bytes() {
+ Some(bytes) => {
+ let arr: Array> = match bytes.try_into() {
+ Ok(array) => array,
+ Err(_) => return Err(pkcs8::spki::Error::KeyMalformed),
+ };
+ EncryptionKey::from_bytes(&arr)
+ }
+ None => return Err(pkcs8::spki::Error::KeyMalformed),
+ };
+
+ Ok(Self::new(enc_key))
+ }
+}
+
+#[cfg(feature = "pkcs8")]
+impl AssociatedAlgorithmIdentifier for DecapsulationKey
+where
+ P: KemParams + AssociatedAlgorithmIdentifier>,
+{
+ type Params = P::Params;
+
+ const ALGORITHM_IDENTIFIER: pkcs8::spki::AlgorithmIdentifier =
+ P::ALGORITHM_IDENTIFIER;
+}
+
+#[cfg(all(feature = "pkcs8", feature = "alloc"))]
+impl pkcs8::EncodePrivateKey for DecapsulationKey
+where
+ P: KemParams + AssociatedAlgorithmIdentifier>,
+{
+ /// Serialize the given `DecapsulationKey` into DER format.
+ /// Returns a `SecretDocument` which wraps the DER document in case of success.
+ fn to_pkcs8_der(&self) -> pkcs8::Result {
+ let decaps_key_bytes: Array::DecapsulationKeySize> = self.as_bytes();
+
+ // NOTE: “The seed format is RECOMMENDED as it efficiently stores both the private and public key”,
+ // but this is impossible with the definition of the type `DecapsulationKey`; see issue 53.
+ let pk_key_der =
+ PrivateKeyChoice::Expanded(OctetStringRef::new(decaps_key_bytes.as_slice())?)
+ .to_der()?;
+ let pk_key_octetstr: OctetStringRef<'_> = OctetStringRef::new(&pk_key_der)?;
+
+ let private_key_info =
+ pkcs8::PrivateKeyInfoRef::new(P::ALGORITHM_IDENTIFIER, pk_key_octetstr);
+ pkcs8::SecretDocument::encode_msg(&private_key_info).map_err(pkcs8::Error::Asn1)
+ }
+}
+
+#[cfg(feature = "pkcs8")]
+impl TryFrom> for DecapsulationKey
+where
+ P: KemParams + AssociatedAlgorithmIdentifier>,
+{
+ type Error = pkcs8::Error;
+
+ /// Deserialize the decapsulation key from DER format found in `spki.private_key`.
+ /// Returns a `DecapsulationKey` containing `dk_{pke}`, `ek`, and `z` in case of success.
+ fn try_from(private_key_info_ref: pkcs8::PrivateKeyInfoRef<'_>) -> Result {
+ if private_key_info_ref.algorithm.oid != P::ALGORITHM_IDENTIFIER.oid {
+ return Err(pkcs8::Error::PublicKey(pkcs8::spki::Error::OidUnknown {
+ oid: P::ALGORITHM_IDENTIFIER.oid,
+ }));
+ }
+
+ let seed_to_key = |seed: OctetStringRef<'_>| -> Result, Self::Error> {
+ let (head, tail) = seed.as_bytes().split_at(32);
+ let d: &B32 = head.try_into().map_err(|_| pkcs8::Error::KeyMalformed)?;
+ let z: &B32 = tail.try_into().map_err(|_| pkcs8::Error::KeyMalformed)?;
+ Ok(Self::generate_deterministic(d, z))
+ };
+
+ let expanded_to_key =
+ |expanded: OctetStringRef<'_>| -> Result, Self::Error> {
+ let bytes = expanded.as_bytes();
+ let array =
+ Encoded::::try_from(bytes).map_err(|_| pkcs8::Error::KeyMalformed)?;
+ Ok(Self::from_bytes(&array))
+ };
+
+ let decaps_key = match private_key_info_ref
+ .private_key
+ .decode_into::()
+ {
+ Ok(PrivateKeyChoice::Seed(seed)) => seed_to_key(seed)?,
+ Ok(PrivateKeyChoice::Expanded(expanded)) => expanded_to_key(expanded)?,
+ Ok(PrivateKeyChoice::Both(PrivateKeyBothChoice { seed, expanded })) => {
+ let computed_decaps_key = seed_to_key(seed)?;
+ let given_decaps_key = expanded_to_key(expanded)?;
+
+ // “When receiving a private key that contains both the seed and the expandedKey,
+ // the recipient SHOULD perform a seed consistency check to ensure
+ // that the sender properly generated the private key”
+ if computed_decaps_key != given_decaps_key {
+ return Err(pkcs8::Error::KeyMalformed);
+ }
+
+ computed_decaps_key
+ }
+ Err(_) => return Err(pkcs8::Error::KeyMalformed),
+ };
+
+ Ok(decaps_key)
+ }
+}
+
#[cfg(test)]
mod test {
use super::*;
diff --git a/ml-kem/src/lib.rs b/ml-kem/src/lib.rs
index a08ae00..484010a 100644
--- a/ml-kem/src/lib.rs
+++ b/ml-kem/src/lib.rs
@@ -80,6 +80,9 @@ pub use util::B32;
pub use param::{ArraySize, ParameterSet};
+#[cfg(feature = "pkcs8")]
+pub use pkcs8::{self, AssociatedOid};
+
/// An object that knows what size it is
pub trait EncodedSizeUser {
/// The size of an encoded object
@@ -165,6 +168,22 @@ impl ParameterSet for MlKem512Params {
type Dv = U4;
}
+#[cfg(feature = "pkcs8")]
+impl AssociatedOid for MlKem512Params {
+ const OID: pkcs8::ObjectIdentifier = const_oid::db::fips203::ID_ALG_ML_KEM_512;
+}
+
+#[cfg(feature = "pkcs8")]
+impl pkcs8::spki::AssociatedAlgorithmIdentifier for MlKem512Params {
+ type Params = pkcs8::der::AnyRef<'static>;
+
+ const ALGORITHM_IDENTIFIER: pkcs8::spki::AlgorithmIdentifier =
+ pkcs8::spki::AlgorithmIdentifier {
+ oid: Self::OID,
+ parameters: None,
+ };
+}
+
/// `MlKem768` is the parameter set for security category 3, corresponding to key search on a block
/// cipher with a 192-bit key.
#[derive(Default, Clone, Debug, PartialEq)]
@@ -178,6 +197,22 @@ impl ParameterSet for MlKem768Params {
type Dv = U4;
}
+#[cfg(feature = "pkcs8")]
+impl AssociatedOid for MlKem768Params {
+ const OID: pkcs8::ObjectIdentifier = const_oid::db::fips203::ID_ALG_ML_KEM_768;
+}
+
+#[cfg(feature = "pkcs8")]
+impl pkcs8::spki::AssociatedAlgorithmIdentifier for MlKem768Params {
+ type Params = pkcs8::der::AnyRef<'static>;
+
+ const ALGORITHM_IDENTIFIER: pkcs8::spki::AlgorithmIdentifier =
+ pkcs8::spki::AlgorithmIdentifier {
+ oid: Self::OID,
+ parameters: None,
+ };
+}
+
/// `MlKem1024` is the parameter set for security category 5, corresponding to key search on a block
/// cipher with a 256-bit key.
#[derive(Default, Clone, Debug, PartialEq)]
@@ -191,6 +226,22 @@ impl ParameterSet for MlKem1024Params {
type Dv = U5;
}
+#[cfg(feature = "pkcs8")]
+impl AssociatedOid for MlKem1024Params {
+ const OID: pkcs8::ObjectIdentifier = const_oid::db::fips203::ID_ALG_ML_KEM_1024;
+}
+
+#[cfg(feature = "pkcs8")]
+impl pkcs8::spki::AssociatedAlgorithmIdentifier for MlKem1024Params {
+ type Params = pkcs8::der::AnyRef<'static>;
+
+ const ALGORITHM_IDENTIFIER: pkcs8::spki::AlgorithmIdentifier =
+ pkcs8::spki::AlgorithmIdentifier {
+ oid: Self::OID,
+ parameters: None,
+ };
+}
+
/// A shared key produced by the KEM `K`
pub type SharedKey = Array::SharedKeySize>;
@@ -212,6 +263,19 @@ pub type MlKem1024 = kem::Kem;
#[cfg(test)]
mod test {
use super::*;
+ #[cfg(all(feature = "pkcs8", feature = "alloc", feature = "pem"))]
+ use crate::kem::PrivateKeyBothChoice;
+ #[cfg(all(feature = "pkcs8", feature = "alloc"))]
+ use {
+ crate::kem::PrivateKeyChoice,
+ pkcs8::{
+ der::{self, Decode},
+ {
+ DecodePrivateKey, DecodePublicKey, EncodePrivateKey, EncodePublicKey,
+ PrivateKeyInfoRef, SubjectPublicKeyInfoRef,
+ },
+ },
+ };
fn round_trip_test()
where
@@ -232,4 +296,254 @@ mod test {
round_trip_test::();
round_trip_test::();
}
+
+ #[cfg(all(feature = "pkcs8", feature = "alloc"))]
+ fn der_serialization_and_deserialization(expected_encaps_len: u32, expected_decaps_len: u32)
+ where
+ K: KemCore,
+ K::EncapsulationKey: EncodePublicKey + DecodePublicKey,
+ K::DecapsulationKey: EncodePrivateKey + DecodePrivateKey,
+ {
+ let mut rng = rand::rng();
+ let (decaps_key, encaps_key) = K::generate(&mut rng);
+
+ // TEST: (de)serialize encapsulation key into DER document
+ {
+ let der_document = encaps_key.to_public_key_der().unwrap();
+ let serialized_document = der_document.as_bytes();
+
+ // deserialize encapsulation key from DER document
+ let parsed = der::Document::from_der(serialized_document).unwrap();
+ assert_eq!(parsed.len(), der::Length::new(expected_encaps_len));
+
+ // verify that original encapsulation key corresponds to deserialized encapsulation key
+ let pub_key = parsed.decode_msg::().unwrap();
+ assert_eq!(
+ encaps_key.as_bytes().as_slice(),
+ pub_key.subject_public_key.as_bytes().unwrap()
+ );
+ }
+
+ // TEST: (de)serialize encapsulation key into DER document with the blanket implementation for DecodePublicKey
+ {
+ let der_document = encaps_key.to_public_key_der().unwrap();
+ let serialized_document = der_document.as_bytes();
+
+ // deserialize encapsulation key from DER document
+ let parsed = K::EncapsulationKey::from_public_key_der(serialized_document).unwrap();
+
+ // verify that original encapsulation key corresponds to deserialized encapsulation key
+ assert_eq!(parsed, encaps_key);
+ }
+
+ // TEST: (de)serialize decapsulation key into DER document
+ {
+ let der_document = decaps_key.to_pkcs8_der().unwrap();
+ let serialized_document = der_document.as_bytes();
+
+ // deserialize decapsulation key from DER document
+ let secret_document = der::SecretDocument::from_pkcs8_der(serialized_document).unwrap();
+ assert_eq!(secret_document.len(), der::Length::new(expected_decaps_len));
+ assert_eq!(secret_document.as_bytes(), der_document.as_bytes());
+
+ // verify that original decapsulation key corresponds to deserialized decapsulation key
+ let priv_key = secret_document.decode_msg::().unwrap();
+
+ if let Ok(PrivateKeyChoice::Expanded(expanded)) = priv_key.private_key.decode_into() {
+ assert_eq!(decaps_key.as_bytes().as_slice(), expanded.as_bytes());
+ } else {
+ panic!("unexpected PrivateKey serialization");
+ }
+ }
+
+ // TEST: (de)serialize decapsulation key into DER document with the blanket implementation for DecodePrivateKey
+ {
+ let der_document = decaps_key.to_pkcs8_der().unwrap();
+ let serialized_document = der_document.as_bytes();
+
+ // deserialize decapsulation key from DER document
+ let parsed = K::DecapsulationKey::from_pkcs8_der(serialized_document).unwrap();
+
+ // verify that original decapsulation key corresponds to deserialized decapsulation key
+ assert_eq!(parsed, decaps_key);
+ }
+ }
+
+ #[cfg(all(feature = "pkcs8", feature = "alloc"))]
+ #[test]
+ fn pkcs8_serialize_and_deserialize_round_trip() {
+ // NOTE: standardized encapsulation key sizes for MlKem{512,768,1024} are {800,1184,1568} bytes respectively.
+ // DER serialization adds 22 bytes. Thus we expect a length of {822,1206,1590} respectively.
+ // NOTE: standardized decapsulation key sizes for MlKem{512,768,1024} are {1632,2400,3168} bytes respectively.
+ // DER serialization adds 28 bytes. Thus we expect a length of {1660,2428,3196} respectively.
+ der_serialization_and_deserialization::(822, 1660);
+ der_serialization_and_deserialization::(1206, 2428);
+ der_serialization_and_deserialization::(1590, 3196);
+ }
+
+ #[cfg(all(feature = "pkcs8", feature = "alloc", feature = "pem"))]
+ fn compare_with_reference_keys(variant: usize, ref_pub_key_pem: &str, ref_priv_key_pem: &str)
+ where
+ K: KemCore,
+ K::EncapsulationKey: EncodePublicKey,
+ K::DecapsulationKey: EncodePrivateKey,
+ {
+ // auxiliary RNG implementation for a static seed
+ struct SeedBasedRng {
+ index: usize,
+ seed: [u8; SEED_LEN],
+ }
+
+ impl rand_core::RngCore for SeedBasedRng {
+ fn next_u32(&mut self) -> u32 {
+ let mut buf = [0u8; 4];
+ self.fill_bytes(&mut buf);
+ u32::from_be_bytes(buf)
+ }
+
+ fn next_u64(&mut self) -> u64 {
+ let mut buf = [0u8; 8];
+ self.fill_bytes(&mut buf);
+ u64::from_be_bytes(buf)
+ }
+
+ fn fill_bytes(&mut self, dst: &mut [u8]) {
+ for item in dst {
+ *item = self.seed[self.index];
+ self.index = self.index.wrapping_add(1) & ((1 << SEED_LEN.ilog2()) - 1);
+ }
+ }
+ }
+
+ impl CryptoRng for SeedBasedRng {}
+
+ const SEED_LEN: usize = 64;
+ assert_eq!(SEED_LEN & (SEED_LEN - 1), 0);
+
+ let seed: [u8; SEED_LEN] = core::array::from_fn(|i| u8::try_from(i).unwrap());
+ let mut rng = SeedBasedRng { seed, index: 0 };
+ let (decaps_key, encaps_key) = K::generate(&mut rng);
+
+ let gen_pub_key_pem = encaps_key
+ .to_public_key_pem(pkcs8::LineEnding::LF)
+ .expect("serialization works");
+ let gen_priv_key_pem = decaps_key
+ .to_pkcs8_pem(pkcs8::LineEnding::LF)
+ .expect("serialization works");
+
+ {
+ // TEST: DER document of public key must match
+ let gen_pub_key_der = encaps_key.to_public_key_der().expect("serialization works");
+ let ref_pub_key_der = der::Document::from_pem(ref_pub_key_pem)
+ .expect("can read pubkey PEM document")
+ .1;
+ assert_eq!(gen_pub_key_der, ref_pub_key_der);
+ }
+
+ // TEST: PEM document of public key must match
+ assert_eq!(
+ gen_pub_key_pem, ref_pub_key_pem,
+ "key generated from static seed and reference public key for ML-KEM-{variant} do not match"
+ );
+ // TEST: PEM document of private key must match
+ assert_eq!(
+ gen_priv_key_pem.as_str(),
+ ref_priv_key_pem,
+ "key generated from static seed and reference private key for ML-KEM-{variant} do not match"
+ );
+ }
+
+ #[cfg(all(feature = "pkcs8", feature = "alloc", feature = "pem"))]
+ #[test]
+ fn pkcs8_generate_same_keys_like_golang_for_static_seed() {
+ // NOTE: test vector files come from https://github.com/lamps-wg/kyber-certificates/tree/624bcaa4bd9ea9e72de5b51d81ce2d90cbd7e54a
+ const PEM_512_PUB: &str = include_str!("../tests/examples/ML-KEM-512.pub");
+ const PEM_768_PUB: &str = include_str!("../tests/examples/ML-KEM-768.pub");
+ const PEM_1024_PUB: &str = include_str!("../tests/examples/ML-KEM-1024.pub");
+ const PEM_512_PRIV: &str = include_str!("../tests/examples/ML-KEM-512-expanded.priv");
+ const PEM_768_PRIV: &str = include_str!("../tests/examples/ML-KEM-768-expanded.priv");
+ const PEM_1024_PRIV: &str = include_str!("../tests/examples/ML-KEM-1024-expanded.priv");
+
+ compare_with_reference_keys::(512, PEM_512_PUB, PEM_512_PRIV);
+ compare_with_reference_keys::(768, PEM_768_PUB, PEM_768_PRIV);
+ compare_with_reference_keys::(1024, PEM_1024_PUB, PEM_1024_PRIV);
+ }
+
+ #[cfg(all(feature = "pkcs8", feature = "alloc", feature = "pem"))]
+ #[test]
+ fn pkcs8_can_read_reference_private_keys() {
+ // NOTE: test vector files come from https://github.com/lamps-wg/kyber-certificates/tree/624bcaa4bd9ea9e72de5b51d81ce2d90cbd7e54a
+ const PEM_512_SEED: &str = include_str!("../tests/examples/ML-KEM-512-seed.priv");
+ const PEM_512_EXPANDED: &str = include_str!("../tests/examples/ML-KEM-512-expanded.priv");
+ const PEM_512_BOTH: &str = include_str!("../tests/examples/ML-KEM-512-both.priv");
+ const PEM_768_SEED: &str = include_str!("../tests/examples/ML-KEM-768-seed.priv");
+ const PEM_768_EXPANDED: &str = include_str!("../tests/examples/ML-KEM-768-expanded.priv");
+ const PEM_768_BOTH: &str = include_str!("../tests/examples/ML-KEM-768-both.priv");
+ const PEM_1024_SEED: &str = include_str!("../tests/examples/ML-KEM-1024-seed.priv");
+ const PEM_1024_EXPANDED: &str = include_str!("../tests/examples/ML-KEM-1024-expanded.priv");
+ const PEM_1024_BOTH: &str = include_str!("../tests/examples/ML-KEM-1024-both.priv");
+
+ fn expect_seed_bytes(ref_pem: &str, expected_seed_prefix: &[u8]) {
+ let length = expected_seed_prefix.len();
+ let secret_document = der::SecretDocument::from_pkcs8_pem(ref_pem)
+ .expect("can read reference PEM private key file");
+ let priv_key = secret_document.decode_msg::().unwrap();
+
+ let given_prefix = match priv_key
+ .private_key
+ .decode_into()
+ .expect("could not read internal structure of PEM private key")
+ {
+ PrivateKeyChoice::Seed(seed)
+ | PrivateKeyChoice::Both(PrivateKeyBothChoice { seed, .. }) => {
+ &seed.as_bytes()[0..length]
+ }
+ PrivateKeyChoice::Expanded(_) => return,
+ };
+
+ assert_eq!(given_prefix, expected_seed_prefix);
+ }
+
+ fn expect_expanded_bytes(ref_pem: &str, expected_expanded_prefix: &[u8]) {
+ let length = expected_expanded_prefix.len();
+ let secret_document = der::SecretDocument::from_pkcs8_pem(ref_pem)
+ .expect("can read reference PEM private key file");
+ let priv_key = secret_document.decode_msg::().unwrap();
+
+ let given_prefix = match priv_key
+ .private_key
+ .decode_into()
+ .expect("could not read internal expanded structure of PEM private key")
+ {
+ PrivateKeyChoice::Seed(_) => return,
+ PrivateKeyChoice::Expanded(expanded)
+ | PrivateKeyChoice::Both(PrivateKeyBothChoice { expanded, .. }) => {
+ &expanded.as_bytes()[0..length]
+ }
+ };
+
+ assert_eq!(given_prefix, expected_expanded_prefix);
+ }
+
+ const STATIC_SEED_PREFIX: &[u8] =
+ &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17];
+ const EXPANDED_512_KEY_PREFIX: &[u8] = &[0x70, 0x55, 0x4f, 0xd4, 0x36, 0x34, 0x4f, 0x27];
+ const EXPANDED_768_KEY_PREFIX: &[u8] = &[0x27, 0xd2, 0xa7, 0x7f, 0x33, 0x75, 0x6f, 0x61];
+ const EXPANDED_1024_KEY_PREFIX: &[u8] = &[0xf7, 0x7b, 0x7f, 0x6b, 0x15, 0xc7, 0x3f, 0xe2];
+
+ expect_seed_bytes(PEM_512_SEED, STATIC_SEED_PREFIX);
+ expect_expanded_bytes(PEM_512_EXPANDED, EXPANDED_512_KEY_PREFIX);
+ expect_seed_bytes(PEM_512_BOTH, STATIC_SEED_PREFIX);
+ expect_expanded_bytes(PEM_512_BOTH, EXPANDED_512_KEY_PREFIX);
+
+ expect_seed_bytes(PEM_768_SEED, STATIC_SEED_PREFIX);
+ expect_expanded_bytes(PEM_768_EXPANDED, EXPANDED_768_KEY_PREFIX);
+ expect_seed_bytes(PEM_768_BOTH, STATIC_SEED_PREFIX);
+ expect_expanded_bytes(PEM_768_BOTH, EXPANDED_768_KEY_PREFIX);
+
+ expect_seed_bytes(PEM_1024_SEED, STATIC_SEED_PREFIX);
+ expect_expanded_bytes(PEM_1024_EXPANDED, EXPANDED_1024_KEY_PREFIX);
+ expect_seed_bytes(PEM_1024_BOTH, STATIC_SEED_PREFIX);
+ expect_expanded_bytes(PEM_1024_BOTH, EXPANDED_1024_KEY_PREFIX);
+ }
}
diff --git a/ml-kem/tests/examples/ML-KEM-1024-both.priv b/ml-kem/tests/examples/ML-KEM-1024-both.priv
new file mode 100644
index 0000000..ff3dcbe
--- /dev/null
+++ b/ml-kem/tests/examples/ML-KEM-1024-both.priv
@@ -0,0 +1,71 @@
+-----BEGIN PRIVATE KEY-----
+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
+Pj8=
+-----END PRIVATE KEY-----
diff --git a/ml-kem/tests/examples/ML-KEM-1024-expanded.priv b/ml-kem/tests/examples/ML-KEM-1024-expanded.priv
new file mode 100644
index 0000000..88ab933
--- /dev/null
+++ b/ml-kem/tests/examples/ML-KEM-1024-expanded.priv
@@ -0,0 +1,69 @@
+-----BEGIN PRIVATE KEY-----
+MIIMeAIBADALBglghkgBZQMEBAMEggxkBIIMYPd7f2sVxz/izFRrZ/t3TKGbQs1G
+Pqn7uYTKR3p3tscQh8vwUavkc2qQcsbocMgxHFWWP1AKPHsbjypYVY9JxiUntsWU
+teess7z1lyc6V0NRfRUSCL1Kph51umewvVlKmUkZYnrAqATUieFxM2vDOfRmZwbl
+E0QSs2aCPVAxjIvyYasSCiigT+wBzBXytxkSzuVKqO7YVGlLa6iGtet2YebVaqwh
+PMHYFNWSs5VVT650R200NxFjEpv4ZFJyUGBswhpTdGsgmXB3u6FVczsopOf6B3Y5
+lSR2PrSBzqoRNmw0dKBGhfQMPwiwQk9Av/lJoKyScEw7oMbrNvH1tiHYvytjJ761
+fNP6y5QYb+P8mrChQ0uykdLJu3ByMFfiJUBZZW9WWRmjLPdFed6JaBzSxak1pStK
+qi0ky11cniBynsVJLsNpYe+4ooy8AKwwNSMpXz2ANqvBYDMHznDXhIo1ZXpWh91Y
+mSfqY3MWJquybsTkMbjrazsLweglc+5zsaAhGDGDUoEIri6srduVtGSguYRpwxnM
+J7+gG8MQVKaMBVArFmK4ef6YoXEcNCb2Q2ywIUzqN5rDp+X7YBhKN8HaHtphxsOc
+HdToR4RYEfKjWKQ3MVKFNtSjKRsEFYwsPcZBYkiCZ4vHgF9YqdlMcQRWeEaiBE5l
+rs4qIlNytgJHmaVHfWAjdQSqXArFe8cKNVjAjE3mh+8TArT8tVlEE9IsuVm8Mb5C
+NFBAPGvFfcQRs/76wQUqxLsWLERUWkyoCJJlf6E6CyxILO1inMSZnZacWT1KrfBz
+zD46RY54qKoDlAjmUr6TsgyLQuxbDlAjnaxyYFKFGm0VMS7DntIItyIJpXfGsncB
+EolXSdUmDn3URsCwEYwQAL5oAdJhH88AeSqcxPS0mSL5otS5yPpaXQ1gUGYxp+lx
+zuhAsI+mPBNynX6lqscDUqmEzbZpMxy6dY/ofsOTGz4xYfzHR6p0lCRon+rhS/fJ
+ov+6EwKyErgDctjpBJ22mjoSYdCihZqbTVeJngukFgehtnp8DhKSNon4xjlTd9lw
+x0kKQSlhGh0Fw7eBO+2UVCByP3+VJah3k/r7v8qYLma7gGgcgySKidoITBmIL0jz
+Hn/AkJOknp/QlpGwIe30Y6/FGbYoU4FhGDRhFfsLiCzGSC88XLzBwYlGl+EjlZiz
+Syqaes0VJE0GkMiBlAl6m+2lheh8Q3EkYkwhB2jmIV03ZIJlPriZR4d8EY03DGlq
+b/zBAYrkE6CKjQ/6qBmUXaehZ8IpkTKQytHICjaSWHYmEOolPmLcJCJqMMiSwSE2
+wybxP0RGZkcSsLkLwGO0AoWTy94GzcIiieJAx+KWtZFywa7ajJngUS0aAWOpQuoz
+FI5pN8AmApQkuBuZax3yLqBiPsZca/CTUAzzvzU3Stw5IDXKfFg7mWhbylQaCAex
+Y6zQiIvgOF3qgg2kbk27RNLkYsc0uDpHP+0TZCcxWSV8wlmoxWdsHHbUHVa5kH7B
+w1mcnokHQDonpwXjYZsEsK0Ebo7IFpwXtGDUTAwMRGTQRMlGGGvHJZZQg6iSvMSV
+wFQDEf+bPlGSwwPYj4ukapAceC7wI4jxsq3atqU1D8NjlwDjFUM3M35KF401HNK1
+buHwv+o0qs+jPS7HkeUHUtTQNMsclRVyyqpcTZCUe2sXWm3Txip3u496ya4kcZtT
+wrEgoodphuIXtyvXzuRKcmWxHO4asiYXYrMaNzg4aWnAgl+3lFLmUuEUL8c8nfb7
+pBF5W0cXkispui1Tq+WowNzBYBsJbJbXk4/VpoqHl8e5R3qGpHLrXaJQyy/sMY2D
+yPQ7vo4Rw143fTSTZshcQ4JZf2/CegBRwPsAsCwByiD5pCfxclmUd8ppDMEyfg8C
+X4DsM4qAoVnjCMEqJ9safhuWCpnTffwihy5Rkw8oxlGrIh9Tq67iC62aPqvLq5Ey
+Ub8TW+spYXtXVDM8TarbIjg0HCrZN4GGKA9kSUQLeEunj12sRNj2Wzt0IZUDl8OR
+Oi3SPsbRy3F7NqX8la8ZHieClpSMElTqhrTsAEuUwpRQERGRgjs1FMmsHqPZglzL
+hjk6LfsEZU+iGS03v60cSXxlAu7lyoCnO/zguvWlSohYWkATl6PSMvQmp6+wgrwh
+pEMXCQ6qx1ksLqiKZTxEkeoZOTEzX1LpiaPEzFbZxVNzLVfEcPtBq3WbZdLQREU4
+L82cTjRKESj6nhHgQ1jhku0BSyMjKn7isi4jcX9EER7jNXU5nDdkbamBPsmyEq/p
+Tl3FwjMKcpTMH0I0ptP7tPFoWriJLASssXzRwXDXsGEbanF2x5TMjGf1X8kjwq0g
+MQDzZZkYgsMCQ9d4E4Q7XsfJZAMiY3BgkuzwDHUWvmTkWYykImwGm7XmfkF1zyKG
+yN1cSIpsWGHzG6oL0CaUcOi1Ud07zTjIbBL5zbF2x33ItsAqcB9HiQLIVT9pTA2C
+cntMSlwsEEEhKqEnSAi4IRGzd+x1IU6bGXj3YATUE52YYT9LjpjSCve1NAc6UJqV
+m3p1ZPm0DKIYv2GCkyCoUCAXlU0yjXrGx2nsKXAHVuewaFs0DV4RgFlQSkmppQoQ
+GY6xCleEZ460J9e0uruVUpM7BiiXlz4TGOrwoOrDdYSmVAGxcD4EKszYN1MUg/JB
+ytzRwdN4EZ5pRCnbGZrIkeTFNDdXCFuzrng2ZzUMRFjZdnLoYegLHSZ5UQ6jpvI2
+DHekaULHoGpVTSKAgMhLR67xTbF2IMsWwGqzChvkzacIK+n4fpwhHEaRY0mluo6q
+UgHHKUo8CIW1O2V0UhCIJexkbJCgRhIyTufQMa/lNDEyy+9ntu+xpewoCbdzU4zn
+ez2LBOsLPCJWAR5McWwZqLoHUr9xSSEXZJ8GFcMpD8KaRv3kvVLbkobWAziCRCWc
+FaesK2QKYMwDN2pYQaP7ikc1aPqbGiZyFfNMAWl7Dw5icXXXIQW3cHwpueYUvcM6
+b2yBipU3C0J4gte0dnlqnsbrmTJ0zZsjkagrpF4zk9Lprpchyp1sG5iLWCdxP5Cm
+WF3pQzUowCsDzhC7X3IBOND7tMMMEma5GOUpJd/hezf5XSK8pU9HWRmshZCYwPDQ
+isWHXvKbVv0UHm7xX3AKC2bzlZXFiBdzc8RmmyG8Bx5MOqXwtKMbYljzXaJKw80p
+x/IJJBDFB4NVsTj7U6a5rm4LnAgkPnuqRcRzduuMfxPUz1Gqc2+jFUDJJB83DaVE
+v5+cKNmlfi8qfKlaTktGbmQas7zHat8ROdVnpvErUvOmXn7AquJryqjFWDOwTlmZ
+jryaGTD7ttIjPFPSwfi5UY48Lec6Gd7ms4Clsylxz2ThKf1sH6bnXUojRQHpZt06
+VAr1yPTzSmtKJT7ihJJWbV5nxvVYVfywUG+wbBVnRNmgOjGib6lMrRTxV7fzA9B6
+acdzdo/LTQecCQWXA6DDqU3kuZ6jovFlg9D5Fwo5UNsHtPC8MIApJ/n3lhtiWYkm
+NqlQKicFMDY3eZ3TRNpFHBz3v2eEDOsweauMa4wZJ/ZAU8YSRQxFyeYDvBZmbllr
+NHHhA7bxVEdCTRcCIEgRH/vTfhxnD2TxS4p7MrlMGkm0XdL8OM1SidkQrWNgLPXh
+MELGSsZ5e4n7VRrQjgWpLSAMzLfnEu8jyTEss1DwKatTfihzR/0wdawQkGp4Pxxs
+B8y4j0EijEvhxkD3kLXDpdXTynkklddLxGFWJljAesYAJ2uSSrW8m+HwSUy3b4L0
+YKdICXJmM4HhaZlgYdeZhZ7FTU9cpcQRwB2xWXsWWXdmneE6koo0r7rCWP6oxHZC
+OclCHcMRm/W0dpkgaXgyexxTRe90anmDhB8FbiU0EAqyTU6au9CxfGqVvUw8DkD2
+nhYSrO6yi5kIbJURbnIEJziTOQv0a4mbNihrDr8ZR7uYhPcyyifagrGbXcDMf4iF
+cUkQiIsjEMT5MZ1BCzTmQzuQA+IXa7mVJXRWEG6JUhY7i6WSUwzFqgrrQ605j+np
+e6pSPXpEMWd8PTrwcZ5HXbhcqVr1CJvqvrBbL6q0iWumD4HIhHKle0aoKIJqDN+0
+RvgYkYLSv16sTsHMXer1mcihPkgjVAbRf/3cg0S2xmmEqGiqkvoCInoIaVDrDIcB
+7VjcYod2uYOILhF1YTSeXBMafhFqBGOGHX0YZjxWJ8OMcUfdqt/Uis16RTUgISIj
+JCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+Pw==
+-----END PRIVATE KEY-----
diff --git a/ml-kem/tests/examples/ML-KEM-1024-seed.priv b/ml-kem/tests/examples/ML-KEM-1024-seed.priv
new file mode 100644
index 0000000..e9d8768
--- /dev/null
+++ b/ml-kem/tests/examples/ML-KEM-1024-seed.priv
@@ -0,0 +1,4 @@
+-----BEGIN PRIVATE KEY-----
+MFQCAQAwCwYJYIZIAWUDBAQDBEKAQAABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZ
+GhscHR4fICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8=
+-----END PRIVATE KEY-----
diff --git a/ml-kem/tests/examples/ML-KEM-1024.pub b/ml-kem/tests/examples/ML-KEM-1024.pub
new file mode 100644
index 0000000..d44e8f0
--- /dev/null
+++ b/ml-kem/tests/examples/ML-KEM-1024.pub
@@ -0,0 +1,36 @@
+-----BEGIN PUBLIC KEY-----
+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
+uYOILhF1
+-----END PUBLIC KEY-----
diff --git a/ml-kem/tests/examples/ML-KEM-512-both.priv b/ml-kem/tests/examples/ML-KEM-512-both.priv
new file mode 100644
index 0000000..51d4be4
--- /dev/null
+++ b/ml-kem/tests/examples/ML-KEM-512-both.priv
@@ -0,0 +1,39 @@
+-----BEGIN PRIVATE KEY-----
+MIIGvgIBADALBglghkgBZQMEBAEEggaqMIIGpgRAAAECAwQFBgcICQoLDA0ODxAR
+EhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+PwSC
+BmBwVU/UNjRPJ4Wxs7G6wYS2Z5ADM2wm8Vp96HjEglxr4D88SkgPdbdIaq0x06AF
+GGI/0gerUo3WJyFJWDWuAGLDZ7dKcbrxCq0OiikCB2vjE0i+sVzMCVfN67Sv8iZ1
+a7xgG2Voq3hKy66zRwLw+GomICEYsisj+DVYd2x5wU26mDN5yAPg3MMWChF1cDDm
+nGkZeY2B62mKmkSDqZ5aXLLDHJpmF5nzzInHkHBuoEFikEXUKoOu2Ihg45TGkYfi
+EF0ozBTsOTWS1n3QCqQ/6LTq5EFAAoZrXHE8ao19Fs94uBnW8S6eWnQjOQjwsV48
+S6gynFzdpVyEko46qAY+WqlnZAP5FzWxEBDH9ZMJE2TchkRbyASECpohckISRp+K
+ewzgrGmOuGytOaf0gk2aUWOqwh7mgIsFPIo/rLC2dEtSYrvLJqQ/ZkyHMrZM/HrP
+CZYF9Bx5YGCXasQzgz/gA0P7GCgwCkJHQRFuS0W7J26oESmg20xuYLzmERAejGJU
+dJJeAiJnkwij53CNGXKntCPrIyhRw20u1T0+07t1AGNwYaXcIpL6HEZsBzVGgzKL
+7Cwe0stcmbeOyglpA4z3w03RGHJOMcrghiBrNDArUg9dF3re1bPM4CrM6AjqJrzA
+cmJf25PxdFil/B1No5Q4Ch9X6cxmEJQ4oHXw0oE/zEoZnMdts4I/JwsAYVlBkpQE
+EaN/+6+uLBUBZc7Fxr9zxZX7ks0VMSYH2gcHeGUr2ZRLxIvH0aU0M4utC61mVsXV
+As54UKsVhyRO61j0OateCFdKcYyKrD13x5i7oVQnM75zRI8j+3DA5TU6J8iDIsUh
+hJOvuzgIZDTW1gpWuoh91JjDqyaghwmTgVqmpAl18hityhWC1k/8hlL7s6mm+8ME
++RlF+kqu8oeP1xXfcBE9I3n0SIb4Esg/8rcZpp4ex0rksVrM067VpTznansJgkcW
+M7lzy0ChoAFdCkJPoRpHnAIwF0NtKikA6ZPrWgoGdADH9KrfIB/E+jEmSmO66VzI
+1lw5lYFeWX0QQ1XPKapTM8kyUYadW82+SHEk9gK4tqZsFsR2Fkitdlz12ABrUV6Q
+Wn8KwHawxi76MoFT58pXAWmfEwXx5rxvkLDkm2k1ErbOmSqLgBbd/BpmLH4/lhnL
+2GnddxrzCJbM1ZGKxst3Rmxed5mW1n/5qryXUD8se34tAA2GRQ+xgHykyr2kZYJa
+MceJobekkas4cnZdMg0LcZIPohPJQJNBa4O4Ek5p9l5iy1AA3MN6qaD/9zlwxHcv
+NX0kGJym9TBVaMDiN2o3YqaMYF5WPF0glXLg/HUyyilHKVNVZ7X8QTxeh5LSRkU2
+zICPmK3XRmTxQVZvkBapClQYKamKBGTOQai7RMLU+jwsIJRgco7xShp8TJuY0SID
+tMw1KRYKmrLXg49/9rU64FqjGn1ka3r6bEWTJSajw3VWGb6ZTCEcKjHAWzRHg2yy
+FQvhgp2uawTFU1z/VG45K6eXQRcg+ST0kKWsVJXyE1bVULeCpkwWiLa2VbzHhCGX
+pDTC9lY7W38Jp4vMSIIyeDVh0W9MurZ1VAAFB4FXDGZgS4F60SUilHNuiwGGGkta
+dFGbi2/lFImlByOS5YdibHE3dlddM4BqHI4nMq+XwmgPUWZjMcTri7wEMcT5aDLa
+8bPEVSj7oVP2x4scGYcClHzNM3cnpG+1O6Ed5ctBkTRoWVFstq1yQA888gmyNq7z
+WlgKyH6z4w+v1mlzyop90mda9B96F7YUM80a+A93CIafZlSISXmAsawQoM3LY2oA
+7YaBs15CkSTKgDUHJbhfg6Xqw6SjzBYAkD5lKTVgubM25a8NUp2sGgSBGTAst6m8
+wRC5SFG/AhF/GZ3EhahSt0c/CbgxpoMdW1TAt5DSJc9ruS2UYqJs2zPdpRI8eq8O
+JqC4NlXuoovzqAdHJQGP1rrktgHPYbqrcaej01GXo0PnS0onLBJdVAiWQm2Ft5WN
+Ozimuph+w3Ilx7RM2xLd5FObSrCCNjaD8Ev3oJzFxB3+gwobFi4LMkM0Ni8IShRG
+dyM0S63QAPjYxTfEj5mPBTB869Ht4LgcO8WaBlobbWOybILxAf9kgGOzduK7bFt0
+VfZVpQwv6treFQ76Dg5vNlrqICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9
+Pj8=
+-----END PRIVATE KEY-----
diff --git a/ml-kem/tests/examples/ML-KEM-512-expanded.priv b/ml-kem/tests/examples/ML-KEM-512-expanded.priv
new file mode 100644
index 0000000..418dfa0
--- /dev/null
+++ b/ml-kem/tests/examples/ML-KEM-512-expanded.priv
@@ -0,0 +1,37 @@
+-----BEGIN PRIVATE KEY-----
+MIIGeAIBADALBglghkgBZQMEBAEEggZkBIIGYHBVT9Q2NE8nhbGzsbrBhLZnkAMz
+bCbxWn3oeMSCXGvgPzxKSA91t0hqrTHToAUYYj/SB6tSjdYnIUlYNa4AYsNnt0px
+uvEKrQ6KKQIHa+MTSL6xXMwJV83rtK/yJnVrvGAbZWireErLrrNHAvD4aiYgIRiy
+KyP4NVh3bHnBTbqYM3nIA+DcwxYKEXVwMOacaRl5jYHraYqaRIOpnlpcssMcmmYX
+mfPMiceQcG6gQWKQRdQqg67YiGDjlMaRh+IQXSjMFOw5NZLWfdAKpD/otOrkQUAC
+hmtccTxqjX0Wz3i4GdbxLp5adCM5CPCxXjxLqDKcXN2lXISSjjqoBj5aqWdkA/kX
+NbEQEMf1kwkTZNyGRFvIBIQKmiFyQhJGn4p7DOCsaY64bK05p/SCTZpRY6rCHuaA
+iwU8ij+ssLZ0S1Jiu8smpD9mTIcytkz8es8JlgX0HHlgYJdqxDODP+ADQ/sYKDAK
+QkdBEW5LRbsnbqgRKaDbTG5gvOYREB6MYlR0kl4CImeTCKPncI0Zcqe0I+sjKFHD
+bS7VPT7Tu3UAY3BhpdwikvocRmwHNUaDMovsLB7Sy1yZt47KCWkDjPfDTdEYck4x
+yuCGIGs0MCtSD10Xet7Vs8zgKszoCOomvMByYl/bk/F0WKX8HU2jlDgKH1fpzGYQ
+lDigdfDSgT/MShmcx22zgj8nCwBhWUGSlAQRo3/7r64sFQFlzsXGv3PFlfuSzRUx
+JgfaBwd4ZSvZlEvEi8fRpTQzi60LrWZWxdUCznhQqxWHJE7rWPQ5q14IV0pxjIqs
+PXfHmLuhVCczvnNEjyP7cMDlNTonyIMixSGEk6+7OAhkNNbWCla6iH3UmMOrJqCH
+CZOBWqakCXXyGK3KFYLWT/yGUvuzqab7wwT5GUX6Sq7yh4/XFd9wET0jefRIhvgS
+yD/ytxmmnh7HSuSxWszTrtWlPOdqewmCRxYzuXPLQKGgAV0KQk+hGkecAjAXQ20q
+KQDpk+taCgZ0AMf0qt8gH8T6MSZKY7rpXMjWXDmVgV5ZfRBDVc8pqlMzyTJRhp1b
+zb5IcST2Ari2pmwWxHYWSK12XPXYAGtRXpBafwrAdrDGLvoygVPnylcBaZ8TBfHm
+vG+QsOSbaTUSts6ZKouAFt38GmYsfj+WGcvYad13GvMIlszVkYrGy3dGbF53mZbW
+f/mqvJdQPyx7fi0ADYZFD7GAfKTKvaRlgloxx4mht6SRqzhydl0yDQtxkg+iE8lA
+k0Frg7gSTmn2XmLLUADcw3qpoP/3OXDEdy81fSQYnKb1MFVowOI3ajdipoxgXlY8
+XSCVcuD8dTLKKUcpU1VntfxBPF6HktJGRTbMgI+YrddGZPFBVm+QFqkKVBgpqYoE
+ZM5BqLtEwtT6PCwglGByjvFKGnxMm5jRIgO0zDUpFgqasteDj3/2tTrgWqMafWRr
+evpsRZMlJqPDdVYZvplMIRwqMcBbNEeDbLIVC+GCna5rBMVTXP9Ubjkrp5dBFyD5
+JPSQpaxUlfITVtVQt4KmTBaItrZVvMeEIZekNML2Vjtbfwmni8xIgjJ4NWHRb0y6
+tnVUAAUHgVcMZmBLgXrRJSKUc26LAYYaS1p0UZuLb+UUiaUHI5Llh2JscTd2V10z
+gGocjicyr5fCaA9RZmMxxOuLvAQxxPloMtrxs8RVKPuhU/bHixwZhwKUfM0zdyek
+b7U7oR3ly0GRNGhZUWy2rXJADzzyCbI2rvNaWArIfrPjD6/WaXPKin3SZ1r0H3oX
+thQzzRr4D3cIhp9mVIhJeYCxrBCgzctjagDthoGzXkKRJMqANQcluF+DperDpKPM
+FgCQPmUpNWC5szblrw1SnawaBIEZMCy3qbzBELlIUb8CEX8ZncSFqFK3Rz8JuDGm
+gx1bVMC3kNIlz2u5LZRiomzbM92lEjx6rw4moLg2Ve6ii/OoB0clAY/WuuS2Ac9h
+uqtxp6PTUZejQ+dLSicsEl1UCJZCbYW3lY07OKa6mH7DciXHtEzbEt3kU5tKsII2
+NoPwS/egnMXEHf6DChsWLgsyQzQ2LwhKFEZ3IzRLrdAA+NjFN8SPmY8FMHzr0e3g
+uBw7xZoGWhttY7JsgvEB/2SAY7N24rtsW3RV9lWlDC/q2t4VDvoODm82WuogISIj
+JCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+Pw==
+-----END PRIVATE KEY-----
diff --git a/ml-kem/tests/examples/ML-KEM-512-seed.priv b/ml-kem/tests/examples/ML-KEM-512-seed.priv
new file mode 100644
index 0000000..8222519
--- /dev/null
+++ b/ml-kem/tests/examples/ML-KEM-512-seed.priv
@@ -0,0 +1,4 @@
+-----BEGIN PRIVATE KEY-----
+MFQCAQAwCwYJYIZIAWUDBAQBBEKAQAABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZ
+GhscHR4fICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8=
+-----END PRIVATE KEY-----
diff --git a/ml-kem/tests/examples/ML-KEM-512.pub b/ml-kem/tests/examples/ML-KEM-512.pub
new file mode 100644
index 0000000..da30af4
--- /dev/null
+++ b/ml-kem/tests/examples/ML-KEM-512.pub
@@ -0,0 +1,20 @@
+-----BEGIN PUBLIC KEY-----
+MIIDMjALBglghkgBZQMEBAEDggMhADmVgV5ZfRBDVc8pqlMzyTJRhp1bzb5IcST2
+Ari2pmwWxHYWSK12XPXYAGtRXpBafwrAdrDGLvoygVPnylcBaZ8TBfHmvG+QsOSb
+aTUSts6ZKouAFt38GmYsfj+WGcvYad13GvMIlszVkYrGy3dGbF53mZbWf/mqvJdQ
+Pyx7fi0ADYZFD7GAfKTKvaRlgloxx4mht6SRqzhydl0yDQtxkg+iE8lAk0Frg7gS
+Tmn2XmLLUADcw3qpoP/3OXDEdy81fSQYnKb1MFVowOI3ajdipoxgXlY8XSCVcuD8
+dTLKKUcpU1VntfxBPF6HktJGRTbMgI+YrddGZPFBVm+QFqkKVBgpqYoEZM5BqLtE
+wtT6PCwglGByjvFKGnxMm5jRIgO0zDUpFgqasteDj3/2tTrgWqMafWRrevpsRZMl
+JqPDdVYZvplMIRwqMcBbNEeDbLIVC+GCna5rBMVTXP9Ubjkrp5dBFyD5JPSQpaxU
+lfITVtVQt4KmTBaItrZVvMeEIZekNML2Vjtbfwmni8xIgjJ4NWHRb0y6tnVUAAUH
+gVcMZmBLgXrRJSKUc26LAYYaS1p0UZuLb+UUiaUHI5Llh2JscTd2V10zgGocjicy
+r5fCaA9RZmMxxOuLvAQxxPloMtrxs8RVKPuhU/bHixwZhwKUfM0zdyekb7U7oR3l
+y0GRNGhZUWy2rXJADzzyCbI2rvNaWArIfrPjD6/WaXPKin3SZ1r0H3oXthQzzRr4
+D3cIhp9mVIhJeYCxrBCgzctjagDthoGzXkKRJMqANQcluF+DperDpKPMFgCQPmUp
+NWC5szblrw1SnawaBIEZMCy3qbzBELlIUb8CEX8ZncSFqFK3Rz8JuDGmgx1bVMC3
+kNIlz2u5LZRiomzbM92lEjx6rw4moLg2Ve6ii/OoB0clAY/WuuS2Ac9huqtxp6PT
+UZejQ+dLSicsEl1UCJZCbYW3lY07OKa6mH7DciXHtEzbEt3kU5tKsII2NoPwS/eg
+nMXEHf6DChsWLgsyQzQ2LwhKFEZ3IzRLrdAA+NjFN8SPmY8FMHzr0e3guBw7xZoG
+WhttY7Js
+-----END PUBLIC KEY-----
diff --git a/ml-kem/tests/examples/ML-KEM-768-both.priv b/ml-kem/tests/examples/ML-KEM-768-both.priv
new file mode 100644
index 0000000..0c603db
--- /dev/null
+++ b/ml-kem/tests/examples/ML-KEM-768-both.priv
@@ -0,0 +1,55 @@
+-----BEGIN PRIVATE KEY-----
+MIIJvgIBADALBglghkgBZQMEBAIEggmqMIIJpgRAAAECAwQFBgcICQoLDA0ODxAR
+EhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+PwSC
+CWAn0qd/M3VvYSCO8ROr6CWVhz1KvHMOW11nlSm/akzrY4NCcjGoYS9BVQUVrLpS
+5I6ti5QoM7vmhl0T0Up50sXD4H8KBW2N56rfyroFjEk8gLN8q4xWJ1O7O6a27IKX
++IXqp1QNUwAVqEQG5VsTZrV34jbOWKJtih61pE1UIyPCFn2b9KR/mFaZygW65DuN
+7GF/AjgKOJCv1LjH7H7eJlU6Al885bxdemITAwQjXLGtSDa1ZrW4Y72b20WihEpw
+R7bI04PkSFJeBAtNyKK0jGw3yW1i1D8/2I4ogcQKIFyeJI9lK1kngad5+GiA8qFH
+tnhj85HMGlqQjACV4HISKR4u+KNuuanAxgcyJbNHA6SvBJOCxHVz2mj96SRa1ETj
+Gx+9tSHx9h83vAzvKSBn5nDSih/9kE9vEZCplpGKEwN6bKvzw3O/gpbNN6szundG
+gJzD+K3hs2Ob1Xv8xpZQqq8d4Zj8TARjKZ5SxGF4DMQo/F0EpcUYUMumwqUnQ0Bn
+V5PdoJvkTCnmOVxl+F0qCnxt9BHmkRsfLLbDUc0uh19Rtji+d2CX6T4vKy+D2gvu
+9KqFup52OrZFAqDKUiLp6rWztwiO1SBg6Mgmm5Q6casK4cWxtofS4BnPgDa8+b9u
+e6w6qjbkFmD6pFQPJkjNk6GJ7Fwt6nC6yqpP/JBvkIEOobZ78k8seM9rqIGq6mHA
+ZSv/lbG65EJtF3O5zCyoLCHjjGNuOxxSMkSYawvoqD9d1c8tVHYvs8Xr9ZuOiFMC
+sc5HAz7fdg9OApvkC21WaxnddYrNXHQSh4ExJE+QFyxT8mZjwh2QUwHUi6+RyRfM
+d3np2IAswQ2Jo3BQmaKtOjqIlnQ8EURpgJO+JX2stm3HhSKLkSyNll0Uqig0LDrE
+qT/vpTKyCUXdwQIBOcFNY4uQjE3d6aBkW5Wy5EFNQLt58EQTgw8VqHPCi7cFnCdB
+ACAV8gQI8FjnFbC/mVtTgLfdMloFarl+ZZor4M32wzcxxoOmNLdx6MkqE5ruS7Dk
+nHB3Mh1C/BmffB8pjKYl0iOlwmOgPMSBWbeBJmW3hjfk4YcgssKaa5n0J2aky8Tc
+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
+Pj8=
+-----END PRIVATE KEY-----
diff --git a/ml-kem/tests/examples/ML-KEM-768-expanded.priv b/ml-kem/tests/examples/ML-KEM-768-expanded.priv
new file mode 100644
index 0000000..9b2f13c
--- /dev/null
+++ b/ml-kem/tests/examples/ML-KEM-768-expanded.priv
@@ -0,0 +1,53 @@
+-----BEGIN PRIVATE KEY-----
+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
+JCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+Pw==
+-----END PRIVATE KEY-----
diff --git a/ml-kem/tests/examples/ML-KEM-768-seed.priv b/ml-kem/tests/examples/ML-KEM-768-seed.priv
new file mode 100644
index 0000000..4e7d5a0
--- /dev/null
+++ b/ml-kem/tests/examples/ML-KEM-768-seed.priv
@@ -0,0 +1,4 @@
+-----BEGIN PRIVATE KEY-----
+MFQCAQAwCwYJYIZIAWUDBAQCBEKAQAABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZ
+GhscHR4fICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8=
+-----END PRIVATE KEY-----
diff --git a/ml-kem/tests/examples/ML-KEM-768.pub b/ml-kem/tests/examples/ML-KEM-768.pub
new file mode 100644
index 0000000..36dddd6
--- /dev/null
+++ b/ml-kem/tests/examples/ML-KEM-768.pub
@@ -0,0 +1,28 @@
+-----BEGIN PUBLIC KEY-----
+MIIEsjALBglghkgBZQMEBAIDggShACmKoQ1CPI3aBp0CvFnmzfA6CWuLPaTKubgM
+pKFJB2cszvHsT68jSgvFt+nUc/KzEzs7JqHRdctnp4BZGWmcAvdlMbmcX4kYBwS7
+TKRTXFuJcmecZgoHxeUUuHAJyGLrj1FXaV77P8QKne9rgcHMAqJJrk8JStDZvTSF
+wcHGgIBSCnyMYyAyzuc4FU5cUXbAfaVgJHdqQw/nbqz2ZaP3uDIQIhW8gvEJOcg1
+VwQzao+sHYHkuwSFql18dNa1m75cXpcqDYusQRtVtdVVfNaAoaj3G064a8SMmgUJ
+cxpUvZ1ykLJ5Y+Q3Lcmxmc/crAsBrNKKYjlREuTENkjWIsSMgjTQFEDozDdskn8j
+pa/JrAR0xmInTkJFJchVLs47P+JlFt6QG8fVFb3olVjmJslcgLkzQvgBAATznmxs
+lIccXjRMqzlmyDX5qWpZr9McQChrOLHBp4RwurlHUYk0RTzoZzapGfH1ptUQqG9U
+VPw5gMtcdlvSvV97NrFBDWY1yM60fE3aDXaijqyTnHHDAkgEhmxxYmZYRCFjwsIh
+F+UKzvzmN4qYVlIwKk7wws4Mxxa3eW4ray43d9+hrD2iWaMbWptTD4y2OKgaYqww
+GEmrr5WnMBvaMAaJCb/bfmfbzLs4pVUaJbGjoPaFdIrVdT2IgPABbGJ0hhZjhMVX
+H+I2WQA2TQODEeLYdds2ZoaTK17GAkMKNp6Hpu9cM4eGZXglvUwFes65I+sJNeaQ
+XmO0ztf4CFenc91ksVDSZhLqmsEgUtsgF78YQ8y0sygbaQ3HKK36hcACgbjjwJKH
+M1+Fa0/CiS9povV5Ia2gGRTECYhmLVd2lmKnhjUbm2ZJPat5WU2YbeIQDWW6D/Tq
+WLgVONJKRDWiWPrCVASqf0H2WLE4UGXhWNy2ARVzJyD0BFmqrBXkBpU6kKxSmX0c
+zQcAYO/GXbnmUzVEZ/rVbscTyG51QMQjrPJmn1L6b0rGiI2HHvPoR8ApqKr7uS4X
+skqgebH0GbphdbRCr7EZCdSla3CgM1soc5IYqnyTSOLDwvPrPRWkHmQXwN2Uv+sh
+QZsxGnuxOhgLvoMyGKmmsXRHzIXyJYWVh6cwdwSay8/UTQ8CVDjhXRU4Jw1Ybhv4
+MZKpRZz2PA6XL4UpdnmDHs8SFQmFHLg0D28Qew+hoO/Rs2qBibwIXE9ct4TlU/Qb
+kY+AOXzhlW94W+43fKmqi+aZitowwmt8PYxrVSVMyWIDsgxCruCsTh67QI5JqeP4
+edCrB4XrcCVCXRMFoimcAV4SDRY7DhlJTOVyU9AkbRgnRcuBl6t0OLPBu3lyvsWj
+BuujVnhVwBRpn+9lrlTHcKDYXBhADPZCrtxmB3e6SxOFAr1aeBL2IfhKSClrmN1D
+IrbxWCi4qPDgCoukSlPDqLFDVxsHQKvVZ9rxzenHnCBLbV4lnRdmoxu7y05qBc9F
+AhdrMBwcL0Ekd1AVe87IXoCbMKTWDXdHzdD1uZqoyCaYdRd5OqqAgKCxJKhVjfcr
+vje3X07btr6CFtbGM/srIoDiURPYaV5DSBw+6zl+sZJQUim2eiAeqJPD4ssy2ovD
+QvpN6gV4
+-----END PUBLIC KEY-----