Newer
Older
gn-synapse / app / signature.py
"""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)