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 })
|
@IsUrl({ require_tld: false })
|
||||||
endpoint!: string;
|
endpoint!: string;
|
||||||
|
|
||||||
@IsString()
|
|
||||||
@MinLength(8)
|
|
||||||
sharedSecret!: string;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
DefaultValuePipe,
|
DefaultValuePipe,
|
||||||
ForbiddenException,
|
ForbiddenException,
|
||||||
Get,
|
Get,
|
||||||
|
Headers,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
Param,
|
Param,
|
||||||
ParseIntPipe,
|
ParseIntPipe,
|
||||||
@@ -18,6 +19,12 @@ import { GuildNode } from '../entities/guild-node.entity';
|
|||||||
import { AuditService } from '../audit/audit.service';
|
import { AuditService } from '../audit/audit.service';
|
||||||
import { RegisterNodeDto } from './dto.register-node.dto';
|
import { RegisterNodeDto } from './dto.register-node.dto';
|
||||||
import { UpdateNodeStatusDto } from './dto.update-node-status.dto';
|
import { UpdateNodeStatusDto } from './dto.update-node-status.dto';
|
||||||
|
import {
|
||||||
|
buildCanonical,
|
||||||
|
safeEqualHex,
|
||||||
|
signCanonical,
|
||||||
|
verifyRequestTime,
|
||||||
|
} from '../common/hmac';
|
||||||
|
|
||||||
@Controller('nodes')
|
@Controller('nodes')
|
||||||
export class NodesController {
|
export class NodesController {
|
||||||
@@ -28,8 +35,26 @@ export class NodesController {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post('register')
|
@Post('register')
|
||||||
async register(@Body() body: RegisterNodeDto) {
|
async register(
|
||||||
if (body.sharedSecret !== process.env.CENTER_SHARED_SECRET) {
|
@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');
|
throw new ForbiddenException('invalid shared secret');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
---
|
---
|
||||||
|
|
||||||
## 3. Center ↔ Guild 协议层
|
## 3. Center ↔ Guild 协议层
|
||||||
- [ ] 鉴权方案定稿(node token / HMAC)
|
- [x] 鉴权方案定稿(node token / HMAC)
|
||||||
- [ ] 注册握手协议文档化
|
- [ ] 注册握手协议文档化
|
||||||
- [ ] 错误码与重试策略统一
|
- [ ] 错误码与重试策略统一
|
||||||
- [ ] 版本协商(`X-Fabric-Version`)
|
- [ ] 版本协商(`X-Fabric-Version`)
|
||||||
|
|||||||
Reference in New Issue
Block a user