본문으로 건너뛰기

웹훅 서명 검증

모든 수신 요청에는 HMAC 서명이 포함됩니다. 여러분의 서버는 처리 전에 반드시 서명을 검증해 위조 요청을 거부해야 합니다.

서명 헤더

헤더
X-Bridge-Signaturev0=<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을 파싱했다가 다시 직렬화하면 공백·키 순서가 달라져 서명이 어긋납니다. 바디를 읽은 그대로 검증하세요.

검증 절차

  1. 원본 바디를 바이트 그대로 확보합니다.
  2. X-Bridge-Timestamp 를 읽습니다.
  3. expected = "v0=" + hex(HMAC_SHA256(whsec, "v0:" + ts + ":" + body)) 를 계산합니다.
  4. X-Bridge-Signature상수시간 비교합니다(타이밍 공격 방지).
  5. (권장) 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 의 고정 벡터 테스트로 계약이 고정돼 있습니다.