Vérifier les signatures HMAC-SHA256 des webhooks SMS
Comment vérifier que la requête vient bien de DroidSender et non de quelqu’un d’autre.
Dernière mise à jour
| En-tête | Contenu |
|---|---|
| X-DroidSender-Event | le nom de l’événement, le même que dans le corps |
| X-DroidSender-Delivery | identifie une tentative de livraison ; pour dédoublonner, utilisez event plus data.id (plus data.status pour sent, delivered et failed) |
| X-DroidSender-Signature | la signature, décrite ci-dessous |
| User-Agent | DroidSender-Webhooks/2.0 |
Chaque requête porte l’en-tête X-DroidSender-Signature sous la forme t=<secondes unix>,v1=<hex>, où v1 est le HMAC-SHA256 de "<t>.<corps brut>" calculé avec le secret de votre webhook.
const crypto = require("crypto");
function signatureIsValid(rawBody, header, secret) {
const match = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header ?? "");
if (!match) return false;
const [, t, received] = match;
// l'horodatage fait partie de ce qui est signé : une livraison interceptée
// ne peut pas être rejouée demain, et t ne peut pas être réécrit sans le secret
const age = Math.abs(Math.floor(Date.now() / 1000) - Number(t));
if (age > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
// comparaison à temps constant : un simple === permet de mesurer le secret
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(received, "hex"),
);
}import hmac, hashlib, re, time
def signature_is_valid(raw_body: bytes, header: str, secret: str) -> bool:
m = re.fullmatch(r"t=(\d+),v1=([0-9a-f]{64})", header or "")
if not m:
return False
t, received = m.group(1), m.group(2)
if abs(int(time.time()) - int(t)) > 300:
return False
expected = hmac.new(
secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, received)<?php
function signature_is_valid(string $rawBody, ?string $header, string $secret): bool {
if (!preg_match('/^t=(\d+),v1=([0-9a-f]{64})$/', $header ?? '', $m)) {
return false;
}
[, $t, $received] = $m;
if (abs(time() - (int) $t) > 300) {
return false;
}
$expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
// hash_equals, jamais == : une comparaison classique laisse fuiter le temps d'exécution
return hash_equals($expected, $received);
}
// le corps brut vient de php://input ; $_POST est déjà décodé et inutile ici
$rawBody = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_DROIDSENDER_SIGNATURE'] ?? null;