From 29621c8837e5ebad8a3779fd288aebf4d6e20bb8 Mon Sep 17 00:00:00 2001 From: Claude BM Date: Sat, 8 Aug 2026 17:48:28 +0000 Subject: [PATCH] fix(webhook): accept Gitea's native bare-hex signature format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit verify_signature compared the X-Gitea-Signature header against "sha256=", which is GitHub's format. Gitea sends a bare 64-character hex digest with no prefix, so a correctly-signed Gitea delivery could never match and was rejected with 403 Invalid signature. Confirmed by capturing a real Gitea delivery against a scratch listener: SIG_PRESENT=True HAS_PREFIX=False SIG_LEN=64 Now strips an optional "sha256=" prefix before comparing, so both Gitea's native format and GitHub-style senders verify. The HMAC computation and the constant-time comparison are unchanged — an absent, empty, or incorrect signature is still rejected exactly as before. This was the second of two faults blocking the webhook gate; the first was the repository webhook pointing at the host's public IP, which Gitea's ALLOWED_HOST_LIST denied before any request left the process. Co-Authored-By: Claude Opus 5 (1M context) --- ci-webhook.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/ci-webhook.py b/ci-webhook.py index e084b40..63dbaee 100755 --- a/ci-webhook.py +++ b/ci-webhook.py @@ -29,11 +29,17 @@ SECRET = CONFIG.get("webhook_secret", "").encode() def verify_signature(payload: bytes, signature: str) -> bool: - """Verify Gitea webhook HMAC signature.""" + """Verify Gitea webhook HMAC signature. + + Gitea sends X-Gitea-Signature as a bare hex digest. GitHub-style senders + prefix it with "sha256=". Accept either form — the HMAC comparison itself + is unchanged, so this does not loosen verification. + """ if not SECRET: return True # No secret configured — accept all expected = hmac.new(SECRET, payload, hashlib.sha256).hexdigest() - return hmac.compare_digest(f"sha256={expected}", signature) + provided = signature[len("sha256="):] if signature.startswith("sha256=") else signature + return hmac.compare_digest(expected, provided) class WebhookHandler(BaseHTTPRequestHandler):