Documentação

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