in reviewhumanPR #118Auth
auth-engineer: delegatable capability tokens with cascading revocation
The default auth plugin (jwt_auth.py) issues flat HMAC tokens and revokes by
Author
@PranavPipariya
github profile →- Status
- In review
- Opened on
- Jul 9
- Branch
- hackathon/auth-engineer-delegatable-tokens
Description
The pitch.
## Problem 04 — Delegatable capability tokens with cascading revocation The default `auth` plugin (`jwt_auth.py`) issues flat HMAC tokens and revokes by *exact token string* (`JwtAuth._revoked: set[str]`). It cannot model the most common multi-agent authorization pattern: an orchestrator hands a worker a **narrowed, time-bounded sub-capability without going back to the issuer**, and revoking the orchestrator's own token silently withdraws every sub-capability underneath it. This PR ships `delegatable` — a macaroon-style auth plugin (Birgisson et al., 2014) that does exactly that. ### How it works A token is not a flat blob: it carries its **entire delegation chain** (root → leaf) plus a single running HMAC signature where each link is keyed on its parent's signature: ``` s0 = HMAC(secret, link0) s1 = HMAC(s0, link1) # the previous signature is the next key sig = s_n ``` Two properties fall out of that construction — and they are the whole point: - **Tamper-evidence.** Changing any byte of any link (a widened scope, a stretched expiry, a swapped audience) breaks the recomputed `sig`. - **Cascading revocation by construction.** Every descendant embeds its ancestors' links, so `verify` walks the whole ancestry offline. Revoking a parent link's id invalidates every descendant at the next verify — **no per-child revocation list.** ### API (extends `Auth`, stays protocol-compatible) - `delegate(parent, audience, scopes_subset, ttl, caveats=None) -> Token` — any holder mints a child **without the issuer**. Attenuation-only: child scopes must be a subset of the parent's (`ScopeEscalationError` otherwise) and child TTL must be ≤ the parent's (`TtlExpansionError`). - `verify(token, presenter=None, context=None) -> AuthContext` — checks the chain signature, then per link: revocation (`RevokedAncestorError`), expiry (`ExpiredTokenError`), scope/expiry monotonicity, and first-party caveats (`CaveatUnsatisfiedError`); binds the presenter to the leaf audience (`AudienceMismatchError`). - `revoke(token)` — O(1); adds the leaf link id to the shared revoked set, which collapses the entire subtree beneath it. All errors subclass one `DelegationError(ValueError)`, mirroring `EscrowError` in the escrow plugin. ### Adversarial validators (`delegated_auth`) Three validators, each derived purely from trace evidence so they catch a *buggy plugin that lets an attack through*, not just self-reported denials: | Validator | Catches | |---|---| | `authz_no_scope_escalation` | a delegated token holding a scope its parent never held | | `authz_cascading_revocation` | a token that verifies OK **after** an ancestor was revoked (order-aware) | | `authz_audience_binding` | a token that verifies OK when presented by an agent other than its audience | **All three PASS under `delegatable` and all three FAIL under the default `jwt` plugin** (no `delegate` surface → the tree never forms → "no delegation observed"). Same discrimination model as the merged escrow PR. ### Scenario `scenarios/delegated_auth.yaml` Coordinator → 3 intermediaries → 12 leaves, exactly as the problem's success criteria require, plus adversarial probes that the plugin rejects: - **scope escalation** — an intermediary delegates a scope it does not hold; - **stale parent** — both an **expired** parent (deterministic via a fixed clock and a pre-minted past-dated token) and a **revoked** parent (the coordinator revokes `intermediary-2`, cascading to its four leaves); - **audience confusion** — `leaf-0`'s token is handed to `leaf-1`, which tries to present it. ### Determinism No wall clock, no unseeded RNG. Like `JwtAuth`, the plugin takes a fixed `clock`; the scenario pins it so **same seed → byte-identical trace** (asserted in the test suite, verified across seeds `[42, 7, 1337]`). ### Tests - `test_delegatable.py` — 16 unit tests: attenuation, 3-deep cascading revoke, sibling isolation, audience mismatch, expiry, delegate-from-dead- …
Try it
Open PR on GitHubView diffCheckout locally
git fetch origin pull/118/head:pr-118
git checkout pr-118