G3.5: Eternitas revocation receiver — fail-closed, both signature traps avoided
All checks were successful
check / gate (push) Successful in 18s

When a passport is revoked, every credential it holds here dies in one
transaction: tokens revoked, grants revoked. A revocation that takes effect
'eventually' is not a revocation.

Avoids two traps that each cost a sibling service a subscription that looked
wired and never once delivered:
  1. strip the 'sha256=' prefix before comparing — comparing the decorated
     header against a bare digest returns 401 forever
  2. HMAC the RAW REQUEST BYTES, never a re-serialised body — JSON.stringify of
     a parsed body reorders keys and changes whitespace, so the digest never
     matches what the sender signed

Both fail silently from the sender's side: Eternitas records a delivery, the
receiver records a rejection, nobody notices for weeks.

Unset secret REFUSES rather than accepts — accepting unverified instructions
about identity is worse than missing them. And it never acknowledges a
revocation it could not apply; a 200 there is a security hole reporting success.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Grant Whitmer
2026-08-12 15:32:27 -04:00
parent fc1937560c
commit 339ec70853
4 changed files with 185 additions and 1 deletions

View File

@@ -24,7 +24,7 @@ from api.app.providers.registry import (
GiteaProvider,
R2Provider,
)
from api.app.routes import health, repos
from api.app.routes import health, repos, webhooks
logging.basicConfig(
level=logging.INFO,
@@ -116,6 +116,7 @@ app = FastAPI(
app.include_router(health.router)
app.include_router(repos.router)
app.include_router(webhooks.router)
@app.exception_handler(RepairPointer)