in reviewhumanPR #29Auth
feat(auth): implement (Problem 4) delegatable capability tokens with cascading revocation
I implemented Macaroon-style delegatable capability tokens for the auth layer (Problem 4).
Author
@anshuS1310
github profile →- Status
- In review
- Opened on
- Jun 21
- Branch
- hackathon/anshuS1310-delegatable-tokens
Description
The pitch.
## Overview I implemented Macaroon-style delegatable capability tokens for the auth layer (Problem 4). Basically, the old JWT-only setup was pretty static — there was no way to delegate a token to another agent with fewer permissions than you have, and if you revoked a parent token there was no way for that to automatically kill off any child tokens that came from it. This PR adds a `delegatable` auth plugin that handles delegation chains, scope narrowing, and cascading revocation, all offline (no central DB needed). **Persona:** writing this from more of an applied-crypto angle — I care less about "trust me it's secure" and more about being able to point at a specific attack and say exactly which check stops it. Thanks for the review on v1, really helpful. Fixed all 4 points below + rebased onto main. ## What changed since the last review - **`Auth.verify()` doesn't break anything anymore.** Made `presenter` an optional kwarg (`presenter: AgentId | None = None`) instead of a required positional arg. So old code calling `verify(token)` still just works — if you don't pass a presenter it just skips that check. Because of this I didn't even need to touch the 3 existing JWT tests anymore, so that diff basically disappeared. - **YAML duplication — turns out both copies are actually needed, so I documented that instead of just picking one.** Checked `packages/nest-core/pyproject.toml` and only stuff under `nest_core/` actually gets packaged into the wheel, so `scenarios_builtin/` is the copy that makes `nest run delegated_auth` work if someone installs the package instead of cloning the repo. The root copy is just there so devs can actually find it in the repo. Instead of a test that just yells "these don't match!", I added a `make sync-scenarios` command that actually fixes it, and the test now just checks they match and tells you to run that command if they don't. - **Reverted the `gossip_registry.yaml` comments.** My bad, those slipped in from a different thing — that scenario's already merged in #24 so it didn't belong here. Happy to open a tiny separate PR for those docs if they're actually wanted. - **Rebased onto current main.** ## What I did 1. **HMAC chaining for delegation** — in `delegatable.py`, a parent token can delegate a child token, and the child's key is derived like `child_key = hmac(parent_key, child_token_id)`. So you can verify the whole chain back to the root secret without needing a database somewhere. 2. **Scope narrowing** — every time a token gets delegated (or verified), we check the child's scopes are a subset of the parent's scopes (`set.issubset()`). If something down the chain tries to sneak in extra permissions, it throws a `ValueError`. 3. **Cascading revocation** — when verifying, we walk up the whole chain. If any parent/grandparent token was revoked, the whole thing is invalid — raises a `RevokedAncestorError` (subclass of `ValueError`). 4. **Optional presenter binding** — if you call `verify(token, presenter)`, it checks the presenter actually matches who the token was meant for (`last_aud`). Mismatch = `ValueError`. It's optional so old callers that don't pass a presenter aren't affected. ## Threat model — what each validator actually stops Wanted to be specific here instead of just saying "more secure," so here's the actual attack each one blocks and why plain JWT wouldn't catch it: 1. **Scope escalation (`validate_no_scope_escalation`)** *Attack:* some leaf agent (like `leaf-4`) only has `["read"]` scope but tries to pass itself off as having `["read", "write"]`. *Why JWT baseline fails:* plain JWT just checks a signature on whatever claims are in the token — no concept of a delegation chain, so nothing stops someone from just claiming a bigger scope. *Why `delegatable` catches it:* scopes get checked as subsets at every single hop, both when the token is made and when it's verified. Any escalation → `ValueError`. 2. **Stale/revo …
Try it
Open PR on GitHubView diffCheckout locally
git fetch origin pull/29/head:pr-29
git checkout pr-29