Skip to content

[Hackathon] fayner-delegatable-auth: delegatable capability tokens with cascading revocation#70

Open
faynercosta wants to merge 1 commit into
projnanda:mainfrom
faynercosta:hackathon/fayner-delegatable-auth
Open

[Hackathon] fayner-delegatable-auth: delegatable capability tokens with cascading revocation#70
faynercosta wants to merge 1 commit into
projnanda:mainfrom
faynercosta:hackathon/fayner-delegatable-auth

Conversation

@faynercosta

Copy link
Copy Markdown

[Hackathon] fayner-delegatable-auth: delegatable capability tokens with cascading revocation

Problem: #4 — Delegatable capability tokens with cascading revocation (auth layer).
Persona: authorization-infrastructure engineer. The risk model, the test
emphasis (adversarial subset/revocation/audience invariants), and the
macaroon idioms below are the submission, not a label on a generic plugin.

Motivation

The default jwt plugin can mint and revoke flat tokens, but it cannot model
the single most common multi-agent authorization pattern: an orchestrator
holding a long-lived root capability, minting narrowly-scoped, short-lived
sub-capabilities for worker agents without going back to the issuer, and
having a single revoke(parent) cascade to every descendant. JwtAuth._revoked
is a set of exact token strings with no parent-child relationship, so revoking a
parent leaves its delegated children valid — the exact gap the problem calls
out, and the gap that makes "an LLM agent sub-renting a tool and cleanly
withdrawing it" impossible to express today.

Design

DelegatableAuth is a macaroon (Birgisson et al., NDSS 2014): a token is a
chain of links, each an attenuating caveat, secured by an HMAC chain
sig_i = HMAC(sig_{i-1}, link_i) anchored at the verifier's root secret. Three
properties follow:

  • Offline attenuation. Any holder can mint a narrower child (attenuate,
    or the verifier-checked delegate) because extending the chain only needs
    the parent's signature, not the root secret. This is real delegation, not
    central re-issuance (the named anti-pattern).
  • Cascading revocation by construction. revoke records the hash of a
    chain prefix; every descendant embeds that prefix, so all of them fail
    verify from the next call on — no per-child revocation list.
  • Defense in depth at verify. The HMAC chain alone cannot stop a malicious
    holder from writing broader scopes or a longer TTL into a link (they can
    compute a valid signature for anything they append). So verify
    independently re-walks the chain and rejects any link that escalates scope,
    outlives its parent, breaks the delegator→audience linkage, or is expired
    against the injected logical clock.

Typed errors (ScopeEscalationError, RevokedAncestorError,
AudienceMismatchError, ExpiredTokenError, TtlViolationError,
InvalidTokenError) subclass ValueError so existing Auth callers that
except ValueError keep working.

Determinism: no wall-clock time anywhere; the logical clock is injected via
set_clock(ctx.time), exactly like the rotating-identity reference plugin.
Same seed → byte-identical trace (asserted in a test).

Adversarial validators (mandatory)

validators/delegation_validators.py ships three attack probes plus a
trace-level replay validator. Each probe runs against both plugins via small
adapter callables, so the "adversarial" property is demonstrated, not asserted:

Attack delegatable reference jwt
Scope escalation (child claims scope parent lacks) denied admitted
Stale parent (ancestor revoked, child re-presented) denied admitted
Audience confusion (B's token presented by C) denied admitted

The delegated_auth scenario (1 coordinator + 1 gatekeeper + 3 intermediaries
× 4 workers) runs all three live. Result on the shipped trace:

auth=delegatable : ok:allow ×12, escalate:deny ×3, stale:deny ×4, confused:deny ×2  → validator PASS
auth=jwt         : ok:allow ×12, escalate:allow ×3, stale:allow ×4, confused:allow ×2 → validator FAIL

The validator FAILS against jwt and PASSES against delegatable — the
charter's bar.

Tests

  • test_delegatable.py — 13 unit tests: happy path, each typed rejection,
    two-level cascade, sibling isolation under revocation, tampered-signature and
    hand-forged-broadening rejection, offline-attenuate equivalence, and the
    cross-plugin probe property.
  • test_delegatable_properties.py — 4 Hypothesis properties: subset invariant
    holds at every chain depth, revocation cascades monotonically (depth ≥ k
    revoked, depth < k spared), token construction is deterministic, escalation
    is always rejected.
  • test_delegated_auth_scenario.py — end-to-end: delegatable denies every
    attack, jwt admits them, trace is byte-deterministic.

20 tests, all green locally under the CI command sequence.

Runnable verification

nest run delegated_auth
python -c "from nest_plugins_reference.validators.delegation_validators import validate_delegated_auth_trace as v; \
           [print(('PASS' if r.passed else 'FAIL'), r.detail) for r in v('./traces/delegated_auth.jsonl')]"

Tradeoffs / scope

  • Strict tree (single parent per token), per the problem's allowance; DAG
    multi-parent delegation is out of scope.
  • Revocation state lives in the verifier (a set of prefix hashes) — O(depth)
    membership checks per verify, no background GC. Fine for simulation; a
    production deployment would bound the set with token expiry.
  • No hardware key attestation, no OAuth2 endpoints, no network introspection —
    all explicitly out of scope for the problem.

…nanda#4)

Macaroon-style (Birgisson et al., 2014) auth plugin: HMAC-chained tokens with
offline attenuation, cascading revocation by chain-prefix hash, and defense-in-
depth verify (scope-subset, nested TTL, audience binding). Ships the mandatory
adversarial validator (scope-escalation, stale-parent, audience-confusion) that
FAILS the reference jwt plugin and PASSES this one, plus a 17-agent delegation-
tree scenario. Deterministic: same seed -> byte-identical trace.

20 targeted tests (13 unit + 4 Hypothesis property + 3 end-to-end scenario);
full repo CI green (ruff, ruff format, pyright strict, pytest).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant