---
title: "hmac em Python: assinar webhooks e mensagens com chave secreta"
url: "https://python.dev.br/blog/python-hmac-assinatura-webhooks/"
markdown_url: "https://python.dev.br/blog/python-hmac-assinatura-webhooks.MD"
description: "Aprenda hmac em Python para assinar webhooks, comparar digests com segurança, montar headers de API e evitar erros comuns de encoding, timing e chave fraca."
date: "2026-08-12"
author: "Equipe Python Brasil"
---

# hmac em Python: assinar webhooks e mensagens com chave secreta

Aprenda hmac em Python para assinar webhooks, comparar digests com segurança, montar headers de API e evitar erros comuns de encoding, timing e chave fraca.


O módulo `hmac` é a peça da biblioteca padrão para **autenticar mensagens com uma chave secreta em Python**. Em português claro: ele responde se este corpo de webhook (ou header de API) ainda é o mesmo que o emissor assinou — e se o emissor conhece o segredo compartilhado.

A recomendação direta é: assine com `hmac.new(chave, mensagem, hashlib.sha256)`; envie o digest em hex ou Base64 conforme o contrato da API; valide sempre com `hmac.compare_digest()`; trate chave e corpo como `bytes`; e nunca confunda HMAC com criptografia. Para checksums sem chave, use [`hashlib`](/blog/python-hashlib-checksums-integridade/); para tokens aleatórios, use [`secrets`](/blog/python-secrets-tokens-senhas-seguros/); para transportar o digest em texto, use [`base64`](/blog/python-base64-codificar-decodificar/).

Neste guia, você vai assinar e verificar mensagens, montar um validador de webhook reutilizável, evitar ataques de timing e encoding, escolher o algoritmo certo e encaixar HMAC em fluxos reais de backend brasileiro — de PSP de pagamento a filas internas e automações.

## O problema que o HMAC resolve

Imagine um endpoint que recebe eventos de um provedor:

```http
POST /webhooks/pagamentos HTTP/1.1
Content-Type: application/json
X-Signature: 3f1c...

{"pedido":"1042","status":"pago","valor":199.90}
```

Se o endpoint confiar no JSON sem autenticação, qualquer pessoa na internet pode forjar um `"status":"pago"`. Um hash SHA-256 do corpo **sem chave** também não basta: o atacante recalcula o hash do corpo adulterado e envia os dois juntos.

HMAC resolve isso com um segredo que só o emissor e o receptor conhecem. A assinatura depende do corpo **e** da chave. Sem a chave, forjar um digest válido fica computacionalmente inviável para algoritmos modernos.

```python
import hashlib
import hmac

chave = b"segredo-compartilhado-do-provedor"
corpo = b'{"pedido":"1042","status":"pago"}'

assinatura = hmac.new(chave, corpo, hashlib.sha256).hexdigest()
print(assinatura)
```

Quem recebe o webhook recalcula o mesmo valor e compara de forma segura. Se bater, o corpo não foi alterado no caminho **e** quem assinou tinha a chave.

## hashlib vs hmac: quando usar cada um

| Necessidade | Ferramenta | Exemplo |
| --- | --- | --- |
| Fingerprint de arquivo ou conteúdo | `hashlib` | SHA-256 de um PDF baixado |
| Detectar alteração sem autenticar origem | `hashlib` | deduplicar blobs no storage |
| Validar webhook / API com segredo compartilhado | `hmac` | Stripe, GitHub, PSP, CRM |
| Token opaco aleatório | `secrets` | reset de senha, API key |
| Transportar bytes em JSON/URL | `base64` | embutir digest ou certificado |

Regra prática:

1. **só integridade do conteúdo** → `hashlib`;
2. **integridade + origem com chave simétrica** → `hmac`;
3. **identidade de usuário / OAuth / chaves assimétricas** → bibliotecas de JWT, OpenID ou assinatura RSA/ECDSA — HMAC sozinho não substitui isso.

O post de [`hashlib`](/blog/python-hashlib-checksums-integridade/) mostra o checksum e menciona HMAC de passagem. Aqui o foco é o contrato completo de assinatura.

## API essencial do módulo hmac

### Criar e ler o digest

```python
import hashlib
import hmac

chave = b"chave-secreta"
mensagem = b"corpo-exato-dos-bytes-recebidos"

digest = hmac.new(chave, mensagem, hashlib.sha256)

print(digest.hexdigest())   # string hex de 64 chars
print(digest.digest())      # 32 bytes brutos
print(digest.name)          # 'hmac-sha256' em builds recentes / dependente da versão
```

Você também pode alimentar a mensagem em partes — útil para streams e arquivos grandes:

```python
import hashlib
import hmac

h = hmac.new(b"chave", digestmod=hashlib.sha256)
h.update(b'{"pedido":')
h.update(b'"1042"}')
print(h.hexdigest())
```

O resultado é idêntico a passar a mensagem inteira de uma vez, desde que a ordem e os bytes sejam os mesmos.

### Comparar com segurança

```python
import hmac

def assinatura_valida(esperada: str, recebida: str) -> bool:
    return hmac.compare_digest(esperada, recebida)
```

Prefira sempre `compare_digest` a `==` em assinaturas, tokens e MACs. As duas strings precisam ter o **mesmo tipo** (`str` com `str`, ou `bytes` com `bytes`) e, em geral, o mesmo comprimento lógico; se os formatos divergirem (hex vs Base64), normalize antes de comparar.

## Assinar e verificar um webhook do zero

O padrão mais comum em APIs:

1. o provedor calcula `HMAC-SHA256(chave, corpo_bruto)`;
2. envia o digest em um header (`X-Signature`, `X-Hub-Signature-256`, etc.);
3. o seu backend lê o **corpo bruto** da requisição, recalcula e compara.

```python
import hashlib
import hmac
from typing import Final

ALGORITMO: Final = hashlib.sha256


def assinar(corpo: bytes, chave: bytes) -> str:
    return hmac.new(chave, corpo, ALGORITMO).hexdigest()


def verificar(corpo: bytes, chave: bytes, assinatura_recebida: str) -> bool:
    # Aceita prefixos comuns como "sha256="
    recebida = assinatura_recebida.removeprefix("sha256=").strip()
    esperada = assinar(corpo, chave)
    return hmac.compare_digest(esperada, recebida)
```

Exemplo de uso em um handler (FastAPI / Starlette leem o body bruto com `await request.body()`):

```python
from fastapi import FastAPI, Header, HTTPException, Request
import os

app = FastAPI()
CHAVE = os.environ["WEBHOOK_SECRET"].encode("utf-8")


@app.post("/webhooks/pagamentos")
async def webhooks_pagamentos(
    request: Request,
    x_signature: str = Header(alias="X-Signature"),
):
    corpo = await request.body()
    if not verificar(corpo, CHAVE, x_signature):
        raise HTTPException(status_code=401, detail="assinatura inválida")

    # Só depois de validar: parse do JSON
    evento = await request.json()
    return {"ok": True, "pedido": evento.get("pedido")}
```

Pontos que quebram integração na prática:

- **parsear JSON antes de assinar** — `json.dumps` reordena chaves, muda espaços e quebra o digest;
- **decodificar como texto e reencodar** com outro encoding;
- **ler `request.json()` e tentar reassinar o dict** — use sempre os bytes originais do socket;
- **comparar hex com Base64** sem converter.

## Hex ou Base64: siga o contrato do provedor

Alguns provedores enviam hex; outros, Base64; outros ainda prefixam o algoritmo.

```python
import base64
import hashlib
import hmac

chave = b"segredo"
corpo = b'{"ok":true}'

bruto = hmac.new(chave, corpo, hashlib.sha256).digest()
em_hex = bruto.hex()
em_b64 = base64.b64encode(bruto).decode("ascii")

print(em_hex)
print(em_b64)
```

Antes de codificar a verificação, leia a documentação do emissor:

- GitHub: header `X-Hub-Signature-256: sha256=<hex>`;
- muitos PSPs brasileiros: hex ou Base64 do HMAC-SHA256/SHA512 do body;
- alguns CRMs: HMAC sobre `timestamp + "." + body` para evitar replay.

Se o provedor assina `timestamp + "." + body`, o seu `mensagem` precisa ser **exatamente** essa concatenação — não só o JSON.

```python
def assinar_com_timestamp(chave: bytes, timestamp: str, corpo: bytes) -> str:
    mensagem = timestamp.encode("utf-8") + b"." + corpo
    return hmac.new(chave, mensagem, hashlib.sha256).hexdigest()
```

## Proteção contra replay com timestamp

HMAC prova autenticidade do corpo, mas **não impede reenvio** da mesma requisição válida minutos depois. Combine com janela de tempo:

```python
import time


def webhook_fresco(timestamp_header: str, tolerancia_segundos: int = 300) -> bool:
    try:
        ts = int(timestamp_header)
    except (TypeError, ValueError):
        return False
    agora = int(time.time())
    return abs(agora - ts) <= tolerancia_segundos
```

Fluxo recomendado:

1. validar formato do timestamp;
2. rejeitar se estiver fora da janela (ex.: 5 minutos);
3. só então validar o HMAC da mensagem canônica;
4. opcionalmente gravar o `event_id` processado para idempotência.

Isso importa em cobranças, conciliação e qualquer efeito colateral financeiro — temas frequentes em automações Python no Brasil. Para conciliação e dinheiro com precisão decimal, veja também [`decimal`](/blog/python-decimal-dinheiro-calculos-precisos/) e o guia de [`conciliação financeira`](/blog/conciliacao-financeira-python/).

## Chaves secretas: geração, armazenamento e rotação

### Gere com secrets, não com uuid “bonito”

```python
import secrets

# 32 bytes → 64 chars hex; adequado para HMAC-SHA256
chave_hex = secrets.token_hex(32)
chave_bytes = bytes.fromhex(chave_hex)
```

Evite senhas curtas, nomes de empresa, CNPJ ou slugs de projeto como chave de webhook. Trate a chave como **credencial de produção**: variável de ambiente, secret manager ou cofre — nunca commit no Git. O padrão com [`.env` e dotenv](/blog/python-dotenv-env-vars-config/) serve para desenvolvimento local; em produção, prefira o segredo injetado pelo orquestrador.

### Rotação sem downtime

Muitos times mantêm duas chaves durante a troca:

```python
def verificar_com_rotacao(
    corpo: bytes,
    assinatura: str,
    chave_atual: bytes,
    chave_anterior: bytes | None = None,
) -> bool:
    if verificar(corpo, chave_atual, assinatura):
        return True
    if chave_anterior is not None and verificar(corpo, chave_anterior, assinatura):
        return True
    return False
```

Publique a chave nova no provedor, aceite as duas por um período, depois remova a antiga.

## Algoritmos: SHA-256, SHA-512 e o que evitar

| Algoritmo | Uso recomendado |
| --- | --- |
| HMAC-SHA256 | padrão seguro e amplamente suportado |
| HMAC-SHA512 | quando o contrato exige ou há alinhamento com políticas internas |
| HMAC-SHA1 | legado; só se o provedor antigo não oferecer alternativa |
| HMAC-MD5 | não use em segurança |

```python
import hashlib
import hmac

hmac.new(b"k", b"m", hashlib.sha256)
hmac.new(b"k", b"m", hashlib.sha512)
```

Para senhas de usuário, **não** use HMAC sozinho como “hash de senha”. Prefira Argon2id, scrypt ou bcrypt. `hashlib.pbkdf2_hmac` existe na stdlib, mas bibliotecas modernas de senha são preferíveis em apps novos — o post de [`hashlib`](/blog/python-hashlib-checksums-integridade/) detalha essa separação.

## Erros comuns (e como detectar rápido)

### 1. Assinar o dict Python em vez dos bytes do HTTP

```python
# Errado: ordem de chaves e espaços diferem do body original
import json, hashlib, hmac
hmac.new(chave, json.dumps(payload).encode(), hashlib.sha256)
```

Correto: assinar `await request.body()` / `request.get_data()` / o buffer bruto do framework.

### 2. Comparar com `==`

```python
# Evite
if esperada == recebida:
    ...
```

Use `hmac.compare_digest(esperada, recebida)`.

### 3. Misturar `str` e `bytes`

```python
# TypeError ou resultado errado conforme a versão/caminho
hmac.new("chave", "mensagem", hashlib.sha256)  # em 3.x a chave/msg devem ser bytes-like
```

Normalize:

```python
def as_bytes(valor: str | bytes) -> bytes:
    return valor if isinstance(valor, bytes) else valor.encode("utf-8")
```

### 4. Logar a chave ou a assinatura completa em texto claro

Registre apenas se a verificação passou/falhou, o `event_id` e um prefixo curto do digest se precisar depurar. Assinaturas e chaves em log viram incidente.

### 5. Esquecer o prefixo `sha256=`

Vários provedores enviam `sha256=abc123...`. Faça `removeprefix` (Python 3.9+) ou `split("=", 1)` antes de comparar.

### 6. Achar que HMAC substitui HTTPS

HMAC sem TLS ainda expõe o corpo e pode vazar metadados. Em produção, **HTTPS + HMAC** é o combo mínimo para webhooks públicos.

## Testes unitários com pytest

Teste o contrato, não a rede:

```python
import hashlib
import hmac

from meuapp.webhooks import assinar, verificar

def test_assinatura_estavel():
    chave = b"teste"
    corpo = b'{"a":1}'
    sig = assinar(corpo, chave)
    assert verificar(corpo, chave, sig)
    assert verificar(corpo, chave, f"sha256={sig}")


def test_corpo_adulterado_falha():
    chave = b"teste"
    corpo = b'{"a":1}'
    sig = assinar(corpo, chave)
    assert not verificar(b'{"a":2}', chave, sig)


def test_compare_digest_tipos_iguais():
    a = hmac.new(b"k", b"m", hashlib.sha256).hexdigest()
    b = a
    assert hmac.compare_digest(a, b)
```

Para testes de handler HTTP, injete a chave por fixture e envie o header calculado com a mesma função de produção — evita duplicar a lógica “de teste” com uma implementação divergente.

## Quando HMAC não é a ferramenta certa

- **Segredo por usuário final no browser**: não coloque a chave HMAC no frontend; qualquer visitante leria o segredo.
- **Prova pública verificável por terceiros**: use assinatura assimétrica (par de chaves), não HMAC simétrico.
- **Criptografar campo sensível no banco**: use envelope encryption / libs de crypto; HMAC só autentica.
- **API key simples de identificação**: uma API key opaca com [`secrets`](/blog/python-secrets-tokens-senhas-seguros/) pode bastar se não houver corpo a assinar — mas para webhooks com efeito colateral, assine o body.

## Mini checklist de produção

1. Chave longa gerada com `secrets`, fora do repositório.
2. Algoritmo SHA-256 ou superior, alinhado ao provedor.
3. Assinatura sobre **bytes brutos** (+ timestamp se o contrato exigir).
4. Verificação com `hmac.compare_digest`.
5. Janela de tempo anti-replay + idempotência por `event_id`.
6. HTTPS obrigatório no endpoint.
7. Rotação de chave com período de sobreposição.
8. Logs sem vazar segredo nem digest completo.
9. Testes cobrindo corpo válido, adulterado e assinatura truncada.
10. Documentação interna do header e do encoding (hex/Base64).

## Relacionados no Python Brasil

- [`hashlib`: checksums e integridade](/blog/python-hashlib-checksums-integridade/)
- [`secrets`: tokens e senhas seguros](/blog/python-secrets-tokens-senhas-seguros/)
- [`base64`: codificar digests e binários](/blog/python-base64-codificar-decodificar/)
- [`uuid`: identificadores de evento](/blog/python-uuid-identificadores-unicos/)
- [Webhooks com FastAPI e CRM](/guias/webhooks-fastapi-crm/)
- [Segurança em aplicações Python](/blog/seguranca-em-aplicacoes-python/)
- [httpx com timeouts e retries](/blog/httpx-timeouts-retries-python/)

## Conclusão

`hmac` é pequeno, está na stdlib e resolve um problema caro: **confiar em eventos externos sem abrir a porta para forjadores**. O fluxo correto é simples de lembrar — chave em bytes, corpo bruto, SHA-256, `compare_digest`, timestamp e HTTPS — e difícil de improvisar no calor de um bug de produção.

Se o seu time integra PSP, ERP, CRM ou filas internas em Python, um helper de assinar/verificar com testes e rotação de chave elimina uma classe inteira de incidentes. Combine com [`hashlib`](/blog/python-hashlib-checksums-integridade/) para fingerprints e com [`secrets`](/blog/python-secrets-tokens-senhas-seguros/) para gerar o material da chave. E quando a automação mexer em valores financeiros, feche o ciclo com boas práticas de [`decimal`](/blog/python-decimal-dinheiro-calculos-precisos/) e conciliação.

Quer seguir no cluster de stdlib segura? Leia em seguida o guia de [`tempfile`](/blog/python-tempfile-arquivos-temporarios/) para artefatos temporários e o de [`shlex`](/blog/python-shlex-comandos-shell-seguros/) se a automação chama processos externos.
