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; para tokens aleatórios, use secrets; para transportar o digest em texto, use base64.
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:
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.
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:
- só integridade do conteúdo →
hashlib; - integridade + origem com chave simétrica →
hmac; - 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 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
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:
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
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:
- o provedor calcula
HMAC-SHA256(chave, corpo_bruto); - envia o digest em um header (
X-Signature,X-Hub-Signature-256, etc.); - o seu backend lê o corpo bruto da requisição, recalcula e compara.
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()):
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.dumpsreordena 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.
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 + "." + bodypara evitar replay.
Se o provedor assina timestamp + "." + body, o seu mensagem precisa ser exatamente essa concatenação — não só o JSON.
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:
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:
- validar formato do timestamp;
- rejeitar se estiver fora da janela (ex.: 5 minutos);
- só então validar o HMAC da mensagem canônica;
- opcionalmente gravar o
event_idprocessado 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 e o guia de conciliação financeira.
Chaves secretas: geração, armazenamento e rotação
Gere com secrets, não com uuid “bonito”
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 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:
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 |
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 detalha essa separação.
Erros comuns (e como detectar rápido)
1. Assinar o dict Python em vez dos bytes do HTTP
# 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 ==
# Evite
if esperada == recebida:
...
Use hmac.compare_digest(esperada, recebida).
3. Misturar str e bytes
# 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:
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:
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
secretspode bastar se não houver corpo a assinar — mas para webhooks com efeito colateral, assine o body.
Mini checklist de produção
- Chave longa gerada com
secrets, fora do repositório. - Algoritmo SHA-256 ou superior, alinhado ao provedor.
- Assinatura sobre bytes brutos (+ timestamp se o contrato exigir).
- Verificação com
hmac.compare_digest. - Janela de tempo anti-replay + idempotência por
event_id. - HTTPS obrigatório no endpoint.
- Rotação de chave com período de sobreposição.
- Logs sem vazar segredo nem digest completo.
- Testes cobrindo corpo válido, adulterado e assinatura truncada.
- Documentação interna do header e do encoding (hex/Base64).
Relacionados no Python Brasil
hashlib: checksums e integridadesecrets: tokens e senhas segurosbase64: codificar digests e bináriosuuid: identificadores de evento- Webhooks com FastAPI e CRM
- Segurança em aplicações Python
- httpx com timeouts e retries
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 para fingerprints e com secrets para gerar o material da chave. E quando a automação mexer em valores financeiros, feche o ciclo com boas práticas de decimal e conciliação.
Quer seguir no cluster de stdlib segura? Leia em seguida o guia de tempfile para artefatos temporários e o de shlex se a automação chama processos externos.
Equipe Python Brasil
Contribuidor do Python Brasil — Aprenda Python em Português