Separate protocol layers: link vs session message types

Refactor protocol.rs to cleanly separate two protocol layers:

Link layer (hop-by-hop, peer-to-peer):
- LinkMessageType: TreeAnnounce, FilterAnnounce, LookupRequest,
  LookupResponse, SessionDatagram
- New SessionDatagram struct for encapsulating session payloads

Session layer (end-to-end):
- SessionMessageType: SessionSetup, SessionAck, DataPacket,
  CoordsRequired, PathBroken

Also:
- Remove obsolete Hello/Challenge/Auth/AuthAck types (replaced by Noise IK)
- Update all design docs for consistency with two-layer architecture
- Add "Link vs Session Encryption" documentation

All 219 tests pass.
This commit is contained in:
Johnathan Corgan
2026-02-01 03:19:54 +00:00
parent 9a6fa07e77
commit 58be9e2e59
5 changed files with 398 additions and 434 deletions
+102 -139
View File
@@ -111,153 +111,107 @@ signature = schnorr_sign(nsec, SHA256(message))
**Challenge-response authentication** is used during connection establishment
to prove a node controls the private key corresponding to its claimed npub.
### Peer Authentication Protocol
### Peer Authentication Protocol (Noise IK)
When two nodes establish a connection, they perform mutual authentication to
verify each other's identity. This prevents impersonation attacks where an
adversary claims to be a node it doesn't control.
When two nodes establish a connection, they perform mutual authentication using
the Noise Protocol Framework with the IK pattern. This provides:
- Mutual authentication (both parties prove identity)
- Forward secrecy (ephemeral keys protect past sessions)
- Encrypted link (all subsequent messages are encrypted)
> **Note**: This authentication protocol is not derived from Yggdrasil, which
> relies on transport-layer security (TLS/QUIC) for identity binding. FIPS
> requires an explicit application-layer protocol because it supports transports
> without built-in encryption or key exchange (radio links, serial connections).
> For initial implementation, peer authentication is always performed regardless
> of transport capabilities; this may be optimized in future versions.
>
> **Terminology note**: *Peer authentication* (this section) is hop-by-hop—it
> verifies that a direct peer is who they claim to be. This is distinct from
> *crypto sessions* (see [fips-protocol-flow.md](fips-protocol-flow.md) §6),
> which provide end-to-end authenticated encryption between source and
> destination using Noise KK. Both layers are necessary: peer auth secures the
> local link; crypto sessions secure the full path.
> verifies that a direct peer is who they claim to be and establishes an
> encrypted link. This is distinct from *session-layer encryption* which
> provides end-to-end authenticated encryption between FIPS addresses. Both
> layers operate independently: peer auth secures each link; session encryption
> secures the full path.
#### Protocol: Noise_IK_secp256k1_ChaChaPoly_SHA256
The IK pattern is chosen because:
- **Outbound connections**: Initiator knows responder's static key from config
- **Inbound connections**: Responder learns initiator's identity from message 1
```text
Pre-message (known before handshake):
<- s (responder's static key known to initiator)
Handshake:
-> e, es, s, ss (msg1: initiator sends ephemeral + encrypted static)
<- e, ee, se (msg2: responder sends ephemeral)
```
```text
Initiator (A) Responder (B)
│ │
───── AuthInit { a_npub, nonce_a } ─────────►
knows B's npub from config
│ │
──── AuthChallenge { b_npub, nonce_b,
sig_b(nonce_a | a_npub) } ─────────
───── AuthComplete {
sig_a(nonce_b | b_npub) } ────────
[A verifies B after msg 2] [B verifies A after msg 3]
──── msg1 (82 bytes) ──────────────────────►
e: ephemeral pubkey (33)
s: encrypted static (33+16)
│ learns A's identity
◄───── msg2 (33 bytes) ──────────────────────
e: ephemeral pubkey (33)
│ │
▼ ▼
Authenticated Authenticated
Noise session established Noise session established
(symmetric keys derived) (symmetric keys derived)
```
**Protocol flow:**
#### Cryptographic Primitives
1. **AuthInit**: Initiator sends its npub and a 32-byte random nonce
2. **AuthChallenge**: Responder sends its npub, its own nonce, and a signature
proving it controls its nsec (signing the initiator's nonce and npub)
3. **AuthComplete**: Initiator sends a signature proving it controls its nsec
(signing the responder's nonce and npub)
| Component | Choice | Notes |
|-----------|---------------------|---------------------------|
| Curve | secp256k1 | Nostr-native |
| DH | ECDH on secp256k1 | Standard EC Diffie-Hellman|
| AEAD | ChaCha20-Poly1305 | Same as NIP-44 |
| Hash | SHA-256 | Nostr-native |
| KDF | HKDF-SHA256 | Standard Noise KDF |
After message 2, the initiator can verify the responder's identity. After
message 3, the responder can verify the initiator's identity. Both nodes have
now proven they control their claimed private keys.
#### Crossing Connection Handling
### Crossing Connection Handling
When both nodes have each other as static peers, both may initiate authentication
simultaneously ("crossing hellos"). This is resolved using deterministic
tie-breaking based on npub ordering:
When both nodes simultaneously initiate connections, a deterministic tie-breaker
resolves which connection survives:
```text
A (lower npub) B (higher npub)
│ │
│─── AuthInit { a_npub, nonce_a } ────────►│
│◄── AuthInit { b_npub, nonce_b } ─────────│ (crossing)
│ │
A < B: ignore B's init, B > A: switch to responder,
wait for challenge send AuthChallenge
│ │
│◄── AuthChallenge { b_npub, ... } ────────│
│─── AuthComplete { ... } ────────────────►│
│ │
▼ ▼
Authenticated Authenticated
Rule: Smaller node_id's OUTBOUND connection wins
If our_node_id < their_node_id:
- Our outbound wins, close our inbound
If our_node_id > their_node_id:
- Our inbound wins, close our outbound
```
**Rules:**
Both nodes independently reach the same conclusion without coordination.
- If a node receives AuthInit while its own AuthInit is pending to the same peer:
- If local npub < remote npub: Continue as initiator, ignore incoming AuthInit
- If local npub > remote npub: Abort own initiation, switch to responder role
#### Post-Authentication State
This ensures exactly one handshake completes with minimal wasted effort.
After the Noise handshake completes:
### Authentication Message Structures
- **NoiseSession** holds symmetric keys for encrypt/decrypt
- Link is fully encrypted (all subsequent messages use AEAD)
- Peer's verified identity is extracted from the handshake
- Node transitions from `PeerConnection` to `ActivePeer` state
```rust
struct AuthInit {
npub: [u8; 32], // Initiator's public key (x-only)
nonce: [u8; 32], // Random challenge
}
#### Link vs Session Encryption
struct AuthChallenge {
npub: [u8; 32], // Responder's public key (x-only)
nonce: [u8; 32], // Responder's challenge
signature: [u8; 64], // sig(initiator_nonce || initiator_npub)
}
FIPS uses two independent encryption layers:
struct AuthComplete {
signature: [u8; 64], // sig(responder_nonce || responder_npub)
}
```
| Layer | Scope | Keys | Purpose |
|-------------|-------------|--------------------------|--------------------------------------|
| **Link** | Hop-by-hop | Per-peer Noise session | Encrypt all traffic on this link |
| **Session** | End-to-end | Per-destination session | Encrypt payload across multiple hops |
### Signature Construction
A packet from A→D through B traverses:
Signatures are constructed with domain separation to prevent cross-protocol
signature reuse:
1. A encrypts payload with A↔D session key
2. A encrypts that with A↔B link key, sends to B
3. B decrypts link layer, routes, re-encrypts with B↔C or B↔D link key
4. D decrypts link layer, then decrypts session layer to get payload
```text
digest = SHA256("fips-peer-auth-v1" || peer_nonce || peer_npub)
signature = schnorr_sign(nsec, digest)
```
**Domain separation**: The `"fips-peer-auth-v1"` prefix ensures that signatures
created for FIPS peer authentication cannot be replayed in other contexts (e.g.,
a Nostr event signature or FIPS crypto session). If the authentication protocol
is revised, the version string changes (e.g., `"fips-peer-auth-v2"`).
**Nonce freshness**: The 32-byte random nonce from the peer ensures that
signatures cannot be pre-computed. Each authentication attempt requires a
fresh signature over the peer's unique challenge.
**Binding to peer identity**: The signature includes the peer's npub, binding
the response to that specific peer. This prevents relay attacks where an
adversary forwards a challenge from one node and uses the response with another.
### Authentication Failure Handling
If authentication fails at any step:
- **Invalid signature**: Connection is terminated immediately
- **Wrong npub**: The node is not who it claimed to be; terminate
- **Expired timestamp**: Possible replay attack; terminate
- **Timeout**: Peer did not respond in time; terminate
Nodes should implement rate limiting on authentication attempts to prevent
denial-of-service attacks that exhaust computational resources through
repeated signature verifications.
### Post-Authentication State
After successful authentication, each node stores:
- The peer's verified npub and derived node_id
- The link over which the peer was authenticated
- Timestamp of successful authentication
This state is used for:
- Routing decisions (only forward to authenticated peers)
- TreeAnnounce signature verification (cached public key lookup)
- Session resumption on transient disconnections (within a timeout window)
Intermediate nodes can route but cannot read session-layer payloads.
---
@@ -627,36 +581,45 @@ A single node may have multiple transports of different types:
## 6. Protocol Messages
FIPS uses two independent message type spaces corresponding to the two protocol
layers. Messages are exchanged over Noise-encrypted links after peer authentication.
### Wire Format
```
```text
┌────────┬────────┬────────────────────────────────────┐
│ Type │ Length │ Payload │
│ 1 byte │ 2 bytes│ Variable │
└────────┴────────┴────────────────────────────────────┘
```
### Message Types
### Link Layer Messages (Peer-to-Peer)
| Type | Name | Description |
|------|------|-------------|
| 0x00 | Dummy | Keepalive/padding |
| 0x01 | TreeAnnounce | Spanning tree state |
| 0x02 | BloomUpdate | Bloom filter update |
| 0x03 | Lookup | Destination lookup request |
| 0x04 | LookupResponse | Coordinates for requested key |
| 0x05 | PathBroken | Route failure notification |
| 0x06 | SessionSetup | Routing session + crypto handshake init |
| 0x07 | SessionAck | Routing session ack + crypto response |
| 0x08 | CoordsRequired | Router cache miss notification |
| 0x09 | AuthInit | Peer authentication initiation |
| 0x0a | AuthChallenge | Peer authentication challenge + response |
| 0x0b | AuthComplete | Peer authentication completion |
| 0x10 | Traffic | Encrypted application data |
| 0x11 | TrafficAck | Delivery acknowledgement |
Exchanged between directly connected peers over Noise-encrypted links.
Peer authentication uses Noise IK handshake (see §1) before any messages.
See [fips-routing.md](fips-routing.md) Part 4 for routing session details and
[fips-protocol-flow.md](fips-protocol-flow.md) §5-6 for combined establishment.
| Type | Name | Description |
|------|----------------|--------------------------------------------|
| 0x10 | TreeAnnounce | Spanning tree state announcement |
| 0x20 | FilterAnnounce | Bloom filter reachability update |
| 0x30 | LookupRequest | Request to discover node coordinates |
| 0x31 | LookupResponse | Response with target's coordinates |
| 0x40 | SessionDatagram| Encapsulated session-layer payload |
### Session Layer Messages (End-to-End)
Carried inside `SessionDatagram`, encrypted with end-to-end session keys.
Intermediate nodes route based on destination but cannot read the payload.
| Type | Name | Description |
|------|----------------|--------------------------------------------|
| 0x00 | SessionSetup | Session establishment with coordinates |
| 0x01 | SessionAck | Session acknowledgement |
| 0x10 | DataPacket | Encrypted IPv6 datagram |
| 0x20 | CoordsRequired | Router cache miss notification |
| 0x21 | PathBroken | Route failure notification |
See [fips-routing.md](fips-routing.md) Part 4 for routing session details.
### TreeAnnounce
+80 -86
View File
@@ -516,62 +516,59 @@ the crypto portion.
---
## 6. Crypto Session Handshake
## 6. Session-Layer Encryption
The crypto session uses the Noise Protocol Framework with secp256k1, aligning
with Nostr's cryptographic primitives.
FIPS uses two independent Noise Protocol handshakes at different layers:
### 6.1 Design Decision: Noise with secp256k1
| Layer | Scope | Pattern | Purpose |
|---------|-------------|----------|-------------------------------------------|
| Link | Hop-by-hop | Noise IK | Authenticate peers, encrypt link |
| Session | End-to-end | Noise IK | Authenticate endpoints, encrypt payload |
**Decision**: Use Noise Protocol Framework adapted for secp256k1.
Both use `Noise_IK_secp256k1_ChaChaPoly_SHA256` with the same cryptographic
primitives, but with separate keys and sessions.
Rationale:
### 6.1 Why Two Layers?
- Well-analyzed framework with formal security proofs
- Used successfully in WireGuard, Lightning (BOLT 8), Signal
- Lightning Network already adapted Noise for secp256k1 (precedent)
- Reuses Nostr's existing key infrastructure (npub/nsec)
- Provides forward secrecy via ephemeral keys
**Link encryption** protects against passive observers on each hop but allows
intermediate nodes to see routing information (destination address).
### 6.2 Pattern Selection
**Session encryption** protects the actual payload end-to-end. Intermediate
nodes forward opaque ciphertext without being able to read the contents.
Since both parties know each other's static public key (npub) before the
handshake begins (from DNS lookup / identity cache), the **Noise KK** pattern
is appropriate:
### 6.2 Session Noise Handshake
The session-layer Noise IK handshake is carried inside `SessionSetup`/`SessionAck`
messages, which themselves travel through the link-encrypted channel:
```text
Noise_KK_secp256k1_ChaChaPoly_SHA256
KK:
-> e, es, ss
<- e, ee, se
Initiator knows destination npub (from DNS lookup)
SessionSetup { coords, handshake_payload: Noise IK msg1 }
▼ (travels through link-encrypted hops)
Responder processes msg1, learns initiator identity
SessionAck { coords, handshake_payload: Noise IK msg2 }
Session keys established (independent of link keys)
```
**Message 1 (Initiator → Responder):**
### 6.3 Cryptographic Primitives
- `e`: Initiator's ephemeral public key
- `es`: DH(initiator_ephemeral, responder_static)
- `ss`: DH(initiator_static, responder_static)
Both link and session layers use the same stack:
**Message 2 (Responder → Initiator):**
- `e`: Responder's ephemeral public key
- `ee`: DH(initiator_ephemeral, responder_ephemeral)
- `se`: DH(responder_ephemeral, initiator_static)
After both messages, both parties derive identical symmetric keys for
encryption in each direction.
### 6.3 Why KK (not IK or XX)
| Pattern | Knowledge | Messages | FIPS Fit |
|---------|-----------|----------|----------|
| **KK** | Both know both static keys | 1 RT | Best - we have npubs |
| IK | Initiator knows responder | 1 RT | Works but asymmetric |
| XX | Neither knows | 2 RT | Unnecessary overhead |
KK provides mutual authentication in a single round-trip since FIPS always
knows the peer's npub before initiating (from DNS/identity cache).
| Component | Choice | Notes |
|----------------|---------------------|----------------------------|
| Curve | secp256k1 | Nostr-native |
| DH | ECDH on secp256k1 | Standard EC Diffie-Hellman |
| Cipher | ChaCha20-Poly1305 | AEAD, same as NIP-44 |
| Hash | SHA-256 | Nostr-native |
| Key derivation | HKDF-SHA256 | Standard Noise KDF |
### 6.4 Cryptographic Primitives
@@ -683,11 +680,11 @@ this should be updated to reflect AEAD-only authentication.
## 7. Peer Connection Establishment
Before any of the traffic flows described above can occur, nodes must establish
authenticated peer connections. This section provides a brief overview; see
[fips-design.md](fips-design.md) §1 for the full peer authentication protocol
and [fips-architecture.md](fips-architecture.md) for the startup sequence.
authenticated peer connections using Noise IK. See [fips-design.md](fips-design.md)
§1 for full protocol details and [fips-architecture.md](fips-architecture.md)
for the startup sequence.
### 7.1 Connection Flow Summary
### 7.1 Connection Flow Summary (Noise IK)
**Outbound (to static peer):**
@@ -698,43 +695,38 @@ Config: npub + transport hint (e.g., "udp:192.168.1.1:4000")
Create link via transport
Send AuthInit { our_npub, nonce }
Noise IK msg1 (82 bytes): ephemeral + encrypted static key
Receive AuthChallenge { peer_npub, peer_nonce, signature }
Receive msg2 (33 bytes): peer's ephemeral key
Verify signature, send AuthComplete { signature }
Peer authenticated → begins tree gossip
Noise session established → link encrypted → begins tree gossip
```
**Inbound (peer connects to us):**
```text
Transport receives data from unknown address
Transport receives Noise msg1 from unknown address
Receive AuthInit { peer_npub, nonce }
Process msg1 → learn peer's identity from encrypted static key
Send AuthChallenge { our_npub, our_nonce, signature }
Send msg2 (33 bytes): our ephemeral key
Receive AuthComplete { signature }
Verify signature → peer authenticated → begins tree gossip
Noise session established → link encrypted → begins tree gossip
```
### 7.2 Post-Authentication
After successful peer authentication:
After successful Noise handshake:
1. **TreeAnnounce exchange**: Both peers send their current tree state
2. **FilterAnnounce exchange**: Both peers send their bloom filters
3. **Peer is Active**: Can now participate in routing and forwarding
1. **Link encrypted**: All subsequent messages use AEAD encryption
2. **TreeAnnounce exchange**: Both peers send their current tree state
3. **FilterAnnounce exchange**: Both peers send their bloom filters
4. **Peer is Active**: Can now participate in routing and forwarding
The first TreeAnnounce from a new peer may trigger parent reselection if that
peer offers a better path to root.
@@ -746,34 +738,36 @@ peer offers a better path to root.
This section tracks items that need reconciliation with existing design docs
or earlier sections of this document.
### 8.1 Completed Updates (Session 40)
### 8.1 Completed Updates (Session 47)
| Location | Status | Notes |
|----------|--------|-------|
| §3.1 | ✓ Done | Updated to AEAD authentication |
| fips-routing.md Part 4 | ✓ Done | Renamed to "Routing Session Establishment", added terminology note |
| fips-routing.md SessionSetup/Ack | ✓ Done | Added `handshake_payload` for crypto handshake |
| fips-design.md §7 Encryption | ✓ Done | Updated to reference Noise KK instead of NIP-44 |
| fips-design.md §6 Messages | ✓ Done | Added SessionSetup, SessionAck, CoordsRequired types |
| fips-design.md §1 Peer Auth | ✓ Done | Added terminology note distinguishing peer auth from crypto sessions |
| fips-architecture.md Config | ✓ Done | Renamed to "Routing Session", added "Crypto Session" section |
| Location | Status | Notes |
|----------------------------------|--------|----------------------------------------------------|
| fips-design.md §1 Peer Auth | ✓ Done | Replaced custom handshake with Noise IK |
| fips-design.md §6 Messages | ✓ Done | Split into LinkMessageType + SessionMessageType |
| protocol.rs | ✓ Done | Removed Hello/Challenge/Auth/AuthAck types |
| protocol.rs | ✓ Done | Added SessionDatagram for link-layer encapsulation |
| This document §6 | ✓ Done | Updated to two-layer Noise IK architecture |
| This document §7 | ✓ Done | Updated connection flow for Noise IK |
### 8.2 Cross-References Added
### 8.2 Previous Updates (Session 40)
All design docs now reference fips-protocol-flow.md in their References sections:
- fips-design.md
- fips-routing.md
- fips-architecture.md
| Location | Status | Notes |
|----------------------------------|--------|-----------------------------------------------------|
| §3.1 | ✓ Done | Updated to AEAD authentication |
| fips-routing.md Part 4 | ✓ Done | Renamed to "Routing Session Establishment" |
| fips-routing.md SessionSetup/Ack | ✓ Done | Added `handshake_payload` for crypto handshake |
| fips-design.md §7 Encryption | ✓ Done | Updated to reference Noise instead of NIP-44 |
| fips-architecture.md Config | ✓ Done | Renamed to "Routing Session", added "Crypto Session"|
### 8.3 Design Doc Alignment Summary
The following decisions from this document have been propagated:
1. **Session terminology** (§5.4): "Routing Session" vs "Crypto Session" distinction
1. **Two-layer architecture**: Link layer (Noise IK peer auth) and session layer
(Noise IK end-to-end) operate independently with separate keys
2. **Session terminology** (§5.4): "Routing Session" vs "Crypto Session" distinction
now consistent across all docs
2. **Combined establishment** (§5.5): SessionSetup/SessionAck carry optional
`handshake_payload` for Noise KK handshake
3. **Noise KK** (§6): fips-design.md encryption section updated, new config
parameters added to fips-architecture.md
4. **Peer auth vs crypto session**: fips-design.md §1 clarifies the distinction
3. **Combined establishment** (§5.5): SessionSetup/SessionAck carry optional
`handshake_payload` for session-layer Noise IK handshake
4. **Message type split**: LinkMessageType for hop-by-hop, SessionMessageType for
end-to-end (carried inside SessionDatagram)
+8 -8
View File
@@ -228,8 +228,7 @@ loop {
```text
PeerSlot::Connecting(PeerConnection)
AuthInit/AuthResponse exchange
│ Noise KK handshake
Noise IK handshake (2 messages)
PeerSlot::Active(ActivePeer)
@@ -240,13 +239,13 @@ PeerSlot::Active(ActivePeer)
**PeerConnection** contains:
- Noise handshake state (ephemeral keys, handshake hash)
- Retry tracking (attempts, last_sent)
- Noise IK handshake state (ephemeral keys, handshake hash)
- Expected identity (for outbound) or discovered identity (for inbound)
- Direction (Inbound vs Outbound)
**ActivePeer** contains:
- Session keys (for encrypt/decrypt)
- NoiseSession (symmetric keys for encrypt/decrypt)
- Tree position (declaration, coordinates)
- Bloom filter (what's reachable through this peer)
- Statistics (last_seen, link_stats)
@@ -342,9 +341,10 @@ struct Node {
For inbound connections from unknown addresses:
1. Parse packet → extract sender's identity (in AuthInit)
2. Create new PeerConnection
3. Add to `peers` and `addr_to_peer`
1. Receive Noise IK msg1 → decrypt to extract sender's static key (identity)
2. Create new PeerConnection with discovered identity
3. Add to `connections` (by LinkId) and `addr_to_link`
4. After handshake completes, promote to ActivePeer (indexed by NodeId)
## Summary