use super::header::{DataHeader, InitHeader, RespHeader};
use super::mac::Mac1Key;
use super::payload::Msg1Payload;
use super::suite::ReferenceSuite;
use super::{Channel, Inbound, classify, golden_vectors};
use crate::constants;
use packtool::{Packed, Packet, View};
fn init_datagram(version: u8) -> Vec<u8> {
let mut dgram = vec![0xABu8; constants::INIT_PACKET_LEN];
dgram[0] = constants::PKT_HANDSHAKE_INIT;
dgram[1] = version;
dgram
}
fn resp_datagram(version: u8) -> Vec<u8> {
let mut dgram = vec![0xCDu8; constants::RESP_PACKET_LEN];
dgram[0] = constants::PKT_HANDSHAKE_RESP;
dgram[1] = version;
dgram
}
fn data_datagram(version: u8, len: usize) -> Vec<u8> {
let mut dgram = vec![0xEFu8; len];
dgram[0] = constants::PKT_DATA;
dgram[1] = version;
dgram
}
fn min_data_len() -> usize {
constants::DATA_HEADER_LEN + constants::AEAD_TAG_LEN
}
#[test]
fn unknown_version_is_dropped_silently() {
for version in 0u8..=0xFF {
if version == constants::VERSION {
continue;
}
assert!(
classify::<ReferenceSuite>(&init_datagram(version)).is_none(),
"Init at version {version:#04x} was not dropped"
);
assert!(
classify::<ReferenceSuite>(&resp_datagram(version)).is_none(),
"Resp at version {version:#04x} was not dropped"
);
assert!(
classify::<ReferenceSuite>(&data_datagram(version, min_data_len())).is_none(),
"Data at version {version:#04x} was not dropped"
);
}
assert!(classify::<ReferenceSuite>(&init_datagram(constants::VERSION)).is_some());
assert!(classify::<ReferenceSuite>(&resp_datagram(constants::VERSION)).is_some());
assert!(
classify::<ReferenceSuite>(&data_datagram(constants::VERSION, min_data_len())).is_some()
);
}
#[test]
fn unknown_and_reserved_types_are_dropped() {
let lengths = [
constants::INIT_PACKET_LEN,
constants::RESP_PACKET_LEN,
min_data_len(),
constants::MAX_DATAGRAM,
];
for type_byte in 0u8..=0xFF {
if type_byte == constants::PKT_HANDSHAKE_INIT
|| type_byte == constants::PKT_HANDSHAKE_RESP
|| type_byte == constants::PKT_DATA
{
continue;
}
for &len in &lengths {
let mut dgram = vec![0x5Au8; len];
dgram[0] = type_byte;
dgram[1] = constants::VERSION;
assert!(
classify::<ReferenceSuite>(&dgram).is_none(),
"type {type_byte:#04x} at length {len} was not dropped"
);
}
}
}
#[test]
fn oversize_is_dropped() {
assert!(
classify::<ReferenceSuite>(&data_datagram(
constants::VERSION,
constants::MAX_DATAGRAM + 1
))
.is_none()
);
assert!(
classify::<ReferenceSuite>(&data_datagram(constants::VERSION, constants::MAX_DATAGRAM))
.is_some()
);
}
#[test]
fn short_is_dropped() {
assert!(classify::<ReferenceSuite>(&[]).is_none());
assert!(classify::<ReferenceSuite>(&[constants::PKT_HANDSHAKE_INIT]).is_none());
let short_init = &init_datagram(constants::VERSION)[..constants::INIT_PACKET_LEN - 1];
assert!(classify::<ReferenceSuite>(short_init).is_none());
let short_resp = &resp_datagram(constants::VERSION)[..constants::RESP_PACKET_LEN - 1];
assert!(classify::<ReferenceSuite>(short_resp).is_none());
assert!(
classify::<ReferenceSuite>(&data_datagram(constants::VERSION, min_data_len() - 1))
.is_none()
);
}
#[test]
fn handshake_length_is_exact_not_a_minimum() {
let init = init_datagram(constants::VERSION);
let short_init = &init[..constants::INIT_PACKET_LEN - 1];
let mut over_init = init.clone();
over_init.push(0x99);
assert_eq!(over_init.len(), constants::INIT_PACKET_LEN + 1);
assert!(
classify::<ReferenceSuite>(short_init).is_none(),
"INIT_PACKET_LEN - 1 must be dropped"
);
assert!(
classify::<ReferenceSuite>(&init).is_some(),
"INIT_PACKET_LEN must pass"
);
assert!(
classify::<ReferenceSuite>(&over_init).is_none(),
"INIT_PACKET_LEN + 1 must be dropped (ruling 65: exact, not a minimum)"
);
let resp = resp_datagram(constants::VERSION);
let short_resp = &resp[..constants::RESP_PACKET_LEN - 1];
let mut over_resp = resp.clone();
over_resp.push(0x99);
assert_eq!(over_resp.len(), constants::RESP_PACKET_LEN + 1);
assert!(
classify::<ReferenceSuite>(short_resp).is_none(),
"RESP_PACKET_LEN - 1 must be dropped"
);
assert!(
classify::<ReferenceSuite>(&resp).is_some(),
"RESP_PACKET_LEN must pass"
);
assert!(
classify::<ReferenceSuite>(&over_resp).is_none(),
"RESP_PACKET_LEN + 1 must be dropped (ruling 65: exact, not a minimum)"
);
}
#[test]
fn a_mismatched_suite_dies_at_the_length_gate() {
const OTHER_SUITE_INIT_LEN: usize = 130;
assert_ne!(OTHER_SUITE_INIT_LEN, constants::INIT_PACKET_LEN);
let mut dgram = vec![0x33u8; OTHER_SUITE_INIT_LEN];
dgram[0] = constants::PKT_HANDSHAKE_INIT;
dgram[1] = constants::VERSION;
assert!(classify::<ReferenceSuite>(&dgram).is_none());
}
#[test]
fn keepalive_is_the_minimum_data_packet() {
assert!(
classify::<ReferenceSuite>(&data_datagram(constants::VERSION, min_data_len())).is_some()
);
assert!(
classify::<ReferenceSuite>(&data_datagram(constants::VERSION, min_data_len() - 1))
.is_none()
);
}
#[test]
fn the_gate_never_panics() {
use rand_chacha::ChaCha20Rng;
use rand_core::{Rng, SeedableRng};
let mut rng = ChaCha20Rng::seed_from_u64(0x5117_4E52_0001);
let mut buf = [0u8; 1300];
for _ in 0..5000 {
let len = (rng.next_u32() as usize) % (constants::MAX_DATAGRAM + 101);
let len = len.min(buf.len());
rng.fill_bytes(&mut buf[..len]);
let _ = classify::<ReferenceSuite>(&buf[..len]);
}
}
#[test]
fn data_ad_is_the_leading_header_bytes_verbatim() {
let dgram = data_datagram(constants::VERSION, min_data_len() + 5);
match classify::<ReferenceSuite>(&dgram) {
Some(Inbound::Data { ad, .. }) => {
assert_eq!(ad.len(), constants::DATA_HEADER_LEN);
assert_eq!(
ad.as_ptr(),
dgram.as_ptr(),
"ad is not a borrow of dgram's own start"
);
assert_eq!(ad, &dgram[..constants::DATA_HEADER_LEN]);
}
_ => panic!("expected Inbound::Data"),
}
}
#[test]
fn handshake_preimage_and_tag_partition_the_datagram() {
let init = init_datagram(constants::VERSION);
match classify::<ReferenceSuite>(&init) {
Some(Inbound::Init { preimage, mac1, .. }) => {
assert_eq!(preimage.len() + mac1.len(), init.len());
assert_eq!(mac1.len(), constants::MAC1_LEN);
assert_eq!(preimage.as_ptr(), init.as_ptr());
assert_eq!(mac1.as_ptr(), init[preimage.len()..].as_ptr());
}
_ => panic!("expected Inbound::Init"),
}
let resp = resp_datagram(constants::VERSION);
match classify::<ReferenceSuite>(&resp) {
Some(Inbound::Resp { preimage, mac1, .. }) => {
assert_eq!(preimage.len() + mac1.len(), resp.len());
assert_eq!(mac1.len(), constants::MAC1_LEN);
assert_eq!(preimage.as_ptr(), resp.as_ptr());
assert_eq!(mac1.as_ptr(), resp[preimage.len()..].as_ptr());
}
_ => panic!("expected Inbound::Resp"),
}
}
#[test]
fn header_sizes_match_constants() {
assert_eq!(<InitHeader as Packed>::SIZE, constants::INIT_HEADER_LEN);
assert_eq!(<RespHeader as Packed>::SIZE, constants::RESP_HEADER_LEN);
assert_eq!(<DataHeader as Packed>::SIZE, constants::DATA_HEADER_LEN);
assert_eq!(
<DataHeader as Packed>::SIZE + constants::AEAD_TAG_LEN + constants::MAX_PLAINTEXT,
constants::MAX_DATAGRAM
);
}
#[test]
fn headers_round_trip() {
for &v in &[0u32, 1, u32::MAX] {
let packed = Packet::pack(&InitHeader::new(v));
let bytes: &[u8] = packed.as_ref();
let decoded = View::<InitHeader>::try_from_slice(bytes)
.expect("well-sized slice")
.unpack();
assert_eq!(decoded.sender_index, v);
}
for &(s, r) in &[
(0u32, 0u32),
(1, 1),
(u32::MAX, 0),
(0, u32::MAX),
(u32::MAX, u32::MAX),
] {
let packed = Packet::pack(&RespHeader::new(s, r));
let bytes: &[u8] = packed.as_ref();
let decoded = View::<RespHeader>::try_from_slice(bytes)
.expect("well-sized slice")
.unpack();
assert_eq!(decoded.sender_index, s);
assert_eq!(decoded.receiver_index, r);
}
for &(rx, c) in &[
(0u32, 0u64),
(1, 1),
(u32::MAX, u64::MAX - 1),
(u32::MAX, u64::MAX),
] {
let packed = Packet::pack(&DataHeader::new(rx, c));
let bytes: &[u8] = packed.as_ref();
let decoded = View::<DataHeader>::try_from_slice(bytes)
.expect("well-sized slice")
.unpack();
assert_eq!(decoded.receiver_index, rx);
assert_eq!(decoded.counter, c);
}
}
#[test]
fn resp_header_fields_do_not_swap() {
const SENDER: u32 = 0x0A0B_0C0D;
const RECEIVER: u32 = 0x1122_3344;
assert_ne!(SENDER, RECEIVER);
let packed = Packet::pack(&RespHeader::new(SENDER, RECEIVER));
let bytes: &[u8] = packed.as_ref();
let decoded = View::<RespHeader>::try_from_slice(bytes)
.expect("well-sized slice")
.unpack();
assert_eq!(decoded.sender_index, SENDER);
assert_eq!(decoded.receiver_index, RECEIVER);
}
#[test]
fn mac1_key_matches_the_golden_vector() {
let key = Mac1Key::derive(&golden_vectors::canonical_static::BYTES);
assert!(key.verify(
&golden_vectors::mac1_init::PREIMAGE,
&golden_vectors::mac1_init::TAG
));
assert!(key.verify(
&golden_vectors::mac1_resp::PREIMAGE,
&golden_vectors::mac1_resp::TAG
));
}
#[test]
fn mac1_tag_matches_the_golden_vectors() {
let key = Mac1Key::derive(&golden_vectors::canonical_static::BYTES);
assert_eq!(
key.tag(&golden_vectors::mac1_init::PREIMAGE),
golden_vectors::mac1_init::TAG
);
assert_eq!(
key.tag(&golden_vectors::mac1_resp::PREIMAGE),
golden_vectors::mac1_resp::TAG
);
}
#[test]
fn mac1_rejects_every_single_bit_flip() {
let key = Mac1Key::derive(&golden_vectors::canonical_static::BYTES);
let preimage = golden_vectors::mac1_init::PREIMAGE;
let tag = key.tag(&preimage);
assert!(
key.verify(&preimage, &tag),
"the unflipped baseline must itself verify"
);
let mut positions: Vec<usize> = (0..8).chain(preimage.len() - 8..preimage.len()).collect();
positions.push(preimage.len() / 2);
for pos in positions {
for bit in 0u8..8 {
let mut flipped = preimage;
flipped[pos] ^= 1 << bit;
assert!(
!key.verify(&flipped, &tag),
"preimage byte {pos} bit {bit} was not rejected"
);
}
}
for byte in 0..tag.len() {
for bit in 0u8..8 {
let mut flipped = tag;
flipped[byte] ^= 1 << bit;
assert!(
!key.verify(&preimage, &flipped),
"tag byte {byte} bit {bit} was not rejected"
);
}
}
}
#[test]
fn mac1_does_not_follow_the_suite_hash() {
mod second_hash_suite {
crate::channel! {
pub(crate) SecondHash<hiss::curve::p256::P256, hiss::noise::cipher::ChaChaPoly, hiss::noise::hash::Sha256>;
}
}
use second_hash_suite::SecondHash;
assert_ne!(
<SecondHash as Channel>::PROTOCOL_NAME,
<ReferenceSuite as Channel>::PROTOCOL_NAME,
"varying only the Hash must still produce a different protocol name"
);
assert_eq!(
<SecondHash as Channel>::MSG1_LEN,
<ReferenceSuite as Channel>::MSG1_LEN
);
assert_eq!(
<SecondHash as Channel>::MSG2_LEN,
<ReferenceSuite as Channel>::MSG2_LEN
);
assert_eq!(
<SecondHash as Channel>::INIT_PACKET_LEN,
<ReferenceSuite as Channel>::INIT_PACKET_LEN
);
assert_eq!(
<SecondHash as Channel>::RESP_PACKET_LEN,
<ReferenceSuite as Channel>::RESP_PACKET_LEN
);
let key = Mac1Key::derive(&golden_vectors::canonical_static::BYTES);
assert_eq!(
key.tag(&golden_vectors::mac1_init::PREIMAGE),
golden_vectors::mac1_init::TAG
);
}
#[test]
fn mac1_keys_on_the_canonical_static() {
assert_eq!(
golden_vectors::canonical_static::BYTES.len(),
constants::STATIC_PUBLIC_LEN
);
assert_eq!(constants::STATIC_PUBLIC_LEN, 65);
let full = Mac1Key::derive(&golden_vectors::canonical_static::BYTES);
let truncated = Mac1Key::derive(&golden_vectors::canonical_static::BYTES[..33]);
let preimage = golden_vectors::mac1_init::PREIMAGE;
assert_ne!(full.tag(&preimage), truncated.tag(&preimage));
}
#[test]
fn msg1_payload_matches_the_golden_vector() {
let payload = Msg1Payload::new(
golden_vectors::msg1_payload::SECS,
golden_vectors::msg1_payload::NANOS,
);
assert_eq!(payload.encode(), golden_vectors::msg1_payload::BYTES);
assert_eq!(
Msg1Payload::decode(&golden_vectors::msg1_payload::BYTES),
payload
);
}
#[test]
fn msg1_payload_is_big_endian_not_little() {
const SECS: u64 = 0x0102_0304_0506_0708;
const NANOS: u32 = 0x0910_1112;
let encoded = Msg1Payload::new(SECS, NANOS).encode();
let mut little_endian = [0u8; constants::MSG1_PAYLOAD_LEN];
little_endian[..8].copy_from_slice(&SECS.to_le_bytes());
little_endian[8..].copy_from_slice(&NANOS.to_le_bytes());
assert_ne!(
encoded, little_endian,
"encode() matched the little-endian encoding of the same fields"
);
let mut big_endian = [0u8; constants::MSG1_PAYLOAD_LEN];
big_endian[..8].copy_from_slice(&SECS.to_be_bytes());
big_endian[8..].copy_from_slice(&NANOS.to_be_bytes());
assert_eq!(encoded, big_endian);
}
#[test]
fn msg1_payload_round_trips_and_orders_chronologically() {
let payload = Msg1Payload::new(
golden_vectors::msg1_payload::SECS,
golden_vectors::msg1_payload::NANOS,
);
assert_eq!(Msg1Payload::decode(&payload.encode()), payload);
let earlier = Msg1Payload::new(1_700_000_000, 0);
let later_by_nanos = Msg1Payload::new(1_700_000_000, 1);
let later_by_secs = Msg1Payload::new(1_700_000_001, 0);
assert!(earlier < later_by_nanos);
assert!(earlier < later_by_secs);
assert!(later_by_nanos < later_by_secs);
}
#[test]
fn golden_prologue() {
assert_eq!(
constants::PROLOGUE.as_slice(),
golden_vectors::prologue::BYTES.as_slice()
);
assert_eq!(golden_vectors::prologue::BYTES.len(), 8);
assert_eq!(golden_vectors::prologue::BYTES[7], constants::VERSION);
}
#[test]
fn golden_mac1_label() {
assert_eq!(
constants::MAC1_LABEL.as_slice(),
golden_vectors::mac1_label::BYTES.as_slice()
);
assert_eq!(golden_vectors::mac1_label::BYTES.len(), 12);
}
#[test]
fn golden_init_header() {
let packed = Packet::pack(&InitHeader::new(golden_vectors::init_header::SENDER_INDEX));
let bytes: &[u8] = packed.as_ref();
assert_eq!(bytes, golden_vectors::init_header::BYTES.as_slice());
assert_eq!(bytes[0], constants::PKT_HANDSHAKE_INIT);
assert_eq!(bytes[1], constants::VERSION);
let decoded = View::<InitHeader>::try_from_slice(&golden_vectors::init_header::BYTES)
.expect("well-sized slice")
.unpack();
assert_eq!(
decoded.sender_index,
golden_vectors::init_header::SENDER_INDEX
);
}
#[test]
fn golden_resp_header() {
let packed = Packet::pack(&RespHeader::new(
golden_vectors::resp_header::SENDER_INDEX,
golden_vectors::resp_header::RECEIVER_INDEX,
));
let bytes: &[u8] = packed.as_ref();
assert_eq!(bytes, golden_vectors::resp_header::BYTES.as_slice());
assert_eq!(bytes[0], constants::PKT_HANDSHAKE_RESP);
assert_eq!(bytes[1], constants::VERSION);
let decoded = View::<RespHeader>::try_from_slice(&golden_vectors::resp_header::BYTES)
.expect("well-sized slice")
.unpack();
assert_eq!(
decoded.sender_index,
golden_vectors::resp_header::SENDER_INDEX
);
assert_eq!(
decoded.receiver_index,
golden_vectors::resp_header::RECEIVER_INDEX
);
}
#[test]
fn golden_data_header() {
let packed = Packet::pack(&DataHeader::new(
golden_vectors::data_header::RECEIVER_INDEX,
golden_vectors::data_header::COUNTER,
));
let bytes: &[u8] = packed.as_ref();
assert_eq!(bytes, golden_vectors::data_header::BYTES.as_slice());
assert_eq!(bytes[0], constants::PKT_DATA);
assert_eq!(bytes[1], constants::VERSION);
let decoded = View::<DataHeader>::try_from_slice(&golden_vectors::data_header::BYTES)
.expect("well-sized slice")
.unpack();
assert_eq!(
decoded.receiver_index,
golden_vectors::data_header::RECEIVER_INDEX
);
assert_eq!(decoded.counter, golden_vectors::data_header::COUNTER);
}
#[test]
fn golden_msg1_payload() {
let payload = Msg1Payload::new(
golden_vectors::msg1_payload::SECS,
golden_vectors::msg1_payload::NANOS,
);
assert_eq!(payload.encode(), golden_vectors::msg1_payload::BYTES);
assert_eq!(
Msg1Payload::decode(&golden_vectors::msg1_payload::BYTES),
payload
);
}
#[test]
fn golden_mac1() {
let key = Mac1Key::derive(&golden_vectors::canonical_static::BYTES);
assert_eq!(
key.tag(&golden_vectors::mac1_init::PREIMAGE),
golden_vectors::mac1_init::TAG
);
assert_eq!(
key.tag(&golden_vectors::mac1_resp::PREIMAGE),
golden_vectors::mac1_resp::TAG
);
assert_eq!(
golden_vectors::mac1_init::PREIMAGE.len(),
constants::INIT_PACKET_LEN - constants::MAC1_LEN
);
assert_eq!(
golden_vectors::mac1_resp::PREIMAGE.len(),
constants::RESP_PACKET_LEN - constants::MAC1_LEN
);
}
#[test]
fn golden_canonical_static() {
assert_eq!(
golden_vectors::canonical_static::BYTES.len(),
constants::STATIC_PUBLIC_LEN
);
}
#[test]
fn sizes_match_the_golden_vectors() {
use golden_vectors::sizes as g;
assert_eq!(g::INIT_HEADER_LEN, constants::INIT_HEADER_LEN);
assert_eq!(g::RESP_HEADER_LEN, constants::RESP_HEADER_LEN);
assert_eq!(g::DATA_HEADER_LEN, constants::DATA_HEADER_LEN);
assert_eq!(g::IK_MSG1_LEN, constants::IK_MSG1_LEN);
assert_eq!(g::IK_MSG2_LEN, constants::IK_MSG2_LEN);
assert_eq!(g::INIT_PACKET_LEN, constants::INIT_PACKET_LEN);
assert_eq!(g::RESP_PACKET_LEN, constants::RESP_PACKET_LEN);
assert_eq!(g::MAC1_LEN, constants::MAC1_LEN);
assert_eq!(g::AEAD_TAG_LEN, constants::AEAD_TAG_LEN);
assert_eq!(g::MAX_DATAGRAM, constants::MAX_DATAGRAM);
assert_eq!(g::MAX_PLAINTEXT, constants::MAX_PLAINTEXT);
assert_eq!(g::MIN_DATA_LEN, min_data_len());
assert_eq!(g::IK_MSG1_LEN, <ReferenceSuite as Channel>::MSG1_LEN);
assert_eq!(g::IK_MSG2_LEN, <ReferenceSuite as Channel>::MSG2_LEN);
assert_eq!(
g::INIT_PACKET_LEN,
<ReferenceSuite as Channel>::INIT_PACKET_LEN
);
assert_eq!(
g::RESP_PACKET_LEN,
<ReferenceSuite as Channel>::RESP_PACKET_LEN
);
}
#[test]
fn no_hand_rolled_byte_order_outside_the_payload_codec() {
let files: &[(&str, &str)] = &[
("mod.rs", include_str!("mod.rs")),
("suite.rs", include_str!("suite.rs")),
("header.rs", include_str!("header.rs")),
("mac.rs", include_str!("mac.rs")),
("payload.rs", include_str!("payload.rs")),
];
for (name, src) in files {
assert!(
!src.contains("to_le_bytes") && !src.contains("from_le_bytes"),
"{name} contains a hand-rolled little-endian conversion; \
packtool's derive is the only permitted LE encoder (ruling 64)"
);
if *name != "payload.rs" {
assert!(
!src.contains("to_be_bytes") && !src.contains("from_be_bytes"),
"{name} contains a hand-rolled big-endian conversion outside payload.rs"
);
}
}
}