mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-09 08:14:42 +00:00
Reorganize FIPS design documentation from implementation-centric structure (routing, gossip protocol, wire protocol, transports) to protocol-layer organization with clear service boundaries. New documents (8): - fips-transport-layer.md — transport layer spec - fips-link-layer.md — FLP spec (peer auth, link encryption, forwarding) - fips-session-layer.md — FSP spec (end-to-end encryption, sessions) - fips-ipv6-adapter.md — IPv6 adaptation (TUN, DNS, MTU enforcement) - fips-mesh-operation.md — routing, discovery, error recovery - fips-wire-formats.md — consolidated wire format reference - fips-spanning-tree.md — tree algorithm reference - fips-bloom-filters.md — bloom filter math reference Rewritten (2): - fips-intro.md — breadth-first intro with layer model diagrams - fips-software-architecture.md — slimmed to stable decisions Updated (3): - spanning-tree-dynamics.md — removed stale root refresh, aligned terminology - fips-configuration.md — fixed priority type (u16 → u8) - fips-state-machines.md — synced code examples with codebase Deleted (6): fips-transports.md, fips-wire-protocol.md, fips-gossip-protocol.md, fips-session-protocol.md, fips-routing.md, fips-tun-driver.md (content absorbed into new structure)
403 lines
16 KiB
Markdown
403 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, hop
|
|
limit, 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 discovery | 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
|