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 »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é.