Skip to content

Commit 51543d3

Browse files
committed
feat: add DTLS 1.2 ChaCha20 and X25519 support
1 parent b468f1e commit 51543d3

30 files changed

Lines changed: 602 additions & 187 deletions

src/buffer.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ impl BufferPool {
3838
}
3939

4040
impl fmt::Debug for BufferPool {
41-
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4242
f.debug_struct("BufferPool")
4343
.field("free", &self.free.len())
4444
.finish()
@@ -118,7 +118,7 @@ impl AsMut<[u8]> for Buf {
118118
}
119119

120120
impl fmt::Debug for Buf {
121-
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122122
f.debug_struct("Buf").field("len", &self.0.len()).finish()
123123
}
124124
}

src/config.rs

Lines changed: 16 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -198,15 +198,6 @@ impl Config {
198198
None => true,
199199
})
200200
}
201-
202-
/// Allowed key exchange groups for DTLS 1.2.
203-
///
204-
/// Like [`kx_groups`](Self::kx_groups) but additionally restricted to
205-
/// groups that DTLS 1.2 supports (currently P-256 and P-384).
206-
pub fn dtls12_kx_groups(&self) -> impl Iterator<Item = &'static dyn SupportedKxGroup> + '_ {
207-
self.kx_groups()
208-
.filter(|kx| matches!(kx.name(), NamedGroup::Secp256r1 | NamedGroup::Secp384r1))
209-
}
210201
}
211202

212203
/// Builder for [`Config`]. See each setter for defaults.
@@ -446,13 +437,13 @@ impl ConfigBuilder {
446437
};
447438
if dtls12_count > 0 {
448439
let dtls12_kx_count = crypto_provider
449-
.supported_dtls12_kx_groups()
440+
.supported_kx_groups()
450441
.filter(|kx| filtered_kx(kx))
451442
.count();
452443
if dtls12_kx_count == 0 {
453444
return Err(Error::ConfigError(
454445
"DTLS 1.2 cipher suites are enabled but no compatible key exchange \
455-
groups remain after filtering. DTLS 1.2 requires P-256 or P-384."
446+
groups remain after filtering."
456447
.to_string(),
457448
));
458449
}
@@ -635,23 +626,15 @@ mod tests {
635626
}
636627

637628
#[test]
638-
fn x25519_only_rejected_for_dtls12() {
639-
// X25519 is not yet supported for DTLS 1.2, so filtering to X25519-only
640-
// while DTLS 1.2 suites are enabled should fail.
641-
match Config::builder()
629+
fn x25519_only_accepted_for_dtls12() {
630+
// X25519 is supported for DTLS 1.2 and should be accepted.
631+
let config = Config::builder()
642632
.dtls13_cipher_suites(&[])
643633
.kx_groups(&[NamedGroup::X25519])
644634
.build()
645-
{
646-
Err(Error::ConfigError(msg)) => {
647-
assert!(
648-
msg.contains("DTLS 1.2") && msg.contains("P-256 or P-384"),
649-
"error should mention DTLS 1.2 and required groups: {msg}"
650-
)
651-
}
652-
Err(other) => panic!("expected ConfigError, got: {other:?}"),
653-
Ok(_) => panic!("expected error for X25519-only with DTLS 1.2"),
654-
}
635+
.expect("X25519-only should be accepted for DTLS 1.2");
636+
let groups: Vec<_> = config.kx_groups().map(|g| g.name()).collect();
637+
assert_eq!(groups, &[NamedGroup::X25519]);
655638
}
656639

657640
#[test]
@@ -667,11 +650,15 @@ mod tests {
667650
}
668651

669652
#[test]
670-
fn dtls12_kx_groups_excludes_x25519() {
653+
fn kx_groups_match_provider_when_unfiltered() {
671654
let config = Config::default();
672-
let dtls12_groups: Vec<_> = config.dtls12_kx_groups().map(|g| g.name()).collect();
673-
assert!(!dtls12_groups.contains(&NamedGroup::X25519));
674-
assert!(dtls12_groups.contains(&NamedGroup::Secp256r1));
655+
let from_config: Vec<_> = config.kx_groups().map(|g| g.name()).collect();
656+
let from_provider: Vec<_> = config
657+
.crypto_provider()
658+
.supported_kx_groups()
659+
.map(|g| g.name())
660+
.collect();
661+
assert_eq!(from_config, from_provider);
675662
}
676663

677664
#[test]

src/crypto/aws_lc_rs/cipher_suite.rs

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,14 @@ impl SupportedDtls12CipherSuite for Aes128GcmSha256 {
159159
(0, 16, 4) // (mac_key_len, enc_key_len, fixed_iv_len)
160160
}
161161

162+
fn explicit_nonce_len(&self) -> usize {
163+
8
164+
}
165+
166+
fn tag_len(&self) -> usize {
167+
16
168+
}
169+
162170
fn create_cipher(&self, key: &[u8]) -> Result<Box<dyn Cipher>, String> {
163171
Ok(Box::new(AesGcm::new(key)?))
164172
}
@@ -181,18 +189,60 @@ impl SupportedDtls12CipherSuite for Aes256GcmSha384 {
181189
(0, 32, 4) // (mac_key_len, enc_key_len, fixed_iv_len)
182190
}
183191

192+
fn explicit_nonce_len(&self) -> usize {
193+
8
194+
}
195+
196+
fn tag_len(&self) -> usize {
197+
16
198+
}
199+
184200
fn create_cipher(&self, key: &[u8]) -> Result<Box<dyn Cipher>, String> {
185201
Ok(Box::new(AesGcm::new(key)?))
186202
}
187203
}
188204

205+
/// TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 cipher suite.
206+
#[derive(Debug)]
207+
struct ChaCha20Poly1305Sha256;
208+
209+
impl SupportedDtls12CipherSuite for ChaCha20Poly1305Sha256 {
210+
fn suite(&self) -> Dtls12CipherSuite {
211+
Dtls12CipherSuite::ECDHE_ECDSA_CHACHA20_POLY1305_SHA256
212+
}
213+
214+
fn hash_algorithm(&self) -> HashAlgorithm {
215+
HashAlgorithm::SHA256
216+
}
217+
218+
fn key_lengths(&self) -> (usize, usize, usize) {
219+
(0, 32, 12) // (mac_key_len, enc_key_len, fixed_iv_len)
220+
}
221+
222+
fn explicit_nonce_len(&self) -> usize {
223+
0
224+
}
225+
226+
fn tag_len(&self) -> usize {
227+
16
228+
}
229+
230+
fn create_cipher(&self, key: &[u8]) -> Result<Box<dyn Cipher>, String> {
231+
Ok(Box::new(ChaCha20Poly1305Cipher::new(key)?))
232+
}
233+
}
234+
189235
/// Static instances of supported DTLS 1.2 cipher suites.
190236
static AES_128_GCM_SHA256: Aes128GcmSha256 = Aes128GcmSha256;
191237
static AES_256_GCM_SHA384: Aes256GcmSha384 = Aes256GcmSha384;
238+
static CHACHA20_POLY1305_SHA256: ChaCha20Poly1305Sha256 = ChaCha20Poly1305Sha256;
192239

193240
/// All supported DTLS 1.2 cipher suites.
194-
pub(super) static ALL_CIPHER_SUITES: &[&dyn SupportedDtls12CipherSuite] =
195-
&[&AES_128_GCM_SHA256, &AES_256_GCM_SHA384];
241+
pub(super) static ALL_CIPHER_SUITES: &[&dyn SupportedDtls12CipherSuite] = &[
242+
&AES_128_GCM_SHA256,
243+
&AES_256_GCM_SHA384,
244+
&CHACHA20_POLY1305_SHA256,
245+
];
196246

197247
// ============================================================================
198248
// DTLS 1.3 Cipher Suites

src/crypto/aws_lc_rs/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,11 @@ use super::CryptoProvider;
7777
///
7878
/// - `TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256` (0xC02B)
7979
/// - `TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384` (0xC02C)
80+
/// - `TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256` (0xCCA9)
8081
///
8182
/// # Supported Key Exchange Groups
8283
///
84+
/// - `x25519` (X25519 / Curve25519)
8385
/// - `secp256r1` (P-256, NIST Curve)
8486
/// - `secp384r1` (P-384, NIST Curve)
8587
///

src/crypto/dtls_aead.rs

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,35 +10,37 @@ use crate::types::{ContentType, Sequence};
1010
/// Explicit nonce length for DTLS AEAD records.
1111
///
1212
/// The explicit nonce is transmitted with each record.
13+
#[cfg(test)]
1314
pub(crate) const DTLS_EXPLICIT_NONCE_LEN: usize = 8;
1415

1516
/// GCM authentication tag length.
1617
///
1718
/// The tag is appended to the ciphertext.
19+
#[cfg(test)]
1820
pub(crate) const GCM_TAG_LEN: usize = 16;
1921

20-
/// Overhead per AEAD record (explicit nonce + tag).
22+
/// Overhead per DTLS 1.2 AES-GCM record (explicit nonce + tag).
2123
///
2224
/// This equals 24 bytes for DTLS AES-GCM.
25+
#[cfg(test)]
2326
pub(crate) const DTLS_AEAD_OVERHEAD: usize = DTLS_EXPLICIT_NONCE_LEN + GCM_TAG_LEN; // 24
2427

25-
/// Compute AAD length from plaintext length for AEAD records.
26-
/// For DTLS AEAD this is the plaintext length.
28+
/// Compute AAD length from plaintext length for DTLS 1.2 AES-GCM records.
2729
#[inline]
2830
#[cfg(test)]
2931
pub fn aad_len_from_plaintext_len(plaintext_len: u16) -> u16 {
3032
plaintext_len
3133
}
3234

33-
/// Compute fragment length from plaintext length for AEAD records.
35+
/// Compute fragment length from plaintext length for DTLS 1.2 AES-GCM records.
3436
/// fragment_len = explicit_nonce(8) + ciphertext(plaintext_len + 16 tag)
3537
#[inline]
3638
#[cfg(test)]
3739
pub fn fragment_len_from_plaintext_len(plaintext_len: usize) -> usize {
3840
DTLS_EXPLICIT_NONCE_LEN + plaintext_len + GCM_TAG_LEN
3941
}
4042

41-
/// Compute plaintext length from fragment length, if large enough.
43+
/// Compute plaintext length from fragment length for DTLS 1.2 AES-GCM records.
4244
/// Returns None if the fragment is smaller than the mandatory AEAD overhead.
4345
#[inline]
4446
#[cfg(test)]
@@ -47,13 +49,51 @@ pub fn plaintext_len_from_fragment_len(fragment_len: usize) -> Option<usize> {
4749
}
4850

4951
/// Fixed IV portion for DTLS AEAD.
52+
///
53+
/// DTLS 1.2 uses:
54+
/// - AES-GCM: 4-byte fixed IV + 8-byte explicit nonce (per record)
55+
/// - ChaCha20-Poly1305: 12-byte fixed IV + 0-byte explicit nonce
5056
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51-
pub(crate) struct Iv(pub [u8; 4]);
57+
pub(crate) struct Iv {
58+
bytes: [u8; 12],
59+
len: u8,
60+
}
5261

5362
impl Iv {
5463
pub(crate) fn new(iv: &[u8]) -> Self {
55-
// invariant: the iv is 4 bytes.
56-
Self(iv.try_into().unwrap())
64+
assert!(
65+
iv.len() <= 12,
66+
"invalid IV length: expected <= 12, got {}",
67+
iv.len()
68+
);
69+
let mut bytes = [0u8; 12];
70+
bytes[..iv.len()].copy_from_slice(iv);
71+
Self {
72+
bytes,
73+
len: iv.len() as u8,
74+
}
75+
}
76+
77+
pub(crate) fn len(&self) -> usize {
78+
self.len as usize
79+
}
80+
81+
pub(crate) fn as_slice(&self) -> &[u8] {
82+
&self.bytes[..self.len()]
83+
}
84+
85+
/// Returns the full 12-byte backing array.
86+
///
87+
/// Only valid for 12-byte IVs (ChaCha20-Poly1305). For 4-byte IVs
88+
/// (AES-GCM), use [`as_slice`] instead.
89+
pub(crate) fn as_12_bytes(&self) -> &[u8; 12] {
90+
debug_assert_eq!(
91+
self.len(),
92+
12,
93+
"as_12_bytes called on {}-byte IV",
94+
self.len()
95+
);
96+
&self.bytes
5797
}
5898
}
5999

@@ -64,15 +104,25 @@ pub struct Nonce(pub [u8; 12]);
64104
impl Nonce {
65105
/// Create a new AEAD nonce by combining fixed IV and explicit nonce (DTLS 1.2).
66106
pub(crate) fn new(iv: Iv, explicit_nonce: &[u8]) -> Self {
107+
assert_eq!(
108+
iv.len() + explicit_nonce.len(),
109+
12,
110+
"invalid DTLS 1.2 nonce parts: iv_len={}, explicit_nonce_len={}",
111+
iv.len(),
112+
explicit_nonce.len()
113+
);
67114
let mut nonce = [0u8; 12];
68-
nonce[..4].copy_from_slice(&iv.0);
69-
nonce[4..].copy_from_slice(explicit_nonce);
115+
let iv_len = iv.len();
116+
nonce[..iv_len].copy_from_slice(iv.as_slice());
117+
nonce[iv_len..].copy_from_slice(explicit_nonce);
70118
Self(nonce)
71119
}
72120

73-
/// Create a DTLS 1.3 nonce by XORing the IV with the padded sequence number.
121+
/// Create a nonce by XORing the IV with the padded sequence number.
74122
///
75-
/// Per RFC 8446 Section 5.3: nonce = iv XOR pad_left(seq, iv_len)
123+
/// Used by both DTLS 1.2 (ChaCha20-Poly1305) and DTLS 1.3:
124+
/// nonce = iv XOR pad_left(sequence_number, 12)
125+
/// See RFC 8446 Section 5.3 / RFC 7905.
76126
pub(crate) fn xor(iv: &[u8; 12], seq: u64) -> Self {
77127
let mut nonce = *iv;
78128
let seq_bytes = seq.to_be_bytes(); // 8 bytes

src/crypto/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ pub use keying::{KeyingMaterial, SrtpProfile};
2222
pub use dtls_aead::{Aad, Nonce};
2323

2424
// Re-export internal AEAD constants/types for crate-internal use
25-
pub(crate) use dtls_aead::{Iv, DTLS_AEAD_OVERHEAD, DTLS_EXPLICIT_NONCE_LEN};
25+
pub(crate) use dtls_aead::Iv;
2626

2727
// Re-export buffer types for provider trait implementations
2828
pub use crate::buffer::{Buf, TmpBuf};

src/crypto/provider.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,14 @@
109109
//! (0, 16, 4) // (mac_key_len, enc_key_len, fixed_iv_len)
110110
//! }
111111
//!
112+
//! fn explicit_nonce_len(&self) -> usize {
113+
//! 8 // AES-GCM: 8-byte explicit nonce per record
114+
//! }
115+
//!
116+
//! fn tag_len(&self) -> usize {
117+
//! 16 // 128-bit authentication tag
118+
//! }
119+
//!
112120
//! fn create_cipher(&self, key: &[u8]) -> Result<Box<dyn Cipher>, String> {
113121
//! // Create your cipher implementation here
114122
//! Ok(Box::new(MyCipher::new(key)?))
@@ -123,8 +131,8 @@
123131
//!
124132
//! For DTLS 1.2, implementations must support:
125133
//!
126-
//! - **Cipher suites**: ECDHE_ECDSA with AES-128-GCM or AES-256-GCM
127-
//! - **Key exchange**: ECDHE with P-256 or P-384 curves
134+
//! - **Cipher suites**: ECDHE_ECDSA with AES-128-GCM, AES-256-GCM, or CHACHA20_POLY1305
135+
//! - **Key exchange**: ECDHE with X25519, P-256, or P-384 curves
128136
//! - **Signatures**: ECDSA with P-256/SHA-256 or P-384/SHA-384
129137
//! - **Hash**: SHA-256 and SHA-384
130138
//! - **PRF**: TLS 1.2 PRF (using HMAC-SHA256 or HMAC-SHA384)
@@ -231,6 +239,14 @@ pub trait SupportedDtls12CipherSuite: CryptoSafe {
231239
/// Key material lengths: (mac_key_len, enc_key_len, fixed_iv_len).
232240
fn key_lengths(&self) -> (usize, usize, usize);
233241

242+
/// Length in bytes of the per-record explicit nonce (carried in the record body).
243+
///
244+
/// AES-GCM suites carry an 8-byte explicit nonce; ChaCha20-Poly1305 carries none.
245+
fn explicit_nonce_len(&self) -> usize;
246+
247+
/// AEAD authentication tag length in bytes.
248+
fn tag_len(&self) -> usize;
249+
234250
/// Create a cipher instance with the given key.
235251
fn create_cipher(&self, key: &[u8]) -> Result<Box<dyn Cipher>, String>;
236252
}

0 commit comments

Comments
 (0)