mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-30 19:46:15 +00:00
Session 49: Wire protocol design with session indices
- Add fips-wire-protocol.md: comprehensive packet dispatch design - Wire format: discriminator byte + session indices for O(1) dispatch - WireGuard-style roaming: crypto authority, not address - Security: rate limiting, replay protection, state machine strictness - Transport considerations for UDP, TCP, Tor - Rename fips-protocol-flow.md → fips-session-protocol.md - Update fips-design.md wire format section - Replace TLV with discriminator + index format - Cross-reference fips-wire-protocol.md for details - Update cross-references in all design docs
This commit is contained in:
@@ -8,6 +8,7 @@ Protocol design specifications and analysis for the Federated Interoperable Peer
|
|||||||
| ------------------------------------------------------ | -------------------------------------------------------------------------------------- |
|
| ------------------------------------------------------ | -------------------------------------------------------------------------------------- |
|
||||||
| [fips-design.md](fips-design.md) | Core protocol specification: goals, architecture, identity, addressing, spanning tree |
|
| [fips-design.md](fips-design.md) | Core protocol specification: goals, architecture, identity, addressing, spanning tree |
|
||||||
| [fips-routing.md](fips-routing.md) | Routing architecture: Bloom filters, discovery protocol, session establishment |
|
| [fips-routing.md](fips-routing.md) | Routing architecture: Bloom filters, discovery protocol, session establishment |
|
||||||
|
| [fips-wire-protocol.md](fips-wire-protocol.md) | Wire protocol: format, session indices, roaming, replay protection, DoS defense |
|
||||||
| [fips-transports.md](fips-transports.md) | Transport protocol characteristics: UDP, Ethernet, Tor, radio, and other link types |
|
| [fips-transports.md](fips-transports.md) | Transport protocol characteristics: UDP, Ethernet, Tor, radio, and other link types |
|
||||||
| [spanning-tree-dynamics.md](spanning-tree-dynamics.md) | Detailed study of spanning tree gossip protocol behavior and convergence |
|
| [spanning-tree-dynamics.md](spanning-tree-dynamics.md) | Detailed study of spanning tree gossip protocol behavior and convergence |
|
||||||
|
|
||||||
@@ -19,4 +20,4 @@ Protocol design specifications and analysis for the Federated Interoperable Peer
|
|||||||
| [fips-architecture-review.md](fips-architecture-review.md) | Architecture review issues and resolution status |
|
| [fips-architecture-review.md](fips-architecture-review.md) | Architecture review issues and resolution status |
|
||||||
| [fips-tun-driver.md](fips-tun-driver.md) | TUN interface driver: reader/writer threads, ICMPv6, packet flow |
|
| [fips-tun-driver.md](fips-tun-driver.md) | TUN interface driver: reader/writer threads, ICMPv6, packet flow |
|
||||||
| [fips-state-machines.md](fips-state-machines.md) | Phase-based state machine pattern: peer lifecycle, transitions, timeout handling |
|
| [fips-state-machines.md](fips-state-machines.md) | Phase-based state machine pattern: peer lifecycle, transitions, timeout handling |
|
||||||
| [fips-protocol-flow.md](fips-protocol-flow.md) | Protocol message flow: packet channel, event loop, dispatching |
|
| [fips-session-protocol.md](fips-session-protocol.md) | Session protocol: traffic flow, crypto sessions, terminology |
|
||||||
|
|||||||
@@ -928,7 +928,7 @@ this node wants to reach. This is the primary cache for endpoint nodes.
|
|||||||
> **Terminology note**: These parameters configure *routing sessions*—hop-by-hop
|
> **Terminology note**: These parameters configure *routing sessions*—hop-by-hop
|
||||||
> cached state at intermediate routers. For *crypto session* (end-to-end
|
> cached state at intermediate routers. For *crypto session* (end-to-end
|
||||||
> encryption) parameters, see the Crypto Session section below. See
|
> encryption) parameters, see the Crypto Session section below. See
|
||||||
> [fips-protocol-flow.md](fips-protocol-flow.md) §5 for the distinction.
|
> [fips-session-protocol.md](fips-session-protocol.md) §5 for the distinction.
|
||||||
|
|
||||||
| Parameter | Type | Default | Description |
|
| Parameter | Type | Default | Description |
|
||||||
|-----------|------|---------|-------------|
|
|-----------|------|---------|-------------|
|
||||||
@@ -945,7 +945,7 @@ configure the same underlying cache but are grouped by purpose.
|
|||||||
### Crypto Session Management
|
### Crypto Session Management
|
||||||
|
|
||||||
> **Note**: Crypto sessions provide end-to-end authenticated encryption using
|
> **Note**: Crypto sessions provide end-to-end authenticated encryption using
|
||||||
> Noise KK. See [fips-protocol-flow.md](fips-protocol-flow.md) §6 for details.
|
> Noise KK. See [fips-session-protocol.md](fips-session-protocol.md) §6 for details.
|
||||||
|
|
||||||
| Parameter | Type | Default | Description |
|
| Parameter | Type | Default | Description |
|
||||||
|-----------|------|---------|-------------|
|
|-----------|------|---------|-------------|
|
||||||
@@ -1041,7 +1041,7 @@ establishment).
|
|||||||
## References
|
## References
|
||||||
|
|
||||||
- [fips-design.md](fips-design.md) — Overall FIPS protocol design
|
- [fips-design.md](fips-design.md) — Overall FIPS protocol design
|
||||||
- [fips-protocol-flow.md](fips-protocol-flow.md) — Traffic flow, session terminology, crypto sessions
|
- [fips-session-protocol.md](fips-session-protocol.md) — Traffic flow, session terminology, crypto sessions
|
||||||
- [fips-transports.md](fips-transports.md) — Transport protocol characteristics
|
- [fips-transports.md](fips-transports.md) — Transport protocol characteristics
|
||||||
- [fips-routing.md](fips-routing.md) — Routing, Bloom filters, discovery
|
- [fips-routing.md](fips-routing.md) — Routing, Bloom filters, discovery
|
||||||
- [spanning-tree-dynamics.md](spanning-tree-dynamics.md) — Tree protocol dynamics
|
- [spanning-tree-dynamics.md](spanning-tree-dynamics.md) — Tree protocol dynamics
|
||||||
|
|||||||
+32
-21
@@ -581,38 +581,49 @@ A single node may have multiple transports of different types:
|
|||||||
|
|
||||||
## 6. Protocol Messages
|
## 6. Protocol Messages
|
||||||
|
|
||||||
FIPS uses a unified TLV (Type-Length-Value) wire format for all messages,
|
FIPS uses a discriminator-based wire format with session indices for efficient
|
||||||
including handshake and post-handshake communication.
|
dispatch. See [fips-wire-protocol.md](fips-wire-protocol.md) for complete
|
||||||
|
wire format specification, security properties, and dispatch logic.
|
||||||
|
|
||||||
### Wire Format
|
### Wire Format
|
||||||
|
|
||||||
All FIPS link messages use this framing:
|
All FIPS link-layer packets begin with a 1-byte discriminator:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
┌────────┬────────┬────────────────────────────────────┐
|
┌─────────────┬────────────────────────────────────────────────┐
|
||||||
│ Type │ Length │ Payload │
|
│ Discriminator│ Type-Specific Payload │
|
||||||
│ 1 byte │ 2 bytes│ Variable │
|
│ 1 byte │ Variable │
|
||||||
└────────┴────────┴────────────────────────────────────┘
|
└─────────────┴────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Type**: Message type identifier (determines encryption state)
|
| Byte | Type | Payload Format |
|
||||||
- **Length**: Big-endian payload length in bytes
|
|------|-----------------|--------------------------------------------------|
|
||||||
- **Payload**: Message-specific data
|
| 0x00 | Encrypted frame | `[receiver_idx:4][counter:8][ciphertext+tag]` |
|
||||||
|
| 0x01 | Noise IK msg1 | `[sender_idx:4][noise_msg1:82]` |
|
||||||
|
| 0x02 | Noise IK msg2 | `[sender_idx:4][receiver_idx:4][noise_msg2:33]` |
|
||||||
|
|
||||||
### Handshake Messages (0x00-0x0F)
|
**Session indices** enable O(1) dispatch without relying on source address,
|
||||||
|
supporting transport-layer roaming. Each party allocates a random 32-bit index
|
||||||
|
during handshake; packets include the receiver's index for fast session lookup.
|
||||||
|
|
||||||
|
### Handshake Messages
|
||||||
|
|
||||||
Exchanged during Noise IK handshake before link encryption is established.
|
Exchanged during Noise IK handshake before link encryption is established.
|
||||||
Payloads are not encrypted (except Noise-internal encryption of static key).
|
|
||||||
|
|
||||||
| Type | Name | Payload | Description |
|
| Type | Name | Size | Description |
|
||||||
|------|-------------|---------|------------------------------------------|
|
|------|-------------|----------|-------------------------------------------------|
|
||||||
| 0x01 | NoiseIKMsg1 | 82 bytes| Initiator: ephemeral + encrypted static |
|
| 0x01 | NoiseIKMsg1 | 87 bytes | Initiator: index + ephemeral + encrypted static |
|
||||||
| 0x02 | NoiseIKMsg2 | 33 bytes| Responder: ephemeral pubkey |
|
| 0x02 | NoiseIKMsg2 | 42 bytes | Responder: indices + ephemeral pubkey |
|
||||||
|
|
||||||
Receiver logic:
|
### Encrypted Frames
|
||||||
|
|
||||||
- Type < 0x10 → handshake message, process as raw Noise
|
Post-handshake packets use the 0x00 discriminator with AEAD encryption:
|
||||||
- Type ≥ 0x10 → post-handshake, decrypt payload with session keys
|
|
||||||
|
- **receiver_idx**: Identifies session for O(1) lookup (no trial decryption)
|
||||||
|
- **counter**: 64-bit monotonic nonce, also used for replay detection
|
||||||
|
- **ciphertext**: ChaCha20-Poly1305 encrypted payload with 16-byte tag
|
||||||
|
|
||||||
|
The plaintext begins with a message type byte (see Link Layer Messages below).
|
||||||
|
|
||||||
### Link Layer Messages (0x10-0x4F)
|
### Link Layer Messages (0x10-0x4F)
|
||||||
|
|
||||||
@@ -727,7 +738,7 @@ authentication and forward secrecy in a single round-trip, since both parties
|
|||||||
know each other's npub before initiating. Session keys are used with
|
know each other's npub before initiating. Session keys are used with
|
||||||
ChaCha20-Poly1305 AEAD for all data packets; no per-packet signatures are
|
ChaCha20-Poly1305 AEAD for all data packets; no per-packet signatures are
|
||||||
required (AEAD tag provides integrity and authenticity). See
|
required (AEAD tag provides integrity and authenticity). See
|
||||||
[fips-protocol-flow.md](fips-protocol-flow.md) §6 for crypto session details.
|
[fips-session-protocol.md](fips-session-protocol.md) §6 for crypto session details.
|
||||||
|
|
||||||
> **Note**: Applications may use additional encryption (NIP-44) for
|
> **Note**: Applications may use additional encryption (NIP-44) for
|
||||||
> application-layer privacy, but FIPS-layer encryption protects against
|
> application-layer privacy, but FIPS-layer encryption protects against
|
||||||
@@ -764,7 +775,7 @@ required (AEAD tag provides integrity and authenticity). See
|
|||||||
|
|
||||||
### FIPS Design Documents
|
### FIPS Design Documents
|
||||||
|
|
||||||
- [fips-protocol-flow.md](fips-protocol-flow.md) — Traffic flow, session terminology, crypto sessions
|
- [fips-session-protocol.md](fips-session-protocol.md) — Traffic flow, session terminology, crypto sessions
|
||||||
- [fips-routing.md](fips-routing.md) — Bloom filters, discovery, routing sessions
|
- [fips-routing.md](fips-routing.md) — Bloom filters, discovery, routing sessions
|
||||||
- [fips-architecture.md](fips-architecture.md) — Software architecture, configuration
|
- [fips-architecture.md](fips-architecture.md) — Software architecture, configuration
|
||||||
- [fips-transports.md](fips-transports.md) — Transport protocol characteristics
|
- [fips-transports.md](fips-transports.md) — Transport protocol characteristics
|
||||||
|
|||||||
@@ -348,7 +348,7 @@ No global routing tables. Each node makes purely local decisions.
|
|||||||
> **Terminology note**: This section describes *routing sessions*—hop-by-hop
|
> **Terminology note**: This section describes *routing sessions*—hop-by-hop
|
||||||
> cached state at intermediate routers. FIPS also has *crypto sessions*—end-to-end
|
> cached state at intermediate routers. FIPS also has *crypto sessions*—end-to-end
|
||||||
> authenticated encryption between source and destination. See
|
> authenticated encryption between source and destination. See
|
||||||
> [fips-protocol-flow.md](fips-protocol-flow.md) §5 for the distinction and §6
|
> [fips-session-protocol.md](fips-session-protocol.md) §5 for the distinction and §6
|
||||||
> for crypto session details.
|
> for crypto session details.
|
||||||
|
|
||||||
### Routing Session Purpose
|
### Routing Session Purpose
|
||||||
@@ -379,7 +379,7 @@ struct SessionSetup {
|
|||||||
dest_coords: Vec<NodeId>, // For forward path routing
|
dest_coords: Vec<NodeId>, // For forward path routing
|
||||||
flags: SessionFlags,
|
flags: SessionFlags,
|
||||||
|
|
||||||
// Crypto session establishment (see fips-protocol-flow.md §6)
|
// Crypto session establishment (see fips-session-protocol.md §6)
|
||||||
// Opaque to routers; only processed by destination
|
// Opaque to routers; only processed by destination
|
||||||
handshake_payload: Option<Vec<u8>>, // Noise KK message 1
|
handshake_payload: Option<Vec<u8>>, // Noise KK message 1
|
||||||
}
|
}
|
||||||
@@ -395,7 +395,7 @@ struct SessionAck {
|
|||||||
dest_addr: Ipv6Addr,
|
dest_addr: Ipv6Addr,
|
||||||
src_coords: Vec<NodeId>, // Acknowledger's coords (for return caching)
|
src_coords: Vec<NodeId>, // Acknowledger's coords (for return caching)
|
||||||
|
|
||||||
// Crypto session response (see fips-protocol-flow.md §6)
|
// Crypto session response (see fips-session-protocol.md §6)
|
||||||
handshake_payload: Option<Vec<u8>>, // Noise KK message 2
|
handshake_payload: Option<Vec<u8>>, // Noise KK message 2
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -564,7 +564,7 @@ impl Sender {
|
|||||||
|
|
||||||
> **Note**: SessionSetup/SessionAck sizes vary based on coordinate depth and
|
> **Note**: SessionSetup/SessionAck sizes vary based on coordinate depth and
|
||||||
> whether they carry crypto handshake payloads (combined establishment per
|
> whether they carry crypto handshake payloads (combined establishment per
|
||||||
> [fips-protocol-flow.md](fips-protocol-flow.md) §5.5).
|
> [fips-session-protocol.md](fips-session-protocol.md) §5.5).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -621,6 +621,6 @@ When nodes join/leave:
|
|||||||
## References
|
## References
|
||||||
|
|
||||||
- [fips-design.md](fips-design.md) — Overall FIPS architecture
|
- [fips-design.md](fips-design.md) — Overall FIPS architecture
|
||||||
- [fips-protocol-flow.md](fips-protocol-flow.md) — Traffic flow, crypto sessions, terminology
|
- [fips-session-protocol.md](fips-session-protocol.md) — Traffic flow, crypto sessions, terminology
|
||||||
- [fips-transports.md](fips-transports.md) — Transport protocol characteristics
|
- [fips-transports.md](fips-transports.md) — Transport protocol characteristics
|
||||||
- [spanning-tree-dynamics.md](spanning-tree-dynamics.md) — Tree protocol details
|
- [spanning-tree-dynamics.md](spanning-tree-dynamics.md) — Tree protocol details
|
||||||
|
|||||||
@@ -0,0 +1,867 @@
|
|||||||
|
# FIPS Packet Dispatch and Session Management
|
||||||
|
|
||||||
|
This document specifies the packet dispatch mechanism for FIPS, including:
|
||||||
|
|
||||||
|
- Wire format with session indices for O(1) dispatch
|
||||||
|
- Index-based session lookup enabling transport-layer roaming
|
||||||
|
- Security properties: rate limiting, replay protection, state machine strictness
|
||||||
|
- Transport-specific considerations
|
||||||
|
|
||||||
|
This document supersedes the wire format section in fips-design.md §6.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Design Goals
|
||||||
|
|
||||||
|
### 1.1 Primary Goals
|
||||||
|
|
||||||
|
1. **Cryptographic authority**: A packet that properly decrypts is authentic,
|
||||||
|
regardless of source address
|
||||||
|
2. **Roaming support**: Peers can change transport addresses (IP:port, etc.)
|
||||||
|
without session interruption
|
||||||
|
3. **Efficient dispatch**: O(1) lookup for authenticated traffic, no trial
|
||||||
|
decryption across multiple sessions
|
||||||
|
4. **DoS resistance**: Minimize resources consumed by unauthenticated traffic
|
||||||
|
5. **State machine correctness**: Strict validation prevents confusion attacks
|
||||||
|
|
||||||
|
### 1.2 WireGuard Influence
|
||||||
|
|
||||||
|
This design follows WireGuard's principle: source address is informational, not
|
||||||
|
authoritative. Only successful cryptographic verification establishes authenticity.
|
||||||
|
When a valid packet arrives from a different address than expected, the peer's
|
||||||
|
address is updated rather than the packet being rejected.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Wire Format
|
||||||
|
|
||||||
|
All FIPS link-layer packets use the following format:
|
||||||
|
|
||||||
|
```text
|
||||||
|
┌─────────────┬────────────────────────────────────────────────┐
|
||||||
|
│ Discriminator│ Type-Specific Payload │
|
||||||
|
│ 1 byte │ Variable │
|
||||||
|
└─────────────┴────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
The discriminator byte determines the payload format:
|
||||||
|
|
||||||
|
| Byte | Type | Payload Format |
|
||||||
|
|------|------|----------------|
|
||||||
|
| 0x00 | Encrypted frame | `[receiver_idx:4][counter:8][ciphertext+tag:N+16]` |
|
||||||
|
| 0x01 | Noise IK msg1 | `[sender_idx:4][noise_msg1:82]` |
|
||||||
|
| 0x02 | Noise IK msg2 | `[sender_idx:4][receiver_idx:4][noise_msg2:33]` |
|
||||||
|
|
||||||
|
### 2.1 Encrypted Frame (0x00)
|
||||||
|
|
||||||
|
Post-handshake encrypted packets:
|
||||||
|
|
||||||
|
```text
|
||||||
|
┌────────┬──────────────┬──────────┬───────────────────────────┐
|
||||||
|
│ 0x00 │ receiver_idx │ counter │ ciphertext + AEAD tag │
|
||||||
|
│ 1 byte │ 4 bytes LE │ 8 bytes LE│ N + 16 bytes │
|
||||||
|
└────────┴──────────────┴──────────┴───────────────────────────┘
|
||||||
|
|
||||||
|
Total overhead: 29 bytes (1 + 4 + 8 + 16)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **receiver_idx**: Session index assigned by the receiver during handshake.
|
||||||
|
Enables O(1) session lookup without relying on source address.
|
||||||
|
- **counter**: Monotonically increasing per-session, per-direction counter.
|
||||||
|
Used as AEAD nonce and for replay detection.
|
||||||
|
- **ciphertext**: ChaCha20-Poly1305 encrypted payload.
|
||||||
|
- **tag**: 16-byte Poly1305 authentication tag.
|
||||||
|
|
||||||
|
The plaintext inside the encrypted frame begins with a message type byte,
|
||||||
|
followed by the message-specific payload (see fips-design.md §6 for message
|
||||||
|
types 0x10-0x4F).
|
||||||
|
|
||||||
|
### 2.2 Noise IK Message 1 (0x01)
|
||||||
|
|
||||||
|
Handshake initiation from the connecting party:
|
||||||
|
|
||||||
|
```text
|
||||||
|
┌────────┬─────────────┬─────────────────────────────────────────┐
|
||||||
|
│ 0x01 │ sender_idx │ Noise IK message 1 │
|
||||||
|
│ 1 byte │ 4 bytes LE │ 82 bytes │
|
||||||
|
└────────┴─────────────┴─────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Total: 87 bytes
|
||||||
|
```
|
||||||
|
|
||||||
|
- **sender_idx**: Index chosen by the initiator. This becomes the responder's
|
||||||
|
`receiver_idx` when sending packets TO the initiator.
|
||||||
|
- **Noise msg1**: Standard Noise IK first message (ephemeral pubkey 33 bytes +
|
||||||
|
encrypted static pubkey 33 + 16 bytes).
|
||||||
|
|
||||||
|
### 2.3 Noise IK Message 2 (0x02)
|
||||||
|
|
||||||
|
Handshake response from the responder:
|
||||||
|
|
||||||
|
```text
|
||||||
|
┌────────┬─────────────┬──────────────┬──────────────────────────┐
|
||||||
|
│ 0x02 │ sender_idx │ receiver_idx │ Noise IK message 2 │
|
||||||
|
│ 1 byte │ 4 bytes LE │ 4 bytes LE │ 33 bytes │
|
||||||
|
└────────┴─────────────┴──────────────┴──────────────────────────┘
|
||||||
|
|
||||||
|
Total: 42 bytes
|
||||||
|
```
|
||||||
|
|
||||||
|
- **sender_idx**: Index chosen by the responder. This becomes the initiator's
|
||||||
|
`receiver_idx` when sending packets TO the responder.
|
||||||
|
- **receiver_idx**: Echo of the initiator's `sender_idx` from msg1. Enables the
|
||||||
|
initiator to match the response to their pending handshake.
|
||||||
|
- **Noise msg2**: Standard Noise IK second message (ephemeral pubkey 33 bytes).
|
||||||
|
|
||||||
|
### 2.4 Index Semantics
|
||||||
|
|
||||||
|
Each party in a session has two indices:
|
||||||
|
|
||||||
|
| Index | Chosen By | Used By | Purpose |
|
||||||
|
|-------|-----------|---------|---------|
|
||||||
|
| our_index | Us | Them | They include this in packets TO us |
|
||||||
|
| their_index | Them | Us | We include this in packets TO them |
|
||||||
|
|
||||||
|
After handshake completion:
|
||||||
|
|
||||||
|
- Initiator's `our_index` = initiator's `sender_idx` from msg1
|
||||||
|
- Responder's `our_index` = responder's `sender_idx` from msg2
|
||||||
|
- Each party's `their_index` = the other party's `sender_idx`
|
||||||
|
|
||||||
|
### 2.5 Index Properties
|
||||||
|
|
||||||
|
Indices MUST be:
|
||||||
|
|
||||||
|
1. **Random**: Unpredictable to prevent guessing attacks. Use cryptographically
|
||||||
|
secure random generation.
|
||||||
|
2. **Unique per transport**: No two active sessions on the same transport may
|
||||||
|
share the same `our_index`.
|
||||||
|
3. **Scoped to transport**: The tuple `(transport_id, receiver_idx)` identifies
|
||||||
|
a session. The same index value may appear on different transports.
|
||||||
|
|
||||||
|
Indices SHOULD be:
|
||||||
|
|
||||||
|
4. **Rotated on rekey**: When a session rekeys, allocate new indices to prevent
|
||||||
|
cross-session correlation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Packet Dispatch
|
||||||
|
|
||||||
|
### 3.1 Overview
|
||||||
|
|
||||||
|
Packet dispatch follows a two-phase approach:
|
||||||
|
|
||||||
|
1. **Parse discriminator**: Determine packet type (O(1))
|
||||||
|
2. **Route by type**:
|
||||||
|
- Encrypted (0x00): Index-based lookup, cryptographic verification
|
||||||
|
- Handshake msg2 (0x02): Index-based lookup for pending outbound
|
||||||
|
- Handshake msg1 (0x01): Rate-limited processing, create new state
|
||||||
|
|
||||||
|
### 3.2 Data Structures
|
||||||
|
|
||||||
|
```
|
||||||
|
Node:
|
||||||
|
// === Authenticated sessions ===
|
||||||
|
// Primary dispatch: our_index → NodeId
|
||||||
|
peers_by_index: HashMap<(TransportId, u32), NodeId>
|
||||||
|
|
||||||
|
// Peer data by identity
|
||||||
|
peers: HashMap<NodeId, ActivePeer>
|
||||||
|
|
||||||
|
// === Pending handshakes ===
|
||||||
|
// Outbound: our sender_idx → connection state
|
||||||
|
pending_outbound: HashMap<(TransportId, u32), PeerConnection>
|
||||||
|
|
||||||
|
// Inbound: source address → connection state (before we know identity)
|
||||||
|
pending_inbound_by_addr: HashMap<(TransportId, TransportAddr), PeerConnection>
|
||||||
|
|
||||||
|
// === Resource management ===
|
||||||
|
index_allocator: IndexAllocator
|
||||||
|
msg1_rate_limiter: TokenBucket
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 Encrypted Frame Dispatch (0x00)
|
||||||
|
|
||||||
|
```
|
||||||
|
receive_encrypted(transport_id, source_addr, data):
|
||||||
|
// Parse header (fail fast on malformed)
|
||||||
|
if data.len() < 29: // 1 + 4 + 8 + 16 minimum
|
||||||
|
drop("too short")
|
||||||
|
|
||||||
|
receiver_idx = u32_le(data[1..5])
|
||||||
|
counter = u64_le(data[5..13])
|
||||||
|
ciphertext = data[13..]
|
||||||
|
|
||||||
|
// O(1) session lookup by index
|
||||||
|
node_id = peers_by_index.get((transport_id, receiver_idx))
|
||||||
|
if node_id is None:
|
||||||
|
drop("unknown index") // No crypto, minimal CPU cost
|
||||||
|
|
||||||
|
peer = peers.get(node_id)
|
||||||
|
|
||||||
|
// Replay check BEFORE decryption (cheap)
|
||||||
|
if not peer.replay_window.check(counter):
|
||||||
|
drop("replay or too old")
|
||||||
|
|
||||||
|
// Decrypt (expensive, but only for valid-looking packets)
|
||||||
|
plaintext = peer.session.decrypt(counter, ciphertext)
|
||||||
|
if plaintext is Err:
|
||||||
|
drop("decrypt failed") // Corrupted or wrong key
|
||||||
|
|
||||||
|
// === PACKET IS AUTHENTIC ===
|
||||||
|
|
||||||
|
// Accept counter into replay window
|
||||||
|
peer.replay_window.accept(counter)
|
||||||
|
|
||||||
|
// Update address (ROAMING)
|
||||||
|
peer.current_addr = source_addr
|
||||||
|
|
||||||
|
// Update statistics
|
||||||
|
peer.stats.record_recv(data.len())
|
||||||
|
|
||||||
|
// Dispatch to message handler
|
||||||
|
dispatch_link_message(node_id, plaintext)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key properties**:
|
||||||
|
|
||||||
|
- Unknown index rejected before any crypto (O(1) map lookup)
|
||||||
|
- Replay check before decryption (fast bitfield check)
|
||||||
|
- Source address updated on successful decrypt (roaming)
|
||||||
|
- Single decryption attempt per packet (no trial decryption)
|
||||||
|
|
||||||
|
### 3.4 Handshake Message 2 Dispatch (0x02)
|
||||||
|
|
||||||
|
```
|
||||||
|
receive_msg2(transport_id, source_addr, data):
|
||||||
|
// Parse header
|
||||||
|
if data.len() != 42: // 1 + 4 + 4 + 33
|
||||||
|
drop("wrong size")
|
||||||
|
|
||||||
|
their_sender_idx = u32_le(data[1..5])
|
||||||
|
our_receiver_idx = u32_le(data[5..9])
|
||||||
|
noise_msg2 = data[9..42]
|
||||||
|
|
||||||
|
// Lookup OUR pending handshake by our sender_idx
|
||||||
|
key = (transport_id, our_receiver_idx)
|
||||||
|
conn = pending_outbound.get(key)
|
||||||
|
if conn is None:
|
||||||
|
drop("no pending handshake") // We didn't initiate this
|
||||||
|
|
||||||
|
if conn.state != SentMsg1:
|
||||||
|
drop("unexpected state") // State machine violation
|
||||||
|
|
||||||
|
// Process Noise msg2 (crypto cost paid here)
|
||||||
|
result = conn.noise.read_msg2(noise_msg2)
|
||||||
|
if result is Err:
|
||||||
|
conn.state = Failed
|
||||||
|
drop("handshake failed")
|
||||||
|
|
||||||
|
// Handshake complete
|
||||||
|
conn.their_index = their_sender_idx
|
||||||
|
conn.source_addr = source_addr // Update address
|
||||||
|
|
||||||
|
// Promote to authenticated peer
|
||||||
|
promote_connection(key)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key properties**:
|
||||||
|
|
||||||
|
- Lookup by OUR index (which we chose), not source address
|
||||||
|
- State machine enforced: msg2 only valid in SentMsg1 state
|
||||||
|
- Cannot be spoofed: requires responding to our ephemeral key
|
||||||
|
|
||||||
|
### 3.5 Handshake Message 1 Dispatch (0x01)
|
||||||
|
|
||||||
|
This is the primary attack surface for unauthenticated traffic.
|
||||||
|
|
||||||
|
```
|
||||||
|
receive_msg1(transport_id, source_addr, data):
|
||||||
|
// === RATE LIMITING (before any processing) ===
|
||||||
|
if not msg1_rate_limiter.try_acquire():
|
||||||
|
drop("rate limited")
|
||||||
|
|
||||||
|
// === CONNECTION LIMITS ===
|
||||||
|
if pending_inbound_by_addr.len() >= MAX_PENDING_INBOUND:
|
||||||
|
drop("too many pending")
|
||||||
|
|
||||||
|
// Parse header
|
||||||
|
if data.len() != 87: // 1 + 4 + 82
|
||||||
|
drop("wrong size")
|
||||||
|
|
||||||
|
their_sender_idx = u32_le(data[1..5])
|
||||||
|
noise_msg1 = data[5..87]
|
||||||
|
|
||||||
|
// Check for existing connection from this address
|
||||||
|
addr_key = (transport_id, source_addr)
|
||||||
|
if pending_inbound_by_addr.contains(addr_key):
|
||||||
|
// Could be retry or attack; existing state handles it
|
||||||
|
drop("duplicate")
|
||||||
|
|
||||||
|
// === CRYPTO COST PAID HERE ===
|
||||||
|
result = NoiseHandshake::process_msg1(our_identity, noise_msg1)
|
||||||
|
if result is Err:
|
||||||
|
drop("invalid msg1")
|
||||||
|
|
||||||
|
(peer_identity, handshake, msg2_payload) = result
|
||||||
|
|
||||||
|
// === IDENTITY CHECKS ===
|
||||||
|
|
||||||
|
// Check if this is a known peer reconnecting
|
||||||
|
if peers.contains(peer_identity.node_id):
|
||||||
|
// Existing peer from new address - handle reconnection
|
||||||
|
handle_peer_reconnection(peer_identity, source_addr, ...)
|
||||||
|
return
|
||||||
|
|
||||||
|
// Optional: check allowlist/blocklist
|
||||||
|
if not should_accept_peer(peer_identity):
|
||||||
|
drop("not allowed")
|
||||||
|
|
||||||
|
// === CREATE STATE ===
|
||||||
|
our_index = index_allocator.allocate(transport_id)
|
||||||
|
|
||||||
|
conn = PeerConnection {
|
||||||
|
direction: Inbound,
|
||||||
|
transport_id,
|
||||||
|
our_index,
|
||||||
|
their_index: their_sender_idx,
|
||||||
|
state: ReceivedMsg1,
|
||||||
|
noise: handshake,
|
||||||
|
discovered_identity: peer_identity,
|
||||||
|
source_addr,
|
||||||
|
created_at: now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
pending_inbound_by_addr.insert(addr_key, conn)
|
||||||
|
|
||||||
|
// === SEND RESPONSE ===
|
||||||
|
// [0x02][our_index:4][their_index:4][noise_msg2:33]
|
||||||
|
msg2 = [0x02]
|
||||||
|
++ our_index.to_le_bytes()
|
||||||
|
++ their_sender_idx.to_le_bytes()
|
||||||
|
++ msg2_payload
|
||||||
|
|
||||||
|
send_to_transport(transport_id, source_addr, msg2)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key properties**:
|
||||||
|
|
||||||
|
- Rate limiting BEFORE any parsing or crypto
|
||||||
|
- Connection limit caps memory usage
|
||||||
|
- Crypto cost (DH operations) only paid after rate limit passes
|
||||||
|
- Duplicate detection prevents state accumulation from retries
|
||||||
|
- Identity learned from msg1, checked against allowlist
|
||||||
|
|
||||||
|
### 3.6 Dispatch Summary
|
||||||
|
|
||||||
|
| Packet Type | Lookup Key | Crypto Before Dispatch? | Can Create State? |
|
||||||
|
|-------------|------------|------------------------|-------------------|
|
||||||
|
| Encrypted (0x00) | `(transport_id, receiver_idx)` | Yes (AEAD decrypt) | No |
|
||||||
|
| Msg2 (0x02) | `(transport_id, our_sender_idx)` | Yes (Noise) | No (existing state) |
|
||||||
|
| Msg1 (0x01) | `(transport_id, source_addr)` | Yes (Noise) | Yes (rate limited) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Roaming
|
||||||
|
|
||||||
|
### 4.1 Definition
|
||||||
|
|
||||||
|
Roaming allows a peer to change their transport-layer address (IP:port for UDP,
|
||||||
|
connection handle for TCP, etc.) while maintaining their authenticated session.
|
||||||
|
|
||||||
|
### 4.2 Mechanism
|
||||||
|
|
||||||
|
When an encrypted packet (0x00) successfully decrypts:
|
||||||
|
|
||||||
|
1. The packet is authentic (AEAD tag verified with session keys)
|
||||||
|
2. Session keys are bound to peer identity via Noise handshake
|
||||||
|
3. Therefore, the sender is the authenticated peer, regardless of source address
|
||||||
|
4. Update `peer.current_addr` to the packet's source address
|
||||||
|
|
||||||
|
```
|
||||||
|
// After successful decryption
|
||||||
|
peer.current_addr = source_addr
|
||||||
|
```
|
||||||
|
|
||||||
|
Subsequent outbound packets to this peer use the updated address.
|
||||||
|
|
||||||
|
### 4.3 Transport Applicability
|
||||||
|
|
||||||
|
| Transport | Roaming Applicable? | Notes |
|
||||||
|
|-----------|--------------------|----|
|
||||||
|
| UDP | Yes | Source IP:port can change freely |
|
||||||
|
| TCP | Limited | Reconnection, not mid-session change |
|
||||||
|
| Tor | Limited | Circuit changes, onion address stable |
|
||||||
|
| Ethernet | Rare | MAC address typically stable |
|
||||||
|
| Radio | Yes | Node may move between base stations |
|
||||||
|
|
||||||
|
For connection-oriented transports (TCP, Tor), "roaming" manifests as
|
||||||
|
reconnection rather than mid-session address change. The index-based lookup
|
||||||
|
still applies: a new connection that produces a valid encrypted packet with
|
||||||
|
a known `receiver_idx` is accepted as the peer returning.
|
||||||
|
|
||||||
|
### 4.4 Security Consideration
|
||||||
|
|
||||||
|
Roaming enables an attacker who compromises session keys to redirect traffic.
|
||||||
|
However, session key compromise already allows full impersonation, so roaming
|
||||||
|
doesn't add attack surface. The session keys are the authority, not the address.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Replay Protection
|
||||||
|
|
||||||
|
### 5.1 Counter-Based Nonces
|
||||||
|
|
||||||
|
Each session maintains per-direction counters:
|
||||||
|
|
||||||
|
- **send_counter**: Incremented for each packet sent, used as AEAD nonce
|
||||||
|
- **recv_window**: Sliding window tracking received counters
|
||||||
|
|
||||||
|
### 5.2 Sliding Window
|
||||||
|
|
||||||
|
The receive window allows for UDP packet reordering while detecting replays:
|
||||||
|
|
||||||
|
```
|
||||||
|
ReplayWindow:
|
||||||
|
top: u64 // Highest counter seen
|
||||||
|
bitmap: [u64; 32] // 2048-bit bitmap for window below top
|
||||||
|
|
||||||
|
check(counter) -> bool:
|
||||||
|
if counter > top:
|
||||||
|
return true // New high, definitely not replay
|
||||||
|
if counter + WINDOW_SIZE < top:
|
||||||
|
return false // Too old, outside window
|
||||||
|
|
||||||
|
bit = (top - counter) as usize
|
||||||
|
return not bitmap.test(bit) // True if not seen
|
||||||
|
|
||||||
|
accept(counter):
|
||||||
|
if counter > top:
|
||||||
|
// Advance window
|
||||||
|
shift = min(counter - top, WINDOW_SIZE)
|
||||||
|
bitmap.shift_left(shift)
|
||||||
|
bitmap.set(0) // Mark new counter as seen
|
||||||
|
top = counter
|
||||||
|
else:
|
||||||
|
bit = (top - counter) as usize
|
||||||
|
bitmap.set(bit)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 Window Size
|
||||||
|
|
||||||
|
A 2048-packet window (matching WireGuard) handles:
|
||||||
|
|
||||||
|
- Typical UDP reordering (tens of packets)
|
||||||
|
- Burst loss followed by retransmission
|
||||||
|
- Multi-path scenarios where packets take different routes
|
||||||
|
|
||||||
|
Packets older than the window are rejected. This bounds the memory for replay
|
||||||
|
state to O(1) per session regardless of session duration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Rate Limiting
|
||||||
|
|
||||||
|
### 6.1 Purpose
|
||||||
|
|
||||||
|
Rate limiting protects against CPU exhaustion from msg1 processing. Each msg1
|
||||||
|
requires:
|
||||||
|
|
||||||
|
- Noise DH operations (~200μs on modern CPU)
|
||||||
|
- State allocation
|
||||||
|
- Response generation
|
||||||
|
|
||||||
|
An attacker flooding msg1 from spoofed addresses can exhaust CPU without the
|
||||||
|
rate limit.
|
||||||
|
|
||||||
|
### 6.2 Token Bucket Algorithm
|
||||||
|
|
||||||
|
```
|
||||||
|
TokenBucket:
|
||||||
|
tokens: u32
|
||||||
|
max_tokens: u32
|
||||||
|
refill_rate: u32 // Tokens per second
|
||||||
|
last_refill: Instant
|
||||||
|
|
||||||
|
try_acquire() -> bool:
|
||||||
|
refill()
|
||||||
|
if tokens > 0:
|
||||||
|
tokens -= 1
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
refill():
|
||||||
|
elapsed = now() - last_refill
|
||||||
|
new_tokens = elapsed.as_secs() * refill_rate
|
||||||
|
tokens = min(tokens + new_tokens, max_tokens)
|
||||||
|
last_refill = now()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 Recommended Parameters
|
||||||
|
|
||||||
|
| Parameter | Value | Rationale |
|
||||||
|
|-----------|-------|-----------|
|
||||||
|
| `max_tokens` | 100 | Burst capacity for legitimate connection storms |
|
||||||
|
| `refill_rate` | 10/sec | Sustained rate of new connections |
|
||||||
|
| `MAX_PENDING_INBOUND` | 1000 | Memory bound on pending handshakes |
|
||||||
|
| `HANDSHAKE_TIMEOUT` | 30 sec | Cleanup interval for stale handshakes |
|
||||||
|
|
||||||
|
These values should be configurable to accommodate different deployment
|
||||||
|
scenarios (high-traffic relays vs. leaf nodes).
|
||||||
|
|
||||||
|
### 6.4 Per-Source vs. Global
|
||||||
|
|
||||||
|
Rate limiting is **global** (not per-source) because:
|
||||||
|
|
||||||
|
- UDP source addresses are trivially spoofable
|
||||||
|
- Per-source limits don't protect against distributed attacks
|
||||||
|
- Global limit bounds total CPU regardless of attack distribution
|
||||||
|
|
||||||
|
The tradeoff is that a flooding attack can deny service to legitimate new
|
||||||
|
connections. Mitigations include:
|
||||||
|
|
||||||
|
- Higher limits for nodes expecting many connections
|
||||||
|
- Priority for configured/known peer addresses
|
||||||
|
- Optional proof-of-work extension (future)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. State Machine Strictness
|
||||||
|
|
||||||
|
### 7.1 Valid State Transitions
|
||||||
|
|
||||||
|
```
|
||||||
|
PeerConnection states:
|
||||||
|
Initial → SentMsg1 (outbound: we sent msg1)
|
||||||
|
Initial → ReceivedMsg1 (inbound: we received msg1, sent msg2)
|
||||||
|
SentMsg1 → Complete (received valid msg2)
|
||||||
|
ReceivedMsg1 → Complete (received valid encrypted packet)
|
||||||
|
* → Failed (any error)
|
||||||
|
|
||||||
|
ActivePeer states:
|
||||||
|
Connected → Stale (no traffic for threshold)
|
||||||
|
Stale → Connected (valid traffic received)
|
||||||
|
* → Disconnected (explicit close or timeout)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 Strict Validation
|
||||||
|
|
||||||
|
Each received packet is validated against expected state:
|
||||||
|
|
||||||
|
| Current State | Received | Valid? | Action |
|
||||||
|
|---------------|----------|--------|--------|
|
||||||
|
| No state | 0x00 (encrypted) | No | Drop (unknown index) |
|
||||||
|
| No state | 0x01 (msg1) | Yes | Create PeerConnection (rate limited) |
|
||||||
|
| No state | 0x02 (msg2) | No | Drop (no pending handshake) |
|
||||||
|
| SentMsg1 | 0x00 | No | Drop (not authenticated yet) |
|
||||||
|
| SentMsg1 | 0x01 | No | Drop (we're initiator, not responder) |
|
||||||
|
| SentMsg1 | 0x02 | Yes | Complete handshake |
|
||||||
|
| ReceivedMsg1 | 0x00 | Yes | First authenticated packet, promote |
|
||||||
|
| ReceivedMsg1 | 0x01 | No | Drop (duplicate initiation) |
|
||||||
|
| ReceivedMsg1 | 0x02 | No | Drop (we're responder, not initiator) |
|
||||||
|
| Authenticated | 0x00 | Yes | Normal encrypted traffic |
|
||||||
|
| Authenticated | 0x01 | See 7.3 | Peer reconnection |
|
||||||
|
| Authenticated | 0x02 | No | Drop (handshake already complete) |
|
||||||
|
|
||||||
|
### 7.3 Reconnection Handling
|
||||||
|
|
||||||
|
When msg1 arrives for an already-authenticated peer (identified by npub in the
|
||||||
|
decrypted static key):
|
||||||
|
|
||||||
|
**Option A: Reject** - Existing session is authoritative, ignore new handshake.
|
||||||
|
Simple but prevents legitimate reconnection after network change.
|
||||||
|
|
||||||
|
**Option B: Replace** - New handshake supersedes existing session. Accepts
|
||||||
|
reconnection but allows DoS via handshake flooding.
|
||||||
|
|
||||||
|
**Option C: Validate first** - Accept new handshake alongside existing session.
|
||||||
|
If it completes successfully, replace old session. Provides graceful migration.
|
||||||
|
|
||||||
|
**Recommendation**: Option C with the caveat that the new handshake must
|
||||||
|
complete within a timeout. This handles:
|
||||||
|
|
||||||
|
- Legitimate reconnection (network changed, process restarted)
|
||||||
|
- NAT rebinding (source port changed)
|
||||||
|
- Cross-connection resolution (both sides initiated)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Index Management
|
||||||
|
|
||||||
|
### 8.1 Allocation
|
||||||
|
|
||||||
|
```
|
||||||
|
IndexAllocator:
|
||||||
|
allocated: HashSet<(TransportId, u32)>
|
||||||
|
rng: CryptoRng
|
||||||
|
|
||||||
|
allocate(transport_id) -> u32:
|
||||||
|
loop:
|
||||||
|
idx = rng.random_u32()
|
||||||
|
key = (transport_id, idx)
|
||||||
|
if not allocated.contains(key):
|
||||||
|
allocated.insert(key)
|
||||||
|
return idx
|
||||||
|
|
||||||
|
release(transport_id, idx):
|
||||||
|
allocated.remove((transport_id, idx))
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 Rekey Index Rotation
|
||||||
|
|
||||||
|
When a session rekeys, new indices are allocated:
|
||||||
|
|
||||||
|
```
|
||||||
|
rekey(node_id):
|
||||||
|
peer = peers.get(node_id)
|
||||||
|
old_index = peer.our_index
|
||||||
|
new_index = index_allocator.allocate(peer.transport_id)
|
||||||
|
|
||||||
|
// Update index mapping
|
||||||
|
peers_by_index.remove((peer.transport_id, old_index))
|
||||||
|
peers_by_index.insert((peer.transport_id, new_index), node_id)
|
||||||
|
|
||||||
|
// Release old index
|
||||||
|
index_allocator.release(peer.transport_id, old_index)
|
||||||
|
|
||||||
|
// Update peer
|
||||||
|
peer.our_index = new_index
|
||||||
|
peer.session.rekey()
|
||||||
|
peer.replay_window.reset()
|
||||||
|
|
||||||
|
// Exchange new indices via encrypted rekey message
|
||||||
|
send_rekey_notification(peer)
|
||||||
|
```
|
||||||
|
|
||||||
|
Index rotation prevents correlation of sessions across rekey events by a
|
||||||
|
passive observer who can see the cleartext `receiver_idx`.
|
||||||
|
|
||||||
|
### 8.3 Index Exhaustion
|
||||||
|
|
||||||
|
With 32-bit indices and random allocation, birthday collision becomes likely
|
||||||
|
around 2^16 = 65536 active sessions per transport. For most deployments this
|
||||||
|
is far beyond expected peer counts. If index exhaustion becomes a concern:
|
||||||
|
|
||||||
|
- Use 64-bit indices (adds 4 bytes to all packets)
|
||||||
|
- Implement index recycling with reuse delay
|
||||||
|
- Partition index space by transport or peer class
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Transport-Specific Considerations
|
||||||
|
|
||||||
|
### 9.1 UDP
|
||||||
|
|
||||||
|
UDP is the primary transport for FIPS and the design target for this document.
|
||||||
|
|
||||||
|
**Address semantics**: `TransportAddr` is `SocketAddr` (IP:port string).
|
||||||
|
|
||||||
|
**Roaming**: Fully supported. Source address updated on valid decrypt.
|
||||||
|
|
||||||
|
**Connection model**: Connectionless. No connection state at transport layer.
|
||||||
|
"Links" are virtual tuples of `(transport_id, remote_addr)`.
|
||||||
|
|
||||||
|
**NAT considerations**: Source port may change due to NAT rebinding. Index-based
|
||||||
|
lookup handles this automatically. Hole punching for NAT traversal is a separate
|
||||||
|
concern (not covered here).
|
||||||
|
|
||||||
|
### 9.2 TCP
|
||||||
|
|
||||||
|
**Address semantics**: `TransportAddr` is the connection handle or remote
|
||||||
|
`SocketAddr` at connection time.
|
||||||
|
|
||||||
|
**Roaming**: Manifests as reconnection. When TCP connection breaks, peer may
|
||||||
|
reconnect from different address. The new connection's first packet should be
|
||||||
|
msg1 (new handshake) which will be recognized as an existing peer reconnecting.
|
||||||
|
|
||||||
|
**Connection model**: Connection-oriented. The transport maintains TCP
|
||||||
|
connection state. A "link" corresponds to a TCP connection.
|
||||||
|
|
||||||
|
**Framing**: TCP is stream-oriented. Requires length-prefix framing:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────┬───────────────────────────────────────────────┐
|
||||||
|
│ Length │ FIPS Packet (as specified in §2) │
|
||||||
|
│ 2 bytes BE │ Variable │
|
||||||
|
└────────────┴───────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.3 Tor
|
||||||
|
|
||||||
|
**Address semantics**: `TransportAddr` is onion address + port, or circuit ID.
|
||||||
|
|
||||||
|
**Roaming**: Limited. Onion address is stable but circuits may change. The
|
||||||
|
index-based lookup handles circuit changes transparently.
|
||||||
|
|
||||||
|
**Connection model**: Connection-oriented (Tor circuits). Similar to TCP for
|
||||||
|
framing and connection state.
|
||||||
|
|
||||||
|
**Privacy note**: Tor already provides transport encryption. Link-layer Noise
|
||||||
|
encryption is still applied for defense-in-depth and to maintain consistent
|
||||||
|
security model across transports.
|
||||||
|
|
||||||
|
### 9.4 Ethernet / WiFi
|
||||||
|
|
||||||
|
**Address semantics**: `TransportAddr` is MAC address.
|
||||||
|
|
||||||
|
**Roaming**: MAC addresses are typically stable. However, some devices randomize
|
||||||
|
MACs for privacy. Index-based lookup handles MAC changes.
|
||||||
|
|
||||||
|
**Connection model**: Connectionless (like UDP). Frames are independent.
|
||||||
|
|
||||||
|
**Broadcast**: Ethernet supports broadcast/multicast for discovery. This is
|
||||||
|
outside the scope of packet dispatch.
|
||||||
|
|
||||||
|
### 9.5 Radio (LoRa, etc.)
|
||||||
|
|
||||||
|
**Address semantics**: Transport-specific identifier (device ID, call sign, etc.).
|
||||||
|
|
||||||
|
**Roaming**: A node may be reachable through different base stations. Index-based
|
||||||
|
lookup handles this.
|
||||||
|
|
||||||
|
**MTU**: Radio often has small MTU (LoRa: ~250 bytes). Wire format overhead
|
||||||
|
(29 bytes for encrypted) is significant. Consider:
|
||||||
|
|
||||||
|
- Header compression for repeated fields
|
||||||
|
- Fragment/reassemble at transport layer
|
||||||
|
- Accept higher overhead as cost of security
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Security Analysis
|
||||||
|
|
||||||
|
### 10.1 Attack Resistance Summary
|
||||||
|
|
||||||
|
| Attack | Mitigation | Section |
|
||||||
|
|--------|------------|---------|
|
||||||
|
| Connection exhaustion | Rate limit + connection limit | §6 |
|
||||||
|
| CPU exhaustion (msg1) | Rate limit before crypto | §6 |
|
||||||
|
| Replay | Counter + sliding window | §5 |
|
||||||
|
| State confusion | Strict state machine | §7 |
|
||||||
|
| Spoofed encrypted | Index lookup + AEAD | §3.3 |
|
||||||
|
| Spoofed msg2 | Index lookup + Noise binding | §3.4 |
|
||||||
|
| Address spoofing | Crypto authority, not address | §4 |
|
||||||
|
| Session correlation | Index rotation on rekey | §8.2 |
|
||||||
|
|
||||||
|
### 10.2 Unauthenticated Attack Surface
|
||||||
|
|
||||||
|
Only msg1 (0x01) can be sent by unauthenticated parties. All other packet types
|
||||||
|
require either:
|
||||||
|
|
||||||
|
- Known session index (encrypted frames)
|
||||||
|
- Response to our ephemeral key (msg2)
|
||||||
|
|
||||||
|
Msg1 processing is protected by:
|
||||||
|
|
||||||
|
- Global rate limit
|
||||||
|
- Connection count limit
|
||||||
|
- Handshake timeout cleanup
|
||||||
|
- Optional peer allowlist
|
||||||
|
|
||||||
|
### 10.3 Authenticated Peer Misbehavior
|
||||||
|
|
||||||
|
An authenticated peer can:
|
||||||
|
|
||||||
|
- Send malformed encrypted packets (fail AEAD, no effect)
|
||||||
|
- Send high-frequency traffic (rate limit at higher layer)
|
||||||
|
- Claim false tree coordinates (validated by signature)
|
||||||
|
|
||||||
|
The authentication layer establishes identity but doesn't grant trust. Higher
|
||||||
|
protocol layers apply additional policy.
|
||||||
|
|
||||||
|
### 10.4 Implementation Notes
|
||||||
|
|
||||||
|
1. **Constant-time comparison**: Use constant-time comparison for indices and
|
||||||
|
counters to prevent timing side channels.
|
||||||
|
|
||||||
|
2. **Memory clearing**: Clear session keys and handshake state from memory
|
||||||
|
after use to limit exposure window.
|
||||||
|
|
||||||
|
3. **Entropy**: Use cryptographically secure RNG for index allocation and
|
||||||
|
ephemeral key generation.
|
||||||
|
|
||||||
|
4. **Error messages**: Avoid detailed error responses that could leak state
|
||||||
|
information. Silent drop is preferred for invalid packets.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. References
|
||||||
|
|
||||||
|
### Internal Documents
|
||||||
|
|
||||||
|
- [fips-design.md](fips-design.md) - Overall protocol design
|
||||||
|
- [fips-session-protocol.md](fips-session-protocol.md) - Session establishment flow
|
||||||
|
- [fips-architecture.md](fips-architecture.md) - Software architecture
|
||||||
|
|
||||||
|
### External References
|
||||||
|
|
||||||
|
- [WireGuard Protocol](https://www.wireguard.com/protocol/) - Index-based
|
||||||
|
dispatch inspiration
|
||||||
|
- [Noise Protocol Framework](https://noiseprotocol.org/) - IK pattern
|
||||||
|
- [RFC 6479](https://tools.ietf.org/html/rfc6479) - IPsec anti-replay window
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Appendix A: Message Size Summary
|
||||||
|
|
||||||
|
| Packet Type | Size | Overhead |
|
||||||
|
|-------------|------|----------|
|
||||||
|
| Noise IK msg1 | 87 bytes | - |
|
||||||
|
| Noise IK msg2 | 42 bytes | - |
|
||||||
|
| Encrypted frame | N + 29 bytes | 29 bytes |
|
||||||
|
| Minimum encrypted | 30 bytes | (1 byte payload) |
|
||||||
|
|
||||||
|
For comparison:
|
||||||
|
|
||||||
|
- IPv6 header: 40 bytes
|
||||||
|
- WireGuard data: N + 32 bytes (type 4, idx 4, counter 8, tag 16)
|
||||||
|
- FIPS: slightly more compact due to 1-byte discriminator vs 4-byte type
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Appendix B: Example Packet Traces
|
||||||
|
|
||||||
|
### B.1 Outbound Connection
|
||||||
|
|
||||||
|
```
|
||||||
|
Node A (initiator) → Node B (responder)
|
||||||
|
|
||||||
|
A generates: sender_idx = 0x12345678
|
||||||
|
A sends msg1:
|
||||||
|
[01] [78 56 34 12] [82 bytes noise_msg1]
|
||||||
|
|
||||||
|
B receives, processes msg1, generates: sender_idx = 0xABCDEF01
|
||||||
|
B sends msg2:
|
||||||
|
[02] [01 EF CD AB] [78 56 34 12] [33 bytes noise_msg2]
|
||||||
|
|
||||||
|
A receives msg2, handshake complete.
|
||||||
|
A's our_index = 0x12345678, their_index = 0xABCDEF01
|
||||||
|
B's our_index = 0xABCDEF01, their_index = 0x12345678
|
||||||
|
|
||||||
|
A sends encrypted:
|
||||||
|
[00] [01 EF CD AB] [00 00 00 00 00 00 00 00] [ciphertext+tag]
|
||||||
|
^ B's our_index (A's their_index)
|
||||||
|
|
||||||
|
B receives, looks up 0xABCDEF01 → finds session with A
|
||||||
|
B decrypts, updates A's address if changed
|
||||||
|
```
|
||||||
|
|
||||||
|
### B.2 Roaming Scenario
|
||||||
|
|
||||||
|
```
|
||||||
|
Initial: A connected from 10.0.0.1:4000, established session
|
||||||
|
|
||||||
|
A's network changes to 10.0.0.2:5000
|
||||||
|
|
||||||
|
A sends encrypted from new address:
|
||||||
|
src=10.0.0.2:5000
|
||||||
|
[00] [01 EF CD AB] [01 00 00 00 00 00 00 00] [ciphertext+tag]
|
||||||
|
|
||||||
|
B receives:
|
||||||
|
1. Lookup index 0xABCDEF01 → finds A's session
|
||||||
|
2. Decrypt succeeds
|
||||||
|
3. Update A's address: 10.0.0.1:4000 → 10.0.0.2:5000
|
||||||
|
|
||||||
|
B's subsequent packets to A now go to 10.0.0.2:5000
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user