mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-30 19:46:15 +00:00
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:
@@ -308,17 +308,57 @@ Inbound:
|
||||
### Peer Lifecycle
|
||||
|
||||
```
|
||||
Discovered ──► Connecting ──► Authenticating ──► Active ──► Disconnected
|
||||
│ │ │ ▲
|
||||
│ v v │
|
||||
└──────── [timeout/fail] ──────────────────────────────────┘
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Disconnected │
|
||||
└─────────────────────────────────────────┘
|
||||
│ │
|
||||
[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
|
||||
- `Connecting`: link establishment in progress (connection-oriented only)
|
||||
- `Authenticating`: FIPS auth handshake in progress
|
||||
- `Active`: fully integrated (has declaration, ancestry, filter)
|
||||
- `Disconnected`: was active, now gone
|
||||
**State descriptions:**
|
||||
|
||||
- `Disconnected`: No active connection; for static peers, retry with backoff
|
||||
- `Connecting`: Link establishment in progress (connection-oriented transports only)
|
||||
- `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:**
|
||||
|
||||
@@ -327,9 +367,10 @@ PeerEvent
|
||||
├── Discovered { link_id, transport_addr, hint: Option<PublicKey> }
|
||||
├── LinkConnected
|
||||
├── LinkFailed { reason }
|
||||
├── AuthChallengeReceived { challenge }
|
||||
├── AuthResponseReceived { response }
|
||||
├── AuthSuccess
|
||||
├── AuthInitReceived { npub, nonce }
|
||||
├── AuthChallengeReceived { npub, nonce, signature }
|
||||
├── AuthCompleteReceived { signature }
|
||||
├── AuthSuccess { npub, node_id }
|
||||
├── AuthFailed { reason }
|
||||
├── TreeAnnounceReceived { declaration, ancestry }
|
||||
├── 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
|
||||
|
||||
### 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
|
||||
├── npub: PublicKey // required: who is this
|
||||
├── alias: Option<String> // human-readable label
|
||||
├── addresses: Vec<PeerAddress> // how to reach them
|
||||
└── connect_policy: ConnectPolicy
|
||||
└── connect_policy: ConnectPolicy // AutoConnect for initial impl
|
||||
|
||||
PeerAddress
|
||||
├── transport_type: TransportType // "udp", "ethernet", "tor", etc.
|
||||
├── addr: String // transport-specific, parsed by driver
|
||||
└── priority: u8 // preference order
|
||||
└── priority: u8 // preference order (lower = preferred)
|
||||
|
||||
ConnectPolicy
|
||||
├── AutoConnect // connect on startup
|
||||
├── OnDemand // connect when traffic needs routing
|
||||
└── Manual // wait for explicit API call
|
||||
├── AutoConnect // connect on startup (initial impl)
|
||||
├── OnDemand // connect when traffic needs routing (future)
|
||||
└── 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
|
||||
|
||||
+83
-32
@@ -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
|
||||
> requires an explicit application-layer protocol because it supports transports
|
||||
> without built-in encryption or key exchange (radio links, serial connections).
|
||||
> On transports that provide identity-binding encryption, this protocol may be
|
||||
> skipped if the transport key is bound to the peer's npub.
|
||||
> 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
|
||||
@@ -134,13 +134,15 @@ adversary claims to be a node it doesn't control.
|
||||
```text
|
||||
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
|
||||
@@ -148,41 +150,87 @@ Initiator (A) Responder (B)
|
||||
|
||||
**Protocol flow:**
|
||||
|
||||
1. **HELLO**: Initiator sends its npub to responder
|
||||
2. **CHALLENGE**: Responder generates a 32-byte random challenge and sends it
|
||||
along with its own npub and a challenge for the initiator
|
||||
3. **AUTH**: Initiator signs both challenges and sends both responses
|
||||
4. **AUTH_ACK**: Responder verifies initiator's response to its challenge,
|
||||
then sends its response to the initiator's challenge
|
||||
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)
|
||||
|
||||
After successful mutual authentication, both nodes have proven they control
|
||||
their claimed private keys.
|
||||
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.
|
||||
|
||||
### Challenge-Response Construction
|
||||
### Crossing Connection Handling
|
||||
|
||||
The challenge response is constructed with domain separation to prevent
|
||||
cross-protocol signature reuse:
|
||||
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:
|
||||
|
||||
```text
|
||||
challenge = random(32)
|
||||
timestamp = current_unix_time()
|
||||
digest = SHA256("fips-auth-v1" || challenge || timestamp)
|
||||
response = schnorr_sign(nsec, digest)
|
||||
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
|
||||
```
|
||||
|
||||
**Domain separation**: The `"fips-auth-v1"` prefix ensures that signatures
|
||||
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"`).
|
||||
**Rules:**
|
||||
|
||||
**Timestamp binding**: The timestamp is included in the signed digest and
|
||||
transmitted alongside the response. The verifier checks that the timestamp
|
||||
is within an acceptable window (e.g., ±5 minutes) to prevent replay attacks
|
||||
where an attacker captures and later reuses a valid response.
|
||||
- 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
|
||||
|
||||
**Nonce freshness**: The 32-byte random challenge ensures that even if an
|
||||
attacker can predict the timestamp, they cannot pre-compute valid responses.
|
||||
Each authentication attempt requires a fresh signature.
|
||||
This ensures exactly one handshake completes with minimal wasted effort.
|
||||
|
||||
### 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
|
||||
|
||||
@@ -601,6 +649,9 @@ A single node may have multiple transports of different types:
|
||||
| 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 |
|
||||
|
||||
|
||||
@@ -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
|
||||
or earlier sections of this document.
|
||||
|
||||
### 7.1 Completed Updates (Session 40)
|
||||
### 8.1 Completed Updates (Session 40)
|
||||
|
||||
| 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-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:
|
||||
|
||||
@@ -705,7 +766,7 @@ All design docs now reference fips-protocol-flow.md in their References sections
|
||||
- fips-routing.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:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user