Przejdź do głównej zawartości

Weryfikacja podpisów

BeepClock podpisuje każde żądanie webhooka sekretem podpisu webhooka. Zweryfikuj podpis, zanim zaufasz treści: potwierdza on, że żądanie pochodzi z BeepClock i że ciało nie zostało zmienione po drodze.

X-BeepClock-Signature: t=1791788292,v1=9f2b...a1,v1=4c8e...77
  • t to moment podpisania żądania jako znacznik czasu Unix w sekundach (UTC).
  • v1 to podpis w wersji 1. Podczas rotacji sekretu nagłówek może zawierać więcej niż jedną wartość v1.

Jak obliczany jest podpis​

Podpis to HMAC-SHA256 ze znacznika czasu, kropki i surowego ciała żądania:

signed_payload = "{t}" + "." + raw_body
signature = lowercase_hex(HMAC_SHA256(key = secret, message = signed_payload))
  • t to dokładnie ta sama wartość co w nagłówku.
  • raw_body to surowe ciało żądania: bajty odebrane z sieci, przed jakimkolwiek parsowaniem JSON.
  • secret to cały sekret podpisu razem z prefiksem whsec_, zakodowany w UTF-8. Nie usuwaj prefiksu i nie dekoduj sekretu z Base64.
  • Wynik jest zapisany szesnastkowo, małymi literami, i ma 64 znaki.

Każda próba jest podpisywana w chwili wysłania, dlatego t i podpis różnią się między próbami tego samego dostarczenia, a treść pozostaje taka sama.

Używaj surowego ciała żądania

Podpis obliczony na payloadzie po ponownej serializacji, np. po JSON.parse i JSON.stringify, nigdy nie będzie zgodny. Odczytaj ciało jako bajty, zweryfikuj podpis i dopiero potem parsuj JSON.

Kroki weryfikacji​

  1. Odczytaj t oraz wszystkie wartości v1 z nagłówka.
  2. Oblicz HMAC-SHA256 z {t}.{raw_body} dla każdego sekretu, który aktualnie akceptujesz.
  3. Porównaj wynik z każdą wartością v1, używając porównania w czasie stałym. Zgodność z dowolną wartością v1 oznacza poprawny podpis.
  4. Odrzucaj żądania bez poprawnego podpisu, np. kodem 401.
  5. Odrzucaj żądania, w których t różni się od bieżącego czasu o więcej niż przyjęta tolerancja, np. 5 minut. Chroni to przed ponownym odtworzeniem przechwyconych żądań. BeepClock sam nie narzuca tolerancji, więc dbaj o synchronizację zegara serwera (NTP).

Przykłady kodu​

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 zachowuje ciało jako Buffer. Nie używaj express.json() na tej ścieżce: podpis musi być obliczany na oryginalnych bajtach. enqueue oznacza Twój kod, który zapisuje zdarzenie, pomija już przetworzone identyfikatory dostarczeń i obsługuje zdarzenie w tle.

Wartości testowe​

Użyj tych wartości, aby sprawdzić swoją implementację. Na czas tego sprawdzenia wyłącz tolerancję znacznika czasu, ponieważ t ma stałą wartość.

DaneWartość
Sekretwhsec_test-secret
t1791788292
Surowe ciało{"id":"0b9e6c41-5d2a-4f78-93e1-c6a8b2f4d071","event":"webhook.test"}
Oczekiwane v181e8ba359713e992413513ead157645df8e568201531f6ef5b3c1764fc5658bc

Najszybszym sprawdzeniem całej ścieżki jest Wyślij zdarzenie testowe na stronie webhooka: poprawnie działający odbiorca odpowiada kodem 2xx, a błąd podpisu pojawi się jako Twój kod 401.

Rotacja sekretu​

Wygeneruj nowy sekret podpisu tworzy nowy sekret i wyświetla go jednorazowo. Przez kolejne 24 godziny każde żądanie jest podpisywane oboma sekretami, więc nagłówek zawiera dwie wartości v1: pierwszą dla nowego sekretu, drugą dla poprzedniego. Po 24 godzinach używany jest tylko nowy sekret.

Aby przeprowadzić rotację bez przerwy w działaniu:

  1. Wybierz Wygeneruj nowy sekret podpisu i skopiuj nowy sekret.
  2. Dodaj go w odbiorcy obok starego. Powyższe przykłady kodu akceptują listę sekretów.
  3. Usuń stary sekret, gdy nie jest już potrzebny, najpóźniej po 24 godzinach.
wskazówka

Generuj nowy sekret zawsze, gdy stary mógł zostać ujawniony, np. po wklejeniu go do zgłoszenia lub na czat albo gdy z zespołu odchodzi osoba, która go znała.