diff --git a/docs/design/fips-design.md b/docs/design/fips-design.md index 94cfbfc..4126fe8 100644 --- a/docs/design/fips-design.md +++ b/docs/design/fips-design.md @@ -111,153 +111,107 @@ signature = schnorr_sign(nsec, SHA256(message)) **Challenge-response authentication** is used during connection establishment to prove a node controls the private key corresponding to its claimed npub. -### Peer Authentication Protocol +### Peer Authentication Protocol (Noise IK) -When two nodes establish a connection, they perform mutual authentication to -verify each other's identity. This prevents impersonation attacks where an -adversary claims to be a node it doesn't control. +When two nodes establish a connection, they perform mutual authentication using +the Noise Protocol Framework with the IK pattern. This provides: + +- Mutual authentication (both parties prove identity) +- Forward secrecy (ephemeral keys protect past sessions) +- Encrypted link (all subsequent messages are encrypted) -> **Note**: This authentication protocol is not derived from Yggdrasil, which -> relies on transport-layer security (TLS/QUIC) for identity binding. FIPS -> requires an explicit application-layer protocol because it supports transports -> without built-in encryption or key exchange (radio links, serial connections). -> For initial implementation, peer authentication is always performed regardless -> of transport capabilities; this may be optimized in future versions. -> > **Terminology note**: *Peer authentication* (this section) is hop-by-hop—it -> verifies that a direct peer is who they claim to be. This is distinct from -> *crypto sessions* (see [fips-protocol-flow.md](fips-protocol-flow.md) §6), -> which provide end-to-end authenticated encryption between source and -> destination using Noise KK. Both layers are necessary: peer auth secures the -> local link; crypto sessions secure the full path. +> verifies that a direct peer is who they claim to be and establishes an +> encrypted link. This is distinct from *session-layer encryption* which +> provides end-to-end authenticated encryption between FIPS addresses. Both +> layers operate independently: peer auth secures each link; session encryption +> secures the full path. + +#### Protocol: Noise_IK_secp256k1_ChaChaPoly_SHA256 + +The IK pattern is chosen because: + +- **Outbound connections**: Initiator knows responder's static key from config +- **Inbound connections**: Responder learns initiator's identity from message 1 + +```text +Pre-message (known before handshake): + <- s (responder's static key known to initiator) + +Handshake: + -> e, es, s, ss (msg1: initiator sends ephemeral + encrypted static) + <- e, ee, se (msg2: responder sends ephemeral) +``` ```text Initiator (A) Responder (B) │ │ - │───── AuthInit { a_npub, nonce_a } ─────────►│ + │ knows B's npub from config │ │ │ - │◄──── AuthChallenge { b_npub, nonce_b, │ - │ sig_b(nonce_a | a_npub) } ─────────│ - │ │ - │───── AuthComplete { │ - │ sig_a(nonce_b | b_npub) } ────────►│ - │ │ - [A verifies B after msg 2] [B verifies A after msg 3] + │───── msg1 (82 bytes) ──────────────────────►│ + │ e: ephemeral pubkey (33) │ + │ s: encrypted static (33+16) │ + │ │ learns A's identity + │◄───── msg2 (33 bytes) ──────────────────────│ + │ e: ephemeral pubkey (33) │ │ │ ▼ ▼ - Authenticated Authenticated + Noise session established Noise session established + (symmetric keys derived) (symmetric keys derived) ``` -**Protocol flow:** +#### Cryptographic Primitives -1. **AuthInit**: Initiator sends its npub and a 32-byte random nonce -2. **AuthChallenge**: Responder sends its npub, its own nonce, and a signature - proving it controls its nsec (signing the initiator's nonce and npub) -3. **AuthComplete**: Initiator sends a signature proving it controls its nsec - (signing the responder's nonce and npub) +| Component | Choice | Notes | +|-----------|---------------------|---------------------------| +| Curve | secp256k1 | Nostr-native | +| DH | ECDH on secp256k1 | Standard EC Diffie-Hellman| +| AEAD | ChaCha20-Poly1305 | Same as NIP-44 | +| Hash | SHA-256 | Nostr-native | +| KDF | HKDF-SHA256 | Standard Noise KDF | -After message 2, the initiator can verify the responder's identity. After -message 3, the responder can verify the initiator's identity. Both nodes have -now proven they control their claimed private keys. +#### Crossing Connection Handling -### Crossing Connection Handling - -When both nodes have each other as static peers, both may initiate authentication -simultaneously ("crossing hellos"). This is resolved using deterministic -tie-breaking based on npub ordering: +When both nodes simultaneously initiate connections, a deterministic tie-breaker +resolves which connection survives: ```text -A (lower npub) B (higher npub) - │ │ - │─── AuthInit { a_npub, nonce_a } ────────►│ - │◄── AuthInit { b_npub, nonce_b } ─────────│ (crossing) - │ │ - A < B: ignore B's init, B > A: switch to responder, - wait for challenge send AuthChallenge - │ │ - │◄── AuthChallenge { b_npub, ... } ────────│ - │─── AuthComplete { ... } ────────────────►│ - │ │ - ▼ ▼ - Authenticated Authenticated +Rule: Smaller node_id's OUTBOUND connection wins + +If our_node_id < their_node_id: + - Our outbound wins, close our inbound +If our_node_id > their_node_id: + - Our inbound wins, close our outbound ``` -**Rules:** +Both nodes independently reach the same conclusion without coordination. -- If a node receives AuthInit while its own AuthInit is pending to the same peer: - - If local npub < remote npub: Continue as initiator, ignore incoming AuthInit - - If local npub > remote npub: Abort own initiation, switch to responder role +#### Post-Authentication State -This ensures exactly one handshake completes with minimal wasted effort. +After the Noise handshake completes: -### Authentication Message Structures +- **NoiseSession** holds symmetric keys for encrypt/decrypt +- Link is fully encrypted (all subsequent messages use AEAD) +- Peer's verified identity is extracted from the handshake +- Node transitions from `PeerConnection` to `ActivePeer` state -```rust -struct AuthInit { - npub: [u8; 32], // Initiator's public key (x-only) - nonce: [u8; 32], // Random challenge -} +#### Link vs Session Encryption -struct AuthChallenge { - npub: [u8; 32], // Responder's public key (x-only) - nonce: [u8; 32], // Responder's challenge - signature: [u8; 64], // sig(initiator_nonce || initiator_npub) -} +FIPS uses two independent encryption layers: -struct AuthComplete { - signature: [u8; 64], // sig(responder_nonce || responder_npub) -} -``` +| Layer | Scope | Keys | Purpose | +|-------------|-------------|--------------------------|--------------------------------------| +| **Link** | Hop-by-hop | Per-peer Noise session | Encrypt all traffic on this link | +| **Session** | End-to-end | Per-destination session | Encrypt payload across multiple hops | -### Signature Construction +A packet from A→D through B traverses: -Signatures are constructed with domain separation to prevent cross-protocol -signature reuse: +1. A encrypts payload with A↔D session key +2. A encrypts that with A↔B link key, sends to B +3. B decrypts link layer, routes, re-encrypts with B↔C or B↔D link key +4. D decrypts link layer, then decrypts session layer to get payload -```text -digest = SHA256("fips-peer-auth-v1" || peer_nonce || peer_npub) -signature = schnorr_sign(nsec, digest) -``` - -**Domain separation**: The `"fips-peer-auth-v1"` prefix ensures that signatures -created for FIPS peer authentication cannot be replayed in other contexts (e.g., -a Nostr event signature or FIPS crypto session). If the authentication protocol -is revised, the version string changes (e.g., `"fips-peer-auth-v2"`). - -**Nonce freshness**: The 32-byte random nonce from the peer ensures that -signatures cannot be pre-computed. Each authentication attempt requires a -fresh signature over the peer's unique challenge. - -**Binding to peer identity**: The signature includes the peer's npub, binding -the response to that specific peer. This prevents relay attacks where an -adversary forwards a challenge from one node and uses the response with another. - -### Authentication Failure Handling - -If authentication fails at any step: - -- **Invalid signature**: Connection is terminated immediately -- **Wrong npub**: The node is not who it claimed to be; terminate -- **Expired timestamp**: Possible replay attack; terminate -- **Timeout**: Peer did not respond in time; terminate - -Nodes should implement rate limiting on authentication attempts to prevent -denial-of-service attacks that exhaust computational resources through -repeated signature verifications. - -### Post-Authentication State - -After successful authentication, each node stores: - -- The peer's verified npub and derived node_id -- The link over which the peer was authenticated -- Timestamp of successful authentication - -This state is used for: - -- Routing decisions (only forward to authenticated peers) -- TreeAnnounce signature verification (cached public key lookup) -- Session resumption on transient disconnections (within a timeout window) +Intermediate nodes can route but cannot read session-layer payloads. --- @@ -627,36 +581,45 @@ A single node may have multiple transports of different types: ## 6. Protocol Messages +FIPS uses two independent message type spaces corresponding to the two protocol +layers. Messages are exchanged over Noise-encrypted links after peer authentication. + ### Wire Format -``` +```text ┌────────┬────────┬────────────────────────────────────┐ │ Type │ Length │ Payload │ │ 1 byte │ 2 bytes│ Variable │ └────────┴────────┴────────────────────────────────────┘ ``` -### Message Types +### Link Layer Messages (Peer-to-Peer) -| Type | Name | Description | -|------|------|-------------| -| 0x00 | Dummy | Keepalive/padding | -| 0x01 | TreeAnnounce | Spanning tree state | -| 0x02 | BloomUpdate | Bloom filter update | -| 0x03 | Lookup | Destination lookup request | -| 0x04 | LookupResponse | Coordinates for requested key | -| 0x05 | PathBroken | Route failure notification | -| 0x06 | SessionSetup | Routing session + crypto handshake init | -| 0x07 | SessionAck | Routing session ack + crypto response | -| 0x08 | CoordsRequired | Router cache miss notification | -| 0x09 | AuthInit | Peer authentication initiation | -| 0x0a | AuthChallenge | Peer authentication challenge + response | -| 0x0b | AuthComplete | Peer authentication completion | -| 0x10 | Traffic | Encrypted application data | -| 0x11 | TrafficAck | Delivery acknowledgement | +Exchanged between directly connected peers over Noise-encrypted links. +Peer authentication uses Noise IK handshake (see §1) before any messages. -See [fips-routing.md](fips-routing.md) Part 4 for routing session details and -[fips-protocol-flow.md](fips-protocol-flow.md) §5-6 for combined establishment. +| Type | Name | Description | +|------|----------------|--------------------------------------------| +| 0x10 | TreeAnnounce | Spanning tree state announcement | +| 0x20 | FilterAnnounce | Bloom filter reachability update | +| 0x30 | LookupRequest | Request to discover node coordinates | +| 0x31 | LookupResponse | Response with target's coordinates | +| 0x40 | SessionDatagram| Encapsulated session-layer payload | + +### Session Layer Messages (End-to-End) + +Carried inside `SessionDatagram`, encrypted with end-to-end session keys. +Intermediate nodes route based on destination but cannot read the payload. + +| Type | Name | Description | +|------|----------------|--------------------------------------------| +| 0x00 | SessionSetup | Session establishment with coordinates | +| 0x01 | SessionAck | Session acknowledgement | +| 0x10 | DataPacket | Encrypted IPv6 datagram | +| 0x20 | CoordsRequired | Router cache miss notification | +| 0x21 | PathBroken | Route failure notification | + +See [fips-routing.md](fips-routing.md) Part 4 for routing session details. ### TreeAnnounce diff --git a/docs/design/fips-protocol-flow.md b/docs/design/fips-protocol-flow.md index dbc34da..809e0c4 100644 --- a/docs/design/fips-protocol-flow.md +++ b/docs/design/fips-protocol-flow.md @@ -516,62 +516,59 @@ the crypto portion. --- -## 6. Crypto Session Handshake +## 6. Session-Layer Encryption -The crypto session uses the Noise Protocol Framework with secp256k1, aligning -with Nostr's cryptographic primitives. +FIPS uses two independent Noise Protocol handshakes at different layers: -### 6.1 Design Decision: Noise with secp256k1 +| Layer | Scope | Pattern | Purpose | +|---------|-------------|----------|-------------------------------------------| +| Link | Hop-by-hop | Noise IK | Authenticate peers, encrypt link | +| Session | End-to-end | Noise IK | Authenticate endpoints, encrypt payload | -**Decision**: Use Noise Protocol Framework adapted for secp256k1. +Both use `Noise_IK_secp256k1_ChaChaPoly_SHA256` with the same cryptographic +primitives, but with separate keys and sessions. -Rationale: +### 6.1 Why Two Layers? -- Well-analyzed framework with formal security proofs -- Used successfully in WireGuard, Lightning (BOLT 8), Signal -- Lightning Network already adapted Noise for secp256k1 (precedent) -- Reuses Nostr's existing key infrastructure (npub/nsec) -- Provides forward secrecy via ephemeral keys +**Link encryption** protects against passive observers on each hop but allows +intermediate nodes to see routing information (destination address). -### 6.2 Pattern Selection +**Session encryption** protects the actual payload end-to-end. Intermediate +nodes forward opaque ciphertext without being able to read the contents. -Since both parties know each other's static public key (npub) before the -handshake begins (from DNS lookup / identity cache), the **Noise KK** pattern -is appropriate: +### 6.2 Session Noise Handshake + +The session-layer Noise IK handshake is carried inside `SessionSetup`/`SessionAck` +messages, which themselves travel through the link-encrypted channel: ```text -Noise_KK_secp256k1_ChaChaPoly_SHA256 - -KK: - -> e, es, ss - <- e, ee, se +Initiator knows destination npub (from DNS lookup) + │ + ▼ +SessionSetup { coords, handshake_payload: Noise IK msg1 } + │ + ▼ (travels through link-encrypted hops) + │ +Responder processes msg1, learns initiator identity + │ + ▼ +SessionAck { coords, handshake_payload: Noise IK msg2 } + │ + ▼ +Session keys established (independent of link keys) ``` -**Message 1 (Initiator → Responder):** +### 6.3 Cryptographic Primitives -- `e`: Initiator's ephemeral public key -- `es`: DH(initiator_ephemeral, responder_static) -- `ss`: DH(initiator_static, responder_static) +Both link and session layers use the same stack: -**Message 2 (Responder → Initiator):** - -- `e`: Responder's ephemeral public key -- `ee`: DH(initiator_ephemeral, responder_ephemeral) -- `se`: DH(responder_ephemeral, initiator_static) - -After both messages, both parties derive identical symmetric keys for -encryption in each direction. - -### 6.3 Why KK (not IK or XX) - -| Pattern | Knowledge | Messages | FIPS Fit | -|---------|-----------|----------|----------| -| **KK** | Both know both static keys | 1 RT | Best - we have npubs | -| IK | Initiator knows responder | 1 RT | Works but asymmetric | -| XX | Neither knows | 2 RT | Unnecessary overhead | - -KK provides mutual authentication in a single round-trip since FIPS always -knows the peer's npub before initiating (from DNS/identity cache). +| Component | Choice | Notes | +|----------------|---------------------|----------------------------| +| Curve | secp256k1 | Nostr-native | +| DH | ECDH on secp256k1 | Standard EC Diffie-Hellman | +| Cipher | ChaCha20-Poly1305 | AEAD, same as NIP-44 | +| Hash | SHA-256 | Nostr-native | +| Key derivation | HKDF-SHA256 | Standard Noise KDF | ### 6.4 Cryptographic Primitives @@ -683,11 +680,11 @@ this should be updated to reflect AEAD-only authentication. ## 7. Peer Connection Establishment Before any of the traffic flows described above can occur, nodes must establish -authenticated peer connections. This section provides a brief overview; see -[fips-design.md](fips-design.md) §1 for the full peer authentication protocol -and [fips-architecture.md](fips-architecture.md) for the startup sequence. +authenticated peer connections using Noise IK. See [fips-design.md](fips-design.md) +§1 for full protocol details and [fips-architecture.md](fips-architecture.md) +for the startup sequence. -### 7.1 Connection Flow Summary +### 7.1 Connection Flow Summary (Noise IK) **Outbound (to static peer):** @@ -698,43 +695,38 @@ Config: npub + transport hint (e.g., "udp:192.168.1.1:4000") Create link via transport │ ▼ -Send AuthInit { our_npub, nonce } +Noise IK msg1 (82 bytes): ephemeral + encrypted static key │ ▼ -Receive AuthChallenge { peer_npub, peer_nonce, signature } +Receive msg2 (33 bytes): peer's ephemeral key │ ▼ -Verify signature, send AuthComplete { signature } - │ - ▼ -Peer authenticated → begins tree gossip +Noise session established → link encrypted → begins tree gossip ``` **Inbound (peer connects to us):** ```text -Transport receives data from unknown address +Transport receives Noise msg1 from unknown address │ ▼ -Receive AuthInit { peer_npub, nonce } +Process msg1 → learn peer's identity from encrypted static key │ ▼ -Send AuthChallenge { our_npub, our_nonce, signature } +Send msg2 (33 bytes): our ephemeral key │ ▼ -Receive AuthComplete { signature } - │ - ▼ -Verify signature → peer authenticated → begins tree gossip +Noise session established → link encrypted → begins tree gossip ``` ### 7.2 Post-Authentication -After successful peer authentication: +After successful Noise handshake: -1. **TreeAnnounce exchange**: Both peers send their current tree state -2. **FilterAnnounce exchange**: Both peers send their bloom filters -3. **Peer is Active**: Can now participate in routing and forwarding +1. **Link encrypted**: All subsequent messages use AEAD encryption +2. **TreeAnnounce exchange**: Both peers send their current tree state +3. **FilterAnnounce exchange**: Both peers send their bloom filters +4. **Peer is Active**: Can now participate in routing and forwarding The first TreeAnnounce from a new peer may trigger parent reselection if that peer offers a better path to root. @@ -746,34 +738,36 @@ peer offers a better path to root. This section tracks items that need reconciliation with existing design docs or earlier sections of this document. -### 8.1 Completed Updates (Session 40) +### 8.1 Completed Updates (Session 47) -| Location | Status | Notes | -|----------|--------|-------| -| §3.1 | ✓ Done | Updated to AEAD authentication | -| fips-routing.md Part 4 | ✓ Done | Renamed to "Routing Session Establishment", added terminology note | -| fips-routing.md SessionSetup/Ack | ✓ Done | Added `handshake_payload` for crypto handshake | -| fips-design.md §7 Encryption | ✓ Done | Updated to reference Noise KK instead of NIP-44 | -| fips-design.md §6 Messages | ✓ Done | Added SessionSetup, SessionAck, CoordsRequired types | -| fips-design.md §1 Peer Auth | ✓ Done | Added terminology note distinguishing peer auth from crypto sessions | -| fips-architecture.md Config | ✓ Done | Renamed to "Routing Session", added "Crypto Session" section | +| Location | Status | Notes | +|----------------------------------|--------|----------------------------------------------------| +| fips-design.md §1 Peer Auth | ✓ Done | Replaced custom handshake with Noise IK | +| fips-design.md §6 Messages | ✓ Done | Split into LinkMessageType + SessionMessageType | +| protocol.rs | ✓ Done | Removed Hello/Challenge/Auth/AuthAck types | +| protocol.rs | ✓ Done | Added SessionDatagram for link-layer encapsulation | +| This document §6 | ✓ Done | Updated to two-layer Noise IK architecture | +| This document §7 | ✓ Done | Updated connection flow for Noise IK | -### 8.2 Cross-References Added +### 8.2 Previous Updates (Session 40) -All design docs now reference fips-protocol-flow.md in their References sections: - -- fips-design.md -- fips-routing.md -- fips-architecture.md +| Location | Status | Notes | +|----------------------------------|--------|-----------------------------------------------------| +| §3.1 | ✓ Done | Updated to AEAD authentication | +| fips-routing.md Part 4 | ✓ Done | Renamed to "Routing Session Establishment" | +| fips-routing.md SessionSetup/Ack | ✓ Done | Added `handshake_payload` for crypto handshake | +| fips-design.md §7 Encryption | ✓ Done | Updated to reference Noise instead of NIP-44 | +| fips-architecture.md Config | ✓ Done | Renamed to "Routing Session", added "Crypto Session"| ### 8.3 Design Doc Alignment Summary The following decisions from this document have been propagated: -1. **Session terminology** (§5.4): "Routing Session" vs "Crypto Session" distinction +1. **Two-layer architecture**: Link layer (Noise IK peer auth) and session layer + (Noise IK end-to-end) operate independently with separate keys +2. **Session terminology** (§5.4): "Routing Session" vs "Crypto Session" distinction now consistent across all docs -2. **Combined establishment** (§5.5): SessionSetup/SessionAck carry optional - `handshake_payload` for Noise KK handshake -3. **Noise KK** (§6): fips-design.md encryption section updated, new config - parameters added to fips-architecture.md -4. **Peer auth vs crypto session**: fips-design.md §1 clarifies the distinction +3. **Combined establishment** (§5.5): SessionSetup/SessionAck carry optional + `handshake_payload` for session-layer Noise IK handshake +4. **Message type split**: LinkMessageType for hop-by-hop, SessionMessageType for + end-to-end (carried inside SessionDatagram) diff --git a/docs/design/fips-state-machines.md b/docs/design/fips-state-machines.md index c923e96..72257cb 100644 --- a/docs/design/fips-state-machines.md +++ b/docs/design/fips-state-machines.md @@ -228,8 +228,7 @@ loop { ```text PeerSlot::Connecting(PeerConnection) │ - │ AuthInit/AuthResponse exchange - │ Noise KK handshake + │ Noise IK handshake (2 messages) ▼ PeerSlot::Active(ActivePeer) │ @@ -240,13 +239,13 @@ PeerSlot::Active(ActivePeer) **PeerConnection** contains: -- Noise handshake state (ephemeral keys, handshake hash) -- Retry tracking (attempts, last_sent) +- Noise IK handshake state (ephemeral keys, handshake hash) +- Expected identity (for outbound) or discovered identity (for inbound) - Direction (Inbound vs Outbound) **ActivePeer** contains: -- Session keys (for encrypt/decrypt) +- NoiseSession (symmetric keys for encrypt/decrypt) - Tree position (declaration, coordinates) - Bloom filter (what's reachable through this peer) - Statistics (last_seen, link_stats) @@ -342,9 +341,10 @@ struct Node { For inbound connections from unknown addresses: -1. Parse packet → extract sender's identity (in AuthInit) -2. Create new PeerConnection -3. Add to `peers` and `addr_to_peer` +1. Receive Noise IK msg1 → decrypt to extract sender's static key (identity) +2. Create new PeerConnection with discovered identity +3. Add to `connections` (by LinkId) and `addr_to_link` +4. After handshake completes, promote to ActivePeer (indexed by NodeId) ## Summary diff --git a/src/lib.rs b/src/lib.rs index 5f2f593..1472848 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,9 +41,9 @@ pub use transport::udp::UdpTransport; // Re-export protocol types pub use protocol::{ - Auth, AuthAck, Challenge, CoordsRequired, DataFlags, DataPacket, FilterAnnounce, Hello, - LookupRequest, LookupResponse, MessageType, PathBroken, ProtocolError, SessionAck, - SessionFlags, SessionSetup, TreeAnnounce, + CoordsRequired, DataFlags, DataPacket, FilterAnnounce, LinkMessageType, LookupRequest, + LookupResponse, PathBroken, ProtocolError, SessionAck, SessionDatagram, SessionFlags, + SessionMessageType, SessionSetup, TreeAnnounce, }; // Re-export cache types diff --git a/src/protocol.rs b/src/protocol.rs index dc26e0c..17381b7 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -1,15 +1,29 @@ //! FIPS Protocol Messages //! -//! Wire format message types for FIPS protocol communication, including -//! authentication handshake, spanning tree announcements, Bloom filter -//! propagation, discovery protocol, and data packets. +//! Wire format definitions for FIPS protocol communication across two layers: +//! +//! ## Link Layer (peer-to-peer, hop-by-hop) +//! +//! Messages exchanged between directly connected peers over Noise-encrypted +//! 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 +//! establishes the encrypted channel before any of these messages are sent. +//! +//! ## Session Layer (end-to-end, between FIPS addresses) +//! +//! Messages exchanged between source and destination FIPS nodes, encrypted +//! with session keys that intermediate nodes cannot read. Includes session +//! establishment, IPv6 datagram encapsulation, and routing errors. +//! +//! Session-layer datagrams are carried as opaque payloads through the link +//! layer, encrypted end-to-end independently of per-hop link encryption. use crate::bloom::BloomFilter; use crate::tree::{ParentDeclaration, TreeCoordinate}; use crate::{FipsAddress, NodeId}; -use rand::Rng; use secp256k1::schnorr::Signature; -use secp256k1::XOnlyPublicKey; use std::fmt; use thiserror::Error; @@ -19,55 +33,47 @@ pub const PROTOCOL_VERSION: u8 = 1; /// Data packet header size in bytes (excluding payload). pub const DATA_HEADER_SIZE: usize = 36; -/// Message type identifiers. +// ============================================================================ +// Link Layer Message Types (peer-to-peer, hop-by-hop) +// ============================================================================ + +/// Link-layer message type identifiers. +/// +/// These messages are exchanged between directly connected peers over +/// Noise-encrypted links. Peer authentication happens via Noise IK +/// handshake before any of these messages are sent. #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[repr(u8)] -pub enum MessageType { - // Authentication (0x00-0x0F) - Hello = 0x00, - Challenge = 0x01, - Auth = 0x02, - AuthAck = 0x03, - +pub enum LinkMessageType { // Tree protocol (0x10-0x1F) + /// Spanning tree state announcement. TreeAnnounce = 0x10, // Bloom filter (0x20-0x2F) + /// Bloom filter reachability update. FilterAnnounce = 0x20, // Discovery (0x30-0x3F) + /// Request to discover a node's coordinates. LookupRequest = 0x30, + /// Response with target's coordinates. LookupResponse = 0x31, - // Session (0x40-0x4F) - SessionSetup = 0x40, - SessionAck = 0x41, - - // Data (0x50-0x5F) - DataPacket = 0x50, - - // Errors (0x60-0x6F) - CoordsRequired = 0x60, - PathBroken = 0x61, + // Forwarding (0x40-0x4F) + /// Encapsulated session-layer datagram for forwarding. + /// Payload is opaque to intermediate nodes (end-to-end encrypted). + SessionDatagram = 0x40, } -impl MessageType { +impl LinkMessageType { /// Try to convert from a byte. pub fn from_byte(b: u8) -> Option { match b { - 0x00 => Some(MessageType::Hello), - 0x01 => Some(MessageType::Challenge), - 0x02 => Some(MessageType::Auth), - 0x03 => Some(MessageType::AuthAck), - 0x10 => Some(MessageType::TreeAnnounce), - 0x20 => Some(MessageType::FilterAnnounce), - 0x30 => Some(MessageType::LookupRequest), - 0x31 => Some(MessageType::LookupResponse), - 0x40 => Some(MessageType::SessionSetup), - 0x41 => Some(MessageType::SessionAck), - 0x50 => Some(MessageType::DataPacket), - 0x60 => Some(MessageType::CoordsRequired), - 0x61 => Some(MessageType::PathBroken), + 0x10 => Some(LinkMessageType::TreeAnnounce), + 0x20 => Some(LinkMessageType::FilterAnnounce), + 0x30 => Some(LinkMessageType::LookupRequest), + 0x31 => Some(LinkMessageType::LookupResponse), + 0x40 => Some(LinkMessageType::SessionDatagram), _ => None, } } @@ -78,27 +84,84 @@ impl MessageType { } } -impl fmt::Display for MessageType { +impl fmt::Display for LinkMessageType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let name = match self { - MessageType::Hello => "Hello", - MessageType::Challenge => "Challenge", - MessageType::Auth => "Auth", - MessageType::AuthAck => "AuthAck", - MessageType::TreeAnnounce => "TreeAnnounce", - MessageType::FilterAnnounce => "FilterAnnounce", - MessageType::LookupRequest => "LookupRequest", - MessageType::LookupResponse => "LookupResponse", - MessageType::SessionSetup => "SessionSetup", - MessageType::SessionAck => "SessionAck", - MessageType::DataPacket => "DataPacket", - MessageType::CoordsRequired => "CoordsRequired", - MessageType::PathBroken => "PathBroken", + LinkMessageType::TreeAnnounce => "TreeAnnounce", + LinkMessageType::FilterAnnounce => "FilterAnnounce", + LinkMessageType::LookupRequest => "LookupRequest", + LinkMessageType::LookupResponse => "LookupResponse", + LinkMessageType::SessionDatagram => "SessionDatagram", }; write!(f, "{}", name) } } +// ============================================================================ +// Session Layer Message Types (end-to-end, between FIPS addresses) +// ============================================================================ + +/// Session-layer message type identifiers. +/// +/// These messages are exchanged end-to-end between FIPS nodes, encrypted +/// with session keys that intermediate nodes cannot read. They are carried +/// as payloads inside `LinkMessageType::SessionDatagram`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(u8)] +pub enum SessionMessageType { + // Session establishment (0x00-0x0F) + /// Session setup with coordinates (warms router caches). + SessionSetup = 0x00, + /// Session acknowledgement. + SessionAck = 0x01, + + // Data (0x10-0x1F) + /// Encrypted IPv6 datagram payload. + DataPacket = 0x10, + + // Errors (0x20-0x2F) + /// Router cache miss - needs coordinates. + CoordsRequired = 0x20, + /// Routing failure (local minimum or unreachable). + PathBroken = 0x21, +} + +impl SessionMessageType { + /// Try to convert from a byte. + pub fn from_byte(b: u8) -> Option { + match b { + 0x00 => Some(SessionMessageType::SessionSetup), + 0x01 => Some(SessionMessageType::SessionAck), + 0x10 => Some(SessionMessageType::DataPacket), + 0x20 => Some(SessionMessageType::CoordsRequired), + 0x21 => Some(SessionMessageType::PathBroken), + _ => None, + } + } + + /// Convert to a byte. + pub fn to_byte(self) -> u8 { + self as u8 + } +} + +impl fmt::Display for SessionMessageType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let name = match self { + SessionMessageType::SessionSetup => "SessionSetup", + SessionMessageType::SessionAck => "SessionAck", + SessionMessageType::DataPacket => "DataPacket", + SessionMessageType::CoordsRequired => "CoordsRequired", + SessionMessageType::PathBroken => "PathBroken", + }; + write!(f, "{}", name) + } +} + +// Legacy type alias for compatibility during transition +#[deprecated(note = "Use LinkMessageType or SessionMessageType instead")] +pub type MessageType = LinkMessageType; + /// Errors related to protocol message handling. #[derive(Debug, Error)] pub enum ProtocolError { @@ -127,102 +190,9 @@ pub enum ProtocolError { TtlExpired, } -// ============ Authentication Messages ============ - -/// Initial hello message from initiator. -/// -/// The first message in the 4-step authentication handshake. -#[derive(Clone, Debug)] -pub struct Hello { - /// Initiator's public key. - pub pubkey: XOnlyPublicKey, -} - -impl Hello { - /// Create a new Hello message. - pub fn new(pubkey: XOnlyPublicKey) -> Self { - Self { pubkey } - } -} - -/// Challenge from responder, also contains responder's identity. -/// -/// The second message in the authentication handshake. -#[derive(Clone, Debug)] -pub struct Challenge { - /// Responder's public key. - pub pubkey: XOnlyPublicKey, - /// Random challenge for initiator to sign. - pub challenge: [u8; 32], -} - -impl Challenge { - /// Create a new Challenge with a specific challenge value. - pub fn new(pubkey: XOnlyPublicKey, challenge: [u8; 32]) -> Self { - Self { pubkey, challenge } - } - - /// Generate a Challenge with a random challenge value. - pub fn generate(pubkey: XOnlyPublicKey) -> Self { - let mut challenge = [0u8; 32]; - rand::thread_rng().fill(&mut challenge); - Self { pubkey, challenge } - } -} - -/// Authentication response from initiator. -/// -/// The third message in the authentication handshake. Contains the -/// initiator's challenge and their response to the responder's challenge. -#[derive(Clone, Debug)] -pub struct Auth { - /// Challenge for responder to sign. - pub challenge: [u8; 32], - /// Initiator's response to responder's challenge. - pub response: Signature, - /// Timestamp included in signed response (Unix seconds). - pub timestamp: u64, -} - -impl Auth { - /// Create a new Auth message. - pub fn new(challenge: [u8; 32], response: Signature, timestamp: u64) -> Self { - Self { - challenge, - response, - timestamp, - } - } - - /// Generate a new Auth with a random challenge. - pub fn generate(response: Signature, timestamp: u64) -> Self { - let mut challenge = [0u8; 32]; - rand::thread_rng().fill(&mut challenge); - Self { - challenge, - response, - timestamp, - } - } -} - -/// Final acknowledgement from responder. -/// -/// The fourth and final message in the authentication handshake. -#[derive(Clone, Debug)] -pub struct AuthAck { - /// Responder's response to initiator's challenge. - pub response: Signature, - /// Timestamp included in signed response (Unix seconds). - pub timestamp: u64, -} - -impl AuthAck { - /// Create a new AuthAck message. - pub fn new(response: Signature, timestamp: u64) -> Self { - Self { response, timestamp } - } -} +// ============================================================================ +// Link Layer Messages +// ============================================================================ // ============ Tree Protocol Messages ============ @@ -413,7 +383,58 @@ impl LookupResponse { } } -// ============ Session Messages ============ +// ============ Session Datagram (Link-Layer Encapsulation) ============ + +/// Encapsulated session-layer datagram for forwarding. +/// +/// This is a link-layer message that carries an opaque, end-to-end encrypted +/// session-layer payload. Intermediate nodes route based on the destination +/// address but cannot decrypt the payload. +#[derive(Clone, Debug)] +pub struct SessionDatagram { + /// Destination FIPS address (for routing decisions). + pub dest_addr: FipsAddress, + /// Hop limit (decremented at each hop). + pub hop_limit: u8, + /// Encrypted session-layer payload (opaque to intermediate nodes). + pub payload: Vec, +} + +impl SessionDatagram { + /// Create a new session datagram. + pub fn new(dest_addr: FipsAddress, payload: Vec) -> Self { + Self { + dest_addr, + hop_limit: 64, + payload, + } + } + + /// Set the hop limit. + pub fn with_hop_limit(mut self, hop_limit: u8) -> Self { + self.hop_limit = hop_limit; + self + } + + /// Decrement hop limit, returning false if exhausted. + pub fn decrement_hop_limit(&mut self) -> bool { + if self.hop_limit > 0 { + self.hop_limit -= 1; + true + } else { + false + } + } + + /// Check if the datagram can be forwarded. + pub fn can_forward(&self) -> bool { + self.hop_limit > 0 + } +} + +// ============================================================================ +// Session Layer Messages (end-to-end, between FIPS addresses) +// ============================================================================ /// Session flags for setup options. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] @@ -708,37 +729,54 @@ mod tests { TreeCoordinate::new(ids.iter().map(|&v| make_node_id(v)).collect()).unwrap() } - // ===== MessageType Tests ===== + // ===== LinkMessageType Tests ===== #[test] - fn test_message_type_roundtrip() { + fn test_link_message_type_roundtrip() { let types = [ - MessageType::Hello, - MessageType::Challenge, - MessageType::Auth, - MessageType::AuthAck, - MessageType::TreeAnnounce, - MessageType::FilterAnnounce, - MessageType::LookupRequest, - MessageType::LookupResponse, - MessageType::SessionSetup, - MessageType::SessionAck, - MessageType::DataPacket, - MessageType::CoordsRequired, - MessageType::PathBroken, + LinkMessageType::TreeAnnounce, + LinkMessageType::FilterAnnounce, + LinkMessageType::LookupRequest, + LinkMessageType::LookupResponse, + LinkMessageType::SessionDatagram, ]; for ty in types { let byte = ty.to_byte(); - let restored = MessageType::from_byte(byte); + let restored = LinkMessageType::from_byte(byte); assert_eq!(restored, Some(ty)); } } #[test] - fn test_message_type_invalid() { - assert!(MessageType::from_byte(0xFF).is_none()); - assert!(MessageType::from_byte(0x99).is_none()); + fn test_link_message_type_invalid() { + assert!(LinkMessageType::from_byte(0xFF).is_none()); + assert!(LinkMessageType::from_byte(0x00).is_none()); + } + + // ===== SessionMessageType Tests ===== + + #[test] + fn test_session_message_type_roundtrip() { + let types = [ + SessionMessageType::SessionSetup, + SessionMessageType::SessionAck, + SessionMessageType::DataPacket, + SessionMessageType::CoordsRequired, + SessionMessageType::PathBroken, + ]; + + for ty in types { + let byte = ty.to_byte(); + let restored = SessionMessageType::from_byte(byte); + assert_eq!(restored, Some(ty)); + } + } + + #[test] + fn test_session_message_type_invalid() { + assert!(SessionMessageType::from_byte(0xFF).is_none()); + assert!(SessionMessageType::from_byte(0x99).is_none()); } // ===== SessionFlags Tests ===== @@ -862,21 +900,6 @@ mod tests { assert_eq!(&bytes[8..40], target.as_bytes()); } - // ===== Challenge Tests ===== - - #[test] - fn test_challenge_generate() { - let secp = secp256k1::Secp256k1::new(); - let keypair = secp256k1::Keypair::new(&secp, &mut rand::thread_rng()); - let pubkey = keypair.x_only_public_key().0; - - let challenge1 = Challenge::generate(pubkey); - let challenge2 = Challenge::generate(pubkey); - - // Challenges should be different (random) - assert_ne!(challenge1.challenge, challenge2.challenge); - } - // ===== FilterAnnounce Tests ===== #[test] @@ -933,22 +956,6 @@ mod tests { assert!(err.last_known_coords.is_some()); } - // ===== Auth Tests ===== - - #[test] - fn test_auth_generate() { - let secp = secp256k1::Secp256k1::new(); - let keypair = secp256k1::Keypair::new(&secp, &mut rand::thread_rng()); - let digest = [0u8; 32]; - let sig = secp.sign_schnorr(&digest, &keypair); - - let auth1 = Auth::generate(sig, 1000); - let auth2 = Auth::generate(sig, 1000); - - // Random challenges should differ - assert_ne!(auth1.challenge, auth2.challenge); - } - // ===== TreeAnnounce Tests ===== #[test]