mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-30 19:46:15 +00:00
Relocate FMP link wire codec into proto/fmp
Move the FMP mesh-layer wire format (common prefix, encrypted/msg1/msg2 headers, and the build_*/inner-header codec fns) out of node/wire.rs and into proto/fmp/wire.rs, so the whole FMP wire surface lives with its subsystem, matching the proto/fsp/wire.rs layout. The wire module becomes pub(crate) mod wire; callers reach it via crate::proto::fmp::wire. Behavior-neutral: pure relocation plus import-path rewrites across the node/peer consumers; no logic change. Full lib suite green at baseline.
This commit is contained in:
@@ -561,7 +561,7 @@ mod tests {
|
|||||||
let open_cipher = LessSafeKey::new(unbound2);
|
let open_cipher = LessSafeKey::new(unbound2);
|
||||||
|
|
||||||
let counter: u64 = 7;
|
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]`
|
// Build a wire packet `[16-byte header][4-byte inner ts][1 byte link msg]`
|
||||||
// with capacity for the trailing AEAD tag. Header bytes
|
// with capacity for the trailing AEAD tag. Header bytes
|
||||||
// double as AAD and as the on-wire prefix.
|
// double as AAD and as the on-wire prefix.
|
||||||
@@ -569,7 +569,7 @@ mod tests {
|
|||||||
// Header: fill the flags byte (the second byte) with both
|
// Header: fill the flags byte (the second byte) with both
|
||||||
// FLAG_CE and FLAG_SP set; the rest is uninterpreted by the
|
// FLAG_CE and FLAG_SP set; the rest is uninterpreted by the
|
||||||
// worker (it just AADs the whole 16 bytes).
|
// worker (it just AADs the whole 16 bytes).
|
||||||
let flags_byte = crate::node::wire::FLAG_CE | crate::node::wire::FLAG_SP;
|
let flags_byte = crate::proto::fmp::wire::FLAG_CE | crate::proto::fmp::wire::FLAG_SP;
|
||||||
let mut header = [0u8; HDR];
|
let mut header = [0u8; HDR];
|
||||||
header[1] = flags_byte;
|
header[1] = flags_byte;
|
||||||
wire.extend_from_slice(&header);
|
wire.extend_from_slice(&header);
|
||||||
@@ -626,11 +626,11 @@ mod tests {
|
|||||||
"fmp_flags must round-trip from DecryptJob to DecryptFallback"
|
"fmp_flags must round-trip from DecryptJob to DecryptFallback"
|
||||||
);
|
);
|
||||||
assert!(
|
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"
|
"FLAG_CE bit lost on worker path"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
fallback.fmp_flags & crate::node::wire::FLAG_SP != 0,
|
fallback.fmp_flags & crate::proto::fmp::wire::FLAG_SP != 0,
|
||||||
"FLAG_SP bit lost on worker path"
|
"FLAG_SP bit lost on worker path"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -724,7 +724,7 @@ mod tests {
|
|||||||
let open_cipher = LessSafeKey::new(unbound);
|
let open_cipher = LessSafeKey::new(unbound);
|
||||||
|
|
||||||
let counter: u64 = 11;
|
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 header = [0u8; HDR];
|
||||||
let mut wire = Vec::with_capacity(HDR + 4 + 1 + 16);
|
let mut wire = Vec::with_capacity(HDR + 4 + 1 + 16);
|
||||||
wire.extend_from_slice(&header);
|
wire.extend_from_slice(&header);
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
// warnings rather than gate every function individually.
|
// warnings rather than gate every function individually.
|
||||||
#![cfg_attr(not(unix), allow(dead_code))]
|
#![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::proto::fsp::wire::FSP_HEADER_SIZE;
|
||||||
use crate::transport::udp::socket::AsyncUdpSocket;
|
use crate::transport::udp::socket::AsyncUdpSocket;
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
@@ -1904,8 +1904,8 @@ mod unix_tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn pipelined_send_wire_layout_roundtrips_canonical_decoders() {
|
fn pipelined_send_wire_layout_roundtrips_canonical_decoders() {
|
||||||
use crate::NodeAddr;
|
use crate::NodeAddr;
|
||||||
use crate::node::wire::{EncryptedHeader, FLAG_KEY_EPOCH, build_established_header};
|
|
||||||
use crate::noise::TAG_SIZE;
|
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::fsp::wire::build_fsp_header;
|
||||||
use crate::proto::link::{
|
use crate::proto::link::{
|
||||||
LinkMessageType, SESSION_DATAGRAM_HEADER_SIZE, SessionDatagramRef,
|
LinkMessageType, SESSION_DATAGRAM_HEADER_SIZE, SessionDatagramRef,
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
//! Encrypted frame handling (hot path).
|
//! Encrypted frame handling (hot path).
|
||||||
|
|
||||||
use crate::node::Node;
|
use crate::node::Node;
|
||||||
use crate::node::wire::{EncryptedHeader, FLAG_CE, FLAG_KEY_EPOCH, FLAG_SP, strip_inner_header};
|
|
||||||
use crate::noise::NoiseError;
|
use crate::noise::NoiseError;
|
||||||
|
use crate::proto::fmp::wire::{
|
||||||
|
EncryptedHeader, FLAG_CE, FLAG_KEY_EPOCH, FLAG_SP, strip_inner_header,
|
||||||
|
};
|
||||||
use crate::transport::ReceivedPacket;
|
use crate::transport::ReceivedPacket;
|
||||||
use tracing::{debug, trace, warn};
|
use tracing::{debug, trace, warn};
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ use crate::NodeAddr;
|
|||||||
use crate::PeerIdentity;
|
use crate::PeerIdentity;
|
||||||
use crate::node::acl::PeerAclContext;
|
use crate::node::acl::PeerAclContext;
|
||||||
use crate::node::reject::{HandshakeReject, RejectReason};
|
use crate::node::reject::{HandshakeReject, RejectReason};
|
||||||
use crate::node::wire::{Msg1Header, Msg2Header, build_msg2};
|
|
||||||
use crate::node::{Node, NodeError};
|
use crate::node::{Node, NodeError};
|
||||||
use crate::peer::{ActivePeer, PeerConnection, PromotionResult};
|
use crate::peer::{ActivePeer, PeerConnection, PromotionResult};
|
||||||
|
use crate::proto::fmp::wire::{Msg1Header, Msg2Header, build_msg2};
|
||||||
use crate::proto::fmp::{
|
use crate::proto::fmp::{
|
||||||
ConnAction, EstablishSnapshot, EstablishView, InboundDecision, InboundReject, OutboundDecision,
|
ConnAction, EstablishSnapshot, EstablishView, InboundDecision, InboundReject, OutboundDecision,
|
||||||
OutboundSnapshot, WireOutcome, cross_connection_winner,
|
OutboundSnapshot, WireOutcome, cross_connection_winner,
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
|
|
||||||
use crate::NodeAddr;
|
use crate::NodeAddr;
|
||||||
use crate::node::Node;
|
use crate::node::Node;
|
||||||
use crate::node::wire::build_msg1;
|
|
||||||
use crate::noise::HandshakeState;
|
use crate::noise::HandshakeState;
|
||||||
|
use crate::proto::fmp::wire::build_msg1;
|
||||||
use crate::proto::fmp::{ConnAction, LifecycleView, PeerSnapshot, RekeyCfg, RekeyResendSnapshot};
|
use crate::proto::fmp::{ConnAction, LifecycleView, PeerSnapshot, RekeyCfg, RekeyResendSnapshot};
|
||||||
use crate::proto::fsp::{
|
use crate::proto::fsp::{
|
||||||
FspAction, RekeyMsg3ResendSnapshot, SessionSetup, SessionSnapshot, cutover_timer_elapsed,
|
FspAction, RekeyMsg3ResendSnapshot, SessionSetup, SessionSnapshot, cutover_timer_elapsed,
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
//! RX event loop and packet dispatch.
|
//! RX event loop and packet dispatch.
|
||||||
|
|
||||||
use crate::control::{ControlSocket, commands};
|
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,
|
COMMON_PREFIX_SIZE, CommonPrefix, FMP_VERSION, PHASE_ESTABLISHED, PHASE_MSG1, PHASE_MSG2,
|
||||||
};
|
};
|
||||||
use crate::node::{Node, NodeError};
|
|
||||||
use crate::transport::ReceivedPacket;
|
use crate::transport::ReceivedPacket;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tracing::{debug, info, warn};
|
use tracing::{debug, info, warn};
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ use crate::NodeAddr;
|
|||||||
use crate::node::handlers::mmp::format_throughput;
|
use crate::node::handlers::mmp::format_throughput;
|
||||||
use crate::node::reject::{RejectReason, SessionReject};
|
use crate::node::reject::{RejectReason, SessionReject};
|
||||||
use crate::node::session::{EndToEndState, EpochSlot, SessionEntry};
|
use crate::node::session::{EndToEndState, EpochSlot, SessionEntry};
|
||||||
#[cfg(unix)]
|
|
||||||
use crate::node::wire::{
|
|
||||||
ESTABLISHED_HEADER_SIZE, FLAG_KEY_EPOCH, FLAG_SP, build_established_header,
|
|
||||||
};
|
|
||||||
use crate::node::{Node, NodeError};
|
use crate::node::{Node, NodeError};
|
||||||
use crate::noise::{
|
use crate::noise::{
|
||||||
HandshakeState, XK_HANDSHAKE_MSG1_SIZE, XK_HANDSHAKE_MSG2_SIZE, XK_HANDSHAKE_MSG3_SIZE,
|
HandshakeState, XK_HANDSHAKE_MSG1_SIZE, XK_HANDSHAKE_MSG2_SIZE, XK_HANDSHAKE_MSG3_SIZE,
|
||||||
};
|
};
|
||||||
|
#[cfg(unix)]
|
||||||
|
use crate::proto::fmp::wire::{
|
||||||
|
ESTABLISHED_HEADER_SIZE, FLAG_KEY_EPOCH, FLAG_SP, build_established_header,
|
||||||
|
};
|
||||||
use crate::proto::fsp::wire::{
|
use crate::proto::fsp::wire::{
|
||||||
FSP_COMMON_PREFIX_SIZE, FSP_FLAG_CP, FSP_FLAG_K, FSP_HEADER_SIZE, FSP_PHASE_ESTABLISHED,
|
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,
|
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 super::{Node, NodeError, NodeState};
|
||||||
use crate::config::{ConnectPolicy, PeerAddress, PeerConfig};
|
use crate::config::{ConnectPolicy, PeerAddress, PeerConfig};
|
||||||
use crate::node::acl::PeerAclContext;
|
use crate::node::acl::PeerAclContext;
|
||||||
use crate::node::wire::build_msg1;
|
|
||||||
use crate::nostr::{BootstrapEvent, NostrRendezvous};
|
use crate::nostr::{BootstrapEvent, NostrRendezvous};
|
||||||
use crate::nostr::{BootstrapHandoffResult, EstablishedTraversal};
|
use crate::nostr::{BootstrapHandoffResult, EstablishedTraversal};
|
||||||
use crate::peer::PeerConnection;
|
use crate::peer::PeerConnection;
|
||||||
|
use crate::proto::fmp::wire::build_msg1;
|
||||||
use crate::proto::fmp::{Disconnect, DisconnectReason};
|
use crate::proto::fmp::{Disconnect, DisconnectReason};
|
||||||
use crate::transport::{Link, LinkDirection, LinkId, TransportAddr, TransportId, packet_channel};
|
use crate::transport::{Link, LinkDirection, LinkId, TransportAddr, TransportId, packet_channel};
|
||||||
use crate::upper::tun::{TunDevice, TunState, run_tun_reader, shutdown_tun_interface};
|
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)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
mod tree;
|
mod tree;
|
||||||
pub(crate) mod wire;
|
|
||||||
|
|
||||||
use self::rate_limit::HandshakeRateLimiter;
|
use self::rate_limit::HandshakeRateLimiter;
|
||||||
use self::reloadable::Reloadable;
|
use self::reloadable::Reloadable;
|
||||||
@@ -35,15 +34,15 @@ use self::reloadable::Reloadable;
|
|||||||
/// dual-initiation in symmetric-start meshes; the configured
|
/// dual-initiation in symmetric-start meshes; the configured
|
||||||
/// `node.rekey.after_secs` remains the nominal interval (mean preserved).
|
/// `node.rekey.after_secs` remains the nominal interval (mean preserved).
|
||||||
pub(crate) const REKEY_JITTER_SECS: i64 = 15;
|
pub(crate) const REKEY_JITTER_SECS: i64 = 15;
|
||||||
use self::wire::{
|
|
||||||
ESTABLISHED_HEADER_SIZE, FLAG_CE, FLAG_KEY_EPOCH, FLAG_SP, build_encrypted,
|
|
||||||
build_established_header, prepend_inner_header,
|
|
||||||
};
|
|
||||||
use crate::cache::CoordCache;
|
use crate::cache::CoordCache;
|
||||||
use crate::node::session::SessionEntry;
|
use crate::node::session::SessionEntry;
|
||||||
use crate::peer::{ActivePeer, PeerConnection};
|
use crate::peer::{ActivePeer, PeerConnection};
|
||||||
use crate::proto::bloom::{BloomFilter, BloomState};
|
use crate::proto::bloom::{BloomFilter, BloomState};
|
||||||
use crate::proto::fmp::Fmp;
|
use crate::proto::fmp::Fmp;
|
||||||
|
use crate::proto::fmp::wire::{
|
||||||
|
ESTABLISHED_HEADER_SIZE, FLAG_CE, FLAG_KEY_EPOCH, FLAG_SP, build_encrypted,
|
||||||
|
build_established_header, prepend_inner_header,
|
||||||
|
};
|
||||||
use crate::proto::fsp::Fsp;
|
use crate::proto::fsp::Fsp;
|
||||||
use crate::proto::lookup::{Lookup, LookupBackoff, LookupForwardRateLimiter};
|
use crate::proto::lookup::{Lookup, LookupBackoff, LookupForwardRateLimiter};
|
||||||
use crate::proto::mmp::Mmp;
|
use crate::proto::mmp::Mmp;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use super::*;
|
|||||||
use crate::ReceivedPacket;
|
use crate::ReceivedPacket;
|
||||||
use crate::node::acl::PeerAclReloader;
|
use crate::node::acl::PeerAclReloader;
|
||||||
use crate::node::reloadable::HostMapReloadable;
|
use crate::node::reloadable::HostMapReloadable;
|
||||||
use crate::node::wire::{build_msg1, build_msg2};
|
use crate::proto::fmp::wire::{build_msg1, build_msg2};
|
||||||
use crate::upper::hosts::HostMap;
|
use crate::upper::hosts::HostMap;
|
||||||
use crate::utils::index::SessionIndex;
|
use crate::utils::index::SessionIndex;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::EstablishedTraversal;
|
use crate::EstablishedTraversal;
|
||||||
use crate::config::{TransportInstances, UdpConfig};
|
use crate::config::{TransportInstances, UdpConfig};
|
||||||
use crate::node::wire::{PHASE_MSG1, PHASE_MSG2};
|
use crate::proto::fmp::wire::{PHASE_MSG1, PHASE_MSG2};
|
||||||
use crate::transport::udp::UdpTransport;
|
use crate::transport::udp::UdpTransport;
|
||||||
use crate::utils::index::IndexAllocator;
|
use crate::utils::index::IndexAllocator;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ fn craft_msg1_wire(
|
|||||||
sender_index: SessionIndex,
|
sender_index: SessionIndex,
|
||||||
ts: u64,
|
ts: u64,
|
||||||
) -> Vec<u8> {
|
) -> Vec<u8> {
|
||||||
use crate::node::wire::build_msg1;
|
use crate::proto::fmp::wire::build_msg1;
|
||||||
let peer_b_identity = PeerIdentity::from_pubkey_full(node.identity().pubkey_full());
|
let peer_b_identity = PeerIdentity::from_pubkey_full(node.identity().pubkey_full());
|
||||||
let link_id = LinkId::new(0x0BAD_C0DE);
|
let link_id = LinkId::new(0x0BAD_C0DE);
|
||||||
let mut conn = PeerConnection::outbound(link_id, peer_b_identity, ts);
|
let mut conn = PeerConnection::outbound(link_id, peer_b_identity, ts);
|
||||||
@@ -453,7 +453,7 @@ async fn chartest_msg1_at_cap_with_pending_outbound_bypasses_early_gate() {
|
|||||||
/// index it assigned while promoting the peer's msg1.
|
/// index it assigned while promoting the peer's msg1.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn chartest_cross_connection_tiebreak_winner_and_loser() {
|
async fn chartest_cross_connection_tiebreak_winner_and_loser() {
|
||||||
use crate::node::wire::build_msg1;
|
use crate::proto::fmp::wire::build_msg1;
|
||||||
|
|
||||||
let mut node_a = make_node();
|
let mut node_a = make_node();
|
||||||
let mut node_b = make_node();
|
let mut node_b = make_node();
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use super::*;
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_two_node_handshake_udp() {
|
async fn test_two_node_handshake_udp() {
|
||||||
use crate::config::UdpConfig;
|
use crate::config::UdpConfig;
|
||||||
use crate::node::wire::{
|
use crate::proto::fmp::wire::{
|
||||||
build_encrypted, build_established_header, build_msg1, prepend_inner_header,
|
build_encrypted, build_established_header, build_msg1, prepend_inner_header,
|
||||||
};
|
};
|
||||||
use crate::transport::udp::UdpTransport;
|
use crate::transport::udp::UdpTransport;
|
||||||
@@ -243,7 +243,7 @@ async fn test_two_node_handshake_udp() {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_run_rx_loop_handshake() {
|
async fn test_run_rx_loop_handshake() {
|
||||||
use crate::config::UdpConfig;
|
use crate::config::UdpConfig;
|
||||||
use crate::node::wire::build_msg1;
|
use crate::proto::fmp::wire::build_msg1;
|
||||||
use crate::transport::udp::UdpTransport;
|
use crate::transport::udp::UdpTransport;
|
||||||
use tokio::time::Duration;
|
use tokio::time::Duration;
|
||||||
|
|
||||||
@@ -434,7 +434,7 @@ async fn test_run_rx_loop_handshake() {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_cross_connection_both_initiate() {
|
async fn test_cross_connection_both_initiate() {
|
||||||
use crate::config::UdpConfig;
|
use crate::config::UdpConfig;
|
||||||
use crate::node::wire::build_msg1;
|
use crate::proto::fmp::wire::build_msg1;
|
||||||
use crate::transport::udp::UdpTransport;
|
use crate::transport::udp::UdpTransport;
|
||||||
use tokio::time::{Duration, timeout};
|
use tokio::time::{Duration, timeout};
|
||||||
|
|
||||||
@@ -788,7 +788,7 @@ async fn test_failed_connection_cleanup() {
|
|||||||
/// Test that msg1 bytes are stored on connection for resend.
|
/// Test that msg1 bytes are stored on connection for resend.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_msg1_stored_for_resend() {
|
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 mut node = make_node();
|
||||||
let transport_id = TransportId::new(1);
|
let transport_id = TransportId::new(1);
|
||||||
@@ -846,7 +846,7 @@ async fn test_resend_scheduling() {
|
|||||||
conn.set_source_addr(remote_addr.clone());
|
conn.set_source_addr(remote_addr.clone());
|
||||||
|
|
||||||
// Store msg1 with first resend at now + 1000ms
|
// 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);
|
conn.set_handshake_msg1(wire_msg1, now_ms + 1000);
|
||||||
|
|
||||||
let link = Link::connectionless(
|
let link = Link::connectionless(
|
||||||
@@ -924,7 +924,7 @@ fn test_resend_count_tracking() {
|
|||||||
/// Test that duplicate msg2 is silently dropped when pending_outbound is already cleared.
|
/// Test that duplicate msg2 is silently dropped when pending_outbound is already cleared.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_duplicate_msg2_dropped() {
|
async fn test_duplicate_msg2_dropped() {
|
||||||
use crate::node::wire::build_msg2;
|
use crate::proto::fmp::wire::build_msg2;
|
||||||
use crate::transport::ReceivedPacket;
|
use crate::transport::ReceivedPacket;
|
||||||
|
|
||||||
let mut node = make_node();
|
let mut node = make_node();
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ pub(super) async fn make_test_node_with_mtu(mtu: u16) -> TestNode {
|
|||||||
/// Sends msg1 over UDP. The drain loop will handle msg1 processing,
|
/// Sends msg1 over UDP. The drain loop will handle msg1 processing,
|
||||||
/// msg2 response, and subsequent TreeAnnounce exchange.
|
/// msg2 response, and subsequent TreeAnnounce exchange.
|
||||||
pub(super) async fn initiate_handshake(nodes: &mut [TestNode], i: usize, j: usize) {
|
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
|
// Extract responder info before mutably borrowing initiator
|
||||||
let responder_addr = nodes[j].addr.clone();
|
let responder_addr = nodes[j].addr.clone();
|
||||||
@@ -267,7 +267,7 @@ pub(super) fn print_tree_snapshot(label: &str, nodes: &[TestNode]) {
|
|||||||
///
|
///
|
||||||
/// Returns the number of packets processed.
|
/// Returns the number of packets processed.
|
||||||
pub(super) async fn process_available_packets(nodes: &mut [TestNode]) -> usize {
|
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,
|
COMMON_PREFIX_SIZE, CommonPrefix, FMP_VERSION, PHASE_ESTABLISHED, PHASE_MSG1, PHASE_MSG2,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1788,7 +1788,7 @@ async fn craft_and_send_msg1(
|
|||||||
addr_b: std::net::SocketAddr,
|
addr_b: std::net::SocketAddr,
|
||||||
timestamp_ms: u64,
|
timestamp_ms: u64,
|
||||||
) -> NodeAddr {
|
) -> NodeAddr {
|
||||||
use crate::node::wire::build_msg1;
|
use crate::proto::fmp::wire::build_msg1;
|
||||||
use crate::utils::index::SessionIndex;
|
use crate::utils::index::SessionIndex;
|
||||||
|
|
||||||
let peer_b_identity = PeerIdentity::from_pubkey_full(node_b.identity().pubkey_full());
|
let peer_b_identity = PeerIdentity::from_pubkey_full(node_b.identity().pubkey_full());
|
||||||
|
|||||||
@@ -1,626 +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 IK msg1 | 114 bytes | Handshake initiation |
|
|
||||||
//! | 0x2 | Noise IK msg2 | 69 bytes | Handshake response |
|
|
||||||
|
|
||||||
use crate::noise::{HANDSHAKE_MSG1_SIZE, HANDSHAKE_MSG2_SIZE, TAG_SIZE};
|
|
||||||
use crate::utils::index::SessionIndex;
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Constants
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// FMP protocol version (4 high bits of byte 0).
|
|
||||||
pub const FMP_VERSION: u8 = 0;
|
|
||||||
|
|
||||||
/// Phase value for established (encrypted) frames.
|
|
||||||
pub const PHASE_ESTABLISHED: u8 = 0x0;
|
|
||||||
|
|
||||||
/// Phase value for Noise IK message 1 (handshake initiation).
|
|
||||||
pub const PHASE_MSG1: u8 = 0x1;
|
|
||||||
|
|
||||||
/// Phase value for Noise IK message 2 (handshake response).
|
|
||||||
pub const PHASE_MSG2: u8 = 0x2;
|
|
||||||
|
|
||||||
/// 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 Noise IK message 1 wire packet: prefix + sender_idx + noise_msg1.
|
|
||||||
pub const MSG1_WIRE_SIZE: usize = COMMON_PREFIX_SIZE + 4 + HANDSHAKE_MSG1_SIZE; // 114 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; // 69 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 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 Noise IK message 1 header (phase 0x1).
|
|
||||||
///
|
|
||||||
/// Wire format (114 bytes):
|
|
||||||
/// ```text
|
|
||||||
/// [0x01][0x00][payload_len:2 LE][sender_idx:4 LE][noise_msg1:106]
|
|
||||||
/// ```
|
|
||||||
#[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 Noise IK message 2 header (phase 0x2).
|
|
||||||
///
|
|
||||||
/// Wire format (69 bytes):
|
|
||||||
/// ```text
|
|
||||||
/// [0x02][0x00][payload_len:2 LE][sender_idx:4 LE][receiver_idx:4 LE][noise_msg2:57]
|
|
||||||
/// ```
|
|
||||||
#[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 has wrong size or version/phase.
|
|
||||||
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.
|
|
||||||
#[cfg(test)]
|
|
||||||
pub fn noise_msg2<'a>(&self, data: &'a [u8]) -> &'a [u8] {
|
|
||||||
&data[self.noise_msg2_offset..]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Serialization Helpers
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Build a wire-format msg1 packet.
|
|
||||||
///
|
|
||||||
/// Format: `[0x01][0x00][payload_len:2 LE][sender_idx:4 LE][noise_msg1:106]`
|
|
||||||
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: `[0x02][0x00][payload_len:2 LE][sender_idx:4 LE][receiver_idx:4 LE][noise_msg2:57]`
|
|
||||||
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(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 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 = [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() {
|
|
||||||
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], 0x00); // ver=0, 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] = 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());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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], 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, 8);
|
|
||||||
assert_eq!(header.noise_msg1(&packet), &noise_msg1[..]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_msg1_header_wrong_size() {
|
|
||||||
let packet = vec![0x01; MSG1_WIRE_SIZE - 1];
|
|
||||||
assert!(Msg1Header::parse(&packet).is_none());
|
|
||||||
|
|
||||||
let packet = vec![0x01; 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] = 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());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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], 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, 12);
|
|
||||||
assert_eq!(header.noise_msg2(&packet), &noise_msg2[..]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_msg2_header_wrong_size() {
|
|
||||||
let packet = vec![0x02; MSG2_WIRE_SIZE - 1];
|
|
||||||
assert!(Msg2Header::parse(&packet).is_none());
|
|
||||||
|
|
||||||
let packet = vec![0x02; MSG2_WIRE_SIZE + 1];
|
|
||||||
assert!(Msg2Header::parse(&packet).is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_msg2_header_wrong_phase() {
|
|
||||||
let mut packet = vec![0x00; MSG2_WIRE_SIZE];
|
|
||||||
packet[0] = 0x00; // phase 0, not phase 2
|
|
||||||
assert!(Msg2Header::parse(&packet).is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_wire_sizes() {
|
|
||||||
assert_eq!(MSG1_WIRE_SIZE, 114); // 4 + 4 + 106
|
|
||||||
assert_eq!(MSG2_WIRE_SIZE, 69); // 4 + 4 + 4 + 57
|
|
||||||
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_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(106) = 110
|
|
||||||
assert_eq!(prefix.payload_len, 110);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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(57) = 65
|
|
||||||
assert_eq!(prefix.payload_len, 65);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-1
@@ -1567,7 +1567,7 @@ mod tests {
|
|||||||
plaintext: &[u8],
|
plaintext: &[u8],
|
||||||
k_bit: bool,
|
k_bit: bool,
|
||||||
) -> (Vec<u8>, u64, [u8; 16]) {
|
) -> (Vec<u8>, u64, [u8; 16]) {
|
||||||
use crate::node::wire::{FLAG_KEY_EPOCH, build_established_header};
|
use crate::proto::fmp::wire::{FLAG_KEY_EPOCH, build_established_header};
|
||||||
let counter = sender.current_send_counter();
|
let counter = sender.current_send_counter();
|
||||||
let flags = if k_bit { FLAG_KEY_EPOCH } else { 0 };
|
let flags = if k_bit { FLAG_KEY_EPOCH } else { 0 };
|
||||||
let header = build_established_header(receiver_idx, counter, flags, plaintext.len() as u16);
|
let header = build_established_header(receiver_idx, counter, flags, plaintext.len() as u16);
|
||||||
|
|||||||
@@ -21,12 +21,14 @@
|
|||||||
//! handles) and its [`HandshakeState`] phase enum, plus [`Fmp`], the
|
//! handles) and its [`HandshakeState`] phase enum, plus [`Fmp`], the
|
||||||
//! (stateless) lifecycle anchor owned by `Node`.
|
//! (stateless) lifecycle anchor owned by `Node`.
|
||||||
//! - `wire.rs` — the FMP link-framing codec: handshake message types,
|
//! - `wire.rs` — the FMP link-framing codec: handshake message types,
|
||||||
//! disconnect reasons, and the orderly disconnect message.
|
//! disconnect reasons, and the orderly disconnect message. Also carries the
|
||||||
|
//! FMP link wire framing relocated from `node/wire.rs` — the common prefix,
|
||||||
|
//! encrypted/msg1/msg2 headers, and the `build_*`/inner-header codec fns.
|
||||||
|
|
||||||
mod core;
|
mod core;
|
||||||
mod limits;
|
mod limits;
|
||||||
mod state;
|
mod state;
|
||||||
mod wire;
|
pub(crate) mod wire;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|||||||
+633
-1
@@ -1,14 +1,22 @@
|
|||||||
//! FMP link-framing messages: handshake message types and orderly disconnect.
|
//! FMP link-framing messages and link-layer wire codec.
|
||||||
//!
|
//!
|
||||||
//! The Noise IK handshake message-type discriminants and the orderly
|
//! The Noise IK handshake message-type discriminants and the orderly
|
||||||
//! disconnect codec, per the wire-migrates-with-subsystem policy.
|
//! disconnect codec, per the wire-migrates-with-subsystem policy.
|
||||||
//! `Disconnect::encode` reads the shared `LinkMessageType::Disconnect` catalog
|
//! `Disconnect::encode` reads the shared `LinkMessageType::Disconnect` catalog
|
||||||
//! variant (a downward `proto -> proto` dependency); the catalog itself lives
|
//! variant (a downward `proto -> proto` dependency); the catalog itself lives
|
||||||
//! in `crate::proto::link`.
|
//! in `crate::proto::link`.
|
||||||
|
//!
|
||||||
|
//! This module also carries the FMP mesh-layer packet-dispatch wire format
|
||||||
|
//! (the common prefix, encrypted/msg1/msg2 headers, and the `build_*`/
|
||||||
|
//! inner-header codec functions), relocated from `node/wire.rs` so all FMP
|
||||||
|
//! wire lives with its subsystem. See the "FMP link wire framing" section
|
||||||
|
//! banner below for the packet-type layout.
|
||||||
|
|
||||||
|
use crate::noise::{HANDSHAKE_MSG1_SIZE, HANDSHAKE_MSG2_SIZE, TAG_SIZE};
|
||||||
use crate::proto::Error;
|
use crate::proto::Error;
|
||||||
use crate::proto::codec::Reader;
|
use crate::proto::codec::Reader;
|
||||||
use crate::proto::link::LinkMessageType;
|
use crate::proto::link::LinkMessageType;
|
||||||
|
use crate::utils::index::SessionIndex;
|
||||||
use ::core::fmt;
|
use ::core::fmt;
|
||||||
|
|
||||||
/// Handshake message type identifiers.
|
/// Handshake message type identifiers.
|
||||||
@@ -160,3 +168,627 @@ impl Disconnect {
|
|||||||
Ok(Self { reason })
|
Ok(Self { reason })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 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 IK msg1 | 114 bytes | Handshake initiation |
|
||||||
|
// | 0x2 | Noise IK msg2 | 69 bytes | Handshake response |
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Constants
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// FMP protocol version (4 high bits of byte 0).
|
||||||
|
pub const FMP_VERSION: u8 = 0;
|
||||||
|
|
||||||
|
/// Phase value for established (encrypted) frames.
|
||||||
|
pub const PHASE_ESTABLISHED: u8 = 0x0;
|
||||||
|
|
||||||
|
/// Phase value for Noise IK message 1 (handshake initiation).
|
||||||
|
pub const PHASE_MSG1: u8 = 0x1;
|
||||||
|
|
||||||
|
/// Phase value for Noise IK message 2 (handshake response).
|
||||||
|
pub const PHASE_MSG2: u8 = 0x2;
|
||||||
|
|
||||||
|
/// 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 Noise IK message 1 wire packet: prefix + sender_idx + noise_msg1.
|
||||||
|
pub const MSG1_WIRE_SIZE: usize = COMMON_PREFIX_SIZE + 4 + HANDSHAKE_MSG1_SIZE; // 114 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; // 69 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 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 Noise IK message 1 header (phase 0x1).
|
||||||
|
///
|
||||||
|
/// Wire format (114 bytes):
|
||||||
|
/// ```text
|
||||||
|
/// [0x01][0x00][payload_len:2 LE][sender_idx:4 LE][noise_msg1:106]
|
||||||
|
/// ```
|
||||||
|
#[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 Noise IK message 2 header (phase 0x2).
|
||||||
|
///
|
||||||
|
/// Wire format (69 bytes):
|
||||||
|
/// ```text
|
||||||
|
/// [0x02][0x00][payload_len:2 LE][sender_idx:4 LE][receiver_idx:4 LE][noise_msg2:57]
|
||||||
|
/// ```
|
||||||
|
#[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 has wrong size or version/phase.
|
||||||
|
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.
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn noise_msg2<'a>(&self, data: &'a [u8]) -> &'a [u8] {
|
||||||
|
&data[self.noise_msg2_offset..]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Serialization Helpers
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Build a wire-format msg1 packet.
|
||||||
|
///
|
||||||
|
/// Format: `[0x01][0x00][payload_len:2 LE][sender_idx:4 LE][noise_msg1:106]`
|
||||||
|
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: `[0x02][0x00][payload_len:2 LE][sender_idx:4 LE][receiver_idx:4 LE][noise_msg2:57]`
|
||||||
|
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(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 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 = [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() {
|
||||||
|
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], 0x00); // ver=0, 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] = 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());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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], 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, 8);
|
||||||
|
assert_eq!(header.noise_msg1(&packet), &noise_msg1[..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_msg1_header_wrong_size() {
|
||||||
|
let packet = vec![0x01; MSG1_WIRE_SIZE - 1];
|
||||||
|
assert!(Msg1Header::parse(&packet).is_none());
|
||||||
|
|
||||||
|
let packet = vec![0x01; 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] = 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());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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], 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, 12);
|
||||||
|
assert_eq!(header.noise_msg2(&packet), &noise_msg2[..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_msg2_header_wrong_size() {
|
||||||
|
let packet = vec![0x02; MSG2_WIRE_SIZE - 1];
|
||||||
|
assert!(Msg2Header::parse(&packet).is_none());
|
||||||
|
|
||||||
|
let packet = vec![0x02; MSG2_WIRE_SIZE + 1];
|
||||||
|
assert!(Msg2Header::parse(&packet).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_msg2_header_wrong_phase() {
|
||||||
|
let mut packet = vec![0x00; MSG2_WIRE_SIZE];
|
||||||
|
packet[0] = 0x00; // phase 0, not phase 2
|
||||||
|
assert!(Msg2Header::parse(&packet).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_wire_sizes() {
|
||||||
|
assert_eq!(MSG1_WIRE_SIZE, 114); // 4 + 4 + 106
|
||||||
|
assert_eq!(MSG2_WIRE_SIZE, 69); // 4 + 4 + 4 + 57
|
||||||
|
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_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(106) = 110
|
||||||
|
assert_eq!(prefix.payload_len, 110);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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(57) = 65
|
||||||
|
assert_eq!(prefix.payload_len, 65);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user