À chaque livraison, Coffrify signe le corps de la requête avec votre secret webhook (préfixé whsec_) à l'aide de HMAC-SHA256. Votre endpoint doit vérifier cette signature avant de traiter l'événement : cela garantit que la requête provient bien de Coffrify et n'a pas été altérée en transit. Coffrify émet deux formats d'en-tête en parallèle : le format legacy (X-Coffrify-Signature) pour la compatibilité ascendante, et le format Standard Webhooks (webhook-signature) pour l'interopérabilité avec des outils comme Svix, Hookdeck ou NestJS. La signature expire après 300 secondes (5 minutes) pour protéger contre les attaques par rejeu.
En-têtes envoyés par Coffrify
| En-tête | Format | Description |
|---|---|---|
X-Coffrify-Signature | t=<ts>,v1=<hex64> | Format legacy. ts = horodatage Unix (secondes), hex64 = HMAC-SHA256 sur ${ts}.${body} encodé en hexadécimal. |
webhook-id | <uuid> | Identifiant stable de la livraison, identique pour chaque tentative (utile pour la déduplication). |
webhook-timestamp | <ts> | Horodatage Unix en secondes (Standard Webhooks). |
webhook-signature | v1,<base64> | Format Standard Webhooks. Signé sur ${webhook-id}.${ts}.${body}, encodé en base64. Plusieurs signatures séparées par des espaces si rotation en cours. |
X-Coffrify-Event-Id | <uuid> | Identique à webhook-id. Conservé pour compatibilité. |
X-Coffrify-Event-Type | <string> | Type d'événement, ex. transfer.downloaded. |
Format du secret
Le secret est généré automatiquement à la création du webhook et retourné une seule fois dans la réponse POST /v1/webhooks. Il a la forme whsec_ suivi de 64 caractères hexadécimaux représentant 32 octets aléatoires (256 bits d'entropie). Lorsque le préfixe whsec_ est présent, le moteur de signature Coffrify décode la partie hexadécimale comme les octets bruts de la clé HMAC, conformément à la spécification Standard Webhooks. Pour la rotation, utilisez PATCH /v1/webhooks/:id : pendant la transition, les deux secrets sont acceptés et matched_secret_index vous indique lequel a matché.
Algorithme de vérification (format legacy)
Pour le format X-Coffrify-Signature: t=<ts>,v1=<hex>, voici les étapes exactes implémentées dans lib/api/webhook-signature.ts et supabase/functions/_shared/coffrify-webhook-signing.ts :
1. Extraire ts et v1 depuis l'en-tête.
2. Vérifier que |now() - ts| <= 300 secondes (tolérance anti-rejeu).
3. Construire le message canonique : ts + "." + body (corps brut, jamais parsé).
4. Calculer HMAC-SHA256(secret_bytes, message) et encoder en hexadécimal.
5. Comparer avec timingSafeEqual pour éviter les attaques temporelles.
Pour le format Standard Webhooks (webhook-signature), le message canonique est webhook-id + "." + webhook-timestamp + "." + body, et la signature est encodée en base64 (non hexadécimal).
Implémentation dans votre endpoint
Structure du payload reçu
Le corps de la requête POST est un objet JSON dont la structure est identique pour tous les types d'événements. C'est ce corps brut (avant tout parsing) qui doit être utilisé pour calculer la signature.
Vérification avec le format Standard Webhooks
Si votre infrastructure utilise un récepteur compatible Standard Webhooks (Svix Receiver, module NestJS, Hookdeck...), utilisez les en-têtes webhook-id, webhook-timestamp et webhook-signature à la place du format legacy. Le message canonique est alors webhook-id + "." + webhook-timestamp + "." + body, et la signature v1,<base64>. La tolérance est identique (300 secondes). Lors d'une rotation de secret, plusieurs signatures séparées par des espaces peuvent apparaître dans webhook-signature : votre récepteur doit en valider au moins une.
Codes d'erreur et résolution
| Code | Quand | Résolution |
|---|---|---|
missing_signature | L'en-tête X-Coffrify-Signature (ou webhook-signature) est absent. | Vérifiez que votre proxy ou load balancer ne supprime pas les en-têtes personnalisés. Testez directement avec curl. |
malformed | L'en-tête ne contient pas les parties t= et v1=. | Contrôlez que vous lisez bien X-Coffrify-Signature et non un autre en-tête. |
expired | L'écart entre l'horodatage du header et l'heure serveur dépasse 300 secondes. | Synchronisez l'horloge de votre serveur (NTP). Si vous rejouez des requêtes en test, désactivez temporairement la vérification du timestamp. |
length_mismatch | La longueur de la signature fournie diffère de la signature calculée. | Assurez-vous de lire le body en bytes bruts (pas parsé, pas retranscrit). Buffer.from sans encoding explicite ou utf8 uniquement. |
invalid_hex | La partie v1= contient des caractères non hexadécimaux. | Vérifiez que l'en-tête n'est pas tronqué ou corrompu par votre framework (ex. body-parser qui transforme les buffers). |
mismatch | La signature est bien formée mais ne correspond pas. | Vérifiez : (1) vous utilisez le body brut avant tout parsing JSON, (2) le secret whsec_ est exact (aucun espace ou saut de ligne), (3) le message canonique est ts.body et non body seul. |
no_secret | Le secret est vide ou absent côté vérificateur. | Chargez COFFRIFY_WEBHOOK_SECRET depuis votre gestionnaire de secrets (Vercel Env, AWS Secrets Manager...). |
Rotation du secret sans interruption
Pour effectuer une rotation sans rejeter de livraisons en cours : utilisez PATCH /v1/webhooks/:id pour mettre à jour le secret. Pendant la période de transition, votre endpoint peut tenter la vérification avec le nouveau secret, puis avec l'ancien en cas d'échec. La réponse de verifySignature retourne matched_secret_index (0 = secret actuel, 1 = ancien) pour vous permettre de détecter les livraisons qui ont utilisé l'ancien secret et de planifier l'abandon de celui-ci.