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
2 changes: 1 addition & 1 deletion ml-kem/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ exclude = ["tests/key-gen.rs", "tests/key-gen.json", "tests/encap-decap.rs", "te
[features]
alloc = ["pkcs8?/alloc"]

deterministic = [] # Expose deterministic encapsulation functions
getrandom = ["kem/getrandom"]
pem = ["pkcs8/pem"]
pkcs8 = ["dep:const-oid", "dep:pkcs8"]
zeroize = ["dep:zeroize"]
hazmat = []

[dependencies]
array = { package = "hybrid-array", version = "0.4.4", features = ["extra-sizes", "subtle"] }
Expand Down
11 changes: 11 additions & 0 deletions ml-kem/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@ In summary, ML-KEM stands at the forefront of post-quantum cryptography, offerin
and efficiency in key encapsulation mechanisms to safeguard sensitive communications in an era where
quantum computers potentially pose a looming threat.

## Features

The following features are provided by this crate:
* `zeroize` — Enables memory zeroing for all cryptographic secrets
* `pkcs8` — Enables PKCS#8 encoding/decoding traits for encapsulation and decapsulation key types
* `alloc` — Enables allocating PKCS#8 encoding functions
* `pem` — Enables PEM encoding/decoding support for PKCS#8 keys
* `hazmat` — Enables `EncapsulationKey::encapsulate_deterministic`. Useful for testing purposes. Do NOT enable unless you know what you are doing.

The **default** features are `[]` (nothing).

## ⚠️ Security Warning

The implementation contained in this crate has never been independently audited!
Expand Down
27 changes: 8 additions & 19 deletions ml-kem/src/kem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ use core::marker::PhantomData;
use rand_core::{CryptoRng, TryCryptoRng, TryRngCore};
use subtle::{ConditionallySelectable, ConstantTimeEq};

#[cfg(feature = "deterministic")]
use core::convert::Infallible;
#[cfg(feature = "zeroize")]
use zeroize::{Zeroize, ZeroizeOnDrop};

Expand Down Expand Up @@ -248,7 +246,13 @@ where
Self { ek_pke, h }
}

fn encapsulate_deterministic_inner(&self, m: &B32) -> (EncodedCiphertext<P>, SharedKey) {
/// Encapsulates with the given randomness. This is useful for testing against known vectors.
///
/// # Warning
/// Do NOT use this function unless you know what you're doing. If you fail to use all uniform
/// random bytes even once, you can have catastrophic security failure.
#[cfg_attr(not(feature = "hazmat"), doc(hidden))]
pub fn encapsulate_deterministic(&self, m: &B32) -> (EncodedCiphertext<P>, SharedKey) {
let (K, r) = G(&[m, &self.h]);
let c = self.ek_pke.encrypt(m, &r);
(c, K)
Expand All @@ -264,7 +268,7 @@ where
rng: &mut R,
) -> Result<(EncodedCiphertext<P>, SharedKey), R::Error> {
let m = B32::try_generate_from_rng(rng)?;
Ok(self.encapsulate_deterministic_inner(&m))
Ok(self.encapsulate_deterministic(&m))
}
}

Expand Down Expand Up @@ -329,21 +333,6 @@ where
}
}

#[cfg(feature = "deterministic")]
impl<P> crate::EncapsulateDeterministic<EncodedCiphertext<P>, SharedKey> for EncapsulationKey<P>
where
P: KemParams,
{
type Error = Infallible;

fn encapsulate_deterministic(
&self,
m: &B32,
) -> Result<(EncodedCiphertext<P>, SharedKey), Self::Error> {
Ok(self.encapsulate_deterministic_inner(m))
}
}

/// An implementation of overall ML-KEM functionality. Generic over parameter sets, but then ties
/// together all of the other related types and sizes.
#[derive(Clone)]
Expand Down
3 changes: 0 additions & 3 deletions ml-kem/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,6 @@ pub use ml_kem_1024::MlKem1024Params;
pub use param::{ArraySize, ExpandedDecapsulationKey, ParameterSet};
pub use traits::*;

#[cfg(feature = "deterministic")]
pub use util::B32;

use array::{
Array,
typenum::{U2, U3, U4, U5, U10, U11, U64},
Expand Down
18 changes: 0 additions & 18 deletions ml-kem/src/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ use core::fmt::Debug;
use kem::{Decapsulate, Encapsulate, InvalidKey};
use rand_core::CryptoRng;

#[cfg(feature = "deterministic")]
use crate::B32;

/// An object that knows what size it is
pub trait EncodedSizeUser: Sized {
/// The size of an encoded object
Expand All @@ -27,21 +24,6 @@ pub trait EncodedSizeUser: Sized {
/// A byte array encoding a value the indicated size
pub type Encoded<T> = Array<u8, <T as EncodedSizeUser>::EncodedSize>;

/// A value that can be encapsulated to. Note that this interface is not safe: In order for the
/// KEM to be secure, the `m` input must be randomly generated.
#[cfg(feature = "deterministic")]
pub trait EncapsulateDeterministic<EK, SS> {
/// Encapsulation error
type Error: Debug;

/// Encapsulates a fresh shared secret.
///
/// # Errors
///
/// Will vary depending on the underlying implementation.
fn encapsulate_deterministic(&self, m: &B32) -> Result<(EK, SS), Self::Error>;
}

/// A generic interface to a Key Encapsulation Method
pub trait KemCore: Clone {
/// The size of a shared key generated by this KEM
Expand Down
35 changes: 30 additions & 5 deletions ml-kem/tests/encap-decap.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,36 @@
#![cfg(feature = "deterministic")]

use ml_kem::*;

use ::kem::Decapsulate;
use array::Array;
use array::{Array, ArrayN};
use std::{fs::read_to_string, path::PathBuf};

// A helper trait for deterministic encapsulation tests
pub trait EncapsulateDeterministic {
// Returns (ciphertext, shared_secret)
fn encapsulate_deterministic(&self, m: &ArrayN<u8, 32>) -> (Vec<u8>, Vec<u8>);
}

impl EncapsulateDeterministic for EncapsulationKey512 {
fn encapsulate_deterministic(&self, m: &ArrayN<u8, 32>) -> (Vec<u8>, Vec<u8>) {
let (c, k) = self.encapsulate_deterministic(m);
(c.to_vec(), k.to_vec())
}
}

impl EncapsulateDeterministic for EncapsulationKey768 {
fn encapsulate_deterministic(&self, m: &ArrayN<u8, 32>) -> (Vec<u8>, Vec<u8>) {
let (c, k) = self.encapsulate_deterministic(m);
(c.to_vec(), k.to_vec())
}
}

impl EncapsulateDeterministic for EncapsulationKey1024 {
fn encapsulate_deterministic(&self, m: &ArrayN<u8, 32>) -> (Vec<u8>, Vec<u8>) {
let (c, k) = self.encapsulate_deterministic(m);
(c.to_vec(), k.to_vec())
}
}

#[test]
fn acvp_encap_decap() {
// Load the JSON test file
Expand Down Expand Up @@ -38,13 +63,13 @@ fn verify_encap_group(tg: &acvp::EncapTestGroup) {
fn verify_encap<K>(tc: &acvp::EncapTestCase)
where
K: KemCore,
K::EncapsulationKey: EncapsulateDeterministic<Ciphertext<K>, SharedKey<K>>,
K::EncapsulationKey: EncapsulateDeterministic,
{
let m = Array::try_from(tc.m.as_slice()).unwrap();
let ek_bytes = Encoded::<K::EncapsulationKey>::try_from(tc.ek.as_slice()).unwrap();
let ek = K::EncapsulationKey::from_encoded_bytes(&ek_bytes).unwrap();

let (c, k) = ek.encapsulate_deterministic(&m).unwrap();
let (c, k) = ek.encapsulate_deterministic(&m);

assert_eq!(k.as_slice(), tc.k.as_slice());
assert_eq!(c.as_slice(), tc.c.as_slice());
Expand Down
8 changes: 3 additions & 5 deletions ml-kem/tests/key-gen.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
#![cfg(feature = "deterministic")]

use ml_kem::*;

use array::Array;
use array::ArrayN;
use std::{fs::read_to_string, path::PathBuf};

#[test]
Expand All @@ -29,8 +27,8 @@ fn acvp_key_gen() {

fn verify<K: KemCore>(tc: &acvp::TestCase) {
// Import test data into the relevant array structures
let d: B32 = Array::try_from(tc.d.as_slice()).unwrap();
let z: B32 = Array::try_from(tc.z.as_slice()).unwrap();
let d = ArrayN::<u8, 32>::try_from(tc.d.as_slice()).unwrap();
let z = ArrayN::<u8, 32>::try_from(tc.z.as_slice()).unwrap();
let dk_bytes = Encoded::<K::DecapsulationKey>::try_from(tc.dk.as_slice()).unwrap();
let ek_bytes = Encoded::<K::EncapsulationKey>::try_from(tc.ek.as_slice()).unwrap();

Expand Down
6 changes: 4 additions & 2 deletions x-wing/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ keywords = ["crypto", "x-wing", "xwing", "kem", "post-quantum"]
exclude = ["src/test-vectors.json"]

[features]
default = []
getrandom = ["kem/getrandom"]
zeroize = ["dep:zeroize", "ml-kem/zeroize", "x25519-dalek/zeroize"]
hazmat = []

[dependencies]
kem = "0.3.0-rc.0"
ml-kem = { version = "=0.3.0-pre.4", default-features = false, features = ["deterministic"] }
ml-kem = { version = "=0.3.0-pre.4", default-features = false, features = ["hazmat"] }
rand_core = { version = "0.10.0-rc-5", default-features = false }
sha3 = { version = "0.11.0-rc.3", default-features = false }
x25519-dalek = { version = "=3.0.0-pre.4", default-features = false, features = ["static_secrets"] }
Expand All @@ -35,4 +37,4 @@ serde_json = "1.0"

[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
rustdoc-args = ["--cfg", "docsrs"]
9 changes: 8 additions & 1 deletion x-wing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,14 @@ The original paper: [X-Wing The Hybrid KEM You’ve Been Looking For][X-WING-PAP

[Documentation][docs-link]

## About
## Features

The following features are provided by this crate:
* `getrandom` — Enables `generate_key_pair` (generate without an explicit RNG)
* `zeroize` — Enables memory zeroing for all cryptographic secrets
* `hazmat` — Enables `EncapsulationKey::encapsulate_deterministic`. Useful for testing purposes. Do NOT enable unless you know what you are doing.

The **default** features are `[]` (nothing).

## ⚠️ Security Warning

Expand Down
68 changes: 48 additions & 20 deletions x-wing/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,18 @@ pub use kem::{
};

use ml_kem::{
B32, EncodedSizeUser, KemCore, MlKem768, MlKem768Params,
EncodedSizeUser, KemCore, MlKem768, MlKem768Params,
array::{
Array, ArrayN, AsArrayRef,
sizes::{U32, U1120, U1216},
sizes::{U32, U1120, U1184, U1216},
},
};
use rand_core::{CryptoRng, TryCryptoRng, TryRngCore};
use sha3::{
Sha3_256, Shake256, Shake256Reader,
digest::{ExtendableOutput, XofReader},
};
use x25519_dalek::{EphemeralSecret, PublicKey, StaticSecret};
use x25519_dalek::{PublicKey, StaticSecret};

#[cfg(feature = "zeroize")]
use zeroize::{Zeroize, ZeroizeOnDrop};
Expand All @@ -58,6 +58,8 @@ pub const ENCAPSULATION_KEY_SIZE: usize = 1216;
pub const DECAPSULATION_KEY_SIZE: usize = 32;
/// Size in bytes of the `Ciphertext`.
pub const CIPHERTEXT_SIZE: usize = 1120;
/// Number of bytes necessary to encapsulate a key
pub const ENCAPSULATION_RANDOMNESS_SIZE: usize = 64;

/// Serialized ciphertext.
pub type Ciphertext = Array<u8, U1120>;
Expand All @@ -81,23 +83,52 @@ pub struct EncapsulationKey {
pk_x: PublicKey,
}

impl Encapsulate for EncapsulationKey {
fn encapsulate_with_rng<R: TryCryptoRng + ?Sized>(
impl EncapsulationKey {
/// Encapsulates with the given randomness. Uses the first 32 bytes for ML-KEM and the last 32
/// bytes for x25519. This is useful for testing against known vectors.
///
/// # Warning
/// Do NOT use this function unless you know what you're doing. If you fail to use all uniform
/// random bytes even once, you can have catastrophic security failure.
#[cfg_attr(not(feature = "hazmat"), doc(hidden))]
#[expect(clippy::must_use_candidate)]
pub fn encapsulate_deterministic(
&self,
rng: &mut R,
) -> Result<(Ciphertext, SharedSecret), R::Error> {
// Swapped order of operations compared to RFC, so that usage of the rng matches the RFC
let (ct_m, ss_m) = self.pk_m.encapsulate_with_rng(rng)?;
randomness: &ArrayN<u8, ENCAPSULATION_RANDOMNESS_SIZE>,
) -> (Ciphertext, SharedSecret) {
// Split randomness into two 32-byte arrays
let (rand_m, rand_x) = randomness.split::<U32>();

// Encapsulate with ML-KEM first. This is infallible
let (ct_m, ss_m) = self.pk_m.encapsulate_deterministic(&rand_m);

let ek_x = EphemeralSecret::random_from_rng(&mut rng.unwrap_err());
let ek_x = StaticSecret::from(rand_x.0);
// Equal to ct_x = x25519(ek_x, BASE_POINT)
let ct_x = PublicKey::from(&ek_x);
// Equal to ss_x = x25519(ek_x, pk_x)
let ss_x = ek_x.diffie_hellman(&self.pk_x);

let ss = combiner(&ss_m, &ss_x, &ct_x, &self.pk_x);
let ct = CiphertextMessage { ct_m, ct_x };
Ok((ct.into(), ss))

(ct.into(), ss)
}
}

impl Encapsulate for EncapsulationKey {
fn encapsulate_with_rng<R: TryCryptoRng + ?Sized>(
&self,
rng: &mut R,
) -> Result<(Ciphertext, SharedSecret), R::Error> {
let mut randomness = Array::default();
rng.try_fill_bytes(&mut randomness)?;

let res = self.encapsulate_deterministic(&randomness);

#[cfg(feature = "zeroize")]
randomness.zeroize();

Ok(res)
}
}

Expand All @@ -122,14 +153,11 @@ impl KeyExport for EncapsulationKey {

impl TryKeyInit for EncapsulationKey {
fn new(key_bytes: &Key<Self>) -> Result<Self, InvalidKey> {
let mut pk_m = [0; 1184];
pk_m.copy_from_slice(&key_bytes[0..1184]);
let pk_m =
MlKem768EncapsulationKey::from_encoded_bytes(&pk_m.into()).map_err(|_| InvalidKey)?;

let mut pk_x = [0; 32];
pk_x.copy_from_slice(&key_bytes[1184..]);
let pk_x = PublicKey::from(pk_x);
let (m_bytes, x_bytes) = key_bytes.split_ref::<U1184>();

let pk_m = MlKem768EncapsulationKey::from_encoded_bytes(m_bytes).map_err(|_| InvalidKey)?;
let pk_x = PublicKey::from(x_bytes.0);

Ok(EncapsulationKey { pk_m, pk_x })
}
}
Expand Down Expand Up @@ -306,7 +334,7 @@ pub fn generate_key_pair_from_rng<R: CryptoRng + ?Sized>(
}

fn combiner(
ss_m: &B32,
ss_m: &ArrayN<u8, 32>,
ss_x: &x25519_dalek::SharedSecret,
ct_x: &PublicKey,
pk_x: &PublicKey,
Expand Down