Zum Hauptinhalt springen

Signaturen prüfen

BeepClock signiert jede Webhook-Anfrage mit dem Signaturgeheimnis des Webhooks. Prüfen Sie die Signatur, bevor Sie dem Payload vertrauen: Sie belegt, dass die Anfrage von BeepClock stammt und der Anfragetext unterwegs nicht verändert wurde.

X-BeepClock-Signature: t=1791788292,v1=9f2b...a1,v1=4c8e...77
  • t ist der Zeitpunkt der Signierung als Unix-Zeitstempel in Sekunden (UTC).
  • v1 ist eine Signatur nach Version 1. Während einer Rotation des Geheimnisses kann der Header mehr als einen v1-Wert enthalten.

Berechnung der Signatur​

Die Signatur ist ein HMAC-SHA256 über den Zeitstempel, einen Punkt und den unveränderten Anfragetext:

signed_payload = "{t}" + "." + raw_body
signature = lowercase_hex(HMAC_SHA256(key = secret, message = signed_payload))
  • t ist genau der Wert aus dem Header.
  • raw_body ist der unveränderte Anfragetext: die aus dem Netz empfangenen Bytes vor jedem JSON-Parsing.
  • secret ist das gesamte Signaturgeheimnis einschließlich des Präfixes whsec_, kodiert als UTF-8. Entfernen Sie das Präfix nicht und dekodieren Sie das Geheimnis nicht aus Base64.
  • Das Ergebnis ist eine Hexadezimalzeichenkette in Kleinbuchstaben mit 64 Zeichen.

Jeder Versuch wird beim Senden signiert. Daher unterscheiden sich t und die Signatur zwischen den Versuchen derselben Zustellung, während der Anfragetext gleich bleibt.

Unveränderten Anfragetext verwenden

Eine Signatur, die über einen erneut serialisierten Payload berechnet wird, zum Beispiel nach JSON.parse und anschließendem JSON.stringify, stimmt nie überein. Lesen Sie den Anfragetext als Bytes, prüfen Sie die Signatur und parsen Sie erst danach das JSON.

Prüfschritte​

  1. Lesen Sie t und alle v1-Werte aus dem Header.
  2. Berechnen Sie HMAC-SHA256 über {t}.{raw_body} mit jedem Geheimnis, das Sie derzeit akzeptieren.
  3. Vergleichen Sie das Ergebnis mit jedem v1-Wert in konstanter Zeit. Eine Übereinstimmung mit einem beliebigen v1-Wert bedeutet, dass die Signatur gültig ist.
  4. Lehnen Sie Anfragen ohne gültige Signatur ab, zum Beispiel mit 401.
  5. Lehnen Sie Anfragen ab, deren t um mehr als eine Toleranz von Ihrer aktuellen Zeit abweicht, zum Beispiel um 5 Minuten. Das schützt vor Replay-Angriffen. BeepClock selbst erzwingt keine Toleranz; halten Sie daher die Uhr Ihres Servers synchron (NTP).

Codebeispiele​

const crypto = require("node:crypto");
const express = require("express");

const secrets = [process.env.BEEPCLOCK_WEBHOOK_SECRET];
const toleranceSeconds = 300;

function verifySignature(header, rawBody, secrets) {
if (!header) return false;

let timestamp = null;
const signatures = [];
for (const part of header.split(",")) {
const index = part.indexOf("=");
const key = part.slice(0, index).trim();
const value = part.slice(index + 1).trim();
if (key === "t") timestamp = value;
if (key === "v1") signatures.push(Buffer.from(value, "hex"));
}
if (!/^\d+$/.test(timestamp ?? "") || signatures.length === 0) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceSeconds) return false;

const signedPayload = Buffer.concat([Buffer.from(`${timestamp}.`, "utf8"), rawBody]);
return secrets.some((secret) => {
const expected = crypto.createHmac("sha256", secret).update(signedPayload).digest();
return signatures.some(
(signature) => signature.length === expected.length && crypto.timingSafeEqual(signature, expected),
);
});
}

const app = express();

app.post("/beepclock/webhooks", express.raw({ type: "application/json" }), (req, res) => {
if (!verifySignature(req.get("X-BeepClock-Signature"), req.body, secrets)) {
return res.sendStatus(401);
}

const deliveryId = req.get("X-BeepClock-Delivery");
const event = JSON.parse(req.body.toString("utf8"));
enqueue(deliveryId, event);
res.sendStatus(204);
});

app.listen(3000);

express.raw behält den Anfragetext als Buffer. Verwenden Sie auf dieser Route nicht express.json(): Die Signatur muss über die ursprünglichen Bytes berechnet werden. enqueue steht für Ihren eigenen Code, der das Ereignis speichert, bereits verarbeitete Zustell-IDs überspringt und das Ereignis im Hintergrund verarbeitet.

Testwerte​

Prüfen Sie Ihre Implementierung mit diesen Werten. Deaktivieren Sie für diese Prüfung die Zeitstempeltoleranz, da t ein fester Zeitpunkt ist.

EingabeWert
Geheimniswhsec_test-secret
t1791788292
Unveränderter Anfragetext{"id":"0b9e6c41-5d2a-4f78-93e1-c6a8b2f4d071","event":"webhook.test"}
Erwartetes v181e8ba359713e992413513ead157645df8e568201531f6ef5b3c1764fc5658bc

Die schnellste End-to-End-Prüfung ist Testereignis senden auf der Seite des Webhooks: Ein korrekt implementierter Empfänger antwortet mit 2xx, ein Signaturfehler zeigt sich als Ihre 401-Antwort.

Rotation des Geheimnisses​

Neues Signaturgeheimnis erzeugen erstellt ein neues Geheimnis und zeigt es einmalig an. In den nächsten 24 Stunden wird jede Anfrage mit beiden Geheimnissen signiert. Der Header enthält dann zwei v1-Werte: den ersten für das neue, den zweiten für das bisherige Geheimnis. Nach 24 Stunden wird nur noch das neue Geheimnis verwendet.

So rotieren Sie das Geheimnis ohne Unterbrechung:

  1. Wählen Sie Neues Signaturgeheimnis erzeugen und kopieren Sie das neue Geheimnis.
  2. Hinterlegen Sie es in Ihrem Empfänger neben dem alten. Die Codebeispiele oben akzeptieren eine Liste von Geheimnissen.
  3. Entfernen Sie das alte Geheimnis, sobald es nicht mehr benötigt wird, spätestens nach 24 Stunden.
tipp

Rotieren Sie das Geheimnis immer dann, wenn es offengelegt worden sein könnte, zum Beispiel nachdem es in ein Ticket oder einen Chat eingefügt wurde oder wenn jemand, der es kannte, das Team verlässt.