Files

357 lines
11 KiB
Rust

//! Coverage for the unified request codec: sender-side [`encode_request`] and
//! receiver-side structural [`decode_request`], plus the packet-level HMAC
//! verifier that separates authentication from structure.
use irtt_proto::{
decode_request, encode_request, hmac::compute_hmac_in_place, verify_packet_hmac, Clock,
DecodedRequest, DecodedRequestKind, Params, ProtoError, ReceivedStats, RequestToEncode,
StampAt, FLAG_CLOSE, FLAG_HMAC, FLAG_OPEN, FLAG_REPLY, HMAC_SIZE, MAGIC,
};
const KEY: &[u8] = b"testkey";
const TOKEN: u64 = 0x7896_b6ab_8771_5213;
const SEQUENCE: u32 = 0x1122_3344;
fn params() -> Params {
Params {
protocol_version: 1,
duration_ns: 3_000_000_000,
interval_ns: 1_000_000_000,
received_stats: ReceivedStats::Both,
stamp_at: StampAt::Both,
clock: Clock::Both,
..Params::default()
}
}
/// Builds a raw request of exactly `len` bytes: header, then an HMAC field when
/// flagged, then `body`, then zero padding.
fn raw(flags: u8, body: &[u8], len: usize) -> Vec<u8> {
let mut packet = MAGIC.to_vec();
packet.push(flags);
if flags & FLAG_HMAC != 0 {
packet.extend_from_slice(&[0xAA; HMAC_SIZE]);
}
packet.extend_from_slice(body);
packet.resize(len, 0);
packet
}
fn header(flags: u8) -> Vec<u8> {
raw(flags, &[], if flags & FLAG_HMAC != 0 { 20 } else { 4 })
}
fn token_and_sequence() -> Vec<u8> {
let mut body = TOKEN.to_le_bytes().to_vec();
body.extend_from_slice(&SEQUENCE.to_le_bytes());
body
}
fn classify(kind: &DecodedRequestKind<'_>) -> &'static str {
match kind {
DecodedRequestKind::Open { no_test: false, .. } => "open",
DecodedRequestKind::Open { no_test: true, .. } => "no-test open",
DecodedRequestKind::Close { .. } => "close",
DecodedRequestKind::Echo { .. } => "echo",
}
}
#[test]
fn flag_combinations_classify_requests_with_orthogonal_hmac_presence() {
let cases = [
(0, false, "echo"),
(FLAG_HMAC, true, "echo"),
(FLAG_CLOSE, false, "close"),
(FLAG_CLOSE | FLAG_HMAC, true, "close"),
(FLAG_OPEN, false, "open"),
(FLAG_OPEN | FLAG_CLOSE, false, "no-test open"),
(FLAG_OPEN | FLAG_HMAC, true, "open"),
(FLAG_OPEN | FLAG_CLOSE | FLAG_HMAC, true, "no-test open"),
];
for no_test in [false, true] {
for key in [None, Some(KEY)] {
let params = Params::default();
let packet = encode_request(
RequestToEncode::Open {
params: &params,
no_test,
},
key,
)
.unwrap();
let expected_flags = FLAG_OPEN
| if no_test { FLAG_CLOSE } else { 0 }
| if key.is_some() { FLAG_HMAC } else { 0 };
assert_eq!(packet[3], expected_flags);
assert_eq!(
decode_request(&packet).unwrap().kind,
DecodedRequestKind::Open {
no_test,
params: &[]
}
);
if let Some(key) = key {
verify_packet_hmac(key, &packet).unwrap();
}
}
}
for (flags, hmac_present, expected) in cases {
// Long enough for any kind, so only the flags decide classification.
let packet = raw(flags, &token_and_sequence(), 64);
let request = decode_request(&packet).unwrap_or_else(|error| {
panic!("flags 0x{flags:02x} must decode structurally, got {error:?}")
});
assert_eq!(
request.hmac_present, hmac_present,
"flags 0x{flags:02x} HMAC presence"
);
if let DecodedRequestKind::Open { params, .. } = request.kind {
let offset = if hmac_present { 20 } else { 4 };
assert_eq!(params, &packet[offset..]);
}
assert_eq!(
classify(&request.kind),
expected,
"flags 0x{flags:02x} classification"
);
}
}
#[test]
fn structurally_invalid_headers_are_rejected() {
assert_eq!(
decode_request(&MAGIC),
Err(ProtoError::PacketTooShort {
needed: 4,
actual: 3,
})
);
assert_eq!(
decode_request(&[0x00, MAGIC[1], MAGIC[2], FLAG_OPEN]),
Err(ProtoError::BadMagic)
);
for reserved in [0x10_u8, 0x20, 0x40, 0x80, 0xF0] {
assert_eq!(
decode_request(&header(reserved)),
Err(ProtoError::ReservedFlags(reserved)),
"reserved bits 0x{reserved:02x}"
);
}
}
#[test]
fn any_reply_flagged_datagram_is_rejected_as_a_request() {
for flags in [
FLAG_REPLY,
FLAG_OPEN | FLAG_REPLY,
FLAG_CLOSE | FLAG_REPLY,
FLAG_OPEN | FLAG_CLOSE | FLAG_REPLY,
FLAG_REPLY | FLAG_HMAC,
FLAG_OPEN | FLAG_REPLY | FLAG_HMAC,
FLAG_CLOSE | FLAG_REPLY | FLAG_HMAC,
FLAG_OPEN | FLAG_CLOSE | FLAG_REPLY | FLAG_HMAC,
] {
assert_eq!(
decode_request(&raw(flags, &token_and_sequence(), 64)),
Err(ProtoError::UnexpectedFlag(FLAG_REPLY)),
"flags 0x{flags:02x}"
);
}
}
#[test]
fn structural_minimum_lengths_are_enforced_per_kind() {
// (flags, minimum accepted length)
let cases = [
(FLAG_OPEN, 4_usize),
(FLAG_CLOSE, 12),
(0, 16),
(FLAG_OPEN | FLAG_HMAC, 20),
(FLAG_CLOSE | FLAG_HMAC, 28),
(FLAG_HMAC, 32),
];
for (flags, minimum) in cases {
let accepted = raw(flags, &token_and_sequence(), minimum);
assert_eq!(accepted.len(), minimum);
assert!(
decode_request(&accepted).is_ok(),
"flags 0x{flags:02x} length {minimum} must be accepted"
);
// One byte short of the minimum, for every kind that has bytes to lose.
let short_len = minimum - 1;
if short_len >= 4 {
assert_eq!(
decode_request(&accepted[..short_len]),
Err(ProtoError::PacketTooShort {
needed: minimum,
actual: short_len,
}),
"flags 0x{flags:02x} length {short_len}"
);
}
}
}
#[test]
fn token_and_sequence_are_read_from_the_hmac_dependent_offsets() {
for hmac in [false, true] {
let close_flags = FLAG_CLOSE | if hmac { FLAG_HMAC } else { 0 };
let close = raw(
close_flags,
&TOKEN.to_le_bytes(),
if hmac { 28 } else { 12 },
);
assert_eq!(
decode_request(&close).unwrap(),
DecodedRequest {
hmac_present: hmac,
kind: DecodedRequestKind::Close { token: TOKEN },
}
);
let echo_flags = if hmac { FLAG_HMAC } else { 0 };
let echo = raw(
echo_flags,
&token_and_sequence(),
if hmac { 32 } else { 16 },
);
assert_eq!(
decode_request(&echo).unwrap(),
DecodedRequest {
hmac_present: hmac,
kind: DecodedRequestKind::Echo {
token: TOKEN,
sequence: SEQUENCE,
tail: &[],
},
}
);
}
}
#[test]
fn open_params_are_borrowed_after_the_header_and_decoded_later() {
// Structural parsing accepts even malformed Params; semantic errors belong
// to Params::decode and are covered in params.rs.
for body in [params().encode(), Vec::new(), vec![1, 0x80]] {
for hmac in [false, true] {
let flags = FLAG_OPEN | if hmac { FLAG_HMAC } else { 0 };
let offset = if hmac { 20 } else { 4 };
let packet = raw(flags, &body, offset + body.len());
match decode_request(&packet).unwrap().kind {
DecodedRequestKind::Open { params, .. } => {
assert_eq!(params, &packet[offset..]);
}
other => panic!("expected an open request, got {other:?}"),
}
}
}
}
#[test]
fn trailing_bytes_are_tolerated_and_no_length_ceiling_applies() {
let mut close = raw(FLAG_CLOSE, &TOKEN.to_le_bytes(), 12);
close.extend_from_slice(&[0xDE; 20]);
assert_eq!(
decode_request(&close).unwrap().kind,
DecodedRequestKind::Close { token: TOKEN }
);
let mut echo = raw(0, &token_and_sequence(), 16);
echo.extend_from_slice(&[0xBE; 4]);
assert_eq!(
decode_request(&echo).unwrap().kind,
DecodedRequestKind::Echo {
token: TOKEN,
sequence: SEQUENCE,
tail: &[0xBE; 4],
}
);
// Far longer than any negotiated length: still just a request with a long
// opaque tail. No `Params` are consulted at any point.
let long = raw(0, &token_and_sequence(), 4096);
match decode_request(&long).unwrap().kind {
DecodedRequestKind::Echo { tail, .. } => assert_eq!(tail.len(), 4096 - 16),
other => panic!("expected an echo request, got {other:?}"),
}
}
#[test]
fn a_zero_token_is_structurally_valid() {
assert_eq!(
decode_request(&raw(FLAG_CLOSE, &0_u64.to_le_bytes(), 12))
.unwrap()
.kind,
DecodedRequestKind::Close { token: 0 }
);
assert_eq!(
decode_request(&raw(0, &[0; 12], 16)).unwrap().kind,
DecodedRequestKind::Echo {
token: 0,
sequence: 0,
tail: &[],
}
);
}
#[test]
fn hmac_presence_is_reported_independently_of_mac_validity() {
// A structurally complete authenticated echo whose MAC is garbage.
let garbage = raw(FLAG_HMAC, &token_and_sequence(), 32);
let request = decode_request(&garbage).unwrap();
assert!(request.hmac_present);
assert!(matches!(request.kind, DecodedRequestKind::Echo { .. }));
// The same packet fails verification.
assert_eq!(verify_packet_hmac(KEY, &garbage), Err(ProtoError::BadHmac));
// Correctly signed: accepted, and verification leaves the bytes alone.
let mut signed = garbage.clone();
compute_hmac_in_place(KEY, &mut signed, 4).unwrap();
let before = signed.clone();
verify_packet_hmac(KEY, &signed).unwrap();
assert_eq!(signed, before);
assert_eq!(
verify_packet_hmac(b"other-key", &signed),
Err(ProtoError::BadHmac)
);
}
#[test]
fn packet_verification_requires_a_present_and_complete_hmac_field() {
// No FLAG_HMAC: there is no field to verify.
let unauthenticated = raw(0, &token_and_sequence(), 16);
assert_eq!(
verify_packet_hmac(KEY, &unauthenticated),
Err(ProtoError::MissingFlag(FLAG_HMAC))
);
// Flagged but truncated inside the field.
let truncated = raw(FLAG_HMAC, &[], 12);
assert_eq!(
verify_packet_hmac(KEY, &truncated),
Err(ProtoError::InvalidHmacOffset)
);
// Structural decoding rejects it too, for the kind's minimum length.
assert_eq!(
decode_request(&truncated),
Err(ProtoError::PacketTooShort {
needed: 32,
actual: 12,
})
);
// A malformed header is rejected before any MAC work.
assert_eq!(
verify_packet_hmac(KEY, &[0x00, MAGIC[1], MAGIC[2], FLAG_HMAC]),
Err(ProtoError::BadMagic)
);
}