Pular para o conteúdo
10 min de leitura

Assinatura HMAC de webhook: como verificar que o evento veio mesmo da plataforma

Por Equipe Owiew ·

O que é a assinatura HMAC de um webhook, como validá-la corretamente com comparação em tempo constante e por que pular essa checagem abre a porta para conversão forjada.

Neste artigo

Um endpoint de webhook é uma porta aberta na internet. Ele existe justamente para receber POSTs de fora — do Shopify, da Meta, de um gateway de pagamento — e qualquer um que descubra a URL pode mandar um POST para ela. Se o seu servidor confia cegamente em tudo que chega, um atacante (ou um bot varrendo endpoints) consegue forjar uma requisição que parece um pedido pago, e a sua camada de conversão dispara um Purchase para uma venda que nunca aconteceu. O relatório mostra faturamento que não existe, a plataforma otimiza para um sinal envenenado, e a causa é a ausência de uma checagem de três linhas: a verificação da assinatura.

A assinatura HMAC é o mecanismo que responde à pergunta que todo endpoint de webhook deveria fazer antes de processar qualquer coisa: este POST veio mesmo de quem diz ter vindo, e chegou intacto? Sem essa resposta, o endpoint aceita conversão forjada como se fosse legítima. Com ela, um payload adulterado ou de origem desconhecida é rejeitado antes de tocar a lógica de negócio.

O que é HMAC#

HMAC significa Hash-based Message Authentication Code — código de autenticação de mensagem baseado em hash. A ideia é combinar duas coisas para produzir uma assinatura:

  1. O corpo da mensagem (o payload do webhook).
  2. Um segredo compartilhado — uma chave que só você e a plataforma conhecem.

A plataforma calcula HMAC(segredo, corpo) e envia o resultado num cabeçalho da requisição (por exemplo, X-Hub-Signature-256 na Meta, X-Shopify-Hmac-Sha256 no Shopify). Você, do seu lado, recalcula HMAC(segredo, corpo) com o mesmo segredo e o mesmo corpo, e compara com a assinatura recebida. Se batem, duas coisas ficam provadas ao mesmo tempo:

  • Autenticidade — só quem tem o segredo consegue produzir uma assinatura válida, então a mensagem veio de quem detém o segredo (a plataforma).
  • Integridade — se um único byte do corpo tivesse sido alterado no caminho, o HMAC recalculado não bateria. A assinatura garante que o payload chegou exatamente como saiu.

O ponto crucial é que o segredo nunca trafega. Ele não vai na requisição; vai só o resultado do cálculo. Um atacante que intercepte o tráfego vê o payload e a assinatura, mas não consegue gerar uma assinatura válida para um payload adulterado, porque não conhece o segredo que entra no cálculo.

Por que um hash simples não basta#

Uma dúvida natural: por que não mandar simplesmente SHA-256(corpo)? Porque um hash puro não prova origem. Qualquer um pode calcular o SHA-256 de qualquer corpo — não há segredo envolvido. Um atacante forjaria o payload e o hash correspondente, e os dois bateriam. O hash puro garante integridade contra corrupção acidental, não contra falsificação intencional.

O HMAC resolve isso justamente por misturar o segredo no cálculo. Sem conhecer o segredo, é inviável produzir a assinatura correta para um corpo escolhido pelo atacante. É a presença da chave secreta que transforma "este corpo tem este hash" em "este corpo veio de quem tem a chave".

Verificar corretamente: os detalhes que quebram tudo#

A verificação parece trivial — recalcular e comparar —, mas três detalhes fazem implementações inteiras falharem em silêncio, aceitando o que deveriam rejeitar ou rejeitando o que deveriam aceitar.

1. Assine o corpo <strong>cru</strong> (raw body)#

O HMAC é calculado sobre os bytes exatos do corpo, como a plataforma os enviou. Se o seu framework faz o parse do JSON e você recalcula o HMAC sobre o objeto re-serializado, os bytes mudam — reordenação de chaves, espaçamento, escape de caracteres — e a assinatura nunca bate, mesmo para requisições legítimas. É preciso capturar o corpo bruto antes de qualquer parse e calcular o HMAC sobre ele. Esse é o erro número um, e ele se manifesta como "a verificação rejeita tudo".

2. Compare em <strong>tempo constante</strong>#

A comparação entre a assinatura recebida e a recalculada não pode ser o == comum de strings. A comparação ingênua para no primeiro byte diferente, e o tempo que ela leva vaza informação: uma assinatura que erra no primeiro caractere é rejeitada mais rápido que uma que acerta os dez primeiros. Um atacante meticuloso mede esses microssegundos e reconstrói a assinatura byte a byte — é o timing attack. A defesa é usar uma função de comparação em tempo constante, que sempre percorre o valor inteiro e leva o mesmo tempo independentemente de onde está a diferença. Em muitas linguagens ela vem pronta: hmac.compare_digest em Python, crypto.timingSafeEqual em Node, hmac.Equal em Go. Use a da biblioteca; não invente a sua.

```python import hmac, hashlib

def webhook_valido(corpo_cru: bytes, assinatura_recebida: str, segredo: bytes) -> bool: esperado = hmac.new(segredo, corpo_cru, hashlib.sha256).hexdigest() return hmac.compare_digest(esperado, assinatura_recebida) ```

Repare em dois pontos do exemplo: o corpo_cru são bytes, não um dicionário; e a comparação é compare_digest, não ==.

3. Cuide da codificação da assinatura#

Plataformas diferentes codificam a assinatura de formas diferentes: hexadecimal, Base64, com ou sem um prefixo como sha256=. Comparar um hex contra um Base64 nunca bate, ainda que o conteúdo seja o mesmo. Leia a documentação da plataforma específica e normalize os dois lados para o mesmo formato antes de comparar.

O que a assinatura protege — e o que não protege#

HMAC prova autenticidade e integridade. É importante saber o que ele não resolve sozinho, para não criar falsa sensação de segurança.

Ele não protege contra replay: um atacante que capture uma requisição legítima válida — assinatura e tudo — pode reenviá-la, e ela vai passar na verificação, porque é genuinamente autêntica. A defesa contra replay é ortogonal e vem de duas frentes: um timestamp assinado junto (rejeite requisições velhas, o que várias plataformas oferecem incluindo o instante no cálculo do HMAC) e a idempotência do consumidor (mesmo que a requisição repetida passe, processá-la de novo não gera conversão nova, porque o event_id/order id já foi tratado). A assinatura garante que o evento é verdadeiro; a idempotência garante que ele conte uma vez só.

Higiene do segredo#

Toda a segurança do HMAC repousa sobre o sigilo do segredo. Consequentemente:

  • O segredo é credencial de servidor. Nunca aparece em código versionado, em bundle de frontend, em variável NEXT_PUBLIC_/VITE_, em log. Vive num gerenciador de segredos ou variável de ambiente do servidor.
  • Deve ser rotacionável. Se vazar, você troca; um esquema que suporta dois segredos válidos durante a transição evita janela de indisponibilidade na rotação.
  • É por integração. Segredos distintos para plataformas distintas limitam o estrago de um vazamento a uma única fonte.

O fluxo de rejeição#

Na prática, a verificação é a primeira coisa que o endpoint faz, antes de qualquer lógica:

  1. Captura o corpo cru da requisição.
  2. Lê o cabeçalho de assinatura e o normaliza para o formato esperado.
  3. Recalcula o HMAC com o segredo da integração.
  4. Compara em tempo constante.
  5. Não bateu: responde 401/403, registra a tentativa e não processa nada.
  6. Bateu: segue para enfileirar e processar o evento.

Uma requisição que falha a verificação não é um erro do sistema — é exatamente o mecanismo funcionando, barrando um POST que não deveria estar ali.

Vale reforçar a ordem: a verificação vem antes de qualquer enfileiramento, parse de negócio ou consulta ao banco. Um endpoint que primeiro processa e depois verifica já gastou recurso, já tocou o estado, e transformou a assinatura em teatro. A verificação só protege se for a primeira coisa que acontece — a cancela na entrada, não um carimbo no fim.

Erros comuns que anulam a verificação#

  • Recalcular o HMAC sobre o corpo já parseado — os bytes mudam, e a verificação rejeita todas as requisições legítimas.
  • Comparar com == — abre a porta para timing attack; use sempre a comparação em tempo constante da biblioteca.
  • Confundir hex com Base64 — comparar codificações diferentes nunca bate, mesmo com o conteúdo correto.
  • Deixar o segredo vazar — em log, em bundle, em código versionado; qualquer vazamento derruba toda a garantia.
  • Verificar depois de processar — a checagem tem de vir primeiro, ou não protege nada.
  • Esquecer o replay — assinatura válida não impede reenvio; a idempotência (e o timestamp assinado) é que fecham essa brecha.

Rotação de segredo sem derrubar a coleta#

Segredos vazam, expiram e precisam ser trocados periodicamente por higiene. O problema é que a troca ingênua — apagar o segredo antigo e cadastrar o novo — cria uma janela em que nada casa: durante o intervalo entre você trocar o segredo do seu lado e a plataforma passar a assinar com o novo, todas as requisições legítimas falham na verificação e as conversões se perdem. Rotacionar sem downtime exige aceitar dois segredos válidos ao mesmo tempo durante a transição.

O mecanismo é simples: a verificação tenta o segredo novo e, se não bater, tenta o antigo; enquanto os dois forem aceitos, a troca acontece sem furo. Um roteiro seguro:

  1. Cadastre o segredo novo mantendo o antigo ainda válido na sua verificação.
  2. Atualize a configuração da plataforma para assinar com o novo.
  3. Confirme, pelo monitoramento, que as requisições passam a bater com o novo.
  4. Só então remova o antigo da lista de aceitos.

Note que a comparação em tempo constante continua valendo para os dois segredos — tentar o antigo depois do novo não pode abrir um canal de timing. E a decisão "aceito ou rejeito" continua sendo binária no fim: bastou um dos segredos válidos bater.

Registrar as tentativas rejeitadas#

Um endpoint que apenas responde 401 e segue em frente joga fora informação valiosa. Toda requisição que falha a verificação merece registro — não o payload inteiro (que pode ser lixo ou ataque), mas o suficiente para entender o que está acontecendo: origem, horário, motivo da rejeição (assinatura ausente, formato errado, HMAC divergente).

Esse registro tem dois usos. O primeiro é operacional: um pico súbito de rejeições legítimas quase sempre significa que algo quebrou do seu lado — o segredo foi trocado sem coordenação, o corpo cru parou de ser capturado por causa de um middleware novo, a codificação da assinatura mudou com uma atualização da plataforma. Sem o registro, essa quebra aparece só semanas depois, como um buraco inexplicável na conversão. O segundo uso é de segurança: uma enxurrada de POSTs com assinatura inválida vindos de origens estranhas é sinal de que alguém está sondando o endpoint, e merece atenção. O que você não mede, não defende.

Verificar a assinatura HMAC é a fronteira entre um endpoint de webhook que é uma porta trancada e um que é uma porta aberta com um aviso de "confio em qualquer um". São poucas linhas de código, mas cada uma tem um detalhe que a torna certa ou inútil: o corpo cru, a comparação em tempo constante, a codificação correta. Feita direito, ela garante que cada conversão que a sua camada processa veio mesmo da plataforma que diz tê-la enviado — e que ninguém injetou faturamento fantasma no seu relatório com um curl bem colocado.

Leituras relacionadas

Nenhum comentário ainda

Seja o primeiro a comentar.

Deixe seu comentário

Entre com sua conta Canverly para comentar. Você pode usar a mesma conta em qualquer site da rede.

Entrar com Canverly