From 8357200b0ef8e40424a179413630f18e0f022a28 Mon Sep 17 00:00:00 2001 From: Johnathan Corgan Date: Fri, 3 Apr 2026 12:42:58 +0000 Subject: [PATCH] Remove IK/XK dead code, rename XX methods as sole pattern - Delete PROTOCOL_NAME_IK, PROTOCOL_NAME_XK and all IK/XK methods - Remove Ik/Xk variants from NoisePattern enum - Rename XX methods to drop xx_ prefix (sole pattern) - Rename HandshakeMessageType variants from NoiseIKMsg1/NoiseIKMsg2 to Msg1/Msg2, add Msg3 variant (0x03) for XX 3-message flow - Fix doc comments with correct XX message sizes and descriptions - Fix stale SessionSetup/SessionAck handshake payload size comments --- src/node/handlers/handshake.rs | 2 +- src/node/handlers/rekey.rs | 14 +- src/node/handlers/session.rs | 80 +-- src/node/lifecycle.rs | 2 +- src/node/session.rs | 14 +- src/node/session_wire.rs | 10 +- src/node/tests/disconnect.rs | 25 +- src/node/tests/discovery.rs | 2 +- src/node/tests/handshake.rs | 2 +- src/node/tests/mod.rs | 2 +- src/node/tests/session.rs | 72 +-- src/node/wire.rs | 32 +- src/noise/handshake.rs | 741 ++--------------------- src/noise/mod.rs | 87 +-- src/noise/tests.rs | 1016 +++++++------------------------- src/peer/active.rs | 10 +- src/peer/connection.rs | 20 +- src/protocol/link.rs | 41 +- src/protocol/mod.rs | 2 +- src/protocol/session.rs | 26 +- src/transport/ble/discovery.rs | 2 +- src/transport/ble/mod.rs | 4 +- 22 files changed, 431 insertions(+), 1775 deletions(-) diff --git a/src/node/handlers/handshake.rs b/src/node/handlers/handshake.rs index 4a96beb1..37deba5f 100644 --- a/src/node/handlers/handshake.rs +++ b/src/node/handlers/handshake.rs @@ -736,7 +736,7 @@ impl Node { // Identity-based restart/rekey detection. // // Now that we know the initiator's identity from msg3, perform the - // same checks that IK handle_msg1 used to do after decrypting msg1. + // same checks that the old handle_msg1 used to do after decrypting msg1. if let Some(existing_peer) = self.peers.get(&peer_node_addr) { let new_epoch = conn.remote_epoch(); let existing_epoch = existing_peer.remote_epoch(); diff --git a/src/node/handlers/rekey.rs b/src/node/handlers/rekey.rs index 9d052dc2..116586c7 100644 --- a/src/node/handlers/rekey.rs +++ b/src/node/handlers/rekey.rs @@ -20,7 +20,7 @@ const DRAIN_WINDOW_SECS: u64 = 10; const REKEY_DAMPENING_SECS: u64 = 30; /// Delay FSP initiator cutover after handshake completion to allow -/// XK msg3 to reach the responder before K-bit-flipped data arrives. +/// XX msg3 to reach the responder before K-bit-flipped data arrives. const FSP_CUTOVER_DELAY_MS: u64 = 2000; impl Node { @@ -162,10 +162,10 @@ impl Node { // Create XX initiator handshake directly (no PeerConnection) let our_keypair = self.identity.keypair(); - let mut hs = HandshakeState::new_xx_initiator(our_keypair); + let mut hs = HandshakeState::new_initiator(our_keypair); hs.set_local_epoch(self.startup_epoch); - let noise_msg1 = match hs.write_xx_message_1() { + let noise_msg1 = match hs.write_message_1() { Ok(msg) => msg, Err(e) => { warn!( @@ -269,7 +269,7 @@ impl Node { /// For each established session: /// - If the initiator has a pending session, perform K-bit cutover /// - If the drain window has expired, clean up the previous session - /// - If the rekey timer/counter fires, initiate a new XK handshake + /// - If the rekey timer/counter fires, initiate a new XX handshake pub(in crate::node) async fn check_session_rekey(&mut self) { if !self.config.node.rekey.enabled { return; @@ -358,7 +358,7 @@ impl Node { /// Initiate an FSP session rekey. /// - /// Creates a new XK handshake as initiator, sends SessionSetup msg1 + /// Creates a new XX handshake as initiator, sends SessionSetup msg1 /// through the mesh, and stores the handshake state on the existing entry. async fn initiate_session_rekey(&mut self, dest_addr: &NodeAddr) { // Check route availability before paying crypto cost @@ -378,10 +378,10 @@ impl Node { // Create Noise XX initiator handshake (rekey: no negotiation payload) let our_keypair = self.identity.keypair(); - let mut handshake = HandshakeState::new_xx_initiator(our_keypair); + let mut handshake = HandshakeState::new_initiator(our_keypair); handshake.set_local_epoch(self.startup_epoch); - let msg1 = match handshake.write_xx_message_1() { + let msg1 = match handshake.write_message_1() { Ok(m) => m, Err(e) => { warn!( diff --git a/src/node/handlers/session.rs b/src/node/handlers/session.rs index b58d8074..94a0c2e3 100644 --- a/src/node/handlers/session.rs +++ b/src/node/handlers/session.rs @@ -15,7 +15,7 @@ use crate::node::session_wire::{ use crate::protocol::{coords_wire_size, encode_coords}; use crate::upper::icmp::FIPS_OVERHEAD; use crate::node::{Node, NodeError}; -use crate::noise::{HandshakeState, XX_HANDSHAKE_MSG1_SIZE, XX_HANDSHAKE_MSG2_SIZE, XX_HANDSHAKE_MSG3_SIZE}; +use crate::noise::{HandshakeState, HANDSHAKE_MSG1_SIZE, HANDSHAKE_MSG2_SIZE, HANDSHAKE_MSG3_SIZE}; use crate::protocol::NegotiationPayload; use crate::mmp::report::ReceiverReport; use crate::mmp::{MAX_SESSION_REPORT_INTERVAL_MS, MIN_SESSION_REPORT_INTERVAL_MS}; @@ -153,7 +153,7 @@ impl Node { } }; // Drop encrypted data if session is not yet established. - // With XK, the responder must wait for msg3 before it can decrypt. + // With XX, the responder must wait for msg3 before it can decrypt. if !entry.is_established() { debug!( src = %self.peer_display_name(src_addr), @@ -357,7 +357,7 @@ impl Node { /// Handle an incoming SessionSetup (Noise XX msg1). /// /// The remote node wants to establish an end-to-end session with us. - /// We create an XK responder handshake, process msg1, send SessionAck with msg2, + /// We create an XX responder handshake, process msg1, send SessionAck with msg2, /// and transition to AwaitingMsg3. async fn handle_session_setup(&mut self, src_addr: &NodeAddr, inner: &[u8]) { let setup = match SessionSetup::decode(inner) { @@ -368,10 +368,10 @@ impl Node { } }; - if setup.handshake_payload.len() != XX_HANDSHAKE_MSG1_SIZE { + if setup.handshake_payload.len() != HANDSHAKE_MSG1_SIZE { debug!( len = setup.handshake_payload.len(), - expected = XX_HANDSHAKE_MSG1_SIZE, + expected = HANDSHAKE_MSG1_SIZE, "Invalid handshake payload size in SessionSetup" ); return; @@ -443,16 +443,16 @@ impl Node { return; } let our_keypair = self.identity.keypair(); - let mut handshake = HandshakeState::new_xx_responder(our_keypair); + let mut handshake = HandshakeState::new_responder(our_keypair); handshake.set_local_epoch(self.startup_epoch); - if let Err(e) = handshake.read_xx_message_1(&setup.handshake_payload) { + if let Err(e) = handshake.read_message_1(&setup.handshake_payload) { debug!(error = %e, "Failed to process rekey XX msg1"); return; } // Generate msg2 - let msg2 = match handshake.write_xx_message_2() { + let msg2 = match handshake.write_message_2() { Ok(m) => m, Err(e) => { debug!(error = %e, "Failed to generate rekey XX msg2"); @@ -491,22 +491,22 @@ impl Node { } } - // Create XK responder handshake and process msg1 + // Create XX responder handshake and process msg1 let our_keypair = self.identity.keypair(); - let mut handshake = HandshakeState::new_xx_responder(our_keypair); + let mut handshake = HandshakeState::new_responder(our_keypair); handshake.set_local_epoch(self.startup_epoch); - if let Err(e) = handshake.read_xx_message_1(&setup.handshake_payload) { + if let Err(e) = handshake.read_message_1(&setup.handshake_payload) { debug!(error = %e, "Failed to process Noise XX msg1 in SessionSetup"); return; } - // XK: responder does NOT learn initiator's identity until msg3 + // XX: responder does NOT learn initiator's identity until msg3 // Use a placeholder pubkey from src_addr for the session entry. // The real pubkey will be registered when msg3 arrives. // Generate msg2 with negotiation payload - let mut msg2 = match handshake.write_xx_message_2() { + let mut msg2 = match handshake.write_message_2() { Ok(m) => m, Err(e) => { debug!(error = %e, "Failed to generate Noise XX msg2 for SessionAck"); @@ -563,10 +563,10 @@ impl Node { } }; - if ack.handshake_payload.len() < XX_HANDSHAKE_MSG2_SIZE { + if ack.handshake_payload.len() < HANDSHAKE_MSG2_SIZE { debug!( len = ack.handshake_payload.len(), - min = XX_HANDSHAKE_MSG2_SIZE, + min = HANDSHAKE_MSG2_SIZE, "Handshake payload too short in SessionAck" ); return; @@ -591,16 +591,16 @@ impl Node { } }; - // Process XK msg2 - if let Err(e) = handshake.read_xx_message_2(&ack.handshake_payload) { + // Process XX msg2 + if let Err(e) = handshake.read_message_2(&ack.handshake_payload) { debug!(error = %e, "Failed to process rekey XX msg2"); entry.abandon_rekey(); self.sessions.insert(*src_addr, entry); return; } - // Generate XK msg3 - let msg3 = match handshake.write_xx_message_3() { + // Generate XX msg3 + let msg3 = match handshake.write_message_3() { Ok(m) => m, Err(e) => { debug!(error = %e, "Failed to generate rekey XX msg3"); @@ -641,7 +641,7 @@ impl Node { debug!( src = %self.peer_display_name(src_addr), - "FSP rekey: completed XK as initiator, pending cutover" + "FSP rekey: completed XX as initiator, pending cutover" ); return; } @@ -658,14 +658,14 @@ impl Node { }; // Split msg2 into base XX part and optional negotiation payload - let (base_msg2, neg_bytes) = if ack.handshake_payload.len() > XX_HANDSHAKE_MSG2_SIZE { - (&ack.handshake_payload[..XX_HANDSHAKE_MSG2_SIZE], Some(&ack.handshake_payload[XX_HANDSHAKE_MSG2_SIZE..])) + let (base_msg2, neg_bytes) = if ack.handshake_payload.len() > HANDSHAKE_MSG2_SIZE { + (&ack.handshake_payload[..HANDSHAKE_MSG2_SIZE], Some(&ack.handshake_payload[HANDSHAKE_MSG2_SIZE..])) } else { (ack.handshake_payload.as_slice(), None) }; // Process XX msg2 (learns responder's identity and epoch) - if let Err(e) = handshake.read_xx_message_2(base_msg2) { + if let Err(e) = handshake.read_message_2(base_msg2) { debug!(error = %e, "Failed to process Noise XX msg2 in SessionAck"); return; } @@ -698,7 +698,7 @@ impl Node { } // Generate XX msg3 with negotiation payload - let mut msg3 = match handshake.write_xx_message_3() { + let mut msg3 = match handshake.write_message_3() { Ok(m) => m, Err(e) => { debug!(error = %e, "Failed to generate Noise XX msg3"); @@ -732,7 +732,7 @@ impl Node { let session = match handshake.into_session() { Ok(s) => s, Err(e) => { - debug!(error = %e, "Failed to create session after XK msg3"); + debug!(error = %e, "Failed to create session after XX msg3"); return; } }; @@ -750,7 +750,7 @@ impl Node { // Flush any queued outbound packets for this destination self.flush_pending_packets(src_addr).await; - info!(src = %self.peer_display_name(src_addr), "Session established (initiator, XK)"); + info!(src = %self.peer_display_name(src_addr), "Session established (initiator, XX)"); } /// Handle an incoming SessionMsg3 (Noise XX msg3). @@ -767,10 +767,10 @@ impl Node { } }; - if msg3.handshake_payload.len() < XX_HANDSHAKE_MSG3_SIZE { + if msg3.handshake_payload.len() < HANDSHAKE_MSG3_SIZE { debug!( len = msg3.handshake_payload.len(), - min = XX_HANDSHAKE_MSG3_SIZE, + min = HANDSHAKE_MSG3_SIZE, "Handshake payload too short in SessionMsg3" ); return; @@ -795,8 +795,8 @@ impl Node { } }; - // Process XK msg3 - if let Err(e) = handshake.read_xx_message_3(&msg3.handshake_payload) { + // Process XX msg3 + if let Err(e) = handshake.read_message_3(&msg3.handshake_payload) { debug!(error = %e, "Failed to process rekey XX msg3"); entry.abandon_rekey(); self.sessions.insert(*src_addr, entry); @@ -819,7 +819,7 @@ impl Node { debug!( src = %self.peer_display_name(src_addr), - "FSP rekey: completed XK as responder, pending cutover" + "FSP rekey: completed XX as responder, pending cutover" ); return; } @@ -836,14 +836,14 @@ impl Node { }; // Split msg3 into base XX part and optional negotiation payload - let (base_msg3, neg_bytes) = if msg3.handshake_payload.len() > XX_HANDSHAKE_MSG3_SIZE { - (&msg3.handshake_payload[..XX_HANDSHAKE_MSG3_SIZE], Some(&msg3.handshake_payload[XX_HANDSHAKE_MSG3_SIZE..])) + let (base_msg3, neg_bytes) = if msg3.handshake_payload.len() > HANDSHAKE_MSG3_SIZE { + (&msg3.handshake_payload[..HANDSHAKE_MSG3_SIZE], Some(&msg3.handshake_payload[HANDSHAKE_MSG3_SIZE..])) } else { (msg3.handshake_payload.as_slice(), None) }; // Process XX msg3 (learns initiator's identity and epoch) - if let Err(e) = handshake.read_xx_message_3(base_msg3) { + if let Err(e) = handshake.read_message_3(base_msg3) { debug!(error = %e, "Failed to process Noise XX msg3"); return; } @@ -865,7 +865,7 @@ impl Node { let remote_pubkey = match handshake.remote_static() { Some(pk) => *pk, None => { - debug!("No remote static key after processing XK msg3"); + debug!("No remote static key after processing XX msg3"); return; } }; @@ -877,7 +877,7 @@ impl Node { let session = match handshake.into_session() { Ok(s) => s, Err(e) => { - debug!(error = %e, "Failed to create session from XK handshake"); + debug!(error = %e, "Failed to create session from XX handshake"); return; } }; @@ -894,7 +894,7 @@ impl Node { // Flush any pending packets self.flush_pending_packets(src_addr).await; - info!(src = %self.peer_display_name(src_addr), "Session established (responder, XK)"); + info!(src = %self.peer_display_name(src_addr), "Session established (responder, XX)"); } // === Session-layer MMP report handlers === @@ -1126,7 +1126,7 @@ impl Node { // Trigger re-discovery to get fresh coordinates, but only if we have // the target's identity cached — otherwise we can't verify the - // LookupResponse proof. This avoids a race when the XK responder + // LookupResponse proof. This avoids a race when the XX responder // receives PathBroken before msg3 completes (identity unknown). if self.has_cached_identity(&msg.dest_addr) { self.maybe_initiate_lookup(&msg.dest_addr).await; @@ -1212,9 +1212,9 @@ impl Node { // Create Noise XX initiator handshake let our_keypair = self.identity.keypair(); - let mut handshake = HandshakeState::new_xx_initiator(our_keypair); + let mut handshake = HandshakeState::new_initiator(our_keypair); handshake.set_local_epoch(self.startup_epoch); - let msg1 = handshake.write_xx_message_1().map_err(|e| NodeError::SendFailed { + let msg1 = handshake.write_message_1().map_err(|e| NodeError::SendFailed { node_addr: dest_addr, reason: format!("Noise XX msg1 generation failed: {}", e), })?; diff --git a/src/node/lifecycle.rs b/src/node/lifecycle.rs index 77beb95a..4dd61923 100644 --- a/src/node/lifecycle.rs +++ b/src/node/lifecycle.rs @@ -190,7 +190,7 @@ impl Node { /// Initiate a connection to a peer on a specific transport and address. /// /// For connectionless transports (UDP, Ethernet): allocates a link, starts - /// the Noise IK handshake, sends msg1, and registers the connection for + /// the Noise XX handshake, sends msg1, and registers the connection for /// msg2 dispatch. /// /// For connection-oriented transports (TCP, Tor): allocates a link and diff --git a/src/node/session.rs b/src/node/session.rs index 0f63dee7..93427a5a 100644 --- a/src/node/session.rs +++ b/src/node/session.rs @@ -1,7 +1,7 @@ //! End-to-end session state. //! -//! Tracks Noise XK sessions between this node and remote endpoints. -//! Sessions are established via a three-message XK handshake +//! Tracks Noise XX sessions between this node and remote endpoints. +//! Sessions are established via a three-message XX handshake //! (SessionSetup/SessionAck/SessionMsg3) carried inside SessionDatagram //! envelopes through the mesh. @@ -15,9 +15,9 @@ use secp256k1::PublicKey; /// State machine for an end-to-end session. pub(crate) enum EndToEndState { - /// We initiated: sent SessionSetup with Noise XK msg1, awaiting SessionAck. + /// We initiated: sent SessionSetup with Noise XX msg1, awaiting SessionAck. Initiating(HandshakeState), - /// XK responder: processed msg1, sent msg2, awaiting msg3. + /// XX responder: processed msg1, sent msg2, awaiting msg3. /// Transitions to Established when msg3 arrives. AwaitingMsg3(HandshakeState), /// Handshake complete, NoiseSession available for encrypt/decrypt. @@ -35,7 +35,7 @@ impl EndToEndState { matches!(self, EndToEndState::Initiating(_)) } - /// Check if we are an XK responder awaiting msg3. + /// Check if we are an XX responder awaiting msg3. pub(crate) fn is_awaiting_msg3(&self) -> bool { matches!(self, EndToEndState::AwaitingMsg3(_)) } @@ -202,7 +202,7 @@ impl SessionEntry { self.state.as_ref().is_some_and(|s| s.is_initiating()) } - /// Check if we are an XK responder awaiting msg3. + /// Check if we are an XX responder awaiting msg3. pub(crate) fn is_awaiting_msg3(&self) -> bool { self.state.as_ref().is_some_and(|s| s.is_awaiting_msg3()) } @@ -400,7 +400,7 @@ impl SessionEntry { self.rekey_state = None; } - /// Set the rekey handshake state (in-progress XK handshake). + /// Set the rekey handshake state (in-progress XX handshake). pub(crate) fn set_rekey_state(&mut self, state: HandshakeState, is_initiator: bool) { self.rekey_state = Some(state); self.rekey_initiator = is_initiator; diff --git a/src/node/session_wire.rs b/src/node/session_wire.rs index 46f32abd..7cff06fe 100644 --- a/src/node/session_wire.rs +++ b/src/node/session_wire.rs @@ -28,9 +28,9 @@ //! |-------|--------|------------------|-----------------------------------| //! | 0x0 | 0 | Encrypted | Post-handshake encrypted data | //! | 0x0 | 1 | Plaintext error | CoordsRequired, PathBroken | -//! | 0x1 | - | Handshake msg1 | SessionSetup (Noise XK msg1) | -//! | 0x2 | - | Handshake msg2 | SessionAck (Noise XK msg2) | -//! | 0x3 | - | Handshake msg3 | SessionMsg3 (Noise XK msg3) | +//! | 0x1 | - | Handshake msg1 | SessionSetup (Noise XX msg1) | +//! | 0x2 | - | Handshake msg2 | SessionAck (Noise XX msg2) | +//! | 0x3 | - | Handshake msg3 | SessionMsg3 (Noise XX msg3) | use crate::protocol::{ProtocolError, decode_optional_coords}; use crate::tree::TreeCoordinate; @@ -45,13 +45,13 @@ pub const FSP_VERSION: u8 = 0; /// Phase value for established (encrypted or plaintext error) messages. pub const FSP_PHASE_ESTABLISHED: u8 = 0x0; -/// Phase value for SessionSetup (Noise IK message 1). +/// Phase value for SessionSetup (Noise XX message 1). pub const FSP_PHASE_MSG1: u8 = 0x1; /// Phase value for SessionAck (Noise handshake message 2). pub const FSP_PHASE_MSG2: u8 = 0x2; -/// Phase value for XK message 3 (initiator's encrypted static). +/// Phase value for XX message 3 (initiator's encrypted static). pub const FSP_PHASE_MSG3: u8 = 0x3; /// Size of the common packet prefix (all FSP message types). diff --git a/src/node/tests/disconnect.rs b/src/node/tests/disconnect.rs index 03389da1..96a0cf4c 100644 --- a/src/node/tests/disconnect.rs +++ b/src/node/tests/disconnect.rs @@ -253,13 +253,12 @@ async fn test_disconnect_clears_session() { let node1_addr = *nodes[1].node.node_addr(); // Inject a synthetic Established session entry into node 1's session table - // to simulate the state after a completed XK handshake with node 0. + // to simulate the state after a completed XX handshake with node 0. let remote_identity = Identity::generate(); { let our_identity = nodes[1].node.identity(); - let mut initiator = - HandshakeState::new_initiator(our_identity.keypair(), remote_identity.pubkey_full()); + let mut initiator = HandshakeState::new_initiator(our_identity.keypair()); let mut responder = HandshakeState::new_responder(remote_identity.keypair()); let mut init_epoch = [0u8; 8]; rand::Rng::fill_bytes(&mut rand::rng(), &mut init_epoch); @@ -271,6 +270,8 @@ async fn test_disconnect_clears_session() { responder.read_message_1(&msg1).unwrap(); let msg2 = responder.write_message_2().unwrap(); initiator.read_message_2(&msg2).unwrap(); + let msg3 = initiator.write_message_3().unwrap(); + responder.read_message_3(&msg3).unwrap(); let session = initiator.into_session().unwrap(); let entry = SessionEntry::new( @@ -283,16 +284,8 @@ async fn test_disconnect_clears_session() { nodes[1].node.sessions.insert(node0_addr, entry); } - assert_eq!( - nodes[1].node.session_count(), - 1, - "Session should exist before disconnect" - ); - assert_eq!( - nodes[1].node.peer_count(), - 1, - "Peer should exist before disconnect" - ); + assert_eq!(nodes[1].node.session_count(), 1, "Session should exist before disconnect"); + assert_eq!(nodes[1].node.peer_count(), 1, "Peer should exist before disconnect"); // Node 0 sends Disconnect to node 1. let disconnect = crate::protocol::Disconnect::new(DisconnectReason::Shutdown); @@ -307,8 +300,7 @@ async fn test_disconnect_clears_session() { // Peer must be gone. assert_eq!( - nodes[1].node.peer_count(), - 0, + nodes[1].node.peer_count(), 0, "Peer should be removed after disconnect" ); @@ -316,8 +308,7 @@ async fn test_disconnect_clears_session() { // Before the fix, session_count() would still be 1 here because // remove_active_peer didn't remove self.sessions[node0_addr]. assert_eq!( - nodes[1].node.session_count(), - 0, + nodes[1].node.session_count(), 0, "Session must be cleaned up when peer is removed (regression: issue #5)" ); diff --git a/src/node/tests/discovery.rs b/src/node/tests/discovery.rs index 905eb1e9..7f8e80f7 100644 --- a/src/node/tests/discovery.rs +++ b/src/node/tests/discovery.rs @@ -240,7 +240,7 @@ async fn test_response_proof_verification_failure() { #[tokio::test] async fn test_response_identity_cache_miss() { // Verify that a response is discarded when the origin lacks the - // target's pubkey in identity_cache (e.g., XK responder before msg3). + // target's pubkey in identity_cache (e.g., XX responder before msg3). let mut node = make_node(); let from = make_node_addr(0xAA); diff --git a/src/node/tests/handshake.rs b/src/node/tests/handshake.rs index 7c6c31de..5f9d4435 100644 --- a/src/node/tests/handshake.rs +++ b/src/node/tests/handshake.rs @@ -63,7 +63,7 @@ async fn test_two_node_handshake_udp() { // Allocate session index for A's outbound let our_index_a = node_a.index_allocator.allocate().unwrap(); - // Start handshake (generates Noise IK msg1) + // Start handshake (generates Noise XX msg1) let our_keypair_a = node_a.identity.keypair(); let noise_msg1 = conn_a.start_handshake(our_keypair_a, node_a.startup_epoch, 1000).unwrap(); conn_a.set_our_index(our_index_a); diff --git a/src/node/tests/mod.rs b/src/node/tests/mod.rs index 57f5c9e0..02f58360 100644 --- a/src/node/tests/mod.rs +++ b/src/node/tests/mod.rs @@ -35,7 +35,7 @@ pub(super) fn make_peer_identity() -> PeerIdentity { PeerIdentity::from_pubkey(identity.pubkey()) } -/// Create a PeerConnection with a completed Noise IK handshake. +/// Create a PeerConnection with a completed Noise XX handshake. /// /// Returns (connection, peer_identity) where the connection is outbound, /// in Complete state, with session, indices, and transport info set. diff --git a/src/node/tests/session.rs b/src/node/tests/session.rs index 82233579..06f3b8ec 100644 --- a/src/node/tests/session.rs +++ b/src/node/tests/session.rs @@ -50,10 +50,7 @@ fn test_session_entry_new_initiating() { let identity_a = Identity::generate(); let identity_b = Identity::generate(); - let handshake = HandshakeState::new_initiator( - identity_a.keypair(), - identity_b.pubkey_full(), - ); + let handshake = HandshakeState::new_initiator(identity_a.keypair()); let entry = crate::node::session::SessionEntry::new( *identity_b.node_addr(), @@ -77,10 +74,7 @@ fn test_session_entry_touch() { let identity_a = Identity::generate(); let identity_b = Identity::generate(); - let handshake = HandshakeState::new_initiator( - identity_a.keypair(), - identity_b.pubkey_full(), - ); + let handshake = HandshakeState::new_initiator(identity_a.keypair()); let mut entry = crate::node::session::SessionEntry::new( *identity_b.node_addr(), @@ -102,10 +96,7 @@ fn test_session_table_operations() { let mut node = make_node(); let identity_b = Identity::generate(); - let handshake = HandshakeState::new_initiator( - node.identity().keypair(), - identity_b.pubkey_full(), - ); + let handshake = HandshakeState::new_initiator(node.identity().keypair()); let dest_addr = *identity_b.node_addr(); let entry = crate::node::session::SessionEntry::new( @@ -163,7 +154,7 @@ async fn test_session_direct_peer_handshake() { let count = process_available_packets(&mut nodes).await; assert!(count > 0, "Expected SessionSetup packet to arrive"); - // Node 1 should now have a session in AwaitingMsg3 state (XK: identity not yet known) + // Node 1 should now have a session in AwaitingMsg3 state (XX: identity not yet known) assert_eq!(nodes[1].node.session_count(), 1); assert!(nodes[1] .node @@ -213,7 +204,7 @@ async fn test_session_direct_peer_data_transfer() { let node1_addr = *nodes[1].node.node_addr(); let node1_pubkey = nodes[1].node.identity().pubkey_full(); - // Establish session (XK: 3 messages — Setup, Ack, Msg3) + // Establish session (XX: 3 messages — Setup, Ack, Msg3) nodes[0] .node .initiate_session(node1_addr, node1_pubkey) @@ -286,7 +277,7 @@ async fn test_session_3node_forwarded_handshake() { tokio::time::sleep(Duration::from_millis(20)).await; process_available_packets(&mut nodes).await; - // Node 2 should have an AwaitingMsg3 session (XK: identity not yet known) + // Node 2 should have an AwaitingMsg3 session (XX: identity not yet known) assert!( nodes[2].node.get_session(&node0_addr).is_some(), "Node 2 should have a session entry for Node 0" @@ -388,7 +379,7 @@ async fn test_session_3node_forwarded_data() { process_available_packets(&mut nodes).await; } - // Node 2 should be Established (transitioned during XK handshake msg3) + // Node 2 should be Established (transitioned during XX handshake msg3) assert!(nodes[2] .node .get_session(&node0_addr) @@ -637,7 +628,7 @@ async fn test_session_100_nodes() { drain_to_quiescence(&mut nodes).await; // Reverse: responder → initiator - // (Responder should already be Established after XK msg3) + // (Responder should already be Established after XX msg3) let rev_payload = format!("rev-{}", pair_idx).into_bytes(); let rev_ipv6 = build_ipv6_packet(&dst_fips, &src_fips, &rev_payload); match nodes[dst] @@ -890,7 +881,7 @@ async fn test_session_100_nodes() { ); assert_eq!( send_reverse_err, 0, - "All reverse sends should succeed (responder Established after XK msg3)" + "All reverse sends should succeed (responder Established after XX msg3)" ); assert_eq!( fwd_delivered, send_forward_ok, @@ -985,7 +976,7 @@ async fn test_tun_outbound_established_session() { let src_fips = crate::FipsAddress::from_node_addr(&node0_addr); let dst_fips = crate::FipsAddress::from_node_addr(&node1_addr); - // Establish session (XK: 3 messages — Setup, Ack, Msg3) + // Establish session (XX: 3 messages — Setup, Ack, Msg3) nodes[0].node.initiate_session(node1_addr, node1_pubkey).await.unwrap(); tokio::time::sleep(Duration::from_millis(20)).await; process_available_packets(&mut nodes).await; // Setup → Node 1 @@ -1191,17 +1182,14 @@ async fn test_tun_outbound_pending_queue_flush() { // Unit tests: Session idle timeout // ============================================================================ -/// Helper: complete a Noise IK handshake and return the initiator's NoiseSession. +/// Helper: complete a Noise XX handshake and return the initiator's NoiseSession. fn make_noise_session( our_identity: &Identity, remote_identity: &Identity, ) -> crate::noise::NoiseSession { use crate::noise::HandshakeState; - let mut initiator = HandshakeState::new_initiator( - our_identity.keypair(), - remote_identity.pubkey_full(), - ); + let mut initiator = HandshakeState::new_initiator(our_identity.keypair()); let mut responder = HandshakeState::new_responder(remote_identity.keypair()); // Set epochs for both sides (required for handshake message encryption) @@ -1216,6 +1204,8 @@ fn make_noise_session( responder.read_message_1(&msg1).unwrap(); let msg2 = responder.write_message_2().unwrap(); initiator.read_message_2(&msg2).unwrap(); + let msg3 = initiator.write_message_3().unwrap(); + responder.read_message_3(&msg3).unwrap(); initiator.into_session().unwrap() } @@ -1281,10 +1271,7 @@ fn test_purge_idle_sessions_ignores_initiating() { let remote = Identity::generate(); let remote_addr = *remote.node_addr(); - let handshake = HandshakeState::new_initiator( - node.identity().keypair(), - remote.pubkey_full(), - ); + let handshake = HandshakeState::new_initiator(node.identity().keypair()); let entry = crate::node::session::SessionEntry::new( remote_addr, remote.pubkey_full(), @@ -1401,10 +1388,7 @@ fn test_coords_warmup_counter_default_zero_on_new() { let identity_a = Identity::generate(); let identity_b = Identity::generate(); - let handshake = HandshakeState::new_initiator( - identity_a.keypair(), - identity_b.pubkey_full(), - ); + let handshake = HandshakeState::new_initiator(identity_a.keypair()); let entry = crate::node::session::SessionEntry::new( *identity_b.node_addr(), @@ -1546,10 +1530,7 @@ fn test_session_entry_handshake_payload_storage() { let identity_a = Identity::generate(); let identity_b = Identity::generate(); - let handshake = HandshakeState::new_initiator( - identity_a.keypair(), - identity_b.pubkey_full(), - ); + let handshake = HandshakeState::new_initiator(identity_a.keypair()); let mut entry = crate::node::session::SessionEntry::new( *identity_b.node_addr(), @@ -1581,10 +1562,7 @@ fn test_session_entry_resend_tracking() { let identity_a = Identity::generate(); let identity_b = Identity::generate(); - let handshake = HandshakeState::new_initiator( - identity_a.keypair(), - identity_b.pubkey_full(), - ); + let handshake = HandshakeState::new_initiator(identity_a.keypair()); let mut entry = crate::node::session::SessionEntry::new( *identity_b.node_addr(), @@ -1615,10 +1593,7 @@ fn test_session_entry_clear_handshake_payload() { let identity_a = Identity::generate(); let identity_b = Identity::generate(); - let handshake = HandshakeState::new_initiator( - identity_a.keypair(), - identity_b.pubkey_full(), - ); + let handshake = HandshakeState::new_initiator(identity_a.keypair()); let mut entry = crate::node::session::SessionEntry::new( *identity_b.node_addr(), @@ -1649,10 +1624,7 @@ async fn test_session_handshake_timeout() { let mut node = make_node(); let identity_b = Identity::generate(); - let handshake = HandshakeState::new_initiator( - node.identity.keypair(), - identity_b.pubkey_full(), - ); + let handshake = HandshakeState::new_initiator(node.identity.keypair()); let dest_addr = *identity_b.node_addr(); @@ -1690,7 +1662,7 @@ async fn test_session_awaiting_msg3_timeout() { let identity_a = Identity::generate(); let identity_b = Identity::generate(); - let handshake = HandshakeState::new_xx_responder( + let handshake = HandshakeState::new_responder( identity_b.keypair(), ); @@ -1733,7 +1705,7 @@ async fn test_tun_outbound_path_mtu_generates_ptb() { let src_fips = crate::FipsAddress::from_node_addr(&node0_addr); let dst_fips = crate::FipsAddress::from_node_addr(&node1_addr); - // Establish session (XK: 3 messages — Setup, Ack, Msg3) + // Establish session (XX: 3 messages — Setup, Ack, Msg3) nodes[0].node.initiate_session(node1_addr, node1_pubkey).await.unwrap(); tokio::time::sleep(Duration::from_millis(20)).await; process_available_packets(&mut nodes).await; diff --git a/src/node/wire.rs b/src/node/wire.rs index 428368c8..d91acb3c 100644 --- a/src/node/wire.rs +++ b/src/node/wire.rs @@ -19,7 +19,7 @@ //! | 0x3 | Noise XX msg3 | 85+ bytes | Handshake completion | use crate::utils::index::SessionIndex; -use crate::noise::{XX_HANDSHAKE_MSG1_SIZE, XX_HANDSHAKE_MSG2_SIZE, XX_HANDSHAKE_MSG3_SIZE, TAG_SIZE}; +use crate::noise::{HANDSHAKE_MSG1_SIZE, HANDSHAKE_MSG2_SIZE, HANDSHAKE_MSG3_SIZE, TAG_SIZE}; // ============================================================================ // Constants @@ -47,15 +47,15 @@ pub const COMMON_PREFIX_SIZE: usize = 4; 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 + XX_HANDSHAKE_MSG1_SIZE; // 41 bytes +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 + XX_HANDSHAKE_MSG2_SIZE; // 118 bytes +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 + XX_HANDSHAKE_MSG3_SIZE; // 85 bytes +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 @@ -395,7 +395,7 @@ impl Msg3Header { /// /// 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 { - debug_assert_eq!(noise_msg1.len(), XX_HANDSHAKE_MSG1_SIZE); + debug_assert_eq!(noise_msg1.len(), HANDSHAKE_MSG1_SIZE); let payload_len = (4 + noise_msg1.len()) as u16; // sender_idx + noise_msg1 @@ -413,7 +413,7 @@ pub fn build_msg1(sender_idx: SessionIndex, noise_msg1: &[u8]) -> Vec { /// 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 { - debug_assert!(noise_msg2.len() >= XX_HANDSHAKE_MSG2_SIZE); + 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(); @@ -433,7 +433,7 @@ pub fn build_msg2(sender_idx: SessionIndex, receiver_idx: SessionIndex, noise_ms /// 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 { - debug_assert!(noise_msg3.len() >= XX_HANDSHAKE_MSG3_SIZE); + 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(); @@ -575,7 +575,7 @@ mod tests { #[test] fn test_msg1_header_parse() { let sender_idx = SessionIndex::new(0xABCDEF01); - let noise_msg1 = vec![0xbb; XX_HANDSHAKE_MSG1_SIZE]; + let noise_msg1 = vec![0xbb; HANDSHAKE_MSG1_SIZE]; let packet = build_msg1(sender_idx, &noise_msg1); @@ -606,7 +606,7 @@ mod tests { #[test] fn test_msg1_header_nonzero_flags() { - let mut packet = build_msg1(SessionIndex::new(1), &[0u8; XX_HANDSHAKE_MSG1_SIZE]); + 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()); } @@ -615,7 +615,7 @@ mod tests { fn test_msg2_header_parse() { let sender_idx = SessionIndex::new(0x11223344); let receiver_idx = SessionIndex::new(0x55667788); - let noise_msg2 = vec![0xcc; XX_HANDSHAKE_MSG2_SIZE]; + let noise_msg2 = vec![0xcc; HANDSHAKE_MSG2_SIZE]; let packet = build_msg2(sender_idx, receiver_idx, &noise_msg2); @@ -664,7 +664,7 @@ mod tests { fn test_roundtrip_indices() { let idx = SessionIndex::new(0xDEADBEEF); - let msg1 = build_msg1(idx, &[0u8; XX_HANDSHAKE_MSG1_SIZE]); + let msg1 = build_msg1(idx, &[0u8; HANDSHAKE_MSG1_SIZE]); let parsed = Msg1Header::parse(&msg1).unwrap(); assert_eq!(parsed.sender_idx.as_u32(), 0xDEADBEEF); @@ -718,7 +718,7 @@ mod tests { #[test] fn test_payload_len_in_msg1() { - let packet = build_msg1(SessionIndex::new(1), &[0u8; XX_HANDSHAKE_MSG1_SIZE]); + 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); @@ -729,7 +729,7 @@ mod tests { let packet = build_msg2( SessionIndex::new(1), SessionIndex::new(2), - &[0u8; XX_HANDSHAKE_MSG2_SIZE], + &[0u8; HANDSHAKE_MSG2_SIZE], ); let prefix = CommonPrefix::parse(&packet).unwrap(); // payload_len = sender_idx(4) + receiver_idx(4) + noise_msg2(106) = 114 @@ -740,7 +740,7 @@ mod tests { fn test_msg3_header_parse() { let sender_idx = SessionIndex::new(0xAABBCCDD); let receiver_idx = SessionIndex::new(0x11223344); - let noise_msg3 = vec![0xdd; XX_HANDSHAKE_MSG3_SIZE]; + let noise_msg3 = vec![0xdd; HANDSHAKE_MSG3_SIZE]; let packet = build_msg3(sender_idx, receiver_idx, &noise_msg3); @@ -779,7 +779,7 @@ mod tests { let mut packet = build_msg3( SessionIndex::new(1), SessionIndex::new(2), - &[0u8; XX_HANDSHAKE_MSG3_SIZE], + &[0u8; HANDSHAKE_MSG3_SIZE], ); packet[1] = 0x01; // flags must be zero during handshake assert!(Msg3Header::parse(&packet).is_none()); @@ -790,7 +790,7 @@ mod tests { let packet = build_msg3( SessionIndex::new(1), SessionIndex::new(2), - &[0u8; XX_HANDSHAKE_MSG3_SIZE], + &[0u8; HANDSHAKE_MSG3_SIZE], ); let prefix = CommonPrefix::parse(&packet).unwrap(); // payload_len = sender_idx(4) + receiver_idx(4) + noise_msg3(73) = 81 diff --git a/src/noise/handshake.rs b/src/noise/handshake.rs index ab43b60b..25f90ad0 100644 --- a/src/noise/handshake.rs +++ b/src/noise/handshake.rs @@ -1,9 +1,7 @@ use super::{ CipherState, HandshakeProgress, HandshakeRole, NoiseError, NoisePattern, NoiseSession, EPOCH_ENCRYPTED_SIZE, EPOCH_SIZE, HANDSHAKE_MSG1_SIZE, HANDSHAKE_MSG2_SIZE, - PROTOCOL_NAME_IK, PROTOCOL_NAME_XK, PROTOCOL_NAME_XX, PUBKEY_SIZE, - XK_HANDSHAKE_MSG1_SIZE, XK_HANDSHAKE_MSG2_SIZE, XK_HANDSHAKE_MSG3_SIZE, - XX_HANDSHAKE_MSG1_SIZE, XX_HANDSHAKE_MSG2_SIZE, XX_HANDSHAKE_MSG3_SIZE, + HANDSHAKE_MSG3_SIZE, PROTOCOL_NAME_XX, PUBKEY_SIZE, }; use hkdf::Hkdf; use rand::Rng; @@ -103,7 +101,7 @@ impl SymmetricState { } } -/// Handshake state for Noise IK, XK, and XX patterns. +/// Handshake state for the Noise XX pattern. pub struct HandshakeState { /// Which Noise pattern is being used. pattern: NoisePattern, @@ -118,10 +116,6 @@ pub struct HandshakeState { /// Our ephemeral keypair (generated at handshake start). ephemeral_keypair: Option, /// Remote static public key. - /// For IK initiator: known before handshake (from config). - /// For IK responder: learned from message 1. - /// For XK initiator: known before handshake (from config). - /// For XK responder: learned from message 3. /// For XX initiator: learned from message 2. /// For XX responder: learned from message 3. remote_static: Option, @@ -136,131 +130,46 @@ pub struct HandshakeState { } impl HandshakeState { - /// Normalize a compressed public key to even parity for pre-message hashing. + /// Create a new XX handshake as initiator. /// - /// Nostr npubs encode x-only keys (no parity). The Noise IK pre-message - /// mixes the responder's static key into the hash before any messages. - /// Both sides must mix identical bytes. Since the initiator may only have - /// the x-only key (from an npub), we normalize to even parity (0x02 prefix) - /// so the hash chain matches regardless of the key's actual parity. - /// - /// This does NOT affect ECDH operations (which use x-coordinate-only output) - /// or the keys sent in handshake messages (which use actual parity). - fn normalize_for_premessage(pubkey: &PublicKey) -> [u8; PUBKEY_SIZE] { - let mut bytes = pubkey.serialize(); - bytes[0] = 0x02; // Force even parity - bytes - } - - /// Create a new IK handshake as initiator. - /// - /// The initiator knows the responder's static key and will send first. - /// Used by FMP (link layer). - pub fn new_initiator(static_keypair: Keypair, remote_static: PublicKey) -> Self { + /// XX: neither side knows the other's static key. No pre-message. + pub fn new_initiator(static_keypair: Keypair) -> Self { let secp = Secp256k1::new(); - let mut state = Self { - pattern: NoisePattern::Ik, + Self { + pattern: NoisePattern::Xx, role: HandshakeRole::Initiator, progress: HandshakeProgress::Initial, - symmetric: SymmetricState::initialize(PROTOCOL_NAME_IK), + symmetric: SymmetricState::initialize(PROTOCOL_NAME_XX), static_keypair, ephemeral_keypair: None, - remote_static: Some(remote_static), + remote_static: None, remote_ephemeral: None, secp, local_epoch: None, remote_epoch: None, - }; - - // Mix in pre-message: <- s (responder's static is known) - // Normalize to even parity so initiator and responder hash chains match - // even when the initiator only has the x-only key (from npub). - let normalized = Self::normalize_for_premessage(&remote_static); - state.symmetric.mix_hash(&normalized); - - state + } + // No pre-message: neither side's static is mixed into hash. } - /// Create a new IK handshake as responder. + /// Create a new XX handshake as responder. /// - /// The responder does NOT know the initiator's static key - it will be - /// learned from message 1. Used by FMP (link layer). + /// XX: neither side knows the other's static key. No pre-message. pub fn new_responder(static_keypair: Keypair) -> Self { let secp = Secp256k1::new(); - let mut state = Self { - pattern: NoisePattern::Ik, + Self { + pattern: NoisePattern::Xx, role: HandshakeRole::Responder, progress: HandshakeProgress::Initial, - symmetric: SymmetricState::initialize(PROTOCOL_NAME_IK), + symmetric: SymmetricState::initialize(PROTOCOL_NAME_XX), static_keypair, ephemeral_keypair: None, - remote_static: None, // Will learn from message 1 + remote_static: None, remote_ephemeral: None, secp, local_epoch: None, remote_epoch: None, - }; - - // Mix in pre-message: <- s (our static, since we're responder) - // Normalize to even parity to match initiator's hash chain. - let normalized = Self::normalize_for_premessage(&state.static_keypair.public_key()); - state.symmetric.mix_hash(&normalized); - - state - } - - /// Create a new XK handshake as initiator. - /// - /// The initiator knows the responder's static key. XK defers the - /// initiator's static key reveal to msg3. Used by FSP (session layer). - pub fn new_xk_initiator(static_keypair: Keypair, remote_static: PublicKey) -> Self { - let secp = Secp256k1::new(); - let mut state = Self { - pattern: NoisePattern::Xk, - role: HandshakeRole::Initiator, - progress: HandshakeProgress::Initial, - symmetric: SymmetricState::initialize(PROTOCOL_NAME_XK), - static_keypair, - ephemeral_keypair: None, - remote_static: Some(remote_static), - remote_ephemeral: None, - secp, - local_epoch: None, - remote_epoch: None, - }; - - // Mix in pre-message: <- s (responder's static is known) - let normalized = Self::normalize_for_premessage(&remote_static); - state.symmetric.mix_hash(&normalized); - - state - } - - /// Create a new XK handshake as responder. - /// - /// The responder does NOT know the initiator's static key - it will be - /// learned from message 3. Used by FSP (session layer). - pub fn new_xk_responder(static_keypair: Keypair) -> Self { - let secp = Secp256k1::new(); - let mut state = Self { - pattern: NoisePattern::Xk, - role: HandshakeRole::Responder, - progress: HandshakeProgress::Initial, - symmetric: SymmetricState::initialize(PROTOCOL_NAME_XK), - static_keypair, - ephemeral_keypair: None, - remote_static: None, // Will learn from message 3 - remote_ephemeral: None, - secp, - local_epoch: None, - remote_epoch: None, - }; - - // Mix in pre-message: <- s (our static, since we're responder) - let normalized = Self::normalize_for_premessage(&state.static_keypair.public_key()); - state.symmetric.mix_hash(&normalized); - - state + } + // No pre-message: neither side's static is mixed into hash. } /// Get our role. @@ -278,7 +187,7 @@ impl HandshakeState { self.progress == HandshakeProgress::Complete } - /// Get the remote static key (available after message 1 for responder). + /// Get the remote static key (available after message 2 for initiator, message 3 for responder). pub fn remote_static(&self) -> Option<&PublicKey> { self.remote_static.as_ref() } @@ -326,12 +235,11 @@ impl HandshakeState { /// Write message 1 (initiator only). /// - /// Message 1 contains: + /// XX msg1: `-> e` /// - e: ephemeral public key (33 bytes) - /// - encrypted s: our static public key encrypted (33 + 16 = 49 bytes) - /// - encrypted epoch: startup epoch for restart detection (8 + 16 = 24 bytes) + /// - No DH operations (responder's static is unknown) /// - /// Total: 106 bytes + /// Total: 33 bytes pub fn write_message_1(&mut self) -> Result, NoiseError> { if self.role != HandshakeRole::Initiator { return Err(NoiseError::WrongState { @@ -346,9 +254,6 @@ impl HandshakeState { }); } - let remote_static = self.remote_static.expect("initiator must have remote static"); - let epoch = self.local_epoch.expect("local epoch must be set before write_message_1"); - // Generate ephemeral keypair self.generate_ephemeral(); let ephemeral = self.ephemeral_keypair.as_ref().unwrap(); @@ -360,23 +265,7 @@ impl HandshakeState { message.extend_from_slice(&e_pub); self.symmetric.mix_hash(&e_pub); - // -> es: DH(e, rs), mix into key - let es = self.ecdh(&ephemeral.secret_key(), &remote_static); - self.symmetric.mix_key(&es); - - // -> s: encrypt our static and send - let our_static = self.static_keypair.public_key().serialize(); - let encrypted_static = self.symmetric.encrypt_and_hash(&our_static)?; - message.extend_from_slice(&encrypted_static); - - // -> ss: DH(s, rs), mix into key - let ss = self.ecdh(&self.static_keypair.secret_key(), &remote_static); - self.symmetric.mix_key(&ss); - - // -> epoch: encrypt startup epoch for restart detection - let encrypted_epoch = self.symmetric.encrypt_and_hash(&epoch)?; - debug_assert_eq!(encrypted_epoch.len(), EPOCH_ENCRYPTED_SIZE); - message.extend_from_slice(&encrypted_epoch); + // No DH here — responder's static is unknown in XX self.progress = HandshakeProgress::Message1Done; @@ -385,7 +274,7 @@ impl HandshakeState { /// Read message 1 (responder only). /// - /// Processes the initiator's first message and learns their identity and epoch. + /// Parses the initiator's ephemeral key. No identity learned. pub fn read_message_1(&mut self, message: &[u8]) -> Result<(), NoiseError> { if self.role != HandshakeRole::Responder { return Err(NoiseError::WrongState { @@ -412,31 +301,7 @@ impl HandshakeState { self.remote_ephemeral = Some(re); self.symmetric.mix_hash(&message[..PUBKEY_SIZE]); - // -> es: DH(s, re), mix into key - // (responder uses their static with initiator's ephemeral) - let es = self.ecdh(&self.static_keypair.secret_key(), &re); - self.symmetric.mix_key(&es); - - // -> s: decrypt initiator's static - let encrypted_static_end = PUBKEY_SIZE + PUBKEY_SIZE + super::TAG_SIZE; - let encrypted_static = &message[PUBKEY_SIZE..encrypted_static_end]; - let decrypted_static = self.symmetric.decrypt_and_hash(encrypted_static)?; - let rs = - PublicKey::from_slice(&decrypted_static).map_err(|_| NoiseError::InvalidPublicKey)?; - self.remote_static = Some(rs); - - // -> ss: DH(s, rs), mix into key - let ss = self.ecdh(&self.static_keypair.secret_key(), &rs); - self.symmetric.mix_key(&ss); - - // -> epoch: decrypt initiator's startup epoch - let encrypted_epoch = &message[encrypted_static_end..]; - debug_assert_eq!(encrypted_epoch.len(), EPOCH_ENCRYPTED_SIZE); - let decrypted_epoch = self.symmetric.decrypt_and_hash(encrypted_epoch)?; - debug_assert_eq!(decrypted_epoch.len(), EPOCH_SIZE); - let mut epoch = [0u8; EPOCH_SIZE]; - epoch.copy_from_slice(&decrypted_epoch); - self.remote_epoch = Some(epoch); + // No DH here — responder's static is not in pre-message for XX self.progress = HandshakeProgress::Message1Done; @@ -445,11 +310,14 @@ impl HandshakeState { /// Write message 2 (responder only). /// - /// Message 2 contains: + /// XX msg2: `<- e, ee, s, es` + encrypted epoch /// - e: ephemeral public key (33 bytes) - /// - encrypted epoch: startup epoch for restart detection (8 + 16 = 24 bytes) + /// - ee: DH(e_priv, re_pub), mix_key + /// - s: encrypt_and_hash(s_pub) — encrypted static (49 bytes) + /// - es: DH(s_priv, re_pub), mix_key + /// - encrypted epoch (24 bytes) /// - /// Total: 57 bytes + /// Total: 106 bytes pub fn write_message_2(&mut self) -> Result, NoiseError> { if self.role != HandshakeRole::Responder { return Err(NoiseError::WrongState { @@ -482,521 +350,6 @@ impl HandshakeState { let ee = self.ecdh(&ephemeral.secret_key(), &re); self.symmetric.mix_key(&ee); - // <- se: DH(s, re), mix into key - let se = self.ecdh(&self.static_keypair.secret_key(), &re); - self.symmetric.mix_key(&se); - - // <- epoch: encrypt startup epoch for restart detection - let encrypted_epoch = self.symmetric.encrypt_and_hash(&epoch)?; - debug_assert_eq!(encrypted_epoch.len(), EPOCH_ENCRYPTED_SIZE); - message.extend_from_slice(&encrypted_epoch); - - self.progress = HandshakeProgress::Complete; - - Ok(message) - } - - /// Read message 2 (initiator only). - /// - /// Processes the responder's message and completes the handshake. - pub fn read_message_2(&mut self, message: &[u8]) -> Result<(), NoiseError> { - if self.role != HandshakeRole::Initiator { - return Err(NoiseError::WrongState { - expected: "initiator".to_string(), - got: "responder".to_string(), - }); - } - if self.progress != HandshakeProgress::Message1Done { - return Err(NoiseError::WrongState { - expected: HandshakeProgress::Message1Done.to_string(), - got: self.progress.to_string(), - }); - } - if message.len() != HANDSHAKE_MSG2_SIZE { - return Err(NoiseError::MessageTooShort { - expected: HANDSHAKE_MSG2_SIZE, - got: message.len(), - }); - } - - // <- e: parse remote ephemeral, mix into hash - let e_pub = &message[..PUBKEY_SIZE]; - let re = PublicKey::from_slice(e_pub).map_err(|_| NoiseError::InvalidPublicKey)?; - self.remote_ephemeral = Some(re); - self.symmetric.mix_hash(e_pub); - - // <- ee: DH(e, re), mix into key - let ephemeral = self.ephemeral_keypair.as_ref().unwrap(); - let ee = self.ecdh(&ephemeral.secret_key(), &re); - self.symmetric.mix_key(&ee); - - // <- se: DH(e, rs), mix into key - // (initiator uses their ephemeral with responder's static) - let rs = self.remote_static.expect("initiator has remote static"); - let se = self.ecdh(&ephemeral.secret_key(), &rs); - self.symmetric.mix_key(&se); - - // <- epoch: decrypt responder's startup epoch - let encrypted_epoch = &message[PUBKEY_SIZE..]; - debug_assert_eq!(encrypted_epoch.len(), EPOCH_ENCRYPTED_SIZE); - let decrypted_epoch = self.symmetric.decrypt_and_hash(encrypted_epoch)?; - debug_assert_eq!(decrypted_epoch.len(), EPOCH_SIZE); - let mut epoch = [0u8; EPOCH_SIZE]; - epoch.copy_from_slice(&decrypted_epoch); - self.remote_epoch = Some(epoch); - - self.progress = HandshakeProgress::Complete; - - Ok(()) - } - - // ======================================================================== - // XK Pattern Methods (Session Layer) - // ======================================================================== - - /// Write XK message 1 (initiator only). - /// - /// XK msg1: `-> e, es` - /// - e: ephemeral public key (33 bytes) - /// - es: DH(e_priv, rs_pub), mix_key - /// - /// Total: 33 bytes (ephemeral only — no static, no epoch) - pub fn write_xk_message_1(&mut self) -> Result, NoiseError> { - if self.role != HandshakeRole::Initiator { - return Err(NoiseError::WrongState { - expected: "initiator".to_string(), - got: "responder".to_string(), - }); - } - if self.progress != HandshakeProgress::Initial { - return Err(NoiseError::WrongState { - expected: HandshakeProgress::Initial.to_string(), - got: self.progress.to_string(), - }); - } - - let remote_static = self.remote_static.expect("initiator must have remote static"); - - // Generate ephemeral keypair - self.generate_ephemeral(); - let ephemeral = self.ephemeral_keypair.as_ref().unwrap(); - let e_pub = ephemeral.public_key().serialize(); - - let mut message = Vec::with_capacity(XK_HANDSHAKE_MSG1_SIZE); - - // -> e: send ephemeral, mix into hash - message.extend_from_slice(&e_pub); - self.symmetric.mix_hash(&e_pub); - - // -> es: DH(e, rs), mix into key - let es = self.ecdh(&ephemeral.secret_key(), &remote_static); - self.symmetric.mix_key(&es); - - self.progress = HandshakeProgress::Message1Done; - - Ok(message) - } - - /// Read XK message 1 (responder only). - /// - /// Processes the initiator's first message. Does NOT learn initiator's - /// identity (that comes in msg3). - pub fn read_xk_message_1(&mut self, message: &[u8]) -> Result<(), NoiseError> { - if self.role != HandshakeRole::Responder { - return Err(NoiseError::WrongState { - expected: "responder".to_string(), - got: "initiator".to_string(), - }); - } - if self.progress != HandshakeProgress::Initial { - return Err(NoiseError::WrongState { - expected: HandshakeProgress::Initial.to_string(), - got: self.progress.to_string(), - }); - } - if message.len() != XK_HANDSHAKE_MSG1_SIZE { - return Err(NoiseError::MessageTooShort { - expected: XK_HANDSHAKE_MSG1_SIZE, - got: message.len(), - }); - } - - // -> e: parse remote ephemeral, mix into hash - let re = PublicKey::from_slice(&message[..PUBKEY_SIZE]) - .map_err(|_| NoiseError::InvalidPublicKey)?; - self.remote_ephemeral = Some(re); - self.symmetric.mix_hash(&message[..PUBKEY_SIZE]); - - // -> es: DH(s, re), mix into key - // (responder uses their static with initiator's ephemeral) - let es = self.ecdh(&self.static_keypair.secret_key(), &re); - self.symmetric.mix_key(&es); - - self.progress = HandshakeProgress::Message1Done; - - Ok(()) - } - - /// Write XK message 2 (responder only). - /// - /// XK msg2: `<- e, ee` + encrypted epoch - /// - e: ephemeral public key (33 bytes) - /// - ee: DH(e_priv, re_pub), mix_key - /// - encrypted epoch (24 bytes) - /// - /// Total: 57 bytes - pub fn write_xk_message_2(&mut self) -> Result, NoiseError> { - if self.role != HandshakeRole::Responder { - return Err(NoiseError::WrongState { - expected: "responder".to_string(), - got: "initiator".to_string(), - }); - } - if self.progress != HandshakeProgress::Message1Done { - return Err(NoiseError::WrongState { - expected: HandshakeProgress::Message1Done.to_string(), - got: self.progress.to_string(), - }); - } - - let re = self.remote_ephemeral.expect("should have remote ephemeral"); - let epoch = self.local_epoch.expect("local epoch must be set before write_xk_message_2"); - - // Generate ephemeral keypair - self.generate_ephemeral(); - let ephemeral = self.ephemeral_keypair.as_ref().unwrap(); - let e_pub = ephemeral.public_key().serialize(); - - let mut message = Vec::with_capacity(XK_HANDSHAKE_MSG2_SIZE); - - // <- e: send ephemeral, mix into hash - message.extend_from_slice(&e_pub); - self.symmetric.mix_hash(&e_pub); - - // <- ee: DH(e, re), mix into key - let ee = self.ecdh(&ephemeral.secret_key(), &re); - self.symmetric.mix_key(&ee); - - // <- epoch: encrypt startup epoch for restart detection - let encrypted_epoch = self.symmetric.encrypt_and_hash(&epoch)?; - debug_assert_eq!(encrypted_epoch.len(), EPOCH_ENCRYPTED_SIZE); - message.extend_from_slice(&encrypted_epoch); - - self.progress = HandshakeProgress::Message2Done; - - Ok(message) - } - - /// Read XK message 2 (initiator only). - /// - /// Processes the responder's message and extracts the responder's epoch. - /// Does NOT complete the handshake — msg3 still needed. - pub fn read_xk_message_2(&mut self, message: &[u8]) -> Result<(), NoiseError> { - if self.role != HandshakeRole::Initiator { - return Err(NoiseError::WrongState { - expected: "initiator".to_string(), - got: "responder".to_string(), - }); - } - if self.progress != HandshakeProgress::Message1Done { - return Err(NoiseError::WrongState { - expected: HandshakeProgress::Message1Done.to_string(), - got: self.progress.to_string(), - }); - } - if message.len() != XK_HANDSHAKE_MSG2_SIZE { - return Err(NoiseError::MessageTooShort { - expected: XK_HANDSHAKE_MSG2_SIZE, - got: message.len(), - }); - } - - // <- e: parse remote ephemeral, mix into hash - let e_pub = &message[..PUBKEY_SIZE]; - let re = PublicKey::from_slice(e_pub).map_err(|_| NoiseError::InvalidPublicKey)?; - self.remote_ephemeral = Some(re); - self.symmetric.mix_hash(e_pub); - - // <- ee: DH(e, re), mix into key - let ephemeral = self.ephemeral_keypair.as_ref().unwrap(); - let ee = self.ecdh(&ephemeral.secret_key(), &re); - self.symmetric.mix_key(&ee); - - // <- epoch: decrypt responder's startup epoch - let encrypted_epoch = &message[PUBKEY_SIZE..]; - debug_assert_eq!(encrypted_epoch.len(), EPOCH_ENCRYPTED_SIZE); - let decrypted_epoch = self.symmetric.decrypt_and_hash(encrypted_epoch)?; - debug_assert_eq!(decrypted_epoch.len(), EPOCH_SIZE); - let mut epoch = [0u8; EPOCH_SIZE]; - epoch.copy_from_slice(&decrypted_epoch); - self.remote_epoch = Some(epoch); - - self.progress = HandshakeProgress::Message2Done; - - Ok(()) - } - - /// Write XK message 3 (initiator only). - /// - /// XK msg3: `-> s, se` + encrypted epoch - /// - s: encrypt_and_hash(s_pub) — encrypted static (49 bytes) - /// - se: DH(s_priv, re_pub), mix_key - /// - encrypted epoch (24 bytes) - /// - /// Total: 73 bytes - pub fn write_xk_message_3(&mut self) -> Result, NoiseError> { - if self.role != HandshakeRole::Initiator { - return Err(NoiseError::WrongState { - expected: "initiator".to_string(), - got: "responder".to_string(), - }); - } - if self.progress != HandshakeProgress::Message2Done { - return Err(NoiseError::WrongState { - expected: HandshakeProgress::Message2Done.to_string(), - got: self.progress.to_string(), - }); - } - - let re = self.remote_ephemeral.expect("should have remote ephemeral after msg2"); - let epoch = self.local_epoch.expect("local epoch must be set before write_xk_message_3"); - - let mut message = Vec::with_capacity(XK_HANDSHAKE_MSG3_SIZE); - - // -> s: encrypt our static and send - let our_static = self.static_keypair.public_key().serialize(); - let encrypted_static = self.symmetric.encrypt_and_hash(&our_static)?; - message.extend_from_slice(&encrypted_static); - - // -> se: DH(s, re), mix into key - let se = self.ecdh(&self.static_keypair.secret_key(), &re); - self.symmetric.mix_key(&se); - - // -> epoch: encrypt startup epoch for restart detection - let encrypted_epoch = self.symmetric.encrypt_and_hash(&epoch)?; - debug_assert_eq!(encrypted_epoch.len(), EPOCH_ENCRYPTED_SIZE); - message.extend_from_slice(&encrypted_epoch); - - self.progress = HandshakeProgress::Complete; - - Ok(message) - } - - /// Read XK message 3 (responder only). - /// - /// Processes the initiator's encrypted static key and epoch. - /// After this, the responder learns the initiator's identity. - pub fn read_xk_message_3(&mut self, message: &[u8]) -> Result<(), NoiseError> { - if self.role != HandshakeRole::Responder { - return Err(NoiseError::WrongState { - expected: "responder".to_string(), - got: "initiator".to_string(), - }); - } - if self.progress != HandshakeProgress::Message2Done { - return Err(NoiseError::WrongState { - expected: HandshakeProgress::Message2Done.to_string(), - got: self.progress.to_string(), - }); - } - if message.len() != XK_HANDSHAKE_MSG3_SIZE { - return Err(NoiseError::MessageTooShort { - expected: XK_HANDSHAKE_MSG3_SIZE, - got: message.len(), - }); - } - - // -> s: decrypt initiator's static - let encrypted_static_end = PUBKEY_SIZE + super::TAG_SIZE; - let encrypted_static = &message[..encrypted_static_end]; - let decrypted_static = self.symmetric.decrypt_and_hash(encrypted_static)?; - let rs = - PublicKey::from_slice(&decrypted_static).map_err(|_| NoiseError::InvalidPublicKey)?; - self.remote_static = Some(rs); - - // -> se: DH(e, rs), mix into key - // (responder uses their ephemeral with initiator's now-known static) - let ephemeral = self.ephemeral_keypair.as_ref().expect("should have ephemeral after msg2"); - let se = self.ecdh(&ephemeral.secret_key(), &rs); - self.symmetric.mix_key(&se); - - // -> epoch: decrypt initiator's startup epoch - let encrypted_epoch = &message[encrypted_static_end..]; - debug_assert_eq!(encrypted_epoch.len(), EPOCH_ENCRYPTED_SIZE); - let decrypted_epoch = self.symmetric.decrypt_and_hash(encrypted_epoch)?; - debug_assert_eq!(decrypted_epoch.len(), EPOCH_SIZE); - let mut epoch = [0u8; EPOCH_SIZE]; - epoch.copy_from_slice(&decrypted_epoch); - self.remote_epoch = Some(epoch); - - self.progress = HandshakeProgress::Complete; - - Ok(()) - } - - // ======================================================================== - // XX Pattern Methods - // ======================================================================== - - /// Create a new XX handshake as initiator. - /// - /// XX: neither side knows the other's static key. No pre-message. - pub fn new_xx_initiator(static_keypair: Keypair) -> Self { - let secp = Secp256k1::new(); - Self { - pattern: NoisePattern::Xx, - role: HandshakeRole::Initiator, - progress: HandshakeProgress::Initial, - symmetric: SymmetricState::initialize(PROTOCOL_NAME_XX), - static_keypair, - ephemeral_keypair: None, - remote_static: None, - remote_ephemeral: None, - secp, - local_epoch: None, - remote_epoch: None, - } - // No pre-message: neither side's static is mixed into hash. - } - - /// Create a new XX handshake as responder. - /// - /// XX: neither side knows the other's static key. No pre-message. - pub fn new_xx_responder(static_keypair: Keypair) -> Self { - let secp = Secp256k1::new(); - Self { - pattern: NoisePattern::Xx, - role: HandshakeRole::Responder, - progress: HandshakeProgress::Initial, - symmetric: SymmetricState::initialize(PROTOCOL_NAME_XX), - static_keypair, - ephemeral_keypair: None, - remote_static: None, - remote_ephemeral: None, - secp, - local_epoch: None, - remote_epoch: None, - } - // No pre-message: neither side's static is mixed into hash. - } - - /// Write XX message 1 (initiator only). - /// - /// XX msg1: `-> e` - /// - e: ephemeral public key (33 bytes) - /// - No DH operations (responder's static is unknown) - /// - /// Total: 33 bytes - pub fn write_xx_message_1(&mut self) -> Result, NoiseError> { - if self.role != HandshakeRole::Initiator { - return Err(NoiseError::WrongState { - expected: "initiator".to_string(), - got: "responder".to_string(), - }); - } - if self.progress != HandshakeProgress::Initial { - return Err(NoiseError::WrongState { - expected: HandshakeProgress::Initial.to_string(), - got: self.progress.to_string(), - }); - } - - // Generate ephemeral keypair - self.generate_ephemeral(); - let ephemeral = self.ephemeral_keypair.as_ref().unwrap(); - let e_pub = ephemeral.public_key().serialize(); - - let mut message = Vec::with_capacity(XX_HANDSHAKE_MSG1_SIZE); - - // -> e: send ephemeral, mix into hash - message.extend_from_slice(&e_pub); - self.symmetric.mix_hash(&e_pub); - - // No DH here — responder's static is unknown in XX - - self.progress = HandshakeProgress::Message1Done; - - Ok(message) - } - - /// Read XX message 1 (responder only). - /// - /// Parses the initiator's ephemeral key. No identity learned. - pub fn read_xx_message_1(&mut self, message: &[u8]) -> Result<(), NoiseError> { - if self.role != HandshakeRole::Responder { - return Err(NoiseError::WrongState { - expected: "responder".to_string(), - got: "initiator".to_string(), - }); - } - if self.progress != HandshakeProgress::Initial { - return Err(NoiseError::WrongState { - expected: HandshakeProgress::Initial.to_string(), - got: self.progress.to_string(), - }); - } - if message.len() != XX_HANDSHAKE_MSG1_SIZE { - return Err(NoiseError::MessageTooShort { - expected: XX_HANDSHAKE_MSG1_SIZE, - got: message.len(), - }); - } - - // -> e: parse remote ephemeral, mix into hash - let re = PublicKey::from_slice(&message[..PUBKEY_SIZE]) - .map_err(|_| NoiseError::InvalidPublicKey)?; - self.remote_ephemeral = Some(re); - self.symmetric.mix_hash(&message[..PUBKEY_SIZE]); - - // No DH here — responder's static is not in pre-message for XX - - self.progress = HandshakeProgress::Message1Done; - - Ok(()) - } - - /// Write XX message 2 (responder only). - /// - /// XX msg2: `<- e, ee, s, es` + encrypted epoch - /// - e: ephemeral public key (33 bytes) - /// - ee: DH(e_priv, re_pub), mix_key - /// - s: encrypt_and_hash(s_pub) — encrypted static (49 bytes) - /// - es: DH(s_priv, re_pub), mix_key - /// - encrypted epoch (24 bytes) - /// - /// Total: 106 bytes - pub fn write_xx_message_2(&mut self) -> Result, NoiseError> { - if self.role != HandshakeRole::Responder { - return Err(NoiseError::WrongState { - expected: "responder".to_string(), - got: "initiator".to_string(), - }); - } - if self.progress != HandshakeProgress::Message1Done { - return Err(NoiseError::WrongState { - expected: HandshakeProgress::Message1Done.to_string(), - got: self.progress.to_string(), - }); - } - - let re = self.remote_ephemeral.expect("should have remote ephemeral"); - let epoch = self.local_epoch.expect("local epoch must be set before write_xx_message_2"); - - // Generate ephemeral keypair - self.generate_ephemeral(); - let ephemeral = self.ephemeral_keypair.as_ref().unwrap(); - let e_pub = ephemeral.public_key().serialize(); - - let mut message = Vec::with_capacity(XX_HANDSHAKE_MSG2_SIZE); - - // <- e: send ephemeral, mix into hash - message.extend_from_slice(&e_pub); - self.symmetric.mix_hash(&e_pub); - - // <- ee: DH(e, re), mix into key - let ee = self.ecdh(&ephemeral.secret_key(), &re); - self.symmetric.mix_key(&ee); - // <- s: encrypt our static and send let our_static = self.static_keypair.public_key().serialize(); let encrypted_static = self.symmetric.encrypt_and_hash(&our_static)?; @@ -1016,11 +369,11 @@ impl HandshakeState { Ok(message) } - /// Read XX message 2 (initiator only). + /// Read message 2 (initiator only). /// /// Processes the responder's ephemeral and encrypted static key. /// After this, the initiator learns the responder's identity. - pub fn read_xx_message_2(&mut self, message: &[u8]) -> Result<(), NoiseError> { + pub fn read_message_2(&mut self, message: &[u8]) -> Result<(), NoiseError> { if self.role != HandshakeRole::Initiator { return Err(NoiseError::WrongState { expected: "initiator".to_string(), @@ -1033,9 +386,9 @@ impl HandshakeState { got: self.progress.to_string(), }); } - if message.len() != XX_HANDSHAKE_MSG2_SIZE { + if message.len() != HANDSHAKE_MSG2_SIZE { return Err(NoiseError::MessageTooShort { - expected: XX_HANDSHAKE_MSG2_SIZE, + expected: HANDSHAKE_MSG2_SIZE, got: message.len(), }); } @@ -1077,7 +430,7 @@ impl HandshakeState { Ok(()) } - /// Write XX message 3 (initiator only). + /// Write message 3 (initiator only). /// /// XX msg3: `-> s, se` + encrypted epoch /// - s: encrypt_and_hash(s_pub) — encrypted static (49 bytes) @@ -1085,7 +438,7 @@ impl HandshakeState { /// - encrypted epoch (24 bytes) /// /// Total: 73 bytes - pub fn write_xx_message_3(&mut self) -> Result, NoiseError> { + pub fn write_message_3(&mut self) -> Result, NoiseError> { if self.role != HandshakeRole::Initiator { return Err(NoiseError::WrongState { expected: "initiator".to_string(), @@ -1100,9 +453,9 @@ impl HandshakeState { } let re = self.remote_ephemeral.expect("should have remote ephemeral after msg2"); - let epoch = self.local_epoch.expect("local epoch must be set before write_xx_message_3"); + let epoch = self.local_epoch.expect("local epoch must be set before write_message_3"); - let mut message = Vec::with_capacity(XX_HANDSHAKE_MSG3_SIZE); + let mut message = Vec::with_capacity(HANDSHAKE_MSG3_SIZE); // -> s: encrypt our static and send let our_static = self.static_keypair.public_key().serialize(); @@ -1123,11 +476,11 @@ impl HandshakeState { Ok(message) } - /// Read XX message 3 (responder only). + /// Read message 3 (responder only). /// /// Processes the initiator's encrypted static key and epoch. /// After this, the responder learns the initiator's identity. - pub fn read_xx_message_3(&mut self, message: &[u8]) -> Result<(), NoiseError> { + pub fn read_message_3(&mut self, message: &[u8]) -> Result<(), NoiseError> { if self.role != HandshakeRole::Responder { return Err(NoiseError::WrongState { expected: "responder".to_string(), @@ -1140,9 +493,9 @@ impl HandshakeState { got: self.progress.to_string(), }); } - if message.len() != XX_HANDSHAKE_MSG3_SIZE { + if message.len() != HANDSHAKE_MSG3_SIZE { return Err(NoiseError::MessageTooShort { - expected: XX_HANDSHAKE_MSG3_SIZE, + expected: HANDSHAKE_MSG3_SIZE, got: message.len(), }); } @@ -1176,12 +529,12 @@ impl HandshakeState { } // ======================================================================== - // Payload Encryption (for negotiation payload in XX msg2/msg3) + // Payload Encryption (for negotiation payload in msg2/msg3) // ======================================================================== /// Encrypt additional payload and mix into the handshake hash. /// - /// Called after write_xx_message_2/3 to append negotiation payload. + /// Called after write_message_2/3 to append negotiation payload. /// Must be called before `into_session()`. pub fn encrypt_payload(&mut self, plaintext: &[u8]) -> Result, NoiseError> { self.symmetric.encrypt_and_hash(plaintext) @@ -1189,7 +542,7 @@ impl HandshakeState { /// Decrypt additional payload and mix ciphertext into the handshake hash. /// - /// Called after read_xx_message_2/3 to extract negotiation payload. + /// Called after read_message_2/3 to extract negotiation payload. /// Must be called before `into_session()`. pub fn decrypt_payload(&mut self, ciphertext: &[u8]) -> Result, NoiseError> { self.symmetric.decrypt_and_hash(ciphertext) diff --git a/src/noise/mod.rs b/src/noise/mod.rs index f5c40ab0..466790e2 100644 --- a/src/noise/mod.rs +++ b/src/noise/mod.rs @@ -1,36 +1,9 @@ -//! Noise Protocol Implementations for FIPS +//! Noise Protocol Implementation for FIPS //! -//! Implements Noise Protocol Framework patterns using secp256k1: -//! -//! - **IK pattern**: Used by FMP (link layer) for hop-by-hop peer authentication. -//! The initiator knows the responder's static key and sends its encrypted -//! static in msg1. Two-message handshake. -//! -//! - **XK pattern**: Used by FSP (session layer) for end-to-end sessions. -//! The initiator knows the responder's static key but defers revealing its -//! own identity until msg3, providing stronger identity hiding. Three-message -//! handshake. -//! -//! - **XX pattern**: Neither side knows the other's static key. Both identities -//! are revealed during the handshake: responder in msg2, initiator in msg3. -//! Three-message handshake. Will replace IK/XK for both layers. -//! -//! ## IK Handshake Pattern (Link Layer) -//! -//! ```text -//! <- s (pre-message: responder's static known) -//! -> e, es, s, ss (msg1: ephemeral + encrypted static) -//! <- e, ee, se (msg2: ephemeral) -//! ``` -//! -//! ## XK Handshake Pattern (Session Layer) -//! -//! ```text -//! <- s (pre-message: responder's static known) -//! -> e, es (msg1: ephemeral + DH with responder's static) -//! <- e, ee (msg2: ephemeral + DH) -//! -> s, se (msg3: encrypted static + DH) -//! ``` +//! Implements the Noise XX pattern using secp256k1 for both link-layer and +//! session-layer encryption. Neither side knows the other's static key before +//! the handshake. Both identities are revealed during the handshake: responder +//! in msg2, initiator in msg3. Three-message handshake. //! //! ## XX Handshake Pattern //! @@ -40,12 +13,9 @@ //! -> s, se (msg3: encrypted static) //! ``` //! -//! ## Separation of Concerns -//! -//! The IK pattern handles **link-layer peer authentication** — securing the -//! direct link between neighboring nodes. The XK pattern handles **session-layer -//! end-to-end encryption** between arbitrary network addresses, with stronger -//! initiator identity protection. +//! The XX pattern handles both **link-layer peer authentication** (securing the +//! direct link between neighboring nodes) and **session-layer end-to-end +//! encryption** between arbitrary network addresses. mod handshake; mod replay; @@ -62,14 +32,6 @@ pub use handshake::HandshakeState; pub use replay::ReplayWindow; pub use session::NoiseSession; -/// Protocol name for Noise IK with secp256k1 (link layer). -/// Format: Noise_IK_secp256k1_ChaChaPoly_SHA256 -pub(crate) const PROTOCOL_NAME_IK: &[u8] = b"Noise_IK_secp256k1_ChaChaPoly_SHA256"; - -/// Protocol name for Noise XK with secp256k1 (session layer). -/// Format: Noise_XK_secp256k1_ChaChaPoly_SHA256 -pub(crate) const PROTOCOL_NAME_XK: &[u8] = b"Noise_XK_secp256k1_ChaChaPoly_SHA256"; - /// Protocol name for Noise XX with secp256k1. /// Format: Noise_XX_secp256k1_ChaChaPoly_SHA256 pub(crate) const PROTOCOL_NAME_XX: &[u8] = b"Noise_XX_secp256k1_ChaChaPoly_SHA256"; @@ -89,29 +51,14 @@ pub const EPOCH_SIZE: usize = 8; /// Size of encrypted epoch (epoch + AEAD tag). pub const EPOCH_ENCRYPTED_SIZE: usize = EPOCH_SIZE + TAG_SIZE; -/// Size of IK handshake message 1: ephemeral (33) + encrypted static (33 + 16 tag) + encrypted epoch (8 + 16 tag). -pub const HANDSHAKE_MSG1_SIZE: usize = PUBKEY_SIZE + PUBKEY_SIZE + TAG_SIZE + EPOCH_ENCRYPTED_SIZE; +/// Handshake msg1: ephemeral only (33 bytes). No DH, no encryption. +pub const HANDSHAKE_MSG1_SIZE: usize = PUBKEY_SIZE; -/// Size of IK handshake message 2: ephemeral (33) + encrypted epoch (8 + 16 tag). -pub const HANDSHAKE_MSG2_SIZE: usize = PUBKEY_SIZE + EPOCH_ENCRYPTED_SIZE; +/// Handshake msg2: ephemeral (33) + encrypted static (33 + 16 tag) + encrypted epoch (8 + 16 tag) = 106 bytes. +pub const HANDSHAKE_MSG2_SIZE: usize = PUBKEY_SIZE + PUBKEY_SIZE + TAG_SIZE + EPOCH_ENCRYPTED_SIZE; -/// XK msg1: ephemeral only (33 bytes). -pub const XK_HANDSHAKE_MSG1_SIZE: usize = PUBKEY_SIZE; - -/// XK msg2: ephemeral (33) + encrypted epoch (8 + 16 tag) = 57 bytes. -pub const XK_HANDSHAKE_MSG2_SIZE: usize = PUBKEY_SIZE + EPOCH_ENCRYPTED_SIZE; - -/// XK msg3: encrypted static (33 + 16 tag) + encrypted epoch (8 + 16 tag) = 73 bytes. -pub const XK_HANDSHAKE_MSG3_SIZE: usize = PUBKEY_SIZE + TAG_SIZE + EPOCH_ENCRYPTED_SIZE; - -/// XX msg1: ephemeral only (33 bytes). No DH, no encryption. -pub const XX_HANDSHAKE_MSG1_SIZE: usize = PUBKEY_SIZE; - -/// XX msg2: ephemeral (33) + encrypted static (33 + 16 tag) + encrypted epoch (8 + 16 tag) = 106 bytes. -pub const XX_HANDSHAKE_MSG2_SIZE: usize = PUBKEY_SIZE + PUBKEY_SIZE + TAG_SIZE + EPOCH_ENCRYPTED_SIZE; - -/// XX msg3: encrypted static (33 + 16 tag) + encrypted epoch (8 + 16 tag) = 73 bytes. -pub const XX_HANDSHAKE_MSG3_SIZE: usize = PUBKEY_SIZE + TAG_SIZE + EPOCH_ENCRYPTED_SIZE; +/// Handshake msg3: encrypted static (33 + 16 tag) + encrypted epoch (8 + 16 tag) = 73 bytes. +pub const HANDSHAKE_MSG3_SIZE: usize = PUBKEY_SIZE + TAG_SIZE + EPOCH_ENCRYPTED_SIZE; /// Replay window size in packets (matching WireGuard). pub const REPLAY_WINDOW_SIZE: usize = 2048; @@ -174,10 +121,6 @@ impl fmt::Display for HandshakeRole { /// Which Noise pattern is being used for this handshake. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum NoisePattern { - /// Noise IK: two-message handshake (link layer). - Ik, - /// Noise XK: three-message handshake (session layer). - Xk, /// Noise XX: three-message handshake, no prior key knowledge. Xx, } @@ -189,7 +132,7 @@ pub enum HandshakeProgress { Initial, /// Message 1 sent/received, ready for message 2. Message1Done, - /// Message 2 sent/received, ready for message 3 (XK/XX only). + /// Message 2 sent/received, ready for message 3. Message2Done, /// Handshake complete, ready for transport. Complete, diff --git a/src/noise/tests.rs b/src/noise/tests.rs index 16df6f97..caf12ce7 100644 --- a/src/noise/tests.rs +++ b/src/noise/tests.rs @@ -24,11 +24,8 @@ fn test_full_handshake() { let initiator_epoch = generate_epoch(); let responder_epoch = generate_epoch(); - let responder_pub = responder_keypair.public_key(); - - // Initiator knows responder's static key - // Responder does NOT know initiator's static key (IK pattern) - let mut initiator = HandshakeState::new_initiator(initiator_keypair, responder_pub); + // XX: neither side knows the other's static key + let mut initiator = HandshakeState::new_initiator(initiator_keypair); initiator.set_local_epoch(initiator_epoch); let mut responder = HandshakeState::new_responder(responder_keypair); responder.set_local_epoch(responder_epoch); @@ -36,38 +33,61 @@ fn test_full_handshake() { assert_eq!(initiator.role(), HandshakeRole::Initiator); assert_eq!(responder.role(), HandshakeRole::Responder); - // Initially, responder doesn't know initiator's identity + // Neither side knows the other's identity + assert!(initiator.remote_static().is_none()); assert!(responder.remote_static().is_none()); - // Message 1: Initiator -> Responder + // Message 1: Initiator -> Responder (e only) let msg1 = initiator.write_message_1().unwrap(); assert_eq!(msg1.len(), HANDSHAKE_MSG1_SIZE); + assert_eq!(msg1.len(), 33); responder.read_message_1(&msg1).unwrap(); - // Now responder knows initiator's identity! + // After msg1: still no identities known + assert!(initiator.remote_static().is_none()); + assert!(responder.remote_static().is_none()); + + // Message 2: Responder -> Initiator (e, ee, s, es + epoch) + let msg2 = responder.write_message_2().unwrap(); + assert_eq!(msg2.len(), HANDSHAKE_MSG2_SIZE); + assert_eq!(msg2.len(), 106); + + initiator.read_message_2(&msg2).unwrap(); + + // After msg2: initiator knows responder's identity + assert!(initiator.remote_static().is_some()); + assert_eq!( + initiator.remote_static().unwrap(), + &responder_keypair.public_key() + ); + assert_eq!(initiator.remote_epoch(), Some(responder_epoch)); + // Responder still doesn't know initiator + assert!(responder.remote_static().is_none()); + + // Neither side is complete yet + assert!(!initiator.is_complete()); + assert!(!responder.is_complete()); + + // Message 3: Initiator -> Responder (s, se + epoch) + let msg3 = initiator.write_message_3().unwrap(); + assert_eq!(msg3.len(), HANDSHAKE_MSG3_SIZE); + assert_eq!(msg3.len(), 73); + + responder.read_message_3(&msg3).unwrap(); + + // Both should be complete now + assert!(initiator.is_complete()); + assert!(responder.is_complete()); + + // After msg3: responder knows initiator's identity assert!(responder.remote_static().is_some()); assert_eq!( responder.remote_static().unwrap(), &initiator_keypair.public_key() ); - - // Responder learned initiator's epoch assert_eq!(responder.remote_epoch(), Some(initiator_epoch)); - // Message 2: Responder -> Initiator - let msg2 = responder.write_message_2().unwrap(); - assert_eq!(msg2.len(), HANDSHAKE_MSG2_SIZE); - - initiator.read_message_2(&msg2).unwrap(); - - // Both should be complete - assert!(initiator.is_complete()); - assert!(responder.is_complete()); - - // Initiator learned responder's epoch - assert_eq!(initiator.remote_epoch(), Some(responder_epoch)); - // Handshake hashes should match assert_eq!(initiator.handshake_hash(), responder.handshake_hash()); @@ -75,79 +95,188 @@ fn test_full_handshake() { let mut initiator_session = initiator.into_session().unwrap(); let mut responder_session = responder.into_session().unwrap(); - // Test encryption/decryption - let plaintext = b"Hello, secure world!"; - + // Test bidirectional encryption + let plaintext = b"Hello via XX!"; let ciphertext = initiator_session.encrypt(plaintext).unwrap(); let decrypted = responder_session.decrypt(&ciphertext).unwrap(); assert_eq!(decrypted, plaintext); - // Test reverse direction - let plaintext2 = b"Hello back!"; + let plaintext2 = b"XX reply!"; let ciphertext2 = responder_session.encrypt(plaintext2).unwrap(); let decrypted2 = initiator_session.decrypt(&ciphertext2).unwrap(); assert_eq!(decrypted2, plaintext2); } #[test] -fn test_multiple_messages() { +fn test_message_sizes() { + assert_eq!(HANDSHAKE_MSG1_SIZE, 33); // ephemeral only + assert_eq!(HANDSHAKE_MSG2_SIZE, 33 + 33 + 16 + 24); // e + encrypted static + encrypted epoch + assert_eq!(HANDSHAKE_MSG3_SIZE, 33 + 16 + 24); // encrypted static + encrypted epoch +} + +#[test] +fn test_identity_timing() { + // XX property: initiator learns responder in msg2, responder learns initiator in msg3 let initiator_keypair = generate_keypair(); let responder_keypair = generate_keypair(); - let mut initiator = - HandshakeState::new_initiator(initiator_keypair, responder_keypair.public_key()); + let mut initiator = HandshakeState::new_initiator(initiator_keypair); initiator.set_local_epoch(generate_epoch()); let mut responder = HandshakeState::new_responder(responder_keypair); responder.set_local_epoch(generate_epoch()); + // Before any messages: neither side knows + assert!(initiator.remote_static().is_none()); + assert!(responder.remote_static().is_none()); + + // After msg1 + let msg1 = initiator.write_message_1().unwrap(); + responder.read_message_1(&msg1).unwrap(); + assert!(initiator.remote_static().is_none(), "XX: initiator should NOT know identity after msg1"); + assert!(responder.remote_static().is_none(), "XX: responder should NOT know identity after msg1"); + + // After msg2: initiator learns responder + let msg2 = responder.write_message_2().unwrap(); + initiator.read_message_2(&msg2).unwrap(); + assert!(initiator.remote_static().is_some(), "XX: initiator should know responder after msg2"); + assert_eq!(initiator.remote_static().unwrap(), &responder_keypair.public_key()); + assert!(responder.remote_static().is_none(), "XX: responder should NOT know initiator after msg2"); + + // After msg3: responder learns initiator + let msg3 = initiator.write_message_3().unwrap(); + responder.read_message_3(&msg3).unwrap(); + assert!(responder.remote_static().is_some(), "XX: responder should know initiator after msg3"); + assert_eq!(responder.remote_static().unwrap(), &initiator_keypair.public_key()); +} + +#[test] +fn test_wrong_state_errors() { + let keypair1 = generate_keypair(); + let keypair2 = generate_keypair(); + + // Initiator can't read msg1 + let mut initiator = HandshakeState::new_initiator(keypair1); + initiator.set_local_epoch(generate_epoch()); + assert!(initiator.read_message_1(&[0u8; HANDSHAKE_MSG1_SIZE]).is_err()); + + // Initiator can't write msg2 + assert!(initiator.write_message_2().is_err()); + + // Initiator can't write msg3 before msg2 + assert!(initiator.write_message_3().is_err()); + + // Responder can't write msg1 + let mut responder = HandshakeState::new_responder(keypair2); + responder.set_local_epoch(generate_epoch()); + assert!(responder.write_message_1().is_err()); + + // Responder can't read msg3 before msg2 + assert!(responder.read_message_3(&[0u8; HANDSHAKE_MSG3_SIZE]).is_err()); +} + +#[test] +fn test_multiple_messages_after_handshake() { + let keypair1 = generate_keypair(); + let keypair2 = generate_keypair(); + + let mut initiator = HandshakeState::new_initiator(keypair1); + initiator.set_local_epoch(generate_epoch()); + let mut responder = HandshakeState::new_responder(keypair2); + responder.set_local_epoch(generate_epoch()); + let msg1 = initiator.write_message_1().unwrap(); responder.read_message_1(&msg1).unwrap(); let msg2 = responder.write_message_2().unwrap(); initiator.read_message_2(&msg2).unwrap(); + let msg3 = initiator.write_message_3().unwrap(); + responder.read_message_3(&msg3).unwrap(); - let mut initiator_session = initiator.into_session().unwrap(); - let mut responder_session = responder.into_session().unwrap(); + let mut init_session = initiator.into_session().unwrap(); + let mut resp_session = responder.into_session().unwrap(); - // Send many messages to test nonce increment + // Send many messages for i in 0..100 { - let msg = format!("Message {}", i); - let ct = initiator_session.encrypt(msg.as_bytes()).unwrap(); - let pt = responder_session.decrypt(&ct).unwrap(); + let msg = format!("XX message {}", i); + let ct = init_session.encrypt(msg.as_bytes()).unwrap(); + let pt = resp_session.decrypt(&ct).unwrap(); assert_eq!(pt, msg.as_bytes()); } - assert_eq!(initiator_session.send_nonce(), 100); - assert_eq!(responder_session.recv_nonce(), 100); + assert_eq!(init_session.send_nonce(), 100); + assert_eq!(resp_session.recv_nonce(), 100); } #[test] -fn test_wrong_role_errors() { +fn test_with_odd_parity() { + // XX: no pre-message normalization needed, but ECDH x-only hashing + // must still produce matching shared secrets regardless of parity. + let secp = secp256k1::Secp256k1::new(); + + // Node A (initiator) - even parity key + let sk_a = secp256k1::SecretKey::from_slice( + &hex::decode("0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20") + .unwrap(), + ) + .unwrap(); + let kp_a = secp256k1::Keypair::from_secret_key(&secp, &sk_a); + + // Node B (responder) - odd parity key + let sk_b = secp256k1::SecretKey::from_slice( + &hex::decode("b102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fb0") + .unwrap(), + ) + .unwrap(); + let kp_b = secp256k1::Keypair::from_secret_key(&secp, &sk_b); + let (_, parity_b) = kp_b.public_key().x_only_public_key(); + assert_eq!(parity_b, Parity::Odd, "Test requires odd-parity responder key"); + + let mut initiator = HandshakeState::new_initiator(kp_a); + initiator.set_local_epoch(generate_epoch()); + let mut responder = HandshakeState::new_responder(kp_b); + responder.set_local_epoch(generate_epoch()); + + let msg1 = initiator.write_message_1().unwrap(); + responder.read_message_1(&msg1).unwrap(); + let msg2 = responder.write_message_2().unwrap(); + initiator.read_message_2(&msg2).unwrap(); + let msg3 = initiator.write_message_3().unwrap(); + responder.read_message_3(&msg3).unwrap(); + + assert!(initiator.is_complete()); + assert!(responder.is_complete()); + + let mut sender = initiator.into_session().unwrap(); + let mut receiver = responder.into_session().unwrap(); + + let counter = sender.current_send_counter(); + let ciphertext = sender.encrypt(b"xx parity test").unwrap(); + let plaintext = receiver.decrypt_with_replay_check(&ciphertext, counter).unwrap(); + assert_eq!(plaintext, b"xx parity test"); +} + +#[test] +fn test_invalid_msg_sizes() { let keypair1 = generate_keypair(); let keypair2 = generate_keypair(); - let mut initiator = HandshakeState::new_initiator(keypair1, keypair2.public_key()); + // Wrong size for msg1 + let mut responder = HandshakeState::new_responder(keypair1); + responder.set_local_epoch(generate_epoch()); + assert!(responder.read_message_1(&[0u8; 10]).is_err()); + + // Wrong size for msg3 + let mut initiator = HandshakeState::new_initiator(keypair1); initiator.set_local_epoch(generate_epoch()); - - // Initiator can't read message 1 - assert!( - initiator - .read_message_1(&[0u8; HANDSHAKE_MSG1_SIZE]) - .is_err() - ); - - // Initiator can't write message 2 before message 1 - assert!(initiator.write_message_2().is_err()); -} - -#[test] -fn test_invalid_pubkey_in_msg1() { - let keypair = generate_keypair(); - let mut responder = HandshakeState::new_responder(keypair); + let mut responder = HandshakeState::new_responder(keypair2); responder.set_local_epoch(generate_epoch()); - // Invalid pubkey bytes (first 33 bytes are zero) - let invalid_msg = [0u8; HANDSHAKE_MSG1_SIZE]; - assert!(responder.read_message_1(&invalid_msg).is_err()); + let msg1 = initiator.write_message_1().unwrap(); + responder.read_message_1(&msg1).unwrap(); + let _msg2 = responder.write_message_2().unwrap(); + + // Responder is now in Message2Done, try wrong-size msg3 + assert!(responder.read_message_3(&[0u8; 10]).is_err()); + assert!(responder.read_message_3(&[0u8; HANDSHAKE_MSG3_SIZE + 1]).is_err()); } #[test] @@ -157,7 +286,7 @@ fn test_decryption_failure_wrong_key() { let keypair3 = generate_keypair(); // Session between 1 and 2 - let mut init1 = HandshakeState::new_initiator(keypair1, keypair2.public_key()); + let mut init1 = HandshakeState::new_initiator(keypair1); init1.set_local_epoch(generate_epoch()); let mut resp1 = HandshakeState::new_responder(keypair2); resp1.set_local_epoch(generate_epoch()); @@ -166,11 +295,13 @@ fn test_decryption_failure_wrong_key() { resp1.read_message_1(&msg1).unwrap(); let msg2 = resp1.write_message_2().unwrap(); init1.read_message_2(&msg2).unwrap(); + let msg3 = init1.write_message_3().unwrap(); + resp1.read_message_3(&msg3).unwrap(); let mut session1 = init1.into_session().unwrap(); // Session between 1 and 3 - let mut init2 = HandshakeState::new_initiator(keypair1, keypair3.public_key()); + let mut init2 = HandshakeState::new_initiator(keypair1); init2.set_local_epoch(generate_epoch()); let mut resp2 = HandshakeState::new_responder(keypair3); resp2.set_local_epoch(generate_epoch()); @@ -179,6 +310,8 @@ fn test_decryption_failure_wrong_key() { resp2.read_message_1(&msg1).unwrap(); let msg2 = resp2.write_message_2().unwrap(); init2.read_message_2(&msg2).unwrap(); + let msg3 = init2.write_message_3().unwrap(); + resp2.read_message_3(&msg3).unwrap(); let mut session2 = resp2.into_session().unwrap(); @@ -206,7 +339,7 @@ fn test_session_remote_static() { let keypair1 = generate_keypair(); let keypair2 = generate_keypair(); - let mut init = HandshakeState::new_initiator(keypair1, keypair2.public_key()); + let mut init = HandshakeState::new_initiator(keypair1); init.set_local_epoch(generate_epoch()); let mut resp = HandshakeState::new_responder(keypair2); resp.set_local_epoch(generate_epoch()); @@ -215,6 +348,8 @@ fn test_session_remote_static() { resp.read_message_1(&msg1).unwrap(); let msg2 = resp.write_message_2().unwrap(); init.read_message_2(&msg2).unwrap(); + let msg3 = init.write_message_3().unwrap(); + resp.read_message_3(&msg3).unwrap(); let session1 = init.into_session().unwrap(); let session2 = resp.into_session().unwrap(); @@ -224,40 +359,6 @@ fn test_session_remote_static() { assert_eq!(session2.remote_static(), &keypair1.public_key()); } -#[test] -fn test_message_sizes() { - // Verify our size constants are correct - assert_eq!(EPOCH_SIZE, 8); - assert_eq!(EPOCH_ENCRYPTED_SIZE, 8 + 16); // epoch + AEAD tag - assert_eq!(HANDSHAKE_MSG1_SIZE, 33 + 33 + 16 + 24); // e + encrypted_s + encrypted_epoch - assert_eq!(HANDSHAKE_MSG2_SIZE, 33 + 24); // e + encrypted_epoch -} - -#[test] -fn test_responder_identity_discovery() { - // This test verifies the key IK property: responder learns initiator's identity - let initiator_keypair = generate_keypair(); - let responder_keypair = generate_keypair(); - - let mut responder = HandshakeState::new_responder(responder_keypair); - responder.set_local_epoch(generate_epoch()); - - // Before message 1: responder has no idea who's connecting - assert!(responder.remote_static().is_none()); - - let mut initiator = - HandshakeState::new_initiator(initiator_keypair, responder_keypair.public_key()); - initiator.set_local_epoch(generate_epoch()); - let msg1 = initiator.write_message_1().unwrap(); - - // After processing message 1: responder knows initiator's identity - responder.read_message_1(&msg1).unwrap(); - let discovered_initiator = responder.remote_static().unwrap(); - assert_eq!(discovered_initiator, &initiator_keypair.public_key()); - - // The discovered key can be used to look up peer config, verify against allow-list, etc. -} - // ===== ReplayWindow Tests ===== #[test] @@ -339,11 +440,7 @@ fn test_replay_window_sequential() { // All should be marked as seen for i in 0..1000 { - assert!( - !window.check(i), - "Counter {} should be rejected as replay", - i - ); + assert!(!window.check(i), "Counter {} should be rejected as replay", i); } assert_eq!(window.highest(), 999); @@ -368,7 +465,7 @@ fn test_session_replay_protection() { let keypair1 = generate_keypair(); let keypair2 = generate_keypair(); - let mut init = HandshakeState::new_initiator(keypair1, keypair2.public_key()); + let mut init = HandshakeState::new_initiator(keypair1); init.set_local_epoch(generate_epoch()); let mut resp = HandshakeState::new_responder(keypair2); resp.set_local_epoch(generate_epoch()); @@ -377,6 +474,8 @@ fn test_session_replay_protection() { resp.read_message_1(&msg1).unwrap(); let msg2 = resp.write_message_2().unwrap(); init.read_message_2(&msg2).unwrap(); + let msg3 = init.write_message_3().unwrap(); + resp.read_message_3(&msg3).unwrap(); let mut sender = init.into_session().unwrap(); let mut receiver = resp.into_session().unwrap(); @@ -398,714 +497,3 @@ fn test_session_replay_protection() { // Check method alone also detects replay assert!(receiver.check_replay(counter).is_err()); } - -#[test] -fn test_handshake_with_odd_parity_responder() { - // Node B's secret key produces an odd-parity public key (0x03 prefix). - // When the initiator only has the npub (x-only), PeerIdentity::pubkey_full() - // returns even parity (0x02). The pre-message mix_hash must normalize - // parity so both sides produce matching hash chains. - let secp = secp256k1::Secp256k1::new(); - - // Node B (responder) - odd parity key - let sk_b = secp256k1::SecretKey::from_slice( - &hex::decode("b102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fb0").unwrap(), - ) - .unwrap(); - let kp_b = secp256k1::Keypair::from_secret_key(&secp, &sk_b); - let (xonly_b, parity_b) = kp_b.public_key().x_only_public_key(); - assert_eq!( - parity_b, - Parity::Odd, - "Test requires odd-parity responder key" - ); - - // Node A (initiator) - even parity key - let sk_a = secp256k1::SecretKey::from_slice( - &hex::decode("0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20").unwrap(), - ) - .unwrap(); - let kp_a = secp256k1::Keypair::from_secret_key(&secp, &sk_a); - - // Simulate the production path: initiator gets responder's key via npub - // (x-only -> assumed even parity) - let assumed_even_b = xonly_b.public_key(Parity::Even); - assert_ne!( - assumed_even_b, - kp_b.public_key(), - "Even assumption should differ from actual odd key" - ); - - // Handshake using assumed-even key (as production code does) - let mut initiator = HandshakeState::new_initiator(kp_a, assumed_even_b); - initiator.set_local_epoch(generate_epoch()); - let mut responder = HandshakeState::new_responder(kp_b); - responder.set_local_epoch(generate_epoch()); - - let msg1 = initiator.write_message_1().unwrap(); - responder.read_message_1(&msg1).unwrap(); - - let msg2 = responder.write_message_2().unwrap(); - initiator.read_message_2(&msg2).unwrap(); - - assert!(initiator.is_complete()); - assert!(responder.is_complete()); - - // Verify sessions can communicate - let mut sender = initiator.into_session().unwrap(); - let mut receiver = responder.into_session().unwrap(); - - let counter = sender.current_send_counter(); - let ciphertext = sender.encrypt(b"parity test").unwrap(); - let plaintext = receiver - .decrypt_with_replay_check(&ciphertext, counter) - .unwrap(); - assert_eq!(plaintext, b"parity test"); -} - -// ===== XK Handshake Tests ===== - -#[test] -fn test_xk_full_handshake() { - let initiator_keypair = generate_keypair(); - let responder_keypair = generate_keypair(); - let initiator_epoch = generate_epoch(); - let responder_epoch = generate_epoch(); - - let responder_pub = responder_keypair.public_key(); - - // XK: initiator knows responder's static, responder learns initiator's in msg3 - let mut initiator = HandshakeState::new_xk_initiator(initiator_keypair, responder_pub); - initiator.set_local_epoch(initiator_epoch); - let mut responder = HandshakeState::new_xk_responder(responder_keypair); - responder.set_local_epoch(responder_epoch); - - assert_eq!(initiator.role(), HandshakeRole::Initiator); - assert_eq!(responder.role(), HandshakeRole::Responder); - - // Initially, responder doesn't know initiator's identity - assert!(responder.remote_static().is_none()); - - // Message 1: Initiator -> Responder (e, es) - let msg1 = initiator.write_xk_message_1().unwrap(); - assert_eq!(msg1.len(), XK_HANDSHAKE_MSG1_SIZE); - assert_eq!(msg1.len(), 33); // ephemeral only - - responder.read_xk_message_1(&msg1).unwrap(); - - // After msg1: responder still doesn't know initiator's identity (XK property) - assert!(responder.remote_static().is_none()); - assert!(responder.remote_epoch().is_none()); - - // Message 2: Responder -> Initiator (e, ee + epoch) - let msg2 = responder.write_xk_message_2().unwrap(); - assert_eq!(msg2.len(), XK_HANDSHAKE_MSG2_SIZE); - assert_eq!(msg2.len(), 57); // 33 ephemeral + 24 encrypted epoch - - initiator.read_xk_message_2(&msg2).unwrap(); - - // After msg2: initiator learned responder's epoch - assert_eq!(initiator.remote_epoch(), Some(responder_epoch)); - // Neither side is complete yet - assert!(!initiator.is_complete()); - assert!(!responder.is_complete()); - - // Message 3: Initiator -> Responder (s, se + epoch) - let msg3 = initiator.write_xk_message_3().unwrap(); - assert_eq!(msg3.len(), XK_HANDSHAKE_MSG3_SIZE); - assert_eq!(msg3.len(), 73); // 49 encrypted static + 24 encrypted epoch - - responder.read_xk_message_3(&msg3).unwrap(); - - // Both should be complete now - assert!(initiator.is_complete()); - assert!(responder.is_complete()); - - // After msg3: responder now knows initiator's identity - assert!(responder.remote_static().is_some()); - assert_eq!( - responder.remote_static().unwrap(), - &initiator_keypair.public_key() - ); - - // Responder learned initiator's epoch from msg3 - assert_eq!(responder.remote_epoch(), Some(initiator_epoch)); - - // Handshake hashes should match - assert_eq!(initiator.handshake_hash(), responder.handshake_hash()); - - // Convert to sessions - let mut initiator_session = initiator.into_session().unwrap(); - let mut responder_session = responder.into_session().unwrap(); - - // Test bidirectional encryption - let plaintext = b"Hello via XK!"; - let ciphertext = initiator_session.encrypt(plaintext).unwrap(); - let decrypted = responder_session.decrypt(&ciphertext).unwrap(); - assert_eq!(decrypted, plaintext); - - let plaintext2 = b"XK reply!"; - let ciphertext2 = responder_session.encrypt(plaintext2).unwrap(); - let decrypted2 = initiator_session.decrypt(&ciphertext2).unwrap(); - assert_eq!(decrypted2, plaintext2); -} - -#[test] -fn test_xk_message_sizes() { - assert_eq!(XK_HANDSHAKE_MSG1_SIZE, 33); // ephemeral only - assert_eq!(XK_HANDSHAKE_MSG2_SIZE, 33 + 24); // ephemeral + encrypted epoch - assert_eq!(XK_HANDSHAKE_MSG3_SIZE, 33 + 16 + 24); // encrypted static + encrypted epoch -} - -#[test] -fn test_xk_identity_timing() { - // XK property: responder doesn't learn initiator identity until msg3 - let initiator_keypair = generate_keypair(); - let responder_keypair = generate_keypair(); - - let mut initiator = - HandshakeState::new_xk_initiator(initiator_keypair, responder_keypair.public_key()); - initiator.set_local_epoch(generate_epoch()); - let mut responder = HandshakeState::new_xk_responder(responder_keypair); - responder.set_local_epoch(generate_epoch()); - - // Before any messages - assert!(responder.remote_static().is_none()); - - // After msg1 - let msg1 = initiator.write_xk_message_1().unwrap(); - responder.read_xk_message_1(&msg1).unwrap(); - assert!( - responder.remote_static().is_none(), - "XK: responder should NOT know identity after msg1" - ); - - // After msg2 - let msg2 = responder.write_xk_message_2().unwrap(); - initiator.read_xk_message_2(&msg2).unwrap(); - assert!( - responder.remote_static().is_none(), - "XK: responder should NOT know identity after msg2" - ); - - // After msg3 - let msg3 = initiator.write_xk_message_3().unwrap(); - responder.read_xk_message_3(&msg3).unwrap(); - assert!( - responder.remote_static().is_some(), - "XK: responder should know identity after msg3" - ); - assert_eq!( - responder.remote_static().unwrap(), - &initiator_keypair.public_key() - ); -} - -#[test] -fn test_xk_wrong_state_errors() { - let keypair1 = generate_keypair(); - let keypair2 = generate_keypair(); - - // Initiator can't read XK msg1 - let mut initiator = HandshakeState::new_xk_initiator(keypair1, keypair2.public_key()); - initiator.set_local_epoch(generate_epoch()); - assert!( - initiator - .read_xk_message_1(&[0u8; XK_HANDSHAKE_MSG1_SIZE]) - .is_err() - ); - - // Initiator can't write msg2 - assert!(initiator.write_xk_message_2().is_err()); - - // Initiator can't write msg3 before msg2 - assert!(initiator.write_xk_message_3().is_err()); - - // Responder can't write msg1 - let mut responder = HandshakeState::new_xk_responder(keypair2); - responder.set_local_epoch(generate_epoch()); - assert!(responder.write_xk_message_1().is_err()); - - // Responder can't read msg3 before msg2 - assert!( - responder - .read_xk_message_3(&[0u8; XK_HANDSHAKE_MSG3_SIZE]) - .is_err() - ); -} - -#[test] -fn test_xk_handshake_hash_differs_from_ik() { - // XK and IK should produce different handshake hashes (different protocol names) - let keypair1 = generate_keypair(); - let keypair2 = generate_keypair(); - let epoch1 = generate_epoch(); - let epoch2 = generate_epoch(); - - // Complete an IK handshake - let mut ik_init = HandshakeState::new_initiator(keypair1, keypair2.public_key()); - ik_init.set_local_epoch(epoch1); - let mut ik_resp = HandshakeState::new_responder(keypair2); - ik_resp.set_local_epoch(epoch2); - let msg1 = ik_init.write_message_1().unwrap(); - ik_resp.read_message_1(&msg1).unwrap(); - let msg2 = ik_resp.write_message_2().unwrap(); - ik_init.read_message_2(&msg2).unwrap(); - let ik_hash = ik_init.handshake_hash(); - - // Complete an XK handshake with the same keys - let mut xk_init = HandshakeState::new_xk_initiator(keypair1, keypair2.public_key()); - xk_init.set_local_epoch(epoch1); - let mut xk_resp = HandshakeState::new_xk_responder(keypair2); - xk_resp.set_local_epoch(epoch2); - let msg1 = xk_init.write_xk_message_1().unwrap(); - xk_resp.read_xk_message_1(&msg1).unwrap(); - let msg2 = xk_resp.write_xk_message_2().unwrap(); - xk_init.read_xk_message_2(&msg2).unwrap(); - let msg3 = xk_init.write_xk_message_3().unwrap(); - xk_resp.read_xk_message_3(&msg3).unwrap(); - let xk_hash = xk_init.handshake_hash(); - - assert_ne!( - ik_hash, xk_hash, - "IK and XK should produce different handshake hashes" - ); -} - -#[test] -fn test_xk_multiple_messages_after_handshake() { - let keypair1 = generate_keypair(); - let keypair2 = generate_keypair(); - - let mut initiator = HandshakeState::new_xk_initiator(keypair1, keypair2.public_key()); - initiator.set_local_epoch(generate_epoch()); - let mut responder = HandshakeState::new_xk_responder(keypair2); - responder.set_local_epoch(generate_epoch()); - - let msg1 = initiator.write_xk_message_1().unwrap(); - responder.read_xk_message_1(&msg1).unwrap(); - let msg2 = responder.write_xk_message_2().unwrap(); - initiator.read_xk_message_2(&msg2).unwrap(); - let msg3 = initiator.write_xk_message_3().unwrap(); - responder.read_xk_message_3(&msg3).unwrap(); - - let mut init_session = initiator.into_session().unwrap(); - let mut resp_session = responder.into_session().unwrap(); - - // Send many messages - for i in 0..100 { - let msg = format!("XK message {}", i); - let ct = init_session.encrypt(msg.as_bytes()).unwrap(); - let pt = resp_session.decrypt(&ct).unwrap(); - assert_eq!(pt, msg.as_bytes()); - } - - assert_eq!(init_session.send_nonce(), 100); - assert_eq!(resp_session.recv_nonce(), 100); -} - -#[test] -fn test_xk_with_odd_parity_responder() { - let secp = secp256k1::Secp256k1::new(); - - // Node B (responder) - odd parity key - let sk_b = secp256k1::SecretKey::from_slice( - &hex::decode("b102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fb0").unwrap(), - ) - .unwrap(); - let kp_b = secp256k1::Keypair::from_secret_key(&secp, &sk_b); - let (xonly_b, parity_b) = kp_b.public_key().x_only_public_key(); - assert_eq!( - parity_b, - Parity::Odd, - "Test requires odd-parity responder key" - ); - - // Node A (initiator) - let sk_a = secp256k1::SecretKey::from_slice( - &hex::decode("0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20").unwrap(), - ) - .unwrap(); - let kp_a = secp256k1::Keypair::from_secret_key(&secp, &sk_a); - - // Simulate npub path: x-only → assumed even parity - let assumed_even_b = xonly_b.public_key(Parity::Even); - - let mut initiator = HandshakeState::new_xk_initiator(kp_a, assumed_even_b); - initiator.set_local_epoch(generate_epoch()); - let mut responder = HandshakeState::new_xk_responder(kp_b); - responder.set_local_epoch(generate_epoch()); - - let msg1 = initiator.write_xk_message_1().unwrap(); - responder.read_xk_message_1(&msg1).unwrap(); - let msg2 = responder.write_xk_message_2().unwrap(); - initiator.read_xk_message_2(&msg2).unwrap(); - let msg3 = initiator.write_xk_message_3().unwrap(); - responder.read_xk_message_3(&msg3).unwrap(); - - assert!(initiator.is_complete()); - assert!(responder.is_complete()); - - let mut sender = initiator.into_session().unwrap(); - let mut receiver = responder.into_session().unwrap(); - - let counter = sender.current_send_counter(); - let ciphertext = sender.encrypt(b"xk parity test").unwrap(); - let plaintext = receiver - .decrypt_with_replay_check(&ciphertext, counter) - .unwrap(); - assert_eq!(plaintext, b"xk parity test"); -} - -#[test] -fn test_xk_invalid_msg1_size() { - let keypair = generate_keypair(); - let mut responder = HandshakeState::new_xk_responder(keypair); - responder.set_local_epoch(generate_epoch()); - - // Wrong size (IK msg1 size instead of XK) - assert!( - responder - .read_xk_message_1(&[0u8; HANDSHAKE_MSG1_SIZE]) - .is_err() - ); - // Too short - assert!(responder.read_xk_message_1(&[0u8; 10]).is_err()); -} - -#[test] -fn test_xk_invalid_msg3_size() { - let keypair1 = generate_keypair(); - let keypair2 = generate_keypair(); - - let mut initiator = HandshakeState::new_xk_initiator(keypair1, keypair2.public_key()); - initiator.set_local_epoch(generate_epoch()); - let mut responder = HandshakeState::new_xk_responder(keypair2); - responder.set_local_epoch(generate_epoch()); - - let msg1 = initiator.write_xk_message_1().unwrap(); - responder.read_xk_message_1(&msg1).unwrap(); - let _msg2 = responder.write_xk_message_2().unwrap(); - - // Responder is now in Message2Done, try wrong-size msg3 - assert!(responder.read_xk_message_3(&[0u8; 10]).is_err()); - assert!( - responder - .read_xk_message_3(&[0u8; XK_HANDSHAKE_MSG3_SIZE + 1]) - .is_err() - ); -} - -// ===== XX Handshake Tests ===== - -#[test] -fn test_xx_full_handshake() { - let initiator_keypair = generate_keypair(); - let responder_keypair = generate_keypair(); - let initiator_epoch = generate_epoch(); - let responder_epoch = generate_epoch(); - - // XX: neither side knows the other's static key - let mut initiator = HandshakeState::new_xx_initiator(initiator_keypair); - initiator.set_local_epoch(initiator_epoch); - let mut responder = HandshakeState::new_xx_responder(responder_keypair); - responder.set_local_epoch(responder_epoch); - - assert_eq!(initiator.role(), HandshakeRole::Initiator); - assert_eq!(responder.role(), HandshakeRole::Responder); - - // Neither side knows the other's identity - assert!(initiator.remote_static().is_none()); - assert!(responder.remote_static().is_none()); - - // Message 1: Initiator -> Responder (e only) - let msg1 = initiator.write_xx_message_1().unwrap(); - assert_eq!(msg1.len(), XX_HANDSHAKE_MSG1_SIZE); - assert_eq!(msg1.len(), 33); - - responder.read_xx_message_1(&msg1).unwrap(); - - // After msg1: still no identities known - assert!(initiator.remote_static().is_none()); - assert!(responder.remote_static().is_none()); - - // Message 2: Responder -> Initiator (e, ee, s, es + epoch) - let msg2 = responder.write_xx_message_2().unwrap(); - assert_eq!(msg2.len(), XX_HANDSHAKE_MSG2_SIZE); - assert_eq!(msg2.len(), 106); - - initiator.read_xx_message_2(&msg2).unwrap(); - - // After msg2: initiator knows responder's identity - assert!(initiator.remote_static().is_some()); - assert_eq!( - initiator.remote_static().unwrap(), - &responder_keypair.public_key() - ); - assert_eq!(initiator.remote_epoch(), Some(responder_epoch)); - // Responder still doesn't know initiator - assert!(responder.remote_static().is_none()); - - // Neither side is complete yet - assert!(!initiator.is_complete()); - assert!(!responder.is_complete()); - - // Message 3: Initiator -> Responder (s, se + epoch) - let msg3 = initiator.write_xx_message_3().unwrap(); - assert_eq!(msg3.len(), XX_HANDSHAKE_MSG3_SIZE); - assert_eq!(msg3.len(), 73); - - responder.read_xx_message_3(&msg3).unwrap(); - - // Both should be complete now - assert!(initiator.is_complete()); - assert!(responder.is_complete()); - - // After msg3: responder knows initiator's identity - assert!(responder.remote_static().is_some()); - assert_eq!( - responder.remote_static().unwrap(), - &initiator_keypair.public_key() - ); - assert_eq!(responder.remote_epoch(), Some(initiator_epoch)); - - // Handshake hashes should match - assert_eq!(initiator.handshake_hash(), responder.handshake_hash()); - - // Convert to sessions - let mut initiator_session = initiator.into_session().unwrap(); - let mut responder_session = responder.into_session().unwrap(); - - // Test bidirectional encryption - let plaintext = b"Hello via XX!"; - let ciphertext = initiator_session.encrypt(plaintext).unwrap(); - let decrypted = responder_session.decrypt(&ciphertext).unwrap(); - assert_eq!(decrypted, plaintext); - - let plaintext2 = b"XX reply!"; - let ciphertext2 = responder_session.encrypt(plaintext2).unwrap(); - let decrypted2 = initiator_session.decrypt(&ciphertext2).unwrap(); - assert_eq!(decrypted2, plaintext2); -} - -#[test] -fn test_xx_message_sizes() { - assert_eq!(XX_HANDSHAKE_MSG1_SIZE, 33); // ephemeral only - assert_eq!(XX_HANDSHAKE_MSG2_SIZE, 33 + 33 + 16 + 24); // e + encrypted static + encrypted epoch - assert_eq!(XX_HANDSHAKE_MSG3_SIZE, 33 + 16 + 24); // encrypted static + encrypted epoch -} - -#[test] -fn test_xx_identity_timing() { - // XX property: initiator learns responder in msg2, responder learns initiator in msg3 - let initiator_keypair = generate_keypair(); - let responder_keypair = generate_keypair(); - - let mut initiator = HandshakeState::new_xx_initiator(initiator_keypair); - initiator.set_local_epoch(generate_epoch()); - let mut responder = HandshakeState::new_xx_responder(responder_keypair); - responder.set_local_epoch(generate_epoch()); - - // Before any messages: neither side knows - assert!(initiator.remote_static().is_none()); - assert!(responder.remote_static().is_none()); - - // After msg1 - let msg1 = initiator.write_xx_message_1().unwrap(); - responder.read_xx_message_1(&msg1).unwrap(); - assert!(initiator.remote_static().is_none(), "XX: initiator should NOT know identity after msg1"); - assert!(responder.remote_static().is_none(), "XX: responder should NOT know identity after msg1"); - - // After msg2: initiator learns responder - let msg2 = responder.write_xx_message_2().unwrap(); - initiator.read_xx_message_2(&msg2).unwrap(); - assert!(initiator.remote_static().is_some(), "XX: initiator should know responder after msg2"); - assert_eq!(initiator.remote_static().unwrap(), &responder_keypair.public_key()); - assert!(responder.remote_static().is_none(), "XX: responder should NOT know initiator after msg2"); - - // After msg3: responder learns initiator - let msg3 = initiator.write_xx_message_3().unwrap(); - responder.read_xx_message_3(&msg3).unwrap(); - assert!(responder.remote_static().is_some(), "XX: responder should know initiator after msg3"); - assert_eq!(responder.remote_static().unwrap(), &initiator_keypair.public_key()); -} - -#[test] -fn test_xx_wrong_state_errors() { - let keypair1 = generate_keypair(); - let keypair2 = generate_keypair(); - - // Initiator can't read XX msg1 - let mut initiator = HandshakeState::new_xx_initiator(keypair1); - initiator.set_local_epoch(generate_epoch()); - assert!(initiator.read_xx_message_1(&[0u8; XX_HANDSHAKE_MSG1_SIZE]).is_err()); - - // Initiator can't write msg2 - assert!(initiator.write_xx_message_2().is_err()); - - // Initiator can't write msg3 before msg2 - assert!(initiator.write_xx_message_3().is_err()); - - // Responder can't write msg1 - let mut responder = HandshakeState::new_xx_responder(keypair2); - responder.set_local_epoch(generate_epoch()); - assert!(responder.write_xx_message_1().is_err()); - - // Responder can't read msg3 before msg2 - assert!(responder.read_xx_message_3(&[0u8; XX_HANDSHAKE_MSG3_SIZE]).is_err()); -} - -#[test] -fn test_xx_handshake_hash_differs() { - // XX should produce different handshake hashes from both IK and XK - let keypair1 = generate_keypair(); - let keypair2 = generate_keypair(); - let epoch1 = generate_epoch(); - let epoch2 = generate_epoch(); - - // Complete an IK handshake - let mut ik_init = HandshakeState::new_initiator(keypair1, keypair2.public_key()); - ik_init.set_local_epoch(epoch1); - let mut ik_resp = HandshakeState::new_responder(keypair2); - ik_resp.set_local_epoch(epoch2); - let msg1 = ik_init.write_message_1().unwrap(); - ik_resp.read_message_1(&msg1).unwrap(); - let msg2 = ik_resp.write_message_2().unwrap(); - ik_init.read_message_2(&msg2).unwrap(); - let ik_hash = ik_init.handshake_hash(); - - // Complete an XK handshake with the same keys - let mut xk_init = HandshakeState::new_xk_initiator(keypair1, keypair2.public_key()); - xk_init.set_local_epoch(epoch1); - let mut xk_resp = HandshakeState::new_xk_responder(keypair2); - xk_resp.set_local_epoch(epoch2); - let msg1 = xk_init.write_xk_message_1().unwrap(); - xk_resp.read_xk_message_1(&msg1).unwrap(); - let msg2 = xk_resp.write_xk_message_2().unwrap(); - xk_init.read_xk_message_2(&msg2).unwrap(); - let msg3 = xk_init.write_xk_message_3().unwrap(); - xk_resp.read_xk_message_3(&msg3).unwrap(); - let xk_hash = xk_init.handshake_hash(); - - // Complete an XX handshake with the same keys - let mut xx_init = HandshakeState::new_xx_initiator(keypair1); - xx_init.set_local_epoch(epoch1); - let mut xx_resp = HandshakeState::new_xx_responder(keypair2); - xx_resp.set_local_epoch(epoch2); - let msg1 = xx_init.write_xx_message_1().unwrap(); - xx_resp.read_xx_message_1(&msg1).unwrap(); - let msg2 = xx_resp.write_xx_message_2().unwrap(); - xx_init.read_xx_message_2(&msg2).unwrap(); - let msg3 = xx_init.write_xx_message_3().unwrap(); - xx_resp.read_xx_message_3(&msg3).unwrap(); - let xx_hash = xx_init.handshake_hash(); - - assert_ne!(ik_hash, xx_hash, "IK and XX should produce different handshake hashes"); - assert_ne!(xk_hash, xx_hash, "XK and XX should produce different handshake hashes"); -} - -#[test] -fn test_xx_multiple_messages_after_handshake() { - let keypair1 = generate_keypair(); - let keypair2 = generate_keypair(); - - let mut initiator = HandshakeState::new_xx_initiator(keypair1); - initiator.set_local_epoch(generate_epoch()); - let mut responder = HandshakeState::new_xx_responder(keypair2); - responder.set_local_epoch(generate_epoch()); - - let msg1 = initiator.write_xx_message_1().unwrap(); - responder.read_xx_message_1(&msg1).unwrap(); - let msg2 = responder.write_xx_message_2().unwrap(); - initiator.read_xx_message_2(&msg2).unwrap(); - let msg3 = initiator.write_xx_message_3().unwrap(); - responder.read_xx_message_3(&msg3).unwrap(); - - let mut init_session = initiator.into_session().unwrap(); - let mut resp_session = responder.into_session().unwrap(); - - // Send many messages - for i in 0..100 { - let msg = format!("XX message {}", i); - let ct = init_session.encrypt(msg.as_bytes()).unwrap(); - let pt = resp_session.decrypt(&ct).unwrap(); - assert_eq!(pt, msg.as_bytes()); - } - - assert_eq!(init_session.send_nonce(), 100); - assert_eq!(resp_session.recv_nonce(), 100); -} - -#[test] -fn test_xx_with_odd_parity() { - // XX: no pre-message normalization needed, but ECDH x-only hashing - // must still produce matching shared secrets regardless of parity. - let secp = secp256k1::Secp256k1::new(); - - // Node A (initiator) - even parity key - let sk_a = secp256k1::SecretKey::from_slice( - &hex::decode("0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20") - .unwrap(), - ) - .unwrap(); - let kp_a = secp256k1::Keypair::from_secret_key(&secp, &sk_a); - - // Node B (responder) - odd parity key - let sk_b = secp256k1::SecretKey::from_slice( - &hex::decode("b102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fb0") - .unwrap(), - ) - .unwrap(); - let kp_b = secp256k1::Keypair::from_secret_key(&secp, &sk_b); - let (_, parity_b) = kp_b.public_key().x_only_public_key(); - assert_eq!(parity_b, Parity::Odd, "Test requires odd-parity responder key"); - - let mut initiator = HandshakeState::new_xx_initiator(kp_a); - initiator.set_local_epoch(generate_epoch()); - let mut responder = HandshakeState::new_xx_responder(kp_b); - responder.set_local_epoch(generate_epoch()); - - let msg1 = initiator.write_xx_message_1().unwrap(); - responder.read_xx_message_1(&msg1).unwrap(); - let msg2 = responder.write_xx_message_2().unwrap(); - initiator.read_xx_message_2(&msg2).unwrap(); - let msg3 = initiator.write_xx_message_3().unwrap(); - responder.read_xx_message_3(&msg3).unwrap(); - - assert!(initiator.is_complete()); - assert!(responder.is_complete()); - - let mut sender = initiator.into_session().unwrap(); - let mut receiver = responder.into_session().unwrap(); - - let counter = sender.current_send_counter(); - let ciphertext = sender.encrypt(b"xx parity test").unwrap(); - let plaintext = receiver.decrypt_with_replay_check(&ciphertext, counter).unwrap(); - assert_eq!(plaintext, b"xx parity test"); -} - -#[test] -fn test_xx_invalid_msg_sizes() { - let keypair1 = generate_keypair(); - let keypair2 = generate_keypair(); - - // Wrong size for msg1 - let mut responder = HandshakeState::new_xx_responder(keypair1); - responder.set_local_epoch(generate_epoch()); - assert!(responder.read_xx_message_1(&[0u8; HANDSHAKE_MSG1_SIZE]).is_err()); // IK msg1 size - assert!(responder.read_xx_message_1(&[0u8; 10]).is_err()); - - // Wrong size for msg3 - let mut initiator = HandshakeState::new_xx_initiator(keypair1); - initiator.set_local_epoch(generate_epoch()); - let mut responder = HandshakeState::new_xx_responder(keypair2); - responder.set_local_epoch(generate_epoch()); - - let msg1 = initiator.write_xx_message_1().unwrap(); - responder.read_xx_message_1(&msg1).unwrap(); - let _msg2 = responder.write_xx_message_2().unwrap(); - - // Responder is now in Message2Done, try wrong-size msg3 - assert!(responder.read_xx_message_3(&[0u8; 10]).is_err()); - assert!(responder.read_xx_message_3(&[0u8; XX_HANDSHAKE_MSG3_SIZE + 1]).is_err()); -} diff --git a/src/peer/active.rs b/src/peer/active.rs index ca42030b..5b21243b 100644 --- a/src/peer/active.rs +++ b/src/peer/active.rs @@ -1021,14 +1021,14 @@ impl ActivePeer { })?; // Split msg2 into base XX part and any extra (negotiation payload) - let base_size = crate::noise::XX_HANDSHAKE_MSG2_SIZE; + let base_size = crate::noise::HANDSHAKE_MSG2_SIZE; let (base_msg2, extra) = if msg2_bytes.len() > base_size { (&msg2_bytes[..base_size], Some(&msg2_bytes[base_size..])) } else { (msg2_bytes, None) }; - hs.read_xx_message_2(base_msg2)?; + hs.read_message_2(base_msg2)?; // Must decrypt negotiation payload (if present) to keep hash chain // in sync, even though rekey doesn't use the negotiation result. @@ -1036,7 +1036,7 @@ impl ActivePeer { let _ = hs.decrypt_payload(encrypted_neg)?; } - let msg3 = hs.write_xx_message_3()?; + let msg3 = hs.write_message_3()?; let session = hs.into_session()?; // Clear msg1 resend state @@ -1062,14 +1062,14 @@ impl ActivePeer { })?; // Split msg3 into base XX part and any extra (negotiation payload) - let base_size = crate::noise::XX_HANDSHAKE_MSG3_SIZE; + let base_size = crate::noise::HANDSHAKE_MSG3_SIZE; let (base_msg3, extra) = if msg3_bytes.len() > base_size { (&msg3_bytes[..base_size], Some(&msg3_bytes[base_size..])) } else { (msg3_bytes, None) }; - hs.read_xx_message_3(base_msg3)?; + hs.read_message_3(base_msg3)?; // Must decrypt negotiation payload (if present) to keep hash chain // in sync, even though rekey doesn't use the negotiation result. diff --git a/src/peer/connection.rs b/src/peer/connection.rs index 58ed315d..1f8a8e4b 100644 --- a/src/peer/connection.rs +++ b/src/peer/connection.rs @@ -421,9 +421,9 @@ impl PeerConnection { } // XX initiator: no remote static needed upfront - let mut hs = noise::HandshakeState::new_xx_initiator(our_keypair); + let mut hs = noise::HandshakeState::new_initiator(our_keypair); hs.set_local_epoch(epoch); - let msg1 = hs.write_xx_message_1()?; + let msg1 = hs.write_message_1()?; self.noise_handshake = Some(hs); self.handshake_state = HandshakeState::SentMsg1; @@ -463,14 +463,14 @@ impl PeerConnection { }); } - let mut hs = noise::HandshakeState::new_xx_responder(our_keypair); + let mut hs = noise::HandshakeState::new_responder(our_keypair); hs.set_local_epoch(epoch); // Process XX message 1 (ephemeral only — no identity learned) - hs.read_xx_message_1(message)?; + hs.read_message_1(message)?; // Generate XX message 2 (sends our static + epoch) - let mut msg2 = hs.write_xx_message_2()?; + let mut msg2 = hs.write_message_2()?; // Append encrypted negotiation payload if provided if let Some(payload) = negotiation_payload { @@ -515,7 +515,7 @@ impl PeerConnection { .expect("noise handshake must exist in SentMsg1 state"); // Split msg2 into base XX part and optional negotiation - let base_size = noise::XX_HANDSHAKE_MSG2_SIZE; + let base_size = noise::HANDSHAKE_MSG2_SIZE; let (base_msg2, extra) = if message.len() > base_size { (&message[..base_size], Some(&message[base_size..])) } else { @@ -523,7 +523,7 @@ impl PeerConnection { }; // Process XX msg2 (learns responder identity + epoch) - hs.read_xx_message_2(base_msg2)?; + hs.read_message_2(base_msg2)?; // Decrypt negotiation payload from msg2 if present let received_negotiation = if let Some(encrypted) = extra { @@ -542,7 +542,7 @@ impl PeerConnection { self.remote_epoch = hs.remote_epoch(); // Generate XX msg3 - let mut msg3 = hs.write_xx_message_3()?; + let mut msg3 = hs.write_message_3()?; // Append encrypted negotiation payload if provided if let Some(payload) = negotiation_payload { @@ -584,7 +584,7 @@ impl PeerConnection { .expect("noise handshake must exist in ReceivedMsg1 state"); // Split msg3 into base XX part and optional negotiation - let base_size = noise::XX_HANDSHAKE_MSG3_SIZE; + let base_size = noise::HANDSHAKE_MSG3_SIZE; let (base_msg3, extra) = if message.len() > base_size { (&message[..base_size], Some(&message[base_size..])) } else { @@ -592,7 +592,7 @@ impl PeerConnection { }; // Process XX msg3 (learns initiator identity + epoch) - hs.read_xx_message_3(base_msg3)?; + hs.read_message_3(base_msg3)?; // Decrypt negotiation payload from msg3 if present let received_negotiation = if let Some(encrypted) = extra { diff --git a/src/protocol/link.rs b/src/protocol/link.rs index 1c4781da..c2d4c7d8 100644 --- a/src/protocol/link.rs +++ b/src/protocol/link.rs @@ -10,27 +10,32 @@ use std::fmt; /// Handshake message type identifiers. /// -/// These messages are exchanged during Noise IK handshake before link +/// These messages are exchanged during Noise XX handshake before link /// encryption is established. They use the same TLV framing as link /// messages but payloads are not encrypted (except Noise-internal encryption). #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[repr(u8)] pub enum HandshakeMessageType { - /// Noise IK message 1: initiator sends ephemeral + encrypted static. - /// Payload: 82 bytes (33 ephemeral + 33 static + 16 tag). - NoiseIKMsg1 = 0x01, + /// Noise XX message 1: initiator sends ephemeral key. + /// Payload: 33 bytes (ephemeral pubkey). + Msg1 = 0x01, - /// Noise IK message 2: responder sends ephemeral. - /// Payload: 33 bytes (ephemeral pubkey only). - NoiseIKMsg2 = 0x02, + /// Noise XX message 2: responder sends ephemeral + encrypted static + epoch. + /// Payload: 106+ bytes (33 ephemeral + 49 encrypted static + 24 encrypted epoch + negotiation). + Msg2 = 0x02, + + /// Noise XX message 3: initiator sends encrypted static + epoch. + /// Payload: 73+ bytes (49 encrypted static + 24 encrypted epoch + negotiation). + Msg3 = 0x03, } impl HandshakeMessageType { /// Try to convert from a byte. pub fn from_byte(b: u8) -> Option { match b { - 0x01 => Some(HandshakeMessageType::NoiseIKMsg1), - 0x02 => Some(HandshakeMessageType::NoiseIKMsg2), + 0x01 => Some(HandshakeMessageType::Msg1), + 0x02 => Some(HandshakeMessageType::Msg2), + 0x03 => Some(HandshakeMessageType::Msg3), _ => None, } } @@ -42,15 +47,16 @@ impl HandshakeMessageType { /// Check if a byte represents a handshake message type. pub fn is_handshake(b: u8) -> bool { - matches!(b, 0x01 | 0x02) + matches!(b, 0x01..=0x03) } } impl fmt::Display for HandshakeMessageType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let name = match self { - HandshakeMessageType::NoiseIKMsg1 => "NoiseIKMsg1", - HandshakeMessageType::NoiseIKMsg2 => "NoiseIKMsg2", + HandshakeMessageType::Msg1 => "Msg1", + HandshakeMessageType::Msg2 => "Msg2", + HandshakeMessageType::Msg3 => "Msg3", }; write!(f, "{}", name) } @@ -64,7 +70,7 @@ impl fmt::Display for HandshakeMessageType { /// /// These messages are exchanged between directly connected peers over /// Noise-encrypted links. All payloads are encrypted with session keys -/// established during the Noise IK handshake. +/// established during the Noise XX handshake. #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[repr(u8)] pub enum LinkMessageType { @@ -391,8 +397,9 @@ mod tests { #[test] fn test_handshake_message_type_roundtrip() { let types = [ - HandshakeMessageType::NoiseIKMsg1, - HandshakeMessageType::NoiseIKMsg2, + HandshakeMessageType::Msg1, + HandshakeMessageType::Msg2, + HandshakeMessageType::Msg3, ]; for ty in types { @@ -405,7 +412,7 @@ mod tests { #[test] fn test_handshake_message_type_invalid() { assert!(HandshakeMessageType::from_byte(0x00).is_none()); - assert!(HandshakeMessageType::from_byte(0x03).is_none()); + assert!(HandshakeMessageType::from_byte(0x04).is_none()); assert!(HandshakeMessageType::from_byte(0x10).is_none()); } @@ -413,7 +420,9 @@ mod tests { fn test_handshake_message_type_is_handshake() { assert!(HandshakeMessageType::is_handshake(0x01)); assert!(HandshakeMessageType::is_handshake(0x02)); + assert!(HandshakeMessageType::is_handshake(0x03)); assert!(!HandshakeMessageType::is_handshake(0x00)); + assert!(!HandshakeMessageType::is_handshake(0x04)); assert!(!HandshakeMessageType::is_handshake(0x10)); } diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs index f0496c15..98bff44b 100644 --- a/src/protocol/mod.rs +++ b/src/protocol/mod.rs @@ -8,7 +8,7 @@ //! links. Includes spanning tree gossip, bloom filter propagation, discovery //! protocol, and forwarding of session-layer datagrams. //! -//! Link-layer peer authentication uses Noise IK (see `noise.rs`), which +//! Link-layer peer authentication uses Noise XX (see `noise.rs`), which //! establishes the encrypted channel before any of these messages are sent. //! //! ## Session Layer (end-to-end, between FIPS addresses) diff --git a/src/protocol/session.rs b/src/protocol/session.rs index b5efa6e5..b28365cc 100644 --- a/src/protocol/session.rs +++ b/src/protocol/session.rs @@ -330,7 +330,7 @@ impl FspInnerFlags { /// /// Carried inside a SessionDatagram envelope which provides src_addr and /// dest_addr. The SessionSetup payload contains coordinates, session flags, -/// and the Noise IK handshake message for session establishment. +/// and the Noise XX handshake message for session establishment. /// /// ## Wire Format /// @@ -343,7 +343,7 @@ impl FspInnerFlags { /// | ... | dest_coords_count| 2 bytes | u16 LE, number of dest coord entries| /// | ... | dest_coords | 16 × m | NodeAddr array (dest → root) | /// | ... | handshake_len | 2 bytes | u16 LE, Noise payload length | -/// | ... | handshake_payload| variable| Noise IK msg1 (82 bytes typical) | +/// | ... | handshake_payload| variable| Noise XX msg1 (33 bytes — ephemeral key) | #[derive(Clone, Debug)] pub struct SessionSetup { /// Source coordinates (for return path caching). @@ -352,7 +352,7 @@ pub struct SessionSetup { pub dest_coords: TreeCoordinate, /// Session options. pub flags: SessionFlags, - /// Noise IK handshake message 1. + /// Noise XX handshake message 1. pub handshake_payload: Vec, } @@ -456,7 +456,7 @@ impl SessionSetup { /// dest_addr. The SessionAck payload contains both the acknowledger's and /// initiator's coordinates for route cache warming (ensuring return-path /// transit nodes can route independently of the forward path) and the Noise -/// IK handshake response. +/// XX handshake response. /// /// ## Wire Format /// @@ -469,7 +469,7 @@ impl SessionSetup { /// | ... | dest_coords_count| 2 bytes | u16 LE | /// | ... | dest_coords | 16 × m | Initiator's coords (for return path)| /// | ... | handshake_len | 2 bytes | u16 LE, Noise payload length | -/// | ... | handshake_payload| variable| Noise IK msg2 (33 bytes typical) | +/// | ... | handshake_payload| variable| Noise XX msg2 (106+ bytes — ephemeral + static + epoch + negotiation) | #[derive(Clone, Debug)] pub struct SessionAck { /// Acknowledger's coordinates. @@ -478,7 +478,7 @@ pub struct SessionAck { pub dest_coords: TreeCoordinate, /// Reserved flags byte (for forward compatibility). pub flags: u8, - /// Noise IK handshake message 2. + /// Noise XX handshake message 2. pub handshake_payload: Vec, } @@ -567,10 +567,10 @@ impl SessionAck { } // ============================================================================ -// Session Msg3 (XK Handshake Message 3) +// Session Msg3 (XX Handshake Message 3) // ============================================================================ -/// XK handshake message 3 (initiator -> responder). +/// XX handshake message 3 (initiator -> responder). /// /// Carries the initiator's encrypted static key and epoch. Sent by the /// initiator after receiving msg2. The responder learns the initiator's @@ -582,12 +582,12 @@ impl SessionAck { /// |--------|------------------|---------|-------------------------------------| /// | 0 | flags | 1 byte | Reserved | /// | 1 | handshake_len | 2 bytes | u16 LE, Noise payload length | -/// | 3 | handshake_payload| variable| Noise XK msg3 (73 bytes typical) | +/// | 3 | handshake_payload| variable| Noise XX msg3 (73 bytes typical) | #[derive(Clone, Debug)] pub struct SessionMsg3 { /// Reserved flags byte. pub flags: u8, - /// Noise XK handshake message 3. + /// Noise XX handshake message 3. pub handshake_payload: Vec, } @@ -1240,7 +1240,7 @@ mod tests { #[test] fn test_session_setup_encode_decode() { - let handshake = vec![0xAA; 82]; // typical Noise IK msg1 + let handshake = vec![0xAA; 82]; // typical Noise XX msg1 let setup = SessionSetup::new(make_coords(&[1, 2, 0]), make_coords(&[3, 4, 0])) .with_flags(SessionFlags::new().with_ack().bidirectional()) .with_handshake(handshake.clone()); @@ -1276,7 +1276,7 @@ mod tests { #[test] fn test_session_ack_encode_decode() { - let handshake = vec![0xBB; 33]; // typical Noise IK msg2 + let handshake = vec![0xBB; 33]; // typical Noise XX msg2 let ack = SessionAck::new(make_coords(&[7, 8, 0]), make_coords(&[3, 4, 0])) .with_handshake(handshake.clone()); @@ -1666,7 +1666,7 @@ mod tests { #[test] fn test_session_msg3_encode_decode() { - let handshake = vec![0xCC; 73]; // typical XK msg3 + let handshake = vec![0xCC; 73]; // typical XX msg3 let msg3 = SessionMsg3::new(handshake.clone()); let encoded = msg3.encode(); diff --git a/src/transport/ble/discovery.rs b/src/transport/ble/discovery.rs index 364b677d..bb8f4b4a 100644 --- a/src/transport/ble/discovery.rs +++ b/src/transport/ble/discovery.rs @@ -45,7 +45,7 @@ impl DiscoveryBuffer { /// /// Used after the pre-handshake pubkey exchange confirms the peer's /// identity. The pubkey_hint enables the node's auto-connect path - /// to initiate the IK handshake. + /// to initiate the XX handshake. pub fn add_peer_with_pubkey(&self, addr: &BleAddr, pubkey: XOnlyPublicKey) { let ta = addr.to_transport_addr(); let peer = DiscoveredPeer::with_hint(self.transport_id, ta.clone(), pubkey); diff --git a/src/transport/ble/mod.rs b/src/transport/ble/mod.rs index 842037b2..fbb206e1 100644 --- a/src/transport/ble/mod.rs +++ b/src/transport/ble/mod.rs @@ -96,7 +96,7 @@ pub struct BleTransport { /// /// BLE advertisements carry only the FIPS UUID, not the pubkey. /// After L2CAP connection, both sides exchange `[0x00][pubkey:32]` - /// so the node layer can initiate the IK handshake. + /// so the node layer can initiate the XX handshake. /// Temporary — removed when FMP switches to XX. local_pubkey: Option<[u8; 32]>, } @@ -662,7 +662,7 @@ impl Transport for BleTransport { /// Pre-handshake pubkey exchange prefix byte. /// /// Distinguishes the identity exchange from FMP packets (version ≥ 0x01). -/// Temporary — removed when FMP switches from IK to XX handshake. +/// Temporary — removed when FMP uses XX handshake for BLE. const PUBKEY_EXCHANGE_PREFIX: u8 = 0x00; /// Pre-handshake pubkey exchange message size: `[0x00][pubkey:32]`.