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
2 changes: 1 addition & 1 deletion Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "dimpl"
authors = ["Martin Algesten <[email protected]>"]
description = "DTLS 1.2/1.3 implementation (Sans‑IO, Sync)"
version = "0.7.1"
version = "0.7.0"
edition = "2024"
license = "MIT OR Apache-2.0"
repository = "https://github.com/algesten/dimpl"
Expand Down
171 changes: 169 additions & 2 deletions src/crypto/aws_lc_rs/kx_group.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,20 @@

use aws_lc_rs::agreement::{ECDH_P256, ECDH_P384, UnparsedPublicKey, X25519};
use aws_lc_rs::agreement::{EphemeralPrivateKey, agree_ephemeral};
use aws_lc_rs::kem::{AlgorithmId, Ciphertext, DecapsulationKey, EncapsulationKey, ML_KEM_768};

use super::super::{ActiveKeyExchange, SupportedKxGroup};
use crate::buffer::Buf;
use crate::types::NamedGroup;
use crate::{CryptoError, CryptoOperation};

/// ML-KEM-768 encapsulation key length (client key_share KEM portion), bytes.
const MLKEM768_ENCAP_KEY_LEN: usize = 1184;
/// ML-KEM-768 ciphertext length (server key_share KEM portion), bytes.
const MLKEM768_CIPHERTEXT_LEN: usize = 1088;
/// X25519 public key length, bytes.
const X25519_PUBLIC_LEN: usize = 32;

/// ECDHE key exchange implementation.
struct EcdhKeyExchange {
group: NamedGroup,
Expand Down Expand Up @@ -131,14 +139,173 @@ impl SupportedKxGroup for P384 {
}
}

/// X25519MLKEM768 hybrid key exchange (client / decapsulation role).
///
/// Wire format (draft-ietf-tls-ecdhe-mlkem):
/// - client key_share body = mlkem768_ek(1184) || x25519_pub(32)
/// - server key_share body = mlkem768_ct(1088) || x25519_pub(32)
/// - shared secret = mlkem768_ss(32) || x25519_ss(32) (ML-KEM first)
struct X25519MlKem768KeyExchange {
decap_key: DecapsulationKey<AlgorithmId>,
x25519_private: EphemeralPrivateKey,
public_key: Buf,
}

impl std::fmt::Debug for X25519MlKem768KeyExchange {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("X25519MlKem768KeyExchange")
.field("public_key_len", &self.public_key.len())
.finish_non_exhaustive()
}
}

impl X25519MlKem768KeyExchange {
fn new(mut buf: Buf) -> Result<Self, CryptoError> {
// ML-KEM-768 decapsulation key + encapsulation key bytes (1184).
let decap_key = DecapsulationKey::generate(&ML_KEM_768)
.map_err(|_| CryptoError::OperationFailed(CryptoOperation::GenerateEphemeralKey))?;
let encap_key = decap_key
.encapsulation_key()
.map_err(|_| CryptoError::OperationFailed(CryptoOperation::ComputePublicKey))?;
let encap_key_bytes = encap_key
.key_bytes()
.map_err(|_| CryptoError::OperationFailed(CryptoOperation::ComputePublicKey))?;

// X25519 ephemeral keypair.
let rng = aws_lc_rs::rand::SystemRandom::new();
let x25519_private = EphemeralPrivateKey::generate(&X25519, &rng)
.map_err(|_| CryptoError::OperationFailed(CryptoOperation::GenerateEphemeralKey))?;
let x25519_public = x25519_private
.compute_public_key()
.map_err(|_| CryptoError::OperationFailed(CryptoOperation::ComputePublicKey))?;

// Client key_share body = mlkem768_ek(1184) || x25519_pub(32).
buf.clear();
buf.extend_from_slice(encap_key_bytes.as_ref());
buf.extend_from_slice(x25519_public.as_ref());

Ok(X25519MlKem768KeyExchange {
decap_key,
x25519_private,
public_key: buf,
})
}
}

impl ActiveKeyExchange for X25519MlKem768KeyExchange {
fn pub_key(&self) -> &[u8] {
&self.public_key
}

fn complete(self: Box<Self>, peer_pub: &[u8], out: &mut Buf) -> Result<(), CryptoError> {
// Server key_share body = mlkem768_ct(1088) || x25519_pub(32).
if peer_pub.len() != MLKEM768_CIPHERTEXT_LEN + X25519_PUBLIC_LEN {
return Err(CryptoError::InvalidPublicKey(NamedGroup::X25519MLKEM768));
}
let (ct_bytes, x25519_peer) = peer_pub.split_at(MLKEM768_CIPHERTEXT_LEN);

// ML-KEM-768 decapsulation -> ss1 (32).
let ml_secret = self
.decap_key
.decapsulate(Ciphertext::from(ct_bytes))
.map_err(|_| CryptoError::InvalidPublicKey(NamedGroup::X25519MLKEM768))?;

// Shared secret = mlkem768_ss(32) || x25519_ss(32). ML-KEM first.
out.clear();
out.extend_from_slice(ml_secret.as_ref());

// X25519 agreement -> ss2 (32), appended after the ML-KEM secret.
let peer_key = UnparsedPublicKey::new(&X25519, x25519_peer);
agree_ephemeral(
self.x25519_private,
peer_key,
CryptoError::InvalidPublicKey(NamedGroup::X25519MLKEM768),
|secret| {
out.extend_from_slice(secret);
Ok(())
},
)
}

fn group(&self) -> NamedGroup {
NamedGroup::X25519MLKEM768
}
}

/// X25519MLKEM768 hybrid key exchange group.
#[derive(Debug)]
struct X25519MlKem768Kx;

impl SupportedKxGroup for X25519MlKem768Kx {
fn name(&self) -> NamedGroup {
NamedGroup::X25519MLKEM768
}

fn start_exchange(&self, buf: Buf) -> Result<Box<dyn ActiveKeyExchange>, CryptoError> {
Ok(Box::new(X25519MlKem768KeyExchange::new(buf)?))
}

fn start_and_complete(
&self,
client_share: &[u8],
out_share: &mut Buf,
out_secret: &mut Buf,
) -> Result<(), CryptoError> {
// Server role (encapsulation). client_share = mlkem768_ek(1184) || x25519_pub(32).
if client_share.len() != MLKEM768_ENCAP_KEY_LEN + X25519_PUBLIC_LEN {
return Err(CryptoError::InvalidPublicKey(NamedGroup::X25519MLKEM768));
}
let (ek_bytes, x25519_client_pub) = client_share.split_at(MLKEM768_ENCAP_KEY_LEN);

// ML-KEM-768 encapsulate against the client's encapsulation key.
let encap_key = EncapsulationKey::new(&ML_KEM_768, ek_bytes)
.map_err(|_| CryptoError::InvalidPublicKey(NamedGroup::X25519MLKEM768))?;
let (ciphertext, ml_secret) = encap_key
.encapsulate()
.map_err(|_| CryptoError::InvalidPublicKey(NamedGroup::X25519MLKEM768))?;

// Server X25519 ephemeral keypair.
let rng = aws_lc_rs::rand::SystemRandom::new();
let x25519_private = EphemeralPrivateKey::generate(&X25519, &rng)
.map_err(|_| CryptoError::OperationFailed(CryptoOperation::GenerateEphemeralKey))?;
let x25519_public = x25519_private
.compute_public_key()
.map_err(|_| CryptoError::OperationFailed(CryptoOperation::ComputePublicKey))?;

// Server key_share body = mlkem768_ct(1088) || x25519_pub(32).
out_share.clear();
out_share.extend_from_slice(ciphertext.as_ref());
out_share.extend_from_slice(x25519_public.as_ref());

// Shared secret = mlkem768_ss(32) || x25519_ss(32). ML-KEM first.
out_secret.clear();
out_secret.extend_from_slice(ml_secret.as_ref());
let peer_key = UnparsedPublicKey::new(&X25519, x25519_client_pub);
agree_ephemeral(
x25519_private,
peer_key,
CryptoError::InvalidPublicKey(NamedGroup::X25519MLKEM768),
|secret| {
out_secret.extend_from_slice(secret);
Ok(())
},
)
}
}

/// Static instances of supported key exchange groups.
static KX_GROUP_X25519: X25519Kx = X25519Kx;
static KX_GROUP_P256: P256 = P256;
static KX_GROUP_P384: P384 = P384;
static KX_GROUP_X25519_ML_KEM_768: X25519MlKem768Kx = X25519MlKem768Kx;

/// All supported key exchange groups.
pub(super) static ALL_KX_GROUPS: &[&dyn SupportedKxGroup] =
&[&KX_GROUP_X25519, &KX_GROUP_P256, &KX_GROUP_P384];
pub(super) static ALL_KX_GROUPS: &[&dyn SupportedKxGroup] = &[
&KX_GROUP_X25519,
&KX_GROUP_P256,
&KX_GROUP_P384,
&KX_GROUP_X25519_ML_KEM_768,
];

#[cfg(test)]
mod tests {
Expand Down
21 changes: 21 additions & 0 deletions src/crypto/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,27 @@ pub trait SupportedKxGroup: CryptoSafe {
/// Start a new key exchange, generating ephemeral keypair.
/// The provided `buf` will be used to store the public key.
fn start_exchange(&self, buf: Buf) -> Result<Box<dyn ActiveKeyExchange>, CryptoError>;

/// Server-side key exchange: given the client's `key_share`, produce the
/// server's `key_share` (into `out_share`) and the shared secret (into
/// `out_secret`).
///
/// The default implementation models Diffie-Hellman: it generates an
/// ephemeral keypair (server share = its public key) and agrees against
/// the client's public key. KEM groups (e.g. X25519MLKEM768) override this
/// to encapsulate against the client's encapsulation key, where both the
/// outgoing ciphertext and the secret depend on the client's share.
fn start_and_complete(
&self,
client_share: &[u8],
out_share: &mut Buf,
out_secret: &mut Buf,
) -> Result<(), CryptoError> {
let kx = self.start_exchange(Buf::new())?;
out_share.clear();
out_share.extend_from_slice(kx.pub_key());
kx.complete(client_share, out_secret)
}
}

/// Signature verification against certificates.
Expand Down
23 changes: 22 additions & 1 deletion src/crypto/validation/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,15 @@ impl CryptoProvider {
/// - X25519
/// - P-256 (secp256r1)
/// - P-384 (secp384r1)
/// - X25519MLKEM768 (hybrid post-quantum, DTLS 1.3 only)
pub fn supported_kx_groups(&self) -> impl Iterator<Item = &'static dyn SupportedKxGroup> {
self.kx_groups.iter().copied().filter(|kx| {
matches!(
kx.name(),
NamedGroup::X25519 | NamedGroup::Secp256r1 | NamedGroup::Secp384r1
NamedGroup::X25519
| NamedGroup::Secp256r1
| NamedGroup::Secp384r1
| NamedGroup::X25519MLKEM768
)
})
}
Expand Down Expand Up @@ -364,6 +368,23 @@ impl CryptoProvider {
for kx in self.supported_kx_groups() {
let group = kx.name();

// The symmetric round-trip self-test below models Diffie-Hellman:
// both parties produce same-format public keys and `complete()`
// consumes the peer's public key. A KEM hybrid like
// X25519MLKEM768 is asymmetric (client encapsulation key vs server
// ciphertext) and only implements the client/decapsulation role,
// so it cannot be self-tested this way. Validate it via
// `start_exchange` only.
if matches!(group, NamedGroup::X25519MLKEM768) {
kx.start_exchange(Buf::new()).map_err(|e| {
provider_error(CryptoProviderValidationError::KeyExchangeStartFailed {
group,
source: e,
})
})?;
continue;
}

let alice = kx.start_exchange(Buf::new()).map_err(|e| {
provider_error(CryptoProviderValidationError::KeyExchangeStartFailed {
group,
Expand Down
30 changes: 15 additions & 15 deletions src/dtls13/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -703,33 +703,33 @@ impl State {
};
};

// Start ECDHE key exchange
// Server-side key exchange. For ECDHE this generates an ephemeral
// keypair (server share = its public key) and agrees with the client's
// public key; for a KEM (X25519MLKEM768) it encapsulates against the
// client's encapsulation key. Either way it yields the server's
// key_share bytes and the shared secret.
let kx_group = server
.engine
.find_kx_group(selected_group)
.ok_or(Error::CryptoError(
crate::CryptoError::KeyExchangeGroupNotFound(selected_group),
))?;

let kx_buf = server.engine.pop_buffer();
let key_exchange = kx_group
.start_exchange(kx_buf)
.map_err(Error::CryptoError)?;
let mut server_share = server.engine.pop_buffer();
let mut shared_secret = server.engine.pop_buffer();
{
let peer_pub_key = &server.defragment_buffer[peer_key_range];
kx_group
.start_and_complete(peer_pub_key, &mut server_share, &mut shared_secret)
.map_err(Error::CryptoError)?;
}

// Store server's public key in extension_data
// Store server's key_share in extension_data
server.extension_data.clear();
let pub_key = key_exchange.pub_key();
let pub_key_start = server.extension_data.len();
server.extension_data.extend_from_slice(pub_key);
server.extension_data.extend_from_slice(&server_share);
let pub_key_end = server.extension_data.len();

// Complete ECDHE with client's public key
let peer_pub_key = &server.defragment_buffer[peer_key_range];
let mut shared_secret = server.engine.pop_buffer();
key_exchange
.complete(peer_pub_key, &mut shared_secret)
.map_err(Error::CryptoError)?;

server.shared_secret = Some(shared_secret);

// Select SRTP profile: first from client list that the server supports.
Expand Down
12 changes: 9 additions & 3 deletions src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ impl NamedGroup {
pub const X25519: Self = Self(29);
/// X448 (Curve448 for ECDHE).
pub const X448: Self = Self(30);
/// X25519MLKEM768 hybrid post-quantum group (X25519 + ML-KEM-768),
/// draft-ietf-tls-ecdhe-mlkem. DTLS 1.3 only.
pub const X25519MLKEM768: Self = Self(0x11EC);

/// Convert a wire format u16 value to a `NamedGroup`.
pub const fn from_u16(value: u16) -> Self {
Expand All @@ -154,7 +157,7 @@ impl NamedGroup {

/// Returns true if this is not a known TLS named group wire value.
pub const fn is_unknown(&self) -> bool {
!matches!(*self, Self(1..=25 | 29..=30))
!matches!(*self, Self(1..=25 | 29..=30 | 0x11EC))
}

/// Parse a `NamedGroup` from wire format.
Expand All @@ -169,7 +172,7 @@ impl NamedGroup {
}

/// All recognized named groups (every non-`Unknown` variant).
pub const fn all() -> &'static [NamedGroup; 27] {
pub const fn all() -> &'static [NamedGroup; 28] {
&[
NamedGroup::Sect163k1,
NamedGroup::Sect163r1,
Expand Down Expand Up @@ -198,16 +201,18 @@ impl NamedGroup {
NamedGroup::Secp521r1,
NamedGroup::X25519,
NamedGroup::X448,
NamedGroup::X25519MLKEM768,
]
}

/// Supported named groups in preference order.
pub const fn supported() -> &'static [NamedGroup; 4] {
pub const fn supported() -> &'static [NamedGroup; 5] {
&[
NamedGroup::X25519,
NamedGroup::Secp256r1,
NamedGroup::Secp384r1,
NamedGroup::Secp521r1,
NamedGroup::X25519MLKEM768,
]
}
}
Expand Down Expand Up @@ -242,6 +247,7 @@ impl fmt::Debug for NamedGroup {
NamedGroup::Secp521r1 => f.write_str("Secp521r1"),
NamedGroup::X25519 => f.write_str("X25519"),
NamedGroup::X448 => f.write_str("X448"),
NamedGroup::X25519MLKEM768 => f.write_str("X25519MLKEM768"),
_ => f.debug_tuple("Unknown").field(&self.0).finish(),
}
}
Expand Down