Webhook signature verification
`X-Moonborn-Signature` HMAC'ini doğrula, replay'leri reddet ve secret rotation'dan sağ çık.
Her webhook delivery'i şu formatta bir X-Moonborn-Signature header
taşır:
X-Moonborn-Signature: t=1747497600,v1=2c4f8a...
t imzanın oluşturulduğu Unix timestamp; v1 signing secret ile
anahtarlanmış {t}.{rawBody}'nin hex-encoded HMAC-SHA256'sı.
Doğrula (Node.js)
import { createHmac, timingSafeEqual } from 'node:crypto';
const MAX_AGE_SECONDS = 300;
export function verify(
rawBody: string,
header: string,
secret: string,
): boolean {
const parts = Object.fromEntries(
header.split(',').map((p) => p.split('=', 2) as [string, string]),
);
const t = Number(parts['t']);
const v1 = parts['v1'];
if (!Number.isFinite(t) || typeof v1 !== 'string') return false;
// Replay window guard.
if (Math.abs(Date.now() / 1000 - t) > MAX_AGE_SECONDS) return false;
const signed = `${t}.${rawBody}`;
const expected = createHmac('sha256', secret).update(signed).digest('hex');
return timingSafeEqual(
Buffer.from(v1, 'hex'),
Buffer.from(expected, 'hex'),
);
}Not: yeniden serialize edilmiş bir JSON nesnesi değil, ham body byte'larını kullan — imza Moonborn'un gönderdiği tam byte'lar üzerinde.
Doğrula (Python)
import hmac, hashlib, time
MAX_AGE = 300
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t = int(parts["t"])
v1 = parts["v1"]
if abs(time.time() - t) > MAX_AGE:
return False
signed = f"{t}.{raw_body.decode()}".encode()
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(v1, expected)Secret rotation grace
Rotation sırasında, delivery'ler iki v1= entry'si (eski + yeni)
taşıyabilir. Eşleşen herhangi bir aday'ı geçerli ele al:
const candidates = header.split(',').filter((p) => p.startsWith('v1='));
const ok = candidates.some((c) => {
const v = c.slice(3);
return timingSafeEqual(
Buffer.from(v, 'hex'),
Buffer.from(expected, 'hex'),
);
});Grace period default'u: api.webhooks.secret_rotation_grace_minutes = 60.
Timestamp guard neden
t-yaşı reddi olmadan, geçerli bir delivery'i yakalayan bir saldırgan
onu sonsuza dek replay edebilir. Beş dakika cömert bir sınır;
receiver'ın bölgeselse 60s'e sıkılaştır.
Tarif
Webhook'lar Team+; doğrulama mantığı her tier'da aynı.