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)
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
| 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