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

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

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.