G3.5: accept platform.test_ping unverified — unverifiable by construction
All checks were successful
check / gate (push) Successful in 19s
canary / probe (push) Successful in 26s

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 <noreply@anthropic.com>
This commit is contained in:
Grant Whitmer
2026-08-12 15:36:03 -04:00
parent 5dd914b8a6
commit a094960da3
2 changed files with 20 additions and 12 deletions

View File

@@ -54,22 +54,30 @@ async def eternitas_webhook(
settings = request.app.state.settings settings = request.app.state.settings
raw = await request.body() raw = await request.body()
# Reachability probe. Eternitas verifies a webhook URL answers BEFORE it # `platform.test_ping` is the ONE event accepted without verification, and
# issues the secret that signs deliveries — so the first request can never # the reason is structural rather than convenient.
# 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. # Eternitas generates the webhook secret at registration time and pings the
# Answering it 200 is honest: the endpoint exists and is ready. It changes # URL to prove it is reachable BEFORE returning that secret. The ping is
# NOTHING — `acted: false` — and anything that claims to be an event still # signed — with a secret the receiver cannot possibly hold yet. So the
# goes through full verification below. The alternative, registering with # signature is unverifiable by construction, not by oversight.
# `skip_validation: true`, would permanently disable a safety check to #
# 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. # 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 { return {
"ready": True, "ready": True,
"acted": False, "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 secret = settings.eternitas_webhook_secret

View File

@@ -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 answers 200 but must never act, and anything claiming to be an event must
still be verified.""" still be verified."""
src = (ROOT / "api" / "app" / "routes" / "webhooks.py").read_text() 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 '"acted": False' in probe
assert "update(" not in probe and "commit" not in probe assert "update(" not in probe and "commit" not in probe