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
This commit is contained in:
Johnathan Corgan
2026-04-11 08:16:01 +00:00
parent ae0f791dbc
commit 8357200b0e
22 changed files with 431 additions and 1775 deletions
+1 -1
View File
@@ -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();
+7 -7
View File
@@ -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!(
+40 -40
View File
@@ -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),
})?;
+1 -1
View File
@@ -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
+7 -7
View File
@@ -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;
+5 -5
View File
@@ -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).
+8 -17
View File
@@ -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)"
);
+1 -1
View File
@@ -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);
+1 -1
View File
@@ -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);
+1 -1
View File
@@ -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.
+22 -50
View File
@@ -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;
+16 -16
View File
@@ -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<u8> {
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<u8> {
/// Format: `[0x12][0x00][payload_len:2 LE][sender_idx:4 LE][receiver_idx:4 LE][noise_msg2:106+]`
/// The noise_msg2 may include an optional negotiation payload beyond the base XX msg2.
pub fn build_msg2(sender_idx: SessionIndex, receiver_idx: SessionIndex, noise_msg2: &[u8]) -> Vec<u8> {
debug_assert!(noise_msg2.len() >= 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<u8> {
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
+47 -694
View File
@@ -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<Keypair>,
/// 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<PublicKey>,
@@ -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<Vec<u8>, 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<Vec<u8>, 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<Vec<u8>, 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<Vec<u8>, 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<Vec<u8>, 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<Vec<u8>, 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<Vec<u8>, 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<Vec<u8>, NoiseError> {
pub fn write_message_3(&mut self) -> Result<Vec<u8>, 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<Vec<u8>, 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<Vec<u8>, NoiseError> {
self.symmetric.decrypt_and_hash(ciphertext)
+15 -72
View File
@@ -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,
+202 -814
View File
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -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.
+10 -10
View File
@@ -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 {
+25 -16
View File
@@ -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<Self> {
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));
}
+1 -1
View File
@@ -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)
+13 -13
View File
@@ -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<u8>,
}
@@ -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<u8>,
}
@@ -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<u8>,
}
@@ -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();
+1 -1
View File
@@ -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);
+2 -2
View File
@@ -96,7 +96,7 @@ pub struct BleTransport<I: BleIo> {
///
/// 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<I: BleIo> Transport for BleTransport<I> {
/// 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]`.