Seguranca e Assinatura
Como verificar a autenticidade dos webhooks recebidos usando HMAC-SHA256
Como funciona
Na hora de cadastrar o webhook, voce envia o secret que voce escolheu. Quando a gente te notificar, a gente assina o payload com esse mesmo secret. O header X-Signature e incluido em toda entrega para que voce possa validar a autenticidade.
Headers enviados na entrega
Cada POST para seu endpoint inclui os seguintes headers:
| Header | Descricao | Exemplo |
|---|---|---|
X-Signature | Assinatura HMAC-SHA256 | sha256=abc123def456... |
X-Webhook-Event | Tipo do evento | ATUALIZACAO_STATUS_PIX |
X-Webhook-Id | UUID do webhook | 550e8400-... |
X-Event-Id | UUID do evento | 770e8400-... |
X-Timestamp | Epoch seconds do envio | 1714564800 |
X-Trace-Id | ID de rastreamento | abc123 |
Content-Type | Tipo do conteudo | application/json |
Verificando a assinatura
Para validar que a notificacao veio de nos:
- Pegue o header
X-Timestamp - Concatene com ponto (
.) e o body raw:{X-Timestamp}.{body} - Calcule HMAC-SHA256 usando o seu proprio secret
- Compare o resultado com o header
X-Signature
input = "{X-Timestamp}.{body}"
assinatura_esperada = "sha256=" + hex(HMAC-SHA256(seu_secret, input))
// Compare assinatura_esperada com o header X-SignatureJava
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
public boolean verificarAssinatura(String secret, String body,
String timestamp, String assinaturaRecebida) {
String input = timestamp + "." + body;
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] hash = mac.doFinal(input.getBytes(StandardCharsets.UTF_8));
String assinaturaCalculada = "sha256=" + bytesToHex(hash);
return assinaturaCalculada.equals(assinaturaRecebida);
}Node.js
import { createHmac, timingSafeEqual } from "node:crypto";
function verificarAssinatura(secret, body, timestamp, assinaturaRecebida) {
const input = `${timestamp}.${body}`;
const assinaturaCalculada =
"sha256=" + createHmac("sha256", secret).update(input).digest("hex");
return timingSafeEqual(
Buffer.from(assinaturaCalculada),
Buffer.from(assinaturaRecebida),
);
}Python
import hmac
import hashlib
def verificar_assinatura(secret: str, body: str, timestamp: str, assinatura_recebida: str) -> bool:
input_data = f"{timestamp}.{body}"
assinatura_calculada = "sha256=" + hmac.new(
secret.encode(), input_data.encode(), hashlib.sha256
).hexdigest()
return hmac.compare_digest(assinatura_calculada, assinatura_recebida)Sempre use comparacao em tempo constante (timingSafeEqual, hmac.compare_digest) para evitar ataques de timing.
Protecao contra replay attacks
Valide o header X-Timestamp para rejeitar entregas antigas:
- Extraia o valor de
X-Timestamp(epoch seconds) - Compare com o horario atual do servidor
- Rejeite se a diferenca for maior que 5 minutos
const MAX_AGE_SECONDS = 300; // 5 minutos
const agora = Math.floor(Date.now() / 1000);
const timestamp = parseInt(headers["x-timestamp"]);
if (Math.abs(agora - timestamp) > MAX_AGE_SECONDS) {
// Rejeitar — possivel replay attack
return res.status(401).send("Timestamp expirado");
}Validacao de URL
Ao registrar ou atualizar um webhook, a URL e validada:
- Deve usar o esquema HTTPS (HTTP nao e aceito)
- Deve resolver para um IP publico
- IPs bloqueados: loopback (
127.0.0.1), link-local (169.254.x.x), redes privadas (10.x.x.x,172.16-31.x.x,192.168.x.x), CGNAT (100.64.0.0/10) - Servidores de metadata bloqueados:
169.254.169.254,metadata.google.internal
URLs invalidas retornam 422 Unprocessable Entity.
Secret
A secret e uma chave que voce gera e nos envia no momento do cadastro do webhook. Nos usamos essa chave para assinar cada notificacao enviada, e voce usa a mesma chave para verificar que a notificacao realmente veio de nos.
A secret nao e devolvida apos o cadastro. Guarde-a de forma segura no seu sistema antes de enviar.
Validacoes
- Minimo 32 caracteres, maximo 500 caracteres
- Deve ser uma string aleatoria (como um hash) para garantir seguranca
- Armazenamos de forma criptografada — nunca em texto puro
Como gerar
openssl rand -hex 32Isso gera uma string hexadecimal de 64 caracteres, segura para uso como secret.
Payload recebido pelo integrador
O body do POST enviado ao seu endpoint e um array JSON com os eventos:
[
{
"numeroDocumento": "12345678901",
"status": "SETTLED",
"valor": 15000,
"end2end": "E12345678202301011200abcdef12345",
"razaoRejeicao": null
}
]| Campo | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
numeroDocumento | string | sim | Numero do documento (CPF/CNPJ) |
status | string | sim | Status da transacao PIX |
valor | long | sim | Valor em centavos |
end2end | string | nao | Identificador end-to-end da transacao |
razaoRejeicao | objeto | nao | Motivo da rejeicao (presente quando status = REJECTED) |
razaoRejeicao.codigo | string | sim* | Codigo do erro (ex: BE08) |
razaoRejeicao.descricao | string | sim* | Descricao do erro |
Os campos de razaoRejeicao sao obrigatorios quando o objeto esta presente.
