Verificar a assinatura
Como confirmar que uma requisição de webhook veio da Clav e não foi alterada nem reenviada por terceiros.
Por que verificar
A URL do seu webhook não é secreta, então qualquer pessoa pode mandar um POST
para ela. Sem conferir a assinatura, o seu servidor não tem como distinguir um
evento da Clav de uma requisição forjada. Recuse toda requisição cuja
assinatura não confira.
Formato do cabeçalho
x-clav-signature: t=1758024251,v1=5f2b0c...e91até o momento do envio, em segundos desde 1970 (Unix).v1é o HMAC-SHA256, em hexadecimal, calculado com o segredo do webhook.
O texto assinado é o timestamp, um ponto e o corpo bruto da requisição:
<t>.<corpo bruto>O timestamp entra na assinatura para que ninguém consiga reaproveitar uma
requisição antiga trocando só o t.
Passo a passo
Leia o corpo bruto
Pegue o corpo exatamente como chegou, antes de qualquer parse. Se o seu framework converter o JSON e você serializar de novo, espaços e ordem dos campos podem mudar e a assinatura não vai bater.
Separe t e v1
Divida o cabeçalho x-clav-signature pelas vírgulas e cada parte pelo sinal de
igual.
Recuse timestamps antigos
Compare t com o relógio do seu servidor. Recomendamos recusar requisições com
mais de 5 minutos de diferença, o que barra reenvios de requisições capturadas.
Calcule e compare
Calcule o HMAC-SHA256 de t + "." + corpo com o segredo e compare com v1
usando uma comparação de tempo constante.
Exemplo em Node.js
import { createHmac, timingSafeEqual } from "node:crypto";
const TOLERANCE_SECONDS = 300;
export function isValidClavSignature(input: {
rawBody: string;
header: string | null;
secret: string;
}): boolean {
if (!input.header) return false;
const parts = new Map(
input.header.split(",").map((part) => {
const [key, ...rest] = part.split("=");
return [key?.trim(), rest.join("=")] as const;
}),
);
const timestamp = Number(parts.get("t"));
const received = parts.get("v1");
if (!Number.isInteger(timestamp) || !received) return false;
const age = Math.abs(Date.now() / 1000 - timestamp);
if (age > TOLERANCE_SECONDS) return false;
const expected = createHmac("sha256", input.secret)
.update(timestamp + "." + input.rawBody)
.digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(received, "hex");
return a.length === b.length && timingSafeEqual(a, b);
}Exemplo em Python
import hashlib
import hmac
import time
TOLERANCE_SECONDS = 300
def is_valid_clav_signature(raw_body: bytes, header: str | None, secret: str) -> bool:
if not header:
return False
parts = dict(part.strip().split("=", 1) for part in header.split(",") if "=" in part)
timestamp = parts.get("t", "")
received = parts.get("v1")
if not timestamp.isdigit() or not received:
return False
if abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS:
return False
signed = timestamp.encode() + b"." + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, received)Use o segredo como texto
O segredo é exibido como uma string base64url. Use essa string diretamente como chave do HMAC, sem decodificar.
Problemas comuns
A assinatura nunca confere
Quase sempre o corpo foi alterado antes da verificação. Confirme que você está
usando o corpo bruto, e não o resultado de JSON.stringify sobre o objeto já
convertido. Confira também se o segredo é o do webhook certo, já que cada
webhook tem o seu.
Algumas requisições são recusadas por timestamp
O relógio do seu servidor provavelmente está adiantado ou atrasado. Sincronize com NTP. Lembre que cada nova tentativa de entrega é assinada de novo, com um timestamp novo.