본문으로 건너뛰기

서명 검증

웹훅 요청은 두 헤더를 포함합니다.

  • X-Webhook-Signaturesha256=<hex> 형식의 HMAC-SHA256 서명
  • X-Webhook-Timestamp — 서명에 사용된 Unix 초
주의

서명 값에는 sha256= 접두사가 포함됩니다. 검증할 때 접두사를 붙여 비교하세요.

알고리즘

  1. raw 요청 본문을 그대로 사용합니다 (JSON을 다시 직렬화하지 마세요 — 직렬화 차이로 서명이 어긋납니다)
  2. <timestamp> + . + <body>를 시크릿으로 HMAC-SHA256 합니다
  3. sha256= + hex 결과가 X-Webhook-Signature와 같은지 constant-time으로 비교합니다
  4. (권장) 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회만 평문으로 반환됩니다.