Skip to content

Commit 1193391

Browse files
authored
Defer anti-replay window update until after record authentication
1 parent 90317be commit 1193391

5 files changed

Lines changed: 109 additions & 48 deletions

File tree

src/dtls12/engine.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1103,10 +1103,14 @@ impl RecordDecrypt for Engine {
11031103
self.peer_encryption_enabled
11041104
}
11051105

1106-
fn replay_check_and_update(&mut self, seq: Sequence) -> bool {
1106+
fn replay_check(&self, seq: Sequence) -> bool {
11071107
// Only epoch 1 (encrypted) records reach here; epoch 0 records are
11081108
// returned early by the DTLS 1.2 incoming parser.
1109-
self.replay.check_and_update(seq.sequence_number)
1109+
self.replay.check(seq.sequence_number)
1110+
}
1111+
1112+
fn replay_update(&mut self, seq: Sequence) {
1113+
self.replay.update(seq.sequence_number);
11101114
}
11111115

11121116
fn decryption_aad_and_nonce(&self, dtls: &DTLSRecord, buf: &[u8]) -> (Aad, Nonce) {

src/dtls12/incoming.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,10 @@ impl Record {
150150

151151
// We need to decrypt the record and redo the parsing.
152152
let dtls = record.record();
153+
let sequence = dtls.sequence;
153154

154-
// Anti-replay check
155-
if !decrypt.replay_check_and_update(dtls.sequence) {
155+
// Anti-replay check (read-only, does not update window)
156+
if !decrypt.replay_check(sequence) {
156157
return Ok(None);
157158
}
158159

@@ -180,6 +181,11 @@ impl Record {
180181
buffer.len()
181182
};
182183

184+
// Decryption succeeded — now commit the replay window update.
185+
// RFC 6347 §4.1.2.6: "The receive window is updated only if the
186+
// MAC verification succeeds."
187+
decrypt.replay_update(sequence);
188+
183189
// Update the length of the record.
184190
buffer[11] = (new_len >> 8) as u8;
185191
buffer[12] = new_len as u8;
@@ -263,7 +269,8 @@ impl ParsedRecord {
263269
/// parsing to depend only on the cryptographic operations it actually uses.
264270
pub trait RecordDecrypt {
265271
fn is_peer_encryption_enabled(&self) -> bool;
266-
fn replay_check_and_update(&mut self, seq: Sequence) -> bool;
272+
fn replay_check(&self, seq: Sequence) -> bool;
273+
fn replay_update(&mut self, seq: Sequence);
267274
fn decryption_aad_and_nonce(&self, dtls: &DTLSRecord, buf: &[u8]) -> (Aad, Nonce);
268275
fn explicit_nonce_len(&self) -> usize;
269276
fn decrypt_data(

src/dtls13/engine.rs

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2273,19 +2273,24 @@ impl RecordDecrypt for Engine {
22732273
reconstruct_sequence(seq_bits, expected, bits)
22742274
}
22752275

2276-
fn replay_check_and_update(&mut self, seq: Sequence) -> bool {
2276+
fn replay_check(&self, seq: Sequence) -> bool {
22772277
// Route to the correct per-epoch replay window
2278-
let accepted = if seq.epoch == 2 {
2279-
self.hs_replay.check_and_update(seq.sequence_number)
2278+
if seq.epoch == 2 {
2279+
self.hs_replay.check(seq.sequence_number)
22802280
} else {
2281-
match self.app_recv_keys.iter_mut().find(|e| e.epoch == seq.epoch) {
2282-
Some(entry) => entry.replay.check_and_update(seq.sequence_number),
2283-
None => return false, // no keys for this epoch
2281+
match self.app_recv_keys.iter().find(|e| e.epoch == seq.epoch) {
2282+
Some(entry) => entry.replay.check(seq.sequence_number),
2283+
None => false, // no keys for this epoch
22842284
}
2285-
};
2285+
}
2286+
}
22862287

2287-
if !accepted {
2288-
return false;
2288+
fn replay_update(&mut self, seq: Sequence) {
2289+
// Update the replay window for this epoch
2290+
if seq.epoch == 2 {
2291+
self.hs_replay.update(seq.sequence_number);
2292+
} else if let Some(entry) = self.app_recv_keys.iter_mut().find(|e| e.epoch == seq.epoch) {
2293+
entry.replay.update(seq.sequence_number);
22892294
}
22902295

22912296
// Advance expected receive sequence for this epoch
@@ -2304,8 +2309,6 @@ impl RecordDecrypt for Engine {
23042309
}
23052310
}
23062311
}
2307-
2308-
true
23092312
}
23102313

23112314
fn decrypt_record(

src/dtls13/incoming.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -245,8 +245,8 @@ impl Record {
245245
sequence_number: full_seq,
246246
};
247247

248-
// Anti-replay check
249-
if !decrypt.replay_check_and_update(full_sequence) {
248+
// Anti-replay check (read-only, does not update window)
249+
if !decrypt.replay_check(full_sequence) {
250250
return Ok(None);
251251
}
252252

@@ -278,6 +278,11 @@ impl Record {
278278
buffer.len()
279279
};
280280

281+
// Decryption succeeded — now commit the replay window update.
282+
// RFC 9147 §4.5.1: "The window MUST NOT be updated due to a received
283+
// record until that record has been deprotected successfully."
284+
decrypt.replay_update(full_sequence);
285+
281286
// Recover inner content type from DTLSInnerPlaintext
282287
let decrypted = &buffer[header_end..header_end + new_len];
283288
let (inner_content_type, content_len) = match recover_inner_content_type(decrypted) {
@@ -395,7 +400,8 @@ pub trait RecordDecrypt {
395400
fn is_peer_encryption_enabled(&self) -> bool;
396401
fn resolve_epoch(&self, epoch_bits: u8) -> u16;
397402
fn resolve_sequence(&self, epoch: u16, seq_bits: u64, s_flag: bool) -> u64;
398-
fn replay_check_and_update(&mut self, seq: Sequence) -> bool;
403+
fn replay_check(&self, seq: Sequence) -> bool;
404+
fn replay_update(&mut self, seq: Sequence);
399405
fn decrypt_record(
400406
&mut self,
401407
header: &[u8],

src/window.rs

Lines changed: 70 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,24 @@ impl ReplayWindow {
1616
Self::default()
1717
}
1818

19-
/// Check if the given sequence number is acceptable and update the window state.
20-
/// Returns true if fresh/acceptable, false if duplicate/too old.
21-
pub fn check_and_update(&mut self, seqno: u64) -> bool {
19+
/// Check if the given sequence number is acceptable (not a replay, not too old).
20+
/// Read-only: does not modify the window state.
21+
pub fn check(&self, seqno: u64) -> bool {
22+
if seqno > self.max_seq {
23+
true
24+
} else {
25+
let offset = self.max_seq - seqno;
26+
if offset >= 64 {
27+
return false; // too old
28+
}
29+
let mask = 1u64 << offset;
30+
(self.window & mask) == 0 // false if duplicate
31+
}
32+
}
33+
34+
/// Update the window state to record that `seqno` has been received.
35+
/// Must only be called after the record has been authenticated (decrypted successfully).
36+
pub fn update(&mut self, seqno: u64) {
2237
if seqno > self.max_seq {
2338
let delta = seqno - self.max_seq;
2439
if delta > 63 {
@@ -29,18 +44,11 @@ impl ReplayWindow {
2944
self.window |= 1; // mark newest as seen
3045
}
3146
self.max_seq = seqno;
32-
true
3347
} else {
3448
let offset = self.max_seq - seqno;
35-
if offset >= 64 {
36-
return false; // too old
37-
}
38-
let mask = 1u64 << offset;
39-
if (self.window & mask) != 0 {
40-
return false; // duplicate
49+
if offset < 64 {
50+
self.window |= 1u64 << offset;
4151
}
42-
self.window |= mask;
43-
true
4452
}
4553
}
4654
}
@@ -49,54 +57,87 @@ impl ReplayWindow {
4957
mod tests {
5058
use super::*;
5159

60+
/// Helper: check and update in one step (simulates authenticated record).
61+
fn check_and_update(w: &mut ReplayWindow, seqno: u64) -> bool {
62+
if w.check(seqno) {
63+
w.update(seqno);
64+
true
65+
} else {
66+
false
67+
}
68+
}
69+
5270
#[test]
5371
fn accepts_fresh_and_rejects_duplicate() {
5472
let mut w = ReplayWindow::new();
55-
assert!(w.check_and_update(1));
56-
assert!(!w.check_and_update(1)); // duplicate
57-
assert!(w.check_and_update(2)); // next fresh
73+
assert!(check_and_update(&mut w, 1));
74+
assert!(!check_and_update(&mut w, 1)); // duplicate
75+
assert!(check_and_update(&mut w, 2)); // next fresh
5876
}
5977

6078
#[test]
6179
fn accepts_out_of_order_within_window() {
6280
let mut w = ReplayWindow::new();
63-
assert!(w.check_and_update(10)); // establish max=10
64-
assert!(w.check_and_update(8)); // unseen within 64
65-
assert!(!w.check_and_update(8)); // duplicate now
66-
assert!(w.check_and_update(9)); // unseen within 64
81+
assert!(check_and_update(&mut w, 10)); // establish max=10
82+
assert!(check_and_update(&mut w, 8)); // unseen within 64
83+
assert!(!check_and_update(&mut w, 8)); // duplicate now
84+
assert!(check_and_update(&mut w, 9)); // unseen within 64
6785
}
6886

6987
#[test]
7088
fn rejects_too_old() {
7189
let mut w = ReplayWindow::new();
72-
assert!(w.check_and_update(100));
90+
assert!(check_and_update(&mut w, 100));
7391
// offset = 64 -> too old
74-
assert!(!w.check_and_update(36));
92+
assert!(!check_and_update(&mut w, 36));
7593
// offset = 63 -> allowed once
76-
assert!(w.check_and_update(37));
94+
assert!(check_and_update(&mut w, 37));
7795
}
7896

7997
#[test]
8098
fn handles_large_jump_and_window_shift() {
8199
let mut w = ReplayWindow::new();
82-
assert!(w.check_and_update(1));
100+
assert!(check_and_update(&mut w, 1));
83101
// Large forward jump clears the window entirely
84-
assert!(w.check_and_update(80));
102+
assert!(check_and_update(&mut w, 80));
85103
// Within window of new max and unseen
86-
assert!(w.check_and_update(79));
104+
assert!(check_and_update(&mut w, 79));
87105
// Too old relative to new max
88-
assert!(!w.check_and_update(15));
106+
assert!(!check_and_update(&mut w, 15));
89107
}
90108

91109
#[test]
92110
fn large_jump_does_not_leave_stale_bits() {
93111
let mut w = ReplayWindow::new();
94-
assert!(w.check_and_update(0));
112+
assert!(check_and_update(&mut w, 0));
95113
// Jump of 200 exceeds window size (64). The window must be fully
96114
// cleared so no stale bits from seq 0 remain.
97-
assert!(w.check_and_update(200));
115+
assert!(check_and_update(&mut w, 200));
98116
// seq 137 is within the window (offset = 200 - 137 = 63) and was
99117
// never seen, so it must be accepted.
100-
assert!(w.check_and_update(137));
118+
assert!(check_and_update(&mut w, 137));
119+
}
120+
121+
#[test]
122+
fn check_does_not_modify_window() {
123+
let mut w = ReplayWindow::new();
124+
w.update(10);
125+
// check alone should not change state
126+
assert!(w.check(11));
127+
assert!(w.check(11)); // still acceptable because update was never called
128+
w.update(11);
129+
assert!(!w.check(11)); // now it's a duplicate
130+
}
131+
132+
#[test]
133+
fn failed_auth_does_not_advance_window() {
134+
let mut w = ReplayWindow::new();
135+
w.update(5);
136+
// Simulate receiving seq 200 that passes check but fails authentication
137+
assert!(w.check(200));
138+
// Do NOT call update (authentication failed)
139+
// Legitimate packet at seq 6 should still be accepted
140+
assert!(w.check(6));
141+
w.update(6);
101142
}
102143
}

0 commit comments

Comments
 (0)