Skip to content

Webhook HMAC 서명 검증

X-Logi-Signature: sha256=<hex>HMAC-SHA256(webhook_secret, request_body) 입니다.

검증 순서

  1. Timestamp 범위 확인X-Logi-Timestamp와 현재 시간의 차이가 ±5분 이내 (replay 방어)
  2. Signature 재계산 후 상수시간 비교

언어별 예시

ts
import crypto from "node:crypto";

export function verifyLogiWebhook(req, secret) {
  const ts = Number(req.header("X-Logi-Timestamp"));
  if (Math.abs(Date.now() / 1000 - ts) > 300) throw new Error("replay");

  const sig = req.header("X-Logi-Signature")?.replace("sha256=", "") ?? "";
  const expected = crypto.createHmac("sha256", secret).update(req.rawBody).digest("hex");

  const a = Buffer.from(sig, "hex"), b = Buffer.from(expected, "hex");
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) throw new Error("bad sig");
}
ruby
require "openssl"

def verify_logi!(request, secret)
  ts = request.headers["X-Logi-Timestamp"].to_i
  raise "replay" if (Time.current.to_i - ts).abs > 300

  sig = request.headers["X-Logi-Signature"].to_s.sub("sha256=", "")
  expected = OpenSSL::HMAC.hexdigest("SHA256", secret, request.raw_post)
  raise "bad sig" unless ActiveSupport::SecurityUtils.secure_compare(sig, expected)
end
python
import hmac, hashlib, time

def verify_logi(headers, raw_body, secret):
    ts = int(headers.get("X-Logi-Timestamp", "0"))
    if abs(time.time() - ts) > 300: raise ValueError("replay")
    sig = headers.get("X-Logi-Signature", "").replace("sha256=", "")
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(sig, expected): raise ValueError("bad sig")

주의: 반드시 raw body (JSON 파싱 전 원본 바이트)로 검증. 파싱 후 직렬화하면 공백/순서가 달라져 서명 불일치.

MIT License · Identity가 제품의 신뢰를 만듭니다.