Como validar a assinatura de um webhook com HMAC
Muitos serviços assinam o webhook com o HMAC-SHA256 do corpo da requisição e enviam a assinatura em um cabeçalho. Validar é recalcular o HMAC com o segredo compartilhado e comparar em tempo constante. Este guia mostra como fazer isso em Node.js, Python e PHP, e quais erros fazem a assinatura nunca bater.
Como a assinatura funciona
O serviço que envia o webhook e o seu sistema compartilham um segredo. A cada envio, o serviço calcula o HMAC-SHA256 (um hash SHA-256 combinado com o segredo) do corpo da requisição com esse segredo e coloca o resultado em um cabeçalho. O GitHub, por exemplo, usa X-Hub-Signature-256, no formato sha256= seguido do valor em hexadecimal. Você repete o cálculo e compara: se bateu, a mensagem veio de quem tem o segredo e não foi alterada no caminho.
Um exemplo da documentação do GitHub: com o segredo It's a Secret to Everybody e o corpo Hello, World!, a assinatura é sha256=757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17. Você pode reproduzir isso no gerador de HMAC.
Alguns provedores, como o Stripe, acrescentam um carimbo de tempo ao texto assinado, para dificultar o reenvio de uma requisição antiga. Confira a documentação de cada serviço para o formato exato.
Use o corpo original da requisição
O erro mais comum é calcular o HMAC sobre um JSON que o framework já transformou em objeto e reescreveu. Mudar um espaço, a ordem das chaves ou a codificação muda o resultado. Leia o corpo bruto (os bytes) antes de qualquer processamento e calcule sobre ele.
Verificar em Node.js
import { createHmac, timingSafeEqual } from 'node:crypto';
// corpo: o corpo ORIGINAL da requisição (Buffer ou texto, sem reformatar)
// cabecalho: o valor do cabeçalho de assinatura, por exemplo "sha256=ab12..."
function assinaturaValida(corpo, segredo, cabecalho) {
const esperada = 'sha256=' + createHmac('sha256', segredo).update(corpo).digest('hex');
const a = Buffer.from(cabecalho ?? '');
const b = Buffer.from(esperada);
return a.length === b.length && timingSafeEqual(a, b);
} No Express, use express.raw para receber o corpo original e chame a função antes de tratar o evento:
import express from 'express';
const app = express();
// express.raw entrega o corpo original em req.body (um Buffer), sem transformar em objeto
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
if (!assinaturaValida(req.body, process.env.WEBHOOK_SECRET, req.get('X-Hub-Signature-256'))) {
return res.status(401).send('Assinatura inválida');
}
const evento = JSON.parse(req.body.toString('utf8'));
// ... trate o evento aqui
res.sendStatus(204);
}); Verificar em Python
Em Python, hmac.compare_digest faz a comparação em tempo constante:
import hashlib
import hmac
def assinatura_valida(corpo: bytes, segredo: str, cabecalho: str | None) -> bool:
esperada = "sha256=" + hmac.new(segredo.encode(), corpo, hashlib.sha256).hexdigest()
return hmac.compare_digest(esperada.encode(), (cabecalho or "").encode()) Verificar em PHP
Em PHP, use hash_hmac para calcular e hash_equals para comparar:
<?php
// $corpo: o corpo ORIGINAL da requisição, por exemplo file_get_contents('php://input')
// $cabecalho: por exemplo $_SERVER['HTTP_X_HUB_SIGNATURE_256'] ?? null
function assinaturaValida(string $corpo, string $segredo, ?string $cabecalho): bool
{
$esperada = 'sha256=' . hash_hmac('sha256', $corpo, $segredo);
return hash_equals($esperada, $cabecalho ?? '');
} Erros que fazem a assinatura não bater
- Calcular sobre o corpo reformatado: espaços, ordem das chaves ou quebras de linha diferentes mudam o HMAC.
- Usar o segredo no formato errado: um segredo entregue em Base64 ou hexadecimal, tratado como texto, gera outro HMAC. Para conferir um valor em Base64, use o codificador de Base64.
- Errar a codificação do texto: o cálculo deve ser feito sobre os bytes do corpo, em UTF-8.
- Comparar com
==: use uma comparação em tempo constante, como nas funções acima. - Errar o prefixo: alguns serviços enviam só o hexadecimal, outros colocam
sha256=na frente. - Ignorar o carimbo de tempo quando o provedor o exige: nesse caso, a assinatura não cobre só o corpo.
Como depurar uma assinatura que não bate
Cole o corpo e o segredo no gerador de HMAC, escolha o algoritmo e cole a assinatura recebida no último campo: a ferramenta diz se ela confere. Se não conferir, troque o formato da chave (texto, hexadecimal ou Base64) e revise o corpo byte a byte.
Achou um erro neste guia? Avise por e-mail.
Ferramentas para usar junto
- Gerador de HMACGere HMAC-SHA256, SHA1, SHA384, SHA512 ou MD5 com uma chave secreta e confira assinaturas de webhook
- Gerador de SHA256Gere o hash SHA-256 de um texto ou arquivo online, em hexadecimal ou Base64
- Codificar Base64Converta texto para Base64 online, com suporte a acentos (UTF-8) e opção URL-safe
Perguntas frequentes
Por que não basta conferir o IP de origem do webhook?
Os endereços de IP mudam, e nem todo provedor publica uma lista estável. Mesmo quando publica, o IP não prova que o conteúdo não foi alterado nem que foi gerado por quem tem o segredo.
Por que usar comparação em tempo constante?
Uma comparação comum para no primeiro caractere diferente, e a diferença de tempo pode, em teoria, revelar a assinatura certa aos poucos. Funções como timingSafeEqual (Node.js), hmac.compare_digest (Python) e hash_equals (PHP) levam o mesmo tempo, não importa onde os valores diferem.
O HMAC protege contra o reenvio de uma requisição antiga?
Sozinho, não: uma requisição válida capturada pode ser reenviada. Por isso alguns provedores incluem um carimbo de tempo na assinatura. O seu sistema também deve recusar mensagens antigas e guardar os identificadores dos eventos já processados.
Qual algoritmo devo usar para validar?
O que o provedor documenta. O SHA-256 é o mais comum. Não troque de algoritmo por conta própria, porque o outro lado precisa calcular exatamente o mesmo.