mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-10 08:37:02 +00:00
FLP wire format revision and MMP link-layer measurement protocol
## FLP Wire Format Revision Replace the 1-byte discriminator with a structured wire format: - 4-byte common prefix (ver+phase, flags, payload_len) and 16-byte established frame header with AEAD AAD binding - 5-byte encrypted inner header (4-byte session-relative timestamp + 1-byte message type) on all link messages - Phase-based packet dispatch replacing discriminator-based dispatch - SessionDatagram reassigned from type 0x40 to 0x00; add SenderReport (0x01) and ReceiverReport (0x02) message types for MMP - SessionDatagram: rename hop_limit to ttl, add path_mtu field (u16 LE) with min(datagram.path_mtu, transport.mtu()) at forwarding - Updated handshake packets (msg1: 87->90 bytes, msg2: 42->45 bytes) - FIPS_OVERHEAD updated from 135 to 144 bytes ## MMP Link-Layer Measurement Protocol Add the Metrics Measurement Protocol for link quality measurement between FIPS peers. Measures RTT, loss, jitter, throughput, OWD trend, and ETX from periodic sender/receiver reports exchanged over established links. Module layout: - mmp/algorithms.rs: JitterEstimator, SrttEstimator, DualEwma, OwdTrend, SpinBit, ETX computation - mmp/report.rs: SenderReport (48B) and ReceiverReport (68B) wire format - mmp/sender.rs: per-peer TX counters and interval tracking - mmp/receiver.rs: per-peer RX counters, jitter, loss, gap tracking - mmp/metrics.rs: derived metrics from report processing (SRTT, goodput_bps) - mmp/mod.rs: MmpMode (Full/Lightweight/Minimal), MmpConfig, MmpPeerState - node/handlers/mmp.rs: report dispatch, timer-driven generation, operator logging (periodic + teardown) Integration: per-frame TX/RX hooks in encrypted message handling, report dispatch from link message router, timer-driven generation from tick handler, and periodic operator logging with throughput formatting. Three operating modes: Full (sender + receiver reports, spin bit, CE echo), Lightweight (receiver reports only), Minimal (spin bit + CE echo only). ## Design Documentation Updated FLP sections across all design documents to match the implemented wire format, including revised overhead calculations and numeric values. 568 tests pass, clippy clean.
This commit is contained in:
+361
-93
@@ -1,15 +1,21 @@
|
||||
//! Wire Format Parsing and Serialization
|
||||
//!
|
||||
//! Defines the FIPS link-layer wire format for packet dispatch.
|
||||
//! All packets begin with a discriminator byte followed by type-specific payload.
|
||||
//! Defines the FIPS link-layer wire format (FLP) for packet dispatch.
|
||||
//! All packets begin with a 4-byte common prefix followed by phase-specific fields.
|
||||
//!
|
||||
//! ## Common Prefix (4 bytes)
|
||||
//!
|
||||
//! ```text
|
||||
//! [ver+phase:1][flags:1][payload_len:2 LE]
|
||||
//! ```
|
||||
//!
|
||||
//! ## Packet Types
|
||||
//!
|
||||
//! | Byte | Type | Size | Description |
|
||||
//! |------|-----------------|-----------|--------------------------------|
|
||||
//! | 0x00 | Encrypted frame | 29+ bytes | Post-handshake encrypted data |
|
||||
//! | 0x01 | Noise IK msg1 | 87 bytes | Handshake initiation |
|
||||
//! | 0x02 | Noise IK msg2 | 42 bytes | Handshake response |
|
||||
//! | Phase | Type | Size | Description |
|
||||
//! |-------|-----------------|-----------|--------------------------------|
|
||||
//! | 0x0 | Encrypted frame | 32+ bytes | Post-handshake encrypted data |
|
||||
//! | 0x1 | Noise IK msg1 | 90 bytes | Handshake initiation |
|
||||
//! | 0x2 | Noise IK msg2 | 45 bytes | Handshake response |
|
||||
|
||||
use crate::utils::index::SessionIndex;
|
||||
use crate::noise::{HANDSHAKE_MSG1_SIZE, HANDSHAKE_MSG2_SIZE, TAG_SIZE};
|
||||
@@ -18,73 +24,171 @@ use crate::noise::{HANDSHAKE_MSG1_SIZE, HANDSHAKE_MSG2_SIZE, TAG_SIZE};
|
||||
// Constants
|
||||
// ============================================================================
|
||||
|
||||
/// Discriminator for encrypted frames (post-handshake data).
|
||||
pub const DISCRIMINATOR_ENCRYPTED: u8 = 0x00;
|
||||
/// FLP protocol version (4 high bits of byte 0).
|
||||
pub const FLP_VERSION: u8 = 0;
|
||||
|
||||
/// Discriminator for Noise IK message 1 (handshake initiation).
|
||||
pub const DISCRIMINATOR_MSG1: u8 = 0x01;
|
||||
/// Phase value for established (encrypted) frames.
|
||||
pub const PHASE_ESTABLISHED: u8 = 0x0;
|
||||
|
||||
/// Discriminator for Noise IK message 2 (handshake response).
|
||||
pub const DISCRIMINATOR_MSG2: u8 = 0x02;
|
||||
/// Phase value for Noise IK message 1 (handshake initiation).
|
||||
pub const PHASE_MSG1: u8 = 0x1;
|
||||
|
||||
/// Size of Noise IK message 1 wire packet: discriminator + sender_idx + noise_msg1.
|
||||
pub const MSG1_WIRE_SIZE: usize = 1 + 4 + HANDSHAKE_MSG1_SIZE; // 87 bytes
|
||||
/// Phase value for Noise IK message 2 (handshake response).
|
||||
pub const PHASE_MSG2: u8 = 0x2;
|
||||
|
||||
/// Size of Noise IK message 2 wire packet: discriminator + sender_idx + receiver_idx + noise_msg2.
|
||||
pub const MSG2_WIRE_SIZE: usize = 1 + 4 + 4 + HANDSHAKE_MSG2_SIZE; // 42 bytes
|
||||
/// Size of the common packet prefix (all packet types).
|
||||
pub const COMMON_PREFIX_SIZE: usize = 4;
|
||||
|
||||
/// Minimum size for encrypted frame: discriminator + receiver_idx + counter + tag.
|
||||
pub const ENCRYPTED_MIN_SIZE: usize = 1 + 4 + 8 + TAG_SIZE; // 29 bytes
|
||||
/// Size of the full established frame header (prefix + receiver_idx + counter).
|
||||
pub const ESTABLISHED_HEADER_SIZE: usize = 16;
|
||||
|
||||
/// Size of Noise IK message 1 wire packet: prefix + sender_idx + noise_msg1.
|
||||
pub const MSG1_WIRE_SIZE: usize = COMMON_PREFIX_SIZE + 4 + HANDSHAKE_MSG1_SIZE; // 90 bytes
|
||||
|
||||
/// Size of Noise IK message 2 wire packet: prefix + sender_idx + receiver_idx + noise_msg2.
|
||||
pub const MSG2_WIRE_SIZE: usize = COMMON_PREFIX_SIZE + 4 + 4 + HANDSHAKE_MSG2_SIZE; // 45 bytes
|
||||
|
||||
/// Minimum size for encrypted frame: header + tag (no plaintext).
|
||||
pub const ENCRYPTED_MIN_SIZE: usize = ESTABLISHED_HEADER_SIZE + TAG_SIZE; // 32 bytes
|
||||
|
||||
/// Size of the encrypted inner header (timestamp + message type).
|
||||
pub const INNER_HEADER_SIZE: usize = 5;
|
||||
|
||||
// Flag bit constants (byte 1 of common prefix, meaningful only for phase 0x0).
|
||||
// Reserved for upcoming rekeying, congestion signaling, and RTT measurement.
|
||||
#[allow(dead_code)]
|
||||
/// Key epoch flag — selects active key during rekeying.
|
||||
pub const FLAG_KEY_EPOCH: u8 = 0x01;
|
||||
#[allow(dead_code)]
|
||||
/// Congestion Experienced echo flag.
|
||||
pub const FLAG_CE: u8 = 0x02;
|
||||
#[allow(dead_code)]
|
||||
/// Spin bit for RTT measurement.
|
||||
pub const FLAG_SP: u8 = 0x04;
|
||||
|
||||
// ============================================================================
|
||||
// Common Prefix
|
||||
// ============================================================================
|
||||
|
||||
/// Parsed common packet prefix (first 4 bytes of every FLP packet).
|
||||
///
|
||||
/// Wire format:
|
||||
/// ```text
|
||||
/// [ver(4bits)+phase(4bits)][flags:1][payload_len:2 LE]
|
||||
/// ```
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CommonPrefix {
|
||||
/// Protocol version (high nibble of byte 0).
|
||||
pub version: u8,
|
||||
/// Session lifecycle phase (low nibble of byte 0).
|
||||
pub phase: u8,
|
||||
/// Per-packet signal flags (meaningful only for phase 0x0).
|
||||
#[allow(dead_code)]
|
||||
pub flags: u8,
|
||||
/// Length of payload following the phase-specific header (excludes AEAD tag).
|
||||
#[allow(dead_code)]
|
||||
pub payload_len: u16,
|
||||
}
|
||||
|
||||
impl CommonPrefix {
|
||||
/// Parse a common prefix from the first 4 bytes of packet data.
|
||||
pub fn parse(data: &[u8]) -> Option<Self> {
|
||||
if data.len() < COMMON_PREFIX_SIZE {
|
||||
return None;
|
||||
}
|
||||
|
||||
let version = data[0] >> 4;
|
||||
let phase = data[0] & 0x0F;
|
||||
let flags = data[1];
|
||||
let payload_len = u16::from_le_bytes([data[2], data[3]]);
|
||||
|
||||
Some(Self {
|
||||
version,
|
||||
phase,
|
||||
flags,
|
||||
payload_len,
|
||||
})
|
||||
}
|
||||
|
||||
/// Encode the ver+phase byte.
|
||||
fn ver_phase_byte(version: u8, phase: u8) -> u8 {
|
||||
(version << 4) | (phase & 0x0F)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Encrypted Frame Header
|
||||
// ============================================================================
|
||||
|
||||
/// Parsed encrypted frame header.
|
||||
/// Parsed established frame header (phase 0x0).
|
||||
///
|
||||
/// Wire format:
|
||||
/// Wire format (16 bytes):
|
||||
/// ```text
|
||||
/// [0x00][receiver_idx:4 LE][counter:8 LE][ciphertext+tag]
|
||||
/// [ver+phase:1][flags:1][payload_len:2 LE][receiver_idx:4 LE][counter:8 LE]
|
||||
/// ```
|
||||
///
|
||||
/// The full 16-byte header is used as AAD for the AEAD construction.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct EncryptedHeader {
|
||||
/// Per-packet flags (K, CE, SP).
|
||||
#[allow(dead_code)]
|
||||
pub flags: u8,
|
||||
/// Length of encrypted payload (excluding AEAD tag).
|
||||
#[allow(dead_code)]
|
||||
pub payload_len: u16,
|
||||
/// Session index chosen by the receiver (for O(1) lookup).
|
||||
pub receiver_idx: SessionIndex,
|
||||
/// Monotonic counter used as AEAD nonce.
|
||||
pub counter: u64,
|
||||
/// Offset where ciphertext begins in the original packet.
|
||||
pub ciphertext_offset: usize,
|
||||
/// Raw 16-byte header for use as AEAD AAD.
|
||||
pub header_bytes: [u8; ESTABLISHED_HEADER_SIZE],
|
||||
}
|
||||
|
||||
impl EncryptedHeader {
|
||||
/// Parse an encrypted frame header from packet data.
|
||||
/// Parse an established frame header from packet data.
|
||||
///
|
||||
/// Returns None if the packet is too short or has wrong discriminator.
|
||||
/// Returns None if the packet is too short or has wrong version/phase.
|
||||
pub fn parse(data: &[u8]) -> Option<Self> {
|
||||
if data.len() < ENCRYPTED_MIN_SIZE {
|
||||
return None;
|
||||
}
|
||||
|
||||
if data[0] != DISCRIMINATOR_ENCRYPTED {
|
||||
let version = data[0] >> 4;
|
||||
let phase = data[0] & 0x0F;
|
||||
|
||||
if version != FLP_VERSION || phase != PHASE_ESTABLISHED {
|
||||
return None;
|
||||
}
|
||||
|
||||
let receiver_idx = SessionIndex::from_le_bytes([data[1], data[2], data[3], data[4]]);
|
||||
let flags = data[1];
|
||||
let payload_len = u16::from_le_bytes([data[2], data[3]]);
|
||||
let receiver_idx = SessionIndex::from_le_bytes([data[4], data[5], data[6], data[7]]);
|
||||
let counter = u64::from_le_bytes([
|
||||
data[5], data[6], data[7], data[8], data[9], data[10], data[11], data[12],
|
||||
data[8], data[9], data[10], data[11],
|
||||
data[12], data[13], data[14], data[15],
|
||||
]);
|
||||
|
||||
let mut header_bytes = [0u8; ESTABLISHED_HEADER_SIZE];
|
||||
header_bytes.copy_from_slice(&data[..ESTABLISHED_HEADER_SIZE]);
|
||||
|
||||
Some(Self {
|
||||
flags,
|
||||
payload_len,
|
||||
receiver_idx,
|
||||
counter,
|
||||
ciphertext_offset: 13,
|
||||
header_bytes,
|
||||
})
|
||||
}
|
||||
|
||||
/// Offset where ciphertext begins in the original packet.
|
||||
pub fn ciphertext_offset(&self) -> usize {
|
||||
ESTABLISHED_HEADER_SIZE
|
||||
}
|
||||
|
||||
/// Get the ciphertext slice from the original packet.
|
||||
#[cfg(test)]
|
||||
pub fn ciphertext<'a>(&self, data: &'a [u8]) -> &'a [u8] {
|
||||
&data[self.ciphertext_offset..]
|
||||
&data[ESTABLISHED_HEADER_SIZE..]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,11 +196,11 @@ impl EncryptedHeader {
|
||||
// Msg1 Header
|
||||
// ============================================================================
|
||||
|
||||
/// Parsed Noise IK message 1 header.
|
||||
/// Parsed Noise IK message 1 header (phase 0x1).
|
||||
///
|
||||
/// Wire format:
|
||||
/// Wire format (90 bytes):
|
||||
/// ```text
|
||||
/// [0x01][sender_idx:4 LE][noise_msg1:82]
|
||||
/// [0x01][0x00][payload_len:2 LE][sender_idx:4 LE][noise_msg1:82]
|
||||
/// ```
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Msg1Header {
|
||||
@@ -109,21 +213,29 @@ pub struct Msg1Header {
|
||||
impl Msg1Header {
|
||||
/// Parse a msg1 header from packet data.
|
||||
///
|
||||
/// Returns None if the packet has wrong size or discriminator.
|
||||
/// Returns None if the packet has wrong size or version/phase.
|
||||
pub fn parse(data: &[u8]) -> Option<Self> {
|
||||
if data.len() != MSG1_WIRE_SIZE {
|
||||
return None;
|
||||
}
|
||||
|
||||
if data[0] != DISCRIMINATOR_MSG1 {
|
||||
let version = data[0] >> 4;
|
||||
let phase = data[0] & 0x0F;
|
||||
|
||||
if version != FLP_VERSION || phase != PHASE_MSG1 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let sender_idx = SessionIndex::from_le_bytes([data[1], data[2], data[3], data[4]]);
|
||||
// flags must be zero during handshake
|
||||
if data[1] != 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let sender_idx = SessionIndex::from_le_bytes([data[4], data[5], data[6], data[7]]);
|
||||
|
||||
Some(Self {
|
||||
sender_idx,
|
||||
noise_msg1_offset: 5,
|
||||
noise_msg1_offset: COMMON_PREFIX_SIZE + 4, // 8
|
||||
})
|
||||
}
|
||||
|
||||
@@ -138,11 +250,11 @@ impl Msg1Header {
|
||||
// Msg2 Header
|
||||
// ============================================================================
|
||||
|
||||
/// Parsed Noise IK message 2 header.
|
||||
/// Parsed Noise IK message 2 header (phase 0x2).
|
||||
///
|
||||
/// Wire format:
|
||||
/// Wire format (45 bytes):
|
||||
/// ```text
|
||||
/// [0x02][sender_idx:4 LE][receiver_idx:4 LE][noise_msg2:33]
|
||||
/// [0x02][0x00][payload_len:2 LE][sender_idx:4 LE][receiver_idx:4 LE][noise_msg2:33]
|
||||
/// ```
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Msg2Header {
|
||||
@@ -157,23 +269,31 @@ pub struct Msg2Header {
|
||||
impl Msg2Header {
|
||||
/// Parse a msg2 header from packet data.
|
||||
///
|
||||
/// Returns None if the packet has wrong size or discriminator.
|
||||
/// Returns None if the packet has wrong size or version/phase.
|
||||
pub fn parse(data: &[u8]) -> Option<Self> {
|
||||
if data.len() != MSG2_WIRE_SIZE {
|
||||
return None;
|
||||
}
|
||||
|
||||
if data[0] != DISCRIMINATOR_MSG2 {
|
||||
let version = data[0] >> 4;
|
||||
let phase = data[0] & 0x0F;
|
||||
|
||||
if version != FLP_VERSION || phase != PHASE_MSG2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let sender_idx = SessionIndex::from_le_bytes([data[1], data[2], data[3], data[4]]);
|
||||
let receiver_idx = SessionIndex::from_le_bytes([data[5], data[6], data[7], data[8]]);
|
||||
// flags must be zero during handshake
|
||||
if data[1] != 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let sender_idx = SessionIndex::from_le_bytes([data[4], data[5], data[6], data[7]]);
|
||||
let receiver_idx = SessionIndex::from_le_bytes([data[8], data[9], data[10], data[11]]);
|
||||
|
||||
Some(Self {
|
||||
sender_idx,
|
||||
receiver_idx,
|
||||
noise_msg2_offset: 9,
|
||||
noise_msg2_offset: COMMON_PREFIX_SIZE + 4 + 4, // 12
|
||||
})
|
||||
}
|
||||
|
||||
@@ -190,12 +310,16 @@ impl Msg2Header {
|
||||
|
||||
/// Build a wire-format msg1 packet.
|
||||
///
|
||||
/// Format: `[0x01][sender_idx:4 LE][noise_msg1:82]`
|
||||
/// Format: `[0x01][0x00][payload_len:2 LE][sender_idx:4 LE][noise_msg1:82]`
|
||||
pub fn build_msg1(sender_idx: SessionIndex, noise_msg1: &[u8]) -> Vec<u8> {
|
||||
debug_assert_eq!(noise_msg1.len(), HANDSHAKE_MSG1_SIZE);
|
||||
|
||||
let payload_len = (4 + noise_msg1.len()) as u16; // sender_idx + noise_msg1
|
||||
|
||||
let mut packet = Vec::with_capacity(MSG1_WIRE_SIZE);
|
||||
packet.push(DISCRIMINATOR_MSG1);
|
||||
packet.push(CommonPrefix::ver_phase_byte(FLP_VERSION, PHASE_MSG1));
|
||||
packet.push(0x00); // flags must be zero
|
||||
packet.extend_from_slice(&payload_len.to_le_bytes());
|
||||
packet.extend_from_slice(&sender_idx.to_le_bytes());
|
||||
packet.extend_from_slice(noise_msg1);
|
||||
packet
|
||||
@@ -203,30 +327,80 @@ pub fn build_msg1(sender_idx: SessionIndex, noise_msg1: &[u8]) -> Vec<u8> {
|
||||
|
||||
/// Build a wire-format msg2 packet.
|
||||
///
|
||||
/// Format: `[0x02][sender_idx:4 LE][receiver_idx:4 LE][noise_msg2:33]`
|
||||
/// Format: `[0x02][0x00][payload_len:2 LE][sender_idx:4 LE][receiver_idx:4 LE][noise_msg2:33]`
|
||||
pub fn build_msg2(sender_idx: SessionIndex, receiver_idx: SessionIndex, noise_msg2: &[u8]) -> Vec<u8> {
|
||||
debug_assert_eq!(noise_msg2.len(), HANDSHAKE_MSG2_SIZE);
|
||||
|
||||
let payload_len = (4 + 4 + noise_msg2.len()) as u16; // sender + receiver + noise
|
||||
|
||||
let mut packet = Vec::with_capacity(MSG2_WIRE_SIZE);
|
||||
packet.push(DISCRIMINATOR_MSG2);
|
||||
packet.push(CommonPrefix::ver_phase_byte(FLP_VERSION, PHASE_MSG2));
|
||||
packet.push(0x00); // flags must be zero
|
||||
packet.extend_from_slice(&payload_len.to_le_bytes());
|
||||
packet.extend_from_slice(&sender_idx.to_le_bytes());
|
||||
packet.extend_from_slice(&receiver_idx.to_le_bytes());
|
||||
packet.extend_from_slice(noise_msg2);
|
||||
packet
|
||||
}
|
||||
|
||||
/// Build the 16-byte outer header for an established frame.
|
||||
///
|
||||
/// Returns the header bytes (for use as AAD) separately from the construction.
|
||||
pub fn build_established_header(
|
||||
receiver_idx: SessionIndex,
|
||||
counter: u64,
|
||||
flags: u8,
|
||||
payload_len: u16,
|
||||
) -> [u8; ESTABLISHED_HEADER_SIZE] {
|
||||
let mut header = [0u8; ESTABLISHED_HEADER_SIZE];
|
||||
header[0] = CommonPrefix::ver_phase_byte(FLP_VERSION, PHASE_ESTABLISHED);
|
||||
header[1] = flags;
|
||||
header[2..4].copy_from_slice(&payload_len.to_le_bytes());
|
||||
header[4..8].copy_from_slice(&receiver_idx.to_le_bytes());
|
||||
header[8..16].copy_from_slice(&counter.to_le_bytes());
|
||||
header
|
||||
}
|
||||
|
||||
/// Build a wire-format encrypted frame.
|
||||
///
|
||||
/// Format: `[0x00][receiver_idx:4 LE][counter:8 LE][ciphertext+tag]`
|
||||
pub fn build_encrypted(receiver_idx: SessionIndex, counter: u64, ciphertext: &[u8]) -> Vec<u8> {
|
||||
let mut packet = Vec::with_capacity(13 + ciphertext.len());
|
||||
packet.push(DISCRIMINATOR_ENCRYPTED);
|
||||
packet.extend_from_slice(&receiver_idx.to_le_bytes());
|
||||
packet.extend_from_slice(&counter.to_le_bytes());
|
||||
/// Format: `[header:16][ciphertext+tag]`
|
||||
///
|
||||
/// The header is constructed from the parameters and used as AAD during
|
||||
/// encryption. The caller should use `build_established_header` to construct
|
||||
/// the header, encrypt with it as AAD, then call this to assemble the packet.
|
||||
pub fn build_encrypted(header: &[u8; ESTABLISHED_HEADER_SIZE], ciphertext: &[u8]) -> Vec<u8> {
|
||||
let mut packet = Vec::with_capacity(ESTABLISHED_HEADER_SIZE + ciphertext.len());
|
||||
packet.extend_from_slice(header);
|
||||
packet.extend_from_slice(ciphertext);
|
||||
packet
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Inner Header Helpers
|
||||
// ============================================================================
|
||||
|
||||
/// Prepend the 5-byte inner header (timestamp + msg_type) to a link message.
|
||||
///
|
||||
/// The caller provides the original plaintext starting with `[msg_type][payload...]`.
|
||||
/// This prepends `[timestamp:4 LE]` before the msg_type byte.
|
||||
pub fn prepend_inner_header(timestamp_ms: u32, plaintext: &[u8]) -> Vec<u8> {
|
||||
let mut buf = Vec::with_capacity(4 + plaintext.len());
|
||||
buf.extend_from_slice(×tamp_ms.to_le_bytes());
|
||||
buf.extend_from_slice(plaintext);
|
||||
buf
|
||||
}
|
||||
|
||||
/// Strip the 4-byte timestamp from a decrypted inner payload.
|
||||
///
|
||||
/// Returns `(timestamp, &payload_starting_at_msg_type)` or None if too short.
|
||||
pub fn strip_inner_header(plaintext: &[u8]) -> Option<(u32, &[u8])> {
|
||||
if plaintext.len() < INNER_HEADER_SIZE {
|
||||
return None;
|
||||
}
|
||||
let timestamp = u32::from_le_bytes([plaintext[0], plaintext[1], plaintext[2], plaintext[3]]);
|
||||
Some((timestamp, &plaintext[4..]))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tests
|
||||
// ============================================================================
|
||||
@@ -235,36 +409,61 @@ pub fn build_encrypted(receiver_idx: SessionIndex, counter: u64, ciphertext: &[u
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_common_prefix_parse() {
|
||||
let data = [0x00, 0x04, 0x20, 0x00]; // ver=0, phase=0, flags=SP, payload_len=32
|
||||
let prefix = CommonPrefix::parse(&data).unwrap();
|
||||
assert_eq!(prefix.version, 0);
|
||||
assert_eq!(prefix.phase, 0);
|
||||
assert_eq!(prefix.flags, FLAG_SP);
|
||||
assert_eq!(prefix.payload_len, 32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_common_prefix_too_short() {
|
||||
assert!(CommonPrefix::parse(&[0, 0, 0]).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypted_header_parse() {
|
||||
// Build a valid encrypted frame
|
||||
let receiver_idx = SessionIndex::new(0x12345678);
|
||||
let counter = 42u64;
|
||||
let ciphertext = vec![0xaa; 32]; // 16 plaintext + 16 tag
|
||||
let flags = 0u8;
|
||||
let payload_len = 32u16; // 16 plaintext + 16 tag
|
||||
let ciphertext = vec![0xaa; 48]; // payload_len + TAG_SIZE
|
||||
|
||||
let packet = build_encrypted(receiver_idx, counter, &ciphertext);
|
||||
let header = build_established_header(receiver_idx, counter, flags, payload_len);
|
||||
let packet = build_encrypted(&header, &ciphertext);
|
||||
|
||||
assert_eq!(packet.len(), 13 + 32);
|
||||
assert_eq!(packet[0], DISCRIMINATOR_ENCRYPTED);
|
||||
assert_eq!(packet.len(), ESTABLISHED_HEADER_SIZE + 48);
|
||||
assert_eq!(packet[0], 0x00); // ver=0, phase=0
|
||||
|
||||
// Parse it back
|
||||
let header = EncryptedHeader::parse(&packet).expect("should parse");
|
||||
assert_eq!(header.receiver_idx, receiver_idx);
|
||||
assert_eq!(header.counter, 42);
|
||||
assert_eq!(header.ciphertext_offset, 13);
|
||||
assert_eq!(header.ciphertext(&packet), &ciphertext[..]);
|
||||
let parsed = EncryptedHeader::parse(&packet).expect("should parse");
|
||||
assert_eq!(parsed.receiver_idx, receiver_idx);
|
||||
assert_eq!(parsed.counter, 42);
|
||||
assert_eq!(parsed.flags, 0);
|
||||
assert_eq!(parsed.payload_len, 32);
|
||||
assert_eq!(parsed.header_bytes, header);
|
||||
assert_eq!(parsed.ciphertext(&packet), &ciphertext[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypted_header_too_short() {
|
||||
let packet = vec![0x00; 28]; // One byte too short
|
||||
let packet = vec![0x00; ENCRYPTED_MIN_SIZE - 1];
|
||||
assert!(EncryptedHeader::parse(&packet).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypted_header_wrong_discriminator() {
|
||||
let mut packet = vec![0x00; 30];
|
||||
packet[0] = 0x01; // Wrong discriminator
|
||||
fn test_encrypted_header_wrong_phase() {
|
||||
let mut packet = vec![0x00; ENCRYPTED_MIN_SIZE];
|
||||
packet[0] = 0x01; // phase 1 (msg1), not established
|
||||
assert!(EncryptedHeader::parse(&packet).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypted_header_wrong_version() {
|
||||
let mut packet = vec![0x00; ENCRYPTED_MIN_SIZE];
|
||||
packet[0] = 0x10; // version 1, phase 0
|
||||
assert!(EncryptedHeader::parse(&packet).is_none());
|
||||
}
|
||||
|
||||
@@ -276,27 +475,34 @@ mod tests {
|
||||
let packet = build_msg1(sender_idx, &noise_msg1);
|
||||
|
||||
assert_eq!(packet.len(), MSG1_WIRE_SIZE);
|
||||
assert_eq!(packet[0], DISCRIMINATOR_MSG1);
|
||||
assert_eq!(packet[0], 0x01); // ver=0, phase=1
|
||||
|
||||
let header = Msg1Header::parse(&packet).expect("should parse");
|
||||
assert_eq!(header.sender_idx, sender_idx);
|
||||
assert_eq!(header.noise_msg1_offset, 5);
|
||||
assert_eq!(header.noise_msg1_offset, 8);
|
||||
assert_eq!(header.noise_msg1(&packet), &noise_msg1[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_msg1_header_wrong_size() {
|
||||
let packet = vec![0x01; 86]; // One byte too short
|
||||
let packet = vec![0x01; MSG1_WIRE_SIZE - 1];
|
||||
assert!(Msg1Header::parse(&packet).is_none());
|
||||
|
||||
let packet = vec![0x01; 88]; // One byte too long
|
||||
let packet = vec![0x01; MSG1_WIRE_SIZE + 1];
|
||||
assert!(Msg1Header::parse(&packet).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_msg1_header_wrong_discriminator() {
|
||||
fn test_msg1_header_wrong_phase() {
|
||||
let mut packet = vec![0x00; MSG1_WIRE_SIZE];
|
||||
packet[0] = 0x02; // Wrong discriminator
|
||||
packet[0] = 0x02; // phase 2, not phase 1
|
||||
assert!(Msg1Header::parse(&packet).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_msg1_header_nonzero_flags() {
|
||||
let mut packet = build_msg1(SessionIndex::new(1), &[0u8; HANDSHAKE_MSG1_SIZE]);
|
||||
packet[1] = 0x01; // flags must be zero during handshake
|
||||
assert!(Msg1Header::parse(&packet).is_none());
|
||||
}
|
||||
|
||||
@@ -309,52 +515,114 @@ mod tests {
|
||||
let packet = build_msg2(sender_idx, receiver_idx, &noise_msg2);
|
||||
|
||||
assert_eq!(packet.len(), MSG2_WIRE_SIZE);
|
||||
assert_eq!(packet[0], DISCRIMINATOR_MSG2);
|
||||
assert_eq!(packet[0], 0x02); // ver=0, phase=2
|
||||
|
||||
let header = Msg2Header::parse(&packet).expect("should parse");
|
||||
assert_eq!(header.sender_idx, sender_idx);
|
||||
assert_eq!(header.receiver_idx, receiver_idx);
|
||||
assert_eq!(header.noise_msg2_offset, 9);
|
||||
assert_eq!(header.noise_msg2_offset, 12);
|
||||
assert_eq!(header.noise_msg2(&packet), &noise_msg2[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_msg2_header_wrong_size() {
|
||||
let packet = vec![0x02; 41]; // One byte too short
|
||||
let packet = vec![0x02; MSG2_WIRE_SIZE - 1];
|
||||
assert!(Msg2Header::parse(&packet).is_none());
|
||||
|
||||
let packet = vec![0x02; 43]; // One byte too long
|
||||
let packet = vec![0x02; MSG2_WIRE_SIZE + 1];
|
||||
assert!(Msg2Header::parse(&packet).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_msg2_header_wrong_discriminator() {
|
||||
fn test_msg2_header_wrong_phase() {
|
||||
let mut packet = vec![0x00; MSG2_WIRE_SIZE];
|
||||
packet[0] = 0x00; // Wrong discriminator
|
||||
packet[0] = 0x00; // phase 0, not phase 2
|
||||
assert!(Msg2Header::parse(&packet).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wire_sizes() {
|
||||
// Verify constants match spec
|
||||
assert_eq!(MSG1_WIRE_SIZE, 87); // 1 + 4 + 82
|
||||
assert_eq!(MSG2_WIRE_SIZE, 42); // 1 + 4 + 4 + 33
|
||||
assert_eq!(ENCRYPTED_MIN_SIZE, 29); // 1 + 4 + 8 + 16
|
||||
assert_eq!(MSG1_WIRE_SIZE, 90); // 4 + 4 + 82
|
||||
assert_eq!(MSG2_WIRE_SIZE, 45); // 4 + 4 + 4 + 33
|
||||
assert_eq!(ENCRYPTED_MIN_SIZE, 32); // 16 + 16
|
||||
assert_eq!(COMMON_PREFIX_SIZE, 4);
|
||||
assert_eq!(ESTABLISHED_HEADER_SIZE, 16);
|
||||
assert_eq!(INNER_HEADER_SIZE, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip_indices() {
|
||||
// Test that indices survive the roundtrip correctly (endianness)
|
||||
let idx = SessionIndex::new(0xDEADBEEF);
|
||||
|
||||
let msg1 = build_msg1(idx, &[0u8; HANDSHAKE_MSG1_SIZE]);
|
||||
let parsed = Msg1Header::parse(&msg1).unwrap();
|
||||
assert_eq!(parsed.sender_idx.as_u32(), 0xDEADBEEF);
|
||||
|
||||
// Verify little-endian encoding
|
||||
assert_eq!(msg1[1], 0xEF);
|
||||
assert_eq!(msg1[2], 0xBE);
|
||||
assert_eq!(msg1[3], 0xAD);
|
||||
assert_eq!(msg1[4], 0xDE);
|
||||
// Verify little-endian encoding (sender_idx starts at offset 4)
|
||||
assert_eq!(msg1[4], 0xEF);
|
||||
assert_eq!(msg1[5], 0xBE);
|
||||
assert_eq!(msg1[6], 0xAD);
|
||||
assert_eq!(msg1[7], 0xDE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inner_header_prepend_strip() {
|
||||
let timestamp: u32 = 12345;
|
||||
let original = vec![0x10, 0xAA, 0xBB]; // msg_type + payload
|
||||
|
||||
let with_header = prepend_inner_header(timestamp, &original);
|
||||
assert_eq!(with_header.len(), 4 + 3); // timestamp + original
|
||||
|
||||
let (ts, rest) = strip_inner_header(&with_header).unwrap();
|
||||
assert_eq!(ts, 12345);
|
||||
assert_eq!(rest, &original[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inner_header_too_short() {
|
||||
assert!(strip_inner_header(&[0, 0, 0, 0]).is_none()); // needs 5 bytes minimum
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_flags_byte() {
|
||||
let header = build_established_header(
|
||||
SessionIndex::new(1),
|
||||
0,
|
||||
FLAG_KEY_EPOCH | FLAG_SP,
|
||||
100,
|
||||
);
|
||||
assert_eq!(header[1], 0x05); // bits 0 and 2 set
|
||||
|
||||
let parsed = EncryptedHeader::parse(&[
|
||||
header[0], header[1], header[2], header[3],
|
||||
header[4], header[5], header[6], header[7],
|
||||
header[8], header[9], header[10], header[11],
|
||||
header[12], header[13], header[14], header[15],
|
||||
// minimum: TAG_SIZE bytes of ciphertext
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
]).unwrap();
|
||||
assert_eq!(parsed.flags & FLAG_KEY_EPOCH, FLAG_KEY_EPOCH);
|
||||
assert_eq!(parsed.flags & FLAG_CE, 0);
|
||||
assert_eq!(parsed.flags & FLAG_SP, FLAG_SP);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_payload_len_in_msg1() {
|
||||
let packet = build_msg1(SessionIndex::new(1), &[0u8; HANDSHAKE_MSG1_SIZE]);
|
||||
let prefix = CommonPrefix::parse(&packet).unwrap();
|
||||
// payload_len = sender_idx(4) + noise_msg1(82) = 86
|
||||
assert_eq!(prefix.payload_len, 86);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_payload_len_in_msg2() {
|
||||
let packet = build_msg2(
|
||||
SessionIndex::new(1),
|
||||
SessionIndex::new(2),
|
||||
&[0u8; HANDSHAKE_MSG2_SIZE],
|
||||
);
|
||||
let prefix = CommonPrefix::parse(&packet).unwrap();
|
||||
// payload_len = sender_idx(4) + receiver_idx(4) + noise_msg2(33) = 41
|
||||
assert_eq!(prefix.payload_len, 41);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user