Vérification de signature
Vérifiez les signatures webhook pour garantir l'authenticité des charges utiles webhook entrantes
Chaque livraison webhook inclut une signature cryptographique vous permettant de vérifier que la requête provient de MailLand et n'a pas été altérée en transit. Vérifiez toujours les signatures en production.
Fonctionnement de la signature
MailLand signe chaque charge utile webhook avec HMAC-SHA256 en utilisant la clé secrète de votre webhook. La signature est incluse dans l'en-tête X-MLND-Signature.
Format de la signature :
X-MLND-Signature: t=1708171200,v1=b9f747ea899eafe911777060ca8f3c90828d37f1e16ee6e9ee98dacf8b316b67| Composant | Description |
|---|---|
t | Horodatage Unix (secondes) au moment de la génération de la signature |
v1 | Signature hexadécimale HMAC-SHA256 (minuscules) |
Charge utile signée :
{timestamp}.{raw_json_body}La signature est calculée sur l'horodatage concaténé avec un point et le corps brut de la requête JSON.
Exemple complet
Vérifiez votre implémentation avec ces valeurs exactes avant la mise en production. Tout ci-dessous est réel : la signature est celle que MailLand produit pour ce secret, cet horodatage et ce corps.
| Entrée | Valeur |
|---|---|
| Secret | whsec_docs_example_secret_do_not_use |
Horodatage (t) | 1708171200 |
| Corps brut | {"event":"message.delivered","event_id":"evt_a1b2c3d4e5f6"} |
Chaîne signée : l’horodatage, un point littéral, puis le corps tel quel :
1708171200.{"event":"message.delivered","event_id":"evt_a1b2c3d4e5f6"}HMAC-SHA256 de cette chaîne, avec le secret comme clé, en hexadécimal minuscule :
b9f747ea899eafe911777060ca8f3c90828d37f1e16ee6e9ee98dacf8b316b67L’en-tête de cette livraison serait donc :
X-MLND-Signature: t=1708171200,v1=b9f747ea899eafe911777060ca8f3c90828d37f1e16ee6e9ee98dacf8b316b67Reproduisez-le en une ligne :
printf '%s' '1708171200.{"event":"message.delivered","event_id":"evt_a1b2c3d4e5f6"}' \
| openssl dgst -sha256 -hmac 'whsec_docs_example_secret_do_not_use'L’horodatage de cet exemple est dans le passé : un vérificateur réel le rejetterait au titre de la tolérance. Testez d’abord le HMAC avec la vérification temporelle désactivée, puis réactivez-la.
En-têtes envoyés
Chaque livraison webhook inclut ces en-têtes :
| En-tête | Exemple | Description |
|---|---|---|
X-MLND-Signature | t=1708171200,v1=abc123... | Horodatage et signature HMAC-SHA256 |
X-MLND-Event-Id | evt_a1b2c3d4e5f6... | Identifiant unique de l'événement pour la déduplication |
X-MLND-Event | message.delivered | Type d'événement |
X-MLND-Event-Version | 1 | Version du schéma de la charge utile |
X-MLND-Webhook-Id | wh_abc123def456 | Identifiant public de votre webhook |
X-MLND-Attempt | 1 | Numéro de tentative de livraison (commence à 1) |
X-MLND-Delivery-Id | p8r2kx7m9wq4rt | Identifiant de cette tentative, nouveau à chaque essai ; le publicId de l’historique des livraisons. Pas pour la déduplication : voir Versionnage |
X-MLND-Delivery-Timestamp | 1708171200 | Heure d’envoi de cette tentative (secondes Unix) ; toujours égale à t dans la signature |
Deux en-têtes supplémentaires sont conditionnels selon l'événement auquel vous êtes abonné :
| En-tête | Exemple | Envoyé quand |
|---|---|---|
X-MLND-Source | inbound | Présent uniquement sur les livraisons inbound.received |
X-MLND-Inbound-Email-Id | inb_a1b2c3d4e5f6 | Présent uniquement sur les livraisons inbound.received |
X-MLND-Message-Id n'est actuellement pas envoyé sur les livraisons message.*. Utilisez le champ data.message_id du payload de l'événement à la place.
Étapes de vérification
Extraire l'horodatage et la signature
Analysez l'en-tête X-MLND-Signature. Divisez sur , pour obtenir les composants t=... et v1=....
Préparer la charge utile signée
Concaténez l'horodatage, un point (.) et le corps brut de la requête :
signed_payload = timestamp + "." + raw_bodyCalculer la signature attendue
Générez un hachage HMAC-SHA256 en utilisant votre secret webhook comme clé et la charge utile signée comme message. Convertissez en hexadécimal minuscule.
Comparer les signatures
Utilisez une comparaison à temps constant pour vérifier si la signature calculée correspond à la valeur v1 de l'en-tête.
Valider l'horodatage
Vérifiez que l'horodatage est dans les 5 minutes du temps actuel pour prévenir les attaques par rejeu.
Exemples de code
#!/usr/bin/env bash
# Compute and verify an HMAC-SHA256 webhook signature using openssl
SIGNATURE_HEADER="t=1708171200,v1=b9f747ea899eafe911777060ca8f3c90828d37f1e16ee6e9ee98dacf8b316b67"
RAW_BODY='{"event":"message.delivered","event_id":"evt_a1b2c3d4e5f6"}'
SECRET="whsec_docs_example_secret_do_not_use"
TOLERANCE=300
# Parse timestamp and signature from header
TIMESTAMP=$(echo "$SIGNATURE_HEADER" | grep -oP '(?<=t=)[^,]+')
RECEIVED_SIG=$(echo "$SIGNATURE_HEADER" | grep -oP '(?<=v1=).+')
# Check timestamp tolerance
NOW=$(date +%s)
DIFF=$(( NOW - TIMESTAMP ))
if [ "${DIFF#-}" -gt "$TOLERANCE" ]; then
echo "Error: Timestamp outside tolerance window" >&2
exit 1
fi
# Compute expected signature
SIGNED_PAYLOAD="${TIMESTAMP}.${RAW_BODY}"
EXPECTED=$(printf '%s' "$SIGNED_PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
# Compare signatures
if [ "$EXPECTED" = "$RECEIVED_SIG" ]; then
echo "Signature valid"
else
echo "Signature invalid" >&2
exit 1
fiPrévention des attaques par rejeu
L'horodatage dans l'en-tête de signature prévient les attaques par rejeu. Si un attaquant capture une livraison webhook valide et la renvoie ultérieurement, la vérification de l'horodatage la rejettera.
La tolérance par défaut de MailLand est de 5 minutes (300 secondes). Vous pouvez ajuster cela dans votre code de vérification, mais gardez-le suffisamment court pour prévenir les attaques par rejeu significatives.
De plus, chaque événement possède un event_id unique (dans l'en-tête X-MLND-Event-Id). Stockez les identifiants d'événements traités et rejetez les doublons pour une protection supplémentaire.
MailLand déduplique l’émission de son côté : un événement généré deux fois n’est distribué qu’une seule fois par webhook dans une fenêtre de 24 heures. Ce n’est pas la même chose que « livré au plus une fois » : une livraison en échec est réessayée, et chaque tentative porte le même event_id et le même corps ; seuls X-MLND-Attempt et l’horodatage de la signature changent. Dédupliquez sur event_id et considérez les livraisons répétées comme normales.
Chaque livraison est signée
Il n'existe aucun chemin de livraison non signé. Si un webhook n'a pas de secret de signature
utilisable, MailLand fait échouer la livraison et enregistre l'échec plutôt que d'envoyer la charge
utile sans en-tête X-MLND-Signature : une signature manquante n'est donc jamais quelque chose que
votre vérificateur doit tolérer. Rejetez toute requête qui n'en porte pas une.
Regénération des secrets
Si vous soupçonnez que votre secret webhook a été compromis :
- Appelez
POST /v1/webhooks/{id}/regenerate-secretpour obtenir un nouveau secret - Mettez à jour le code de vérification de votre serveur avec le nouveau secret
- L'ancien secret est invalidé immédiatement
Il n'y a pas de période de grâce lors de la regénération des secrets. Mettez à jour votre code de vérification avant la prochaine livraison webhook pour éviter de rejeter des événements valides.
Erreurs courantes
- Analyser le corps en JSON avant de vérifier : la signature est calculée sur les octets bruts, pas sur un objet JSON re-sérialisé. Vérifiez d'abord, puis analysez.
- Utiliser une comparaison de chaînes plutôt qu'une comparaison à temps constant : la comparaison de chaînes ordinaire est vulnérable aux attaques par mesure du temps.
- Ignorer l'horodatage : sans validation de l'horodatage, les charges utiles capturées peuvent être rejouées indéfiniment.
- Coder le secret en dur : utilisez des variables d'environnement ou un gestionnaire de secrets.
Prochaines étapes
- Logique de réessai : comprendre le comportement de réessai et la gestion des échecs
- Événements : référence complète des types d'événements
- Présentation des webhooks : créer et gérer des webhooks