diff --git a/Fabric.Backend.Center/src/common/hmac.ts b/Fabric.Backend.Center/src/common/hmac.ts new file mode 100644 index 0000000..aa3f150 --- /dev/null +++ b/Fabric.Backend.Center/src/common/hmac.ts @@ -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); +} diff --git a/Fabric.Backend.Center/src/nodes/dto.register-node.dto.ts b/Fabric.Backend.Center/src/nodes/dto.register-node.dto.ts index b12eaee..bf52409 100644 --- a/Fabric.Backend.Center/src/nodes/dto.register-node.dto.ts +++ b/Fabric.Backend.Center/src/nodes/dto.register-node.dto.ts @@ -11,8 +11,4 @@ export class RegisterNodeDto { @IsUrl({ require_tld: false }) endpoint!: string; - - @IsString() - @MinLength(8) - sharedSecret!: string; } diff --git a/Fabric.Backend.Center/src/nodes/nodes.controller.ts b/Fabric.Backend.Center/src/nodes/nodes.controller.ts index c126d92..867f8ed 100644 --- a/Fabric.Backend.Center/src/nodes/nodes.controller.ts +++ b/Fabric.Backend.Center/src/nodes/nodes.controller.ts @@ -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'); } diff --git a/docs/TODO-backend-center-guild.md b/docs/TODO-backend-center-guild.md index 97eb763..14da932 100644 --- a/docs/TODO-backend-center-guild.md +++ b/docs/TODO-backend-center-guild.md @@ -63,7 +63,7 @@ --- ## 3. Center ↔ Guild 协议层 -- [ ] 鉴权方案定稿(node token / HMAC) +- [x] 鉴权方案定稿(node token / HMAC) - [ ] 注册握手协议文档化 - [ ] 错误码与重试策略统一 - [ ] 版本协商(`X-Fabric-Version`)