MailLandDocs
Webhooks

Signaturverifizierung

Verifizieren Sie Webhook-Signaturen, um die Authentizität eingehender Webhook-Payloads sicherzustellen

Jede Webhook-Zustellung enthält eine kryptografische Signatur, mit der Sie verifizieren können, dass die Anfrage von MailLand stammt und während der Übertragung nicht manipuliert wurde. Verifizieren Sie Signaturen in der Produktion grundsätzlich.

Funktionsweise der Signierung

MailLand signiert jede Webhook-Payload mit HMAC-SHA256 unter Verwendung des geheimen Schlüssels Ihres Webhooks. Die Signatur wird im Header X-MLND-Signature übermittelt.

Signaturformat:

X-MLND-Signature: t=1708171200,v1=b9f747ea899eafe911777060ca8f3c90828d37f1e16ee6e9ee98dacf8b316b67
KomponenteBeschreibung
tUnix-Zeitstempel (Sekunden) zum Zeitpunkt der Signaturerstellung
v1Hexadezimale HMAC-SHA256-Signatur (Kleinbuchstaben)

Signierte Payload:

{timestamp}.{raw_json_body}

Die Signatur wird über den Zeitstempel berechnet, der mit einem Punkt und dem rohen JSON-Anfrage-Body verkettet wird.

Durchgerechnetes Beispiel

Prüfen Sie Ihre Implementierung vor dem Produktivstart gegen genau diese Werte. Alles unten ist echt: Die Signatur ist die, die MailLand für dieses Secret, diesen Zeitstempel und diesen Body erzeugt.

EingabeWert
Secretwhsec_docs_example_secret_do_not_use
Zeitstempel (t)1708171200
Roher Body{"event":"message.delivered","event_id":"evt_a1b2c3d4e5f6"}

Signierte Zeichenkette: Zeitstempel, ein Punkt, dann der Body unverändert:

1708171200.{"event":"message.delivered","event_id":"evt_a1b2c3d4e5f6"}

HMAC-SHA256 dieser Zeichenkette mit dem Secret als Schlüssel, Kleinbuchstaben-Hex:

b9f747ea899eafe911777060ca8f3c90828d37f1e16ee6e9ee98dacf8b316b67

Der Header dieser Zustellung wäre also:

X-MLND-Signature: t=1708171200,v1=b9f747ea899eafe911777060ca8f3c90828d37f1e16ee6e9ee98dacf8b316b67

Reproduzieren Sie es in einer Zeile:

printf '%s' '1708171200.{"event":"message.delivered","event_id":"evt_a1b2c3d4e5f6"}' \
  | openssl dgst -sha256 -hmac 'whsec_docs_example_secret_do_not_use'

Der Zeitstempel dieses Beispiels liegt in der Vergangenheit, ein echter Verifier würde ihn also wegen der Toleranz ablehnen. Testen Sie zuerst das HMAC mit deaktivierter Zeitprüfung und schalten Sie sie danach wieder ein.

Gesendete Header

Jede Webhook-Zustellung enthält diese Header:

HeaderBeispielBeschreibung
X-MLND-Signaturet=1708171200,v1=abc123...Zeitstempel und HMAC-SHA256-Signatur
X-MLND-Event-Idevt_a1b2c3d4e5f6...Eindeutige Ereignis-ID zur Deduplizierung
X-MLND-Eventmessage.deliveredEreignistyp
X-MLND-Event-Version1Payload-Schema-Version
X-MLND-Webhook-Idwh_abc123def456Öffentliche ID Ihres Webhooks
X-MLND-Attempt1Zustellversuchsnummer (beginnt bei 1)
X-MLND-Delivery-Idp8r2kx7m9wq4rtID dieses Versuchs, bei jeder Wiederholung neu; die publicId im Zustellverlauf. Nicht zur Deduplizierung: siehe Versionierung
X-MLND-Delivery-Timestamp1708171200Sendezeit dieses Versuchs (Unix-Sekunden); immer gleich t in der Signatur

Zwei weitere Header sind davon abhängig, welches Ereignis Sie abonniert haben:

HeaderBeispielGesendet wenn
X-MLND-SourceinboundNur bei inbound.received-Zustellungen vorhanden
X-MLND-Inbound-Email-Idinb_a1b2c3d4e5f6Nur bei inbound.received-Zustellungen vorhanden

X-MLND-Message-Id wird derzeit bei message.*-Zustellungen nicht gesendet. Verwenden Sie stattdessen das Feld data.message_id in der Ereignis-Payload.

Verifizierungsschritte

Zeitstempel und Signatur extrahieren

Parsen Sie den Header X-MLND-Signature. Teilen Sie bei ,, um die Komponenten t=... und v1=... zu erhalten.

Die signierte Payload vorbereiten

Verketten Sie den Zeitstempel, einen Punkt (.) und den rohen Request-Body:

signed_payload = timestamp + "." + raw_body

Die erwartete Signatur berechnen

Generieren Sie einen HMAC-SHA256-Hash, wobei Ihr Webhook-Secret als Schlüssel und die signierte Payload als Nachricht verwendet wird. Konvertieren Sie das Ergebnis in Kleinbuchstaben-Hexadezimal.

Signaturen vergleichen

Verwenden Sie einen Constant-Time-Vergleich, um zu prüfen, ob die berechnete Signatur mit dem v1-Wert aus dem Header übereinstimmt.

Zeitstempel validieren

Überprüfen Sie, ob der Zeitstempel innerhalb von 5 Minuten der aktuellen Zeit liegt, um Replay-Angriffe zu verhindern.

Codebeispiele

verify-signature.sh
#!/usr/bin/env bash
# Compute and verify an HMAC-SHA256 webhook signature using openssl

SIGNATURE_HEADER="t=1708171200,v1=b9f747ea899eafe911777060ca8f3c90828d37f1e16ee6e9ee98dacf8b316b67"
RAW_BODY='{"event":"message.delivered","event_id":"evt_a1b2c3d4e5f6"}'
SECRET="whsec_docs_example_secret_do_not_use"
TOLERANCE=300

# Parse timestamp and signature from header
TIMESTAMP=$(echo "$SIGNATURE_HEADER" | grep -oP '(?<=t=)[^,]+')
RECEIVED_SIG=$(echo "$SIGNATURE_HEADER" | grep -oP '(?<=v1=).+')

# Check timestamp tolerance
NOW=$(date +%s)
DIFF=$(( NOW - TIMESTAMP ))
if [ "${DIFF#-}" -gt "$TOLERANCE" ]; then
  echo "Error: Timestamp outside tolerance window" >&2
  exit 1
fi

# Compute expected signature
SIGNED_PAYLOAD="${TIMESTAMP}.${RAW_BODY}"
EXPECTED=$(printf '%s' "$SIGNED_PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')

# Compare signatures
if [ "$EXPECTED" = "$RECEIVED_SIG" ]; then
  echo "Signature valid"
else
  echo "Signature invalid" >&2
  exit 1
fi

Schutz vor Replay-Angriffen

Der Zeitstempel im Signatur-Header verhindert Replay-Angriffe. Wenn ein Angreifer eine gültige Webhook-Zustellung abfängt und später erneut sendet, wird die Zeitstempel-Prüfung sie ablehnen.

Die Standardtoleranz von MailLand beträgt 5 Minuten (300 Sekunden). Sie können dies in Ihrem Verifizierungscode anpassen, aber halten Sie es kurz genug, um bedeutsame Replay-Angriffe zu verhindern.

Zusätzlich hat jedes Ereignis eine eindeutige event_id (im Header X-MLND-Event-Id). Speichern Sie verarbeitete Ereignis-IDs und lehnen Sie Duplikate für zusätzlichen Schutz ab.

MailLand dedupliziert das Auslösen auf seiner Seite: Ein doppelt erzeugtes Ereignis wird pro Webhook innerhalb von 24 Stunden nur einmal versandt. Das ist nicht dasselbe wie „höchstens einmal zugestellt“: eine fehlgeschlagene Zustellung wird wiederholt, und jeder Versuch trägt dieselbe event_id und denselben Body; nur X-MLND-Attempt und der Signatur-Zeitstempel ändern sich. Deduplizieren Sie über event_id und rechnen Sie mit wiederholten Zustellungen.

Jede Zustellung ist signiert

Es gibt keinen unsignierten Zustellpfad. Hat ein Webhook kein brauchbares Signing-Secret, lässt MailLand die Zustellung fehlschlagen und protokolliert den Fehler, statt die Nutzdaten ohne X-MLND-Signature-Header zu senden: ein fehlender Signatur-Header ist also nie etwas, das Ihr Verifizierer tolerieren muss. Lehnen Sie jede Anfrage ab, die keinen trägt.

Secrets neu generieren

Wenn Sie vermuten, dass Ihr Webhook-Secret kompromittiert wurde:

  1. Rufen Sie POST /v1/webhooks/{id}/regenerate-secret auf, um ein neues Secret zu erhalten
  2. Aktualisieren Sie den Verifizierungscode Ihres Servers mit dem neuen Secret
  3. Das alte Secret wird sofort ungültig

Beim Neuerstellen von Secrets gibt es keine Übergangsfrist. Aktualisieren Sie Ihren Verifizierungscode vor der nächsten Webhook-Zustellung, um zu vermeiden, dass gültige Ereignisse abgelehnt werden.

Häufige Fehler

  • Den Body als JSON parsen, bevor man verifiziert: die Signatur wird über die rohen Bytes berechnet, nicht über ein re-serialisiertes JSON-Objekt. Erst verifizieren, dann parsen.
  • Stringvergleich statt Constant-Time-Vergleich verwenden: normaler Stringvergleich ist anfällig für Timing-Angriffe.
  • Den Zeitstempel ignorieren: ohne Zeitstempel-Validierung können aufgefangene Payloads unbegrenzt wiedergegeben werden.
  • Das Secret fest kodieren: verwenden Sie Umgebungsvariablen oder einen Secret Manager.

Nächste Schritte

Auf dieser Seite