G3.5: answer the reachability probe honestly instead of skipping validation
All checks were successful
check / gate (push) Successful in 18s
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:
@@ -54,6 +54,24 @@ 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
|
||||||
|
# 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
|
secret = settings.eternitas_webhook_secret
|
||||||
if not secret:
|
if not secret:
|
||||||
# I-8: refuse rather than accept unverified instructions about identity.
|
# I-8: refuse rather than accept unverified instructions about identity.
|
||||||
|
|||||||
@@ -583,3 +583,21 @@ def test_g35_revocation_never_acknowledges_what_it_did_not_apply():
|
|||||||
that reports success."""
|
that reports success."""
|
||||||
src = (ROOT / "api" / "app" / "routes" / "webhooks.py").read_text()
|
src = (ROOT / "api" / "app" / "routes" / "webhooks.py").read_text()
|
||||||
assert "refusing to acknowledge a revocation we did not apply" in src
|
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()
|
||||||
|
|||||||
Reference in New Issue
Block a user