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.
Der Signatur-Header
X-BeepClock-Signature: t=1791788292,v1=9f2b...a1,v1=4c8e...77
tist der Zeitpunkt der Signierung als Unix-Zeitstempel in Sekunden (UTC).v1ist eine Signatur nach Version 1. Während einer Rotation des Geheimnisses kann der Header mehr als einenv1-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))
tist genau der Wert aus dem Header.raw_bodyist der unveränderte Anfragetext: die aus dem Netz empfangenen Bytes vor jedem JSON-Parsing.secretist das gesamte Signaturgeheimnis einschließlich des Präfixeswhsec_, 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.
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
- Lesen Sie
tund allev1-Werte aus dem Header. - Berechnen Sie HMAC-SHA256 über
{t}.{raw_body}mit jedem Geheimnis, das Sie derzeit akzeptieren. - Vergleichen Sie das Ergebnis mit jedem
v1-Wert in konstanter Zeit. Eine Übereinstimmung mit einem beliebigenv1-Wert bedeutet, dass die Signatur gültig ist. - Lehnen Sie Anfragen ohne gültige Signatur ab, zum Beispiel mit
401. - Lehnen Sie Anfragen ab, deren
tum 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
- Node.js (Express)
- C# (ASP.NET Core)
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.
using System.Security.Cryptography;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var secrets = new[] { builder.Configuration["BeepClock:WebhookSecret"]! };
app.MapPost("/beepclock/webhooks", async (HttpRequest request) =>
{
using var buffer = new MemoryStream();
await request.Body.CopyToAsync(buffer);
var body = buffer.ToArray();
if (!BeepClockSignature.IsValid(request.Headers["X-BeepClock-Signature"].ToString(), body, secrets,
DateTimeOffset.UtcNow))
return Results.Unauthorized();
var deliveryId = request.Headers["X-BeepClock-Delivery"].ToString();
var payload = System.Text.Json.JsonDocument.Parse(body);
// Store the event, skip delivery IDs that were already processed and handle it in the background.
return Results.NoContent();
});
app.Run();
static class BeepClockSignature
{
private static readonly TimeSpan Tolerance = TimeSpan.FromMinutes(5);
public static bool IsValid(string header, byte[] body, IEnumerable<string> secrets, DateTimeOffset now)
{
long? timestamp = null;
var signatures = new List<byte[]>();
foreach (var part in header.Split(',', StringSplitOptions.TrimEntries))
{
var index = part.IndexOf('=');
if (index <= 0) continue;
var key = part[..index];
var value = part[(index + 1)..];
if (key == "t" && long.TryParse(value, out var t))
timestamp = t;
else if (key == "v1" && value.Length == 64 && value.All(Uri.IsHexDigit))
signatures.Add(Convert.FromHexString(value));
}
if (timestamp is not { } seconds || signatures.Count == 0) return false;
if ((now - DateTimeOffset.FromUnixTimeSeconds(seconds)).Duration() > Tolerance) return false;
var signedPayload = Encoding.UTF8.GetBytes($"{seconds}.").Concat(body).ToArray();
return secrets.Any(secret =>
{
var expected = HMACSHA256.HashData(Encoding.UTF8.GetBytes(secret), signedPayload);
return signatures.Any(signature => CryptographicOperations.FixedTimeEquals(signature, expected));
});
}
}
Testwerte
Prüfen Sie Ihre Implementierung mit diesen Werten. Deaktivieren Sie für diese Prüfung die Zeitstempeltoleranz, da t ein fester Zeitpunkt ist.
| Eingabe | Wert |
|---|---|
| Geheimnis | whsec_test-secret |
t | 1791788292 |
| Unveränderter Anfragetext | {"id":"0b9e6c41-5d2a-4f78-93e1-c6a8b2f4d071","event":"webhook.test"} |
Erwartetes v1 | 81e8ba359713e992413513ead157645df8e568201531f6ef5b3c1764fc5658bc |
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:
- Wählen Sie Neues Signaturgeheimnis erzeugen und kopieren Sie das neue Geheimnis.
- Hinterlegen Sie es in Ihrem Empfänger neben dem alten. Die Codebeispiele oben akzeptieren eine Liste von Geheimnissen.
- Entfernen Sie das alte Geheimnis, sobald es nicht mehr benötigt wird, spätestens nach 24 Stunden.
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.