Wie prüfe ich die Webhook-Signatur?
Kurzantwort
Abschnitt betitelt „Kurzantwort“Jede Zustellung trägt den Header X-Webhook-Signature: sha256=<hexdigest>. Der Wert
ist ein HMAC-SHA256 über den rohen Request-Body, geschlüsselt mit dem
Signatur-Secret deines Webhooks. Zum Prüfen berechnest du denselben HMAC auf deiner
Seite und vergleichst zeitkonstant. Stimmen die Werte nicht überein: Anfrage
verwerfen. Sie stammt nicht (unverändert) von Notory.
Voraussetzungen
Abschnitt betitelt „Voraussetzungen“Wichtig: Der HMAC wird über die unveränderten Body-Bytes gebildet. Verifiziere also vor dem JSON-Parsen bzw. auf dem Roh-Body: Ein re-serialisiertes JSON hat fast nie dieselben Bytes.
Verifikation implementieren
Abschnitt betitelt „Verifikation implementieren“-
Secret hinterlegen. Lege das beim Erstellen angezeigte Secret im Secret-Store deines Empfänger-Systems ab (Umgebungsvariable, Vault, nicht im Code).
-
Prüf-Logik einbauen. Implementiere im Endpunkt: rohen Body lesen → HMAC-SHA256 mit dem Secret berechnen → mit dem Header
X-Webhook-Signature(nach dem Präfixsha256=) zeitkonstant vergleichen → bei Abweichung mit401ablehnen. -
Mit „Test senden” verifizieren. Löse in Notory eine Testzustellung aus (
ping-Ereignis). Erst wenn dein Endpunkt die Signatur akzeptiert und 2xx liefert, nimmst du produktive Ereignisse an.
Referenz-Implementierungen:
import hashlib, hmac, os
SECRET = os.environ["NOTORY_WEBHOOK_SECRET"].encode()
@app.post("/hooks/notory")def notory_hook(): raw = request.get_data() # roher Body (Bytes!) header = request.headers.get("X-Webhook-Signature", "") expected = "sha256=" + hmac.new(SECRET, raw, hashlib.sha256).hexdigest() if not hmac.compare_digest(header, expected): # zeitkonstant vergleichen return "invalid signature", 401 payload = json.loads(raw) … return "", 204import crypto from "node:crypto";import express from "express";
const app = express();const SECRET = process.env.NOTORY_WEBHOOK_SECRET;
// Wichtig: Roh-Body sichern, bevor JSON geparst wird.app.post("/hooks/notory", express.raw({ type: "application/json" }), (req, res) => { const expected = "sha256=" + crypto.createHmac("sha256", SECRET).update(req.body).digest("hex"); const given = req.get("X-Webhook-Signature") ?? ""; const ok = given.length === expected.length && crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected)); if (!ok) return res.status(401).send("invalid signature");
const payload = JSON.parse(req.body); // … res.sendStatus(204);});Zum manuellen Nachrechnen (z. B. bei der Fehlersuche):
printf '%s' '{"event": "ping", "data": {"message": "test delivery"}}' \ | openssl dgst -sha256 -hmac "dein_signatur_secret"# → muss dem Header-Wert nach "sha256=" entsprechenWas passiert dabei?
Abschnitt betitelt „Was passiert dabei?“- Signatur-Bildung in Notory. Vor dem Versand serialisiert Notory den Umschlag
{"event": …, "data": …}zu JSON und berechnetHMAC_SHA256(secret, body)als Hex-Digest. Der Header lautet exaktX-Webhook-Signature: sha256=<hexdigest>; zusätzlich nenntX-Webhook-Eventden Ereignisnamen. - Was die Prüfung garantiert: Authentizität (nur wer das Secret kennt, kann gültig signieren) und Integrität (jede Änderung am Body invalidiert die Signatur). Sie ersetzt kein TLS: Nutze HTTPS-Endpunkte, damit auch Vertraulichkeit gilt.
- Kein Zeitstempel im Umschlag. Der Payload enthält keinen signierten Zeitstempel oder
eine Zustell-ID. Einen strengen Replay-Schutz musst du bei Bedarf fachlich lösen
(z. B. Idempotenz über die
iddes Objekts indata). - Zeitkonstant vergleichen. Nutze
hmac.compare_digest/crypto.timingSafeEqualstatt==, um Timing-Angriffe auf den Vergleich auszuschließen. - Secret-Rotation. Es gibt keinen Endpunkt zum Neuausstellen des Secrets: Webhook neu anlegen, Empfänger auf das neue Secret umstellen, alten Webhook löschen.