Um webhook é autenticado por uma assinatura HMAC-SHA256 que o remetente calcula sobre um timestamp mais o corpo em bruto exato, usando um segredo partilhado, e envia num header. O recetor recalcula o HMAC, compara-o em tempo constante e rejeita o pedido se a assinatura não corresponder ou se o timestamp estiver fora de uma janela de tolerância (proteção contra replay).
Porquê assinaturas, não allowlists de IP
Um endpoint de webhook é um URL HTTPS público, pelo que qualquer pessoa pode fazer POST para ele. O TLS prova que o cliente chegou ao seu servidor; não prova de forma alguma que o pedido veio efetivamente da plataforma de mensagens. O allowlisting de IP é frágil (os IPs dos remetentes mudam e falha atrás de saídas partilhadas). A resposta padrão é um código de autenticação de mensagem com chave: o remetente e o recetor partilham um segredo, o remetente assina cada payload com ele e o recetor verifica. HMAC (RFC 2104) com SHA-256 (RFC 6234) é a construção omnipresente porque é rápida, normalizada e — ao contrário de um hash simples do segredo concatenado com a mensagem — comprovadamente resistente a ataques de length-extension. Uma assinatura válida prova duas coisas ao mesmo tempo: o payload foi produzido por um detentor do segredo e não foi alterado em trânsito.
Os headers de assinatura e timestamp
O remetente inclui pelo menos dois headers: um timestamp (quando o pedido foi assinado) e uma assinatura. O conteúdo assinado não é o JSON isolado, mas uma string canónica que liga o timestamp ao corpo — normalmente o timestamp, um separador e o corpo em bruto do pedido — para que um atacante não possa transferir uma assinatura válida para outro payload nem repeti-la noutro momento. A nomenclatura varia por fornecedor (Webhook-Signature / Webhook-Timestamp, ou X-Signature / X-Timestamp), mas a construção é a mesma. A assinatura costuma ser hex ou base64 e traz muitas vezes um prefixo de esquema (por exemplo v1,) para que o segredo possa ser rodado para um novo esquema sem quebrar os verificadores antigos.
signed_content = timestamp + '.' + raw_body
signature = HMAC_SHA256(secret, signed_content)
Webhook-Timestamp: 1752916522
Webhook-Signature: v1,3f2b8c9a1d... # hex or base64, scheme-prefixedVerificar contra o corpo em bruto
O bug de verificação mais comum é assinar um corpo re-serializado. O HMAC é calculado sobre bytes exatos, pelo que o recetor tem de verificar contra o corpo em bruto do pedido, precisamente como recebido — antes de qualquer parse-and-re-encode de JSON. Fazer parse para um objeto e voltar a converter em string reordena chaves, altera o whitespace e normaliza números e escapes Unicode, produzindo bytes diferentes e uma discrepância de assinatura mesmo quando nada foi adulterado. Na prática, isto significa capturar o corpo em bruto na framework web antes de o middleware de parsing o consumir, verificar o HMAC sobre esses bytes e só então fazer parse do JSON para uso. O charset também importa: assine e verifique a mesma sequência de octetos (UTF-8), não uma string descodificada.
raw = request.raw_body # exact bytes, pre-parse
ts = header('Webhook-Timestamp')
sig = header('Webhook-Signature')
if abs(now() - ts) > TOLERANCE: reject # replay window
expected = hmac_sha256(secret, ts + '.' + raw)
if not constant_time_equal(expected, sig): reject
accept(parse_json(raw))Proteção contra replay: tolerância de timestamp e nonce
Uma assinatura por si só não impede o replay — um pedido capturado mas válido poderia ser reenviado mais tarde. Dois mecanismos fecham essa falha. Primeiro, como o timestamp está dentro do conteúdo assinado, o recetor compara-o com o seu próprio relógio e rejeita tudo o que seja mais antigo do que uma tolerância (habitualmente alguns minutos, ampla o suficiente para absorver skew de relógio e atraso de retry, estreita o suficiente para limitar a janela de replay). Segundo, para garantias mais rigorosas, o recetor regista um nonce — o event id único — durante o período de tolerância e rejeita um segundo pedido que traga um id que já viu. A tolerância de timestamp limita a janela; a cache de nonce torna o replay dentro dessa janela ineficaz. Note que isto é complementar à idempotência do consumidor, que trata duplicados legítimos e não replays maliciosos.
| Controlo | Defende contra | Ajuste (Tuning) |
|---|---|---|
| Carimbo de data/hora no conteúdo assinado | Transferir uma assinatura para um novo momento | Deve ser assinado, não um simples cabeçalho |
| Janela de tolerância do timestamp | Repetição atrasada de uma solicitação capturada | Poucos minutos; equilibrar desvio de relógio vs. exposição |
| Cache de nonce / event-id | Replay dentro da janela de tolerância | Armazenar os IDs em cache por pelo menos o período de tolerância |
| Comparação em tempo constante | Canal lateral de temporização na assinatura | Sempre, independentemente do que foi mencionado acima |
Comparação em tempo constante
Comparar as assinaturas calculada e recebida com uma verificação de igualdade comum deixa escapar informação: uma comparação byte a byte devolve mais depressa numa discrepância precoce, e um atacante que consiga medir esse tempo pode recuperar a assinatura correta um byte de cada vez. A verificação tem portanto de usar uma comparação em tempo constante (independente do comprimento) — a comparação timing-safe da biblioteca padrão de cada plataforma — que examina sempre o comprimento completo antes de devolver. Isto é barato e não negociável; é a diferença entre um esquema de assinatura que é sólido e outro que é derrotável com pedidos cronometrados suficientes.
Rotação de segredos
Os segredos de assinatura têm de ser rotativos sem descartar webhooks legítimos a meio do voo. O padrão seguro são chaves sobrepostas: ao remetente é aprovisionado um novo segredo enquanto o antigo permanece válido e, durante a sobreposição, o remetente ou assina com ambos (emitindo duas assinaturas) ou o recetor aceita uma assinatura que corresponda a qualquer dos segredos. Assim que todos os produtores estiverem confirmados no novo segredo, o antigo é retirado. O prefixo de esquema na assinatura (v1, v2,) torna isto observável e permite que uma mudança de algoritmo versionada acompanhe. O validador de assinatura de webhook alojado pela BroadNet permite verificar um payload, timestamp e segredo contra um HMAC esperado sem escrever código; a mecânica da assinatura de DLR com HMAC-SHA256 é abordada na biblioteca de respostas.
Perguntas frequentes
Como é calculada uma assinatura de webhook?
O remetente constrói uma string canónica que liga o timestamp ao corpo em bruto (normalmente timestamp + '.' + body), calcula HMAC-SHA256 sobre ela com um segredo partilhado e coloca o resultado num header de assinatura junto ao timestamp. O recetor recalcula o mesmo HMAC e compara. Uma correspondência prova que o payload veio de um detentor do segredo e não foi alterado.
Porque tenho de verificar contra o corpo em bruto do pedido?
O HMAC é calculado sobre bytes exatos. Se fizer parse do JSON e o re-serializar, a ordem das chaves, o whitespace, a formatação de números e os escapes Unicode mudam, pelo que os bytes diferem e a assinatura não corresponde mesmo sem adulteração. Capture o corpo em bruto antes do middleware de parsing, verifique e depois faça o parse.
Como é que a tolerância de timestamp impede ataques de replay?
O timestamp faz parte do conteúdo assinado, pelo que não pode ser alterado sem quebrar a assinatura. O recetor compara-o com o seu próprio relógio e rejeita tudo o que seja mais antigo do que uma tolerância (habitualmente alguns minutos). Isso limita durante quanto tempo um pedido válido capturado pode ser repetido; uma cache de nonce fecha os replays dentro dessa janela.
Porque é obrigatória a comparação em tempo constante?
Uma verificação de igualdade comum devolve cedo no primeiro byte discrepante, e um atacante que consiga cronometrar a resposta pode recuperar a assinatura correta byte a byte. Uma comparação em tempo constante examina sempre o comprimento completo antes de devolver, removendo o canal lateral de tempo. Use a função de comparação timing-safe da plataforma.
Como roto um segredo de assinatura de webhook de forma segura?
Use chaves sobrepostas. Aprovisione o novo segredo enquanto o antigo permanece válido; durante a sobreposição o remetente assina com ambos ou o recetor aceita qualquer um, para que nenhum webhook legítimo seja descartado. Assim que todos os produtores estiverem confirmados no novo segredo, retire o antigo. Um prefixo de esquema na assinatura torna a transição observável.
O HTTPS não é suficiente para confiar num webhook?
Não. O TLS prova que o transporte foi cifrado e que o cliente chegou ao seu servidor, mas não que o pedido veio da plataforma de mensagens — qualquer pessoa pode fazer POST para um URL público. Só uma assinatura HMAC com segredo partilhado autentica o remetente e prova que o payload não foi modificado. O allowlisting de IP é um substituto fraco porque os IPs de saída mudam.
Especificações de origem
Os padrões publicados nos quais esta página se baseia. Abre num novo separador.