From d0ed43cf5877ee29eb3d7aa449887c1205891c11 Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Tue, 31 Mar 2026 18:18:50 +0200 Subject: [PATCH] fix(federation): add nonce length validation and fix verification flow docs (FED-008) --- docs/systems/federation.md | 5 +++-- packages/server/src/utils/federationAuth.ts | 3 +++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/systems/federation.md b/docs/systems/federation.md index 90baf535..633fb9c9 100644 --- a/docs/systems/federation.md +++ b/docs/systems/federation.md @@ -135,10 +135,11 @@ Two layers of replay protection: ### Inbound Verification Flow (`POST /api/federation/relay`) -1. `parseFederationHeaders()` extracts origin, timestamp, signature from headers +1. `parseFederationHeaders()` extracts origin, timestamp, signature, and nonce from headers 2. Look up peer by `origin` in `federation_peers` -- must exist and be `status = 'active'` 3. Re-serialize request body to JSON: `JSON.stringify(request.body)` -4. `verifySignature(bodyString, signature, peer.hmacSecret, timestamp)` -- reject if false +4. `verifySignature(bodyString, signature, peer.hmacSecret, timestamp, nonce)` -- reject if false +5. Nonce enforcement: duplicate nonce → 409, missing nonce from ratcheted peer → 401, legacy peer → warn **Important:** The body is re-serialized server-side. This means Fastify's JSON parsing and re-stringification must produce identical output to the sender's `JSON.stringify`. In practice this works because both sides use standard `JSON.stringify` with no custom replacers. diff --git a/packages/server/src/utils/federationAuth.ts b/packages/server/src/utils/federationAuth.ts index 1bb6e337..49c69acf 100644 --- a/packages/server/src/utils/federationAuth.ts +++ b/packages/server/src/utils/federationAuth.ts @@ -132,6 +132,9 @@ export function parseFederationHeaders( const rawNonce = getHeader('x-federation-nonce') ?? getHeader('X-Federation-Nonce'); const nonce = rawNonce && typeof rawNonce === 'string' && rawNonce.trim() ? rawNonce.trim() : null; + // Reject nonces that are unreasonably long (UUID v4 is 36 chars) + if (nonce && nonce.length > 64) return null; + return { origin, timestamp: timestampMs, signature, nonce }; }