서명 검증
웹훅 요청은 두 헤더를 포함합니다.
X-Webhook-Signature—sha256=<hex>형식의 HMAC-SHA256 서명X-Webhook-Timestamp— 서명에 사용된 Unix 초
주의
서명 값에는 sha256= 접두사가 포함됩니다. 검증할 때 접두사를 붙여 비교하세요.
알고리즘
- raw 요청 본문을 그대로 사용합니다 (JSON을 다시 직렬화하지 마세요 — 직렬화 차이로 서명이 어긋납니다)
<timestamp>+.+<body>를 시크릿으로 HMAC-SHA256 합니다sha256=+ hex 결과가X-Webhook-Signature와 같은지 constant-time으로 비교합니다- (권장)
X-Webhook-Timestamp가 현재 시각 ±5분 이내인지 확인해 replay를 방지합니다
예시
const crypto = require('crypto');
function verify(rawBody, signatureHeader, timestamp, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(timestamp + '.' + rawBody)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expected));
}
import hmac, hashlib
def verify(raw_body: bytes, signature_header: str, timestamp: str, secret: str) -> bool:
expected = 'sha256=' + hmac.new(
secret.encode(), timestamp.encode() + b'.' + raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature_header, expected)
func verify(rawBody []byte, signatureHeader, timestamp, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(timestamp))
mac.Write([]byte("."))
mac.Write(rawBody)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(signatureHeader), []byte(expected))
}
시크릿 회전
회전 시 서버 측 grace 기간이 없습니다. 회전 직후 짧은 시간 동안 옛 시크릿으로 서명된 요청이 도착할 수 있으니, 무중단이 필요하면 회전 전에 새 시크릿 배포를 준비하세요.
대시보드의 Regenerate Secret, 또는 PAT API의 POST /outbound-webhooks/{id}/regenerate-secret으로 회전합니다. 두 경우 모두 새 시크릿은 응답에서 1회만 평문으로 반환됩니다.