"""HMAC-подпись вебхуков — одна схема на всю экосистему (docs/05).
Зеркало gnexus-auth `app/Domain/Webhooks/WebhookSignature.php`:
sig = "t=<unix>,v1=" + hex(hmac_sha256("<t>.<raw_body>", secret))
verify — константное сравнение (hmac.compare_digest, аналог hash_equals)
+ окно свежести для защиты от replay. Референс-реализация для
отправки (`app/worker/senders.py`) и приёма (входящие вебхуки gnexus-auth).
"""
import hashlib
import hmac
import time
def make_signature(body: bytes, secret: str, timestamp: int | None = None) -> str:
"""Подпись по «сырому» телу запроса (байт в байт, до любого парсинга)."""
if timestamp is None:
timestamp = int(time.time())
digest = hmac.new(
secret.encode(), f"{timestamp}.".encode() + body, hashlib.sha256
).hexdigest()
return f"t={timestamp},v1={digest}"
def verify_signature(body: bytes, header: str, secret: str, max_skew: int = 300) -> bool:
"""Проверка заголовка «t=…,v1=…»: свежесть + константное сравнение."""
try:
parts = dict(part.split("=", 1) for part in header.split(","))
timestamp = int(parts["t"].strip())
except (TypeError, AttributeError, ValueError):
return False
if abs(int(time.time()) - timestamp) > max_skew:
return False
expected = make_signature(body, secret, timestamp).split("v1=", 1)[1]
supplied = parts.get("v1", "").strip()
return hmac.compare_digest(expected, supplied)