Files
fips/src/noise/session.rs
T
Johnathan Corgan d8cb4d407e FLP wire format revision and MMP link-layer measurement protocol
## FLP Wire Format Revision

Replace the 1-byte discriminator with a structured wire format:

- 4-byte common prefix (ver+phase, flags, payload_len) and 16-byte
  established frame header with AEAD AAD binding
- 5-byte encrypted inner header (4-byte session-relative timestamp +
  1-byte message type) on all link messages
- Phase-based packet dispatch replacing discriminator-based dispatch
- SessionDatagram reassigned from type 0x40 to 0x00; add SenderReport
  (0x01) and ReceiverReport (0x02) message types for MMP
- SessionDatagram: rename hop_limit to ttl, add path_mtu field (u16 LE)
  with min(datagram.path_mtu, transport.mtu()) at forwarding
- Updated handshake packets (msg1: 87->90 bytes, msg2: 42->45 bytes)
- FIPS_OVERHEAD updated from 135 to 144 bytes

## MMP Link-Layer Measurement Protocol

Add the Metrics Measurement Protocol for link quality measurement
between FIPS peers. Measures RTT, loss, jitter, throughput, OWD trend,
and ETX from periodic sender/receiver reports exchanged over established
links.

Module layout:
- mmp/algorithms.rs: JitterEstimator, SrttEstimator, DualEwma, OwdTrend,
  SpinBit, ETX computation
- mmp/report.rs: SenderReport (48B) and ReceiverReport (68B) wire format
- mmp/sender.rs: per-peer TX counters and interval tracking
- mmp/receiver.rs: per-peer RX counters, jitter, loss, gap tracking
- mmp/metrics.rs: derived metrics from report processing (SRTT, goodput_bps)
- mmp/mod.rs: MmpMode (Full/Lightweight/Minimal), MmpConfig, MmpPeerState
- node/handlers/mmp.rs: report dispatch, timer-driven generation, operator
  logging (periodic + teardown)

Integration: per-frame TX/RX hooks in encrypted message handling, report
dispatch from link message router, timer-driven generation from tick
handler, and periodic operator logging with throughput formatting.

Three operating modes: Full (sender + receiver reports, spin bit, CE echo),
Lightweight (receiver reports only), Minimal (spin bit + CE echo only).

## Design Documentation

Updated FLP sections across all design documents to match the implemented
wire format, including revised overhead calculations and numeric values.

568 tests pass, clippy clean.
2026-02-18 21:54:21 +00:00

195 lines
6.4 KiB
Rust

use super::{CipherState, HandshakeRole, NoiseError, ReplayWindow};
use secp256k1::{PublicKey, XOnlyPublicKey};
use std::fmt;
/// Completed Noise session for transport encryption.
///
/// Provides bidirectional authenticated encryption with replay protection.
/// The send counter is monotonically incremented; received counters are
/// validated against a sliding window to prevent replay attacks.
pub struct NoiseSession {
/// Our role in the original handshake.
role: HandshakeRole,
/// Cipher for sending.
send_cipher: CipherState,
/// Cipher for receiving.
recv_cipher: CipherState,
/// Handshake hash for channel binding.
handshake_hash: [u8; 32],
/// Remote peer's static public key.
remote_static: PublicKey,
/// Replay window for received packets.
replay_window: ReplayWindow,
}
impl NoiseSession {
/// Create a new session from completed handshake data.
pub(super) fn from_handshake(
role: HandshakeRole,
send_cipher: CipherState,
recv_cipher: CipherState,
handshake_hash: [u8; 32],
remote_static: PublicKey,
) -> Self {
Self {
role,
send_cipher,
recv_cipher,
handshake_hash,
remote_static,
replay_window: ReplayWindow::new(),
}
}
/// Encrypt a message for sending (using internal counter).
///
/// Returns the ciphertext. The current send counter should be included
/// in the wire format before calling this method.
pub fn encrypt(&mut self, plaintext: &[u8]) -> Result<Vec<u8>, NoiseError> {
self.send_cipher.encrypt(plaintext)
}
/// Get the current send counter (before incrementing).
///
/// Use this to get the counter to include in the wire format.
/// The counter will be incremented when `encrypt` is called.
pub fn current_send_counter(&self) -> u64 {
self.send_cipher.nonce
}
/// Decrypt a received message (using internal counter).
///
/// This is for handshake-phase decryption. For transport phase with
/// explicit counters, use `decrypt_with_replay_check` instead.
pub fn decrypt(&mut self, ciphertext: &[u8]) -> Result<Vec<u8>, NoiseError> {
self.recv_cipher.decrypt(ciphertext)
}
/// Check if a counter passes the replay window.
///
/// Returns Ok(()) if the counter is acceptable, Err if it should be rejected.
/// Call this before attempting decryption to avoid wasting CPU on replay attacks.
pub fn check_replay(&self, counter: u64) -> Result<(), NoiseError> {
if self.replay_window.check(counter) {
Ok(())
} else {
Err(NoiseError::ReplayDetected(counter))
}
}
/// Decrypt with explicit counter and replay protection.
///
/// This is the primary decryption method for transport phase.
/// The counter comes from the wire format and is validated against
/// the replay window before and after decryption.
///
/// On success, the counter is accepted into the replay window.
pub fn decrypt_with_replay_check(
&mut self,
ciphertext: &[u8],
counter: u64,
) -> Result<Vec<u8>, NoiseError> {
// Check replay window first (cheap)
if !self.replay_window.check(counter) {
return Err(NoiseError::ReplayDetected(counter));
}
// Attempt decryption (expensive)
let plaintext = self.recv_cipher.decrypt_with_counter(ciphertext, counter)?;
// Only accept into window after successful decryption
// This prevents DoS attacks that exhaust the window
self.replay_window.accept(counter);
Ok(plaintext)
}
/// Encrypt a message with Additional Authenticated Data (AAD).
///
/// Returns the ciphertext. The current send counter should be included
/// in the wire format before calling this method.
pub fn encrypt_with_aad(
&mut self,
plaintext: &[u8],
aad: &[u8],
) -> Result<Vec<u8>, NoiseError> {
self.send_cipher.encrypt_with_aad(plaintext, aad)
}
/// Decrypt with explicit counter, replay protection, and AAD.
///
/// This is the primary decryption method for the FLP transport phase
/// with AAD binding. The AAD (typically the 16-byte outer header) must
/// match what was used during encryption.
pub fn decrypt_with_replay_check_and_aad(
&mut self,
ciphertext: &[u8],
counter: u64,
aad: &[u8],
) -> Result<Vec<u8>, NoiseError> {
// Check replay window first (cheap)
if !self.replay_window.check(counter) {
return Err(NoiseError::ReplayDetected(counter));
}
// Attempt decryption with AAD (expensive)
let plaintext = self.recv_cipher.decrypt_with_counter_and_aad(ciphertext, counter, aad)?;
// Only accept into window after successful decryption
self.replay_window.accept(counter);
Ok(plaintext)
}
/// Get the highest received counter.
pub fn highest_received_counter(&self) -> u64 {
self.replay_window.highest()
}
/// Reset the replay window (use when rekeying).
pub fn reset_replay_window(&mut self) {
self.replay_window.reset();
}
/// Get the handshake hash for channel binding.
pub fn handshake_hash(&self) -> &[u8; 32] {
&self.handshake_hash
}
/// Get the remote peer's static public key.
pub fn remote_static(&self) -> &PublicKey {
&self.remote_static
}
/// Get the remote peer's x-only public key.
pub fn remote_static_xonly(&self) -> XOnlyPublicKey {
self.remote_static.x_only_public_key().0
}
/// Get our role in the handshake.
pub fn role(&self) -> HandshakeRole {
self.role
}
/// Get the send nonce (for debugging).
pub fn send_nonce(&self) -> u64 {
self.send_cipher.nonce()
}
/// Get the receive nonce (for debugging).
pub fn recv_nonce(&self) -> u64 {
self.recv_cipher.nonce()
}
}
impl fmt::Debug for NoiseSession {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("NoiseSession")
.field("role", &self.role)
.field("send_nonce", &self.send_cipher.nonce())
.field("recv_nonce", &self.recv_cipher.nonce())
.field("handshake_hash", &hex::encode(&self.handshake_hash[..8]))
.finish()
}
}