Confirmação de webhooks com assinatura de conteúdo
Confirmar autenticidade da notificação
Visão geral
Toda notificação deve ter sua autenticidade validada antes de qualquer atualização no sistema do integrador.
O PagBank assina o corpo da notificação com uma chave privada. O integrador consulta a chave pública correspondente e a utiliza para verificar as assinaturas recebidas no header x-payload-signature.
A validação utiliza:
- algoritmo de hash:
SHA-256; - algoritmo de assinatura:
ECDSA; - identificação comum nas bibliotecas:
SHA256withECDSA; - conteúdo validado: corpo original da requisição HTTP;
- assinatura: valor em Base64 recebido em
x-payload-signature.
A notificação é considerada autêntica quando pelo menos uma assinatura recebida for válida.
Antes de começar
Sua aplicação deve:
- possuir um token válido para consultar a chave pública;
- conseguir acessar o corpo bruto da requisição;
- capturar todos os valores de
x-payload-signature; - utilizar uma biblioteca com suporte a ECDSA e SHA-256;
- armazenar a chave pública de forma segura e atualizada.
Valide a assinatura antes de converter o payload em objeto, alterar sua formatação ou iniciar qualquer processamento de negócio.
1. Consulte a chave pública
Obtenha a chave pública específica para autenticação de webhooks utilizando type=webhook.
curl --location \
'https://sandbox.api.pagseguro.com/public-keys?type=webhook' \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--header 'Authorization: Bearer SEU_TOKEN'Exemplo de resposta:
{
"public_key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMgZPv1MegPbRkddk7LdfRtix4uTVs5HeAE0kjlzEJwod3aSIcG3LiKbQFOxHzYlzqrq8xXO/T+VbstgwuHPjzg==",
"links": []
}O campo public_key contém uma chave pública codificada em Base64 no formato X.509.
Armazene a chave em cache para evitar uma consulta a cada notificação. Atualize o valor quando houver renovação das chaves.
Antes da publicação desta documentação como padrão geral do PagBank, confirme a URL de produção do endpoint de chaves públicas do novo serviço de notificações.
2. Capture a assinatura e o payload original
O PagBank enviará uma ou mais assinaturas no header:
x-payload-signature: MEQCIA...Durante a rotação de chaves, o header pode conter mais de um valor:
x-payload-signature = [
MEQCIAv7vXuOvhJlFpHHln5gIiiB8mRnYX6b13tNGEMwHaajAiBhZzuWaiLzuFf4PyevOHrx6gXLRJVUrm1kR/649td+dQ==,
XYZaxAv7vXuOvhJlFpHHln5gIiiB8mRnYX6b13tNGEMwHaajAiBhZzuWaiLzuFf4PyevOHrx6gXLRJVUrm1kR/649td+dQ==
]Valide todos os valores recebidos. Basta que uma assinatura seja válida.
Preserve o corpo bruto
A assinatura é calculada sobre os bytes exatos enviados na requisição.
Não faça antes da validação:
- parse e nova serialização do JSON;
- remoção ou inclusão de espaços;
- alteração de quebras de linha;
- ordenação de propriedades;
- conversão de caracteres;
- criação de um novo JSON a partir do objeto recebido.
Use o corpo bruto recebido pelo servidor, interpretado em UTF-8.
3. Valide a assinatura
Para cada assinatura:
- decodifique a chave pública de Base64;
- carregue a chave no formato X.509;
- decodifique a assinatura de Base64;
- utilize
SHA256withECDSA; - valide a assinatura contra os bytes do payload original;
- aceite a notificação se pelo menos uma assinatura for válida.
Payload original
+
Chave pública
+
Assinatura recebida
│
▼
SHA256withECDSA
│
├── uma assinatura válida → processar
└── nenhuma válida → rejeitarExemplos
const crypto = require("crypto");
function toPublicKeyPem(publicKeyBase64) {
const lines = publicKeyBase64.match(/.{1,64}/g) ?? [];
return [
"-----BEGIN PUBLIC KEY-----",
...lines,
"-----END PUBLIC KEY-----"
].join("\n");
}
function normalizeSignatures(headerValue) {
if (!headerValue) return [];
const values = Array.isArray(headerValue)
? headerValue
: headerValue.split(",");
return values.map((value) => value.trim()).filter(Boolean);
}
function validateWebhook({ rawBody, signatureHeader, publicKeyBase64 }) {
if (!Buffer.isBuffer(rawBody)) {
throw new TypeError("rawBody deve ser o Buffer original da requisição.");
}
const signatures = normalizeSignatures(signatureHeader);
if (signatures.length === 0) return false;
const publicKeyPem = toPublicKeyPem(publicKeyBase64);
return signatures.some((signatureBase64) => {
try {
return crypto.verify(
"sha256",
rawBody,
publicKeyPem,
Buffer.from(signatureBase64, "base64")
);
} catch {
return false;
}
});
}const express = require("express");
const app = express();
app.post(
"/webhooks/pagbank",
express.raw({ type: "application/json" }),
async (request, response) => {
const isValid = validateWebhook({
rawBody: request.body,
signatureHeader: request.headers["x-payload-signature"],
publicKeyBase64: process.env.PAGBANK_WEBHOOK_PUBLIC_KEY
});
if (!isValid) {
return response.status(401).json({
error: "INVALID_WEBHOOK_SIGNATURE"
});
}
const event = JSON.parse(request.body.toString("utf8"));
await processEvent(event);
return response.sendStatus(204);
}
);import java.nio.charset.StandardCharsets
import java.security.KeyFactory
import java.security.Signature
import java.security.spec.X509EncodedKeySpec
import java.util.Base64
fun isWebhookAuthentic(
rawPayload: String,
headerSignatures: List<String>,
publicKeyBase64: String
): Boolean {
if (headerSignatures.isEmpty()) return false
val publicKeyBytes = Base64.getDecoder().decode(publicKeyBase64)
val publicKeySpec = X509EncodedKeySpec(publicKeyBytes)
val publicKey = KeyFactory
.getInstance("EC")
.generatePublic(publicKeySpec)
return headerSignatures.any { signatureBase64 ->
try {
val verifier = Signature.getInstance("SHA256withECDSA")
verifier.initVerify(publicKey)
verifier.update(rawPayload.toByteArray(StandardCharsets.UTF_8))
val signatureBytes = Base64
.getDecoder()
.decode(signatureBase64)
verifier.verify(signatureBytes)
} catch (exception: Exception) {
false
}
}
}4. Trate o resultado
Assinatura válida
Quando pelo menos uma assinatura for válida:
- converta o payload para o objeto esperado;
- verifique o tipo do evento;
- aplique controles de idempotência;
- processe a atualização;
- retorne uma resposta HTTP de sucesso.
Assinatura inválida
Quando nenhuma assinatura for válida:
- não processe o evento;
- não atualize pedidos, cobranças ou qualquer outro recurso crítico;
- não execute operações financeiras;
- registre a tentativa de forma segura;
- retorne um erro HTTP de autenticação.
Quando x-payload-signature estiver ausente ou vazio, trate a notificação como não autenticada.
5. Renove o par de chaves
O par de chaves utilizado na autenticação pode ser renovado.
curl --location --request PUT \
'https://sandbox.api.pagseguro.com/public-keys' \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--header 'Authorization: Bearer SEU_TOKEN' \
--data '{
"type": "webhook"
}'A renovação somente poderá ser realizada após sete dias da criação do par atual.
Durante a transição, o PagBank pode assinar o payload com a chave anterior e com a nova chave. Por isso, x-payload-signature pode conter mais de uma assinatura.
Após a renovação:
- consulte novamente a chave pública;
- atualize o cache da aplicação;
- valide todas as assinaturas recebidas;
- monitore falhas de autenticação durante a transição.
Diferença em relação ao modelo anterior
| Modelo anterior | Novo modelo |
|---|---|
| Concatenação entre token e payload | Validação do payload com chave pública |
| Hash SHA-256 simples | Assinatura ECDSA com SHA-256 |
Header x-authenticity-token | Header x-payload-signature |
| Um hash esperado | Uma ou mais assinaturas possíveis |
| Segredo compartilhado participa do cálculo | A chave privada permanece no PagBank |
Não utilize o token da conta para calcular a assinatura do payload. O token serve apenas para autenticar a consulta e a renovação da chave pública.
Tratamento de erros
| Cenário | Tratamento |
|---|---|
x-payload-signature ausente | Rejeitar a notificação. |
| Header vazio | Rejeitar a notificação. |
| Chave pública inválida | Não processar e atualizar a chave pública. |
| Assinatura Base64 inválida | Ignorar esse valor e testar os demais. |
| Nenhuma assinatura válida | Rejeitar a notificação. |
| Uma assinatura válida entre várias | Considerar a notificação autêntica. |
| Payload alterado antes da validação | Repetir a validação com o corpo bruto original. |
| Falha ao consultar a chave | Utilizar uma chave válida em cache; não processar sem uma chave confiável. |
Boas práticas
- Valide a assinatura antes de processar o JSON;
- Preserve o corpo bruto da requisição;
- Valide todos os valores de
x-payload-signature; - Armazene a chave pública em cache;
- Proteja o token usado para consultar e renovar a chave;
- Registre falhas sem expor dados sensíveis;
- Aplique idempotência usando o identificador da notificação;
- Utilize HTTPS;
- Não confie apenas no IP de origem;
- Não processe notificações sem assinatura válida.
