Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 53 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions ml-kem/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down
158 changes: 158 additions & 0 deletions ml-kem/src/kem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -57,6 +68,15 @@ where
#[cfg(feature = "zeroize")]
impl<P> ZeroizeOnDrop for DecapsulationKey<P> where P: KemParams {}

impl<P> From<Seed> for DecapsulationKey<P>
where
P: KemParams,
{
fn from(seed: Seed) -> Self {
Self::from_seed(seed)
}
}

impl<P> EncodedSizeUser for DecapsulationKey<P>
where
P: KemParams,
Expand Down Expand Up @@ -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<P> AssociatedAlgorithmIdentifier for EncapsulationKey<P>
where
P: KemParams + AssociatedAlgorithmIdentifier<Params = AnyRef<'static>>,
{
type Params = P::Params;

const ALGORITHM_IDENTIFIER: pkcs8::spki::AlgorithmIdentifier<Self::Params> =
P::ALGORITHM_IDENTIFIER;
}

#[cfg(all(feature = "pkcs8", feature = "alloc"))]
impl<P> pkcs8::EncodePublicKey for EncapsulationKey<P>
where
P: KemParams + AssociatedAlgorithmIdentifier<Params = AnyRef<'static>>,
{
/// 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<pkcs8::Document> {
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<P> TryFrom<pkcs8::SubjectPublicKeyInfoRef<'_>> for EncapsulationKey<P>
where
P: KemParams + AssociatedAlgorithmIdentifier<Params = AnyRef<'static>>,
{
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<Self, Self::Error> {
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<u8, EncapsulationKeySize<P>> = 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<P> AssociatedAlgorithmIdentifier for DecapsulationKey<P>
where
P: KemParams + AssociatedAlgorithmIdentifier<Params = AnyRef<'static>>,
{
type Params = P::Params;

const ALGORITHM_IDENTIFIER: pkcs8::spki::AlgorithmIdentifier<Self::Params> =
P::ALGORITHM_IDENTIFIER;
}

#[cfg(all(feature = "pkcs8", feature = "alloc"))]
impl<P> pkcs8::EncodePrivateKey for DecapsulationKey<P>
where
P: KemParams + AssociatedAlgorithmIdentifier<Params = AnyRef<'static>>,
{
/// 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<pkcs8::SecretDocument> {
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<P> TryFrom<pkcs8::PrivateKeyInfoRef<'_>> for DecapsulationKey<P>
where
P: KemParams + AssociatedAlgorithmIdentifier<Params = AnyRef<'static>>,
{
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<Self, Self::Error> {
private_key_info_ref
.algorithm
.assert_algorithm_oid(P::ALGORITHM_IDENTIFIER.oid)?;

let decaps_key = match private_key_info_ref
.private_key
.decode_into::<PrivateKeyChoice>()
{
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::*;
Expand Down
51 changes: 51 additions & 0 deletions ml-kem/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u8, U64>;
Expand Down Expand Up @@ -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<Self::Params> =
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)]
Expand All @@ -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<Self::Params> =
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)]
Expand All @@ -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<Self::Params> =
pkcs8::spki::AlgorithmIdentifier {
oid: Self::OID,
parameters: None,
};
}

/// A shared key produced by the KEM `K`
pub type SharedKey<K> = Array<u8, <K as KemCore>::SharedKeySize>;

Expand Down
4 changes: 4 additions & 0 deletions ml-kem/tests/examples/ML-KEM-1024-seed.priv
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-----BEGIN PRIVATE KEY-----
MFQCAQAwCwYJYIZIAWUDBAQDBEKAQAABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZ
GhscHR4fICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8=
-----END PRIVATE KEY-----
Loading