App öffnen
Moonborn — Developers

Webhook-Signature-Verifizierung

Verifiziere den `X-Moonborn-Signature`-HMAC, lehne Replays ab und überlebe eine Secret-Rotation.

Jede Webhook-Delivery trägt einen X-Moonborn-Signature-Header mit dem Format:

X-Moonborn-Signature: t=1747497600,v1=2c4f8a...

t ist der Unix-Timestamp, zu dem die Signatur erstellt wurde; v1 ist das HMAC-SHA256 von {t}.{rawBody}, hex-encodiert, gekeyt mit dem Signing-Secret.

Verifizieren (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'),
  );
}

Hinweis: nutze die rohen Body-Bytes, nicht ein re-serialisiertes JSON-Objekt — die Signatur geht über die exakten Bytes, die Moonborn gesendet hat.

Verifizieren (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

Während der Rotation können Deliveries zwei v1=-Entries tragen (alt + neu). Behandle jeden matchenden Kandidaten als gültig:

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: api.webhooks.secret_rotation_grace_minutes = 60.

Warum der Timestamp-Guard

Ohne t-Age-Rejection kann ein Angreifer, der eine gültige Delivery abfängt, sie für immer replayen. Fünf Minuten ist ein großzügiger Bound; verenge auf 60s, wenn dein Receiver regional ist.

Tarif

Webhooks sind Team+; Verifikations-Logik ist auf jedem Tier dieselbe.

Verwandt