DocsWebhooksSignature et vérification HMAC

Signature et vérification HMAC

Vérifiez l'authenticité de chaque webhook Coffrify avec HMAC-SHA256, au format Standard Webhooks ou au format Coffrify historique, avec exemples Node et Python.

Webhooks4 min de lectureMis à jour le 10 juin 2026
Télécharger en PDF

Chaque webhook que Coffrify envoie à votre URL est signé. La signature vous permet de prouver deux choses avant de traiter l'événement : qu'il provient bien de Coffrify (et pas d'un tiers qui aurait deviné votre URL) et qu'il n'a pas été modifié en transit. Coffrify joint deux signatures en parallèle sur la même requête : le format Standard Webhooks, compatible avec les bibliothèques de vérification du marché, et le format Coffrify historique (legacy), conservé pour les intégrations existantes. Les deux reposent sur le même algorithme, HMAC-SHA256, et sur le secret de signature de votre endpoint. Vous n'êtes pas obligé de vérifier les deux : choisissez celui qui correspond à votre bibliothèque ou à votre code, et vérifiez-le systématiquement avant d'agir sur la charge utile.

Le secret de signature

Quand vous créez un endpoint via POST /v1/webhooks, la réponse contient un champ secret au format whsec_ suivi d'une chaîne hexadécimale. Ce secret n'est affiché qu'une seule fois : copiez-le immédiatement et stockez-le comme un identifiant sensible (variable d'environnement, coffre à secrets). Il sert de clé pour calculer et vérifier les signatures HMAC. Si vous le perdez, faites-le tourner depuis votre espace plutôt que de recréer l'endpoint.

Les en-têtes envoyés sur chaque livraison

Coffrify ajoute à chaque requête POST les en-têtes des deux formats. Vous trouverez donc côte à côte les en-têtes Standard Webhooks et les en-têtes Coffrify historiques :

En-têteFormatContenu
webhook-idStandard WebhooksIdentifiant unique de la livraison. Sert aussi à dédupliquer (idempotence côté réception).
webhook-timestampStandard WebhooksHorodatage Unix en secondes au moment de la signature.
webhook-signatureStandard WebhooksSignature(s) au format v1,<base64>. Plusieurs signatures peuvent être séparées par une espace.
X-Coffrify-SignatureCoffrify historiqueHorodatage et signature au format t=<ts>,v1=<hex>.
X-Coffrify-Event-IdCoffrify historiqueIdentifiant de l'événement.
X-Coffrify-Event-TypeCoffrify historiqueType de l'événement, par exemple transfer.downloaded.

Format 1 : Standard Webhooks

Le format Standard Webhooks est une convention ouverte, prise en charge par de nombreuses bibliothèques de réception. La chaîne signée est la concaténation de l'identifiant de livraison, de l'horodatage et du corps brut de la requête, séparés par des points, dans cet ordre : webhook-id.webhook-timestamp.body. On calcule le HMAC-SHA256 de cette chaîne avec votre secret, on encode le résultat en base64, et on le préfixe par v1,. Pour vérifier, recalculez cette même valeur et comparez-la, en temps constant, à celle de l'en-tête webhook-signature.

chaîne signée = webhook-id + "." + webhook-timestamp + "." + corps_brut
signature = "v1," + base64( HMAC_SHA256( secret, chaîne signée ) )

Format 2 : Coffrify historique

Le format historique est transmis dans l'en-tête X-Coffrify-Signature. Sa valeur ressemble à t=1717000000,v1=ab12.... La partie t= porte l'horodatage Unix en secondes ; la partie v1= porte la signature en hexadécimal. Ici la chaîne signée est l'horodatage suivi d'un point puis du corps brut : timestamp.body. On calcule le HMAC-SHA256 avec votre secret et on encode le résultat en hexadécimal. L'horodatage est donc lu directement dans l'en-tête, contrairement au format Standard Webhooks où il arrive dans un en-tête séparé.

chaîne signée = timestamp + "." + corps_brut
signature = hex( HMAC_SHA256( secret, chaîne signée ) )
en-tête = "t=" + timestamp + ",v1=" + signature

Tolérance d'horodatage et anti-rejeu

Comparer la signature ne suffit pas : un attaquant qui aurait capté une livraison valide pourrait la renvoyer plus tard. Pour s'en prémunir, vérifiez aussi que l'horodatage est récent. Coffrify recommande une tolérance de 5 minutes : rejetez toute requête dont l'horodatage (celui de webhook-timestamp ou celui de t= selon le format) s'écarte de plus de 300 secondes de l'heure courante de votre serveur. Pour une protection complète contre le rejeu, mémorisez le webhook-id des livraisons déjà traitées et ignorez les doublons : Coffrify réutilise le même identifiant lors des nouvelles tentatives d'une même livraison.

Vérifier en Node.js

L'exemple suivant gère les deux formats à partir du module natif crypto, sans dépendance. Il décode la clé depuis le secret whsec_, reconstruit la chaîne signée, applique l'encodage attendu (base64 ou hexadécimal) puis compare en temps constant. Adaptez la récupération du corps brut à votre framework (par exemple express.raw plutôt que express.json).

import crypto from "node:crypto";
 
function secretToKey(secret) {
// Retirer le préfixe whsec_ et décoder l'hexadécimal en octets bruts.
return Buffer.from(secret.replace(/^whsec_/, ""), "hex");
}
 
export function verifyStandard(secret, rawBody, headers, toleranceSec = 300) {
const id = headers["webhook-id"];
const ts = headers["webhook-timestamp"];
const sigHeader = headers["webhook-signature"]; // "v1,<base64> v1,<base64>"
if (!id || !ts || !sigHeader) return false;
 
// 1. Fenêtre anti-rejeu de 5 minutes.
if (Math.abs(Math.floor(Date.now() / 1000) - Number(ts)) > toleranceSec) return false;
 
// 2. Recalculer la signature attendue.
const signed = `${id}.${ts}.${rawBody}`;
const expected = crypto
.createHmac("sha256", secretToKey(secret))
.update(signed)
.digest(); // octets bruts
 
// 3. Comparer en temps constant à chaque signature de l'en-tête.
return sigHeader.split(" ").some((part) => {
const [, b64] = part.split(",");
if (!b64) return false;
const provided = Buffer.from(b64, "base64");
return provided.length === expected.length &&
crypto.timingSafeEqual(provided, expected);
});
}

Vérifier en Python

Même logique en Python avec les modules hmac, hashlib et base64 de la bibliothèque standard. Pensez à passer le corps brut en octets (request.get_data() avec Flask, await request.body() avec FastAPI) et non un dictionnaire déjà désérialisé.

import hmac, hashlib, base64, time
 
def _secret_to_key(secret: str) -> bytes:
# Retirer whsec_ et décoder l'hexadécimal en octets bruts.
return bytes.fromhex(secret.removeprefix("whsec_"))
 
def verify_standard(secret: str, raw_body: bytes, headers: dict, tolerance: int = 300) -> bool:
wid = headers.get("webhook-id")
ts = headers.get("webhook-timestamp")
sig_header = headers.get("webhook-signature") # "v1,<base64> v1,<base64>"
if not (wid and ts and sig_header):
return False
 
# 1. Fenêtre anti-rejeu de 5 minutes.
if abs(int(time.time()) - int(ts)) > tolerance:
return False
 
# 2. Recalculer la signature attendue (encodage base64).
signed = f"{wid}.{ts}.".encode() + raw_body
expected = base64.b64encode(
hmac.new(_secret_to_key(secret), signed, hashlib.sha256).digest()
).decode()
 
# 3. Comparer en temps constant à chaque signature de l'en-tête.
for part in sig_header.split(" "):
_, _, provided = part.partition(",")
if provided and hmac.compare_digest(provided, expected):
return True
return False

Tester votre vérification

Une fois votre endpoint en place, déclenchez une livraison de test signée avec POST /v1/webhooks/{id}/test. Vous recevrez une charge utile réelle, signée dans les deux formats, ce qui vous permet de valider votre code de vérification de bout en bout sans attendre un vrai événement.

POST/v1/webhooks/{id}/testEnvoie un événement de test signé vers l'URL de l'endpoint, avec les en-têtes Standard Webhooks et Coffrify historique.
$ curl -X POST https://api.coffrify.com/v1/webhooks/wh_123/test \
-H "Authorization: Bearer cof_live_…"

Cette page vous a-t-elle aidé ?
Anonyme, dédupliqué 24h par signature locale.