BroadNet
HTTP API & webhooksSegurança

Verificação de assinatura de webhook: HMAC-SHA256 e proteção contra replay

Atualizado em 6 min de leituraBaseado em padrões
Resposta direta

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).

§01

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.

§02

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.

Conteúdo assinado e layout dos headerstext
signed_content = timestamp + '.' + raw_body
signature      = HMAC_SHA256(secret, signed_content)

Webhook-Timestamp: 1752916522
Webhook-Signature: v1,3f2b8c9a1d...   # hex or base64, scheme-prefixed
§03

Verificar 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.

Verificar (pseudocódigo) — corpo em bruto, tempo constantetext
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))
§04

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.

Controlos de proteção contra replay
ControloDefende contraAjuste (Tuning)
Carimbo de data/hora no conteúdo assinadoTransferir uma assinatura para um novo momentoDeve ser assinado, não um simples cabeçalho
Janela de tolerância do timestampRepetição atrasada de uma solicitação capturadaPoucos minutos; equilibrar desvio de relógio vs. exposição
Cache de nonce / event-idReplay dentro da janela de tolerânciaArmazenar os IDs em cache por pelo menos o período de tolerância
Comparação em tempo constanteCanal lateral de temporização na assinaturaSempre, independentemente do que foi mencionado acima
§05

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.

§06

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

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.

Referências

Especificações de origem

Os padrões publicados nos quais esta página se baseia. Abre num novo separador.

BNMantido pela equipa de engenharia BroadNet·Mensagens de nível de operadora desde 2003
Coloque-o no fio.

Construa com uma vinculação SMPP real e uma API REST.

Solicite acesso de teste com chaves de teste reais: envie primeiro para o seu próprio número verificado e, em seguida, passe para o ambiente de produção em mais de 200 países assim que a sua identidade de remetente e rotas forem aprovadas.