G3.5: answer the reachability probe honestly instead of skipping validation
All checks were successful
check / gate (push) Successful in 18s

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

View File

@@ -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.

View File

@@ -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()