Verificar Assinatura
Como verificar a assinatura HMAC-SHA256 dos webhooks. Exemplos em curl, Node.js e Python com proteção anti-replay de ±5 minutos.
Formato da assinatura
O header X-MinhaCNH-Signature segue o formato Stripe-style:
t=<unix_timestamp>,v1=<hmac-sha256-hex>t— timestamp Unix (segundos) do momento da entrega.v1— HMAC-SHA256 do payload com o segredo da organização.
A mensagem assinada é: <timestamp>.<corpo-json-raw>.
Janela anti-replay de ±5 minutos
Rejeite entregas cujo t esteja a mais de 5 minutos do horário atual (passado ou futuro).
Isso protege contra ataques de replay de pacotes capturados.
Implementação
import crypto from 'node:crypto';
function verifyWebhookSignature(rawBody, signatureHeader, secret) {
const parts = Object.fromEntries(
signatureHeader.split(',').map(p => p.split('='))
);
const timestamp = parseInt(parts.t, 10);
const receivedSig = parts.v1;
// Anti-replay: rejeitar se delta > 5 minutos
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - timestamp) > 300) {
throw new Error('Webhook timestamp out of tolerance window');
}
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(receivedSig, 'hex'),
Buffer.from(expected, 'hex')
);
}
// Uso (Express) — note express.raw para preservar o corpo cru:
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['x-minhacnh-signature'];
if (!verifyWebhookSignature(req.body.toString(), sig, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
// processar event.status, event.reportId, event.diagnosis ...
res.status(200).send('ok');
});Usar X-MinhaCNH-Delivery para idempotência no receptor
O header X-MinhaCNH-Delivery contém um id único por entrega. Armazene ids já processados
para evitar efeitos duplicados em caso de retry:
const deliveryId = req.headers['x-minhacnh-delivery'];
if (await isAlreadyProcessed(deliveryId)) {
return res.status(200).send('already processed');
}
await markAsProcessed(deliveryId);
// ... processar evento