refactor(federation): consolidate inbound S2S-auth preamble into one helper (#11)

Six S2S-HMAC endpoints repeated the same inbound-auth preamble verbatim
(parse federation headers -> resolve active peer -> optional per-peer rate
limit -> verify HMAC signature -> nonce replay protection). Extract it into
authenticateS2SPeer() so the trust boundary has a single, tested definition.

Adopters (preamble only; every post-auth side effect, body validation, and
response is unchanged):
- DELETE /api/federation/identity      (no rate limiter; warns on missing nonce)
- POST   /api/federation/relay         (relay limiter; warns; keeps in-handler
                                        epoch-baseline populate + nonce ratchet)
- POST   /api/federation/sync          (no limiter; warns with the [sync] tag;
                                        keeps in-handler nonce ratchet)
- POST   /api/federation/users/lookup       (lookup limiter, Retry-After 60)
- POST   /api/federation/users/by-home-id   (same)
- POST   /api/federation/verify-attach-proof(shares lookup bucket, Retry-After 60)

Deliberate non-adopters, each keeping a load-bearing gate the helper would
flatten (documented at each site + in the helper docstring):
- POST /api/federation/epoch        gates status != 'revoked' (peer recovery),
                                    400 on missing headers, no nonce check
- POST /api/federation/peer/rotate  active-only but no nonce check
- POST /api/federation/peer/denied  awaiting_approval gate (404/409), synthetic
                                    no-grace secret verify

Behavior-preserving. The rate limiter is injected (plain { limited, retryAfter }),
so the limit still fires BEFORE signature verification. The only ordering change:
/relay's opportunistic epoch-baseline populate now runs just after the shared
preamble (i.e. after the nonce check) instead of between signature and nonce.
This is provably equivalent for every reachable honest-peer state (a duplicate
nonce means the baseline is already non-null; a valid-signature-but-no-nonce
request from a nonce-supporting peer is unreachable in transit and carries no
security/correctness consequence) and the populate is documented as not
affecting relay accept/reject.

Adds a dedicated unit test covering the full decision table (headers, peer
status, rate-limit + Retry-After, rate-limit-before-signature ordering,
signature, nonce duplicate/missing, log flag + context suffix, success). Full
server suite green (804 tests).
This commit is contained in:
TheZwiss
2026-07-10 03:08:09 +02:00
committed by GitHub
parent c79bf91398
commit d76e06a023
7 changed files with 492 additions and 217 deletions
@@ -625,6 +625,9 @@ export function registerPeerHandshakeRoutes(app: FastifyInstance): void {
// ─── POST /api/federation/peer/rotate ───────────────────────────────────────
// Server-to-server: accept a secret rotation request from a peer instance.
// Authenticated via HMAC-SHA256 signature (current secret), NOT JWT.
// NON-ADOPTER of authenticateS2SPeer (deliberate): active-only like the helper
// but runs NO nonce replay check (the rotation body is its own replay unit);
// sharing the helper would add a nonce gate this endpoint never had.
app.post<{ Body: { newSecret: string } }>(
'/api/federation/peer/rotate',
async (request, reply) => {
@@ -684,6 +687,10 @@ export function registerPeerHandshakeRoutes(app: FastifyInstance): void {
// Server-to-server: receive a denial notification from a remote instance.
// Authenticated via HMAC-SHA256 signature (the secret we sent in our original
// peer/accept request, which the remote stored in their approval queue).
// NON-ADOPTER of authenticateS2SPeer (deliberate): gates on 'awaiting_approval'
// (404 on no peer row, 409 on wrong status — not the helper's active-only 403),
// verifies against a SYNTHETIC no-grace secret object, and runs no nonce check.
// Entirely different control flow.
app.post<{ Body: { origin: string; reason: 'denied_by_admin' | 'expired'; message?: string } }>(
'/api/federation/peer/denied',
async (request, reply) => {