diff --git a/docs/design/fips-gossip-protocol.md b/docs/design/fips-gossip-protocol.md index 618b9b7..3ea904b 100644 --- a/docs/design/fips-gossip-protocol.md +++ b/docs/design/fips-gossip-protocol.md @@ -368,6 +368,314 @@ Vec encoding: --- +## Appendix A: Detailed Packet Layouts + +All gossip messages are link-layer messages carried inside encrypted frames +(discriminator 0x00). The layouts below show the plaintext structure after +link-layer decryption. + +### A.1 TreeAnnounce (0x10) + +Propagates spanning tree state between directly connected peers. + +```text +┌─────────────────────────────────────────────────────────────────────────────┐ +│ FULL PACKET (Link Layer + TreeAnnounce) │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌───────────────────────────────────────────────────────────────────────┐ │ +│ │ LINK LAYER FRAME (encrypted) │ │ +│ ├───────────┬──────────────┬────────────┬───────────────────────────────┤ │ +│ │ 0x00 │ receiver_idx │ counter │ ciphertext + tag │ │ +│ │ 1 byte │ 4 bytes LE │ 8 bytes LE │ N + 16 bytes │ │ +│ └───────────┴──────────────┴────────────┴───────────────────────────────┘ │ +│ │ │ +│ ┌───────────────────────┘ │ +│ │ Decrypt │ +│ ▼ │ +│ ┌───────────────────────────────────────────────────────────────────────┐ │ +│ │ TREE ANNOUNCE (plaintext) │ │ +│ ├────────┬──────────────────┬───────────┬───────────────────────────────┤ │ +│ │ Offset │ Field │ Size │ Description │ │ +│ ├────────┼──────────────────┼───────────┼───────────────────────────────┤ │ +│ │ 0 │ msg_type │ 1 byte │ 0x10 │ │ +│ │ 1 │ sequence │ 8 bytes │ u64 LE, monotonic counter │ │ +│ │ 9 │ timestamp │ 8 bytes │ u64 LE, Unix seconds │ │ +│ │ 17 │ parent │ 32 bytes │ NodeId of selected parent │ │ +│ │ 49 │ ancestry_count │ 2 bytes │ u16 LE, number of entries │ │ +│ │ 51 │ ancestry[0..n] │ 112 × n │ AncestryEntry array │ │ +│ │ ... │ signature │ 64 bytes │ Schnorr sig over all above │ │ +│ └────────┴──────────────────┴───────────┴───────────────────────────────┘ │ +│ │ +│ ┌───────────────────────────────────────────────────────────────────────┐ │ +│ │ ANCESTRY ENTRY (112 bytes each) │ │ +│ ├────────┬──────────────────┬───────────┬───────────────────────────────┤ │ +│ │ Offset │ Field │ Size │ Description │ │ +│ ├────────┼──────────────────┼───────────┼───────────────────────────────┤ │ +│ │ 0 │ node_id │ 32 bytes │ SHA-256(npub) of this node │ │ +│ │ 32 │ sequence │ 8 bytes │ u64 LE, node's seq number │ │ +│ │ 40 │ timestamp │ 8 bytes │ u64 LE, node's timestamp │ │ +│ │ 48 │ signature │ 64 bytes │ Node's sig over its decl │ │ +│ └────────┴──────────────────┴───────────┴───────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +**Size calculation**: `1 + 8 + 8 + 32 + 2 + (depth × 112) + 64` bytes + +| Tree Depth | Payload Size | With Link Overhead | +|------------|--------------|-------------------| +| 1 (root) | 227 bytes | 256 bytes | +| 3 | 451 bytes | 480 bytes | +| 5 | 675 bytes | 704 bytes | +| 10 | 1235 bytes | 1264 bytes | + +**Concrete example** (node D at depth 3, ancestry = [D, P1, P2, Root]): + +```text +PLAINTEXT BYTES (hex layout): +10 ← msg_type = TreeAnnounce +05 00 00 00 00 00 00 00 ← sequence = 5 +C3 B2 A1 67 00 00 00 00 ← timestamp (Unix seconds) +[32 bytes P1's node_id] ← parent +04 00 ← ancestry_count = 4 + +ANCESTRY[0] - Self (D): + [32 bytes D's node_id] + 05 00 00 00 00 00 00 00 ← D's sequence + C3 B2 A1 67 00 00 00 00 ← D's timestamp + [64 bytes D's signature] + +ANCESTRY[1] - Parent (P1): + [32 bytes P1's node_id] + 0A 00 00 00 00 00 00 00 ← P1's sequence + 00 B0 A1 67 00 00 00 00 ← P1's timestamp + [64 bytes P1's signature] + +ANCESTRY[2] - Grandparent (P2): + [32 bytes P2's node_id] + 03 00 00 00 00 00 00 00 ← P2's sequence + 00 A0 A1 67 00 00 00 00 ← P2's timestamp + [64 bytes P2's signature] + +ANCESTRY[3] - Root: + [32 bytes Root's node_id] + 01 00 00 00 00 00 00 00 ← Root's sequence + 00 90 A1 67 00 00 00 00 ← Root's timestamp + [64 bytes Root's signature] + +[64 bytes D's outer signature] ← signs entire message + +Total payload: 1 + 8 + 8 + 32 + 2 + (4 × 112) + 64 = 563 bytes +``` + +### A.2 FilterAnnounce (0x11) + +Propagates Bloom filter reachability information. + +```text +┌─────────────────────────────────────────────────────────────────────────────┐ +│ FILTER ANNOUNCE (0x11) │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌───────────────────────────────────────────────────────────────────────┐ │ +│ │ WIRE FORMAT │ │ +│ ├────────┬──────────────────┬───────────┬───────────────────────────────┤ │ +│ │ Offset │ Field │ Size │ Description │ │ +│ ├────────┼──────────────────┼───────────┼───────────────────────────────┤ │ +│ │ 0 │ msg_type │ 1 byte │ 0x11 │ │ +│ │ 1 │ sequence │ 8 bytes │ u64 LE, monotonic counter │ │ +│ │ 9 │ ttl │ 1 byte │ Remaining propagation hops │ │ +│ │ 10 │ hash_count │ 1 byte │ Number of hash functions (7) │ │ +│ │ 11 │ filter_bits │ 4096 bytes│ Bloom filter bit array │ │ +│ └────────┴──────────────────┴───────────┴───────────────────────────────┘ │ +│ │ +│ Total payload: 4107 bytes (fixed size) │ +│ With link overhead: 4136 bytes │ +│ │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ BLOOM FILTER STRUCTURE │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ filter_bits[4096]: │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ Byte 0 │ Byte 1 │ ... │ Byte 4095 │ │ +│ │ bits 0-7 │ bits 8-15 │ │ bits 32760-32767 │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ +│ To test membership of node_id: │ +│ for i in 0..hash_count: │ +│ bit_index = hash(node_id, i) % 32768 │ +│ if !filter_bits[bit_index]: return false │ +│ return true // "maybe present" │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +**Concrete example**: + +```text +PLAINTEXT BYTES: +11 ← msg_type = FilterAnnounce +2A 00 00 00 00 00 00 00 ← sequence = 42 +02 ← ttl = 2 (will propagate 2 more hops) +07 ← hash_count = 7 +[4096 bytes of filter bits] ← Bloom filter + +Total: 4107 bytes +``` + +### A.3 LookupRequest (0x12) + +Discovers tree coordinates for distant destinations. + +```text +┌─────────────────────────────────────────────────────────────────────────────┐ +│ LOOKUP REQUEST (0x12) │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌───────────────────────────────────────────────────────────────────────┐ │ +│ │ WIRE FORMAT │ │ +│ ├────────┬──────────────────┬───────────┬───────────────────────────────┤ │ +│ │ Offset │ Field │ Size │ Description │ │ +│ ├────────┼──────────────────┼───────────┼───────────────────────────────┤ │ +│ │ 0 │ msg_type │ 1 byte │ 0x12 │ │ +│ │ 1 │ request_id │ 8 bytes │ u64 LE, unique identifier │ │ +│ │ 9 │ target │ 32 bytes │ NodeId being searched for │ │ +│ │ 41 │ origin │ 32 bytes │ NodeId of requester │ │ +│ │ 73 │ ttl │ 1 byte │ Remaining propagation hops │ │ +│ │ 74 │ origin_coords_cnt│ 2 bytes │ u16 LE │ │ +│ │ 76 │ origin_coords │ 32 × n │ Requester's ancestry │ │ +│ │ ... │ visited_hash_cnt │ 1 byte │ Hash functions for visited │ │ +│ │ ... │ visited_bits │ 256 bytes │ Compact bloom of visited nodes│ │ +│ └────────┴──────────────────┴───────────┴───────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +**Size calculation**: `1 + 8 + 32 + 32 + 1 + 2 + (depth × 32) + 1 + 256` bytes + +| Origin Depth | Payload Size | +|--------------|--------------| +| 3 | 429 bytes | +| 5 | 493 bytes | +| 10 | 653 bytes | + +**Concrete example** (origin at depth 4): + +```text +PLAINTEXT BYTES: +12 ← msg_type = LookupRequest +[8 bytes request_id] ← random unique ID +[32 bytes target node_id] ← who we're looking for +[32 bytes origin node_id] ← who's asking +08 ← ttl = 8 +04 00 ← origin_coords_count = 4 +[32 bytes] × 4 ← origin's ancestry (128 bytes) +07 ← visited hash_count = 7 +[256 bytes visited bloom] ← nodes that have seen this request + +Total: 1 + 8 + 32 + 32 + 1 + 2 + 128 + 1 + 256 = 461 bytes +``` + +### A.4 LookupResponse (0x13) + +Returns target's coordinates to the requester. + +```text +┌─────────────────────────────────────────────────────────────────────────────┐ +│ LOOKUP RESPONSE (0x13) │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌───────────────────────────────────────────────────────────────────────┐ │ +│ │ WIRE FORMAT │ │ +│ ├────────┬──────────────────┬───────────┬───────────────────────────────┤ │ +│ │ Offset │ Field │ Size │ Description │ │ +│ ├────────┼──────────────────┼───────────┼───────────────────────────────┤ │ +│ │ 0 │ msg_type │ 1 byte │ 0x13 │ │ +│ │ 1 │ request_id │ 8 bytes │ u64 LE, echoes request │ │ +│ │ 9 │ target │ 32 bytes │ NodeId that was found │ │ +│ │ 41 │ target_coords_cnt│ 2 bytes │ u16 LE │ │ +│ │ 43 │ target_coords │ 32 × n │ Target's ancestry to root │ │ +│ │ ... │ proof │ 64 bytes │ Target's signature │ │ +│ └────────┴──────────────────┴───────────┴───────────────────────────────┘ │ +│ │ +│ Proof signature covers: (request_id || target || target_coords) │ +│ Prevents malicious nodes from claiming reachability for any target. │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +**Size calculation**: `1 + 8 + 32 + 2 + (depth × 32) + 64` bytes + +| Target Depth | Payload Size | +|--------------|--------------| +| 3 | 203 bytes | +| 5 | 267 bytes | +| 10 | 427 bytes | + +**Concrete example** (target at depth 5): + +```text +PLAINTEXT BYTES: +13 ← msg_type = LookupResponse +[8 bytes request_id] ← echoed from request +[32 bytes target node_id] ← confirms who was found +05 00 ← target_coords_count = 5 +[32 bytes] × 5 ← target's ancestry (160 bytes) +[64 bytes proof signature] ← target signs to prove existence + +Total: 1 + 8 + 32 + 2 + 160 + 64 = 267 bytes +``` + +### A.5 Message Flow Example + +Complete lookup flow showing packet nesting: + +```text +Source S wants to reach distant destination D (not in local filters) + +1. S creates LookupRequest, sends to peer P1: + + UDP DATAGRAM + ┌──────────────────────────────────────────────────────────────┐ + │ LINK FRAME (S→P1 encrypted) │ + │ ┌──────┬────────────┬─────────┬─────────────────────────────┐│ + │ │ 0x00 │ P1_recv_idx│ counter │ ciphertext + tag ││ + │ └──────┴────────────┴─────────┴─────────────────────────────┘│ + │ │ │ + │ ┌───────────┘ │ + │ ▼ │ + │ ┌──────┬───────────────────────────────────┐ │ + │ │ 0x12 │ LookupRequest payload │ │ + │ │ │ (target=D, origin=S, ttl=8, ...) │ │ + │ └──────┴───────────────────────────────────┘ │ + └──────────────────────────────────────────────────────────────┘ + +2. Request propagates through network, reaches D + +3. D creates LookupResponse, routes back via greedy routing: + + UDP DATAGRAM + ┌──────────────────────────────────────────────────────────────┐ + │ LINK FRAME (D→Pn encrypted) │ + │ ┌──────┬────────────┬─────────┬─────────────────────────────┐│ + │ │ 0x00 │ Pn_recv_idx│ counter │ ciphertext + tag ││ + │ └──────┴────────────┴─────────┴─────────────────────────────┘│ + │ │ │ + │ ┌───────────┘ │ + │ ▼ │ + │ ┌──────┬───────────────────────────────────┐ │ + │ │ 0x13 │ LookupResponse payload │ │ + │ │ │ (target=D, coords=[D,P1,P2,Root]) │ │ + │ └──────┴───────────────────────────────────┘ │ + └──────────────────────────────────────────────────────────────┘ + +4. S receives response, caches D's coordinates, can now route directly +``` + +--- + ## References - [spanning-tree-dynamics.md](spanning-tree-dynamics.md) - Tree protocol behavior diff --git a/docs/design/fips-routing.md b/docs/design/fips-routing.md index f49a4a0..26a1147 100644 --- a/docs/design/fips-routing.md +++ b/docs/design/fips-routing.md @@ -293,20 +293,19 @@ No global routing tables. Each node makes purely local decisions. --- -## Part 4: Routing Session Establishment +## Part 4: Route Cache Management -> **Terminology note**: This section describes *routing sessions*—hop-by-hop -> cached state at intermediate routers. FIPS also has *crypto sessions*—end-to-end -> authenticated encryption between source and destination. See -> [fips-session-protocol.md](fips-session-protocol.md) §3 for crypto session details -> and §5 for route cache warming. +> **Wire formats**: For session layer message wire formats (SessionSetup, +> SessionAck, DataPacket, CoordsRequired, PathBroken), see +> [fips-session-protocol.md](fips-session-protocol.md) §8. -### Routing Session Purpose +### Route Cache Purpose -Establish cached coordinate state along a path so that subsequent data packets -can omit coordinates, minimizing per-packet overhead. +Intermediate routers cache coordinate mappings so that data packets can use +minimal headers (addresses only, no coordinates). This reduces per-packet +overhead from ~300 bytes to 38 bytes. -### Routing Session Lifecycle +### Cache Lifecycle ```text ┌─────────────────────────────────────────────────────────────────┐ @@ -318,74 +317,7 @@ can omit coordinates, minimizing per-packet overhead. └─────────────────────────────────────────────────────────────────┘ ``` -### Routing Session Message Formats - -```rust -/// Establishes cached state along path; optionally carries crypto handshake -struct SessionSetup { - src_addr: Ipv6Addr, - dest_addr: Ipv6Addr, - src_coords: Vec, // For return path caching - dest_coords: Vec, // For forward path routing - flags: SessionFlags, - - // Crypto session establishment (see fips-session-protocol.md §6) - // Opaque to routers; only processed by destination - handshake_payload: Option>, // Noise IK message 1 -} - -struct SessionFlags { - request_ack: bool, // Ask destination to confirm - bidirectional: bool, // Set up both directions -} - -/// Confirms session establishment; optionally carries crypto response -struct SessionAck { - src_addr: Ipv6Addr, - dest_addr: Ipv6Addr, - src_coords: Vec, // Acknowledger's coords (for return caching) - - // Crypto session response (see fips-session-protocol.md §6) - handshake_payload: Option>, // Noise IK message 2 -} - -/// Data packet with optional coordinates -struct DataPacket { - flags: u8, // Bit 0: COORDS_PRESENT - hop_limit: u8, - payload_length: u16, - src_addr: Ipv6Addr, // 16 bytes - dest_addr: Ipv6Addr, // 16 bytes - - // Optional: present only if COORDS_PRESENT flag is set - src_coords: Option>, - dest_coords: Option>, - - payload: Vec, -} - -/// Error when router cannot route (cache miss) -struct CoordsRequired { - dest_addr: Ipv6Addr, - reporter: NodeId, // Which router had the miss -} -``` - -### Data Packet Overhead - -| Field | Size | -|-------|------| -| flags | 1 byte | -| hop_limit | 1 byte | -| payload_length | 2 bytes | -| src_addr | 16 bytes | -| dest_addr | 16 bytes | -| **Minimal header** | **36 bytes** | - -Comparable to IPv6 (40 bytes). When COORDS_PRESENT flag is set, coordinates -add variable overhead based on tree depth (typically 200-400 bytes). - -### Routing Session Setup Flow +### Session Setup Flow ```text S R1 R2 D @@ -460,7 +392,7 @@ impl Router { } ``` -### Cache Management +### Cache Data Structure ```rust struct CoordCache { @@ -485,7 +417,7 @@ by: - SessionRefresh message (lightweight, just touches expiry) - Data packet transit (optional: refresh on use) -### Handling Cache Eviction +### Cache Miss Recovery When a router's cache entry is evicted mid-session: @@ -501,7 +433,7 @@ When a router's cache entry is evicted mid-session: The crypto session remains active throughout—only routing state is refreshed. From application perspective: one packet delayed, transparent recovery. -### Sender Behavior +### Sender State Machine ```rust impl Sender { diff --git a/docs/design/fips-session-protocol.md b/docs/design/fips-session-protocol.md index a33c436..fd36d0a 100644 --- a/docs/design/fips-session-protocol.md +++ b/docs/design/fips-session-protocol.md @@ -495,3 +495,228 @@ After successful Noise IK handshake: The first TreeAnnounce from a new peer may trigger parent reselection if that peer offers a better path to root. See [fips-gossip-protocol.md](fips-gossip-protocol.md) for TreeAnnounce and FilterAnnounce wire formats. + +--- + +## 8. Session Layer Wire Format + +Session layer messages are carried inside `SessionDatagram` (type 0x40) at the +link layer. The session datagram is encrypted hop-by-hop with link keys, but +the inner payload is encrypted end-to-end with session keys. + +### 8.1 Message Type Codes + +| Type Code | Message | Direction | Purpose | +|-----------|----------------|-----------|-----------------------------------| +| 0x00 | SessionSetup | S → D | Establish session + warm caches | +| 0x01 | SessionAck | D → S | Confirm session establishment | +| 0x10 | DataPacket | Both | Application data | +| 0x20 | CoordsRequired | R → S | Router cache miss | +| 0x21 | PathBroken | R → S | Greedy routing failed | + +### 8.2 SessionSetup (0x00) + +Establishes a crypto session and warms router coordinate caches along the path. + +```text +┌─────────────────────────────────────────────────────────────────────────────┐ +│ SESSION SETUP PACKET │ +├────────┬──────────────────┬───────────┬─────────────────────────────────────┤ +│ Offset │ Field │ Size │ Description │ +├────────┼──────────────────┼───────────┼─────────────────────────────────────┤ +│ 0 │ msg_type │ 1 byte │ 0x00 │ +│ 1 │ flags │ 1 byte │ Bit 0: REQUEST_ACK │ +│ │ │ │ Bit 1: BIDIRECTIONAL │ +│ 2 │ src_addr │ 16 bytes │ Source fd::/8 address │ +│ 18 │ dest_addr │ 16 bytes │ Destination fd::/8 address │ +│ 34 │ src_coords_count │ 2 bytes │ u16 LE, number of src coord entries │ +│ 36 │ src_coords │ 32 × n │ NodeId array (self → root) │ +│ ... │ dest_coords_count│ 2 bytes │ u16 LE, number of dest coord entries│ +│ ... │ dest_coords │ 32 × m │ NodeId array (dest → root) │ +│ ... │ handshake_len │ 2 bytes │ u16 LE, Noise payload length │ +│ ... │ handshake_payload│ variable │ Noise IK msg1 (82 bytes typical) │ +└────────┴──────────────────┴───────────┴─────────────────────────────────────┘ +``` + +**Example** (depth 3 source, depth 4 destination, with Noise handshake): + +```text +┌──────┬───────┬──────────────────┬──────────────────┬───────┬─────────────┐ +│ 0x00 │ 0x01 │ src_addr │ dest_addr │ 0x03 │ src_coords │ +│ type │ flags │ 16 bytes │ 16 bytes │ count │ 3 × 32 bytes│ +├──────┴───────┴──────────────────┴──────────────────┴───────┴─────────────┤ +│ 0x04 │ dest_coords │ 0x52 │ handshake_payload │ +│ count │ 4 × 32 bytes │ len=82│ 82 bytes (Noise IK msg1) │ +└───────┴───────────────┴───────┴──────────────────────────────────────────┘ + +Total: 1 + 1 + 16 + 16 + 2 + 96 + 2 + 128 + 2 + 82 = 346 bytes +``` + +### 8.3 SessionAck (0x01) + +Confirms session establishment and completes the Noise handshake. + +```text +┌─────────────────────────────────────────────────────────────────────────────┐ +│ SESSION ACK PACKET │ +├────────┬──────────────────┬───────────┬─────────────────────────────────────┤ +│ Offset │ Field │ Size │ Description │ +├────────┼──────────────────┼───────────┼─────────────────────────────────────┤ +│ 0 │ msg_type │ 1 byte │ 0x01 │ +│ 1 │ flags │ 1 byte │ Reserved │ +│ 2 │ src_addr │ 16 bytes │ Acknowledger's address │ +│ 18 │ dest_addr │ 16 bytes │ Original sender's address │ +│ 34 │ src_coords_count │ 2 bytes │ u16 LE │ +│ 36 │ src_coords │ 32 × n │ Acknowledger's coords (for caching) │ +│ ... │ handshake_len │ 2 bytes │ u16 LE, Noise payload length │ +│ ... │ handshake_payload│ variable │ Noise IK msg2 (33 bytes typical) │ +└────────┴──────────────────┴───────────┴─────────────────────────────────────┘ +``` + +### 8.4 DataPacket (0x10) + +Carries encrypted application data (typically IPv6 payloads). + +```text +┌─────────────────────────────────────────────────────────────────────────────┐ +│ DATA PACKET (Minimal Header) │ +├────────┬──────────────────┬───────────┬─────────────────────────────────────┤ +│ Offset │ Field │ Size │ Description │ +├────────┼──────────────────┼───────────┼─────────────────────────────────────┤ +│ 0 │ msg_type │ 1 byte │ 0x10 │ +│ 1 │ flags │ 1 byte │ Bit 0: COORDS_PRESENT │ +│ 2 │ hop_limit │ 1 byte │ Decremented each hop │ +│ 3 │ reserved │ 1 byte │ Alignment padding │ +│ 4 │ payload_length │ 2 bytes │ u16 LE │ +│ 6 │ src_addr │ 16 bytes │ Source fd::/8 address │ +│ 22 │ dest_addr │ 16 bytes │ Destination fd::/8 address │ +│ 38 │ payload │ variable │ Encrypted application data │ +└────────┴──────────────────┴───────────┴─────────────────────────────────────┘ + +Minimal header: 38 bytes (comparable to IPv6's 40 bytes) +``` + +When `COORDS_PRESENT` flag is set (route warming after CoordsRequired): + +```text +┌─────────────────────────────────────────────────────────────────────────────┐ +│ DATA PACKET (With Coordinates) │ +├────────┬──────────────────┬───────────┬─────────────────────────────────────┤ +│ Offset │ Field │ Size │ Description │ +├────────┼──────────────────┼───────────┼─────────────────────────────────────┤ +│ 0 │ msg_type │ 1 byte │ 0x10 │ +│ 1 │ flags │ 1 byte │ 0x01 (COORDS_PRESENT) │ +│ 2 │ hop_limit │ 1 byte │ Decremented each hop │ +│ 3 │ reserved │ 1 byte │ Alignment padding │ +│ 4 │ payload_length │ 2 bytes │ u16 LE │ +│ 6 │ src_addr │ 16 bytes │ Source fd::/8 address │ +│ 22 │ dest_addr │ 16 bytes │ Destination fd::/8 address │ +│ 38 │ src_coords_count │ 2 bytes │ u16 LE │ +│ 40 │ src_coords │ 32 × n │ Source coordinates │ +│ ... │ dest_coords_count│ 2 bytes │ u16 LE │ +│ ... │ dest_coords │ 32 × m │ Destination coordinates │ +│ ... │ payload │ variable │ Encrypted application data │ +└────────┴──────────────────┴───────────┴─────────────────────────────────────┘ + +With depth-4 coords both directions: 38 + 2 + 128 + 2 + 128 = 298 bytes header +``` + +### 8.5 CoordsRequired (0x20) + +Sent by an intermediate router when it cannot forward a DataPacket due to +coordinate cache miss. + +```text +┌─────────────────────────────────────────────────────────────────────────────┐ +│ COORDS REQUIRED PACKET │ +├────────┬──────────────────┬───────────┬─────────────────────────────────────┤ +│ Offset │ Field │ Size │ Description │ +├────────┼──────────────────┼───────────┼─────────────────────────────────────┤ +│ 0 │ msg_type │ 1 byte │ 0x20 │ +│ 1 │ flags │ 1 byte │ Reserved │ +│ 2 │ dest_addr │ 16 bytes │ The address we couldn't route │ +│ 18 │ reporter │ 32 bytes │ NodeId of reporting router │ +└────────┴──────────────────┴───────────┴─────────────────────────────────────┘ + +Total: 50 bytes +``` + +### 8.6 PathBroken (0x21) + +Sent when greedy routing fails (no peer is closer to destination). + +```text +┌─────────────────────────────────────────────────────────────────────────────┐ +│ PATH BROKEN PACKET │ +├────────┬──────────────────┬───────────┬─────────────────────────────────────┤ +│ Offset │ Field │ Size │ Description │ +├────────┼──────────────────┼───────────┼─────────────────────────────────────┤ +│ 0 │ msg_type │ 1 byte │ 0x21 │ +│ 1 │ flags │ 1 byte │ Reserved │ +│ 2 │ dest_addr │ 16 bytes │ The unreachable destination │ +│ 18 │ reporter │ 32 bytes │ NodeId of reporting router │ +│ 50 │ last_coords_count│ 2 bytes │ u16 LE │ +│ 52 │ last_known_coords│ 32 × n │ Stale coords that failed │ +└────────┴──────────────────┴───────────┴─────────────────────────────────────┘ +``` + +### 8.7 Full Packet Layout Example + +A DataPacket from source S to destination D, transiting router R: + +```text +┌─────────────────────────────────────────────────────────────────────────────┐ +│ UDP DATAGRAM │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌───────────────────────────────────────────────────────────────────────┐ │ +│ │ FIPS LINK LAYER (S→R encrypted) │ │ +│ ├───────────┬──────────────┬────────────┬───────────────────────────────┤ │ +│ │ 0x00 │ R's recv_idx │ counter │ ciphertext + tag │ │ +│ │ 1 byte │ 4 bytes LE │ 8 bytes LE │ N + 16 bytes │ │ +│ └───────────┴──────────────┴────────────┴───────────────────────────────┘ │ +│ │ │ +│ ┌───────────────────────┘ │ +│ │ Decrypt with S↔R link keys │ +│ ▼ │ +│ ┌───────────────────────────────────────────────────────────────────────┐ │ +│ │ LINK MESSAGE (plaintext for R) │ │ +│ ├───────────┬───────────────────────────────────────────────────────────┤ │ +│ │ 0x40 │ SessionDatagram payload │ │ +│ │ msg_type │ (routable by R, encrypted end-to-end) │ │ +│ └───────────┴───────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌───────────────────────────────────────────────────────────────────────┐ │ +│ │ SESSION LAYER (S↔D encrypted) │ │ +│ ├───────────┬───────┬──────────┬──────────┬─────────────────────────────┤ │ +│ │ 0x10 │ flags │ hop_limit│ pay_len │ src_addr │ dest_addr │ │ +│ │ DataPacket│ 0x00 │ 64 │ 1400 │ 16 bytes │ 16 bytes │ │ +│ ├───────────┴───────┴──────────┴──────────┴──────────────┴──────────────┤ │ +│ │ │ │ +│ │ ENCRYPTED PAYLOAD (S↔D session keys) │ │ +│ │ ┌─────────────────────────────────────────────┐ │ │ +│ │ │ IPv6 packet or application data │ │ │ +│ │ │ (+ 16-byte AEAD tag) │ │ │ +│ │ └─────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ └───────────────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ + +Router R can see: dest_addr (for routing decision) +Router R cannot see: payload contents (encrypted with S↔D keys) +``` + +### 8.8 Encoding Rules + +- All multi-byte integers are **little-endian** +- NodeId is 32 bytes (SHA-256 hash of npub) +- IPv6 addresses are 16 bytes (network byte order) +- Variable-length coordinate arrays use 2-byte u16 count prefix + +```text +Vec encoding: + count: u16 (little-endian) + items: [u8; 32] × count +``` diff --git a/docs/design/fips-wire-protocol.md b/docs/design/fips-wire-protocol.md index a88853e..d72d696 100644 --- a/docs/design/fips-wire-protocol.md +++ b/docs/design/fips-wire-protocol.md @@ -803,7 +803,222 @@ protocol layers apply additional policy. --- -## Appendix A: Message Size Summary +## Appendix A: Detailed Packet Layouts + +### A.1 Encrypted Frame (0x00) + +Post-handshake data packets between authenticated peers. + +```text +┌─────────────────────────────────────────────────────────────────────────────┐ +│ ENCRYPTED FRAME (0x00) │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌───────────────────────────────────────────────────────────────────────┐ │ +│ │ WIRE FORMAT │ │ +│ ├────────┬──────────────────┬───────────┬───────────────────────────────┤ │ +│ │ Offset │ Field │ Size │ Description │ │ +│ ├────────┼──────────────────┼───────────┼───────────────────────────────┤ │ +│ │ 0 │ discriminator │ 1 byte │ 0x00 │ │ +│ │ 1 │ receiver_idx │ 4 bytes │ u32 LE, receiver's session idx│ │ +│ │ 5 │ counter │ 8 bytes │ u64 LE, monotonic nonce │ │ +│ │ 13 │ ciphertext │ N bytes │ ChaCha20 encrypted payload │ │ +│ │ 13+N │ tag │ 16 bytes │ Poly1305 auth tag │ │ +│ └────────┴──────────────────┴───────────┴───────────────────────────────┘ │ +│ │ +│ Total overhead: 29 bytes (1 + 4 + 8 + 16) │ +│ │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ PLAINTEXT STRUCTURE │ +│ (after decryption) │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌────────┬──────────────────┬───────────┬───────────────────────────────┐ │ +│ │ Offset │ Field │ Size │ Description │ │ +│ ├────────┼──────────────────┼───────────┼───────────────────────────────┤ │ +│ │ 0 │ msg_type │ 1 byte │ Link message type (see below) │ │ +│ │ 1 │ payload │ variable │ Message-specific payload │ │ +│ └────────┴──────────────────┴───────────┴───────────────────────────────┘ │ +│ │ +│ Link message types: │ +│ 0x10 = TreeAnnounce 0x12 = LookupRequest │ +│ 0x11 = FilterAnnounce 0x13 = LookupResponse │ +│ 0x40 = SessionDatagram │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +**Concrete example** (TreeAnnounce inside encrypted frame): + +```text +WIRE BYTES (hex): +00 ← discriminator +78 56 34 12 ← receiver_idx = 0x12345678 (LE) +2A 00 00 00 00 00 00 00 ← counter = 42 (LE) +[N bytes ciphertext] ← encrypted link message +[16 bytes tag] ← Poly1305 authentication tag + +DECRYPTED PLAINTEXT: +10 ← msg_type = TreeAnnounce +[TreeAnnounce payload] ← see fips-gossip-protocol.md +``` + +### A.2 Noise IK Message 1 (0x01) + +Handshake initiation from connecting party (initiator → responder). + +```text +┌─────────────────────────────────────────────────────────────────────────────┐ +│ NOISE IK MESSAGE 1 (0x01) │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌───────────────────────────────────────────────────────────────────────┐ │ +│ │ WIRE FORMAT │ │ +│ ├────────┬──────────────────┬───────────┬───────────────────────────────┤ │ +│ │ Offset │ Field │ Size │ Description │ │ +│ ├────────┼──────────────────┼───────────┼───────────────────────────────┤ │ +│ │ 0 │ discriminator │ 1 byte │ 0x01 │ │ +│ │ 1 │ sender_idx │ 4 bytes │ u32 LE, initiator's session idx│ │ +│ │ 5 │ noise_msg1 │ 82 bytes │ Noise IK first message │ │ +│ └────────┴──────────────────┴───────────┴───────────────────────────────┘ │ +│ │ +│ Total: 87 bytes │ +│ │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ NOISE MSG1 BREAKDOWN │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌────────┬──────────────────┬───────────┬───────────────────────────────┐ │ +│ │ Offset │ Field │ Size │ Description │ │ +│ ├────────┼──────────────────┼───────────┼───────────────────────────────┤ │ +│ │ 0 │ ephemeral_pubkey │ 33 bytes │ Initiator's ephemeral pubkey │ │ +│ │ │ │ │ (compressed secp256k1) │ │ +│ │ 33 │ encrypted_static │ 33 bytes │ Initiator's static pubkey │ │ +│ │ │ │ │ (encrypted with es key) │ │ +│ │ 66 │ tag │ 16 bytes │ AEAD tag for encrypted_static │ │ +│ └────────┴──────────────────┴───────────┴───────────────────────────────┘ │ +│ │ +│ Noise pattern: -> e, es, s, ss │ +│ - e: ephemeral pubkey sent in clear │ +│ - es: DH(ephemeral, responder_static) → mix into key │ +│ - s: static pubkey encrypted with current key │ +│ - ss: DH(static, responder_static) → mix into key │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +**Concrete example**: + +```text +WIRE BYTES (hex): +01 ← discriminator +78 56 34 12 ← sender_idx = 0x12345678 (LE) +02 [32 bytes] ← ephemeral pubkey (compressed, 02/03 prefix) +[33 bytes] ← encrypted static pubkey +[16 bytes] ← AEAD tag + +Total: 87 bytes +``` + +### A.3 Noise IK Message 2 (0x02) + +Handshake response from responder (responder → initiator). + +```text +┌─────────────────────────────────────────────────────────────────────────────┐ +│ NOISE IK MESSAGE 2 (0x02) │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌───────────────────────────────────────────────────────────────────────┐ │ +│ │ WIRE FORMAT │ │ +│ ├────────┬──────────────────┬───────────┬───────────────────────────────┤ │ +│ │ Offset │ Field │ Size │ Description │ │ +│ ├────────┼──────────────────┼───────────┼───────────────────────────────┤ │ +│ │ 0 │ discriminator │ 1 byte │ 0x02 │ │ +│ │ 1 │ sender_idx │ 4 bytes │ u32 LE, responder's session idx│ │ +│ │ 5 │ receiver_idx │ 4 bytes │ u32 LE, echo of initiator's idx│ │ +│ │ 9 │ noise_msg2 │ 33 bytes │ Noise IK second message │ │ +│ └────────┴──────────────────┴───────────┴───────────────────────────────┘ │ +│ │ +│ Total: 42 bytes │ +│ │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ NOISE MSG2 BREAKDOWN │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌────────┬──────────────────┬───────────┬───────────────────────────────┐ │ +│ │ Offset │ Field │ Size │ Description │ │ +│ ├────────┼──────────────────┼───────────┼───────────────────────────────┤ │ +│ │ 0 │ ephemeral_pubkey │ 33 bytes │ Responder's ephemeral pubkey │ │ +│ │ │ │ │ (compressed secp256k1) │ │ +│ └────────┴──────────────────┴───────────┴───────────────────────────────┘ │ +│ │ +│ Noise pattern: <- e, ee, se │ +│ - e: ephemeral pubkey sent in clear │ +│ - ee: DH(responder_ephemeral, initiator_ephemeral) → mix into key │ +│ - se: DH(responder_ephemeral, initiator_static) → mix into key │ +│ │ +│ After msg2, both parties derive identical session keys. │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +**Concrete example**: + +```text +WIRE BYTES (hex): +02 ← discriminator +01 EF CD AB ← sender_idx = 0xABCDEF01 (LE) +78 56 34 12 ← receiver_idx = 0x12345678 (LE, echoed) +03 [32 bytes] ← ephemeral pubkey (compressed, 02/03 prefix) + +Total: 42 bytes +``` + +### A.4 Complete Handshake Flow + +```text +Initiator (A) Responder (B) +───────────── ───────────── +generates sender_idx = 0x12345678 +generates ephemeral keypair + + ┌──────────────────────────────────────────────────────┐ + │ 0x01 | 0x12345678 | [82 bytes noise_msg1] │ + └──────────────────────────────────────────────────────┘ + ──────────────────────────────► + + validates msg1 + learns A's static pubkey + generates sender_idx = 0xABCDEF01 + generates ephemeral keypair + + ┌──────────────────────────────────────────────────────┐ + │ 0x02 | 0xABCDEF01 | 0x12345678 | [33 bytes noise_msg2]│ + └──────────────────────────────────────────────────────┘ + ◄────────────────────────────── + +validates msg2 +derives session keys + +═══════════════════════ HANDSHAKE COMPLETE ═══════════════════════ + +A's view: B's view: + our_index = 0x12345678 our_index = 0xABCDEF01 + their_index = 0xABCDEF01 their_index = 0x12345678 + +A sends to B: B sends to A: + receiver_idx = 0xABCDEF01 receiver_idx = 0x12345678 + + ┌──────────────────────────────────────────────────────┐ + │ 0x00 | 0xABCDEF01 | counter=0 | [ciphertext+tag] │ + └──────────────────────────────────────────────────────┘ + ──────────────────────────────► +``` + +--- + +## Appendix B: Message Size Summary | Packet Type | Size | Overhead | |-------------|------|----------|