Session 41: Peer authentication protocol design

Design 3-message peer auth handshake (AuthInit → AuthChallenge → AuthComplete):
- Deterministic crossing connection handling via npub ordering
- Domain-separated signatures (fips-peer-auth-v1)
- New message types: 0x09 AuthInit, 0x0a AuthChallenge, 0x0b AuthComplete

fips-design.md:
- Replace 4-message auth with 3-message design
- Add crossing connection handling rules
- Add message structures and signature construction

fips-architecture.md:
- Expand peer lifecycle state machine with auth states
- Add Node Startup Sequence section
- Add Static Peer Retry Policy (exponential backoff, jitter)
- Add Inbound Connection Acceptance section
- Update peer config for static-only initial impl

fips-protocol-flow.md:
- Add §7 Peer Connection Establishment
- Add §7.2 Post-Authentication flow

Design decisions: static peers only, always do peer auth regardless of
transport, accept all authenticated inbound connections.
This commit is contained in:
Johnathan Corgan
2026-01-31 19:13:15 +00:00
parent b54c347e76
commit 738775258a
3 changed files with 299 additions and 55 deletions
+151 -19
View File
@@ -308,17 +308,57 @@ Inbound:
### Peer Lifecycle ### Peer Lifecycle
``` ```
Discovered ──► Connecting ──► Authenticating ──► Active ──► Disconnected ┌─────────────────────────────────────────┐
Disconnected
│ v v │ └─────────────────────────────────────────┘
└──────── [timeout/fail] ──────────────────────────────────┘ │ │
[outbound] │ │ [inbound data]
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ Connecting │ │AwaitingAuthInit │
│(conn-oriented) │ │ │
└──────────────────┘ └──────────────────┘
│ │
[link ready] │ [recv AuthInit]
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│AwaitingChallenge │ │AwaitingComplete │
│ (sent AuthInit) │ │(sent AuthChallenge)│
└──────────────────┘ └──────────────────┘
│ │
[recv AuthChallenge] │ │ [recv AuthComplete]
[verify, send │ │ [verify]
AuthComplete] │ │
▼ ▼
┌─────────────────────────────────────────┐
│ Active │
│ (tree gossip, filter exchange) │
└─────────────────────────────────────────┘
│ [link down / timeout]
┌─────────────────────────────────────────┐
│ Disconnected │
│ (retry if static peer) │
└─────────────────────────────────────────┘
``` ```
- `Discovered`: known via discovery or config, no link yet **State descriptions:**
- `Connecting`: link establishment in progress (connection-oriented only)
- `Authenticating`: FIPS auth handshake in progress - `Disconnected`: No active connection; for static peers, retry with backoff
- `Active`: fully integrated (has declaration, ancestry, filter) - `Connecting`: Link establishment in progress (connection-oriented transports only)
- `Disconnected`: was active, now gone - `AwaitingAuthInit`: Inbound connection, waiting for peer's AuthInit
- `AwaitingChallenge`: Sent AuthInit, waiting for AuthChallenge
- `AwaitingComplete`: Sent AuthChallenge, waiting for AuthComplete
- `Active`: Authenticated; participating in tree gossip and filter exchange
**Crossing connection handling:**
When in `AwaitingChallenge` and we receive an AuthInit from the same peer:
- If local npub < remote npub: Ignore incoming AuthInit, remain initiator
- If local npub > remote npub: Switch to responder role, send AuthChallenge,
transition to `AwaitingComplete`
**Events:** **Events:**
@@ -327,9 +367,10 @@ PeerEvent
├── Discovered { link_id, transport_addr, hint: Option<PublicKey> } ├── Discovered { link_id, transport_addr, hint: Option<PublicKey> }
├── LinkConnected ├── LinkConnected
├── LinkFailed { reason } ├── LinkFailed { reason }
├── AuthChallengeReceived { challenge } ├── AuthInitReceived { npub, nonce }
├── AuthResponseReceived { response } ├── AuthChallengeReceived { npub, nonce, signature }
├── AuthSuccess ├── AuthCompleteReceived { signature }
├── AuthSuccess { npub, node_id }
├── AuthFailed { reason } ├── AuthFailed { reason }
├── TreeAnnounceReceived { declaration, ancestry } ├── TreeAnnounceReceived { declaration, ancestry }
├── FilterAnnounceReceived { filter, sequence, ttl } ├── FilterAnnounceReceived { filter, sequence, ttl }
@@ -683,28 +724,119 @@ transport.*.auto_connect # single configured peer
--- ---
## Node Startup Sequence
The startup sequence initializes components in dependency order:
```text
1. Load configuration
├── Parse config files (system, user, local)
├── Validate transport and peer configurations
└── Merge with defaults
2. Initialize identity
├── Load nsec from config (or generate if absent)
├── Derive npub, node_id, and FIPS address
└── Log identity information
3. Initialize transports
├── Create transport instances from config
└── Transports in Configured state
4. Start transports (begin listening)
├── Bind sockets, open interfaces
├── Transports transition to Up state
└── Ready to accept inbound connections
5. Connect to static peers
├── For each configured peer with AutoConnect policy:
│ ├── Create link via appropriate transport
│ ├── Send AuthInit to initiate authentication
│ └── On success: peer joins tree gossip
└── Failed connections enter retry with backoff
6. Node operational
├── Participating in spanning tree (even with 0 peers)
├── Processing inbound connections
└── Retrying unreachable static peers in background
```
**Notes:**
- Transports start listening (step 4) before outbound connections (step 5) to
accept inbound connections from peers who have us configured
- The node is "operational" as soon as any peer authenticates successfully
- Static peer connection attempts continue in background with retry policy
- With 0 authenticated peers, the node considers itself a potential root
### Static Peer Retry Policy
When a static peer is unreachable or authentication fails:
| Parameter | Default | Description |
|-----------|---------|-------------|
| Initial delay | 1s | First retry delay |
| Max delay | 300s | Cap on exponential backoff |
| Backoff factor | 2.0 | Multiplier per attempt |
| Jitter | ±25% | Randomization to avoid thundering herd |
| Max attempts | unlimited | Static peers retry indefinitely |
The retry timer resets to initial delay after a successful connection that
later disconnects.
### Inbound Connection Acceptance
For initial implementation, all inbound connections that successfully
authenticate are accepted. Future versions may add:
- Peer allowlists/blocklists
- Connection limits per transport
- Rate limiting on authentication attempts
- Reputation-based acceptance
---
## Configuration ## Configuration
### Peer Configuration ### Peer Configuration
Peers are configured separately from transports: Peers are configured at the node level, separately from transports. For initial
implementation, only static peers with `AutoConnect` policy are supported;
discovery-based and on-demand peering are future enhancements.
``` ```text
PeerConfig PeerConfig
├── npub: PublicKey // required: who is this ├── npub: PublicKey // required: who is this
├── alias: Option<String> // human-readable label ├── alias: Option<String> // human-readable label
├── addresses: Vec<PeerAddress> // how to reach them ├── addresses: Vec<PeerAddress> // how to reach them
└── connect_policy: ConnectPolicy └── connect_policy: ConnectPolicy // AutoConnect for initial impl
PeerAddress PeerAddress
├── transport_type: TransportType // "udp", "ethernet", "tor", etc. ├── transport_type: TransportType // "udp", "ethernet", "tor", etc.
├── addr: String // transport-specific, parsed by driver ├── addr: String // transport-specific, parsed by driver
└── priority: u8 // preference order └── priority: u8 // preference order (lower = preferred)
ConnectPolicy ConnectPolicy
├── AutoConnect // connect on startup ├── AutoConnect // connect on startup (initial impl)
├── OnDemand // connect when traffic needs routing ├── OnDemand // connect when traffic needs routing (future)
└── Manual // wait for explicit API call └── Manual // wait for explicit API call (future)
```
**Example configuration (YAML):**
```yaml
node:
peers:
- npub: "npub1abc..."
alias: "gateway"
addresses:
- transport: udp
addr: "192.168.1.1:4000"
priority: 1
- transport: tor
addr: "xyz...abc.onion:4000"
priority: 2
connect_policy: auto_connect
``` ```
### Transport Configuration ### Transport Configuration
+83 -32
View File
@@ -121,8 +121,8 @@ adversary claims to be a node it doesn't control.
> relies on transport-layer security (TLS/QUIC) for identity binding. FIPS > relies on transport-layer security (TLS/QUIC) for identity binding. FIPS
> requires an explicit application-layer protocol because it supports transports > requires an explicit application-layer protocol because it supports transports
> without built-in encryption or key exchange (radio links, serial connections). > without built-in encryption or key exchange (radio links, serial connections).
> On transports that provide identity-binding encryption, this protocol may be > For initial implementation, peer authentication is always performed regardless
> skipped if the transport key is bound to the peer's npub. > of transport capabilities; this may be optimized in future versions.
> >
> **Terminology note**: *Peer authentication* (this section) is hop-by-hop—it > **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 > verifies that a direct peer is who they claim to be. This is distinct from
@@ -134,13 +134,15 @@ adversary claims to be a node it doesn't control.
```text ```text
Initiator (A) Responder (B) Initiator (A) Responder (B)
│ │ │ │
│──────────── HELLO(npub_A) ─────────────────►│ │───── AuthInit { a_npub, nonce_a } ─────────►│
│ │ │ │
│◄───────── CHALLENGE(npub_B, challenge_B) ─── │◄──── AuthChallenge { b_npub, nonce_b,
│ sig_b(nonce_a | a_npub) } ─────────│
│ │ │ │
│── AUTH(challenge_A, response_A, response_B)─► │───── AuthComplete {
│ sig_a(nonce_b | b_npub) } ────────►│
│ │ │ │
│◄─────────── AUTH_ACK(response_A') ──────────│ [A verifies B after msg 2] [B verifies A after msg 3]
│ │ │ │
▼ ▼ ▼ ▼
Authenticated Authenticated Authenticated Authenticated
@@ -148,41 +150,87 @@ Initiator (A) Responder (B)
**Protocol flow:** **Protocol flow:**
1. **HELLO**: Initiator sends its npub to responder 1. **AuthInit**: Initiator sends its npub and a 32-byte random nonce
2. **CHALLENGE**: Responder generates a 32-byte random challenge and sends it 2. **AuthChallenge**: Responder sends its npub, its own nonce, and a signature
along with its own npub and a challenge for the initiator proving it controls its nsec (signing the initiator's nonce and npub)
3. **AUTH**: Initiator signs both challenges and sends both responses 3. **AuthComplete**: Initiator sends a signature proving it controls its nsec
4. **AUTH_ACK**: Responder verifies initiator's response to its challenge, (signing the responder's nonce and npub)
then sends its response to the initiator's challenge
After successful mutual authentication, both nodes have proven they control After message 2, the initiator can verify the responder's identity. After
their claimed private keys. message 3, the responder can verify the initiator's identity. Both nodes have
now proven they control their claimed private keys.
### Challenge-Response Construction ### Crossing Connection Handling
The challenge response is constructed with domain separation to prevent When both nodes have each other as static peers, both may initiate authentication
cross-protocol signature reuse: simultaneously ("crossing hellos"). This is resolved using deterministic
tie-breaking based on npub ordering:
```text ```text
challenge = random(32) A (lower npub) B (higher npub)
timestamp = current_unix_time() │ │
digest = SHA256("fips-auth-v1" || challenge || timestamp) │─── AuthInit { a_npub, nonce_a } ────────►│
response = schnorr_sign(nsec, digest) │◄── 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
``` ```
**Domain separation**: The `"fips-auth-v1"` prefix ensures that signatures **Rules:**
created for FIPS authentication cannot be replayed in other contexts (e.g.,
a Nostr event signature). If the authentication protocol is revised, the
version string changes (e.g., `"fips-auth-v2"`).
**Timestamp binding**: The timestamp is included in the signed digest and - If a node receives AuthInit while its own AuthInit is pending to the same peer:
transmitted alongside the response. The verifier checks that the timestamp - If local npub < remote npub: Continue as initiator, ignore incoming AuthInit
is within an acceptable window (e.g., ±5 minutes) to prevent replay attacks - If local npub > remote npub: Abort own initiation, switch to responder role
where an attacker captures and later reuses a valid response.
**Nonce freshness**: The 32-byte random challenge ensures that even if an This ensures exactly one handshake completes with minimal wasted effort.
attacker can predict the timestamp, they cannot pre-compute valid responses.
Each authentication attempt requires a fresh signature. ### Authentication Message Structures
```rust
struct AuthInit {
npub: [u8; 32], // Initiator's public key (x-only)
nonce: [u8; 32], // Random challenge
}
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)
}
struct AuthComplete {
signature: [u8; 64], // sig(responder_nonce || responder_npub)
}
```
### Signature Construction
Signatures are constructed with domain separation to prevent cross-protocol
signature reuse:
```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 ### Authentication Failure Handling
@@ -601,6 +649,9 @@ A single node may have multiple transports of different types:
| 0x06 | SessionSetup | Routing session + crypto handshake init | | 0x06 | SessionSetup | Routing session + crypto handshake init |
| 0x07 | SessionAck | Routing session ack + crypto response | | 0x07 | SessionAck | Routing session ack + crypto response |
| 0x08 | CoordsRequired | Router cache miss notification | | 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 | | 0x10 | Traffic | Encrypted application data |
| 0x11 | TrafficAck | Delivery acknowledgement | | 0x11 | TrafficAck | Delivery acknowledgement |
+65 -4
View File
@@ -680,12 +680,73 @@ this should be updated to reflect AEAD-only authentication.
--- ---
## 7. Document Reconciliation ## 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.
### 7.1 Connection Flow Summary
**Outbound (to static peer):**
```text
Config: npub + transport hint (e.g., "udp:192.168.1.1:4000")
Create link via transport
Send AuthInit { our_npub, nonce }
Receive AuthChallenge { peer_npub, peer_nonce, signature }
Verify signature, send AuthComplete { signature }
Peer authenticated → begins tree gossip
```
**Inbound (peer connects to us):**
```text
Transport receives data from unknown address
Receive AuthInit { peer_npub, nonce }
Send AuthChallenge { our_npub, our_nonce, signature }
Receive AuthComplete { signature }
Verify signature → peer authenticated → begins tree gossip
```
### 7.2 Post-Authentication
After successful peer authentication:
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
The first TreeAnnounce from a new peer may trigger parent reselection if that
peer offers a better path to root.
---
## 8. Document Reconciliation
This section tracks items that need reconciliation with existing design docs This section tracks items that need reconciliation with existing design docs
or earlier sections of this document. or earlier sections of this document.
### 7.1 Completed Updates (Session 40) ### 8.1 Completed Updates (Session 40)
| Location | Status | Notes | | Location | Status | Notes |
|----------|--------|-------| |----------|--------|-------|
@@ -697,7 +758,7 @@ or earlier sections of this document.
| fips-design.md §1 Peer Auth | ✓ Done | Added terminology note distinguishing peer auth from crypto sessions | | 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 | | fips-architecture.md Config | ✓ Done | Renamed to "Routing Session", added "Crypto Session" section |
### 7.2 Cross-References Added ### 8.2 Cross-References Added
All design docs now reference fips-protocol-flow.md in their References sections: All design docs now reference fips-protocol-flow.md in their References sections:
@@ -705,7 +766,7 @@ All design docs now reference fips-protocol-flow.md in their References sections
- fips-routing.md - fips-routing.md
- fips-architecture.md - fips-architecture.md
### 7.3 Design Doc Alignment Summary ### 8.3 Design Doc Alignment Summary
The following decisions from this document have been propagated: The following decisions from this document have been propagated: