Skip to main content

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

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):
app.use('/webhooks', (req, res, next) => {
  let data = '';
  req.on('data', chunk => { data += chunk; });
  req.on('end', () => {
    req.rawBody = data;
    next();
  });
});
FastAPI (Python):
@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)

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)

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

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

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

Troubleshooting

SintomaCausa provávelAção
Verificação sempre falhaRaw body modificado por middlewarePreservar buffer antes do express.json() ou equivalente.
Verificação falha após atualizaçãoChave pública rotacionadaLimpar cache e buscar nova chave.
Header ausenteRequisição não é da plataforma (ou WAF removeu o header)Investigar regras de WAF/proxy.
Erro de decodificação Base64Encoding incorretoConfirmar que não há espaços ou quebras de linha na assinatura.

Referências