웹훅 서명 검증
모든 수신 요청에는 HMAC 서명이 포함됩니다. 여러분의 서버는 처리 전에 반드시 서명을 검증해 위조 요청을 거부해야 합니다.
서명 헤더
| 헤더 | 값 |
|---|---|
X-Bridge-Signature | v0=<hex> — 서명(소문자 hex, v0= 접두) |
X-Bridge-Timestamp | 서명 생성 시각(unix 초) |
X-Bridge-Event | 이벤트 kind |
X-Bridge-Delivery-Id | 전달 고유 ID(UUID, 재시도 시 동일) |
서명 알고리즘
서명대상 = "v0:" + X-Bridge-Timestamp + ":" + <원본 요청 바디 문자열>
서명값 = "v0=" + lowerhex( HMAC_SHA256( whsec, 서명대상 ) )
- 해시: HMAC-SHA256
- 키: 서명 검증 키 문자열
whsec_...전체(UTF-8 바이트). 호스티드 게이트웨이는 톡브릿지 센터에 표시되며(연결한 Agent 키에서 파생), CLI 게이트웨이는--webhook-secret값입니다. - 인코딩: 소문자 hex,
v0=접두
원본 바디 그대로
서명은 수신한 원본 바이트(raw body) 를 대상으로 합니다. JSON을 파싱했다가 다시 직렬화하면 공백·키 순서가 달라져 서명이 어긋납니다. 바디를 읽은 그대로 검증하세요.
검증 절차
- 원본 바디를 바이트 그대로 확보합니다.
X-Bridge-Timestamp를 읽습니다.expected = "v0=" + hex(HMAC_SHA256(whsec, "v0:" + ts + ":" + body))를 계산합니다.X-Bridge-Signature와 상수시간 비교합니다(타이밍 공격 방지).- (권장)
ts가 현재 시각과 크게 벗어나면 리플레이로 간주해 거부합니다.
참조 구현
Node.js (Express)
const crypto = require('crypto');
const express = require('express');
const app = express();
const WHSEC = process.env.WHSEC; // "whsec_..."
// 원본 바디를 그대로 확보하려면 raw 파서를 쓴다.
app.post('/webhook', express.raw({ type: '*/*' }), (req, res) => {
const ts = req.get('X-Bridge-Timestamp') || '';
const sig = req.get('X-Bridge-Signature') || '';
const body = req.body; // Buffer (raw)
const mac = crypto.createHmac('sha256', WHSEC)
.update('v0:' + ts + ':').update(body)
.digest('hex');
const expected = 'v0=' + mac;
const ok = sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
if (!ok) return res.status(401).send('bad signature');
const evt = JSON.parse(body.toString('utf8'));
// evt = { brand, userKey, kind, seq }
// ... 처리 ...
res.sendStatus(200); // 반드시 2xx
});
C# (ASP.NET minimal API)
app.MapPost("/webhook", async (HttpRequest req) =>
{
using var ms = new MemoryStream();
await req.Body.CopyToAsync(ms);
var body = ms.ToArray(); // 원본 바이트
var ts = req.Headers["X-Bridge-Timestamp"].ToString();
var sig = req.Headers["X-Bridge-Signature"].ToString();
var key = Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("WHSEC")!);
var msg = Encoding.UTF8.GetBytes($"v0:{ts}:").Concat(body).ToArray();
var expected = "v0=" + Convert.ToHexString(HMACSHA256.HashData(key, msg)).ToLowerInvariant();
var ok = CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(sig), Encoding.UTF8.GetBytes(expected));
if (!ok) return Results.Unauthorized();
// JsonSerializer.Deserialize(body) → { brand, userKey, kind, seq }
return Results.Ok(); // 반드시 2xx
});
참조 수신기
TalkBridge 저장소의 project/webconsult-test 가 이 검증 스킴의 참조 구현입니다 —
수신 → 서명검증 → 본문조회 → 상담 화면 → 발신까지 이어지는 완결형이며,
멱등(X-Bridge-Delivery-Id)·즉시 2xx 후 비동기 후처리·kind:"agent" echo 필터를 실제로 구현했습니다.
서명 판정은 project/webconsult-test.tests 의 고정 벡터 테스트로 계약이 고정돼 있습니다.