mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-09 08:14:42 +00:00
The session-layer handshake now uses the 3-message XK pattern instead of the 2-message IK pattern, providing stronger initiator identity hiding. The initiator static key is deferred to msg3 and encrypted under the es+ee DH chain, so eavesdroppers cannot identify the initiator from the handshake. XK pattern: -> e, es (msg1) / <- e, ee + epoch (msg2) / -> s, se + epoch (msg3) Key changes: - Add XK handshake methods alongside existing IK methods in noise module - Add SessionMsg3 wire format and FSP_PHASE_MSG3 (0x03) prefix - Replace Responding state with AwaitingMsg3 in session state machine - Rewrite session handlers: handle_session_setup defers identity to msg3, handle_session_ack processes msg2 and sends msg3, new handle_session_msg3 completes the responder handshake and registers identity - Link-layer (FMP) continues to use Noise IK unchanged - Add comprehensive XK unit tests and update all integration tests
263 lines
9.1 KiB
Rust
263 lines
9.1 KiB
Rust
//! End-to-end session state.
|
|
//!
|
|
//! Tracks Noise XK sessions between this node and remote endpoints.
|
|
//! Sessions are established via a three-message XK handshake
|
|
//! (SessionSetup/SessionAck/SessionMsg3) carried inside SessionDatagram
|
|
//! envelopes through the mesh.
|
|
|
|
use crate::config::SessionMmpConfig;
|
|
use crate::mmp::MmpSessionState;
|
|
use crate::noise::{HandshakeState, NoiseSession};
|
|
use crate::NodeAddr;
|
|
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.
|
|
Initiating(HandshakeState),
|
|
/// XK responder: processed msg1, sent msg2, awaiting msg3.
|
|
/// Transitions to Established when msg3 arrives.
|
|
AwaitingMsg3(HandshakeState),
|
|
/// Handshake complete, NoiseSession available for encrypt/decrypt.
|
|
Established(NoiseSession),
|
|
}
|
|
|
|
impl EndToEndState {
|
|
/// Check if the session is established and ready for data.
|
|
pub(crate) fn is_established(&self) -> bool {
|
|
matches!(self, EndToEndState::Established(_))
|
|
}
|
|
|
|
/// Check if we are the initiator (waiting for ack).
|
|
pub(crate) fn is_initiating(&self) -> bool {
|
|
matches!(self, EndToEndState::Initiating(_))
|
|
}
|
|
|
|
/// Check if we are an XK responder awaiting msg3.
|
|
pub(crate) fn is_awaiting_msg3(&self) -> bool {
|
|
matches!(self, EndToEndState::AwaitingMsg3(_))
|
|
}
|
|
}
|
|
|
|
/// A single end-to-end session with a remote node.
|
|
///
|
|
/// The state is wrapped in `Option` to allow taking ownership of the
|
|
/// handshake state during transitions without placeholder values.
|
|
/// The state is `None` only transiently during handler processing.
|
|
pub(crate) struct SessionEntry {
|
|
/// Remote node's address (session table key).
|
|
#[allow(dead_code)]
|
|
remote_addr: NodeAddr,
|
|
/// Remote node's static public key.
|
|
remote_pubkey: PublicKey,
|
|
/// Current session state. `None` only during state transitions.
|
|
state: Option<EndToEndState>,
|
|
/// When the session was created (Unix milliseconds).
|
|
#[cfg_attr(not(test), allow(dead_code))]
|
|
created_at: u64,
|
|
/// Last application data activity timestamp (Unix milliseconds).
|
|
/// Only updated for DataPacket send/receive and session establishment.
|
|
/// MMP reports do not update this field. Used for idle session timeout.
|
|
last_activity: u64,
|
|
/// When the session transitioned to Established (Unix milliseconds).
|
|
/// Used to compute session-relative timestamps for the FSP inner header.
|
|
/// Set to 0 until the session is established.
|
|
session_start_ms: u64,
|
|
/// Remaining data packets that should include COORDS_PRESENT.
|
|
/// Initialized from config when session becomes Established;
|
|
/// reset on CoordsRequired receipt.
|
|
coords_warmup_remaining: u8,
|
|
/// Whether this node initiated the Noise handshake.
|
|
/// Used for spin bit role assignment in session-layer MMP.
|
|
is_initiator: bool,
|
|
/// Session-layer MMP state. Initialized on Established transition.
|
|
mmp: Option<MmpSessionState>,
|
|
|
|
// === Handshake Resend ===
|
|
/// Encoded session-layer payload for resend (SessionSetup or SessionAck).
|
|
/// Cleared on Established transition.
|
|
handshake_payload: Option<Vec<u8>>,
|
|
/// Number of resends performed.
|
|
resend_count: u32,
|
|
/// When the next resend should fire (Unix ms). 0 = no resend scheduled.
|
|
next_resend_at_ms: u64,
|
|
}
|
|
|
|
impl SessionEntry {
|
|
/// Create a new session entry.
|
|
pub(crate) fn new(
|
|
remote_addr: NodeAddr,
|
|
remote_pubkey: PublicKey,
|
|
state: EndToEndState,
|
|
now_ms: u64,
|
|
is_initiator: bool,
|
|
) -> Self {
|
|
Self {
|
|
remote_addr,
|
|
remote_pubkey,
|
|
state: Some(state),
|
|
created_at: now_ms,
|
|
last_activity: now_ms,
|
|
session_start_ms: 0,
|
|
coords_warmup_remaining: 0,
|
|
is_initiator,
|
|
mmp: None,
|
|
handshake_payload: None,
|
|
resend_count: 0,
|
|
next_resend_at_ms: 0,
|
|
}
|
|
}
|
|
|
|
/// Get the remote node's public key.
|
|
pub(crate) fn remote_pubkey(&self) -> &PublicKey {
|
|
&self.remote_pubkey
|
|
}
|
|
|
|
/// Get the current session state.
|
|
#[cfg(test)]
|
|
pub(crate) fn state(&self) -> &EndToEndState {
|
|
self.state.as_ref().expect("session state taken but not restored")
|
|
}
|
|
|
|
/// Get mutable access to the session state.
|
|
pub(crate) fn state_mut(&mut self) -> &mut EndToEndState {
|
|
self.state.as_mut().expect("session state taken but not restored")
|
|
}
|
|
|
|
/// Replace the session state.
|
|
pub(crate) fn set_state(&mut self, state: EndToEndState) {
|
|
self.state = Some(state);
|
|
}
|
|
|
|
/// Take the state out, leaving `None`.
|
|
///
|
|
/// The caller must call `set_state()` to restore a valid state,
|
|
/// or discard the entry entirely.
|
|
pub(crate) fn take_state(&mut self) -> Option<EndToEndState> {
|
|
self.state.take()
|
|
}
|
|
|
|
/// Update the last application data activity timestamp.
|
|
///
|
|
/// Only call for DataPacket send/receive and session establishment,
|
|
/// not for MMP reports. Used by the idle session timeout.
|
|
pub(crate) fn touch(&mut self, now_ms: u64) {
|
|
self.last_activity = now_ms;
|
|
}
|
|
|
|
/// Check if the session is established.
|
|
pub(crate) fn is_established(&self) -> bool {
|
|
self.state.as_ref().is_some_and(|s| s.is_established())
|
|
}
|
|
|
|
/// Check if we are the initiator (waiting for ack).
|
|
pub(crate) fn is_initiating(&self) -> bool {
|
|
self.state.as_ref().is_some_and(|s| s.is_initiating())
|
|
}
|
|
|
|
/// Check if we are an XK responder awaiting msg3.
|
|
pub(crate) fn is_awaiting_msg3(&self) -> bool {
|
|
self.state.as_ref().is_some_and(|s| s.is_awaiting_msg3())
|
|
}
|
|
|
|
/// Get creation time.
|
|
#[cfg(test)]
|
|
pub(crate) fn created_at(&self) -> u64 {
|
|
self.created_at
|
|
}
|
|
|
|
/// Get last activity time.
|
|
pub(crate) fn last_activity(&self) -> u64 {
|
|
self.last_activity
|
|
}
|
|
|
|
/// Remaining DataPackets that should include COORDS_PRESENT.
|
|
pub(crate) fn coords_warmup_remaining(&self) -> u8 {
|
|
self.coords_warmup_remaining
|
|
}
|
|
|
|
/// Set the coords warmup counter (used on Established transition
|
|
/// and CoordsRequired reset).
|
|
pub(crate) fn set_coords_warmup_remaining(&mut self, value: u8) {
|
|
self.coords_warmup_remaining = value;
|
|
}
|
|
|
|
/// Mark the session as started (transition to Established).
|
|
///
|
|
/// Records the current time as the session start for computing
|
|
/// session-relative timestamps in the FSP inner header.
|
|
pub(crate) fn mark_established(&mut self, now_ms: u64) {
|
|
self.session_start_ms = now_ms;
|
|
}
|
|
|
|
/// Compute a session-relative timestamp for the FSP inner header.
|
|
///
|
|
/// Returns `(now_ms - session_start_ms)` truncated to u32.
|
|
/// Wraps naturally at ~49.7 days, which is fine for relative timing.
|
|
pub(crate) fn session_timestamp(&self, now_ms: u64) -> u32 {
|
|
now_ms.wrapping_sub(self.session_start_ms) as u32
|
|
}
|
|
|
|
/// Whether this node initiated the Noise handshake.
|
|
#[cfg_attr(not(test), allow(dead_code))]
|
|
pub(crate) fn is_initiator(&self) -> bool {
|
|
self.is_initiator
|
|
}
|
|
|
|
/// Get a reference to the session-layer MMP state, if initialized.
|
|
pub(crate) fn mmp(&self) -> Option<&MmpSessionState> {
|
|
self.mmp.as_ref()
|
|
}
|
|
|
|
/// Get a mutable reference to the session-layer MMP state, if initialized.
|
|
pub(crate) fn mmp_mut(&mut self) -> Option<&mut MmpSessionState> {
|
|
self.mmp.as_mut()
|
|
}
|
|
|
|
/// Initialize session-layer MMP state (called on Established transition).
|
|
pub(crate) fn init_mmp(&mut self, config: &SessionMmpConfig) {
|
|
self.mmp = Some(MmpSessionState::new(config, self.is_initiator));
|
|
}
|
|
|
|
// === Handshake Resend ===
|
|
|
|
/// Store the encoded session-layer payload for potential resend.
|
|
///
|
|
/// For initiators, this is the SessionSetup payload bytes.
|
|
/// For responders, this is the SessionAck payload bytes.
|
|
/// The payload is re-wrapped in a fresh SessionDatagram on each resend
|
|
/// so routing can adapt to topology changes.
|
|
pub(crate) fn set_handshake_payload(&mut self, payload: Vec<u8>, next_resend_at_ms: u64) {
|
|
self.handshake_payload = Some(payload);
|
|
self.resend_count = 0;
|
|
self.next_resend_at_ms = next_resend_at_ms;
|
|
}
|
|
|
|
/// Get the stored handshake payload for resend.
|
|
pub(crate) fn handshake_payload(&self) -> Option<&[u8]> {
|
|
self.handshake_payload.as_deref()
|
|
}
|
|
|
|
/// Clear the stored handshake payload (called on Established transition).
|
|
pub(crate) fn clear_handshake_payload(&mut self) {
|
|
self.handshake_payload = None;
|
|
self.next_resend_at_ms = 0;
|
|
}
|
|
|
|
/// Number of resends performed so far.
|
|
pub(crate) fn resend_count(&self) -> u32 {
|
|
self.resend_count
|
|
}
|
|
|
|
/// When the next resend should fire (Unix ms). 0 = no resend scheduled.
|
|
pub(crate) fn next_resend_at_ms(&self) -> u64 {
|
|
self.next_resend_at_ms
|
|
}
|
|
|
|
/// Record a resend and schedule the next one.
|
|
pub(crate) fn record_resend(&mut self, next_resend_at_ms: u64) {
|
|
self.resend_count += 1;
|
|
self.next_resend_at_ms = next_resend_at_ms;
|
|
}
|
|
}
|