# Vérification de signature des webhooks : HMAC-SHA256 et protection contre le rejeu

> Un webhook est authentifié par une signature HMAC-SHA256 que l'émetteur calcule sur un timestamp plus le corps brut exact, à l'aide d'un secret partagé, et transmet dans un en-tête. Le récepteur recalcule le HMAC, le compare en temps constant, et rejette la requête si la signature ne correspond pas ou si le timestamp est hors d'une fenêtre de tolérance (protection contre le rejeu).

- URL canonique: https://www.broadnet.me/fr/reference/webhook-signature-verification
- Langue: fr
- Dernière mise à jour: 2026-07-19
- Éditeur: BroadNet (https://www.broadnet.me)

Comment vérifier qu'un webhook est authentique et non altéré : HMAC-SHA256 sur le corps brut, les en-têtes de signature et de timestamp, la protection contre le rejeu avec une tolérance de timestamp et un nonce, la comparaison en temps constant pour déjouer les attaques temporelles, et la rotation sûre des secrets avec chevauchement des clés.

## Pourquoi des signatures, pas des listes d'IP autorisées

Un endpoint de webhook est une URL HTTPS publique, si bien que n'importe qui peut y faire un POST. TLS prouve que le client a atteint votre serveur ; il ne prouve en rien que la requête provient réellement de la plateforme de messagerie. Le filtrage par IP est fragile (les IP d'émission changent, et il échoue derrière une sortie partagée). La réponse standard est un code d'authentification de message à clé : l'émetteur et le récepteur partagent un secret, l'émetteur signe chaque charge utile avec lui, et le récepteur vérifie. HMAC (RFC 2104) avec SHA-256 (RFC 6234) est la construction omniprésente car elle est rapide, standardisée et — contrairement à un simple hash du secret concaténé au message — prouvée résistante aux attaques par extension de longueur. Une signature valide prouve deux choses à la fois : la charge utile a été produite par un détenteur du secret, et elle n'a pas été altérée en transit.

## Les en-têtes de signature et de timestamp

L'émetteur inclut au moins deux en-têtes : un timestamp (quand la requête a été signée) et une signature. Le contenu signé n'est pas le seul JSON mais une chaîne canonique qui lie le timestamp au corps — typiquement le timestamp, un séparateur, et le corps brut de la requête — de sorte qu'un attaquant ne puisse pas transposer une signature valide sur une charge utile différente ni la rejouer à un autre moment. Le nommage varie selon le fournisseur (Webhook-Signature / Webhook-Timestamp, ou X-Signature / X-Timestamp), mais la construction est la même. La signature est généralement en hexadécimal ou base64 et porte souvent un préfixe de schéma (par exemple v1,) pour que le secret puisse être basculé vers un nouveau schéma sans casser les anciens vérificateurs.

*Contenu signé et disposition des en-têtes*

```text
signed_content = timestamp + '.' + raw_body
signature      = HMAC_SHA256(secret, signed_content)

Webhook-Timestamp: 1752916522
Webhook-Signature: v1,3f2b8c9a1d...   # hex or base64, scheme-prefixed
```

## Vérifier sur le corps brut

Le bug de vérification le plus courant est de signer un corps re-sérialisé. Le HMAC est calculé sur des octets exacts, si bien que le récepteur doit vérifier sur le corps brut de la requête précisément tel que reçu — avant toute analyse-et-réencodage JSON. Analyser vers un objet puis restringifier réordonne les clés, change les espaces et normalise les nombres et les échappements Unicode, produisant des octets différents et une signature qui ne correspond pas même si rien n'a été altéré. Concrètement, cela signifie capturer le corps brut dans le framework web avant que le middleware d'analyse du corps ne le consomme, vérifier le HMAC sur ces octets, et seulement ensuite analyser le JSON pour l'utiliser. Le jeu de caractères compte aussi : signez et vérifiez la même séquence d'octets (UTF-8), pas une chaîne décodée.

*Vérification (pseudo-code) — corps brut, temps constant*

```text
raw   = request.raw_body            # exact bytes, pre-parse
ts    = header('Webhook-Timestamp')
sig   = header('Webhook-Signature')

if abs(now() - ts) > TOLERANCE: reject   # replay window
expected = hmac_sha256(secret, ts + '.' + raw)
if not constant_time_equal(expected, sig): reject
accept(parse_json(raw))
```

## Protection contre le rejeu : tolérance de timestamp et nonce

Une signature seule n'arrête pas le rejeu — une requête capturée mais valide pourrait être renvoyée plus tard. Deux mécanismes comblent cette faille. Premièrement, comme le timestamp est à l'intérieur du contenu signé, le récepteur le compare à sa propre horloge et rejette tout ce qui est plus ancien qu'une tolérance (couramment quelques minutes, assez large pour absorber la dérive d'horloge et le délai de reprise, assez étroite pour limiter la fenêtre de rejeu). Deuxièmement, pour des garanties plus strictes, le récepteur enregistre un nonce — l'id unique de l'événement — pendant la période de tolérance et rejette une seconde requête portant un id déjà vu. La tolérance de timestamp borne la fenêtre ; le cache de nonce rend le rejeu inefficace à l'intérieur de cette fenêtre. Notez que ceci est complémentaire à l'idempotence du consommateur, qui gère les doublons légitimes plutôt que les rejeux malveillants.

*Contrôles de protection contre le rejeu*

| Contrôle | Se défend contre | Optimisation |
|---|---|---|
| Horodatage dans le contenu signé | Réutiliser une signature à un autre instant | Doit être signé, pas un simple en-tête |
| Fenêtre de tolérance d'horodatage | Relecture différée d'une requête capturée | Quelques minutes ; compromis entre décalage d'horloge et exposition |
| Cache de nonces / d'ID d'événement | Rejeu dans la fenêtre de tolérance | Mettre en cache les identifiants au moins pendant la durée de tolérance |
| Comparaison à temps constant | Canal auxiliaire temporel sur la vérification de signature | Toujours, indépendamment de ce qui précède |

## Comparaison en temps constant

Comparer les signatures calculée et reçue avec un test d'égalité ordinaire fuit de l'information : une comparaison octet par octet renvoie plus vite en cas de non-concordance précoce, et un attaquant capable de mesurer ce délai peut recouvrer la signature correcte octet par octet. La vérification doit donc utiliser une comparaison en temps constant (indépendante de la longueur) — la comparaison à durée sûre de la bibliothèque standard de chaque plateforme — qui examine toujours toute la longueur avant de renvoyer. C'est peu coûteux et non négociable ; c'est la différence entre un schéma de signature solide et un schéma défaisable avec suffisamment de requêtes chronométrées.

## Rotation des secrets

Les secrets de signature doivent être rotatables sans perdre de webhooks légitimes en cours de route. Le motif sûr est le chevauchement des clés : l'émetteur se voit provisionner un nouveau secret pendant que l'ancien reste valide, et durant le chevauchement l'émetteur signe soit avec les deux (émettant deux signatures) soit le récepteur accepte une signature qui correspond à l'un ou l'autre secret. Une fois chaque producteur confirmé sur le nouveau secret, l'ancien est retiré. Le préfixe de schéma sur la signature (v1, v2,) rend cela observable et permet à un changement d'algorithme versionné de suivre. Le validateur de signature de webhook hébergé de BroadNet vous permet de vérifier une charge utile, un timestamp et un secret contre un HMAC attendu sans écrire de code ; la mécanique de la signature DLR HMAC-SHA256 est traitée dans la bibliothèque de réponses.

## Questions fréquentes

### Comment une signature de webhook est-elle calculée ?

L'émetteur construit une chaîne canonique liant le timestamp au corps brut (typiquement timestamp + '.' + corps), calcule HMAC-SHA256 dessus avec un secret partagé, et place le résultat dans un en-tête de signature aux côtés du timestamp. Le récepteur recalcule le même HMAC et compare. Une correspondance prouve que la charge utile provient d'un détenteur du secret et n'a pas été altérée.

### Pourquoi dois-je vérifier sur le corps brut de la requête ?

Le HMAC est calculé sur des octets exacts. Si vous analysez le JSON puis le re-sérialisez, l'ordre des clés, les espaces, le format des nombres et les échappements Unicode changent, si bien que les octets diffèrent et que la signature ne correspond plus, même si rien n'a été altéré. Capturez le corps brut avant le middleware d'analyse du corps, vérifiez, puis analysez.

### Comment la tolérance de timestamp arrête-t-elle les attaques par rejeu ?

Le timestamp fait partie du contenu signé, il ne peut donc être modifié sans casser la signature. Le récepteur le compare à sa propre horloge et rejette tout ce qui est plus ancien qu'une tolérance (couramment quelques minutes). Cela borne combien de temps une requête valide capturée peut être rejouée ; un cache de nonce ferme les rejeux à l'intérieur de cette fenêtre.

### Pourquoi la comparaison en temps constant est-elle requise ?

Un test d'égalité ordinaire renvoie tôt au premier octet non concordant, et un attaquant capable de chronométrer la réponse peut recouvrer la signature correcte octet par octet. Une comparaison en temps constant examine toujours toute la longueur avant de renvoyer, supprimant le canal auxiliaire temporel. Utilisez la fonction de comparaison à durée sûre de la plateforme.

### Comment faire tourner un secret de signature de webhook en toute sécurité ?

Utilisez le chevauchement des clés. Provisionnez le nouveau secret pendant que l'ancien reste valide ; durant le chevauchement, l'émetteur signe avec les deux ou le récepteur accepte l'un ou l'autre, de sorte qu'aucun webhook légitime ne soit perdu. Une fois tous les producteurs confirmés sur le nouveau secret, retirez l'ancien. Un préfixe de schéma sur la signature rend la transition observable.

### HTTPS ne suffit-il pas à faire confiance à un webhook ?

Non. TLS prouve que le transport était chiffré et que le client a atteint votre serveur, mais pas que la requête provient de la plateforme de messagerie — n'importe qui peut faire un POST vers une URL publique. Seule une signature HMAC à secret partagé authentifie l'émetteur et prouve que la charge utile n'a pas été modifiée. Le filtrage par IP est un substitut faible car les IP de sortie changent.

## Sources

- [IETF RFC 2104 — HMAC: Keyed-Hashing for Message Authentication](https://datatracker.ietf.org/doc/html/rfc2104)
- [IETF RFC 6234 — US Secure Hash Algorithms (SHA and HMAC-SHA)](https://datatracker.ietf.org/doc/html/rfc6234)

## Voir aussi

- [Livraison et fiabilité des webhooks](https://www.broadnet.me/fr/reference/webhook-delivery-and-reliability)
- [Statut des messages et webhooks entrants](https://www.broadnet.me/fr/reference/message-status-and-inbound-webhooks)
- [Authentification et signature d'API](https://www.broadnet.me/fr/reference/api-authentication-and-signing)
- [Validateur de signature de webhook](https://www.broadnet.me/fr/tools/webhook-signature-validator)
- [Signature DLR HMAC-SHA256](https://www.broadnet.me/fr/answers/hmac-sha256-dlr-signing)
