Assinaturas HMAC
Como confirmar que o pedido veio mesmo do DroidSender e não de outra pessoa.
| Cabeçalho | O que traz |
|---|---|
X-DroidSender-Event | o nome do evento, o mesmo que vai no corpo |
X-DroidSender-Delivery | id único desta tentativa de entrega; deduplica por ele |
X-DroidSender-Signature | a assinatura, descrita abaixo |
User-Agent | DroidSender-Webhooks/2.0 |
Todos os pedidos levam o cabeçalho X-DroidSender-Signature na forma t=<segundos unix>,v1=<hex>, onde v1 é o HMAC-SHA256 de "<t>.<corpo em bruto>" com o segredo do teu webhook.
const crypto = require("crypto");
function assinaturaValida(corpoBruto, cabecalho, segredo) {
const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(cabecalho ?? "");
if (!m) return false;
const [, t, recebida] = m;
// o carimbo faz parte do que é assinado: uma entrega intercetada nao
// se reenvia amanha, e o t nao se reescreve sem o segredo
const idade = Math.abs(Math.floor(Date.now() / 1000) - Number(t));
if (idade > 300) return false;
const esperada = crypto
.createHmac("sha256", segredo)
.update(`${t}.${corpoBruto}`)
.digest("hex");
// comparacao em tempo constante: um === simples deixa medir o segredo
return crypto.timingSafeEqual(
Buffer.from(esperada, "hex"),
Buffer.from(recebida, "hex"),
);
}Recusa o que tiver mais de cinco minutos
Sem a verificação da idade, um pedido reenviado continua a validar. Cinco minutos é a janela recomendada.
Usa o corpo em bruto
A assinatura é calculada sobre o JSON tal como chegou. Se o verificares depois de o converter em objeto e voltar a serializar, os espaços mudam e a assinatura nunca bate certo.