From 5dd914b8a6504678400053e58448904bdf45b730 Mon Sep 17 00:00:00 2001 From: Grant Whitmer Date: Wed, 12 Aug 2026 15:33:56 -0400 Subject: [PATCH] G3.5: answer the reachability probe honestly instead of skipping validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eternitas verifies a webhook URL answers BEFORE issuing the secret that signs deliveries, so the very first request can never carry a signature — refusing it makes registration impossible. Real chicken-and-egg, not a reason to disable validation. A probe is a request claiming no event and carrying no signature. Answering it 200 is honest: the endpoint exists and is ready. It changes nothing (acted: false), and anything claiming to BE an event still goes through full HMAC verification. Registering with skip_validation:true would have permanently disabled a safety check to solve a one-time ordering problem. Co-Authored-By: Claude Opus 5 --- api/app/routes/webhooks.py | 18 ++++++++++++++++++ api/tests/test_invariants.py | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/api/app/routes/webhooks.py b/api/app/routes/webhooks.py index 56bebf0..61f2340 100644 --- a/api/app/routes/webhooks.py +++ b/api/app/routes/webhooks.py @@ -54,6 +54,24 @@ async def eternitas_webhook( settings = request.app.state.settings raw = await request.body() + # Reachability probe. Eternitas verifies a webhook URL answers BEFORE it + # issues the secret that signs deliveries — so the first request can never + # carry a signature, and refusing it makes registration impossible. That is + # a real chicken-and-egg, not a reason to disable validation. + # + # A probe is a request claiming to be no event and carrying no signature. + # Answering it 200 is honest: the endpoint exists and is ready. It changes + # NOTHING — `acted: false` — and anything that claims to be an event still + # goes through full verification below. The alternative, registering with + # `skip_validation: true`, would permanently disable a safety check to + # solve a one-time ordering problem. + if not x_eternitas_event and not x_eternitas_signature: + return { + "ready": True, + "acted": False, + "detail": "reachability probe acknowledged; signed events are verified", + } + secret = settings.eternitas_webhook_secret if not secret: # I-8: refuse rather than accept unverified instructions about identity. diff --git a/api/tests/test_invariants.py b/api/tests/test_invariants.py index d60054c..1020107 100644 --- a/api/tests/test_invariants.py +++ b/api/tests/test_invariants.py @@ -583,3 +583,21 @@ def test_g35_revocation_never_acknowledges_what_it_did_not_apply(): that reports success.""" src = (ROOT / "api" / "app" / "routes" / "webhooks.py").read_text() assert "refusing to acknowledge a revocation we did not apply" in src + + +def test_g35_probe_acknowledgement_changes_nothing(): + """Eternitas verifies a webhook URL answers BEFORE issuing the secret that + signs deliveries, so the first request can never be signed. The probe path + answers 200 but must never act, and anything claiming to be an event must + still be verified.""" + src = (ROOT / "api" / "app" / "routes" / "webhooks.py").read_text() + probe = src[src.index("if not x_eternitas_event") : src.index("secret = settings")] + assert '"acted": False' in probe + assert "update(" not in probe and "commit" not in probe + + +def test_g35_did_not_disable_validation_to_register(): + """skip_validation would permanently disable a safety check to solve a + one-time ordering problem.""" + for f in (ROOT / "scripts").glob("*.py"): + assert "skip_validation" not in f.read_text()