feat(federation): wire nonce verification into relay and sync handlers (FED-008)
Pass nonce through to verifySignature and enforce replay protection in both /api/federation/relay and /api/federation/sync: reject duplicate nonces (409), reject nonce-less requests from peers that previously sent nonces (401), warn for legacy peers, and auto-ratchet nonceSupported flag.
This commit is contained in:
@@ -461,10 +461,22 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
|
||||
|
||||
// Serialize body back to JSON for HMAC verification (we control both sides)
|
||||
const bodyString = JSON.stringify(request.body);
|
||||
if (!verifySignature(bodyString, fedHeaders.signature, peer.hmacSecret, fedHeaders.timestamp)) {
|
||||
if (!verifySignature(bodyString, fedHeaders.signature, peer.hmacSecret, fedHeaders.timestamp, fedHeaders.nonce)) {
|
||||
return reply.code(401).send({ error: 'Invalid signature', statusCode: 401 });
|
||||
}
|
||||
|
||||
// 1c. Nonce-based replay protection
|
||||
if (fedHeaders.nonce) {
|
||||
if (isNonceDuplicate(peer.origin, fedHeaders.nonce)) {
|
||||
return reply.code(409).send({ error: 'Duplicate nonce — possible replay', statusCode: 409 });
|
||||
}
|
||||
} else if (peer.nonceSupported) {
|
||||
// Peer previously sent nonces but this request doesn't have one — reject
|
||||
return reply.code(401).send({ error: 'Nonce required — peer previously supported nonces', statusCode: 401 });
|
||||
} else {
|
||||
console.warn(`[federation] Peer ${peer.origin} does not support replay protection (no nonce)`);
|
||||
}
|
||||
|
||||
// 2. Validate request body shape
|
||||
const body = request.body;
|
||||
if (!body || body.version !== 1 || !Array.isArray(body.events)) {
|
||||
@@ -488,6 +500,7 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
|
||||
.set({
|
||||
lastSeenAt: Date.now(),
|
||||
consecutiveFailures: 0,
|
||||
...(fedHeaders.nonce && !peer.nonceSupported ? { nonceSupported: 1 } : {}),
|
||||
})
|
||||
.where(eq(schema.federationPeers.id, peer.id))
|
||||
.run();
|
||||
@@ -537,10 +550,29 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
|
||||
}
|
||||
|
||||
const bodyString = JSON.stringify(request.body);
|
||||
if (!verifySignature(bodyString, fedHeaders.signature, peer.hmacSecret, fedHeaders.timestamp)) {
|
||||
if (!verifySignature(bodyString, fedHeaders.signature, peer.hmacSecret, fedHeaders.timestamp, fedHeaders.nonce)) {
|
||||
return reply.code(401).send({ error: 'Invalid signature', statusCode: 401 });
|
||||
}
|
||||
|
||||
// 1b. Nonce-based replay protection
|
||||
if (fedHeaders.nonce) {
|
||||
if (isNonceDuplicate(peer.origin, fedHeaders.nonce)) {
|
||||
return reply.code(409).send({ error: 'Duplicate nonce — possible replay', statusCode: 409 });
|
||||
}
|
||||
} else if (peer.nonceSupported) {
|
||||
return reply.code(401).send({ error: 'Nonce required — peer previously supported nonces', statusCode: 401 });
|
||||
} else {
|
||||
console.warn(`[federation] Peer ${peer.origin} does not support replay protection (no nonce) [sync]`);
|
||||
}
|
||||
|
||||
// Ratchet: mark peer as nonce-supporting if this is the first nonce we've seen
|
||||
if (fedHeaders.nonce && !peer.nonceSupported) {
|
||||
db.update(schema.federationPeers)
|
||||
.set({ nonceSupported: 1 })
|
||||
.where(eq(schema.federationPeers.id, peer.id))
|
||||
.run();
|
||||
}
|
||||
|
||||
// 2. Validate & normalize request body
|
||||
const body = request.body;
|
||||
if (!body || typeof body.sinceTimestamp !== 'number' || body.sinceTimestamp < 0) {
|
||||
|
||||
Reference in New Issue
Block a user