#![allow(clippy::items_after_statements)]
use std::future::Future;
use std::net::SocketAddr;
use std::num::NonZeroU64;
use std::pin::pin;
use std::task::Poll;
use cryptoxide::chacha20poly1305::ChaCha20Poly1305;
use slither::config::Config;
use slither::constants::{
AEAD_TAG_LEN, DATA_HEADER_LEN, MAX_DATAGRAM, MAX_DATAGRAM_PAYLOAD, MAX_EPOCH_JUMP, PKT_DATA,
PKT_HANDSHAKE_INIT, PKT_HANDSHAKE_RESP,
};
use slither::testutil::{FlakyPolicy, Pair, Tap, TestConnection, TestRecvStream, local, settle};
const EPOCH: u64 = 16;
fn small_epoch_config() -> Config {
Config::new().with_epoch_size(NonZeroU64::new(EPOCH).expect("EPOCH is nonzero"))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Sealed {
counter: u64,
len: usize,
}
impl Sealed {
fn epoch(self) -> u64 {
self.counter / EPOCH
}
}
fn sealed_by(tap: &Tap, who: SocketAddr) -> Vec<Sealed> {
tap.snapshot()
.iter()
.filter(|s| s.src == who && s.bytes.first() == Some(&PKT_DATA))
.map(|s| {
assert!(
s.bytes.len() >= DATA_HEADER_LEN + AEAD_TAG_LEN,
"a Data packet shorter than its own header plus tag: {} bytes",
s.bytes.len()
);
Sealed {
counter: u64::from_le_bytes(s.bytes[6..14].try_into().expect("eight header bytes")),
len: s.bytes.len(),
}
})
.collect()
}
fn sealed_bytes(tap: &Tap, who: SocketAddr) -> Vec<Vec<u8>> {
tap.snapshot()
.iter()
.filter(|s| s.src == who && s.bytes.first() == Some(&PKT_DATA))
.map(|s| s.bytes.clone())
.collect()
}
fn sent_from(tap: &Tap, who: SocketAddr) -> usize {
tap.snapshot().iter().filter(|s| s.src == who).count()
}
fn tagged(tag: u16, len: usize) -> Vec<u8> {
assert!(len >= 2, "a self-naming datagram needs its two tag bytes");
let mut v = vec![0u8; len];
v[..2].copy_from_slice(&tag.to_be_bytes());
for (i, b) in v[2..].iter_mut().enumerate() {
*b = ((i + usize::from(tag)) % 251) as u8;
}
v
}
fn tag_of(d: &[u8]) -> u16 {
assert!(d.len() >= 2, "a datagram shorter than its own tag: {d:?}");
let tag = u16::from_be_bytes([d[0], d[1]]);
let want = tagged(tag, d.len());
assert!(
d == want.as_slice(),
"a datagram tagged {tag} has a foreign body — the payload was corrupted, \
truncated or misattributed"
);
tag
}
async fn poll_once<F: Future>(mut fut: std::pin::Pin<&mut F>) -> Poll<F::Output> {
std::future::poll_fn(|cx| Poll::Ready(fut.as_mut().poll(cx))).await
}
async fn claim_ready(c: &TestConnection, what: &str) -> Vec<u16> {
let mut out = Vec::new();
loop {
let mut fut = pin!(c.recv_datagram());
match poll_once(fut.as_mut()).await {
Poll::Ready(Ok(d)) => out.push(tag_of(&d)),
Poll::Ready(Err(e)) => panic!("{what}: connection lost mid-claim: {e:?}"),
Poll::Pending => break,
}
}
out
}
async fn nudge(c: &TestConnection, tag: u16) {
c.send_datagram(&tagged(tag, 8)).expect("send_datagram");
settle().await;
}
async fn pump_until_epoch(
pair: &Pair,
ca: &TestConnection,
cb: &TestConnection,
tap: &Tap,
target: u64,
first_tag: u16,
) -> u16 {
let mut tag = first_tag;
let cap = (target + 2) * EPOCH + 16;
while sealed_by(tap, pair.a.addr())
.last()
.expect("A has sealed at least one Data packet")
.epoch()
< target
{
nudge(ca, tag).await;
let _ = claim_ready(cb, "pump drain").await;
tag += 1;
assert!(
u64::from(tag - first_tag) < cap,
"the pump sent {} datagrams without reaching epoch {target}: A's \
counter is not advancing one per packet, which every distance in \
this file assumes",
tag - first_tag
);
}
let last = *sealed_by(tap, pair.a.addr())
.last()
.expect("A has sealed at least one Data packet");
assert_eq!(
last.epoch(),
target,
"the pump overshot: it stopped at counter {} (epoch {}) rather than \
inside epoch {target}, so every epoch distance asserted after it would \
be wrong",
last.counter,
last.epoch()
);
tag
}
async fn hold_one(pair: &Pair, ca: &TestConnection, tap: &Tap, tag: u16) -> (Vec<u8>, Sealed) {
const WINDOW: usize = 4;
let base = sent_from(tap, pair.a.addr());
let before = sealed_by(tap, pair.a.addr()).len();
pair.a
.wire
.set_policy(FlakyPolicy::drop_at(base..base + WINDOW));
ca.send_datagram(&tagged(tag, MAX_DATAGRAM_PAYLOAD))
.expect("send_datagram");
settle().await;
let after = sent_from(tap, pair.a.addr());
assert!(
after > base,
"the drop window was never reached: A made no send at index {base}, so \
nothing was held and everything below would prove nothing"
);
assert!(
after <= base + WINDOW,
"A made {} sends while a {WINDOW}-index window was armed, so one escaped \
it and the held packet may have been delivered after all",
after - base
);
pair.a.wire.set_policy(FlakyPolicy::perfect());
let packets = sealed_by(tap, pair.a.addr());
let bytes = sealed_bytes(tap, pair.a.addr());
let held: Vec<usize> = (before..packets.len())
.filter(|&i| packets[i].len == MAX_DATAGRAM)
.collect();
assert_eq!(
held.len(),
1,
"expected exactly one {MAX_DATAGRAM}-byte packet inside the drop window, \
found {} among {:?} — a `MAX_DATAGRAM_PAYLOAD` datagram occupies exactly \
`DATA_HEADER_LEN + 1 + {MAX_DATAGRAM_PAYLOAD} + AEAD_TAG_LEN` = \
{MAX_DATAGRAM} bytes and nothing else in these tests is that size",
held.len(),
&packets[before..]
);
let i = held[0];
(bytes[i].clone(), packets[i])
}
fn handshakes(tap: &Tap) -> usize {
tap.snapshot()
.iter()
.filter(|s| {
matches!(
s.bytes.first(),
Some(&PKT_HANDSHAKE_INIT) | Some(&PKT_HANDSHAKE_RESP)
)
})
.count()
}
async fn read_exactly(r: &mut TestRecvStream, n: usize, what: &str) -> Vec<u8> {
let mut out = Vec::with_capacity(n);
let mut buf = vec![0u8; 16 * 1024];
while out.len() < n {
match tokio::time::timeout(std::time::Duration::from_secs(30), r.read(&mut buf)).await {
Ok(Ok(Some(k))) => out.extend_from_slice(&buf[..k]),
Ok(Ok(None)) => panic!(
"{what}: end of stream after {} of {n} bytes — the ratchet lost data",
out.len()
),
Ok(Err(e)) => panic!("{what}: read failed after {} bytes: {e:?}", out.len()),
Err(_) => panic!(
"{what}: stalled after {} of {n} bytes with 30 s of virtual time \
spent — a receiver that stopped opening packets at a boundary \
looks exactly like this",
out.len()
),
}
}
out
}
#[test]
fn rk1_the_rekey_of_zeros_vector() {
fn rekey(key: &[u8; 32]) -> [u8; 32] {
let mut nonce = [0u8; 12];
nonce[4..].copy_from_slice(&u64::MAX.to_le_bytes());
let mut aead = ChaCha20Poly1305::new(key, &nonce, &[]);
let mut out = [0u8; 32];
let mut tag = [0u8; AEAD_TAG_LEN];
aead.encrypt(&[0u8; 32], &mut out, &mut tag);
out
}
fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
const REKEY_OF_ZEROS: &str = "25ce5d37df19f3783185f2ffd5ab17fa3397c212f02d62fb1733e0b875b74c58";
let once = rekey(&[0u8; 32]);
assert_eq!(
hex(&once),
REKEY_OF_ZEROS,
"§7.7's `REKEY(0³²)` vector. If this is the only red in the file, the \
spec's constant and this construction disagree — that is a ruling, not \
an expectation to update (CLAUDE.md's wire-pin rule)."
);
let twice = rekey(&once);
assert_ne!(
hex(&twice),
hex(&once),
"§7.7: epoch `e`'s key is `Rekey()` applied `e` times, so applying it \
again must move"
);
assert_ne!(
hex(&twice),
hex(&[0u8; 32]),
"and must not return to the key it started from"
);
}
#[tokio::test(start_paused = true)]
async fn rk2_crossing_several_epoch_boundaries_is_invisible() {
local(async {
let pair = Pair::seeded_with(0x5EED_0002, small_epoch_config());
let (ca, cb) = pair.establish().await;
settle().await;
let tap = pair.net.tap();
let handshakes_after_establish = handshakes(&tap);
let (dhs_a, dhs_b) = (pair.a.dhs.get(), pair.b.dhs.get());
const LEN: usize = 96 * 1024;
let want: Vec<u8> = (0..LEN).map(|i| (i % 251) as u8).collect();
let mut send = ca.open_uni().await.expect("open_uni");
let writer = async {
let mut done = 0usize;
while done < want.len() {
done += send.write(&want[done..]).await.expect("write");
}
send.finish().await.expect("finish");
};
let reader = async {
let mut recv = cb.accept_uni().await.expect("accept_uni");
read_exactly(&mut recv, LEN, "the stream across the boundaries").await
};
let (_, got) = tokio::join!(writer, reader);
assert!(
got == want,
"§7.7: the stream is byte-exact across every epoch boundary it \
crossed"
);
let sealed = sealed_by(&tap, pair.a.addr());
let first = sealed.first().expect("A sealed Data packets").counter;
let last = sealed.last().expect("A sealed Data packets").counter;
assert!(
sealed.windows(2).all(|w| w[1].counter > w[0].counter),
"§7.7: *the counter is never reset by the ratchet* — A's counters \
must be strictly increasing across every boundary, got {:?}",
sealed.iter().map(|s| s.counter).collect::<Vec<_>>()
);
let crossed = last / EPOCH - first / EPOCH;
assert!(
crossed >= 3,
"the fixture never crossed enough boundaries to prove anything: A's \
counters ran {first}..={last} at an epoch of {EPOCH}, {crossed} \
boundaries. This is the assertion that keeps this test from being \
vacuous."
);
assert_eq!(
handshakes(&tap),
handshakes_after_establish,
"S23: *there is no DH re-handshake* — not one handshake packet may \
appear after establishment"
);
assert_eq!(
(pair.a.dhs.get(), pair.b.dhs.get()),
(dhs_a, dhs_b),
"S23: the ratchet is symmetric-key only; a single charged DH means a \
handshake happened"
);
assert!(
claim_ready(&cb, "after the crossings").await.is_empty(),
"no datagram was sent, so nothing may be readable: the ratchet \
surfaces nothing to the application"
);
nudge(&ca, 1).await;
nudge(&cb, 2).await;
assert_eq!(
claim_ready(&cb, "A→B after the crossings").await,
vec![1],
"A→B still carries datagrams after the boundaries"
);
assert_eq!(
claim_ready(&ca, "B→A after the crossings").await,
vec![2],
"B→A still carries datagrams after the boundaries"
);
})
.await;
}
#[tokio::test(start_paused = true)]
async fn rk3_the_two_directions_ratchet_independently() {
local(async {
let pair = Pair::seeded_with(0x5EED_0003, small_epoch_config());
let (ca, cb) = pair.establish().await;
settle().await;
let tap = pair.net.tap();
nudge(&ca, 0).await;
let _ = claim_ready(&cb, "priming").await;
pump_until_epoch(&pair, &ca, &cb, &tap, MAX_EPOCH_JUMP, 1).await;
let a_epoch = sealed_by(&tap, pair.a.addr())
.last()
.expect("A sealed packets")
.epoch();
let b_epoch = sealed_by(&tap, pair.b.addr())
.last()
.map_or(0, |s| s.epoch());
assert!(
a_epoch > b_epoch,
"the fixture never separated the two directions: A is in epoch \
{a_epoch} and B in epoch {b_epoch}, so a build with one shared \
epoch would be indistinguishable here"
);
nudge(&cb, 500).await;
assert_eq!(
claim_ready(&ca, "B's reply from its own epoch").await,
vec![500],
"§7.7: each direction ratchets independently — B seals in epoch \
{b_epoch} while A is in epoch {a_epoch}, and A must open it"
);
nudge(&ca, 501).await;
assert_eq!(
claim_ready(&cb, "A's reply from its own epoch").await,
vec![501],
"§7.7: and the higher-epoch direction is unaffected by the lower one"
);
})
.await;
}
#[tokio::test(start_paused = true)]
async fn rk4_a_packet_two_epochs_back_does_not_open() {
local(async {
let pair = Pair::seeded_with(0x5EED_0004, small_epoch_config());
let (ca, cb) = pair.establish().await;
settle().await;
let tap = pair.net.tap();
nudge(&ca, 0).await;
assert_eq!(
claim_ready(&cb, "the warm-up datagram").await,
vec![0],
"the fixture must deliver over a perfect path before an absence \
below can mean anything"
);
let (stale, stale_at) = hold_one(&pair, &ca, &tap, 1).await;
let stale_epoch = stale_at.epoch();
let next_tag =
pump_until_epoch(&pair, &ca, &cb, &tap, stale_epoch + MAX_EPOCH_JUMP, 2).await;
let marker = next_tag;
nudge(&ca, marker).await;
let landed = *sealed_by(&tap, pair.a.addr())
.last()
.expect("A sealed packets");
assert_eq!(
claim_ready(&cb, "the marker that moves B's commit").await,
vec![marker],
"B must open a packet in epoch {} for its commit to be there — this \
arrival is what makes the held packet two epochs old rather than \
merely old-looking",
landed.epoch()
);
assert!(
landed.epoch() >= stale_epoch + MAX_EPOCH_JUMP,
"B committed only to epoch {} while the held packet is in epoch \
{stale_epoch}: that is one epoch back, which §7.7 says must open, \
and this test would then be asserting the opposite of the spec",
landed.epoch()
);
pair.net.inject(pair.a.addr(), pair.b.addr(), &stale);
settle().await;
let arrived = claim_ready(&cb, "after the stale packet was posted").await;
assert!(
arrived.is_empty(),
"§7.7: a packet from epoch {stale_epoch}, {} epochs behind B's \
commit at epoch {}, must be refused *without key derivation* — it \
reached the application instead, as {arrived:?}. This is the \
never-rekey build's signature.",
landed.epoch() - stale_epoch,
landed.epoch()
);
nudge(&ca, 900).await;
assert_eq!(
claim_ready(&cb, "after the refusal").await,
vec![900],
"§7.7: the refusal is *a generic decryption failure at the hiss \
surface* — it may not kill or disturb the connection"
);
let (fresh, fresh_at) = hold_one(&pair, &ca, &tap, 3).await;
assert!(
fresh_at.epoch() >= landed.epoch(),
"the oracle packet must be in B's committed epoch or later, else it \
proves nothing about the instrument"
);
pair.net.inject(pair.a.addr(), pair.b.addr(), &fresh);
settle().await;
assert_eq!(
claim_ready(&cb, "the oracle").await,
vec![3],
"a held-and-injected packet in the current epoch must arrive: if it \
does not, `inject` or the hold is broken and the absence asserted \
above proves nothing"
);
})
.await;
}
#[tokio::test(start_paused = true)]
async fn rk5_a_packet_one_epoch_back_still_opens() {
local(async {
let pair = Pair::seeded_with(0x5EED_0005, small_epoch_config());
let (ca, cb) = pair.establish().await;
settle().await;
let tap = pair.net.tap();
nudge(&ca, 0).await;
assert_eq!(
claim_ready(&cb, "the warm-up datagram").await,
vec![0],
"the fixture must deliver over a perfect path first"
);
let (straggler, straggler_at) = hold_one(&pair, &ca, &tap, 1).await;
let straggler_epoch = straggler_at.epoch();
let next_tag = pump_until_epoch(&pair, &ca, &cb, &tap, straggler_epoch + 1, 2).await;
let marker = next_tag;
nudge(&ca, marker).await;
let landed = *sealed_by(&tap, pair.a.addr())
.last()
.expect("A sealed packets");
assert_eq!(
claim_ready(&cb, "the marker that moves B's commit").await,
vec![marker],
"B must open a packet in the new epoch for its commit to move there"
);
assert_eq!(
landed.epoch(),
straggler_epoch + 1,
"B must be **exactly** one epoch ahead of the straggler: at two it \
would be §7.7's refusal case and this test would assert the \
opposite of the spec"
);
pair.net.inject(pair.a.addr(), pair.b.addr(), &straggler);
settle().await;
assert_eq!(
claim_ready(&cb, "the straggler from the previous epoch").await,
vec![1],
"§7.7: the receiver retains the immediately preceding epoch's key — \
a packet sealed in epoch {straggler_epoch} must still open after \
the commit to epoch {}",
landed.epoch()
);
})
.await;
}