mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-10 00:26:59 +00:00
## FLP Wire Format Revision Replace the 1-byte discriminator with a structured wire format: - 4-byte common prefix (ver+phase, flags, payload_len) and 16-byte established frame header with AEAD AAD binding - 5-byte encrypted inner header (4-byte session-relative timestamp + 1-byte message type) on all link messages - Phase-based packet dispatch replacing discriminator-based dispatch - SessionDatagram reassigned from type 0x40 to 0x00; add SenderReport (0x01) and ReceiverReport (0x02) message types for MMP - SessionDatagram: rename hop_limit to ttl, add path_mtu field (u16 LE) with min(datagram.path_mtu, transport.mtu()) at forwarding - Updated handshake packets (msg1: 87->90 bytes, msg2: 42->45 bytes) - FIPS_OVERHEAD updated from 135 to 144 bytes ## MMP Link-Layer Measurement Protocol Add the Metrics Measurement Protocol for link quality measurement between FIPS peers. Measures RTT, loss, jitter, throughput, OWD trend, and ETX from periodic sender/receiver reports exchanged over established links. Module layout: - mmp/algorithms.rs: JitterEstimator, SrttEstimator, DualEwma, OwdTrend, SpinBit, ETX computation - mmp/report.rs: SenderReport (48B) and ReceiverReport (68B) wire format - mmp/sender.rs: per-peer TX counters and interval tracking - mmp/receiver.rs: per-peer RX counters, jitter, loss, gap tracking - mmp/metrics.rs: derived metrics from report processing (SRTT, goodput_bps) - mmp/mod.rs: MmpMode (Full/Lightweight/Minimal), MmpConfig, MmpPeerState - node/handlers/mmp.rs: report dispatch, timer-driven generation, operator logging (periodic + teardown) Integration: per-frame TX/RX hooks in encrypted message handling, report dispatch from link message router, timer-driven generation from tick handler, and periodic operator logging with throughput formatting. Three operating modes: Full (sender + receiver reports, spin bit, CE echo), Lightweight (receiver reports only), Minimal (spin bit + CE echo only). ## Design Documentation Updated FLP sections across all design documents to match the implemented wire format, including revised overhead calculations and numeric values. 568 tests pass, clippy clean.
404 lines
16 KiB
Markdown
404 lines
16 KiB
Markdown
# FIPS Session Protocol (FSP)
|
|
|
|
The FIPS Session Protocol is the top protocol layer in the FIPS stack. It sits
|
|
above the FIPS Link Protocol (FLP) and below applications (native FIPS API or
|
|
IPv6 adapter). FSP provides end-to-end authenticated, encrypted datagram
|
|
delivery between any two FIPS nodes, regardless of how many intermediate hops
|
|
separate them.
|
|
|
|
## Role
|
|
|
|
FSP manages end-to-end communication sessions between FIPS nodes identified by
|
|
their public keys (npubs). Each session provides:
|
|
|
|
- **End-to-end encryption**: Payload confidentiality independent of how many
|
|
intermediate nodes handle the traffic
|
|
- **Mutual authentication**: Both parties prove they control the private key
|
|
for their claimed identity
|
|
- **Replay protection**: Counter-based nonces with sliding window, tolerant of
|
|
UDP packet loss and reordering
|
|
- **Transport independence**: Sessions survive transport changes, route
|
|
changes, and address changes — they are bound to npub identities, not to
|
|
transport paths
|
|
|
|
FSP is a datagram session protocol. It provides encrypted datagrams, not
|
|
reliable streams. There is no FIPS equivalent of TCP; if applications need
|
|
reliability, ordering, or flow control, they provide it themselves (typically
|
|
by running TCP over the FIPS IPv6 adapter).
|
|
|
|
## Services Provided to Applications
|
|
|
|
Applications access the FIPS mesh through two interfaces, both served by FSP:
|
|
|
|
### Native FIPS API
|
|
|
|
Applications address destinations directly by npub or public key. The FIPS
|
|
stack resolves the destination's node_addr, establishes or reuses a session,
|
|
encrypts the payload, and routes through FLP. No DNS involvement.
|
|
|
|
### IPv6 Adapter
|
|
|
|
Unmodified IPv6 applications use a TUN device with `fd::/8` routing. A local
|
|
DNS service maps npub → IPv6 address and primes the identity cache. Packets
|
|
arriving at the TUN are translated to FIPS datagrams and routed through FSP.
|
|
|
|
See [fips-ipv6-adapter.md](fips-ipv6-adapter.md) for the IPv6 adaptation
|
|
layer.
|
|
|
|
### What Applications Get
|
|
|
|
- **Authenticated datagram delivery**: Each datagram is encrypted and
|
|
authenticated with session keys bound to both parties' npubs
|
|
- **Session transparency**: Sessions are established on demand and maintained
|
|
automatically. Applications send packets; FSP handles session setup,
|
|
encryption, and teardown.
|
|
- **Endpoint identity**: Applications address destinations by npub. The FIPS
|
|
address is the public key.
|
|
|
|
### What Applications Do Not Get
|
|
|
|
- **Reliability**: Datagrams may be lost, duplicated, or delivered out of
|
|
order. FSP provides no retransmission or ordering.
|
|
- **Path MTU discovery**: FSP does not signal MTU to applications. The IPv6
|
|
adapter handles MTU enforcement via ICMP Packet Too Big and TCP MSS
|
|
clamping.
|
|
- **Congestion control**: FSP does not throttle traffic. Applications running
|
|
TCP over IPv6 get TCP's congestion control; native API applications must
|
|
manage their own sending rate.
|
|
|
|
## Services Required from FLP
|
|
|
|
FSP treats FLP as a black box providing three services. FSP knows nothing about
|
|
transports, transport addresses, links, peers, spanning trees, coordinates,
|
|
bloom filters, hop counts, or network topology.
|
|
|
|
### SessionDatagram Forwarding
|
|
|
|
FLP accepts a SessionDatagram (source node_addr, destination node_addr, TTL,
|
|
path MTU, payload) and delivers it best-effort toward the destination. Delivery
|
|
may traverse multiple hops, each with independent link encryption.
|
|
|
|
### Error Signaling
|
|
|
|
FLP signals routing failures asynchronously:
|
|
|
|
- **CoordsRequired**: A transit node lacks the destination's tree coordinates.
|
|
FSP responds by re-initiating discovery and resetting the coordinate warmup
|
|
strategy.
|
|
- **PathBroken**: Greedy routing reached a dead end. FSP responds by
|
|
re-discovering the destination's current coordinates and resetting warmup.
|
|
|
|
Both signals are generated by transit nodes (not the destination) and travel
|
|
back to the source inside a new SessionDatagram. They are plaintext (not
|
|
end-to-end encrypted) because transit nodes have no session with the source.
|
|
|
|
### Local Delivery
|
|
|
|
When a SessionDatagram arrives with a destination node_addr matching the local
|
|
node, FLP delivers it to FSP for session-layer processing.
|
|
|
|
## Session Lifecycle
|
|
|
|
### Session Establishment
|
|
|
|
Sessions are established on demand when the first datagram needs to be sent to
|
|
a destination with no existing session.
|
|
|
|
FSP uses Noise IK for session key agreement. The initiator knows the
|
|
destination's npub (from DNS lookup or native API); the responder learns the
|
|
initiator's identity from the handshake. This is the same asymmetry as
|
|
link-layer peer connections.
|
|
|
|
The handshake is carried in SessionSetup and SessionAck messages:
|
|
|
|
1. **Initiator** sends SessionSetup containing Noise IK msg1 and both
|
|
parties' tree coordinates
|
|
2. **Responder** processes msg1, learns initiator identity, sends SessionAck
|
|
containing Noise IK msg2 and its own coordinates
|
|
3. Both parties derive identical symmetric session keys
|
|
|
|
Packets that trigger session establishment are queued (with bounded buffer)
|
|
and transmitted after the session is established.
|
|
|
|
### Self-Bootstrapping
|
|
|
|
SessionSetup is self-bootstrapping for routing. It carries the source's and
|
|
destination's tree coordinates in the clear (not inside the Noise payload).
|
|
As the message transits intermediate nodes, each node caches these coordinates,
|
|
warming the path for subsequent DataPackets that carry only addresses (no
|
|
coordinates).
|
|
|
|
SessionAck carries the responder's coordinates back along the reverse path,
|
|
warming caches in the other direction.
|
|
|
|
### Simultaneous Initiation
|
|
|
|
When both nodes attempt to establish a session simultaneously ("crossing
|
|
hellos"), a deterministic tie-breaker resolves the conflict:
|
|
|
|
- If `local_node_addr < remote_node_addr`: Continue as initiator, ignore
|
|
incoming setup
|
|
- If `local_node_addr > remote_node_addr`: Abort own initiation, switch to
|
|
responder role
|
|
|
|
This ensures exactly one handshake completes.
|
|
|
|
### Data Transfer
|
|
|
|
Once established, sessions carry DataPacket messages containing encrypted
|
|
application data. Each DataPacket includes:
|
|
|
|
- An explicit 8-byte counter for replay protection (used as the AEAD nonce)
|
|
- A flags byte (including COORDS_PRESENT for cache warming)
|
|
- The encrypted payload
|
|
|
|
### Session Idle Timeout
|
|
|
|
Sessions that see no traffic for a configurable duration (default 90s) are
|
|
torn down. When traffic resumes, a new session is established automatically.
|
|
|
|
The idle timeout is deliberately shorter than the coordinate cache TTL (300s).
|
|
This ordering ensures that when traffic stops and the session tears down, the
|
|
transit node coordinate caches are still warm when a new session is established.
|
|
The fresh SessionSetup re-warms the caches, maintaining routing continuity.
|
|
|
|
### Session Independence from Transport
|
|
|
|
Sessions exist above the routing layer and are bound to npub identities, not
|
|
transport addresses or routing paths. A session survives:
|
|
|
|
- Transport failover (UDP → Ethernet → back to UDP)
|
|
- Route changes (different intermediate hops)
|
|
- Transport address changes (IP address or port changes)
|
|
- Topology changes (direct peer becomes multi-hop or vice versa)
|
|
|
|
## End-to-End Encryption
|
|
|
|
### Noise IK Pattern
|
|
|
|
FSP uses the same Noise IK pattern as FLP link encryption, but with
|
|
independent keys and sessions. The full Noise descriptor is
|
|
`Noise_IK_secp256k1_ChaChaPoly_SHA256`.
|
|
|
|
The IK pattern:
|
|
- **msg1** (`→ e, es, s, ss`): Initiator sends ephemeral key, encrypts static
|
|
key to responder. Four DH operations establish session keys.
|
|
- **msg2** (`← e, ee, se`): Responder sends ephemeral key. Both parties now
|
|
share identical session keys.
|
|
|
|
After the handshake, Noise produces two directional symmetric keys
|
|
(`send_key`, `recv_key`) used with ChaCha20-Poly1305 for all subsequent data.
|
|
|
|
### Cryptographic Primitives
|
|
|
|
| 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 |
|
|
|
|
These choices prioritize compatibility with the Nostr cryptographic stack.
|
|
|
|
### secp256k1 Parity Normalization
|
|
|
|
Nostr npubs encode x-only public keys (32 bytes, no y-coordinate parity). The
|
|
Noise IK pre-message mixes the responder's static key as a 33-byte compressed
|
|
key, and the default secp256k1 ECDH hash includes a parity-dependent version
|
|
byte.
|
|
|
|
Both operations are normalized to be parity-independent: the pre-message hash
|
|
uses even parity (`0x02` prefix), and ECDH hashes only the x-coordinate of the
|
|
result point. This ensures handshakes succeed regardless of the responder's
|
|
actual key parity.
|
|
|
|
### Privacy Note
|
|
|
|
Noise IK does not provide initiator anonymity if the responder's static key is
|
|
compromised. An attacker who obtains the responder's nsec can decrypt the
|
|
initiator's identity from captured handshake messages. Noise XK would protect
|
|
initiator identity in this scenario but requires an additional round-trip (3
|
|
handshake messages vs. 2). The privacy/latency tradeoff may be revisited with
|
|
deployment experience.
|
|
|
|
### Data Packet Authentication
|
|
|
|
FSP uses AEAD authentication only — no per-packet signatures. The Noise
|
|
handshake binds session keys to both parties' static keys, so only holders of
|
|
the corresponding nsecs can derive the session keys. This provides implicit
|
|
authentication for every packet, matching WireGuard and Lightning's approach.
|
|
|
|
### Forward Secrecy
|
|
|
|
Ephemeral keys in the Noise handshake provide forward secrecy. Compromise of
|
|
static keys (nsec) does not reveal past session keys, because session keys are
|
|
derived in part from ephemeral-ephemeral DH (`ee`), and ephemeral keys are
|
|
discarded after the handshake.
|
|
|
|
## Replay Protection
|
|
|
|
FSP uses explicit 8-byte counters on the wire for replay protection. Each side
|
|
maintains a monotonically increasing send counter, transmitted with every
|
|
DataPacket. The receiver maintains a sliding window (2048-entry bitmap)
|
|
tracking which counters have been seen.
|
|
|
|
This design is critical for operation over unreliable transports. Under UDP
|
|
packet loss or reordering, implicit nonce counters (where the receiver
|
|
increments on each decrypt attempt) would desynchronize permanently — a failed
|
|
`decrypt()` increments the nonce, and the desync grows with each lost packet.
|
|
Explicit counters allow the receiver to decrypt any packet independently,
|
|
regardless of what packets were lost or reordered.
|
|
|
|
The same `ReplayWindow` and `decrypt_with_replay_check()` implementation is
|
|
used at both the link and session layers.
|
|
|
|
## COORDS_PRESENT Warmup Strategy
|
|
|
|
Session establishment (SessionSetup/SessionAck) warms transit node coordinate
|
|
caches along the path. But coordinate caches have a finite TTL (default 300s),
|
|
and entries may be evicted under memory pressure. When a transit node's cache
|
|
entry expires, it cannot forward DataPackets (which carry only addresses, not
|
|
coordinates) and sends a CoordsRequired error.
|
|
|
|
FSP uses a warmup-then-reactive strategy to keep transit caches populated:
|
|
|
|
### Warmup Phase
|
|
|
|
After session establishment, the first N DataPackets (configurable, default 5)
|
|
include both source and destination coordinates via the COORDS_PRESENT flag.
|
|
Transit nodes cache these coordinates as packets pass through, reinforcing the
|
|
path established by SessionSetup.
|
|
|
|
### Steady State
|
|
|
|
After the warmup count is reached, FSP clears the COORDS_PRESENT flag and
|
|
sends minimal DataPackets (4-byte header instead of ~136 bytes with
|
|
coordinates). Transit nodes serve from their coordinate caches.
|
|
|
|
### Reactive Recovery
|
|
|
|
When FSP receives a CoordsRequired signal:
|
|
|
|
1. The warmup counter resets — subsequent DataPackets include coordinates again
|
|
2. A new LookupRequest may be initiated to rediscover the destination's
|
|
current coordinates
|
|
3. When the LookupResponse arrives for an established session, the warmup
|
|
counter resets again (handling the timing gap where warmup packets might
|
|
fire before transit caches are repopulated by discovery)
|
|
|
|
When FSP receives a PathBroken signal:
|
|
|
|
1. A LookupRequest is initiated to discover the destination's current
|
|
coordinates (which may have changed due to topology change)
|
|
2. The warmup counter resets
|
|
|
|
Both signals are rate-limited at transit nodes (100ms per destination) to
|
|
prevent storms during topology changes.
|
|
|
|
### Warmup State Machine
|
|
|
|
```text
|
|
┌──────────────┐
|
|
│ WARMUP │ ◄── Send first N packets with coords
|
|
└──────┬───────┘
|
|
│ N packets sent without CoordsRequired
|
|
▼
|
|
┌──────────────┐
|
|
│ MINIMAL │ ◄── Send packets without coords
|
|
└──────┬───────┘
|
|
│ CoordsRequired or PathBroken received
|
|
▼
|
|
┌──────────────┐
|
|
│ WARMUP │ ◄── Counter reset, send coords again
|
|
└──────────────┘
|
|
```
|
|
|
|
## Identity Cache
|
|
|
|
The identity cache maps FIPS address prefix (15 bytes, the `fd::/8` IPv6
|
|
address minus the `fd` prefix) to `(NodeAddr, PublicKey)`. This cache is
|
|
needed only when using the IPv6 adapter — the native FIPS API provides the
|
|
public key directly.
|
|
|
|
The mapping is deterministic (derived from the public key via SHA-256) and
|
|
never becomes stale. The cache uses LRU-only eviction bounded by a
|
|
configurable size (default 10K entries). There is no TTL — entries are evicted
|
|
only when the cache is full and space is needed for a new entry.
|
|
|
|
Cache population mechanisms:
|
|
- **DNS lookup**: The primary path. Resolving `npub1xxx...xxx.fips` derives
|
|
the IPv6 address and populates the identity cache.
|
|
- **Inbound traffic**: Authenticated sessions from other nodes populate the
|
|
cache with their identity information.
|
|
|
|
## Coordinate Cache
|
|
|
|
The coordinate cache maps `NodeAddr → TreeCoordinate` and is the critical
|
|
data structure that enables efficient multi-hop routing. Without cached
|
|
coordinates for a destination, FLP cannot make forwarding decisions and must
|
|
either fall back to bloom-filter-only routing or signal CoordsRequired.
|
|
|
|
### Unified Cache
|
|
|
|
The coordinate cache is a single unified cache (merged from previously
|
|
separate coord_cache and route_cache). All coordinate sources — SessionSetup
|
|
transit, COORDS_PRESENT DataPackets, LookupResponse — write to the same cache.
|
|
|
|
### Eviction Policy
|
|
|
|
- **TTL-based expiration**: Entries expire after a configurable duration
|
|
(default 300s)
|
|
- **Refresh on use**: Active routing through a cache entry resets its TTL,
|
|
keeping hot entries alive
|
|
- **LRU eviction**: When the cache is full, least recently used entries are
|
|
evicted first
|
|
- **Flush on parent change**: When the local node's tree parent changes, the
|
|
entire coordinate cache is flushed. Tree parent changes mean the node's own
|
|
coordinates have changed, making cached coordinates for other nodes
|
|
potentially stale for routing purposes.
|
|
|
|
### Timer Ordering
|
|
|
|
Cache and session timers are ordered so that idle sessions tear down before
|
|
transit caches expire:
|
|
|
|
| Timer | Default | Purpose |
|
|
| ----- | ------- | ------- |
|
|
| Session idle timeout | 90s | Tear down unused sessions |
|
|
| Coordinate cache TTL | 300s | Expire stale coordinates |
|
|
| DNS TTL | 300s | Expire DNS resolutions |
|
|
|
|
When traffic stops: the session tears down at 90s. When traffic resumes: DNS
|
|
re-resolves the identity, a fresh SessionSetup carries coordinates, and transit
|
|
node caches (still within their 300s TTL) are re-warmed.
|
|
|
|
## Implementation Status
|
|
|
|
| Feature | Status |
|
|
| ------- | ------ |
|
|
| Session establishment (Noise IK) | **Implemented** |
|
|
| End-to-end encryption (ChaCha20-Poly1305) | **Implemented** |
|
|
| Explicit counter replay protection | **Implemented** |
|
|
| COORDS_PRESENT warmup-then-reactive | **Implemented** |
|
|
| Identity cache (LRU-only) | **Implemented** |
|
|
| Coordinate cache (unified, TTL + refresh) | **Implemented** |
|
|
| Session idle timeout | **Implemented** |
|
|
| CoordsRequired handling | **Implemented** |
|
|
| PathBroken handling | **Implemented** |
|
|
| Simultaneous initiation tie-breaker | **Implemented** |
|
|
| Flush coord cache on parent change | **Implemented** |
|
|
| Rekey | Planned |
|
|
| Path MTU tracking (FLP SessionDatagram field) | **Implemented** |
|
|
| Path MTU notification (end-to-end echo) | Planned |
|
|
|
|
## References
|
|
|
|
- [fips-intro.md](fips-intro.md) — Protocol overview and architecture
|
|
- [fips-link-layer.md](fips-link-layer.md) — FLP specification (below FSP)
|
|
- [fips-ipv6-adapter.md](fips-ipv6-adapter.md) — IPv6 adaptation layer (above FSP)
|
|
- [fips-mesh-operation.md](fips-mesh-operation.md) — Routing, discovery, and
|
|
error recovery
|
|
- [fips-wire-formats.md](fips-wire-formats.md) — Wire format reference for all
|
|
session message types
|