From a094960da317c469e9fb03ecf7d7573a2c9691e7 Mon Sep 17 00:00:00 2001 From: Grant Whitmer Date: Wed, 12 Aug 2026 15:36:03 -0400 Subject: [PATCH] =?UTF-8?q?G3.5:=20accept=20platform.test=5Fping=20unverif?= =?UTF-8?q?ied=20=E2=80=94=20unverifiable=20by=20construction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Read the sender rather than guessing: Eternitas generates the webhook secret at registration time and pings the URL to prove reachability BEFORE returning that secret. The ping IS signed — with a secret the receiver cannot possibly hold yet. Unverifiable by construction, not by oversight. Accepting it is safe because the event is definitionally a no-op: nothing read, nothing written, acted:false. Every event that changes anything still requires a valid HMAC. The alternative, skip_validation:true, would permanently disable reachability checking for this platform to solve a one-time ordering problem. Co-Authored-By: Claude Opus 5 --- api/app/routes/webhooks.py | 30 +++++++++++++++++++----------- api/tests/test_invariants.py | 2 +- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/api/app/routes/webhooks.py b/api/app/routes/webhooks.py index 61f2340..b6b3d54 100644 --- a/api/app/routes/webhooks.py +++ b/api/app/routes/webhooks.py @@ -54,22 +54,30 @@ 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. + # `platform.test_ping` is the ONE event accepted without verification, and + # the reason is structural rather than convenient. # - # 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 + # Eternitas generates the webhook secret at registration time and pings the + # URL to prove it is reachable BEFORE returning that secret. The ping is + # signed — with a secret the receiver cannot possibly hold yet. So the + # signature is unverifiable by construction, not by oversight. + # + # Accepting it is safe because the event is definitionally a no-op: nothing + # is read, nothing is written, `acted` is false. Every event that changes + # anything — `passport.revoked` above all — still requires a valid HMAC + # below. The alternative was `skip_validation: true` at registration, which + # would permanently disable reachability checking for this platform to # solve a one-time ordering problem. - if not x_eternitas_event and not x_eternitas_signature: + if x_eternitas_event == "platform.test_ping" or ( + not x_eternitas_event and not x_eternitas_signature + ): return { "ready": True, "acted": False, - "detail": "reachability probe acknowledged; signed events are verified", + "detail": ( + "reachability ping acknowledged; it is unverifiable by " + "construction and changes nothing. Signed events are verified." + ), } secret = settings.eternitas_webhook_secret diff --git a/api/tests/test_invariants.py b/api/tests/test_invariants.py index 1020107..765c36c 100644 --- a/api/tests/test_invariants.py +++ b/api/tests/test_invariants.py @@ -591,7 +591,7 @@ def test_g35_probe_acknowledgement_changes_nothing(): 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")] + probe = src[src.index('if x_eternitas_event == "platform.test_ping"') : src.index("secret = settings")] assert '"acted": False' in probe assert "update(" not in probe and "commit" not in probe