Verifica las firmas HMAC-SHA256 de los webhooks de SMS
Cómo confirmar que la petición viene de verdad de DroidSender y no de otra persona.
Última actualización
| Cabecera | Qué contiene |
|---|---|
| X-DroidSender-Event | el nombre del evento, el mismo que en el cuerpo |
| X-DroidSender-Delivery | identifica un intento de entrega; para eliminar duplicados, usa event más data.id (más data.status para sent, delivered y failed) |
| X-DroidSender-Signature | la firma, descrita más abajo |
| User-Agent | DroidSender-Webhooks/2.0 |
Cada petición lleva la cabecera X-DroidSender-Signature con la forma t=<segundos unix>,v1=<hex>, donde v1 es el HMAC-SHA256 de "<t>.<cuerpo en bruto>" con el secreto de tu 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;
// la marca de tiempo forma parte de lo que se firma: una entrega interceptada
// no se puede reenviar mañana, y t no se puede reescribir sin el secreto
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");
// comparación en tiempo constante: un === normal permite medir el secreto
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, nunca ==: una comparación normal filtra el tiempo
return hash_equals($expected, $received);
}
// el cuerpo en bruto viene de php://input; $_POST ya está procesado y aquí no sirve
$rawBody = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_DROIDSENDER_SIGNATURE'] ?? null;