> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kycert.com.br/llms.txt
> Use this file to discover all available pages before exploring further.

# Segurança de webhooks

> Como verificar a assinatura HMAC-SHA256 do kycert

## 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...
```

| Parte | Descrição                        |
| ----- | -------------------------------- |
| `t`   | Timestamp Unix da requisição     |
| `v1`  | HMAC-SHA256 de `${t}.${payload}` |

## Onde encontrar o segredo

No dashboard kycert, vá em **Integrações & API → Webhooks** e copie o **Webhook Secret** (hex puro, sem prefixo — ex: `a218fc6f3b...`). Armazene como variável de ambiente — nunca em código.

<Warning>
  O Webhook Secret é exibido **apenas no momento da criação**. Copie imediatamente. Se perdido, use **Regenerar secret** — isso invalida o secret anterior e requer atualização em todos os receptores.
</Warning>

## 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.

<Note>
  Use o valor `t` do header `kycert-signature` para a verificação de timestamp — o campo `created` no body pode diferir em \~2s e **não deve ser usado** na verificação de replay.
</Note>

## Exemplos de implementação

<CodeGroup>
  ```typescript Node.js theme={null}
  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', Buffer.from(secret, 'hex'))
      .update(`${timestamp}.${rawBody.toString()}`)
      .digest('hex')

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

  ```python Python theme={null}
  import hmac
  import hashlib
  import time

  def verify_kycert_signature(
      raw_body: bytes,
      signature_header: str,
      secret: str,
  ) -> bool:
      parts = dict(p.split('=', 1) for p in signature_header.split(','))
      timestamp = parts.get('t', '')
      received  = parts.get('v1', '')

      if not timestamp or not received:
          return False

      # Rejeitar eventos com mais de 5 minutos
      if abs(time.time() - int(timestamp)) > 300:
          return False

      computed = hmac.new(
          bytes.fromhex(secret),
          f"{timestamp}.{raw_body.decode()}".encode(),
          hashlib.sha256,
      ).hexdigest()

      return hmac.compare_digest(computed, received)
  ```

  ```go Go theme={null}
  import (
      "crypto/hmac"
      "crypto/sha256"
      "encoding/hex"
      "math"
      "strconv"
      "strings"
      "time"
  )

  func VerifyKycertSignature(rawBody []byte, signatureHeader, secret string) bool {
      parts := map[string]string{}
      for _, p := range strings.Split(signatureHeader, ",") {
          kv := strings.SplitN(p, "=", 2)
          if len(kv) == 2 {
              parts[kv[0]] = kv[1]
          }
      }

      timestamp, ok1 := parts["t"]
      received, ok2  := parts["v1"]
      if !ok1 || !ok2 {
          return false
      }

      ts, err := strconv.ParseInt(timestamp, 10, 64)
      if err != nil || math.Abs(float64(time.Now().Unix()-ts)) > 300 {
          return false
      }

      secretBytes, err := hex.DecodeString(secret)
      if err != nil {
          return false
      }

      mac := hmac.New(sha256.New, secretBytes)
      mac.Write([]byte(timestamp + "." + string(rawBody)))
      computed := hex.EncodeToString(mac.Sum(nil))

      return hmac.Equal([]byte(computed), []byte(received))
  }
  ```
</CodeGroup>

## Uso em frameworks

<CodeGroup>
  ```typescript Express theme={null}
  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))
    }
  )
  ```

  ```typescript Next.js Route Handler theme={null}
  // app/api/webhooks/kycert/route.ts
  export async function POST(request: Request) {
    const rawBody = Buffer.from(await request.arrayBuffer())
    const signature = request.headers.get('kycert-signature') ?? ''

    const valid = verifyKycertSignature(
      rawBody,
      signature,
      process.env.KYCERT_WEBHOOK_SECRET!,
    )

    if (!valid) {
      return new Response('Unauthorized', { status: 401 })
    }

    const event = JSON.parse(rawBody.toString())
    // processar de forma assíncrona
    void processEvent(event)

    return new Response(null, { status: 200 })
  }
  ```
</CodeGroup>

<Warning>
  **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.
</Warning>

## Erros comuns

| Problema                    | Causa                              | Solução                                                        |
| --------------------------- | ---------------------------------- | -------------------------------------------------------------- |
| Assinatura sempre inválida  | Body parseado antes da verificação | Usar middleware raw body                                       |
| Erro de tolerância de tempo | Relógio do servidor desatualizado  | Sincronizar com NTP                                            |
| `timingSafeEqual` falha     | Tamanhos diferentes de buffer      | Garantir que ambos os buffers são hex strings do mesmo tamanho |
| Header ausente              | Endpoint não é HTTPS em produção   | Certificar TLS no endpoint                                     |
