Skip to content

Commit 4ed8cfb

Browse files
jaredwolffclaude
andcommitted
Implement and harden DTLS 1.2 Connection ID (RFC 9146)
Adds DTLS 1.2 Connection ID (RFC 9146) with record-layer hardening and adjacent DTLS 1.3 (RFC 9147) / RFC 5246 / RFC 8446 extension-handling gaps closed. Record-layer hardening (RFC 9146 / RFC 6347 §4.1.2): - Wire CID constant-time compared and threaded into AAD (not the cached copy); wire version bytes likewise threaded through the AAD. - Malformed record boundaries, too-short AEAD records, and the 2^14 inner-plaintext ceiling all silent-drop per §4.1.2.7, symmetric with the send-side guard. - Epoch-0 `tls12_cid` rejected at DTLSRecord::parse; legacy-framed epoch-1 records rejected when inbound CID is expected. - Pre-CCS CID records cleartext-filtered against the negotiated inbound CID before entering queue_rx, closing a spray DoS vector. - Replay window updates only after AEAD success AND CID inner-type unwrap succeeds — peer-bug inner types no longer consume sequence slots. CID state model: - Single private `CidState` enum replaces separate `our_cid`/`peer_cid` fields so valid combinations are unrepresentable outside the module. - Per-direction activation: outbound armed at negotiation, inbound flipped live only after the peer's ChangeCipherSpec. Extension parser and HVR cookie: - Duplicate extensions (supported and unknown) rejected fail-closed via a 64-codepoint tracker with `try_push(...).map_err(...)?`, including a defense-in-depth ExtensionVec dedup pass. - HVR cookie HMAC binds the raw offered-CID extension bytes with a 0x00/0x01 marker, catching CH1/CH2 CID swap attempts. - Stateful `offered_cid` flag on dtls13::Client replaces the earlier config-based proxy so the direct `Dtls::new_13` path still rejects unsolicited `0x0036` echoes per RFC 8446 §4.2. Outbound sizing and lifetimes: - Shared `Engine::outbound_record_overhead` keeps create_handshake fragmentation in sync with CID + AEAD overhead. - New errors: `Error::Oversized`, `Error::MtuTooSmall`, `Error::SequenceNumberExhausted`; 2^14 plaintext ceiling enforced both sides; 48-bit sequence wrap surfaced distinctly from payload size failures. Auto-mode: - Hybrid ClientHello now emits `connection_id(0x0036)` when configured, so CH1/CH2 carry identical CID bytes and the stateless cookie binds once. - DTLS 1.3 client silent-accepts a server echo only when its own ClientHello on the wire solicited `0x0036`. - DTLS 1.3 server and client explicitly log-ignore `0x0036` per RFC. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 83e913f commit 4ed8cfb

36 files changed

Lines changed: 5746 additions & 250 deletions

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,25 @@ Four constructors control which DTLS version is used:
4747
- **DTLS‑SRTP**: Exports keying material for `SRTP_AEAD_AES_256_GCM`,
4848
`SRTP_AEAD_AES_128_GCM`, and `SRTP_AES128_CM_SHA1_80` ([RFC 5764], [RFC 7714]).
4949
- **Extended Master Secret** ([RFC 7627]) is negotiated and enforced (DTLS 1.2).
50+
- **Connection ID** ([RFC 9146]) is supported for DTLS 1.2 (extension codepoint
51+
54 / 0x0036). Configure with [`Config::with_connection_id`][with_cid];
52+
`Output::ConnectionId` is emitted when negotiation completes. The CID is a
53+
**routing hint, not authorization to change the send address**: per RFC
54+
9146 §6 the caller must (a) wait for an authentication-positive signal
55+
(e.g. `ApplicationData` from `poll_output`) before updating peer address
56+
`handle_packet` returning `Ok(())` is **not** proof of authentication,
57+
since invalid records are silently discarded per RFC 6347 §4.1.2.7; (b)
58+
require strict-monotonic (epoch, sequence_number) on the authenticated
59+
record; (c) apply an address-reachability policy. See
60+
[`Config::with_connection_id`][with_cid] rustdoc for the full pattern.
61+
62+
### Known non-interop: pre-RFC Connection ID codepoint 53 (0x35)
63+
Older implementations that predate RFC 9146 — notably OpenSSL before 3.2 and
64+
some embedded stacks — use extension type `53` from
65+
`draft-ietf-tls-dtls-connection-id-07`. dimpl implements the final RFC 9146
66+
codepoint (54) only, so peers stuck on the draft codepoint will fall back to
67+
legacy framing instead of successfully negotiating CID. This is expected for
68+
roaming PSK/IoT deployments that still ship the draft codepoint.
5069

5170
### Certificate model
5271
During the handshake the engine emits
@@ -188,6 +207,8 @@ Rust 1.85.0
188207
[RFC 5764]: https://www.rfc-editor.org/rfc/rfc5764
189208
[RFC 7714]: https://www.rfc-editor.org/rfc/rfc7714
190209
[RFC 7627]: https://www.rfc-editor.org/rfc/rfc7627
210+
[RFC 9146]: https://www.rfc-editor.org/rfc/rfc9146
211+
[with_cid]: https://docs.rs/dimpl/latest/dimpl/struct.ConfigBuilder.html#method.with_connection_id
191212

192213

193214
License: MIT OR Apache-2.0

src/auto.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const EXT_PADDING: u16 = 0x0015;
3939
const EXT_EXTENDED_MASTER_SECRET: u16 = 0x0017;
4040
const EXT_SUPPORTED_VERSIONS: u16 = 0x002B;
4141
const EXT_KEY_SHARE: u16 = 0x0033;
42+
const EXT_CONNECTION_ID: u16 = 0x0036;
4243
const EXT_RENEGOTIATION_INFO: u16 = 0xFF01;
4344

4445
/// A self-contained hybrid ClientHello compatible with both DTLS 1.2 and 1.3.
@@ -189,7 +190,20 @@ impl HybridClientHello {
189190
ext_buf.push(0); // renegotiated_connection length = 0
190191
ext_entries.push((EXT_RENEGOTIATION_INFO, start, ext_buf.len()));
191192

192-
// 9. padding: fill to MTU
193+
// 9. connection_id (RFC 9146, DTLS 1.2 compat): emit when the caller
194+
// configured a CID. Without this, auto-mode hybrid CH1 would omit
195+
// the extension, causing CH1 (no CID) / CH2 (with CID) to disagree
196+
// across HVR; `Config::build` already rejects a CID-without-DTLS-1.2
197+
// combination, so reaching here guarantees at least one DTLS 1.2
198+
// suite is offered.
199+
if let Some(cid) = config.connection_id() {
200+
let start = ext_buf.len();
201+
ext_buf.push(cid.len() as u8);
202+
ext_buf.extend_from_slice(cid);
203+
ext_entries.push((EXT_CONNECTION_ID, start, ext_buf.len()));
204+
}
205+
206+
// 10. padding: fill to MTU
193207
let record_header = 13usize;
194208
let handshake_header = 12usize;
195209
let body_so_far = ch_body.len()

src/buffer.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ impl Buf {
7878
self.0.resize(len, value);
7979
}
8080

81+
/// Shorten the buffer to the specified length, dropping trailing bytes.
82+
pub fn truncate(&mut self, len: usize) {
83+
self.0.truncate(len);
84+
}
85+
8186
/// Convert the buffer into the underlying `Vec<u8>`.
8287
pub fn into_vec(mut self) -> Vec<u8> {
8388
std::mem::take(&mut self.0)

0 commit comments

Comments
 (0)