From 74a90b6eafc4e6adead8b2f722c5c4dd22bba9d5 Mon Sep 17 00:00:00 2001 From: Bruce Wayne Date: Thu, 5 Mar 2026 21:48:42 +0800 Subject: [PATCH 1/5] docs: sync README and changelog --- CHANGELOG.md | 1 + README.md | 2 ++ 2 files changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 732e371..50bb3bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ * Bump MSRV to 1.85.0 #75 * Make cipher and kx configurable #73 + * Add DTLS 1.2 ChaCha20-Poly1305 and X25519 support #71 # 0.4.0 diff --git a/README.md b/README.md index 816b3bc..9441d4f 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ Three constructors control which DTLS version is used: - **Cipher suites (TLS 1.2 over DTLS)** - `ECDHE_ECDSA_AES256_GCM_SHA384` - `ECDHE_ECDSA_AES128_GCM_SHA256` + - `ECDHE_ECDSA_CHACHA20_POLY1305_SHA256` - **Cipher suites (TLS 1.3 over DTLS)** - `TLS_AES_128_GCM_SHA256` - `TLS_AES_256_GCM_SHA384` @@ -105,6 +106,7 @@ fn example_event_loop(mut dtls: Dtls) -> Result<(), dimpl::Error> { Output::ApplicationData(_data) => { // Deliver plaintext to application } + _ => {} } } From d54640e6e59a192823fb963bf8b79e5d11e5fed1 Mon Sep 17 00:00:00 2001 From: Bruce Wayne Date: Thu, 5 Mar 2026 22:39:06 +0800 Subject: [PATCH 2/5] chore: simplify dependency version specs and update deps --- Cargo.lock | 44 ++++++++++++++++++++++---------------------- Cargo.toml | 20 ++++++++++---------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 560a78b..8c5b680 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "aead" @@ -158,9 +158,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.16.0" +version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9a7b350e3bb1767102698302bc37256cbd48422809984b98d292c40e2579aa9" +checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf" dependencies = [ "aws-lc-sys", "untrusted", @@ -169,9 +169,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.37.1" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549" +checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e" dependencies = [ "cc", "cmake", @@ -187,9 +187,9 @@ checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" [[package]] name = "base64ct" -version = "1.7.3" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bindgen" @@ -445,9 +445,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", ] @@ -744,9 +744,9 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jiff" -version = "0.2.22" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819b44bc7c87d9117eb522f14d46e918add69ff12713c475946b0a29363ed1c2" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" dependencies = [ "jiff-static", "log", @@ -757,9 +757,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.22" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "470252db18ecc35fd766c0891b1e3ec6cbbcd62507e85276c01bf75d8e94d4a1" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" dependencies = [ "proc-macro2", "quote", @@ -1076,9 +1076,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -1349,30 +1349,30 @@ dependencies = [ [[package]] name = "time" -version = "0.3.41" +version = "0.3.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" [[package]] name = "time-macros" -version = "0.2.22" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd" dependencies = [ "num-conv", "time-core", diff --git a/Cargo.toml b/Cargo.toml index 3df84a4..1180d2c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,23 +38,23 @@ _crypto-common = ["dep:der", "dep:pkcs8", "dep:sec1", "dep:signature", "dep:spki rcgen = ["dep:rcgen", "aws-lc-rs"] [dependencies] -log = "0.4.29" +log = "0.4" nom = { version = "8", default-features = false, features = ["std"] } -once_cell = "1.21.3" +once_cell = "1" rand = "0.9" time = { version = "0.3", features = ["formatting"] } -arrayvec = "0.7.6" -subtle = "2.6" +arrayvec = "0.7" +subtle = "2" der = { version = "0.7", optional = true } pkcs8 = { version = "0.10", features = ["pem"], optional = true } sec1 = { version = "0.7", optional = true } -signature = { version = "2.2", optional = true } +signature = { version = "2", optional = true } spki = { version = "0.7", optional = true } x509-cert = { version = "0.2", default-features = false, optional = true } # aws-lc-rs backend -aws-lc-rs = { version = "^1.16", default-features = false, features = ["aws-lc-sys", "prebuilt-nasm"], optional = true } +aws-lc-rs = { version = "1", default-features = false, features = ["aws-lc-sys", "prebuilt-nasm"], optional = true } # RustCrypto backend aes-gcm = { version = "0.10", optional = true } @@ -71,15 +71,15 @@ chacha20 = { version = "0.9", optional = true } x25519-dalek = { version = "2", optional = true, features = ["static_secrets"] } # certificate generation -rcgen = { version = "0.14.7", default-features = false, features = ["aws_lc_rs"], optional = true } +rcgen = { version = "0.14", default-features = false, features = ["aws_lc_rs"], optional = true } [dev-dependencies] -openssl = { version = "0.10.75", features = ["vendored"] } +openssl = { version = "0.10", features = ["vendored"] } libc = "0.2" -env_logger = "0.11.9" +env_logger = "0.11" x509-parser = "0.18" bytes = "1" # wolfssl doesn't build on Windows [target.'cfg(not(windows))'.dev-dependencies] -wolfssl = "3.0.0" +wolfssl = "3" From b0e28ea85cbbb3c78d4a77791eb92f364de48498 Mon Sep 17 00:00:00 2001 From: Bruce Wayne Date: Fri, 6 Mar 2026 01:47:01 +0800 Subject: [PATCH 3/5] feat: add DTLS 1.2 ChaCha20 and X25519 support --- src/buffer.rs | 4 +- src/config.rs | 45 ++++------ src/crypto/aws_lc_rs/cipher_suite.rs | 54 +++++++++++- src/crypto/aws_lc_rs/mod.rs | 2 + src/crypto/dtls_aead.rs | 74 ++++++++++++++--- src/crypto/mod.rs | 2 +- src/crypto/provider.rs | 20 ++++- src/crypto/rust_crypto/cipher_suite.rs | 54 +++++++++++- src/crypto/rust_crypto/mod.rs | 2 + src/crypto/validation/mod.rs | 49 ++++------- src/detect.rs | 1 - src/dtls12/context.rs | 12 +-- src/dtls12/engine.rs | 110 +++++++++++++++++++------ src/dtls12/incoming.rs | 12 +-- src/dtls12/message/client_hello.rs | 4 +- src/dtls12/message/handshake.rs | 2 +- src/dtls12/message/mod.rs | 29 +++++-- src/dtls12/message/record.rs | 13 ++- src/dtls12/server.rs | 106 +++++++++++++++++++++--- src/dtls13/client.rs | 7 +- src/dtls13/engine.rs | 2 +- src/dtls13/message/handshake.rs | 2 +- src/dtls13/server.rs | 10 +-- src/lib.rs | 6 +- src/types.rs | 2 +- tests/dtls12/crypto.rs | 108 ++++++++++++++++++++++-- tests/dtls13/handshake.rs | 2 +- tests/dtls13/wolfssl.rs | 18 ++-- tests/ossl/cert.rs | 9 +- tests/ossl/dtls.rs | 28 +++++-- 30 files changed, 602 insertions(+), 187 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index 27cafa9..31b5bd6 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -38,7 +38,7 @@ impl BufferPool { } impl fmt::Debug for BufferPool { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("BufferPool") .field("free", &self.free.len()) .finish() @@ -118,7 +118,7 @@ impl AsMut<[u8]> for Buf { } impl fmt::Debug for Buf { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Buf").field("len", &self.0.len()).finish() } } diff --git a/src/config.rs b/src/config.rs index 22367fe..6940bc3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -198,15 +198,6 @@ impl Config { None => true, }) } - - /// Allowed key exchange groups for DTLS 1.2. - /// - /// Like [`kx_groups`](Self::kx_groups) but additionally restricted to - /// groups that DTLS 1.2 supports (currently P-256 and P-384). - pub fn dtls12_kx_groups(&self) -> impl Iterator + '_ { - self.kx_groups() - .filter(|kx| matches!(kx.name(), NamedGroup::Secp256r1 | NamedGroup::Secp384r1)) - } } /// Builder for [`Config`]. See each setter for defaults. @@ -446,13 +437,13 @@ impl ConfigBuilder { }; if dtls12_count > 0 { let dtls12_kx_count = crypto_provider - .supported_dtls12_kx_groups() + .supported_kx_groups() .filter(|kx| filtered_kx(kx)) .count(); if dtls12_kx_count == 0 { return Err(Error::ConfigError( "DTLS 1.2 cipher suites are enabled but no compatible key exchange \ - groups remain after filtering. DTLS 1.2 requires P-256 or P-384." + groups remain after filtering." .to_string(), )); } @@ -635,23 +626,15 @@ mod tests { } #[test] - fn x25519_only_rejected_for_dtls12() { - // X25519 is not yet supported for DTLS 1.2, so filtering to X25519-only - // while DTLS 1.2 suites are enabled should fail. - match Config::builder() + fn x25519_only_accepted_for_dtls12() { + // X25519 is supported for DTLS 1.2 and should be accepted. + let config = Config::builder() .dtls13_cipher_suites(&[]) .kx_groups(&[NamedGroup::X25519]) .build() - { - Err(Error::ConfigError(msg)) => { - assert!( - msg.contains("DTLS 1.2") && msg.contains("P-256 or P-384"), - "error should mention DTLS 1.2 and required groups: {msg}" - ) - } - Err(other) => panic!("expected ConfigError, got: {other:?}"), - Ok(_) => panic!("expected error for X25519-only with DTLS 1.2"), - } + .expect("X25519-only should be accepted for DTLS 1.2"); + let groups: Vec<_> = config.kx_groups().map(|g| g.name()).collect(); + assert_eq!(groups, &[NamedGroup::X25519]); } #[test] @@ -667,11 +650,15 @@ mod tests { } #[test] - fn dtls12_kx_groups_excludes_x25519() { + fn kx_groups_match_provider_when_unfiltered() { let config = Config::default(); - let dtls12_groups: Vec<_> = config.dtls12_kx_groups().map(|g| g.name()).collect(); - assert!(!dtls12_groups.contains(&NamedGroup::X25519)); - assert!(dtls12_groups.contains(&NamedGroup::Secp256r1)); + let from_config: Vec<_> = config.kx_groups().map(|g| g.name()).collect(); + let from_provider: Vec<_> = config + .crypto_provider() + .supported_kx_groups() + .map(|g| g.name()) + .collect(); + assert_eq!(from_config, from_provider); } #[test] diff --git a/src/crypto/aws_lc_rs/cipher_suite.rs b/src/crypto/aws_lc_rs/cipher_suite.rs index 9334935..3b43570 100644 --- a/src/crypto/aws_lc_rs/cipher_suite.rs +++ b/src/crypto/aws_lc_rs/cipher_suite.rs @@ -159,6 +159,14 @@ impl SupportedDtls12CipherSuite for Aes128GcmSha256 { (0, 16, 4) // (mac_key_len, enc_key_len, fixed_iv_len) } + fn explicit_nonce_len(&self) -> usize { + 8 + } + + fn tag_len(&self) -> usize { + 16 + } + fn create_cipher(&self, key: &[u8]) -> Result, String> { Ok(Box::new(AesGcm::new(key)?)) } @@ -181,18 +189,60 @@ impl SupportedDtls12CipherSuite for Aes256GcmSha384 { (0, 32, 4) // (mac_key_len, enc_key_len, fixed_iv_len) } + fn explicit_nonce_len(&self) -> usize { + 8 + } + + fn tag_len(&self) -> usize { + 16 + } + fn create_cipher(&self, key: &[u8]) -> Result, String> { Ok(Box::new(AesGcm::new(key)?)) } } +/// TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 cipher suite. +#[derive(Debug)] +struct ChaCha20Poly1305Sha256; + +impl SupportedDtls12CipherSuite for ChaCha20Poly1305Sha256 { + fn suite(&self) -> Dtls12CipherSuite { + Dtls12CipherSuite::ECDHE_ECDSA_CHACHA20_POLY1305_SHA256 + } + + fn hash_algorithm(&self) -> HashAlgorithm { + HashAlgorithm::SHA256 + } + + fn key_lengths(&self) -> (usize, usize, usize) { + (0, 32, 12) // (mac_key_len, enc_key_len, fixed_iv_len) + } + + fn explicit_nonce_len(&self) -> usize { + 0 + } + + fn tag_len(&self) -> usize { + 16 + } + + fn create_cipher(&self, key: &[u8]) -> Result, String> { + Ok(Box::new(ChaCha20Poly1305Cipher::new(key)?)) + } +} + /// Static instances of supported DTLS 1.2 cipher suites. static AES_128_GCM_SHA256: Aes128GcmSha256 = Aes128GcmSha256; static AES_256_GCM_SHA384: Aes256GcmSha384 = Aes256GcmSha384; +static CHACHA20_POLY1305_SHA256: ChaCha20Poly1305Sha256 = ChaCha20Poly1305Sha256; /// All supported DTLS 1.2 cipher suites. -pub(super) static ALL_CIPHER_SUITES: &[&dyn SupportedDtls12CipherSuite] = - &[&AES_128_GCM_SHA256, &AES_256_GCM_SHA384]; +pub(super) static ALL_CIPHER_SUITES: &[&dyn SupportedDtls12CipherSuite] = &[ + &AES_128_GCM_SHA256, + &AES_256_GCM_SHA384, + &CHACHA20_POLY1305_SHA256, +]; // ============================================================================ // DTLS 1.3 Cipher Suites diff --git a/src/crypto/aws_lc_rs/mod.rs b/src/crypto/aws_lc_rs/mod.rs index 4de3c4e..4ac662c 100644 --- a/src/crypto/aws_lc_rs/mod.rs +++ b/src/crypto/aws_lc_rs/mod.rs @@ -77,9 +77,11 @@ use super::CryptoProvider; /// /// - `TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256` (0xC02B) /// - `TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384` (0xC02C) +/// - `TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256` (0xCCA9) /// /// # Supported Key Exchange Groups /// +/// - `x25519` (X25519 / Curve25519) /// - `secp256r1` (P-256, NIST Curve) /// - `secp384r1` (P-384, NIST Curve) /// diff --git a/src/crypto/dtls_aead.rs b/src/crypto/dtls_aead.rs index 9ad015e..1f16d9e 100644 --- a/src/crypto/dtls_aead.rs +++ b/src/crypto/dtls_aead.rs @@ -10,27 +10,29 @@ use crate::types::{ContentType, Sequence}; /// Explicit nonce length for DTLS AEAD records. /// /// The explicit nonce is transmitted with each record. +#[cfg(test)] pub(crate) const DTLS_EXPLICIT_NONCE_LEN: usize = 8; /// GCM authentication tag length. /// /// The tag is appended to the ciphertext. +#[cfg(test)] pub(crate) const GCM_TAG_LEN: usize = 16; -/// Overhead per AEAD record (explicit nonce + tag). +/// Overhead per DTLS 1.2 AES-GCM record (explicit nonce + tag). /// /// This equals 24 bytes for DTLS AES-GCM. +#[cfg(test)] pub(crate) const DTLS_AEAD_OVERHEAD: usize = DTLS_EXPLICIT_NONCE_LEN + GCM_TAG_LEN; // 24 -/// Compute AAD length from plaintext length for AEAD records. -/// For DTLS AEAD this is the plaintext length. +/// Compute AAD length from plaintext length for DTLS 1.2 AES-GCM records. #[inline] #[cfg(test)] pub fn aad_len_from_plaintext_len(plaintext_len: u16) -> u16 { plaintext_len } -/// Compute fragment length from plaintext length for AEAD records. +/// Compute fragment length from plaintext length for DTLS 1.2 AES-GCM records. /// fragment_len = explicit_nonce(8) + ciphertext(plaintext_len + 16 tag) #[inline] #[cfg(test)] @@ -38,7 +40,7 @@ pub fn fragment_len_from_plaintext_len(plaintext_len: usize) -> usize { DTLS_EXPLICIT_NONCE_LEN + plaintext_len + GCM_TAG_LEN } -/// Compute plaintext length from fragment length, if large enough. +/// Compute plaintext length from fragment length for DTLS 1.2 AES-GCM records. /// Returns None if the fragment is smaller than the mandatory AEAD overhead. #[inline] #[cfg(test)] @@ -47,13 +49,51 @@ pub fn plaintext_len_from_fragment_len(fragment_len: usize) -> Option { } /// Fixed IV portion for DTLS AEAD. +/// +/// DTLS 1.2 uses: +/// - AES-GCM: 4-byte fixed IV + 8-byte explicit nonce (per record) +/// - ChaCha20-Poly1305: 12-byte fixed IV + 0-byte explicit nonce #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) struct Iv(pub [u8; 4]); +pub(crate) struct Iv { + bytes: [u8; 12], + len: u8, +} impl Iv { pub(crate) fn new(iv: &[u8]) -> Self { - // invariant: the iv is 4 bytes. - Self(iv.try_into().unwrap()) + assert!( + iv.len() <= 12, + "invalid IV length: expected <= 12, got {}", + iv.len() + ); + let mut bytes = [0u8; 12]; + bytes[..iv.len()].copy_from_slice(iv); + Self { + bytes, + len: iv.len() as u8, + } + } + + pub(crate) fn len(&self) -> usize { + self.len as usize + } + + pub(crate) fn as_slice(&self) -> &[u8] { + &self.bytes[..self.len()] + } + + /// Returns the full 12-byte backing array. + /// + /// Only valid for 12-byte IVs (ChaCha20-Poly1305). For 4-byte IVs + /// (AES-GCM), use [`as_slice`] instead. + pub(crate) fn as_12_bytes(&self) -> &[u8; 12] { + debug_assert_eq!( + self.len(), + 12, + "as_12_bytes called on {}-byte IV", + self.len() + ); + &self.bytes } } @@ -64,15 +104,25 @@ pub struct Nonce(pub [u8; 12]); impl Nonce { /// Create a new AEAD nonce by combining fixed IV and explicit nonce (DTLS 1.2). pub(crate) fn new(iv: Iv, explicit_nonce: &[u8]) -> Self { + assert_eq!( + iv.len() + explicit_nonce.len(), + 12, + "invalid DTLS 1.2 nonce parts: iv_len={}, explicit_nonce_len={}", + iv.len(), + explicit_nonce.len() + ); let mut nonce = [0u8; 12]; - nonce[..4].copy_from_slice(&iv.0); - nonce[4..].copy_from_slice(explicit_nonce); + let iv_len = iv.len(); + nonce[..iv_len].copy_from_slice(iv.as_slice()); + nonce[iv_len..].copy_from_slice(explicit_nonce); Self(nonce) } - /// Create a DTLS 1.3 nonce by XORing the IV with the padded sequence number. + /// Create a nonce by XORing the IV with the padded sequence number. /// - /// Per RFC 8446 Section 5.3: nonce = iv XOR pad_left(seq, iv_len) + /// Used by both DTLS 1.2 (ChaCha20-Poly1305) and DTLS 1.3: + /// nonce = iv XOR pad_left(sequence_number, 12) + /// See RFC 8446 Section 5.3 / RFC 7905. pub(crate) fn xor(iv: &[u8; 12], seq: u64) -> Self { let mut nonce = *iv; let seq_bytes = seq.to_be_bytes(); // 8 bytes diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index 89a83de..2afa7c3 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -22,7 +22,7 @@ pub use keying::{KeyingMaterial, SrtpProfile}; pub use dtls_aead::{Aad, Nonce}; // Re-export internal AEAD constants/types for crate-internal use -pub(crate) use dtls_aead::{Iv, DTLS_AEAD_OVERHEAD, DTLS_EXPLICIT_NONCE_LEN}; +pub(crate) use dtls_aead::Iv; // Re-export buffer types for provider trait implementations pub use crate::buffer::{Buf, TmpBuf}; diff --git a/src/crypto/provider.rs b/src/crypto/provider.rs index 37acb7c..b3e0723 100644 --- a/src/crypto/provider.rs +++ b/src/crypto/provider.rs @@ -109,6 +109,14 @@ //! (0, 16, 4) // (mac_key_len, enc_key_len, fixed_iv_len) //! } //! +//! fn explicit_nonce_len(&self) -> usize { +//! 8 // AES-GCM: 8-byte explicit nonce per record +//! } +//! +//! fn tag_len(&self) -> usize { +//! 16 // 128-bit authentication tag +//! } +//! //! fn create_cipher(&self, key: &[u8]) -> Result, String> { //! // Create your cipher implementation here //! Ok(Box::new(MyCipher::new(key)?)) @@ -123,8 +131,8 @@ //! //! For DTLS 1.2, implementations must support: //! -//! - **Cipher suites**: ECDHE_ECDSA with AES-128-GCM or AES-256-GCM -//! - **Key exchange**: ECDHE with P-256 or P-384 curves +//! - **Cipher suites**: ECDHE_ECDSA with AES-128-GCM, AES-256-GCM, or CHACHA20_POLY1305 +//! - **Key exchange**: ECDHE with X25519, P-256, or P-384 curves //! - **Signatures**: ECDSA with P-256/SHA-256 or P-384/SHA-384 //! - **Hash**: SHA-256 and SHA-384 //! - **PRF**: TLS 1.2 PRF (using HMAC-SHA256 or HMAC-SHA384) @@ -231,6 +239,14 @@ pub trait SupportedDtls12CipherSuite: CryptoSafe { /// Key material lengths: (mac_key_len, enc_key_len, fixed_iv_len). fn key_lengths(&self) -> (usize, usize, usize); + /// Length in bytes of the per-record explicit nonce (carried in the record body). + /// + /// AES-GCM suites carry an 8-byte explicit nonce; ChaCha20-Poly1305 carries none. + fn explicit_nonce_len(&self) -> usize; + + /// AEAD authentication tag length in bytes. + fn tag_len(&self) -> usize; + /// Create a cipher instance with the given key. fn create_cipher(&self, key: &[u8]) -> Result, String>; } diff --git a/src/crypto/rust_crypto/cipher_suite.rs b/src/crypto/rust_crypto/cipher_suite.rs index db62a58..a45805b 100644 --- a/src/crypto/rust_crypto/cipher_suite.rs +++ b/src/crypto/rust_crypto/cipher_suite.rs @@ -209,6 +209,14 @@ impl SupportedDtls12CipherSuite for Aes128GcmSha256 { (0, 16, 4) // (mac_key_len, enc_key_len, fixed_iv_len) } + fn explicit_nonce_len(&self) -> usize { + 8 + } + + fn tag_len(&self) -> usize { + 16 + } + fn create_cipher(&self, key: &[u8]) -> Result, String> { Ok(Box::new(AesGcm::new(key)?)) } @@ -231,18 +239,60 @@ impl SupportedDtls12CipherSuite for Aes256GcmSha384 { (0, 32, 4) // (mac_key_len, enc_key_len, fixed_iv_len) } + fn explicit_nonce_len(&self) -> usize { + 8 + } + + fn tag_len(&self) -> usize { + 16 + } + fn create_cipher(&self, key: &[u8]) -> Result, String> { Ok(Box::new(AesGcm::new(key)?)) } } +/// TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 cipher suite. +#[derive(Debug)] +struct ChaCha20Poly1305Sha256; + +impl SupportedDtls12CipherSuite for ChaCha20Poly1305Sha256 { + fn suite(&self) -> Dtls12CipherSuite { + Dtls12CipherSuite::ECDHE_ECDSA_CHACHA20_POLY1305_SHA256 + } + + fn hash_algorithm(&self) -> HashAlgorithm { + HashAlgorithm::SHA256 + } + + fn key_lengths(&self) -> (usize, usize, usize) { + (0, 32, 12) // (mac_key_len, enc_key_len, fixed_iv_len) + } + + fn explicit_nonce_len(&self) -> usize { + 0 + } + + fn tag_len(&self) -> usize { + 16 + } + + fn create_cipher(&self, key: &[u8]) -> Result, String> { + Ok(Box::new(ChaCha20Poly1305Cipher::new(key)?)) + } +} + /// Static instances of supported DTLS 1.2 cipher suites. static AES_128_GCM_SHA256: Aes128GcmSha256 = Aes128GcmSha256; static AES_256_GCM_SHA384: Aes256GcmSha384 = Aes256GcmSha384; +static CHACHA20_POLY1305_SHA256: ChaCha20Poly1305Sha256 = ChaCha20Poly1305Sha256; /// All supported DTLS 1.2 cipher suites. -pub(super) static ALL_CIPHER_SUITES: &[&dyn SupportedDtls12CipherSuite] = - &[&AES_128_GCM_SHA256, &AES_256_GCM_SHA384]; +pub(super) static ALL_CIPHER_SUITES: &[&dyn SupportedDtls12CipherSuite] = &[ + &AES_128_GCM_SHA256, + &AES_256_GCM_SHA384, + &CHACHA20_POLY1305_SHA256, +]; // ============================================================================ // DTLS 1.3 Cipher Suites diff --git a/src/crypto/rust_crypto/mod.rs b/src/crypto/rust_crypto/mod.rs index 53e0a07..8b4bcb5 100644 --- a/src/crypto/rust_crypto/mod.rs +++ b/src/crypto/rust_crypto/mod.rs @@ -59,9 +59,11 @@ use super::CryptoProvider; /// /// - `TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256` (0xC02B) /// - `TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384` (0xC02C) +/// - `TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256` (0xCCA9) /// /// # Supported Key Exchange Groups /// +/// - `x25519` (X25519 / Curve25519) /// - `secp256r1` (P-256, NIST Curve) /// - `secp384r1` (P-384, NIST Curve) /// diff --git a/src/crypto/validation/mod.rs b/src/crypto/validation/mod.rs index e7274a1..afd9812 100644 --- a/src/crypto/validation/mod.rs +++ b/src/crypto/validation/mod.rs @@ -7,7 +7,6 @@ use arrayvec::ArrayVec; use super::{Aad, CryptoProvider, Nonce, SupportedDtls12CipherSuite, SupportedKxGroup}; use crate::buffer::{Buf, TmpBuf}; -use crate::dtls12::message::Dtls12CipherSuite; use crate::types::{Dtls13CipherSuite, HashAlgorithm, NamedGroup, SignatureAlgorithm}; use crate::Error; @@ -17,16 +16,14 @@ impl CryptoProvider { /// Only cipher suites documented in lib.rs are returned: /// - `TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256` /// - `TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384` + /// - `TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256` pub fn supported_cipher_suites( &self, ) -> impl Iterator { - self.cipher_suites.iter().copied().filter(|cs| { - matches!( - cs.suite(), - Dtls12CipherSuite::ECDHE_ECDSA_AES128_GCM_SHA256 - | Dtls12CipherSuite::ECDHE_ECDSA_AES256_GCM_SHA384 - ) - }) + self.cipher_suites + .iter() + .copied() + .filter(|cs| cs.suite().is_supported()) } /// Returns an iterator over validated key exchange groups supported by dimpl. @@ -44,21 +41,6 @@ impl CryptoProvider { }) } - /// Returns an iterator over key exchange groups supported for DTLS 1.2. - /// - /// DTLS 1.2 only supports ECDHE with NIST curves: - /// - P-256 (secp256r1) - /// - P-384 (secp384r1) - pub fn supported_dtls12_kx_groups( - &self, - ) -> impl Iterator { - self.kx_groups - .iter() - .copied() - .filter(|kx| matches!(kx.name(), NamedGroup::Secp256r1 | NamedGroup::Secp384r1)) - } - - /// Returns cipher suites compatible with a specific signature algorithm. /// /// Combines provider filtering with signature algorithm compatibility. pub fn supported_cipher_suites_for_signature_algorithm( @@ -73,13 +55,8 @@ impl CryptoProvider { /// /// Returns true if any supported cipher suite uses ECDH key exchange. pub fn has_ecdh(&self) -> bool { - self.supported_cipher_suites().any(|cs| { - matches!( - cs.suite(), - Dtls12CipherSuite::ECDHE_ECDSA_AES128_GCM_SHA256 - | Dtls12CipherSuite::ECDHE_ECDSA_AES256_GCM_SHA384 - ) - }) + self.supported_cipher_suites() + .any(|cs| cs.suite().has_ecc()) } /// Validates the provider configuration for use with dimpl. @@ -703,6 +680,7 @@ const SN_TEST_VECTORS: &[SnTestVector] = &[ mod tests_aws_lc_rs { use super::*; use crate::crypto::aws_lc_rs; + use crate::dtls12::message::Dtls12CipherSuite; #[test] fn test_default_provider_validates() { @@ -714,7 +692,7 @@ mod tests_aws_lc_rs { fn test_default_provider_has_cipher_suites() { let provider = aws_lc_rs::default_provider(); let count = provider.supported_cipher_suites().count(); - assert_eq!(count, 2); // AES-128 and AES-256 + assert_eq!(count, 3); // AES-128, AES-256, and ChaCha20-Poly1305 } #[test] @@ -738,9 +716,10 @@ mod tests_aws_lc_rs { .map(|cs| cs.suite()) .collect(); - assert_eq!(ecdsa_suites.len(), 2); + assert_eq!(ecdsa_suites.len(), 3); assert!(ecdsa_suites.contains(&Dtls12CipherSuite::ECDHE_ECDSA_AES128_GCM_SHA256)); assert!(ecdsa_suites.contains(&Dtls12CipherSuite::ECDHE_ECDSA_AES256_GCM_SHA384)); + assert!(ecdsa_suites.contains(&Dtls12CipherSuite::ECDHE_ECDSA_CHACHA20_POLY1305_SHA256)); } } @@ -749,6 +728,7 @@ mod tests_aws_lc_rs { mod tests_rust_crypto { use super::*; use crate::crypto::rust_crypto; + use crate::dtls12::message::Dtls12CipherSuite; #[test] fn test_default_provider_validates() { @@ -760,7 +740,7 @@ mod tests_rust_crypto { fn test_default_provider_has_cipher_suites() { let provider = rust_crypto::default_provider(); let count = provider.supported_cipher_suites().count(); - assert_eq!(count, 2); // AES-128 and AES-256 + assert_eq!(count, 3); // AES-128, AES-256, and ChaCha20-Poly1305 } #[test] @@ -784,8 +764,9 @@ mod tests_rust_crypto { .map(|cs| cs.suite()) .collect(); - assert_eq!(ecdsa_suites.len(), 2); + assert_eq!(ecdsa_suites.len(), 3); assert!(ecdsa_suites.contains(&Dtls12CipherSuite::ECDHE_ECDSA_AES128_GCM_SHA256)); assert!(ecdsa_suites.contains(&Dtls12CipherSuite::ECDHE_ECDSA_AES256_GCM_SHA384)); + assert!(ecdsa_suites.contains(&Dtls12CipherSuite::ECDHE_ECDSA_CHACHA20_POLY1305_SHA256)); } } diff --git a/src/detect.rs b/src/detect.rs index 5546f2b..1433310 100644 --- a/src/detect.rs +++ b/src/detect.rs @@ -24,7 +24,6 @@ use crate::dtls13::message::SupportedGroupsExtension; use crate::dtls13::message::UseSrtpExtension; use crate::types::NamedGroup; use crate::{Config, DtlsCertificate, Error, Output, SeededRng}; - // Extension type constants const EXT_SUPPORTED_GROUPS: u16 = 0x000A; const EXT_EC_POINT_FORMATS: u16 = 0x000B; diff --git a/src/dtls12/context.rs b/src/dtls12/context.rs index b4efdcc..58887b8 100644 --- a/src/dtls12/context.rs +++ b/src/dtls12/context.rs @@ -32,10 +32,10 @@ pub struct CryptoContext { /// Server write key server_write_key: Option, - /// Client write IV (for AES-GCM) + /// Client write IV (4 bytes for AES-GCM, 12 bytes for ChaCha20-Poly1305) client_write_iv: Option, - /// Server write IV (for AES-GCM) + /// Server write IV (4 bytes for AES-GCM, 12 bytes for ChaCha20-Poly1305) server_write_iv: Option, /// Client MAC key (not used for AEAD ciphers) @@ -160,10 +160,10 @@ impl CryptoContext { named_group: NamedGroup, kx_buf: &mut Buf, ) -> Result<&[u8], String> { - // Find the matching key exchange group from the provider (DTLS 1.2 groups only) + // Find the matching key exchange group from the provider let kx_group = self .provider() - .supported_dtls12_kx_groups() + .supported_kx_groups() .find(|g| g.name() == named_group) .ok_or_else(|| format!("Unsupported ECDHE named group: {:?}", named_group))?; @@ -179,10 +179,10 @@ impl CryptoContext { server_public: &[u8], kx_buf: &mut Buf, ) -> Result<(), String> { - // Find the matching key exchange group from the provider (DTLS 1.2 groups only) + // Find the matching key exchange group from the provider let kx_group = self .provider() - .supported_dtls12_kx_groups() + .supported_kx_groups() .find(|g| g.name() == group) .ok_or_else(|| format!("Unsupported ECDHE named group: {:?}", group))?; diff --git a/src/dtls12/engine.rs b/src/dtls12/engine.rs index 2f1a752..a0343eb 100644 --- a/src/dtls12/engine.rs +++ b/src/dtls12/engine.rs @@ -5,7 +5,7 @@ use std::time::{Duration, Instant}; use super::queue::{QueueRx, QueueTx}; use crate::buffer::{Buf, BufferPool, TmpBuf}; -use crate::crypto::{Aad, Iv, Nonce, DTLS_AEAD_OVERHEAD, DTLS_EXPLICIT_NONCE_LEN}; +use crate::crypto::{Aad, Iv, Nonce}; use crate::dtls12::context::CryptoContext; use crate::dtls12::incoming::{Incoming, Record, RecordDecrypt}; use crate::dtls12::message::{Body, HashAlgorithm, Header, MessageType, ProtocolVersion, Sequence}; @@ -45,6 +45,12 @@ pub struct Engine { /// The cipher suite in use. Set by ServerHello. cipher_suite: Option, + /// Per-record explicit nonce length (from provider). 0 for ChaCha20, 8 for AES-GCM. + explicit_nonce_len: usize, + + /// AEAD tag length (from provider). + tag_len: usize, + /// Cryptographic context for handling encryption/decryption pub(crate) crypto_context: CryptoContext, @@ -120,6 +126,8 @@ impl Engine { queue_rx: QueueRx::new(), queue_tx: QueueTx::new(), cipher_suite: None, + explicit_nonce_len: 0, + tag_len: 0, crypto_context, peer_encryption_enabled: false, is_client: false, @@ -164,6 +172,11 @@ impl Engine { self.cipher_suite } + /// Total per-record AEAD overhead: explicit_nonce_len + tag_len. + pub fn aead_overhead(&self) -> usize { + self.explicit_nonce_len + self.tag_len + } + /// Is the given cipher suite allowed by configuration pub fn is_cipher_suite_allowed(&self, suite: Dtls12CipherSuite) -> bool { self.config @@ -288,7 +301,7 @@ impl Engine { // For epoch 0, we can get duplicates due to resends. // For epoch 1, we have the replay window and there should // be no duplicates. - assert!(seq_current.epoch == 0); + assert_eq!(seq_current.epoch, 0); } } @@ -616,6 +629,15 @@ impl Engine { where F: FnOnce(&mut Buf), { + let maybe_suite = + if epoch >= 1 { + Some(self.cipher_suite().ok_or_else(|| { + Error::UnexpectedMessage("No cipher suite selected".to_string()) + })?) + } else { + None + }; + // Prepare the plaintext fragment let mut fragment = self.buffers_free.pop(); @@ -635,7 +657,11 @@ impl Engine { // Compute wire length of the record if serialized into a datagram // Record header (13) + handshake/change/app data bytes + AEAD overhead (if epoch >= 1) - let overhead = if epoch >= 1 { DTLS_AEAD_OVERHEAD } else { 0 }; + let overhead = if maybe_suite.is_some() { + self.aead_overhead() + } else { + 0 + }; let record_wire_len = DTLSRecord::HEADER_LEN + fragment.len() + overhead; // Decide whether to append to the existing last datagram or create a new one @@ -665,7 +691,9 @@ impl Engine { // Handle encryption for epochs >= 1 if epoch >= 1 { - // Get the fixed part of the IV (4 bytes) + let suite = maybe_suite.expect("cipher suite must be set for encrypted epochs"); + + // Get the fixed part of the IV let iv = if self.is_client { self.crypto_context.get_client_write_iv() } else { @@ -679,27 +707,37 @@ impl Engine { ))); }; - // Generate 8 random bytes for the explicit part of the nonce - let explicit_nonce: [u8; 8] = self.rng.random(); - - // Combine the fixed IV and the explicit nonce - let nonce = Nonce::new(iv, &explicit_nonce); + let explicit_nonce_len = self.explicit_nonce_len; + let mut explicit_nonce = [0u8; DTLSRecord::EXPLICIT_NONCE_LEN]; + let seq64 = ((sequence.epoch as u64) << 48) | sequence.sequence_number; + let nonce = match explicit_nonce_len { + 0 => Nonce::xor(iv.as_12_bytes(), seq64), + DTLSRecord::EXPLICIT_NONCE_LEN => { + explicit_nonce = self.rng.random(); + Nonce::new(iv, &explicit_nonce) + } + _ => { + return Err(Error::CryptoError(format!( + "Unsupported DTLS 1.2 record_iv_len={} for {:?}", + explicit_nonce_len, suite + ))); + } + }; - // DTLS 1.2 AEAD (AES-GCM): AAD uses the plaintext length (DTLSCompressed.length). - // See RFC 5246/5288 and RFC 6347. The record fragment on the wire will be: - // 8-byte explicit nonce || ciphertext(plaintext) || 16-byte GCM tag. + // DTLS 1.2 AEAD: AAD uses the plaintext length (DTLSCompressed.length). let aad = Aad::new_dtls12(content_type, sequence, length); // Encrypt the fragment in-place self.encrypt_data(&mut fragment, aad, nonce)?; let ctext_len = fragment.len(); - // Increase the size to make space for the explicit nonce. - fragment.resize(DTLS_EXPLICIT_NONCE_LEN + ctext_len, 0); - - // Shift the encrypted data to make space for the nonce and write it - fragment.copy_within(0..ctext_len, DTLS_EXPLICIT_NONCE_LEN); - fragment[..DTLS_EXPLICIT_NONCE_LEN].copy_from_slice(&explicit_nonce); + // For suites with a per-record nonce (e.g. AES-GCM), prefix it on the wire. + if explicit_nonce_len > 0 { + fragment.resize(explicit_nonce_len + ctext_len, 0); + fragment.copy_within(0..ctext_len, explicit_nonce_len); + fragment[..explicit_nonce_len] + .copy_from_slice(&explicit_nonce[..explicit_nonce_len]); + } } // Build the record structure referencing the (possibly encrypted) fragment @@ -780,7 +818,13 @@ impl Engine { // Handshake header is 12 bytes let handshake_header_len = 12usize; - let aead_overhead = if epoch >= 1 { DTLS_AEAD_OVERHEAD } else { 0 }; + let aead_overhead = if epoch >= 1 { + self.cipher_suite() + .ok_or_else(|| Error::UnexpectedMessage("No cipher suite selected".to_string()))?; + self.aead_overhead() + } else { + 0 + }; // At least one record must be created even if total_len == 0 while offset < total_len || (total_len == 0 && offset == 0) { @@ -946,6 +990,16 @@ impl Engine { } pub fn set_cipher_suite(&mut self, cipher_suite: Dtls12CipherSuite) { + // Look up AEAD record parameters from the provider (single source of truth) + let provider_suite = self + .crypto_context + .provider() + .cipher_suites + .iter() + .find(|cs| cs.suite() == cipher_suite) + .expect("cipher suite must be in provider"); + self.explicit_nonce_len = provider_suite.explicit_nonce_len(); + self.tag_len = provider_suite.tag_len(); self.cipher_suite = Some(cipher_suite); } @@ -991,13 +1045,17 @@ impl Engine { } pub fn decryption_aad_and_nonce(&self, dtls: &DTLSRecord, buf: &[u8]) -> (Aad, Nonce) { - // DTLS 1.2 AEAD (AES-GCM): AAD uses the plaintext length. The fragment on the wire is - // 8-byte explicit nonce || ciphertext || 16-byte GCM tag. Recover plaintext length from - // the record header's fragment length field. - let plaintext_len = dtls.length.saturating_sub(DTLS_AEAD_OVERHEAD as u16); + // DTLS 1.2 AEAD: AAD uses the plaintext length. Recover plaintext length + // from the record header by subtracting this suite's wire overhead. + let plaintext_len = dtls.length.saturating_sub(self.aead_overhead() as u16); let aad = Aad::new_dtls12(dtls.content_type, dtls.sequence, plaintext_len); let iv = self.peer_iv(); - let nonce = Nonce::new(iv, dtls.nonce(buf)); + let seq64 = ((dtls.sequence.epoch as u64) << 48) | dtls.sequence.sequence_number; + let nonce = match self.explicit_nonce_len { + 0 => Nonce::xor(iv.as_12_bytes(), seq64), + DTLSRecord::EXPLICIT_NONCE_LEN => Nonce::new(iv, dtls.nonce(buf)), + len => Nonce::new(iv, dtls.nonce_with_len(buf, len)), + }; (aad, nonce) } @@ -1055,6 +1113,10 @@ impl RecordDecrypt for Engine { Engine::decryption_aad_and_nonce(self, dtls, buf) } + fn explicit_nonce_len(&self) -> usize { + self.explicit_nonce_len + } + fn decrypt_data( &mut self, ciphertext: &mut TmpBuf, diff --git a/src/dtls12/incoming.rs b/src/dtls12/incoming.rs index ab95b00..26ed3b0 100644 --- a/src/dtls12/incoming.rs +++ b/src/dtls12/incoming.rs @@ -5,7 +5,7 @@ use arrayvec::ArrayVec; use std::fmt; use crate::buffer::{Buf, TmpBuf}; -use crate::crypto::{Aad, Nonce, DTLS_EXPLICIT_NONCE_LEN}; +use crate::crypto::{Aad, Nonce}; use crate::dtls12::message::{ContentType, DTLSRecord, Dtls12CipherSuite, Handshake, Sequence}; use crate::Error; @@ -161,14 +161,15 @@ impl Record { // Extract the buffer for decryption let mut buffer = record.buffer; + let explicit_nonce_len = decrypt.explicit_nonce_len(); // Local shorthand for where the encrypted ciphertext starts - const CIPH: usize = DTLSRecord::HEADER_LEN + DTLS_EXPLICIT_NONCE_LEN; + let ciph = DTLSRecord::HEADER_LEN + explicit_nonce_len; - // The encrypted part is after the DTLS header and explicit nonce. + // The encrypted part is after the DTLS header and optional explicit nonce. // The entire buffer is only the single record, since we chunk // records up in Records::parse() - let ciphertext = &mut buffer[CIPH..]; + let ciphertext = &mut buffer[ciph..]; let new_len = { let mut buffer = TmpBuf::new(ciphertext); @@ -183,7 +184,7 @@ impl Record { buffer[11] = (new_len >> 8) as u8; buffer[12] = new_len as u8; - let parsed = ParsedRecord::parse(&buffer, cs, DTLS_EXPLICIT_NONCE_LEN)?; + let parsed = ParsedRecord::parse(&buffer, cs, explicit_nonce_len)?; let parsed = Box::new(parsed); Ok(Some(Record { buffer, parsed })) @@ -264,6 +265,7 @@ pub trait RecordDecrypt { fn is_peer_encryption_enabled(&self) -> bool; fn replay_check_and_update(&mut self, seq: Sequence) -> bool; fn decryption_aad_and_nonce(&self, dtls: &DTLSRecord, buf: &[u8]) -> (Aad, Nonce); + fn explicit_nonce_len(&self) -> usize; fn decrypt_data( &mut self, ciphertext: &mut TmpBuf, diff --git a/src/dtls12/message/client_hello.rs b/src/dtls12/message/client_hello.rs index d70ce86..2190862 100644 --- a/src/dtls12/message/client_hello.rs +++ b/src/dtls12/message/client_hello.rs @@ -57,9 +57,9 @@ impl ClientHello { // Add supported groups and EC point formats if using ECDH if has_ecdh { - // Add supported groups extension from config (DTLS 1.2 filtered) + // Add supported groups extension from config let mut groups = super::NamedGroupVec::new(); - for kx_group in config.dtls12_kx_groups() { + for kx_group in config.kx_groups() { groups.push(kx_group.name()); } let supported_groups = SupportedGroupsExtension { groups }; diff --git a/src/dtls12/message/handshake.rs b/src/dtls12/message/handshake.rs index f469d15..b429efb 100644 --- a/src/dtls12/message/handshake.rs +++ b/src/dtls12/message/handshake.rs @@ -99,7 +99,7 @@ impl Handshake { let is_fragment = header.fragment_offset > 0 || header.fragment_length < header.length; if !as_fragment && is_fragment { - return Err(nom::Err::Failure(Error::new(input, ErrorKind::LengthValue))); + return Err(Err::Failure(Error::new(input, ErrorKind::LengthValue))); } let (input, body) = if as_fragment { diff --git a/src/dtls12/message/mod.rs b/src/dtls12/message/mod.rs index bbdb3dd..9e0b40a 100644 --- a/src/dtls12/message/mod.rs +++ b/src/dtls12/message/mod.rs @@ -63,6 +63,8 @@ pub enum Dtls12CipherSuite { ECDHE_ECDSA_AES256_GCM_SHA384, // 0xC02C /// ECDHE with ECDSA authentication, AES-128-GCM, SHA-256 ECDHE_ECDSA_AES128_GCM_SHA256, // 0xC02B + /// ECDHE with ECDSA authentication, ChaCha20-Poly1305, SHA-256 + ECDHE_ECDSA_CHACHA20_POLY1305_SHA256, // 0xCCA9 /// Unknown or unsupported cipher suite by its IANA value Unknown(u16), @@ -81,6 +83,7 @@ impl Dtls12CipherSuite { // ECDHE with AES-GCM 0xC02C => Dtls12CipherSuite::ECDHE_ECDSA_AES256_GCM_SHA384, 0xC02B => Dtls12CipherSuite::ECDHE_ECDSA_AES128_GCM_SHA256, + 0xCCA9 => Dtls12CipherSuite::ECDHE_ECDSA_CHACHA20_POLY1305_SHA256, _ => Dtls12CipherSuite::Unknown(value), } @@ -92,6 +95,7 @@ impl Dtls12CipherSuite { // ECDHE with AES-GCM Dtls12CipherSuite::ECDHE_ECDSA_AES256_GCM_SHA384 => 0xC02C, Dtls12CipherSuite::ECDHE_ECDSA_AES128_GCM_SHA256 => 0xC02B, + Dtls12CipherSuite::ECDHE_ECDSA_CHACHA20_POLY1305_SHA256 => 0xCCA9, Dtls12CipherSuite::Unknown(value) => *value, } @@ -108,7 +112,8 @@ impl Dtls12CipherSuite { match self { // AES-GCM suites Dtls12CipherSuite::ECDHE_ECDSA_AES256_GCM_SHA384 - | Dtls12CipherSuite::ECDHE_ECDSA_AES128_GCM_SHA256 => 12, + | Dtls12CipherSuite::ECDHE_ECDSA_AES128_GCM_SHA256 + | Dtls12CipherSuite::ECDHE_ECDSA_CHACHA20_POLY1305_SHA256 => 12, Dtls12CipherSuite::Unknown(_) => 12, // Default length for unknown cipher suites } @@ -119,7 +124,10 @@ impl Dtls12CipherSuite { match self { // All ECDHE ciphers Dtls12CipherSuite::ECDHE_ECDSA_AES256_GCM_SHA384 - | Dtls12CipherSuite::ECDHE_ECDSA_AES128_GCM_SHA256 => KeyExchangeAlgorithm::EECDH, + | Dtls12CipherSuite::ECDHE_ECDSA_AES128_GCM_SHA256 + | Dtls12CipherSuite::ECDHE_ECDSA_CHACHA20_POLY1305_SHA256 => { + KeyExchangeAlgorithm::EECDH + } Dtls12CipherSuite::Unknown(_) => KeyExchangeAlgorithm::Unknown, } @@ -131,25 +139,28 @@ impl Dtls12CipherSuite { self, Dtls12CipherSuite::ECDHE_ECDSA_AES256_GCM_SHA384 | Dtls12CipherSuite::ECDHE_ECDSA_AES128_GCM_SHA256 + | Dtls12CipherSuite::ECDHE_ECDSA_CHACHA20_POLY1305_SHA256 ) } /// All supported cipher suites in server preference order. - pub const fn all() -> &'static [Dtls12CipherSuite; 2] { + pub const fn all() -> &'static [Dtls12CipherSuite; 3] { &[ Dtls12CipherSuite::ECDHE_ECDSA_AES256_GCM_SHA384, Dtls12CipherSuite::ECDHE_ECDSA_AES128_GCM_SHA256, + Dtls12CipherSuite::ECDHE_ECDSA_CHACHA20_POLY1305_SHA256, ] } /// Cipher suites compatible with a given certificate's signature algorithm. pub fn compatible_with_certificate( cert_type: SignatureAlgorithm, - ) -> &'static [Dtls12CipherSuite; 2] { + ) -> &'static [Dtls12CipherSuite; 3] { match cert_type { SignatureAlgorithm::ECDSA => &[ Dtls12CipherSuite::ECDHE_ECDSA_AES256_GCM_SHA384, Dtls12CipherSuite::ECDHE_ECDSA_AES128_GCM_SHA256, + Dtls12CipherSuite::ECDHE_ECDSA_CHACHA20_POLY1305_SHA256, ], _ => panic!("Need either RSA or ECDSA certificate"), } @@ -167,7 +178,8 @@ impl Dtls12CipherSuite { pub fn hash_algorithm(&self) -> HashAlgorithm { match self { Dtls12CipherSuite::ECDHE_ECDSA_AES256_GCM_SHA384 => HashAlgorithm::SHA384, - Dtls12CipherSuite::ECDHE_ECDSA_AES128_GCM_SHA256 => HashAlgorithm::SHA256, + Dtls12CipherSuite::ECDHE_ECDSA_AES128_GCM_SHA256 + | Dtls12CipherSuite::ECDHE_ECDSA_CHACHA20_POLY1305_SHA256 => HashAlgorithm::SHA256, Dtls12CipherSuite::Unknown(_) => HashAlgorithm::Unknown(0), } } @@ -175,8 +187,9 @@ impl Dtls12CipherSuite { /// The signature algorithm associated with the suite's key exchange. pub fn signature_algorithm(&self) -> SignatureAlgorithm { match self { - Dtls12CipherSuite::ECDHE_ECDSA_AES256_GCM_SHA384 => SignatureAlgorithm::ECDSA, - Dtls12CipherSuite::ECDHE_ECDSA_AES128_GCM_SHA256 => SignatureAlgorithm::ECDSA, + Dtls12CipherSuite::ECDHE_ECDSA_AES256_GCM_SHA384 + | Dtls12CipherSuite::ECDHE_ECDSA_AES128_GCM_SHA256 + | Dtls12CipherSuite::ECDHE_ECDSA_CHACHA20_POLY1305_SHA256 => SignatureAlgorithm::ECDSA, Dtls12CipherSuite::Unknown(_) => SignatureAlgorithm::Unknown(0), } } @@ -187,7 +200,7 @@ impl Dtls12CipherSuite { } /// Supported DTLS 1.2 cipher suites in server preference order. - pub const fn supported() -> &'static [Dtls12CipherSuite; 2] { + pub const fn supported() -> &'static [Dtls12CipherSuite; 3] { Self::all() } } diff --git a/src/dtls12/message/record.rs b/src/dtls12/message/record.rs index 25536dd..e22ef78 100644 --- a/src/dtls12/message/record.rs +++ b/src/dtls12/message/record.rs @@ -32,7 +32,7 @@ impl DTLSRecord { /// DTLS record header length: content_type(1) + version(2) + epoch(2) + seq(6) + length(2) pub const HEADER_LEN: usize = 13; - /// Length of the explicit nonce prefix in AEAD ciphers (e.g., AES-GCM) + /// Length of the explicit nonce prefix in DTLS 1.2 AES-GCM records. pub const EXPLICIT_NONCE_LEN: usize = 8; /// Byte offset in the record header where the 2-byte length field is @@ -111,10 +111,15 @@ impl DTLSRecord { output.extend_from_slice(self.fragment(buf)); } - /// Get the explicit nonce from the fragment. - pub fn nonce<'a>(&self, buf: &'a [u8]) -> &'a [u8] { + /// Get the explicit nonce from the fragment using the requested length. + pub fn nonce_with_len<'a>(&self, buf: &'a [u8], len: usize) -> &'a [u8] { let fragment = self.fragment(buf); - &fragment[..Self::EXPLICIT_NONCE_LEN] + &fragment[..len] + } + + /// Get the explicit nonce from the fragment (AES-GCM default). + pub fn nonce<'a>(&self, buf: &'a [u8]) -> &'a [u8] { + self.nonce_with_len(buf, Self::EXPLICIT_NONCE_LEN) } } diff --git a/src/dtls12/server.rs b/src/dtls12/server.rs index 2cf5f2e..2072f30 100644 --- a/src/dtls12/server.rs +++ b/src/dtls12/server.rs @@ -461,9 +461,31 @@ impl State { // unwrap: is ok because we set the random in handle_timeout let server_random = server.random.unwrap(); - // Select ECDHE group from client offers (prefer P-256, then P-384). - // If none present, default to P-256. - let selected_named_group = select_named_group(server.client_supported_groups.as_ref()); + // Select ECDHE group from the intersection of: + // - client offers, and + // - server-allowed DTLS 1.2 groups (provider + config filter) + // Preference follows Config::kx_groups() order. + let allowed_named_groups: Vec = server + .engine + .config() + .kx_groups() + .map(|g| g.name()) + .collect(); + let selected_named_group = select_named_group( + server.client_supported_groups.as_ref(), + &allowed_named_groups, + ) + .ok_or_else(|| { + if server.client_supported_groups.is_some() { + Error::SecurityError( + "No common DTLS 1.2 key exchange group between client supported_groups \ + and server configuration" + .into(), + ) + } else { + Error::CryptoError("No DTLS 1.2 key exchange groups configured".into()) + } + })?; // Select signature/hash for SKE by intersecting client's list // with our key type (prefer SHA256, then SHA384) @@ -1081,18 +1103,80 @@ fn handshake_serialize_certificate_request( Ok(()) } -fn select_named_group(client_groups: Option<&NamedGroupVec>) -> NamedGroup { - // Server preference order - let preferred = [NamedGroup::Secp256r1, NamedGroup::Secp384r1]; +fn select_named_group( + client_groups: Option<&NamedGroupVec>, + server_groups: &[NamedGroup], +) -> Option { if let Some(groups) = client_groups { - for p in preferred.iter() { - if groups.iter().any(|g| g == p) { - return *p; + // Server preference order from Config::kx_groups() + for sg in server_groups { + if groups.iter().any(|g| g == sg) { + return Some(*sg); } } + // Client advertised supported_groups, but there is no overlap. + return None; + } + + // Fallback only when client did not advertise supported_groups: + // pick the first server-configured group. + server_groups.first().copied() +} + +#[cfg(test)] +mod tests { + use super::*; + + fn named_group_vec(groups: &[NamedGroup]) -> NamedGroupVec { + let mut out = NamedGroupVec::new(); + for g in groups { + out.push(*g); + } + out + } + + #[test] + fn select_named_group_prefers_x25519_when_available() { + let client = named_group_vec(&[ + NamedGroup::Secp256r1, + NamedGroup::X25519, + NamedGroup::Secp384r1, + ]); + let provider = [NamedGroup::X25519, NamedGroup::Secp256r1]; + + let selected = select_named_group(Some(&client), &provider); + + assert_eq!(selected, Some(NamedGroup::X25519)); + } + + #[test] + fn select_named_group_respects_provider_capabilities() { + let client = named_group_vec(&[NamedGroup::X25519, NamedGroup::Secp256r1]); + let provider = [NamedGroup::Secp256r1]; + + let selected = select_named_group(Some(&client), &provider); + + assert_eq!(selected, Some(NamedGroup::Secp256r1)); + } + + #[test] + fn select_named_group_falls_back_to_provider_when_client_missing() { + let provider = [NamedGroup::Secp384r1]; + + let selected = select_named_group(None, &provider); + + assert_eq!(selected, Some(NamedGroup::Secp384r1)); + } + + #[test] + fn select_named_group_rejects_when_client_has_no_overlap() { + let client = named_group_vec(&[NamedGroup::X25519]); + let provider = [NamedGroup::Secp256r1]; + + let selected = select_named_group(Some(&client), &provider); + + assert_eq!(selected, None); } - // Fallback if client did not advertise groups or only unsupported ones - NamedGroup::Secp256r1 } fn select_ske_signature_algorithm( diff --git a/src/dtls13/client.rs b/src/dtls13/client.rs index f4a8908..56efe23 100644 --- a/src/dtls13/client.rs +++ b/src/dtls13/client.rs @@ -1323,7 +1323,7 @@ pub(crate) fn signature_scheme_to_components( crate::types::HashAlgorithm, crate::types::SignatureAlgorithm, ), - crate::Error, + Error, > { use crate::types::{HashAlgorithm, SignatureAlgorithm}; match scheme { @@ -1366,10 +1366,7 @@ pub(crate) fn signature_scheme_to_components( /// RFC 8446 ยง4.4.3: For ECDSA schemes, verify the curve in the [`SignatureScheme`] /// matches the certificate's public key curve. #[cfg(feature = "_crypto-common")] -pub(crate) fn verify_scheme_curve( - scheme: SignatureScheme, - cert_der: &[u8], -) -> Result<(), crate::Error> { +pub(crate) fn verify_scheme_curve(scheme: SignatureScheme, cert_der: &[u8]) -> Result<(), Error> { if let Some(expected_group) = scheme.named_group() { let cert_group = crate::crypto::cert_named_group(cert_der).map_err(Error::SecurityError)?; if expected_group != cert_group { diff --git a/src/dtls13/engine.rs b/src/dtls13/engine.rs index dca03fe..d6c2e6f 100644 --- a/src/dtls13/engine.rs +++ b/src/dtls13/engine.rs @@ -2038,7 +2038,7 @@ impl Engine { // Append the preserved tail (HRR bytes) new_transcript.extend_from_slice(&self.transcript[split_at..]); - let old = std::mem::replace(&mut self.transcript, new_transcript); + let old = mem::replace(&mut self.transcript, new_transcript); self.buffers_free.push(old); } diff --git a/src/dtls13/message/handshake.rs b/src/dtls13/message/handshake.rs index 6856002..05d224e 100644 --- a/src/dtls13/message/handshake.rs +++ b/src/dtls13/message/handshake.rs @@ -91,7 +91,7 @@ impl Handshake { let is_fragment = header.fragment_offset > 0 || header.fragment_length < header.length; if !as_fragment && is_fragment { - return Err(nom::Err::Failure(Error::new(input, ErrorKind::LengthValue))); + return Err(Err::Failure(Error::new(input, ErrorKind::LengthValue))); } let (input, body) = if as_fragment { diff --git a/src/dtls13/server.rs b/src/dtls13/server.rs index 7884d8d..7014398 100644 --- a/src/dtls13/server.rs +++ b/src/dtls13/server.rs @@ -524,7 +524,7 @@ impl State { .find(|g| client_groups.contains(g)) .copied(); - if let Some(group) = common_group { + return if let Some(group) = common_group { // Need HRR for key exchange if server.hello_retry { return Err(Error::SecurityError( @@ -560,12 +560,12 @@ impl State { server.engine.reset_for_hello_retry(); server.hello_retry = true; - return Ok(Self::AwaitClientHello); + Ok(Self::AwaitClientHello) } else { - return Err(Error::SecurityError( + Err(Error::SecurityError( "No common key exchange group".to_string(), - )); - } + )) + }; }; // Start ECDHE key exchange diff --git a/src/lib.rs b/src/lib.rs index 5819bc1..46adb1b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,6 +31,7 @@ //! - **Cipher suites (TLS 1.2 over DTLS)** //! - `ECDHE_ECDSA_AES256_GCM_SHA384` //! - `ECDHE_ECDSA_AES128_GCM_SHA256` +//! - `ECDHE_ECDSA_CHACHA20_POLY1305_SHA256` //! - **Cipher suites (TLS 1.3 over DTLS)** //! - `TLS_AES_128_GCM_SHA256` //! - `TLS_AES_256_GCM_SHA384` @@ -105,6 +106,7 @@ //! Output::ApplicationData(_data) => { //! // Deliver plaintext to application //! } +//! _ => {} //! } //! } //! @@ -212,8 +214,8 @@ pub struct DtlsCertificate { pub private_key: Vec, } -impl std::fmt::Debug for DtlsCertificate { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl fmt::Debug for DtlsCertificate { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("DtlsCertificate") .field("certificate", &self.certificate.len()) .field("private_key", &self.private_key.len()) diff --git a/src/types.rs b/src/types.rs index befbd39..7f94163 100644 --- a/src/types.rs +++ b/src/types.rs @@ -841,7 +841,7 @@ impl ProtocolVersion { } /// Serialize this `ProtocolVersion` to wire format. - pub fn serialize(&self, output: &mut crate::buffer::Buf) { + pub fn serialize(&self, output: &mut Buf) { output.extend_from_slice(&self.as_u16().to_be_bytes()); } } diff --git a/tests/dtls12/crypto.rs b/tests/dtls12/crypto.rs index b75b0b2..b95f77f 100644 --- a/tests/dtls12/crypto.rs +++ b/tests/dtls12/crypto.rs @@ -9,6 +9,59 @@ use dimpl::{Config, Dtls, Output}; use crate::ossl_helper::{DtlsCertOptions, DtlsEvent, DtlsPKeyType, OsslDtlsCert}; +const GROUPS_PREFER_X25519: &str = "X25519:P-256:P-384"; +const NAMED_GROUP_X25519: u16 = 0x001D; + +fn read_u24(bytes: &[u8]) -> usize { + ((bytes[0] as usize) << 16) | ((bytes[1] as usize) << 8) | (bytes[2] as usize) +} + +fn find_server_key_exchange_group(packet: &[u8]) -> Option { + let mut rec_offset = 0usize; + while rec_offset + 13 <= packet.len() { + let content_type = packet[rec_offset]; + let rec_len = + u16::from_be_bytes([packet[rec_offset + 11], packet[rec_offset + 12]]) as usize; + let rec_start = rec_offset + 13; + let rec_end = rec_start + rec_len; + if rec_end > packet.len() { + return None; + } + + // Handshake records only (22) + if content_type == 22 { + let mut hs_offset = rec_start; + while hs_offset + 12 <= rec_end { + let hs_type = packet[hs_offset]; + let _hs_len = read_u24(&packet[hs_offset + 1..hs_offset + 4]); + let frag_offset = read_u24(&packet[hs_offset + 6..hs_offset + 9]); + let frag_len = read_u24(&packet[hs_offset + 9..hs_offset + 12]); + let body_start = hs_offset + 12; + let body_end = body_start + frag_len; + if body_end > rec_end { + break; + } + + // ServerKeyExchange (12), first fragment, ECDHE params: + // curve_type(1) + named_group(2) + pubkey_len(1) + pubkey + signature... + if hs_type == 12 && frag_offset == 0 && frag_len >= 4 && packet[body_start] == 3 { + let group = + u16::from_be_bytes([packet[body_start + 1], packet[body_start + 2]]); + return Some(group); + } + + if frag_len == 0 { + break; + } + hs_offset = body_end; + } + } + + rec_offset = rec_end; + } + None +} + #[test] fn dtls12_all_cipher_suites() { let _ = env_logger::try_init(); @@ -24,6 +77,27 @@ fn dtls12_all_cipher_suites() { } } +fn config_for_suite(suite: Dtls12CipherSuite) -> Arc { + let mut provider = Config::default().crypto_provider().clone(); + let selected = provider + .cipher_suites + .iter() + .copied() + .find(|cs| cs.suite() == suite) + .unwrap_or_else(|| panic!("Suite {:?} not found in provider", suite)); + + // Leak a tiny fixed-size slice so it can satisfy the provider's 'static requirement. + let suites = Box::leak(Box::new([selected])); + provider.cipher_suites = suites; + + Arc::new( + Config::builder() + .with_crypto_provider(provider) + .build() + .expect("build config for single suite"), + ) +} + fn run_dimpl_client_vs_ossl_server_for_suite(suite: Dtls12CipherSuite) { // Generate certificates for both client and server matching the suite's signature algorithm let pkey_type = match suite.signature_algorithm() { @@ -44,13 +118,12 @@ fn run_dimpl_client_vs_ossl_server_for_suite(suite: Dtls12CipherSuite) { // Create OpenSSL server impl let mut server = server_cert - .new_dtls_impl() + .new_dtls_impl_with_groups(GROUPS_PREFER_X25519) .expect("Failed to create DTLS server"); server.set_active(false); - // Initialize dimpl client restricted to the single suite - // Note: cipher suites are determined by the crypto provider - let config = Arc::new(Config::default()); + // Initialize dimpl client restricted to the single suite. + let config = config_for_suite(suite); // DER encodings for our client let client_x509_der = client_cert.x509.to_der().expect("client cert der"); @@ -74,6 +147,7 @@ fn run_dimpl_client_vs_ossl_server_for_suite(suite: Dtls12CipherSuite) { let mut server_events = VecDeque::new(); let mut client_connected = false; let mut server_connected = false; + let mut server_kx_group = None; let mut out_buf = vec![0u8; 2048]; for _ in 0..60 { @@ -103,6 +177,9 @@ fn run_dimpl_client_vs_ossl_server_for_suite(suite: Dtls12CipherSuite) { // Send server datagrams back to client while let Some(datagram) = server.poll_datagram() { + if server_kx_group.is_none() { + server_kx_group = find_server_key_exchange_group(&datagram); + } client .handle_packet(&datagram) .expect("Failed to handle server packet"); @@ -123,6 +200,12 @@ fn run_dimpl_client_vs_ossl_server_for_suite(suite: Dtls12CipherSuite) { "Server should connect for suite {:?}", suite ); + assert_eq!( + server_kx_group, + Some(NAMED_GROUP_X25519), + "OpenSSL server should negotiate X25519 for suite {:?}", + suite + ); } fn run_ossl_client_vs_dimpl_server_for_suite(suite: Dtls12CipherSuite) { @@ -144,13 +227,12 @@ fn run_ossl_client_vs_dimpl_server_for_suite(suite: Dtls12CipherSuite) { // OpenSSL DTLS client let mut ossl_client = client_cert - .new_dtls_impl() + .new_dtls_impl_with_groups(GROUPS_PREFER_X25519) .expect("Failed to create DTLS client"); ossl_client.set_active(true); - // dimpl server with single-suite config - // Note: cipher suites are determined by the crypto provider - let config = Arc::new(Config::default()); + // dimpl server with single-suite config. + let config = config_for_suite(suite); let server_x509_der = server_cert.x509.to_der().expect("server cert der"); let server_pkey_der = server_cert @@ -174,6 +256,7 @@ fn run_ossl_client_vs_dimpl_server_for_suite(suite: Dtls12CipherSuite) { let mut client_events = VecDeque::new(); let mut server_connected = false; let mut client_connected = false; + let mut server_kx_group = None; let mut out_buf = vec![0u8; 2048]; for _ in 0..60 { @@ -191,6 +274,9 @@ fn run_ossl_client_vs_dimpl_server_for_suite(suite: Dtls12CipherSuite) { loop { match server.poll_output(&mut out_buf) { Output::Packet(data) => { + if server_kx_group.is_none() { + server_kx_group = find_server_key_exchange_group(data); + } ossl_client .handle_receive(data, &mut client_events) .expect("Client failed to handle server packet"); @@ -232,4 +318,10 @@ fn run_ossl_client_vs_dimpl_server_for_suite(suite: Dtls12CipherSuite) { "Client should connect for suite {:?}", suite ); + assert_eq!( + server_kx_group, + Some(NAMED_GROUP_X25519), + "dimpl server should negotiate X25519 for suite {:?}", + suite + ); } diff --git a/tests/dtls13/handshake.rs b/tests/dtls13/handshake.rs index f39b8ac..9e13512 100644 --- a/tests/dtls13/handshake.rs +++ b/tests/dtls13/handshake.rs @@ -598,7 +598,7 @@ fn dtls13_handshake_x25519_key_exchange() { let server_cert = generate_self_signed_certificate().expect("gen server cert"); // Use config filter to select only X25519 and disable DTLS 1.2 - // (X25519 is not yet supported for DTLS 1.2) + // to keep this test focused on DTLS 1.3 behavior. let config = Arc::new( Config::builder() .kx_groups(&[NamedGroup::X25519]) diff --git a/tests/dtls13/wolfssl.rs b/tests/dtls13/wolfssl.rs index ee67b84..a175792 100644 --- a/tests/dtls13/wolfssl.rs +++ b/tests/dtls13/wolfssl.rs @@ -1973,21 +1973,17 @@ fn dtls13_wolfssl_server_bidirectional_data() { ); } -// NOTE: HRR (HelloRetryRequest) test is skipped because dimpl only supports P-256 and -// P-384 key exchange groups, and WolfSSL DTLS 1.3 accepts both of these groups. There is -// no way to configure the dimpl client to offer a key share that WolfSSL would reject -// (triggering HRR for a different group) while still being able to complete the handshake. -// The CryptoProvider's kx_groups list determines which group is offered first, but both -// groups are acceptable to WolfSSL. HRR is tested separately in the dimpl-only tests. +// NOTE: HRR (HelloRetryRequest) test is skipped because in the current WolfSSL interop +// setup, WolfSSL accepts all key-share groups dimpl offers. There is no way to configure +// the dimpl client to offer an initial key share that WolfSSL rejects (to trigger HRR) +// while still being able to complete the handshake. HRR is tested separately in the +// dimpl-only tests. #[test] #[ignore = "cannot trigger HRR: WolfSSL accepts all groups dimpl offers"] #[cfg(feature = "rcgen")] fn dtls13_wolfssl_client_hrr_flow() { - // Cannot trigger HRR with the current WolfSSL + dimpl configuration. - // dimpl offers P-256 or P-384, and WolfSSL accepts both without requesting - // a HelloRetryRequest. To trigger HRR, dimpl would need to offer a group - // that WolfSSL doesn't accept (e.g. X25519 only), but dimpl currently does - // not support X25519. + // Cannot trigger HRR with the current WolfSSL + dimpl configuration: + // WolfSSL accepts the groups dimpl offers in this test setup. } #[test] diff --git a/tests/ossl/cert.rs b/tests/ossl/cert.rs index 9bc22ef..2e83ad7 100644 --- a/tests/ossl/cert.rs +++ b/tests/ossl/cert.rs @@ -61,7 +61,7 @@ impl OsslDtlsCert { // The libWebRTC code we try to match is at: // https://webrtc.googlesource.com/src/+/1568f1b1330f94494197696fe235094e6293b258/rtc_base/openssl_certificate.cc#58 fn self_signed(options: DtlsCertOptions) -> Result { - let f4 = BigNum::from_u32(RSA_F4).unwrap(); + let f4 = BigNum::from_u32(RSA_F4)?; let pkey = match options.pkey_type { DtlsPKeyType::Rsa2048 => { let key = Rsa::generate_with_e(2048, &f4)?; @@ -137,6 +137,13 @@ impl OsslDtlsCert { pub fn new_dtls_impl(&self) -> Result { OsslDtlsImpl::new(self.clone()) } + + pub fn new_dtls_impl_with_groups( + &self, + groups_list: &str, + ) -> Result { + OsslDtlsImpl::new_with_groups(self.clone(), Some(groups_list)) + } } #[derive(Debug)] diff --git a/tests/ossl/dtls.rs b/tests/ossl/dtls.rs index 31a6456..04380da 100644 --- a/tests/ossl/dtls.rs +++ b/tests/ossl/dtls.rs @@ -11,9 +11,10 @@ use super::io_buf::IoBuffer; use super::stream::TlsStream; use super::{CryptoError, DtlsEvent, DATAGRAM_MTU, DATAGRAM_MTU_WARN}; -// We restrict cipher suites to those that include ephermeral Diffie-Hellman or ephemeral -// Elliptical Curve Diffie-Hellman AND AES-256 or AES-GCM. -const DTLS_CIPHERS: &str = "ECDHE+AESGCM:DHE+AESGCM:ECDHE+AES256:DHE+AES256"; +// We restrict cipher suites to those that include ephemeral Diffie-Hellman or +// ephemeral Elliptic Curve Diffie-Hellman and modern AEAD ciphers. +const DTLS_CIPHERS: &str = + "ECDHE+CHACHA20:DHE+CHACHA20:ECDHE+AESGCM:DHE+AESGCM:ECDHE+AES256:DHE+AES256"; pub struct OsslDtlsImpl { /// Certificate for the DTLS session. @@ -30,8 +31,15 @@ pub struct OsslDtlsImpl { } impl OsslDtlsImpl { - pub fn new(cert: OsslDtlsCert) -> Result { - let context = dtls_create_ctx(&cert)?; + pub fn new(cert: OsslDtlsCert) -> Result { + Self::new_with_groups(cert, None) + } + + pub fn new_with_groups( + cert: OsslDtlsCert, + groups_list: Option<&str>, + ) -> Result { + let context = dtls_create_ctx_with_groups(&cert, groups_list)?; let ssl = dtls_ssl_create(&context)?; Ok(OsslDtlsImpl { _cert: cert, @@ -133,6 +141,13 @@ impl OsslDtlsImpl { } pub fn dtls_create_ctx(cert: &OsslDtlsCert) -> Result { + dtls_create_ctx_with_groups(cert, None) +} + +pub fn dtls_create_ctx_with_groups( + cert: &OsslDtlsCert, + groups_list: Option<&str>, +) -> Result { // TODO: Technically we want to disallow DTLS < 1.2, but that requires // us to use this commented out unsafe. We depend on browsers disallowing // it instead. @@ -140,6 +155,9 @@ pub fn dtls_create_ctx(cert: &OsslDtlsCert) -> Result { let mut ctx = SslContextBuilder::new(SslMethod::dtls())?; ctx.set_cipher_list(DTLS_CIPHERS)?; + if let Some(groups) = groups_list { + ctx.set_groups_list(groups)?; + } let srtp_profiles = { // Rust can't join directly to a string, need to allocate a vec first :( // This happens very rarely so the extra allocations don't matter From 062ca39f0b6028df15733227170cc321d05940e7 Mon Sep 17 00:00:00 2001 From: Martin Algesten Date: Thu, 5 Mar 2026 21:37:46 +0100 Subject: [PATCH 4/5] changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50bb3bd..0fe8308 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # Unreleased + * Add DTLS 1.2 ChaCha20 and X25519 support #77 * Bump MSRV to 1.85.0 #75 * Make cipher and kx configurable #73 - * Add DTLS 1.2 ChaCha20-Poly1305 and X25519 support #71 # 0.4.0 From e99193bd56823f650d57b380c89741ba1adc421d Mon Sep 17 00:00:00 2001 From: Bruce Wayne Date: Fri, 6 Mar 2026 09:00:03 +0800 Subject: [PATCH 5/5] fix: use assert_eq in Iv::as_12_bytes to prevent silent nonce reuse in release builds --- src/crypto/dtls_aead.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/dtls_aead.rs b/src/crypto/dtls_aead.rs index 1f16d9e..d223457 100644 --- a/src/crypto/dtls_aead.rs +++ b/src/crypto/dtls_aead.rs @@ -87,7 +87,7 @@ impl Iv { /// Only valid for 12-byte IVs (ChaCha20-Poly1305). For 4-byte IVs /// (AES-GCM), use [`as_slice`] instead. pub(crate) fn as_12_bytes(&self) -> &[u8; 12] { - debug_assert_eq!( + assert_eq!( self.len(), 12, "as_12_bytes called on {}-byte IV",