Pular para o conteúdo principal

Verificar Signatures HMAC

Todo webhook do DVS é assinado com HMAC-SHA256 usando um secret fornecido no onboarding. Sempre verifique a signature antes de processar — caso esse passo seja ignorado, um atacante que conheça a URL do endpoint pode enviar eventos falsos.

O algoritmo

O DVS calcula:

payload_to_sign = timestamp + "." + raw_body
signature = hmac_sha256_hex(secret, payload_to_sign)

E envia dois headers:

X-DVS-Signature: t=1748884800,v1=a8d3e8b1c2...
X-DVS-Signature-Timestamp: 1748884800

O que deve ser feito:

  1. Extrair timestamp de X-DVS-Signature-Timestamp.
  2. Construir o payload esperado: timestamp + "." + raw_request_body.
  3. Calcular hmac_sha256_hex(seu_secret, payload).
  4. Comparar com a parte v1=... de X-DVS-Signature usando comparação de tempo constante.
  5. Rejeitar se |agora - timestamp| > 300 segundos (anti-replay).

Regras críticas

aviso

Use o raw request body, não o JSON parseado. A signature cobre exatamente os bytes que o DVS enviou. Se houver parse do JSON e re-serialização, espaços em branco e ordem das chaves mudam e a signature não confere. Configure o framework para fornecer raw bytes (express.raw(), FastAPI await request.body(), etc.).

aviso

Use comparação de tempo constante. Utilize hmac.compare_digest (Python), crypto.timingSafeEqual (Node), hash_equals (PHP), MessageDigest.isEqual (Java). Uma comparação ingênua com == é vulnerável a timing attacks.

aviso

Sincronize o clock do servidor com NTP. A janela anti-replay de ±5 minutos pressupõe tempo preciso. Clocks com drift rejeitarão eventos legítimos.

Implementações

import hmac
import hashlib
import time
from fastapi import FastAPI, Request, HTTPException, Header

DVS_WEBHOOK_SECRET = os.environ["DVS_WEBHOOK_SECRET"]
SIGNATURE_TOLERANCE_SECONDS = 300

app = FastAPI()


def verify_dvs_signature(
raw_body: bytes,
signature_header: str | None,
timestamp_header: str | None,
secret: str,
) -> bool:
if not signature_header or not timestamp_header:
return False

# 1) Janela anti-replay
try:
ts = int(timestamp_header)
except (TypeError, ValueError):
return False
if abs(time.time() - ts) > SIGNATURE_TOLERANCE_SECONDS:
return False

# 2) Parse do header de signature (formato: "t=<ts>,v1=<hex>")
parts = {k: v for k, v in (kv.split("=", 1) for kv in signature_header.split(","))}
received = parts.get("v1")
if not received:
return False

# 3) Calcula a signature esperada
payload = f"{timestamp_header}.".encode() + raw_body
expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()

# 4) Comparação de tempo constante
return hmac.compare_digest(received, expected)


@app.post("/integrations/dvs/webhooks")
async def receive_dvs_webhook(
request: Request,
x_dvs_signature: str | None = Header(None),
x_dvs_signature_timestamp: str | None = Header(None),
x_dvs_event_id: str | None = Header(None),
x_dvs_event_type: str | None = Header(None),
):
raw_body = await request.body()

if not verify_dvs_signature(
raw_body, x_dvs_signature, x_dvs_signature_timestamp, DVS_WEBHOOK_SECRET
):
raise HTTPException(status_code=401, detail="Invalid signature")

if already_processed(x_dvs_event_id):
return {"status": "duplicate_ignored"}

enqueue_for_processing(x_dvs_event_id, x_dvs_event_type, raw_body)
return {"status": "received"}

Troubleshooting

SintomaCausa provável
Receiver retorna 401 para todo webhookSecret incorreto, ou cálculo do HMAC sobre o JSON parseado em vez do raw body.
Alguns webhooks passam, outros falham (após rotação)Esqueceu de suportar ambos os secrets durante a janela de 24h. Consulte Rotacionando Webhook Secrets.
Todos os webhooks rejeitados com "timestamp too old"Clock do servidor com drift. Instale NTP / chrony / systemd-timesyncd.
Funciona localmente com ngrok, falha em prodSecret diferente no ambiente de prod, ou proxy alterando o body (alguns load balancers/CDNs modificam JSON). Evite qualquer transformação entre a borda da rede e o handler.

Teste sem tráfego real

Solicite à OSIGU o envio de um evento test.ping ao endpoint via a ação admin :test. O payload é sintético, mas assinado com o secret real — confirma que a verificação ponta a ponta funciona.