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); +}