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

# Verificar assinaturas de webhook

> Como validar o header Digital-Signature com ECDSA P-256 SHA-256; boas práticas, cache da chave pública e exemplos de código.

## Por que verificar a assinatura

A verificação garante que o payload recebido foi gerado pela ClearPago e não foi adulterado em trânsito. **Nunca processe um webhook sem verificar a assinatura** — isso é um requisito de segurança, não uma recomendação.

## Como funciona

A ClearPago assina o **corpo bruto** (raw body) da requisição com **ECDSA P-256 + SHA-256** e inclui a assinatura em Base64 no header `Digital-Signature`.

```
Digital-Signature: <base64(ECDSA P-256 SHA-256(rawBody))>
```

A chave pública para verificação está disponível em `GET /public-key`.

## Passo 1 — Obter e armazenar a chave pública

```bash theme={null}
curl -s https://api.clearpago.com.br/public-key
```

**Resposta (PEM):**

```
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
-----END PUBLIC KEY-----
```

### Boas práticas de cache

* Faça cache da chave com um **TTL razoável** (ex.: 1 hora ou 24 horas).
* **Prepare rotação:** implemente lógica para buscar novamente a chave se a verificação falhar com a versão em cache.
* Nunca hardcode a chave no código — busque programaticamente.

## Passo 2 — Preservar o corpo bruto

A assinatura é calculada sobre o **corpo bruto** da requisição, antes do parse JSON. Configure seu framework para disponibilizar o buffer original.

**Express (Node.js):**

```javascript theme={null}
app.use('/webhooks', (req, res, next) => {
  let data = '';
  req.on('data', chunk => { data += chunk; });
  req.on('end', () => {
    req.rawBody = data;
    next();
  });
});
```

**FastAPI (Python):**

```python theme={null}
@app.post("/webhooks/pix")
async def receive_webhook(request: Request):
    raw_body = await request.body()  # bytes brutos
    signature = request.headers.get("digital-signature")
    # ...
```

## Passo 3 — Verificar a assinatura

### Node.js (crypto nativo)

```javascript theme={null}
const crypto = require('crypto');

function verifyWebhookSignature(publicKeyPem, rawBody, signatureBase64) {
  const verify = crypto.createVerify('SHA256');
  verify.update(rawBody);
  return verify.verify(
    { key: publicKeyPem, dsaEncoding: 'ieee-p1363' },
    Buffer.from(signatureBase64, 'base64')
  );
}

// Uso no handler
app.post('/webhooks/pix', (req, res) => {
  const isValid = verifyWebhookSignature(
    cachedPublicKey,
    req.rawBody,
    req.headers['digital-signature']
  );

  if (!isValid) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  res.status(200).send();
  processEvent(req.body); // assíncrono
});
```

### Python (cryptography)

```python theme={null}
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.exceptions import InvalidSignature
import base64

def verify_webhook_signature(public_key_pem: bytes, raw_body: bytes, signature_b64: str) -> bool:
    public_key = serialization.load_pem_public_key(public_key_pem)
    signature = base64.b64decode(signature_b64)
    try:
        public_key.verify(signature, raw_body, ec.ECDSA(hashes.SHA256()))
        return True
    except InvalidSignature:
        return False

# Uso no handler
@app.post("/webhooks/pix")
async def receive_webhook(request: Request):
    raw_body = await request.body()
    signature = request.headers.get("digital-signature", "")

    if not verify_webhook_signature(cached_public_key, raw_body, signature):
        raise HTTPException(status_code=401, detail="Invalid signature")

    background_tasks.add_task(process_event, await request.json())
    return Response(status_code=200)
```

### Go

```go theme={null}
import (
    "crypto/ecdsa"
    "crypto/sha256"
    "encoding/base64"
    "encoding/asn1"
    "math/big"
)

func verifySignature(pubKey *ecdsa.PublicKey, rawBody []byte, signatureB64 string) bool {
    sigBytes, err := base64.StdEncoding.DecodeString(signatureB64)
    if err != nil {
        return false
    }
    hash := sha256.Sum256(rawBody)

    // Parse DER ASN.1 se necessário, ou IEEE P1363 conforme o formato
    var sig struct{ R, S *big.Int }
    if _, err := asn1.Unmarshal(sigBytes, &sig); err != nil {
        return false
    }
    return ecdsa.Verify(pubKey, hash[:], sig.R, sig.S)
}
```

`TODO: confirmar o formato de encoding da assinatura (DER ASN.1 vs. IEEE P1363) com a implementação. Os exemplos acima usam formatos diferentes — alinhe com o header real enviado pela plataforma.`

## Checklist de verificação

<Check>Corpo bruto preservado antes do parse JSON.</Check>
<Check>Chave pública obtida de `GET /public-key` (não hardcoded).</Check>
<Check>Cache da chave com TTL e rotação automática em caso de falha.</Check>
<Check>Verificação ocorre **antes** de qualquer lógica de negócio.</Check>
<Check>Retorno `401` quando a assinatura for inválida.</Check>
<Check>Retorno `2xx` rápido quando a assinatura for válida.</Check>
<Check>Lógica de negócio executada de forma assíncrona.</Check>

## Troubleshooting

| Sintoma                            | Causa provável                                           | Ação                                                            |
| ---------------------------------- | -------------------------------------------------------- | --------------------------------------------------------------- |
| Verificação sempre falha           | Raw body modificado por middleware                       | Preservar buffer antes do `express.json()` ou equivalente.      |
| Verificação falha após atualização | Chave pública rotacionada                                | Limpar cache e buscar nova chave.                               |
| Header ausente                     | Requisição não é da plataforma (ou WAF removeu o header) | Investigar regras de WAF/proxy.                                 |
| Erro de decodificação Base64       | Encoding incorreto                                       | Confirmar que não há espaços ou quebras de linha na assinatura. |

## Referências

* [GET /public-key (API)](/api-reference/get-public-key)
* [Webhooks: ClearPago → comerciante](/webhooks/clearpago-to-merchant)
* [Referência de eventos](/webhooks/webhook-event-reference)
* [Registrar webhooks](/guides/register-merchant-webhooks)
