Skip to main content

Por que verificar

Qualquer pessoa na internet pode fazer POST para o seu endpoint. A verificação de assinatura garante que o evento veio realmente do kycert e não foi adulterado em trânsito.

Como funciona

O kycert assina cada requisição com HMAC-SHA256 usando o seu segredo de webhook. A assinatura é enviada no header kycert-signature:
kycert-signature: t=1718200818,v1=3f5e8a2b1c...
ParteDescrição
tTimestamp Unix da requisição
v1HMAC-SHA256 de ${t}.${payload}

Onde encontrar o segredo

No dashboard kycert, vá em Settings → Webhooks e copie o Webhook Secret (prefixo whsec_). Armazene como variável de ambiente — nunca em código.

Algoritmo de verificação

string_to_sign = timestamp + "." + raw_request_body
signature      = HMAC-SHA256(webhook_secret, string_to_sign)
Comparar signature com v1 do header usando comparação de tempo constante. Tolerância de tempo: rejeitar eventos com timestamp mais antigo que 5 minutos (300 segundos) em relação ao relógio atual. Isso protege contra ataques de replay.

Exemplos de implementação

import crypto from 'crypto'

function verifyKycertSignature(
  rawBody: Buffer,
  signatureHeader: string,
  secret: string,
): boolean {
  const parts = Object.fromEntries(
    signatureHeader.split(',').map(p => p.split('=') as [string, string])
  )
  const timestamp = parts['t']
  const received  = parts['v1']

  if (!timestamp || !received) return false

  // Rejeitar eventos com mais de 5 minutos
  if (Math.abs(Date.now() / 1000 - parseInt(timestamp, 10)) > 300) {
    return false
  }

  const computed = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody.toString()}`)
    .digest('hex')

  return crypto.timingSafeEqual(
    Buffer.from(computed, 'hex'),
    Buffer.from(received, 'hex'),
  )
}

Uso em frameworks

import express from 'express'

const app = express()

app.post(
  '/webhooks/kycert',
  express.raw({ type: 'application/json' }), // IMPORTANTE: raw body
  (req, res) => {
    const signature = req.headers['kycert-signature'] as string
    const valid = verifyKycertSignature(
      req.body,
      signature,
      process.env.KYCERT_WEBHOOK_SECRET!,
    )

    if (!valid) return res.status(401).json({ error: 'invalid signature' })

    res.sendStatus(200) // responder imediatamente
    const event = JSON.parse(req.body.toString())
    setImmediate(() => processEvent(event))
  }
)
Use o body raw, não parseado. Middlewares como express.json() transformam o body antes que você possa lê-lo como string — a assinatura vai falhar. Configure express.raw() ou equivalente antes de qualquer parser JSON.

Erros comuns

ProblemaCausaSolução
Assinatura sempre inválidaBody parseado antes da verificaçãoUsar middleware raw body
Erro de tolerância de tempoRelógio do servidor desatualizadoSincronizar com NTP
timingSafeEqual falhaTamanhos diferentes de bufferGarantir que ambos os buffers são hex strings do mesmo tamanho
Header ausenteEndpoint não é HTTPS em produçãoCertificar TLS no endpoint