Vote for your favorite SkillMD. The submission with the most likes wins the $1,000 Audience Choice Award for the NandaHack x HCLTech hackathon. Voting is open through September 25.Vote now →
in reviewhumanPR #73Auth

capsec-engineer: offline-attenuating capability tokens with epoch-fenced cascading revocation

The reference jwt auth plugin signs exact tokens and revokes exact token strings. That is useful scaffolding, but it is not a capability system: a holder cannot attenuate authority offline, and revoking a parent does not invalidate…

Author

zukhriddingit avatar

@zukhriddingit

github profile →
Status
In review
Opened on
Jul 6
Branch
hackathon/capsec-engineer-offline-attenuation

Description

The pitch.

# NandaHack Step 1: capsec-engineer offline attenuation for auth

## Motivation

The reference `jwt` auth plugin signs exact tokens and revokes exact token strings. That is useful scaffolding, but it is not a capability system: a holder cannot attenuate authority offline, and revoking a parent does not invalidate children because there is no parent-child cryptographic relation to inspect.

This PR adds `auth: capability_tokens`, a Biscuit/macaroon-style auth plugin for Nanda Town's capability-delegation problem. The security objective is deliberately narrow and testable: authority only shrinks as it moves down a strict delegation tree, and stale revocation knowledge fails closed.

## Design

`CapabilityTokens.issue(subject, scopes)` creates a root link signed with `HMAC(root_secret, canonical_root_caveats)`. `delegate(parent_token, audience, scopes_subset, ttl)` parses the parent token and mints a child link by signing canonical child caveats with the parent link signature as the HMAC key. The root secret is not used for child minting, so a holder can attenuate offline without issuer re-issuance.

Verification is a full replay from the root secret. For each link, the verifier checks the expected HMAC, the embedded parent chain hash, scope monotonicity, TTL monotonicity, expiry, audience binding, and revocation. Every link's chain hash is derived from the previous chain hash, canonical caveats, and signature. Revocation stores the chain hash of the revoked link, so revoking an ancestor invalidates descendants by construction: a descendant verify necessarily replays and encounters the revoked ancestor hash.

Revocations carry a monotonic epoch in `RevocationStore`. A verifier tracks a local visible epoch and is configured with `stale_after`. If `store.epoch - visible_epoch > stale_after`, verification raises `RevocationViewStaleError` rather than accepting on stale knowledge. That is the explicit partition semantics: availability loses to fail-closed authorization.

## Differentiation

What sets this apart from a plain HMAC-chained macaroon is that two additional guards are load-bearing in the adversarial scenario: the `stale_after` / `RevocationViewStaleError` epoch fence treats a stale revocation view as an attack surface and fails closed, while `authorize()` binds presenter verification and a required resource scope in one confused-deputy guard. The scenario makes the epoch fence observable with a real partition-group split and `partition_heal_at_tick: 80`, rather than proving it only in an isolated unit test.

## Threat Model

| Attack | Mechanism that stops it | Test/validator proving it |
|---|---|---|
| Scope escalation by requesting `admin:all` from a `read` parent | `delegate` rejects scopes not subset of parent; verifier also rejects widened links as invalid chains | `test_attack_scope_escalation_denied_before_child_exists`; `delegated_auth_scope_escalation_blocked` |
| TTL extension beyond parent lifetime | `delegate` rejects child expiry later than parent expiry; verifier rejects outliving child links | `test_attack_ttl_extension_denied_at_delegation_boundary`; property attenuation monotonicity |
| Stale parent / revoked ancestor with child replay | Verify checks every replayed ancestor chain hash against the revocation view | `test_attack_revoked_ancestor_cascades_to_child`; `delegated_auth_cascading_revocation`; revocation completeness property |
| Audience confusion / stolen token presentation | `verify_for_audience` and per-agent `verify` bind final token audience to presenter | `test_attack_audience_confusion_rejected`; `delegated_auth_audience_confusion_blocked` |
| Confused deputy using its own token for a third-party unscoped action | `authorize(token, presenter, required_scope)` rejects actions outside the token's verified scopes; scenario records `payments:write` rejection for a `payments:read` deputy token | `test_attack_confused_deputy_unscoped_resource_rejected`; `delegated_auth_confused_deputy_blocked` 

…

Try it

Open PR on GitHubView diff

Checkout locally

git fetch origin pull/73/head:pr-73
git checkout pr-73