mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-30 19:46:15 +00:00
Merge FMP wire + PromotionResult relocations into next
Forward-merge the master-line FMP-establish relocations onto next: the FMP link wire codec into proto/fmp/wire.rs and PromotionResult into proto/fmp/core.rs. The wire move was hand-resolved against next's wire format (msg3/Msg3Header/build_msg3, FMP_VERSION=1, no spin-bit flag), so next's behavior is unchanged. Behavior-neutral; full lib suite green at next baseline.
This commit is contained in:
+2
-3
@@ -91,13 +91,12 @@ pub use proto::fmp::{NegotiationPayload, NodeProfile, TlvEntry};
|
||||
// Re-export cache types
|
||||
pub use cache::{CacheEntry, CacheError, CacheStats, CoordCache};
|
||||
|
||||
// Re-export FMP tie-break helper (relocated from peer:: to proto::fmp)
|
||||
pub use proto::fmp::cross_connection_winner;
|
||||
// Re-export FMP tie-break helper and promotion result (relocated from peer:: to proto::fmp)
|
||||
pub use proto::fmp::{PromotionResult, cross_connection_winner};
|
||||
|
||||
// Re-export peer types
|
||||
pub use peer::{
|
||||
ActivePeer, ConnectivityState, HandshakeState, PeerConnection, PeerError, PeerSlot,
|
||||
PromotionResult,
|
||||
};
|
||||
|
||||
// Re-export node types
|
||||
|
||||
@@ -560,7 +560,7 @@ mod tests {
|
||||
let open_cipher = LessSafeKey::new(unbound2);
|
||||
|
||||
let counter: u64 = 7;
|
||||
const HDR: usize = crate::node::wire::ESTABLISHED_HEADER_SIZE;
|
||||
const HDR: usize = crate::proto::fmp::wire::ESTABLISHED_HEADER_SIZE;
|
||||
// Build a wire packet `[16-byte header][4-byte inner ts][1 byte link msg]`
|
||||
// with capacity for the trailing AEAD tag. Header bytes
|
||||
// double as AAD and as the on-wire prefix.
|
||||
@@ -568,7 +568,7 @@ mod tests {
|
||||
// Header: fill the flags byte (the second byte) with both
|
||||
// FLAG_CE and FLAG_KEY_EPOCH set; the rest is uninterpreted by
|
||||
// the worker (it just AADs the whole 16 bytes).
|
||||
let flags_byte = crate::node::wire::FLAG_CE | crate::node::wire::FLAG_KEY_EPOCH;
|
||||
let flags_byte = crate::proto::fmp::wire::FLAG_CE | crate::proto::fmp::wire::FLAG_KEY_EPOCH;
|
||||
let mut header = [0u8; HDR];
|
||||
header[1] = flags_byte;
|
||||
wire.extend_from_slice(&header);
|
||||
@@ -625,11 +625,11 @@ mod tests {
|
||||
"fmp_flags must round-trip from DecryptJob to DecryptFallback"
|
||||
);
|
||||
assert!(
|
||||
fallback.fmp_flags & crate::node::wire::FLAG_CE != 0,
|
||||
fallback.fmp_flags & crate::proto::fmp::wire::FLAG_CE != 0,
|
||||
"FLAG_CE bit lost on worker path"
|
||||
);
|
||||
assert!(
|
||||
fallback.fmp_flags & crate::node::wire::FLAG_KEY_EPOCH != 0,
|
||||
fallback.fmp_flags & crate::proto::fmp::wire::FLAG_KEY_EPOCH != 0,
|
||||
"FLAG_KEY_EPOCH bit lost on worker path"
|
||||
);
|
||||
}
|
||||
@@ -723,7 +723,7 @@ mod tests {
|
||||
let open_cipher = LessSafeKey::new(unbound);
|
||||
|
||||
let counter: u64 = 11;
|
||||
const HDR: usize = crate::node::wire::ESTABLISHED_HEADER_SIZE;
|
||||
const HDR: usize = crate::proto::fmp::wire::ESTABLISHED_HEADER_SIZE;
|
||||
let header = [0u8; HDR];
|
||||
let mut wire = Vec::with_capacity(HDR + 4 + 1 + 16);
|
||||
wire.extend_from_slice(&header);
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
// warnings rather than gate every function individually.
|
||||
#![cfg_attr(not(unix), allow(dead_code))]
|
||||
|
||||
use crate::node::wire::ESTABLISHED_HEADER_SIZE;
|
||||
use crate::proto::fmp::wire::ESTABLISHED_HEADER_SIZE;
|
||||
use crate::proto::fsp::wire::FSP_HEADER_SIZE;
|
||||
use crate::transport::udp::socket::AsyncUdpSocket;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
@@ -1904,8 +1904,8 @@ mod unix_tests {
|
||||
#[test]
|
||||
fn pipelined_send_wire_layout_roundtrips_canonical_decoders() {
|
||||
use crate::NodeAddr;
|
||||
use crate::node::wire::{EncryptedHeader, FLAG_KEY_EPOCH, build_established_header};
|
||||
use crate::noise::TAG_SIZE;
|
||||
use crate::proto::fmp::wire::{EncryptedHeader, FLAG_KEY_EPOCH, build_established_header};
|
||||
use crate::proto::fsp::wire::build_fsp_header;
|
||||
use crate::proto::link::{
|
||||
LinkMessageType, SESSION_DATAGRAM_HEADER_SIZE, SessionDatagramRef,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Encrypted frame handling (hot path).
|
||||
|
||||
use crate::node::Node;
|
||||
use crate::node::wire::{EncryptedHeader, FLAG_CE, FLAG_KEY_EPOCH, strip_inner_header};
|
||||
use crate::noise::NoiseError;
|
||||
use crate::proto::fmp::wire::{EncryptedHeader, FLAG_CE, FLAG_KEY_EPOCH, strip_inner_header};
|
||||
use crate::transport::ReceivedPacket;
|
||||
use tracing::{debug, trace, warn};
|
||||
|
||||
|
||||
@@ -8,12 +8,13 @@
|
||||
use crate::PeerIdentity;
|
||||
use crate::node::acl::PeerAclContext;
|
||||
use crate::node::reject::{HandshakeReject, RejectReason};
|
||||
use crate::node::wire::{Msg1Header, Msg2Header, Msg3Header, build_msg2, build_msg3};
|
||||
use crate::node::{Node, NodeError};
|
||||
use crate::peer::{ActivePeer, PeerConnection, PromotionResult};
|
||||
use crate::peer::{ActivePeer, PeerConnection};
|
||||
use crate::proto::fmp::wire::{Msg1Header, Msg2Header, Msg3Header, build_msg2, build_msg3};
|
||||
use crate::proto::fmp::{
|
||||
Disconnect, DisconnectReason, EstablishSnapshot, InboundDecision, InboundReject,
|
||||
NegotiationPayload, WireOutcome, cross_connection_winner, decide_fmp_negotiation,
|
||||
NegotiationPayload, PromotionResult, WireOutcome, cross_connection_winner,
|
||||
decide_fmp_negotiation,
|
||||
};
|
||||
use crate::transport::{Link, LinkDirection, LinkId, ReceivedPacket};
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
use crate::NodeAddr;
|
||||
use crate::node::Node;
|
||||
use crate::node::reject::{HandshakeReject, RejectReason};
|
||||
use crate::node::wire::build_msg1;
|
||||
use crate::noise::HandshakeState;
|
||||
use crate::proto::fmp::wire::build_msg1;
|
||||
use crate::proto::fmp::{ConnAction, LifecycleView, PeerSnapshot, RekeyCfg, RekeyResendSnapshot};
|
||||
use crate::proto::fsp::{
|
||||
FspAction, RekeyMsg3ResendSnapshot, SessionSetup, SessionSnapshot, cutover_timer_elapsed,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
//! RX event loop and packet dispatch.
|
||||
|
||||
use crate::control::{ControlSocket, commands};
|
||||
use crate::node::wire::{
|
||||
use crate::node::{Node, NodeError};
|
||||
use crate::proto::fmp::wire::{
|
||||
COMMON_PREFIX_SIZE, CommonPrefix, FMP_VERSION, PHASE_ESTABLISHED, PHASE_MSG1, PHASE_MSG2,
|
||||
PHASE_MSG3,
|
||||
};
|
||||
use crate::node::{Node, NodeError};
|
||||
use crate::transport::ReceivedPacket;
|
||||
use std::time::Duration;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
@@ -9,11 +9,11 @@ use crate::NodeAddr;
|
||||
use crate::node::handlers::mmp::format_throughput;
|
||||
use crate::node::reject::{RejectReason, SessionReject};
|
||||
use crate::node::session::{EndToEndState, EpochSlot, SessionEntry};
|
||||
#[cfg(unix)]
|
||||
use crate::node::wire::{ESTABLISHED_HEADER_SIZE, FLAG_KEY_EPOCH, build_established_header};
|
||||
use crate::node::{Node, NodeError};
|
||||
use crate::noise::{HANDSHAKE_MSG1_SIZE, HANDSHAKE_MSG2_SIZE, HANDSHAKE_MSG3_SIZE, HandshakeState};
|
||||
use crate::proto::fmp::NegotiationPayload;
|
||||
#[cfg(unix)]
|
||||
use crate::proto::fmp::wire::{ESTABLISHED_HEADER_SIZE, FLAG_KEY_EPOCH, build_established_header};
|
||||
use crate::proto::fsp::wire::{
|
||||
FSP_COMMON_PREFIX_SIZE, FSP_FLAG_CP, FSP_FLAG_K, FSP_HEADER_SIZE, FSP_PHASE_ESTABLISHED,
|
||||
FSP_PHASE_MSG1, FSP_PHASE_MSG2, FSP_PHASE_MSG3, FSP_PORT_HEADER_SIZE, FSP_PORT_IPV6_SHIM,
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
use super::{Node, NodeError, NodeState};
|
||||
use crate::config::{ConnectPolicy, PeerAddress, PeerConfig};
|
||||
use crate::node::acl::PeerAclContext;
|
||||
use crate::node::wire::build_msg1;
|
||||
use crate::nostr::{BootstrapEvent, NostrRendezvous};
|
||||
use crate::nostr::{BootstrapHandoffResult, EstablishedTraversal};
|
||||
use crate::peer::PeerConnection;
|
||||
use crate::proto::fmp::wire::build_msg1;
|
||||
use crate::proto::fmp::{Disconnect, DisconnectReason};
|
||||
use crate::transport::{Link, LinkDirection, LinkId, TransportAddr, TransportId, packet_channel};
|
||||
use crate::upper::tun::{TunDevice, TunState, run_tun_reader, shutdown_tun_interface};
|
||||
|
||||
+4
-5
@@ -24,7 +24,6 @@ pub(crate) mod stats_history;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
mod tree;
|
||||
pub(crate) mod wire;
|
||||
|
||||
use self::rate_limit::HandshakeRateLimiter;
|
||||
use self::reloadable::Reloadable;
|
||||
@@ -48,16 +47,16 @@ use self::reloadable::Reloadable;
|
||||
/// bounding that cross-connection branch below the rekey age floor so a
|
||||
/// rekey-aged msg3 falls through to the rekey-responder path. See CHANGELOG.
|
||||
pub(crate) const REKEY_JITTER_SECS: i64 = 15;
|
||||
use self::wire::{
|
||||
ESTABLISHED_HEADER_SIZE, FLAG_CE, FLAG_KEY_EPOCH, build_encrypted, build_established_header,
|
||||
prepend_inner_header,
|
||||
};
|
||||
use crate::cache::CoordCache;
|
||||
use crate::node::session::SessionEntry;
|
||||
use crate::peer::{ActivePeer, PeerConnection};
|
||||
use crate::proto::bloom::{BloomFilter, BloomState};
|
||||
use crate::proto::fmp::Fmp;
|
||||
use crate::proto::fmp::NodeProfile;
|
||||
use crate::proto::fmp::wire::{
|
||||
ESTABLISHED_HEADER_SIZE, FLAG_CE, FLAG_KEY_EPOCH, build_encrypted, build_established_header,
|
||||
prepend_inner_header,
|
||||
};
|
||||
use crate::proto::fsp::Fsp;
|
||||
use crate::proto::lookup::{Lookup, LookupBackoff, LookupForwardRateLimiter};
|
||||
use crate::proto::mmp::Mmp;
|
||||
|
||||
@@ -2,7 +2,7 @@ use super::*;
|
||||
use crate::ReceivedPacket;
|
||||
use crate::node::acl::PeerAclReloader;
|
||||
use crate::node::reloadable::HostMapReloadable;
|
||||
use crate::node::wire::build_msg2;
|
||||
use crate::proto::fmp::wire::build_msg2;
|
||||
use crate::upper::hosts::HostMap;
|
||||
use crate::utils::index::SessionIndex;
|
||||
use std::path::PathBuf;
|
||||
@@ -123,7 +123,7 @@ async fn test_outbound_msg2_denied_after_acl_reload() {
|
||||
async fn test_inbound_msg3_denied_triggers_disconnect() {
|
||||
use crate::config::UdpConfig;
|
||||
use crate::node::acl::PeerAclReloader;
|
||||
use crate::node::wire::build_msg1;
|
||||
use crate::proto::fmp::wire::build_msg1;
|
||||
use crate::transport::udp::UdpTransport;
|
||||
use crate::transport::{TransportHandle, packet_channel};
|
||||
use tokio::time::{Duration, timeout};
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use super::*;
|
||||
use crate::EstablishedTraversal;
|
||||
use crate::config::{TransportInstances, UdpConfig};
|
||||
use crate::node::wire::{PHASE_MSG1, PHASE_MSG2, PHASE_MSG3};
|
||||
use crate::proto::fmp::wire::{PHASE_MSG1, PHASE_MSG2, PHASE_MSG3};
|
||||
use crate::transport::udp::UdpTransport;
|
||||
use crate::utils::index::IndexAllocator;
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -8,7 +8,7 @@ use super::*;
|
||||
#[tokio::test]
|
||||
async fn test_two_node_handshake_udp() {
|
||||
use crate::config::UdpConfig;
|
||||
use crate::node::wire::{
|
||||
use crate::proto::fmp::wire::{
|
||||
build_encrypted, build_established_header, build_msg1, prepend_inner_header,
|
||||
};
|
||||
use crate::transport::udp::UdpTransport;
|
||||
@@ -268,7 +268,7 @@ async fn test_two_node_handshake_udp() {
|
||||
#[tokio::test]
|
||||
async fn test_run_rx_loop_handshake() {
|
||||
use crate::config::UdpConfig;
|
||||
use crate::node::wire::build_msg1;
|
||||
use crate::proto::fmp::wire::build_msg1;
|
||||
use crate::transport::udp::UdpTransport;
|
||||
use tokio::time::Duration;
|
||||
|
||||
@@ -456,7 +456,7 @@ async fn test_run_rx_loop_handshake() {
|
||||
#[tokio::test]
|
||||
async fn test_cross_connection_both_initiate() {
|
||||
use crate::config::UdpConfig;
|
||||
use crate::node::wire::build_msg1;
|
||||
use crate::proto::fmp::wire::build_msg1;
|
||||
use crate::transport::udp::UdpTransport;
|
||||
use tokio::time::{Duration, timeout};
|
||||
|
||||
@@ -829,7 +829,7 @@ async fn test_failed_connection_cleanup() {
|
||||
/// Test that msg1 bytes are stored on connection for resend.
|
||||
#[tokio::test]
|
||||
async fn test_msg1_stored_for_resend() {
|
||||
use crate::node::wire::build_msg1;
|
||||
use crate::proto::fmp::wire::build_msg1;
|
||||
|
||||
let mut node = make_node();
|
||||
let transport_id = TransportId::new(1);
|
||||
@@ -887,7 +887,7 @@ async fn test_resend_scheduling() {
|
||||
conn.set_source_addr(remote_addr.clone());
|
||||
|
||||
// Store msg1 with first resend at now + 1000ms
|
||||
let wire_msg1 = crate::node::wire::build_msg1(our_index, &noise_msg1);
|
||||
let wire_msg1 = crate::proto::fmp::wire::build_msg1(our_index, &noise_msg1);
|
||||
conn.set_handshake_msg1(wire_msg1, now_ms + 1000);
|
||||
|
||||
let link = Link::connectionless(
|
||||
@@ -965,7 +965,7 @@ fn test_resend_count_tracking() {
|
||||
/// Test that duplicate msg2 is silently dropped when pending_outbound is already cleared.
|
||||
#[tokio::test]
|
||||
async fn test_duplicate_msg2_dropped() {
|
||||
use crate::node::wire::build_msg2;
|
||||
use crate::proto::fmp::wire::build_msg2;
|
||||
use crate::transport::ReceivedPacket;
|
||||
|
||||
let mut node = make_node();
|
||||
@@ -1066,7 +1066,7 @@ async fn test_full_leaf_accepted() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_xx_duplicate_msg1_resends_msg2() {
|
||||
use crate::node::wire::build_msg1;
|
||||
use crate::proto::fmp::wire::build_msg1;
|
||||
use crate::transport::ReceivedPacket;
|
||||
|
||||
// Node B with NO transport — msg2 send silently skips (if let Some check),
|
||||
|
||||
@@ -999,7 +999,7 @@ fn build_ipv6_packet(
|
||||
|
||||
#[test]
|
||||
fn test_identity_cache_populated_on_promote() {
|
||||
use crate::peer::PromotionResult;
|
||||
use crate::proto::fmp::PromotionResult;
|
||||
|
||||
let mut node = make_node();
|
||||
let transport_id = TransportId::new(1);
|
||||
|
||||
@@ -126,7 +126,7 @@ async fn make_test_node_inner(config: Config, mtu: u16) -> TestNode {
|
||||
/// Sends msg1 over UDP. The drain loop will handle msg1 processing,
|
||||
/// msg2 response, and subsequent TreeAnnounce exchange.
|
||||
pub(super) async fn initiate_handshake(nodes: &mut [TestNode], i: usize, j: usize) {
|
||||
use crate::node::wire::build_msg1;
|
||||
use crate::proto::fmp::wire::build_msg1;
|
||||
|
||||
// Extract responder info before mutably borrowing initiator
|
||||
let responder_addr = nodes[j].addr.clone();
|
||||
@@ -288,7 +288,7 @@ pub(super) fn print_tree_snapshot(label: &str, nodes: &[TestNode]) {
|
||||
///
|
||||
/// Returns the number of packets processed.
|
||||
pub(super) async fn process_available_packets(nodes: &mut [TestNode]) -> usize {
|
||||
use crate::node::wire::{
|
||||
use crate::proto::fmp::wire::{
|
||||
COMMON_PREFIX_SIZE, CommonPrefix, FMP_VERSION, PHASE_ESTABLISHED, PHASE_MSG1, PHASE_MSG2,
|
||||
PHASE_MSG3,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::*;
|
||||
use crate::nostr::{BootstrapEvent, NostrRendezvous};
|
||||
use crate::peer::PromotionResult;
|
||||
use crate::proto::fmp::PromotionResult;
|
||||
use crate::transport::udp::UdpTransport;
|
||||
use crate::transport::{TransportHandle, packet_channel};
|
||||
use std::sync::Arc;
|
||||
@@ -1799,7 +1799,7 @@ async fn drive_xx_handshake(
|
||||
packet_rx_a: &mut crate::transport::PacketRx,
|
||||
packet_rx_b: &mut crate::transport::PacketRx,
|
||||
) -> NodeAddr {
|
||||
use crate::node::wire::build_msg1;
|
||||
use crate::proto::fmp::wire::build_msg1;
|
||||
use tokio::time::{Duration, timeout};
|
||||
|
||||
let remote_addr_b = TransportAddr::from_string(&addr_b.to_string());
|
||||
|
||||
@@ -1,797 +0,0 @@
|
||||
//! Wire Format Parsing and Serialization
|
||||
//!
|
||||
//! Defines the FIPS mesh-layer wire format (FMP) 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
|
||||
//!
|
||||
//! | Phase | Type | Size | Description |
|
||||
//! |-------|-----------------|------------|--------------------------------|
|
||||
//! | 0x0 | Encrypted frame | 32+ bytes | Post-handshake encrypted data |
|
||||
//! | 0x1 | Noise XX msg1 | 41 bytes | Handshake initiation |
|
||||
//! | 0x2 | Noise XX msg2 | 118+ bytes | Handshake response |
|
||||
//! | 0x3 | Noise XX msg3 | 85+ bytes | Handshake completion |
|
||||
|
||||
use crate::noise::{HANDSHAKE_MSG1_SIZE, HANDSHAKE_MSG2_SIZE, HANDSHAKE_MSG3_SIZE, TAG_SIZE};
|
||||
use crate::utils::index::SessionIndex;
|
||||
|
||||
// ============================================================================
|
||||
// Constants
|
||||
// ============================================================================
|
||||
|
||||
/// FMP protocol version (4 high bits of byte 0).
|
||||
pub const FMP_VERSION: u8 = 1;
|
||||
|
||||
/// Phase value for established (encrypted) frames.
|
||||
pub const PHASE_ESTABLISHED: u8 = 0x0;
|
||||
|
||||
/// Phase value for handshake message 1 (initiation).
|
||||
pub const PHASE_MSG1: u8 = 0x1;
|
||||
|
||||
/// Phase value for handshake message 2 (response).
|
||||
pub const PHASE_MSG2: u8 = 0x2;
|
||||
|
||||
/// Phase value for handshake message 3 (completion, XX only).
|
||||
pub const PHASE_MSG3: u8 = 0x3;
|
||||
|
||||
/// Size of the common packet prefix (all packet types).
|
||||
pub const COMMON_PREFIX_SIZE: usize = 4;
|
||||
|
||||
/// Size of the full established frame header (prefix + receiver_idx + counter).
|
||||
pub const ESTABLISHED_HEADER_SIZE: usize = 16;
|
||||
|
||||
/// Size of handshake msg1 wire packet: prefix + sender_idx + noise_msg1.
|
||||
pub const MSG1_WIRE_SIZE: usize = COMMON_PREFIX_SIZE + 4 + HANDSHAKE_MSG1_SIZE; // 41 bytes
|
||||
|
||||
/// Minimum size of handshake msg2 wire packet: prefix + sender_idx + receiver_idx + noise_msg2.
|
||||
/// Actual size may be larger due to optional negotiation payload.
|
||||
pub const MSG2_WIRE_SIZE: usize = COMMON_PREFIX_SIZE + 4 + 4 + HANDSHAKE_MSG2_SIZE; // 118 bytes
|
||||
|
||||
/// Minimum size of handshake msg3 wire packet: prefix + sender_idx + receiver_idx + noise_msg3.
|
||||
/// Actual size may be larger due to optional negotiation payload.
|
||||
pub const MSG3_WIRE_SIZE: usize = COMMON_PREFIX_SIZE + 4 + 4 + HANDSHAKE_MSG3_SIZE; // 85 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;
|
||||
|
||||
// ============================================================================
|
||||
// Common Prefix
|
||||
// ============================================================================
|
||||
|
||||
/// Parsed common packet prefix (first 4 bytes of every FMP 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 established frame header (phase 0x0).
|
||||
///
|
||||
/// Wire format (16 bytes):
|
||||
/// ```text
|
||||
/// [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,
|
||||
/// Raw 16-byte header for use as AEAD AAD.
|
||||
pub header_bytes: [u8; ESTABLISHED_HEADER_SIZE],
|
||||
}
|
||||
|
||||
impl EncryptedHeader {
|
||||
/// Parse an established frame header from packet data.
|
||||
///
|
||||
/// 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;
|
||||
}
|
||||
|
||||
let version = data[0] >> 4;
|
||||
let phase = data[0] & 0x0F;
|
||||
|
||||
if version != FMP_VERSION || phase != PHASE_ESTABLISHED {
|
||||
return None;
|
||||
}
|
||||
|
||||
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[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,
|
||||
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[ESTABLISHED_HEADER_SIZE..]
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Msg1 Header
|
||||
// ============================================================================
|
||||
|
||||
/// Parsed handshake message 1 header (phase 0x1).
|
||||
///
|
||||
/// Wire format (41 bytes, Noise XX):
|
||||
/// ```text
|
||||
/// [0x11][0x00][payload_len:2 LE][sender_idx:4 LE][noise_msg1:33]
|
||||
/// ```
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Msg1Header {
|
||||
/// Session index chosen by the sender (becomes receiver_idx for responses).
|
||||
pub sender_idx: SessionIndex,
|
||||
/// Offset where Noise msg1 payload begins.
|
||||
pub noise_msg1_offset: usize,
|
||||
}
|
||||
|
||||
impl Msg1Header {
|
||||
/// Parse a msg1 header from packet data.
|
||||
///
|
||||
/// 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;
|
||||
}
|
||||
|
||||
let version = data[0] >> 4;
|
||||
let phase = data[0] & 0x0F;
|
||||
|
||||
if version != FMP_VERSION || phase != PHASE_MSG1 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// 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: COMMON_PREFIX_SIZE + 4, // 8
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the Noise msg1 payload from the original packet.
|
||||
#[cfg(test)]
|
||||
pub fn noise_msg1<'a>(&self, data: &'a [u8]) -> &'a [u8] {
|
||||
&data[self.noise_msg1_offset..]
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Msg2 Header
|
||||
// ============================================================================
|
||||
|
||||
/// Parsed handshake message 2 header (phase 0x2).
|
||||
///
|
||||
/// Wire format (118+ bytes, Noise XX):
|
||||
/// ```text
|
||||
/// [0x12][0x00][payload_len:2 LE][sender_idx:4 LE][receiver_idx:4 LE][noise_msg2:106+]
|
||||
/// ```
|
||||
/// Size is variable due to optional negotiation payload appended after base XX msg2.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Msg2Header {
|
||||
/// Session index chosen by the responder.
|
||||
pub sender_idx: SessionIndex,
|
||||
/// Echo of the initiator's sender_idx from msg1.
|
||||
pub receiver_idx: SessionIndex,
|
||||
/// Offset where Noise msg2 payload begins.
|
||||
pub noise_msg2_offset: usize,
|
||||
}
|
||||
|
||||
impl Msg2Header {
|
||||
/// Parse a msg2 header from packet data.
|
||||
///
|
||||
/// Returns None if the packet is too short or has wrong version/phase.
|
||||
/// Accepts variable size (base + optional negotiation payload).
|
||||
pub fn parse(data: &[u8]) -> Option<Self> {
|
||||
if data.len() < MSG2_WIRE_SIZE {
|
||||
return None;
|
||||
}
|
||||
|
||||
let version = data[0] >> 4;
|
||||
let phase = data[0] & 0x0F;
|
||||
|
||||
if version != FMP_VERSION || phase != PHASE_MSG2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// 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: COMMON_PREFIX_SIZE + 4 + 4, // 12
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the Noise msg2 payload from the original packet (variable length).
|
||||
#[cfg(test)]
|
||||
pub fn noise_msg2<'a>(&self, data: &'a [u8]) -> &'a [u8] {
|
||||
&data[self.noise_msg2_offset..]
|
||||
}
|
||||
|
||||
/// Get the total noise payload length (base + optional negotiation).
|
||||
#[allow(dead_code)]
|
||||
pub fn noise_payload_len(&self, data: &[u8]) -> usize {
|
||||
data.len() - self.noise_msg2_offset
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Msg3 Header
|
||||
// ============================================================================
|
||||
|
||||
/// Parsed handshake message 3 header (phase 0x3, XX pattern).
|
||||
///
|
||||
/// Wire format (85+ bytes, Noise XX):
|
||||
/// ```text
|
||||
/// [0x13][0x00][payload_len:2 LE][sender_idx:4 LE][receiver_idx:4 LE][noise_msg3:73+]
|
||||
/// ```
|
||||
/// Size is variable due to optional negotiation payload appended after base XX msg3.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Msg3Header {
|
||||
/// Session index chosen by the initiator (echo of msg1 sender_idx).
|
||||
pub sender_idx: SessionIndex,
|
||||
/// Echo of the responder's sender_idx from msg2.
|
||||
pub receiver_idx: SessionIndex,
|
||||
/// Offset where Noise msg3 payload begins.
|
||||
pub noise_msg3_offset: usize,
|
||||
}
|
||||
|
||||
impl Msg3Header {
|
||||
/// Parse a msg3 header from packet data.
|
||||
///
|
||||
/// Returns None if the packet is too short or has wrong version/phase.
|
||||
/// Accepts variable size (base + optional negotiation payload).
|
||||
pub fn parse(data: &[u8]) -> Option<Self> {
|
||||
if data.len() < MSG3_WIRE_SIZE {
|
||||
return None;
|
||||
}
|
||||
|
||||
let version = data[0] >> 4;
|
||||
let phase = data[0] & 0x0F;
|
||||
|
||||
if version != FMP_VERSION || phase != PHASE_MSG3 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// 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_msg3_offset: COMMON_PREFIX_SIZE + 4 + 4, // 12
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the Noise msg3 payload from the original packet (variable length).
|
||||
#[cfg(test)]
|
||||
pub fn noise_msg3<'a>(&self, data: &'a [u8]) -> &'a [u8] {
|
||||
&data[self.noise_msg3_offset..]
|
||||
}
|
||||
|
||||
/// Get the total noise payload length (base + optional negotiation).
|
||||
#[allow(dead_code)]
|
||||
pub fn noise_payload_len(&self, data: &[u8]) -> usize {
|
||||
data.len() - self.noise_msg3_offset
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Serialization Helpers
|
||||
// ============================================================================
|
||||
|
||||
/// Build a wire-format msg1 packet.
|
||||
///
|
||||
/// Format: `[0x11][0x00][payload_len:2 LE][sender_idx:4 LE][noise_msg1:33]`
|
||||
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(CommonPrefix::ver_phase_byte(FMP_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
|
||||
}
|
||||
|
||||
/// Build a wire-format msg2 packet.
|
||||
///
|
||||
/// Format: `[0x12][0x00][payload_len:2 LE][sender_idx:4 LE][receiver_idx:4 LE][noise_msg2:106+]`
|
||||
/// The noise_msg2 may include an optional negotiation payload beyond the base XX msg2.
|
||||
pub fn build_msg2(
|
||||
sender_idx: SessionIndex,
|
||||
receiver_idx: SessionIndex,
|
||||
noise_msg2: &[u8],
|
||||
) -> Vec<u8> {
|
||||
debug_assert!(noise_msg2.len() >= HANDSHAKE_MSG2_SIZE);
|
||||
|
||||
let payload_len = (4 + 4 + noise_msg2.len()) as u16; // sender + receiver + noise
|
||||
let total = COMMON_PREFIX_SIZE + 4 + 4 + noise_msg2.len();
|
||||
|
||||
let mut packet = Vec::with_capacity(total);
|
||||
packet.push(CommonPrefix::ver_phase_byte(FMP_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 a wire-format msg3 packet (XX handshake completion).
|
||||
///
|
||||
/// Format: `[0x13][0x00][payload_len:2 LE][sender_idx:4 LE][receiver_idx:4 LE][noise_msg3:73+]`
|
||||
/// The noise_msg3 may include an optional negotiation payload beyond the base XX msg3.
|
||||
pub fn build_msg3(
|
||||
sender_idx: SessionIndex,
|
||||
receiver_idx: SessionIndex,
|
||||
noise_msg3: &[u8],
|
||||
) -> Vec<u8> {
|
||||
debug_assert!(noise_msg3.len() >= HANDSHAKE_MSG3_SIZE);
|
||||
|
||||
let payload_len = (4 + 4 + noise_msg3.len()) as u16; // sender + receiver + noise
|
||||
let total = COMMON_PREFIX_SIZE + 4 + 4 + noise_msg3.len();
|
||||
|
||||
let mut packet = Vec::with_capacity(total);
|
||||
packet.push(CommonPrefix::ver_phase_byte(FMP_VERSION, PHASE_MSG3));
|
||||
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_msg3);
|
||||
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(FMP_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: `[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
|
||||
// ============================================================================
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_common_prefix_parse() {
|
||||
let data = [0x10, 0x02, 0x20, 0x00]; // ver=1, phase=0, flags=CE, payload_len=32
|
||||
let prefix = CommonPrefix::parse(&data).unwrap();
|
||||
assert_eq!(prefix.version, 1);
|
||||
assert_eq!(prefix.phase, 0);
|
||||
assert_eq!(prefix.flags, FLAG_CE);
|
||||
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() {
|
||||
let receiver_idx = SessionIndex::new(0x12345678);
|
||||
let counter = 42u64;
|
||||
let flags = 0u8;
|
||||
let payload_len = 32u16; // 16 plaintext + 16 tag
|
||||
let ciphertext = vec![0xaa; 48]; // payload_len + TAG_SIZE
|
||||
|
||||
let header = build_established_header(receiver_idx, counter, flags, payload_len);
|
||||
let packet = build_encrypted(&header, &ciphertext);
|
||||
|
||||
assert_eq!(packet.len(), ESTABLISHED_HEADER_SIZE + 48);
|
||||
assert_eq!(packet[0], 0x10); // ver=1, phase=0
|
||||
|
||||
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; ENCRYPTED_MIN_SIZE - 1];
|
||||
assert!(EncryptedHeader::parse(&packet).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypted_header_wrong_phase() {
|
||||
let mut packet = vec![0x00; ENCRYPTED_MIN_SIZE];
|
||||
packet[0] = 0x11; // ver=1, 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] = 0x00; // version 0 (old), phase 0
|
||||
assert!(EncryptedHeader::parse(&packet).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_msg1_header_parse() {
|
||||
let sender_idx = SessionIndex::new(0xABCDEF01);
|
||||
let noise_msg1 = vec![0xbb; HANDSHAKE_MSG1_SIZE];
|
||||
|
||||
let packet = build_msg1(sender_idx, &noise_msg1);
|
||||
|
||||
assert_eq!(packet.len(), MSG1_WIRE_SIZE);
|
||||
assert_eq!(packet[0], 0x11); // ver=1, phase=1
|
||||
|
||||
let header = Msg1Header::parse(&packet).expect("should parse");
|
||||
assert_eq!(header.sender_idx, sender_idx);
|
||||
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![0x11; MSG1_WIRE_SIZE - 1];
|
||||
assert!(Msg1Header::parse(&packet).is_none());
|
||||
|
||||
let packet = vec![0x11; MSG1_WIRE_SIZE + 1];
|
||||
assert!(Msg1Header::parse(&packet).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_msg1_header_wrong_phase() {
|
||||
let mut packet = vec![0x00; MSG1_WIRE_SIZE];
|
||||
packet[0] = 0x12; // ver=1, 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());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_msg2_header_parse() {
|
||||
let sender_idx = SessionIndex::new(0x11223344);
|
||||
let receiver_idx = SessionIndex::new(0x55667788);
|
||||
let noise_msg2 = vec![0xcc; HANDSHAKE_MSG2_SIZE];
|
||||
|
||||
let packet = build_msg2(sender_idx, receiver_idx, &noise_msg2);
|
||||
|
||||
assert_eq!(packet.len(), MSG2_WIRE_SIZE);
|
||||
assert_eq!(packet[0], 0x12); // ver=1, 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, 12);
|
||||
assert_eq!(header.noise_msg2(&packet), &noise_msg2[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_msg2_header_wrong_size() {
|
||||
let packet = vec![0x12; MSG2_WIRE_SIZE - 1];
|
||||
assert!(Msg2Header::parse(&packet).is_none());
|
||||
|
||||
// Larger than minimum is now accepted (variable-length negotiation payload)
|
||||
let mut packet = vec![0x12; MSG2_WIRE_SIZE + 10];
|
||||
packet[0] = 0x12; // ver=1, phase=2
|
||||
packet[1] = 0x00;
|
||||
let header = Msg2Header::parse(&packet);
|
||||
assert!(header.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_msg2_header_wrong_phase() {
|
||||
let mut packet = vec![0x00; MSG2_WIRE_SIZE];
|
||||
packet[0] = 0x10; // ver=1, phase 0, not phase 2
|
||||
assert!(Msg2Header::parse(&packet).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wire_sizes() {
|
||||
assert_eq!(MSG1_WIRE_SIZE, 41); // 4 + 4 + 33 (XX msg1)
|
||||
assert_eq!(MSG2_WIRE_SIZE, 118); // 4 + 4 + 4 + 106 (XX msg2 minimum)
|
||||
assert_eq!(MSG3_WIRE_SIZE, 85); // 4 + 4 + 4 + 73 (XX msg3 minimum)
|
||||
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() {
|
||||
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 (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_CE, 100);
|
||||
assert_eq!(header[1], 0x03); // bits 0 and 1 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, FLAG_CE);
|
||||
}
|
||||
|
||||
#[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(33) = 37
|
||||
assert_eq!(prefix.payload_len, 37);
|
||||
}
|
||||
|
||||
#[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(106) = 114
|
||||
assert_eq!(prefix.payload_len, 114);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_msg3_header_parse() {
|
||||
let sender_idx = SessionIndex::new(0xAABBCCDD);
|
||||
let receiver_idx = SessionIndex::new(0x11223344);
|
||||
let noise_msg3 = vec![0xdd; HANDSHAKE_MSG3_SIZE];
|
||||
|
||||
let packet = build_msg3(sender_idx, receiver_idx, &noise_msg3);
|
||||
|
||||
assert_eq!(packet.len(), MSG3_WIRE_SIZE);
|
||||
assert_eq!(packet[0], 0x13); // ver=1, phase=3
|
||||
|
||||
let header = Msg3Header::parse(&packet).expect("should parse");
|
||||
assert_eq!(header.sender_idx, sender_idx);
|
||||
assert_eq!(header.receiver_idx, receiver_idx);
|
||||
assert_eq!(header.noise_msg3_offset, 12);
|
||||
assert_eq!(header.noise_msg3(&packet), &noise_msg3[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_msg3_header_wrong_size() {
|
||||
let packet = vec![0x13; MSG3_WIRE_SIZE - 1];
|
||||
assert!(Msg3Header::parse(&packet).is_none());
|
||||
|
||||
// Larger than minimum is now accepted (variable-length negotiation payload)
|
||||
let mut packet = vec![0x13; MSG3_WIRE_SIZE + 10];
|
||||
packet[0] = 0x13; // ver=1, phase=3
|
||||
packet[1] = 0x00;
|
||||
let header = Msg3Header::parse(&packet);
|
||||
assert!(header.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_msg3_header_wrong_phase() {
|
||||
let mut packet = vec![0x00; MSG3_WIRE_SIZE];
|
||||
packet[0] = 0x12; // ver=1, phase 2, not phase 3
|
||||
assert!(Msg3Header::parse(&packet).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_msg3_header_nonzero_flags() {
|
||||
let mut packet = build_msg3(
|
||||
SessionIndex::new(1),
|
||||
SessionIndex::new(2),
|
||||
&[0u8; HANDSHAKE_MSG3_SIZE],
|
||||
);
|
||||
packet[1] = 0x01; // flags must be zero during handshake
|
||||
assert!(Msg3Header::parse(&packet).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_payload_len_in_msg3() {
|
||||
let packet = build_msg3(
|
||||
SessionIndex::new(1),
|
||||
SessionIndex::new(2),
|
||||
&[0u8; HANDSHAKE_MSG3_SIZE],
|
||||
);
|
||||
let prefix = CommonPrefix::parse(&packet).unwrap();
|
||||
// payload_len = sender_idx(4) + receiver_idx(4) + noise_msg3(73) = 81
|
||||
assert_eq!(prefix.payload_len, 81);
|
||||
}
|
||||
}
|
||||
+1
-60
@@ -59,66 +59,6 @@ pub enum PeerError {
|
||||
MaxPeersExceeded { max: usize },
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Cross-Connection Handling
|
||||
// ============================================================================
|
||||
|
||||
/// Result of attempting to promote a connection to active peer.
|
||||
///
|
||||
/// When a handshake completes, we may discover that we already have a
|
||||
/// connection to this peer (cross-connection). The tie-breaker rule
|
||||
/// determines which connection survives.
|
||||
///
|
||||
/// Note: Returns NodeAddr instead of ActivePeer because ActivePeer cannot
|
||||
/// be cloned (it contains NoiseSession which has cryptographic state).
|
||||
/// Callers can look up the peer from the peers map using the NodeAddr.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum PromotionResult {
|
||||
/// New peer created successfully.
|
||||
Promoted(NodeAddr),
|
||||
|
||||
/// Cross-connection detected. This connection lost the tie-breaker
|
||||
/// and should be closed.
|
||||
CrossConnectionLost {
|
||||
/// The link that won (existing connection).
|
||||
winner_link_id: LinkId,
|
||||
},
|
||||
|
||||
/// Cross-connection detected. This connection won the tie-breaker.
|
||||
/// The existing connection was replaced.
|
||||
CrossConnectionWon {
|
||||
/// The link that lost (previous connection, now closed).
|
||||
loser_link_id: LinkId,
|
||||
/// The node ID of the peer.
|
||||
node_addr: NodeAddr,
|
||||
},
|
||||
}
|
||||
|
||||
impl PromotionResult {
|
||||
/// Get the node ID if promotion succeeded.
|
||||
pub fn node_addr(&self) -> Option<NodeAddr> {
|
||||
match self {
|
||||
PromotionResult::Promoted(node_addr) => Some(*node_addr),
|
||||
PromotionResult::CrossConnectionWon { node_addr, .. } => Some(*node_addr),
|
||||
PromotionResult::CrossConnectionLost { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this connection should be closed.
|
||||
pub fn should_close_this_connection(&self) -> bool {
|
||||
matches!(self, PromotionResult::CrossConnectionLost { .. })
|
||||
}
|
||||
|
||||
/// Get the link that should be closed, if any.
|
||||
pub fn link_to_close(&self) -> Option<LinkId> {
|
||||
match self {
|
||||
PromotionResult::CrossConnectionLost { .. } => None, // Caller's link
|
||||
PromotionResult::CrossConnectionWon { loser_link_id, .. } => Some(*loser_link_id),
|
||||
PromotionResult::Promoted(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PeerSlot
|
||||
// ============================================================================
|
||||
@@ -240,6 +180,7 @@ impl fmt::Display for PeerSlot {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::proto::fmp::PromotionResult;
|
||||
use crate::transport::LinkId;
|
||||
use crate::{Identity, PeerIdentity};
|
||||
|
||||
|
||||
@@ -54,6 +54,62 @@ pub fn cross_connection_winner(
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of attempting to promote a connection to active peer.
|
||||
///
|
||||
/// When a handshake completes, we may discover that we already have a
|
||||
/// connection to this peer (cross-connection). The tie-breaker rule
|
||||
/// determines which connection survives.
|
||||
///
|
||||
/// Note: Returns NodeAddr instead of ActivePeer because ActivePeer cannot
|
||||
/// be cloned (it contains NoiseSession which has cryptographic state).
|
||||
/// Callers can look up the peer from the peers map using the NodeAddr.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum PromotionResult {
|
||||
/// New peer created successfully.
|
||||
Promoted(NodeAddr),
|
||||
|
||||
/// Cross-connection detected. This connection lost the tie-breaker
|
||||
/// and should be closed.
|
||||
CrossConnectionLost {
|
||||
/// The link that won (existing connection).
|
||||
winner_link_id: LinkId,
|
||||
},
|
||||
|
||||
/// Cross-connection detected. This connection won the tie-breaker.
|
||||
/// The existing connection was replaced.
|
||||
CrossConnectionWon {
|
||||
/// The link that lost (previous connection, now closed).
|
||||
loser_link_id: LinkId,
|
||||
/// The node ID of the peer.
|
||||
node_addr: NodeAddr,
|
||||
},
|
||||
}
|
||||
|
||||
impl PromotionResult {
|
||||
/// Get the node ID if promotion succeeded.
|
||||
pub fn node_addr(&self) -> Option<NodeAddr> {
|
||||
match self {
|
||||
PromotionResult::Promoted(node_addr) => Some(*node_addr),
|
||||
PromotionResult::CrossConnectionWon { node_addr, .. } => Some(*node_addr),
|
||||
PromotionResult::CrossConnectionLost { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this connection should be closed.
|
||||
pub fn should_close_this_connection(&self) -> bool {
|
||||
matches!(self, PromotionResult::CrossConnectionLost { .. })
|
||||
}
|
||||
|
||||
/// Get the link that should be closed, if any.
|
||||
pub fn link_to_close(&self) -> Option<LinkId> {
|
||||
match self {
|
||||
PromotionResult::CrossConnectionLost { .. } => None, // Caller's link
|
||||
PromotionResult::CrossConnectionWon { loser_link_id, .. } => Some(*loser_link_id),
|
||||
PromotionResult::Promoted(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A snapshot of one handshake connection's lifecycle-relevant state, taken by
|
||||
/// the shell so the core decides without touching live `Node` state or reading
|
||||
/// a clock.
|
||||
|
||||
@@ -24,20 +24,23 @@
|
||||
//! (stateless) lifecycle anchor owned by `Node`.
|
||||
//! - `wire.rs` — the FMP wire codec: XX handshake message types, disconnect
|
||||
//! reasons, the orderly disconnect message, and the negotiation payload.
|
||||
//! Also carries the relocated FMP link wire framing (moved from
|
||||
//! `node/wire.rs`): the common prefix, encrypted/established headers, and
|
||||
//! the msg1/msg2/msg3 handshake framing.
|
||||
|
||||
mod core;
|
||||
mod limits;
|
||||
mod state;
|
||||
mod wire;
|
||||
pub(crate) mod wire;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub use core::cross_connection_winner;
|
||||
pub(crate) use core::{
|
||||
ConnAction, ConnSnapshot, EstablishSnapshot, InboundDecision, InboundReject, LifecycleView,
|
||||
PeerSnapshot, RekeyCfg, RekeyResendSnapshot, WireOutcome, decide_fmp_negotiation,
|
||||
};
|
||||
pub use core::{PromotionResult, cross_connection_winner};
|
||||
pub(crate) use limits::backoff_ms;
|
||||
pub use state::HandshakeState;
|
||||
pub(crate) use state::{ConnectionState, Fmp};
|
||||
|
||||
@@ -10,9 +10,11 @@
|
||||
//! negotiation *decision* logic (version agreement, profile validation, FMP
|
||||
//! feature helpers) lives in `core.rs`; only the payload codec is here.
|
||||
|
||||
use crate::noise::{HANDSHAKE_MSG1_SIZE, HANDSHAKE_MSG2_SIZE, HANDSHAKE_MSG3_SIZE, TAG_SIZE};
|
||||
use crate::proto::Error;
|
||||
use crate::proto::codec::Reader;
|
||||
use crate::proto::link::LinkMessageType;
|
||||
use crate::utils::index::SessionIndex;
|
||||
use ::core::fmt;
|
||||
|
||||
/// Handshake message type identifiers.
|
||||
@@ -368,3 +370,798 @@ impl NegotiationPayload {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// FMP link wire framing (relocated from node/wire.rs)
|
||||
// ============================================================================
|
||||
//
|
||||
// The FIPS mesh-layer wire format (FMP) for packet dispatch. All packets begin
|
||||
// with a 4-byte common prefix followed by phase-specific fields.
|
||||
//
|
||||
// Common Prefix (4 bytes):
|
||||
//
|
||||
// [ver+phase:1][flags:1][payload_len:2 LE]
|
||||
//
|
||||
// Packet Types:
|
||||
//
|
||||
// | Phase | Type | Size | Description |
|
||||
// |-------|-----------------|------------|--------------------------------|
|
||||
// | 0x0 | Encrypted frame | 32+ bytes | Post-handshake encrypted data |
|
||||
// | 0x1 | Noise XX msg1 | 41 bytes | Handshake initiation |
|
||||
// | 0x2 | Noise XX msg2 | 118+ bytes | Handshake response |
|
||||
// | 0x3 | Noise XX msg3 | 85+ bytes | Handshake completion |
|
||||
|
||||
// ============================================================================
|
||||
// Constants
|
||||
// ============================================================================
|
||||
|
||||
/// FMP protocol version (4 high bits of byte 0).
|
||||
pub const FMP_VERSION: u8 = 1;
|
||||
|
||||
/// Phase value for established (encrypted) frames.
|
||||
pub const PHASE_ESTABLISHED: u8 = 0x0;
|
||||
|
||||
/// Phase value for handshake message 1 (initiation).
|
||||
pub const PHASE_MSG1: u8 = 0x1;
|
||||
|
||||
/// Phase value for handshake message 2 (response).
|
||||
pub const PHASE_MSG2: u8 = 0x2;
|
||||
|
||||
/// Phase value for handshake message 3 (completion, XX only).
|
||||
pub const PHASE_MSG3: u8 = 0x3;
|
||||
|
||||
/// Size of the common packet prefix (all packet types).
|
||||
pub const COMMON_PREFIX_SIZE: usize = 4;
|
||||
|
||||
/// Size of the full established frame header (prefix + receiver_idx + counter).
|
||||
pub const ESTABLISHED_HEADER_SIZE: usize = 16;
|
||||
|
||||
/// Size of handshake msg1 wire packet: prefix + sender_idx + noise_msg1.
|
||||
pub const MSG1_WIRE_SIZE: usize = COMMON_PREFIX_SIZE + 4 + HANDSHAKE_MSG1_SIZE; // 41 bytes
|
||||
|
||||
/// Minimum size of handshake msg2 wire packet: prefix + sender_idx + receiver_idx + noise_msg2.
|
||||
/// Actual size may be larger due to optional negotiation payload.
|
||||
pub const MSG2_WIRE_SIZE: usize = COMMON_PREFIX_SIZE + 4 + 4 + HANDSHAKE_MSG2_SIZE; // 118 bytes
|
||||
|
||||
/// Minimum size of handshake msg3 wire packet: prefix + sender_idx + receiver_idx + noise_msg3.
|
||||
/// Actual size may be larger due to optional negotiation payload.
|
||||
pub const MSG3_WIRE_SIZE: usize = COMMON_PREFIX_SIZE + 4 + 4 + HANDSHAKE_MSG3_SIZE; // 85 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;
|
||||
|
||||
// ============================================================================
|
||||
// Common Prefix
|
||||
// ============================================================================
|
||||
|
||||
/// Parsed common packet prefix (first 4 bytes of every FMP 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 established frame header (phase 0x0).
|
||||
///
|
||||
/// Wire format (16 bytes):
|
||||
/// ```text
|
||||
/// [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,
|
||||
/// Raw 16-byte header for use as AEAD AAD.
|
||||
pub header_bytes: [u8; ESTABLISHED_HEADER_SIZE],
|
||||
}
|
||||
|
||||
impl EncryptedHeader {
|
||||
/// Parse an established frame header from packet data.
|
||||
///
|
||||
/// 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;
|
||||
}
|
||||
|
||||
let version = data[0] >> 4;
|
||||
let phase = data[0] & 0x0F;
|
||||
|
||||
if version != FMP_VERSION || phase != PHASE_ESTABLISHED {
|
||||
return None;
|
||||
}
|
||||
|
||||
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[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,
|
||||
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[ESTABLISHED_HEADER_SIZE..]
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Msg1 Header
|
||||
// ============================================================================
|
||||
|
||||
/// Parsed handshake message 1 header (phase 0x1).
|
||||
///
|
||||
/// Wire format (41 bytes, Noise XX):
|
||||
/// ```text
|
||||
/// [0x11][0x00][payload_len:2 LE][sender_idx:4 LE][noise_msg1:33]
|
||||
/// ```
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Msg1Header {
|
||||
/// Session index chosen by the sender (becomes receiver_idx for responses).
|
||||
pub sender_idx: SessionIndex,
|
||||
/// Offset where Noise msg1 payload begins.
|
||||
pub noise_msg1_offset: usize,
|
||||
}
|
||||
|
||||
impl Msg1Header {
|
||||
/// Parse a msg1 header from packet data.
|
||||
///
|
||||
/// 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;
|
||||
}
|
||||
|
||||
let version = data[0] >> 4;
|
||||
let phase = data[0] & 0x0F;
|
||||
|
||||
if version != FMP_VERSION || phase != PHASE_MSG1 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// 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: COMMON_PREFIX_SIZE + 4, // 8
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the Noise msg1 payload from the original packet.
|
||||
#[cfg(test)]
|
||||
pub fn noise_msg1<'a>(&self, data: &'a [u8]) -> &'a [u8] {
|
||||
&data[self.noise_msg1_offset..]
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Msg2 Header
|
||||
// ============================================================================
|
||||
|
||||
/// Parsed handshake message 2 header (phase 0x2).
|
||||
///
|
||||
/// Wire format (118+ bytes, Noise XX):
|
||||
/// ```text
|
||||
/// [0x12][0x00][payload_len:2 LE][sender_idx:4 LE][receiver_idx:4 LE][noise_msg2:106+]
|
||||
/// ```
|
||||
/// Size is variable due to optional negotiation payload appended after base XX msg2.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Msg2Header {
|
||||
/// Session index chosen by the responder.
|
||||
pub sender_idx: SessionIndex,
|
||||
/// Echo of the initiator's sender_idx from msg1.
|
||||
pub receiver_idx: SessionIndex,
|
||||
/// Offset where Noise msg2 payload begins.
|
||||
pub noise_msg2_offset: usize,
|
||||
}
|
||||
|
||||
impl Msg2Header {
|
||||
/// Parse a msg2 header from packet data.
|
||||
///
|
||||
/// Returns None if the packet is too short or has wrong version/phase.
|
||||
/// Accepts variable size (base + optional negotiation payload).
|
||||
pub fn parse(data: &[u8]) -> Option<Self> {
|
||||
if data.len() < MSG2_WIRE_SIZE {
|
||||
return None;
|
||||
}
|
||||
|
||||
let version = data[0] >> 4;
|
||||
let phase = data[0] & 0x0F;
|
||||
|
||||
if version != FMP_VERSION || phase != PHASE_MSG2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// 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: COMMON_PREFIX_SIZE + 4 + 4, // 12
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the Noise msg2 payload from the original packet (variable length).
|
||||
#[cfg(test)]
|
||||
pub fn noise_msg2<'a>(&self, data: &'a [u8]) -> &'a [u8] {
|
||||
&data[self.noise_msg2_offset..]
|
||||
}
|
||||
|
||||
/// Get the total noise payload length (base + optional negotiation).
|
||||
#[allow(dead_code)]
|
||||
pub fn noise_payload_len(&self, data: &[u8]) -> usize {
|
||||
data.len() - self.noise_msg2_offset
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Msg3 Header
|
||||
// ============================================================================
|
||||
|
||||
/// Parsed handshake message 3 header (phase 0x3, XX pattern).
|
||||
///
|
||||
/// Wire format (85+ bytes, Noise XX):
|
||||
/// ```text
|
||||
/// [0x13][0x00][payload_len:2 LE][sender_idx:4 LE][receiver_idx:4 LE][noise_msg3:73+]
|
||||
/// ```
|
||||
/// Size is variable due to optional negotiation payload appended after base XX msg3.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Msg3Header {
|
||||
/// Session index chosen by the initiator (echo of msg1 sender_idx).
|
||||
pub sender_idx: SessionIndex,
|
||||
/// Echo of the responder's sender_idx from msg2.
|
||||
pub receiver_idx: SessionIndex,
|
||||
/// Offset where Noise msg3 payload begins.
|
||||
pub noise_msg3_offset: usize,
|
||||
}
|
||||
|
||||
impl Msg3Header {
|
||||
/// Parse a msg3 header from packet data.
|
||||
///
|
||||
/// Returns None if the packet is too short or has wrong version/phase.
|
||||
/// Accepts variable size (base + optional negotiation payload).
|
||||
pub fn parse(data: &[u8]) -> Option<Self> {
|
||||
if data.len() < MSG3_WIRE_SIZE {
|
||||
return None;
|
||||
}
|
||||
|
||||
let version = data[0] >> 4;
|
||||
let phase = data[0] & 0x0F;
|
||||
|
||||
if version != FMP_VERSION || phase != PHASE_MSG3 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// 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_msg3_offset: COMMON_PREFIX_SIZE + 4 + 4, // 12
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the Noise msg3 payload from the original packet (variable length).
|
||||
#[cfg(test)]
|
||||
pub fn noise_msg3<'a>(&self, data: &'a [u8]) -> &'a [u8] {
|
||||
&data[self.noise_msg3_offset..]
|
||||
}
|
||||
|
||||
/// Get the total noise payload length (base + optional negotiation).
|
||||
#[allow(dead_code)]
|
||||
pub fn noise_payload_len(&self, data: &[u8]) -> usize {
|
||||
data.len() - self.noise_msg3_offset
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Serialization Helpers
|
||||
// ============================================================================
|
||||
|
||||
/// Build a wire-format msg1 packet.
|
||||
///
|
||||
/// Format: `[0x11][0x00][payload_len:2 LE][sender_idx:4 LE][noise_msg1:33]`
|
||||
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(CommonPrefix::ver_phase_byte(FMP_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
|
||||
}
|
||||
|
||||
/// Build a wire-format msg2 packet.
|
||||
///
|
||||
/// Format: `[0x12][0x00][payload_len:2 LE][sender_idx:4 LE][receiver_idx:4 LE][noise_msg2:106+]`
|
||||
/// The noise_msg2 may include an optional negotiation payload beyond the base XX msg2.
|
||||
pub fn build_msg2(
|
||||
sender_idx: SessionIndex,
|
||||
receiver_idx: SessionIndex,
|
||||
noise_msg2: &[u8],
|
||||
) -> Vec<u8> {
|
||||
debug_assert!(noise_msg2.len() >= HANDSHAKE_MSG2_SIZE);
|
||||
|
||||
let payload_len = (4 + 4 + noise_msg2.len()) as u16; // sender + receiver + noise
|
||||
let total = COMMON_PREFIX_SIZE + 4 + 4 + noise_msg2.len();
|
||||
|
||||
let mut packet = Vec::with_capacity(total);
|
||||
packet.push(CommonPrefix::ver_phase_byte(FMP_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 a wire-format msg3 packet (XX handshake completion).
|
||||
///
|
||||
/// Format: `[0x13][0x00][payload_len:2 LE][sender_idx:4 LE][receiver_idx:4 LE][noise_msg3:73+]`
|
||||
/// The noise_msg3 may include an optional negotiation payload beyond the base XX msg3.
|
||||
pub fn build_msg3(
|
||||
sender_idx: SessionIndex,
|
||||
receiver_idx: SessionIndex,
|
||||
noise_msg3: &[u8],
|
||||
) -> Vec<u8> {
|
||||
debug_assert!(noise_msg3.len() >= HANDSHAKE_MSG3_SIZE);
|
||||
|
||||
let payload_len = (4 + 4 + noise_msg3.len()) as u16; // sender + receiver + noise
|
||||
let total = COMMON_PREFIX_SIZE + 4 + 4 + noise_msg3.len();
|
||||
|
||||
let mut packet = Vec::with_capacity(total);
|
||||
packet.push(CommonPrefix::ver_phase_byte(FMP_VERSION, PHASE_MSG3));
|
||||
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_msg3);
|
||||
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(FMP_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: `[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
|
||||
// ============================================================================
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_common_prefix_parse() {
|
||||
let data = [0x10, 0x02, 0x20, 0x00]; // ver=1, phase=0, flags=CE, payload_len=32
|
||||
let prefix = CommonPrefix::parse(&data).unwrap();
|
||||
assert_eq!(prefix.version, 1);
|
||||
assert_eq!(prefix.phase, 0);
|
||||
assert_eq!(prefix.flags, FLAG_CE);
|
||||
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() {
|
||||
let receiver_idx = SessionIndex::new(0x12345678);
|
||||
let counter = 42u64;
|
||||
let flags = 0u8;
|
||||
let payload_len = 32u16; // 16 plaintext + 16 tag
|
||||
let ciphertext = vec![0xaa; 48]; // payload_len + TAG_SIZE
|
||||
|
||||
let header = build_established_header(receiver_idx, counter, flags, payload_len);
|
||||
let packet = build_encrypted(&header, &ciphertext);
|
||||
|
||||
assert_eq!(packet.len(), ESTABLISHED_HEADER_SIZE + 48);
|
||||
assert_eq!(packet[0], 0x10); // ver=1, phase=0
|
||||
|
||||
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; ENCRYPTED_MIN_SIZE - 1];
|
||||
assert!(EncryptedHeader::parse(&packet).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypted_header_wrong_phase() {
|
||||
let mut packet = vec![0x00; ENCRYPTED_MIN_SIZE];
|
||||
packet[0] = 0x11; // ver=1, 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] = 0x00; // version 0 (old), phase 0
|
||||
assert!(EncryptedHeader::parse(&packet).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_msg1_header_parse() {
|
||||
let sender_idx = SessionIndex::new(0xABCDEF01);
|
||||
let noise_msg1 = vec![0xbb; HANDSHAKE_MSG1_SIZE];
|
||||
|
||||
let packet = build_msg1(sender_idx, &noise_msg1);
|
||||
|
||||
assert_eq!(packet.len(), MSG1_WIRE_SIZE);
|
||||
assert_eq!(packet[0], 0x11); // ver=1, phase=1
|
||||
|
||||
let header = Msg1Header::parse(&packet).expect("should parse");
|
||||
assert_eq!(header.sender_idx, sender_idx);
|
||||
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![0x11; MSG1_WIRE_SIZE - 1];
|
||||
assert!(Msg1Header::parse(&packet).is_none());
|
||||
|
||||
let packet = vec![0x11; MSG1_WIRE_SIZE + 1];
|
||||
assert!(Msg1Header::parse(&packet).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_msg1_header_wrong_phase() {
|
||||
let mut packet = vec![0x00; MSG1_WIRE_SIZE];
|
||||
packet[0] = 0x12; // ver=1, 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());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_msg2_header_parse() {
|
||||
let sender_idx = SessionIndex::new(0x11223344);
|
||||
let receiver_idx = SessionIndex::new(0x55667788);
|
||||
let noise_msg2 = vec![0xcc; HANDSHAKE_MSG2_SIZE];
|
||||
|
||||
let packet = build_msg2(sender_idx, receiver_idx, &noise_msg2);
|
||||
|
||||
assert_eq!(packet.len(), MSG2_WIRE_SIZE);
|
||||
assert_eq!(packet[0], 0x12); // ver=1, 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, 12);
|
||||
assert_eq!(header.noise_msg2(&packet), &noise_msg2[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_msg2_header_wrong_size() {
|
||||
let packet = vec![0x12; MSG2_WIRE_SIZE - 1];
|
||||
assert!(Msg2Header::parse(&packet).is_none());
|
||||
|
||||
// Larger than minimum is now accepted (variable-length negotiation payload)
|
||||
let mut packet = vec![0x12; MSG2_WIRE_SIZE + 10];
|
||||
packet[0] = 0x12; // ver=1, phase=2
|
||||
packet[1] = 0x00;
|
||||
let header = Msg2Header::parse(&packet);
|
||||
assert!(header.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_msg2_header_wrong_phase() {
|
||||
let mut packet = vec![0x00; MSG2_WIRE_SIZE];
|
||||
packet[0] = 0x10; // ver=1, phase 0, not phase 2
|
||||
assert!(Msg2Header::parse(&packet).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wire_sizes() {
|
||||
assert_eq!(MSG1_WIRE_SIZE, 41); // 4 + 4 + 33 (XX msg1)
|
||||
assert_eq!(MSG2_WIRE_SIZE, 118); // 4 + 4 + 4 + 106 (XX msg2 minimum)
|
||||
assert_eq!(MSG3_WIRE_SIZE, 85); // 4 + 4 + 4 + 73 (XX msg3 minimum)
|
||||
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() {
|
||||
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 (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_CE, 100);
|
||||
assert_eq!(header[1], 0x03); // bits 0 and 1 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, FLAG_CE);
|
||||
}
|
||||
|
||||
#[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(33) = 37
|
||||
assert_eq!(prefix.payload_len, 37);
|
||||
}
|
||||
|
||||
#[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(106) = 114
|
||||
assert_eq!(prefix.payload_len, 114);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_msg3_header_parse() {
|
||||
let sender_idx = SessionIndex::new(0xAABBCCDD);
|
||||
let receiver_idx = SessionIndex::new(0x11223344);
|
||||
let noise_msg3 = vec![0xdd; HANDSHAKE_MSG3_SIZE];
|
||||
|
||||
let packet = build_msg3(sender_idx, receiver_idx, &noise_msg3);
|
||||
|
||||
assert_eq!(packet.len(), MSG3_WIRE_SIZE);
|
||||
assert_eq!(packet[0], 0x13); // ver=1, phase=3
|
||||
|
||||
let header = Msg3Header::parse(&packet).expect("should parse");
|
||||
assert_eq!(header.sender_idx, sender_idx);
|
||||
assert_eq!(header.receiver_idx, receiver_idx);
|
||||
assert_eq!(header.noise_msg3_offset, 12);
|
||||
assert_eq!(header.noise_msg3(&packet), &noise_msg3[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_msg3_header_wrong_size() {
|
||||
let packet = vec![0x13; MSG3_WIRE_SIZE - 1];
|
||||
assert!(Msg3Header::parse(&packet).is_none());
|
||||
|
||||
// Larger than minimum is now accepted (variable-length negotiation payload)
|
||||
let mut packet = vec![0x13; MSG3_WIRE_SIZE + 10];
|
||||
packet[0] = 0x13; // ver=1, phase=3
|
||||
packet[1] = 0x00;
|
||||
let header = Msg3Header::parse(&packet);
|
||||
assert!(header.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_msg3_header_wrong_phase() {
|
||||
let mut packet = vec![0x00; MSG3_WIRE_SIZE];
|
||||
packet[0] = 0x12; // ver=1, phase 2, not phase 3
|
||||
assert!(Msg3Header::parse(&packet).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_msg3_header_nonzero_flags() {
|
||||
let mut packet = build_msg3(
|
||||
SessionIndex::new(1),
|
||||
SessionIndex::new(2),
|
||||
&[0u8; HANDSHAKE_MSG3_SIZE],
|
||||
);
|
||||
packet[1] = 0x01; // flags must be zero during handshake
|
||||
assert!(Msg3Header::parse(&packet).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_payload_len_in_msg3() {
|
||||
let packet = build_msg3(
|
||||
SessionIndex::new(1),
|
||||
SessionIndex::new(2),
|
||||
&[0u8; HANDSHAKE_MSG3_SIZE],
|
||||
);
|
||||
let prefix = CommonPrefix::parse(&packet).unwrap();
|
||||
// payload_len = sender_idx(4) + receiver_idx(4) + noise_msg3(73) = 81
|
||||
assert_eq!(prefix.payload_len, 81);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user