diff --git a/docs/design/fips-architecture.md b/docs/design/fips-architecture.md index 05a4c42..b31db0a 100644 --- a/docs/design/fips-architecture.md +++ b/docs/design/fips-architecture.md @@ -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 } ├── 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 // human-readable label ├── addresses: Vec // 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 diff --git a/docs/design/fips-design.md b/docs/design/fips-design.md index 39196c4..94cfbfc 100644 --- a/docs/design/fips-design.md +++ b/docs/design/fips-design.md @@ -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 | diff --git a/docs/design/fips-protocol-flow.md b/docs/design/fips-protocol-flow.md index 73a4621..dbc34da 100644 --- a/docs/design/fips-protocol-flow.md +++ b/docs/design/fips-protocol-flow.md @@ -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: