Skip to content
Draft
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
4 changes: 4 additions & 0 deletions curve25519/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,16 @@ libcrux-hacl-rs = { version = "=0.0.3", path = "../hacl-rs/" }
libcrux-macros = { version = "=0.0.3", path = "../macros" }
libcrux-traits = { version = "=0.0.3", path = "../traits" }
libcrux-secrets = { version = "=0.0.3", path = "../secrets"}
libcrux-hkdf = { version = "=0.0.3", path = "../libcrux-hkdf", optional = true }

[features]
kem-api = ["libcrux-hkdf"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have naming convention for these? It could also just be kem. I'm fine either way.


# Checking secret independence
check-secret-independence = ["libcrux-secrets/check-secret-independence"]

[dev-dependencies]
libcrux-traits = { version = "0.0.3", path = "../traits", features = [
"generic-tests",
] }
rand = { version = "0.9", features = ["thread_rng"] }
151 changes: 151 additions & 0 deletions curve25519/src/kem_api.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
//! This module implements a DH-Based KEM (DHKEM) based on RFC 9180.
//!
//! The DHKEM uses a fixed KDF (HKDF-SHA2_256) and generates shared
//! secrets of lenght `L` using the following domain separation values:
//! - `extract_label = "eae_prk"`
//! - `expand_label = "shared_secret"`
//! - `suite_id = "KEM" | I2OSP(kem_id,2)"`, where `kem_id = 0x0020` is
//! the KEM ID associated with DHKEM(X25519,HKDF-SHA256) in RFC 9180.
//! - `ikm_label = "HPKE-v1" | suite_id | extract_label`
//! - `expand_label = I2OSP(L,2) | "HPKE-v1" | suite_id | expand_label`
//!
//! The module does not implement the AuthKEM functionality defined in RFC
//! 9180.
//!
//! Usage:
//! ```
//! use rand::RngCore;
//! use libcrux_curve25519::*;
//! use libcrux_curve25519::kem_api::*;
//!
//! let mut rng = rand::rng();
//!
//! // Responder key generation
//! let mut pk = [0u8; EK_LEN];
//! let mut sk = [0u8; DK_LEN];
//! let mut rand_keygen = [0u8; DK_LEN];
//!
//! rng.fill_bytes(&mut rand_keygen);
//!
//! X25519::keygen(&mut pk, &mut sk, &rand_keygen).unwrap();
//!
//! // Encapsulation
//! let mut rand_encaps = [0u8; DK_LEN];
//! rng.fill_bytes(&mut rand_encaps);
//!
//! let mut enc = [0u8; EK_LEN];
//! let mut shared_secret_initiator = [0u8; SHARED_SECRET_LEN];
//! X25519::encaps(&mut enc, &mut shared_secret_initiator, &pk, &rand_encaps).unwrap();
//!
//! // Decapsulation
//! let mut shared_secret_responder = [0u8; SHARED_SECRET_LEN];
//! X25519::decaps(&mut shared_secret_responder, &enc, &sk).unwrap();
//!
//! assert_eq!(shared_secret_initiator, shared_secret_responder);
//!
//! ```
use super::{DK_LEN, EK_LEN, X25519};
use libcrux_hkdf::HkdfMode;
use libcrux_secrets::{ClassifyRef as _, ClassifyRefMut as _};
pub use libcrux_traits::kem::arrayref::Kem;
use libcrux_traits::{
ecdh::arrayref::EcdhArrayref,
kem::arrayref::{DecapsError, EncapsError, KeyGenError},
};

/// An internal error that occurs during DHKEM operations.
enum DhKemError {
Hkdf,
}

pub const SHARED_SECRET_LEN: usize = 32;

impl Kem<DK_LEN, EK_LEN, EK_LEN, SHARED_SECRET_LEN, DK_LEN, DK_LEN> for X25519 {
fn keygen(
ek: &mut [u8; DK_LEN],
dk: &mut [u8; EK_LEN],
rand: &[u8; DK_LEN],
) -> Result<(), KeyGenError> {
X25519::generate_pair(ek, dk.classify_ref_mut(), rand.classify_ref())
.map_err(|_| KeyGenError::InvalidRandomness)
}

fn encaps(
ct: &mut [u8; EK_LEN],
ss: &mut [u8; SHARED_SECRET_LEN],
ek: &[u8; EK_LEN],
rand: &[u8; DK_LEN],
) -> Result<(), EncapsError> {
let mut ephemeral_secret = [0u8; DK_LEN];

X25519::generate_pair(ct, ephemeral_secret.classify_ref_mut(), rand.classify_ref())
.map_err(|_| EncapsError::InvalidRandomness)?;

let mut derived_ecdh = [0u8; EK_LEN];
X25519::derive_ecdh(
derived_ecdh.classify_ref_mut(),
ek,
ephemeral_secret.classify_ref(),
)
.map_err(|_| EncapsError::InvalidEncapsKey)?;

extract_and_expand(ct, ss, ek, derived_ecdh).map_err(|_| EncapsError::Unknown)
}

fn decaps(
ss: &mut [u8; SHARED_SECRET_LEN],
ct: &[u8; DK_LEN],
dk: &[u8; EK_LEN],
) -> Result<(), DecapsError> {
let mut derived_ecdh = [0u8; EK_LEN];
X25519::derive_ecdh(derived_ecdh.classify_ref_mut(), ct, dk.classify_ref())
.map_err(|_| DecapsError::InvalidCiphertext)?;

let mut ek = [0u8; EK_LEN];
X25519::secret_to_public(&mut ek, dk.classify_ref())
.map_err(|_| DecapsError::InvalidDecapsKey)?;

extract_and_expand(ct, ss, &ek, derived_ecdh).map_err(|_| DecapsError::Unknown)
}
}

fn extract_and_expand<const SHARED_SECRET_LEN: usize>(
ct: &[u8; 32],
ss: &mut [u8; SHARED_SECRET_LEN],
ek: &[u8; 32],
derived_ecdh: [u8; 32],
) -> Result<(), DhKemError> {
debug_assert!(SHARED_SECRET_LEN <= u16::MAX as usize);

let mut kem_context = [0u8; 2 * EK_LEN];
kem_context[..EK_LEN].copy_from_slice(ct.as_slice());
kem_context[EK_LEN..].copy_from_slice(ek.as_slice());

// `YY` marks bytes reserved for KEM ID
let ikm_label = b"HPKE-v1YYeae_prk";
let mut labeled_ikm = [0u8; EK_LEN + 16];
labeled_ikm[..ikm_label.len()].copy_from_slice(ikm_label.as_slice());
// Fill in KEM ID
labeled_ikm[7..9].copy_from_slice(&[0x00, 0x20]);
labeled_ikm[ikm_label.len()..].copy_from_slice(derived_ecdh.as_slice());

let mut eae_prk = [0u8; 32];
libcrux_hkdf::HkdfSha2_256::extract(&mut eae_prk, &[], &labeled_ikm)
.map_err(|_| DhKemError::Hkdf)?;

// `XX` marks bytes reserved for I2OSP(SHARED_SECRET_LEN, 2)
// `YY` marks bytes reserved for KEM ID
let info_label = b"YYHPKE-v1XXshared_secret";
let mut labeled_info = [0u8; 2 * EK_LEN + 24];
labeled_info[..info_label.len()].copy_from_slice(info_label.as_slice());
// Fill in I2OSP(SHARED_SECRET_LEN, 2)
labeled_info[0..2].copy_from_slice(&(SHARED_SECRET_LEN as u16).to_be_bytes());
// Fill in KEM ID
labeled_info[9..11].copy_from_slice(&[0x00, 0x20]);
labeled_info[info_label.len()..].copy_from_slice(kem_context.as_slice());

libcrux_hkdf::HkdfSha2_256::expand(ss, eae_prk.as_slice(), &labeled_info)
.map_err(|_| DhKemError::Hkdf)
}

libcrux_traits::kem::slice::impl_trait!(X25519 => EK_LEN, DK_LEN, EK_LEN, SHARED_SECRET_LEN, DK_LEN, DK_LEN);
39 changes: 3 additions & 36 deletions curve25519/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ mod impl_hacl;

pub mod ecdh_api;

#[cfg(feature = "kem-api")]
pub mod kem_api;

pub use impl_hacl::{ecdh, secret_to_public};

/// The length of Curve25519 secret keys.
Expand Down Expand Up @@ -34,42 +37,6 @@ trait Curve25519 {

pub struct X25519;

impl libcrux_traits::kem::arrayref::Kem<DK_LEN, EK_LEN, EK_LEN, SS_LEN, DK_LEN, DK_LEN> for X25519 {
fn keygen(
ek: &mut [u8; DK_LEN],
dk: &mut [u8; EK_LEN],
rand: &[u8; DK_LEN],
) -> Result<(), libcrux_traits::kem::arrayref::KeyGenError> {
dk.copy_from_slice(rand);
clamp(dk);
secret_to_public(ek, dk);
Ok(())
}

fn encaps(
ct: &mut [u8; EK_LEN],
ss: &mut [u8; SS_LEN],
ek: &[u8; EK_LEN],
rand: &[u8; DK_LEN],
) -> Result<(), libcrux_traits::kem::arrayref::EncapsError> {
let mut eph_dk = *rand;
clamp(&mut eph_dk);
secret_to_public(ct, &eph_dk);

ecdh(ss, ek, &eph_dk).map_err(|_| libcrux_traits::kem::arrayref::EncapsError::Unknown)
}

fn decaps(
ss: &mut [u8; SS_LEN],
ct: &[u8; DK_LEN],
dk: &[u8; EK_LEN],
) -> Result<(), libcrux_traits::kem::arrayref::DecapsError> {
ecdh(ss, ct, dk).map_err(|_| libcrux_traits::kem::arrayref::DecapsError::Unknown)
}
}

libcrux_traits::kem::slice::impl_trait!(X25519 => EK_LEN, DK_LEN, EK_LEN, EK_LEN, DK_LEN, DK_LEN);

/// Clamp a scalar.
fn clamp(scalar: &mut [u8; DK_LEN]) {
// We clamp the key already to make sure it can't be misused.
Expand Down
22 changes: 17 additions & 5 deletions libcrux-kem/src/kem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
//! For ECDH structs, check the [`libcrux_ecdh`] crate.
//!
//! Available algorithms:
//! * [`Algorithm::X25519`]\: x25519 ECDH KEM. Also see [`libcrux::ecdh#x25519`].
//! * [`Algorithm::Secp256r1`]\: NIST P256 ECDH KEM. Also see [`libcrux::ecdh#P256`].
//! * [`Algorithm::X25519`]\: x25519 ECDH KEM. Also see [`libcrux_curve25519`].
//! * [`Algorithm::Secp256r1`]\: NIST P256 ECDH KEM. Also see [`libcrux_p256`].
//! * [`Algorithm::MlKem512`]\: ML-KEM 512 from [FIPS 203].
//! * [`Algorithm::MlKem768`]\: ML-KEM 768 from [FIPS 203].
//! * [`Algorithm::MlKem1024`]\: ML-KEM 1024 from [FIPS 203].
Expand Down Expand Up @@ -902,6 +902,7 @@ pub fn key_gen_derand(alg: Algorithm, seed: &[u8]) -> Result<(PrivateKey, Public

/// The XWing KEM combiner with ML-KEM 768 and x25519
mod xwing {
use libcrux_curve25519::ecdh_api::EcdhArrayref;
use libcrux_ecdh::X25519PrivateKey;

use super::*;
Expand Down Expand Up @@ -962,7 +963,9 @@ mod xwing {
let dk_x: &mut [u8; X25519_DK_LEN] = dk_x.try_into().unwrap();

MlKem768::keygen(ek_m, dk_m, rand_m)?;
X25519::keygen(ek_x, dk_x, rand_x)?;

X25519::generate_pair(ek_x, dk_x, rand_x)
.map_err(|_| libcrux_traits::kem::owned::KeyGenError::InvalidRandomness)?;

Ok(())
}
Expand Down Expand Up @@ -993,7 +996,14 @@ mod xwing {
MlKem768::encaps(ct_m, ss_m, ek_m, rand_m)?;

let ss_x: &mut [u8; 32] = (&mut hash_buffer[32..64]).try_into().unwrap();
X25519::encaps(ct_x, ss_x, ek_x, rand_x)?;

let mut ephemeral_secret_x = [0u8; X25519_DK_LEN];
X25519::generate_pair(ct_x, &mut ephemeral_secret_x, rand_x)
.map_err(|_| libcrux_traits::kem::owned::EncapsError::InvalidRandomness)?;

X25519::derive_ecdh(ss_x, ek_x, &ephemeral_secret_x)
.map_err(|_| libcrux_traits::kem::owned::EncapsError::InvalidEncapsKey)?;

hash_buffer[64..96].copy_from_slice(ct_x);
sha3::sha256_ema(ss, &hash_buffer);

Expand Down Expand Up @@ -1025,7 +1035,9 @@ mod xwing {
MlKem768::decaps(ss_m, ct_m, dk_m)?;

let ss_x: &mut [u8; 32] = (&mut hash_buffer[32..64]).try_into().unwrap();
X25519::decaps(ss_x, ct_x, dk_x)?;
X25519::derive_ecdh(ss_x, ct_x, dk_x)
.map_err(|_| libcrux_traits::kem::owned::DecapsError::Unknown)?;

sha3::sha256_ema(ss, &hash_buffer);

Ok(())
Expand Down
Loading