Webhook HMAC 서명 검증
X-Logi-Signature: sha256=<hex> 는 HMAC-SHA256(webhook_secret, request_body) 입니다.
검증 순서
- Timestamp 범위 확인 —
X-Logi-Timestamp와 현재 시간의 차이가 ±5분 이내 (replay 방어) - 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)
endpython
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 파싱 전 원본 바이트)로 검증. 파싱 후 직렬화하면 공백/순서가 달라져 서명 불일치.