357 lines
11 KiB
Rust
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: ¶ms,
|
|
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)
|
|
);
|
|
}
|