Przejdź do treści

Weryfikacja podpisów

Jak jest podpisywane

Podpisywana treść to nagłówek webhook-id, nagłówek webhook-timestamp i surowe ciało żądania połączone kropkami: <id>.<timestamp>.<body>. Kluczem jest Twój sekret bez prefiksu whsec_, zdekodowany z base64. webhook-signature zawiera v1, a po nim base64 z HMAC-SHA256 tej treści.

Gdy zrotowany sekret jest w okresie karencji, webhook-signature zawiera dwa podpisy rozdzielone spacją, po jednym na sekret; zaakceptuj dostawę, gdy pasuje którykolwiek.

  • Weryfikuj surowe ciało dokładnie tak, jak zostało odebrane, przed parsowaniem JSON: ponowna serializacja zmienia bajty.
  • Odrzucaj webhook-timestamp oddalony od Twojego zegara o więcej niż 5 min, aby przechwyconego żądania nie dało się odtworzyć później.
  • Porównuj podpisy w stałym czasie.
  • Odpowiadaj szybko kodem 2xx, a wolną pracę wykonuj potem: czekamy 10 s.

Z biblioteką Standard Webhooks

Pakiety standardwebhooks w npm i PyPI (oraz biblioteki dla większości innych języków) sprawdzają podpis i znacznik czasu jednym wywołaniem i zgłaszają błąd, gdy któryś jest nieprawidłowy.

przykładweryfikacja
// npm install standardwebhooksimport { Webhook } from "standardwebhooks";const wh = new Webhook(process.env.DOTWICK_WEBHOOK_SECRET); // whsec_…// Express: app.post("/dotwick", express.raw({ type: "application/json" }), handler)function handler(req, res) {  let event;  try {    event = wh.verify(req.body.toString("utf8"), {      "webhook-id": req.header("webhook-id"),      "webhook-timestamp": req.header("webhook-timestamp"),      "webhook-signature": req.header("webhook-signature"),    });  } catch {    return res.status(400).end();  }  res.status(204).end(); // answer first, then work  queue.add(event.type, event);}

Ręcznie

Bez zależności: cała weryfikacja to kilka linii biblioteki standardowej.

przykładweryfikacja
import { createHmac, timingSafeEqual } from "node:crypto";function verifyDotwick(secret, headers, rawBody) {  const id = headers["webhook-id"];  const timestamp = headers["webhook-timestamp"];  const signatures = headers["webhook-signature"] ?? "";  if (!id || !timestamp) return false;  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");  const expected = createHmac("sha256", key).update(`${id}.${timestamp}.${rawBody}`).digest();  return signatures.split(" ").some((entry) => {    const [version, signature] = entry.split(",");    const received = Buffer.from(signature ?? "", "base64");    return (      version === "v1" &&      received.length === expected.length &&      timingSafeEqual(received, expected)    );  });}