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:
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user