feat(center-protocol): enforce HMAC auth for node registration

This commit is contained in:
nav
2026-05-12 11:11:29 +00:00
parent 670762aa7a
commit ab01a83a90
4 changed files with 66 additions and 7 deletions

View File

@@ -0,0 +1,38 @@
import { createHmac, timingSafeEqual } from 'crypto';
export type HmacInput = {
method: string;
path: string;
timestamp: string;
nonce: string;
body: string;
};
const CLOCK_SKEW_MS = 5 * 60 * 1000;
export function buildCanonical(input: HmacInput): string {
return [
input.method.toUpperCase(),
input.path,
input.timestamp,
input.nonce,
input.body,
].join('\n');
}
export function signCanonical(secret: string, canonical: string): string {
return createHmac('sha256', secret).update(canonical).digest('hex');
}
export function verifyRequestTime(timestamp: string): boolean {
const ts = Date.parse(timestamp);
if (Number.isNaN(ts)) return false;
return Math.abs(Date.now() - ts) <= CLOCK_SKEW_MS;
}
export function safeEqualHex(a: string, b: string): boolean {
const aa = Buffer.from(a, 'hex');
const bb = Buffer.from(b, 'hex');
if (aa.length !== bb.length) return false;
return timingSafeEqual(aa, bb);
}

View File

@@ -11,8 +11,4 @@ export class RegisterNodeDto {
@IsUrl({ require_tld: false })
endpoint!: string;
@IsString()
@MinLength(8)
sharedSecret!: string;
}

View File

@@ -5,6 +5,7 @@ import {
DefaultValuePipe,
ForbiddenException,
Get,
Headers,
NotFoundException,
Param,
ParseIntPipe,
@@ -18,6 +19,12 @@ import { GuildNode } from '../entities/guild-node.entity';
import { AuditService } from '../audit/audit.service';
import { RegisterNodeDto } from './dto.register-node.dto';
import { UpdateNodeStatusDto } from './dto.update-node-status.dto';
import {
buildCanonical,
safeEqualHex,
signCanonical,
verifyRequestTime,
} from '../common/hmac';
@Controller('nodes')
export class NodesController {
@@ -28,8 +35,26 @@ export class NodesController {
) {}
@Post('register')
async register(@Body() body: RegisterNodeDto) {
if (body.sharedSecret !== process.env.CENTER_SHARED_SECRET) {
async register(
@Body() body: RegisterNodeDto,
@Headers('x-fabric-signature') signature?: string,
@Headers('x-fabric-timestamp') timestamp?: string,
@Headers('x-fabric-nonce') nonce?: string,
) {
const secret = process.env.CENTER_SHARED_SECRET as string;
if (!signature || !timestamp || !nonce || !verifyRequestTime(timestamp)) {
throw new ForbiddenException('invalid hmac headers');
}
const canonical = buildCanonical({
method: 'POST',
path: '/api/nodes/register',
timestamp,
nonce,
body: JSON.stringify(body),
});
const expected = signCanonical(secret, canonical);
if (!safeEqualHex(signature, expected)) {
throw new ForbiddenException('invalid shared secret');
}