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

- Endereço oficial: https://pradev.com.br/guias/como-validar-assinatura-de-webhook-hmac
- Tipo: Guia
- Publicado em: 2026-09-30
- Atualizado em: 2026-09-30
- Autor: Leonardo Gaertner (https://pradev.com.br/sobre)
- Ferramentas: [Gerador de HMAC](https://pradev.com.br/gerador-de-hmac), [Gerador de SHA256](https://pradev.com.br/gerador-de-sha256), [Codificar Base64](https://pradev.com.br/codificar-base64)

## 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](https://pradev.com.br/gerador-de-sha256) 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](https://pradev.com.br/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

```javascript
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);
}
```

_JavaScript_

No Express, use `express.raw` para receber o corpo original e chame a função antes de tratar o evento:

```javascript
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);
});
```

_JavaScript_

## Verificar em Python

Em Python, `hmac.compare_digest` faz a comparação em tempo constante:

```python
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())
```

_Python_

## Verificar em PHP

Em PHP, use `hash_hmac` para calcular e `hash_equals` para comparar:

```php
<?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 ?? '');
}
```

_PHP_

## 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](https://pradev.com.br/codificar-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](https://pradev.com.br/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.

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

## Fontes

- [RFC 2104: HMAC](https://datatracker.ietf.org/doc/html/rfc2104)
- [GitHub: validando entregas de webhook](https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries)

## Guias relacionados

- [Como decodificar e verificar um JWT em JavaScript e Python](https://pradev.com.br/guias/como-decodificar-e-verificar-jwt): Entenda a diferença entre decodificar e verificar um JWT e veja código testado em JavaScript e Python para ler o token e checar a assinatura HS256.
