feat(federation): add /peer/rotate endpoint for secret rotation (FED-011)
This commit is contained in:
@@ -379,6 +379,64 @@ export async function federationRoutes(app: FastifyInstance): Promise<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.
|
||||||
|
app.post<{ Body: { newSecret: string } }>(
|
||||||
|
'/api/federation/peer/rotate',
|
||||||
|
async (request, reply) => {
|
||||||
|
const db = getDb();
|
||||||
|
|
||||||
|
// 1. Verify HMAC signature
|
||||||
|
const fedHeaders = parseFederationHeaders(request.headers as Record<string, string | string[] | undefined>);
|
||||||
|
if (!fedHeaders) {
|
||||||
|
return reply.code(401).send({ error: 'Missing or malformed federation headers', statusCode: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const peer = db
|
||||||
|
.select()
|
||||||
|
.from(schema.federationPeers)
|
||||||
|
.where(eq(schema.federationPeers.origin, fedHeaders.origin))
|
||||||
|
.get();
|
||||||
|
|
||||||
|
if (!peer || peer.status !== 'active') {
|
||||||
|
return reply.code(403).send({ error: 'Unknown or inactive peer', statusCode: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const bodyString = JSON.stringify(request.body);
|
||||||
|
if (!verifyPeerSignature(bodyString, fedHeaders.signature, fedHeaders.timestamp, fedHeaders.nonce, peer)) {
|
||||||
|
return reply.code(401).send({ error: 'Invalid signature', statusCode: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Validate request body
|
||||||
|
const { newSecret } = request.body ?? {};
|
||||||
|
if (!newSecret || typeof newSecret !== 'string' || newSecret.length !== 64) {
|
||||||
|
return reply.code(400).send({ error: 'newSecret must be a 64-character hex string', statusCode: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Reject if rotation already in progress
|
||||||
|
if (peer.pendingHmacSecret) {
|
||||||
|
return reply.code(409).send({
|
||||||
|
error: 'A secret rotation is already in progress — wait for it to complete',
|
||||||
|
statusCode: 409,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Store pending secret and activate grace period
|
||||||
|
db.update(schema.federationPeers)
|
||||||
|
.set({
|
||||||
|
pendingHmacSecret: newSecret,
|
||||||
|
secretRotationAt: Date.now(),
|
||||||
|
})
|
||||||
|
.where(eq(schema.federationPeers.id, peer.id))
|
||||||
|
.run();
|
||||||
|
|
||||||
|
console.log(`[federation] Secret rotation accepted from peer ${peer.origin}`);
|
||||||
|
|
||||||
|
return reply.code(200).send({ accepted: true, gracePeriodMs: 900_000 });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// ─── GET /api/federation/peers ─────────────────────────────────────────────
|
// ─── GET /api/federation/peers ─────────────────────────────────────────────
|
||||||
// Admin-only: list all federation peers (hmacSecret excluded).
|
// Admin-only: list all federation peers (hmacSecret excluded).
|
||||||
app.get(
|
app.get(
|
||||||
|
|||||||
Reference in New Issue
Block a user