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.
L’en-tête Service-Signature
Section intitulée « L’en-tête Service-Signature »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| Champ | Signification |
|---|---|
t |
L’horodatage Unix (en secondes) du moment où la signature a été générée. |
v1 |
La signature : un HMAC-SHA256 en hexadécimal. |
Comment la signature est calculée
Section intitulée « Comment la signature est calculée »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.
- Extrayez
tetv1de l’en-têteService-Signature. - Rejetez la requête si
ts’écarte de plus de 5 minutes de l’heure actuelle (protection contre le rejeu). - Calculez
HMAC-SHA256("{t}.{raw_body}", secret). - Comparez-le à
v1en utilisant une comparaison à temps constant.
Node.js (Express)
Section intitulée « Node.js (Express) »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"); },);Python (Flask)
Section intitulée « Python (Flask) »import hashlibimport hmacimport osimport 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 "", 200Rotation du secret
Section intitulée « Rotation du secret »Renouveler le secret d’un point de terminaison le remplace immédiatement. Il n’y a pas de fenêtre de chevauchement : dès la rotation, chaque livraison est signée avec le seul nouveau secret, et une signature calculée avec l’ancien cesse de correspondre.
L’en-tête ne porte donc qu’un seul champ v1=, et les analyseurs ci-dessus sont
complets tels quels : vous n’avez jamais à accepter une seconde signature. Une
nouvelle tentative est signée au moment de son envoi et non à la construction de
l’événement, si bien qu’une tentative postérieure à une rotation porte elle aussi
le nouveau secret.
Effectuez la rotation lorsque votre consommateur peut prendre le nouveau secret sans délai : révélez-le dans le back-office, déployez-le, et attendez-vous à ce que les livraisons faites pendant la bascule échouent à la vérification de signature et reviennent selon le calendrier de nouvelles tentatives.