feat(center-protocol): enforce HMAC auth for node registration
This commit is contained in:
38
Fabric.Backend.Center/src/common/hmac.ts
Normal file
38
Fabric.Backend.Center/src/common/hmac.ts
Normal 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);
|
||||
}
|
||||
@@ -11,8 +11,4 @@ export class RegisterNodeDto {
|
||||
|
||||
@IsUrl({ require_tld: false })
|
||||
endpoint!: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(8)
|
||||
sharedSecret!: string;
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
---
|
||||
|
||||
## 3. Center ↔ Guild 协议层
|
||||
- [ ] 鉴权方案定稿(node token / HMAC)
|
||||
- [x] 鉴权方案定稿(node token / HMAC)
|
||||
- [ ] 注册握手协议文档化
|
||||
- [ ] 错误码与重试策略统一
|
||||
- [ ] 版本协商(`X-Fabric-Version`)
|
||||
|
||||
Reference in New Issue
Block a user