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

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

- URL canônico: https://www.broadnet.me/pt/reference/webhook-signature-verification
- Idioma: pt
- Última atualização: 2026-07-19
- Editor: BroadNet (https://www.broadnet.me)

Como verificar que um webhook é autêntico e não adulterado: HMAC-SHA256 sobre o corpo em bruto, os headers de assinatura e timestamp, proteção contra replay com uma tolerância de timestamp e um nonce, comparação em tempo constante para derrotar timing attacks e rotação segura de segredos com chaves sobrepostas.

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

*Conteúdo assinado e layout dos headers*

```text
signed_content = timestamp + '.' + raw_body
signature      = HMAC_SHA256(secret, signed_content)

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

## 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 constante*

```text
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.

*Controlos de proteção contra replay*

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

## Fontes

- [IETF RFC 2104 — HMAC: Keyed-Hashing for Message Authentication](https://datatracker.ietf.org/doc/html/rfc2104)
- [IETF RFC 6234 — US Secure Hash Algorithms (SHA and HMAC-SHA)](https://datatracker.ietf.org/doc/html/rfc6234)

## Relacionados

- [Entrega e fiabilidade de webhooks](https://www.broadnet.me/pt/reference/webhook-delivery-and-reliability)
- [Estado de mensagem e webhooks de entrada](https://www.broadnet.me/pt/reference/message-status-and-inbound-webhooks)
- [Autenticação de API e assinatura](https://www.broadnet.me/pt/reference/api-authentication-and-signing)
- [Validador de assinatura de webhook](https://www.broadnet.me/pt/tools/webhook-signature-validator)
- [Assinatura de DLR com HMAC-SHA256](https://www.broadnet.me/pt/answers/hmac-sha256-dlr-signing)
