Skip to content

Commit 14d311c

Browse files
tarcierimeisterluk
andauthored
ml-kem: PKCS#8 support (#135)
This is #124, rebased on `master`, and modified to remove support for expanded decapsulation keys and the option to encode both the seed and the expanded decapsulation key, to discourage proliferation of these options which were added to sate HSM vendors who shipped expanded keys prior to the final FIPS 203 draft. We should not encourage the proliferation of their mistakes. Additionally, the encoding of PKCS#8 private keys now includes only the seed, making them significantly smaller. --------- Co-authored-by: Lukas PROKOP <[email protected]>
1 parent 1a6b6ca commit 14d311c

File tree

11 files changed

+584
-2
lines changed

11 files changed

+584
-2
lines changed

Cargo.lock

Lines changed: 53 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ml-kem/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ exclude = ["tests/key-gen.rs", "tests/key-gen.json", "tests/encap-decap.rs", "te
1717

1818
[features]
1919
deterministic = [] # Expose deterministic generation and encapsulation functions
20+
alloc = ["pkcs8?/alloc"]
2021
zeroize = ["dep:zeroize"]
22+
pkcs8 = ["dep:const-oid", "dep:pkcs8"]
23+
pem = ["pkcs8?/pem"]
2124

2225
[dependencies]
2326
kem = "0.3.0-pre.0"
@@ -26,6 +29,9 @@ rand_core = "0.9"
2629
sha3 = { version = "0.11.0-rc.0", default-features = false }
2730
subtle = { version = "2", default-features = false }
2831
zeroize = { version = "1.8.1", optional = true, default-features = false }
32+
pkcs8 = { version = "0.11.0-rc.4", optional = true, default-features = false }
33+
const-oid = { version = "0.10.1", optional = true, features = ["db"], default-features = false }
34+
der = { version = "0.8.0-rc.0", features = ["derive"] }
2935

3036
[dev-dependencies]
3137
criterion = "0.5.1"

ml-kem/src/kem.rs

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,17 @@ pub use ::kem::{Decapsulate, Encapsulate};
1919
/// A shared key resulting from an ML-KEM transaction
2020
pub(crate) type SharedKey = B32;
2121

22+
#[cfg(all(feature = "pkcs8", feature = "alloc"))]
23+
use pkcs8::der::{Encode, asn1::BitStringRef};
24+
#[cfg(feature = "pkcs8")]
25+
use {
26+
hybrid_array::Array,
27+
pkcs8::{
28+
der::{AnyRef, asn1::OctetStringRef},
29+
spki::AssociatedAlgorithmIdentifier,
30+
},
31+
};
32+
2233
/// A `DecapsulationKey` provides the ability to generate a new key pair, and decapsulate an
2334
/// encapsulated shared key.
2435
#[derive(Clone, Debug)]
@@ -57,6 +68,15 @@ where
5768
#[cfg(feature = "zeroize")]
5869
impl<P> ZeroizeOnDrop for DecapsulationKey<P> where P: KemParams {}
5970

71+
impl<P> From<Seed> for DecapsulationKey<P>
72+
where
73+
P: KemParams,
74+
{
75+
fn from(seed: Seed) -> Self {
76+
Self::from_seed(seed)
77+
}
78+
}
79+
6080
impl<P> EncodedSizeUser for DecapsulationKey<P>
6181
where
6282
P: KemParams,
@@ -270,6 +290,144 @@ where
270290
}
271291
}
272292

293+
/// The serialization of the private key is a choice between three different formats
294+
/// [according to PKCS#8](https://lamps-wg.github.io/kyber-certificates/draft-ietf-lamps-kyber-certificates.html#name-private-key-format).
295+
///
296+
/// “For ML-KEM private keys, the privateKey field in `OneAsymmetricKey`
297+
/// contains one of the following DER-encoded `CHOICE` structures.
298+
/// The seed format is a fixed 64-byte `OCTET STRING` (66 bytes total
299+
/// with the 0x8040 tag and length) for all security levels,
300+
/// while the expandedKey and both formats vary in size by security level”
301+
#[cfg(feature = "pkcs8")]
302+
#[derive(Clone, Debug, pkcs8::der::Choice)]
303+
pub enum PrivateKeyChoice<'o> {
304+
/// FIPS 203 format for an ML-KEM private key: a 64-octet seed
305+
#[asn1(tag_mode = "IMPLICIT", context_specific = "0")]
306+
Seed(OctetStringRef<'o>),
307+
}
308+
309+
#[cfg(feature = "pkcs8")]
310+
impl<P> AssociatedAlgorithmIdentifier for EncapsulationKey<P>
311+
where
312+
P: KemParams + AssociatedAlgorithmIdentifier<Params = AnyRef<'static>>,
313+
{
314+
type Params = P::Params;
315+
316+
const ALGORITHM_IDENTIFIER: pkcs8::spki::AlgorithmIdentifier<Self::Params> =
317+
P::ALGORITHM_IDENTIFIER;
318+
}
319+
320+
#[cfg(all(feature = "pkcs8", feature = "alloc"))]
321+
impl<P> pkcs8::EncodePublicKey for EncapsulationKey<P>
322+
where
323+
P: KemParams + AssociatedAlgorithmIdentifier<Params = AnyRef<'static>>,
324+
{
325+
/// Serialize the given `EncapsulationKey` into DER format.
326+
/// Returns a `Document` which wraps the DER document in case of success.
327+
fn to_public_key_der(&self) -> pkcs8::spki::Result<pkcs8::Document> {
328+
let public_key = self.as_bytes();
329+
let subject_public_key = BitStringRef::new(0, &public_key)?;
330+
331+
pkcs8::SubjectPublicKeyInfo {
332+
algorithm: P::ALGORITHM_IDENTIFIER,
333+
subject_public_key,
334+
}
335+
.try_into()
336+
}
337+
}
338+
339+
#[cfg(feature = "pkcs8")]
340+
impl<P> TryFrom<pkcs8::SubjectPublicKeyInfoRef<'_>> for EncapsulationKey<P>
341+
where
342+
P: KemParams + AssociatedAlgorithmIdentifier<Params = AnyRef<'static>>,
343+
{
344+
type Error = pkcs8::spki::Error;
345+
346+
/// Deserialize the encapsulation key from DER format found in `spki.subject_public_key`.
347+
/// Returns an `EncapsulationKey` containing `ek_{pke}` and `h` in case of success.
348+
fn try_from(spki: pkcs8::SubjectPublicKeyInfoRef<'_>) -> Result<Self, Self::Error> {
349+
if spki.algorithm.oid != P::ALGORITHM_IDENTIFIER.oid {
350+
return Err(pkcs8::spki::Error::OidUnknown {
351+
oid: P::ALGORITHM_IDENTIFIER.oid,
352+
});
353+
}
354+
355+
let bitstring_of_encapsulation_key = spki.subject_public_key;
356+
let enc_key = match bitstring_of_encapsulation_key.as_bytes() {
357+
Some(bytes) => {
358+
let arr: Array<u8, EncapsulationKeySize<P>> = match bytes.try_into() {
359+
Ok(array) => array,
360+
Err(_) => return Err(pkcs8::spki::Error::KeyMalformed),
361+
};
362+
EncryptionKey::from_bytes(&arr)
363+
}
364+
None => return Err(pkcs8::spki::Error::KeyMalformed),
365+
};
366+
367+
Ok(Self::new(enc_key))
368+
}
369+
}
370+
371+
#[cfg(feature = "pkcs8")]
372+
impl<P> AssociatedAlgorithmIdentifier for DecapsulationKey<P>
373+
where
374+
P: KemParams + AssociatedAlgorithmIdentifier<Params = AnyRef<'static>>,
375+
{
376+
type Params = P::Params;
377+
378+
const ALGORITHM_IDENTIFIER: pkcs8::spki::AlgorithmIdentifier<Self::Params> =
379+
P::ALGORITHM_IDENTIFIER;
380+
}
381+
382+
#[cfg(all(feature = "pkcs8", feature = "alloc"))]
383+
impl<P> pkcs8::EncodePrivateKey for DecapsulationKey<P>
384+
where
385+
P: KemParams + AssociatedAlgorithmIdentifier<Params = AnyRef<'static>>,
386+
{
387+
/// Serialize the given `DecapsulationKey` into DER format.
388+
/// Returns a `SecretDocument` which wraps the DER document in case of success.
389+
fn to_pkcs8_der(&self) -> pkcs8::Result<pkcs8::SecretDocument> {
390+
let decaps_key_bytes = self.to_seed().ok_or(pkcs8::Error::KeyMalformed)?;
391+
let pk_key_der =
392+
PrivateKeyChoice::Seed(OctetStringRef::new(decaps_key_bytes.as_slice())?).to_der()?;
393+
let pk_key_octetstr: OctetStringRef<'_> = OctetStringRef::new(&pk_key_der)?;
394+
395+
let private_key_info =
396+
pkcs8::PrivateKeyInfoRef::new(P::ALGORITHM_IDENTIFIER, pk_key_octetstr);
397+
pkcs8::SecretDocument::encode_msg(&private_key_info).map_err(pkcs8::Error::Asn1)
398+
}
399+
}
400+
401+
#[cfg(feature = "pkcs8")]
402+
impl<P> TryFrom<pkcs8::PrivateKeyInfoRef<'_>> for DecapsulationKey<P>
403+
where
404+
P: KemParams + AssociatedAlgorithmIdentifier<Params = AnyRef<'static>>,
405+
{
406+
type Error = pkcs8::Error;
407+
408+
/// Deserialize the decapsulation key from DER format found in `spki.private_key`.
409+
/// Returns a `DecapsulationKey` containing `dk_{pke}`, `ek`, and `z` in case of success.
410+
fn try_from(private_key_info_ref: pkcs8::PrivateKeyInfoRef<'_>) -> Result<Self, Self::Error> {
411+
private_key_info_ref
412+
.algorithm
413+
.assert_algorithm_oid(P::ALGORITHM_IDENTIFIER.oid)?;
414+
415+
let decaps_key = match private_key_info_ref
416+
.private_key
417+
.decode_into::<PrivateKeyChoice>()
418+
{
419+
Ok(PrivateKeyChoice::Seed(seed)) => Self::from_seed(
420+
seed.as_bytes()
421+
.try_into()
422+
.map_err(|_| pkcs8::Error::KeyMalformed)?,
423+
),
424+
Err(_) => return Err(pkcs8::Error::KeyMalformed),
425+
};
426+
427+
Ok(decaps_key)
428+
}
429+
}
430+
273431
#[cfg(test)]
274432
mod test {
275433
use super::*;

ml-kem/src/lib.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ pub use util::B32;
8080

8181
pub use param::{ArraySize, ParameterSet};
8282

83+
#[cfg(feature = "pkcs8")]
84+
pub use pkcs8::{self, AssociatedOid};
85+
8386
/// ML-KEM seeds are decapsulation (private) keys, which are consistently 64-bytes across all
8487
/// security levels, and are the preferred serialization for representing such keys.
8588
pub type Seed = Array<u8, U64>;
@@ -168,6 +171,22 @@ impl ParameterSet for MlKem512Params {
168171
type Dv = U4;
169172
}
170173

174+
#[cfg(feature = "pkcs8")]
175+
impl AssociatedOid for MlKem512Params {
176+
const OID: pkcs8::ObjectIdentifier = const_oid::db::fips203::ID_ALG_ML_KEM_512;
177+
}
178+
179+
#[cfg(feature = "pkcs8")]
180+
impl pkcs8::spki::AssociatedAlgorithmIdentifier for MlKem512Params {
181+
type Params = pkcs8::der::AnyRef<'static>;
182+
183+
const ALGORITHM_IDENTIFIER: pkcs8::spki::AlgorithmIdentifier<Self::Params> =
184+
pkcs8::spki::AlgorithmIdentifier {
185+
oid: Self::OID,
186+
parameters: None,
187+
};
188+
}
189+
171190
/// `MlKem768` is the parameter set for security category 3, corresponding to key search on a block
172191
/// cipher with a 192-bit key.
173192
#[derive(Default, Clone, Debug, PartialEq)]
@@ -181,6 +200,22 @@ impl ParameterSet for MlKem768Params {
181200
type Dv = U4;
182201
}
183202

203+
#[cfg(feature = "pkcs8")]
204+
impl AssociatedOid for MlKem768Params {
205+
const OID: pkcs8::ObjectIdentifier = const_oid::db::fips203::ID_ALG_ML_KEM_768;
206+
}
207+
208+
#[cfg(feature = "pkcs8")]
209+
impl pkcs8::spki::AssociatedAlgorithmIdentifier for MlKem768Params {
210+
type Params = pkcs8::der::AnyRef<'static>;
211+
212+
const ALGORITHM_IDENTIFIER: pkcs8::spki::AlgorithmIdentifier<Self::Params> =
213+
pkcs8::spki::AlgorithmIdentifier {
214+
oid: Self::OID,
215+
parameters: None,
216+
};
217+
}
218+
184219
/// `MlKem1024` is the parameter set for security category 5, corresponding to key search on a block
185220
/// cipher with a 256-bit key.
186221
#[derive(Default, Clone, Debug, PartialEq)]
@@ -194,6 +229,22 @@ impl ParameterSet for MlKem1024Params {
194229
type Dv = U5;
195230
}
196231

232+
#[cfg(feature = "pkcs8")]
233+
impl AssociatedOid for MlKem1024Params {
234+
const OID: pkcs8::ObjectIdentifier = const_oid::db::fips203::ID_ALG_ML_KEM_1024;
235+
}
236+
237+
#[cfg(feature = "pkcs8")]
238+
impl pkcs8::spki::AssociatedAlgorithmIdentifier for MlKem1024Params {
239+
type Params = pkcs8::der::AnyRef<'static>;
240+
241+
const ALGORITHM_IDENTIFIER: pkcs8::spki::AlgorithmIdentifier<Self::Params> =
242+
pkcs8::spki::AlgorithmIdentifier {
243+
oid: Self::OID,
244+
parameters: None,
245+
};
246+
}
247+
197248
/// A shared key produced by the KEM `K`
198249
pub type SharedKey<K> = Array<u8, <K as KemCore>::SharedKeySize>;
199250

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MFQCAQAwCwYJYIZIAWUDBAQDBEKAQAABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZ
3+
GhscHR4fICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8=
4+
-----END PRIVATE KEY-----

0 commit comments

Comments
 (0)