from __future__ import annotations import hashlib import hmac from typing import Any # Forgejo (Gitea-compatible) signs webhook payloads with HMAC-SHA256 over the # raw request body, sent as a hex digest in X-Forgejo-Signature (Forgejo) or # X-Gitea-Signature (Gitea) -- both forges use the same scheme during the # transition, so both header names are accepted. SIGNATURE_HEADERS = ("X-Forgejo-Signature", "X-Gitea-Signature") RELEVANT_PATH_PREFIX = "registry/indexes/" def verify_signature(secret: str, body: bytes, signature: str | None) -> bool: """Constant-time HMAC-SHA256 verification. Returns False (never raises) for a missing/malformed signature or a misconfigured (empty) secret -- callers must treat False as 'reject', not 'skip verification'.""" if not secret or not signature: return False expected = hmac.new(secret.encode("utf-8"), body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, signature.strip()) def push_touches_registry_index(payload: dict[str, Any]) -> bool: """True if any commit in a Forgejo/Gitea push-event payload adds, modifies, or removes a path under registry/indexes/. Only inspects paths -- never parses file content (design principle 2: no push-parsing of payloads into registry state, the webhook only triggers a pull-based recompose).""" for commit in payload.get("commits") or []: for key in ("added", "modified", "removed"): for path in commit.get(key) or []: if path.startswith(RELEVANT_PATH_PREFIX): return True return False