Files
fips/src/noise/replay.rs
T
Johnathan Corgan b8a4449762 Refuse a ceiling frame counter and saturate the gap tracker
An authenticated peer sending a frame counter of u64::MAX pinned its own
replay window's high-water mark at the ceiling, after which every later
counter it sent fell more than a window below `highest` and was dropped,
wedging that peer's receive path until a rekey replaced the session. The
same counter overflowed the MMP gap tracker's expected-counter add, which
wraps to zero in a release build and aborts under a build with overflow
checks on.

Refuse the value in ReplayWindow::check, which covers the FSP session
paths and the off-task FMP decrypt worker in one place, and advance the
gap tracker with a saturating add. Neither send path in this tree can
emit u64::MAX, so no conforming peer notices; u64::MAX - 1, the highest
counter an honest peer can send, is unaffected.
2026-08-23 11:44:45 +01:00

147 lines
4.7 KiB
Rust

use super::REPLAY_WINDOW_SIZE;
use std::fmt;
/// Sliding window for replay protection.
///
/// Tracks which packet counters have been received within a window of
/// REPLAY_WINDOW_SIZE. Packets with counters below the window or already
/// seen within the window are rejected.
///
/// Based on WireGuard's anti-replay mechanism (RFC 6479 style).
#[derive(Clone)]
pub struct ReplayWindow {
/// Highest counter value seen.
highest: u64,
/// Bitmap tracking which counters in the window have been seen.
/// Bit i corresponds to counter (highest - i).
bitmap: [u64; REPLAY_WINDOW_SIZE / 64],
}
impl ReplayWindow {
/// Create a new replay window.
pub fn new() -> Self {
Self {
highest: 0,
bitmap: [0; REPLAY_WINDOW_SIZE / 64],
}
}
/// Check if a counter is valid (not replayed, not too old).
///
/// Returns true if the counter is acceptable, false if it should be rejected.
/// Does NOT update the window - call `accept` after successful decryption.
pub fn check(&self, counter: u64) -> bool {
// The send side refuses to emit u64::MAX (`CipherState::advance_nonce`,
// `NoiseSession::take_send_counter`), so no conforming peer produces it.
// Refusing it here keeps `accept` from pinning `highest` at the ceiling,
// which would wedge the window against every later counter.
if counter == u64::MAX {
return false;
}
if counter > self.highest {
// New highest - always acceptable
return true;
}
// Counter is <= highest, check if it's within the window
let diff = self.highest - counter;
if diff as usize >= REPLAY_WINDOW_SIZE {
// Too old (outside window)
return false;
}
// Check bitmap - bit is set if counter was already seen
let word_idx = (diff as usize) / 64;
let bit_idx = (diff as usize) % 64;
(self.bitmap[word_idx] & (1u64 << bit_idx)) == 0
}
/// Accept a counter into the window.
///
/// Call this only after successful decryption to prevent
/// DoS attacks that exhaust the window.
pub fn accept(&mut self, counter: u64) {
if counter > self.highest {
// Shift the window
let shift = counter - self.highest;
if shift as usize >= REPLAY_WINDOW_SIZE {
// Complete reset
self.bitmap = [0; REPLAY_WINDOW_SIZE / 64];
} else {
// Shift bitmap
self.shift_bitmap(shift as usize);
}
self.highest = counter;
// Mark counter 0 (which is now the highest) as seen
self.bitmap[0] |= 1;
} else {
// Mark the counter as seen
let diff = self.highest - counter;
let word_idx = (diff as usize) / 64;
let bit_idx = (diff as usize) % 64;
self.bitmap[word_idx] |= 1u64 << bit_idx;
}
}
/// Shift the bitmap by the given number of positions.
///
/// This moves old counters to higher bit positions to make room for the
/// new highest counter at position 0.
fn shift_bitmap(&mut self, shift: usize) {
if shift >= REPLAY_WINDOW_SIZE {
self.bitmap = [0; REPLAY_WINDOW_SIZE / 64];
return;
}
let word_shift = shift / 64;
let bit_shift = shift % 64;
// Shift entire words first (from high to low to avoid overwriting)
if word_shift > 0 {
for i in (word_shift..self.bitmap.len()).rev() {
self.bitmap[i] = self.bitmap[i - word_shift];
}
for i in 0..word_shift {
self.bitmap[i] = 0;
}
}
// Shift bits within words (from low to high so carry propagates correctly)
if bit_shift > 0 {
let mut carry = 0u64;
for i in 0..self.bitmap.len() {
let new_carry = self.bitmap[i] >> (64 - bit_shift);
self.bitmap[i] = (self.bitmap[i] << bit_shift) | carry;
carry = new_carry;
}
}
}
/// Get the highest counter seen.
pub fn highest(&self) -> u64 {
self.highest
}
/// Reset the window (use when rekeying).
pub fn reset(&mut self) {
self.highest = 0;
self.bitmap = [0; REPLAY_WINDOW_SIZE / 64];
}
}
impl Default for ReplayWindow {
fn default() -> Self {
Self::new()
}
}
impl fmt::Debug for ReplayWindow {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ReplayWindow")
.field("highest", &self.highest)
.field("window_size", &REPLAY_WINDOW_SIZE)
.finish()
}
}