diff --git a/Cargo.lock b/Cargo.lock
index 7f4a9c3..56b78d6 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"
@@ -855,13 +874,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",
@@ -975,6 +997,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"
@@ -987,6 +1018,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 4b2937e..02cd917 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"
@@ -26,6 +29,9 @@ rand_core = "0.9"
sha3 = { version = "0.11.0-rc.0", default-features = false }
subtle = { version = "2", 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 8f105d1..ff9e0db 100644
--- a/ml-kem/src/kem.rs
+++ b/ml-kem/src/kem.rs
@@ -19,6 +19,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)]
@@ -57,6 +68,15 @@ where
#[cfg(feature = "zeroize")]
impl
ZeroizeOnDrop for DecapsulationKey
where P: KemParams {}
+impl
From for DecapsulationKey
+where
+ P: KemParams,
+{
+ fn from(seed: Seed) -> Self {
+ Self::from_seed(seed)
+ }
+}
+
impl
EncodedSizeUser for DecapsulationKey
where
P: KemParams,
@@ -270,6 +290,144 @@ 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 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>),
+}
+
+#[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 = self.to_seed().ok_or(pkcs8::Error::KeyMalformed)?;
+ let pk_key_der =
+ PrivateKeyChoice::Seed(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 {
+ private_key_info_ref
+ .algorithm
+ .assert_algorithm_oid(P::ALGORITHM_IDENTIFIER.oid)?;
+
+ let decaps_key = match private_key_info_ref
+ .private_key
+ .decode_into::()
+ {
+ Ok(PrivateKeyChoice::Seed(seed)) => Self::from_seed(
+ seed.as_bytes()
+ .try_into()
+ .map_err(|_| pkcs8::Error::KeyMalformed)?,
+ ),
+ 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 e77ac68..8a866a3 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};
+
/// ML-KEM seeds are decapsulation (private) keys, which are consistently 64-bytes across all
/// security levels, and are the preferred serialization for representing such keys.
pub type Seed = Array;
@@ -168,6 +171,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)]
@@ -181,6 +200,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)]
@@ -194,6 +229,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>;
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-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-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-----
diff --git a/ml-kem/tests/pkcs8.rs b/ml-kem/tests/pkcs8.rs
new file mode 100644
index 0000000..290ee20
--- /dev/null
+++ b/ml-kem/tests/pkcs8.rs
@@ -0,0 +1,220 @@
+//! PKCS#8 tests.
+
+#![cfg(all(feature = "pkcs8", feature = "alloc"))]
+
+use {
+ ml_kem::{
+ EncodedSizeUser, KemCore, MlKem512, MlKem768, MlKem1024, Seed, kem::PrivateKeyChoice,
+ },
+ pkcs8::{
+ der::{self, Decode},
+ {
+ DecodePrivateKey, DecodePublicKey, EncodePrivateKey, EncodePublicKey,
+ PrivateKeyInfoRef, SubjectPublicKeyInfoRef,
+ },
+ },
+ rand_core::CryptoRng,
+};
+
+fn der_serialization_and_deserialization(expected_encaps_len: u32)
+where
+ K: KemCore,
+ K::EncapsulationKey: EncodePublicKey + DecodePublicKey,
+ K::DecapsulationKey: EncodePrivateKey + DecodePrivateKey + From + PartialEq,
+{
+ 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();
+ let expected_decaps_len = 64 + 22; // 22-byte PKCS#8 header
+ 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::Seed(seed)) = priv_key.private_key.decode_into() {
+ let seed = Seed::try_from(seed.as_bytes()).unwrap();
+ assert_eq!(decaps_key, K::DecapsulationKey::from(seed));
+ } else {
+ core::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);
+ }
+}
+
+#[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.
+ der_serialization_and_deserialization::(822);
+ der_serialization_and_deserialization::(1206);
+ der_serialization_and_deserialization::(1590);
+}
+
+#[cfg(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(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!("examples/ML-KEM-512.pub");
+ const PEM_768_PUB: &str = include_str!("examples/ML-KEM-768.pub");
+ const PEM_1024_PUB: &str = include_str!("examples/ML-KEM-1024.pub");
+ const PEM_512_PRIV: &str = include_str!("examples/ML-KEM-512-seed.priv");
+ const PEM_768_PRIV: &str = include_str!("examples/ML-KEM-768-seed.priv");
+ const PEM_1024_PRIV: &str = include_str!("examples/ML-KEM-1024-seed.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(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!("examples/ML-KEM-512-seed.priv");
+ const PEM_768_SEED: &str = include_str!("examples/ML-KEM-768-seed.priv");
+ const PEM_1024_SEED: &str = include_str!("examples/ML-KEM-1024-seed.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) => &seed.as_bytes()[0..length],
+ };
+
+ assert_eq!(given_prefix, expected_seed_prefix);
+ }
+
+ const STATIC_SEED_PREFIX: &[u8] =
+ &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17];
+
+ expect_seed_bytes(PEM_512_SEED, STATIC_SEED_PREFIX);
+ expect_seed_bytes(PEM_768_SEED, STATIC_SEED_PREFIX);
+ expect_seed_bytes(PEM_1024_SEED, STATIC_SEED_PREFIX);
+}