Aller au contenu

Vérifier les signatures

Chaque livraison de webhook est signée afin que vous puissiez confirmer qu’elle provient bien de Service et qu’elle n’a pas été altérée ni rejouée. Vérifiez toujours la signature avant d’agir sur une charge utile.

Chaque requête porte un en-tête Service-Signature comportant deux champs séparés par une virgule :

Service-Signature: t=1719515400,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
ChampSignification
tL’horodatage Unix (en secondes) du moment où la signature a été générée.
v1La signature : un HMAC-SHA256 en hexadécimal.

La signature v1 est le HMAC-SHA256 de la chaîne "{t}.{body}" — l’horodatage, un point littéral (.), puis le corps brut de la requête — avec pour clé le secret de signature de votre point de terminaison (whsec_…) :

v1 = HMAC_SHA256(secret = whsec_…, message = "{t}.{raw_request_body}")

Pour vérifier, recalculez ce HMAC et comparez-le à v1 en temps constant.

  1. Extrayez t et v1 de l’en-tête Service-Signature.
  2. Rejetez la requête si t s’écarte de plus de 5 minutes de l’heure actuelle (protection contre le rejeu).
  3. Calculez HMAC-SHA256("{t}.{raw_body}", secret).
  4. Comparez-le à v1 en utilisant une comparaison à temps constant.
import crypto from "node:crypto";
import express from "express";
const TOLERANCE_SECONDS = 5 * 60;
function verifyServiceSignature(rawBody, header, secret) {
// header looks like: "t=1719515400,v1=abc123..."
const parts = Object.fromEntries(
header.split(",").map((kv) => kv.split("=")),
);
const timestamp = parts.t;
const signature = parts.v1;
if (!timestamp || !signature) return false;
// 1. Replay protection: reject stale timestamps.
const ageSeconds = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
if (ageSeconds > TOLERANCE_SECONDS) return false;
// 2. Recompute the HMAC over "{t}.{rawBody}".
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`, "utf8")
.digest("hex");
// 3. Constant-time compare.
const a = Buffer.from(expected);
const b = Buffer.from(signature);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
const app = express();
app.post(
"/webhooks/service",
express.raw({ type: "application/json" }), // keep the RAW body as a Buffer
(req, res) => {
const rawBody = req.body.toString("utf8");
const ok = verifyServiceSignature(
rawBody,
req.get("Service-Signature") ?? "",
process.env.SERVICE_WEBHOOK_SECRET, // your whsec_... value
);
if (!ok) return res.status(400).send("invalid signature");
const event = JSON.parse(rawBody);
// Deduplicate on event.id, then process asynchronously.
res.status(200).send("ok");
},
);
import hashlib
import hmac
import os
import time
from flask import Flask, abort, request
TOLERANCE_SECONDS = 5 * 60
def verify_service_signature(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
timestamp, signature = parts.get("t"), parts.get("v1")
if not timestamp or not signature:
return False
# 1. Replay protection.
if abs(int(time.time()) - int(timestamp)) > TOLERANCE_SECONDS:
return False
# 2. Recompute the HMAC over "{t}.{raw_body}" (raw bytes).
signed_payload = f"{timestamp}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed_payload, hashlib.sha256).hexdigest()
# 3. Constant-time compare.
return hmac.compare_digest(expected, signature)
app = Flask(__name__)
@app.post("/webhooks/service")
def handle_webhook():
raw_body = request.get_data() # raw bytes, before parsing
if not verify_service_signature(
raw_body,
request.headers.get("Service-Signature", ""),
os.environ["SERVICE_WEBHOOK_SECRET"], # your whsec_... value
):
abort(400)
event = request.get_json()
# Deduplicate on event["id"], then process asynchronously.
return "", 200

Les secrets de point de terminaison peuvent être renouvelés avec une fenêtre de chevauchement, durant laquelle les livraisons peuvent être signées avec l’ancien ou le nouveau secret. Lorsque vous effectuez une rotation, acceptez une signature qui correspond à l’un ou l’autre secret jusqu’à ce que l’ancien soit retiré.