Skip to content

Implement draft-02: JWT chain encoding, shared chain model, 0.6.0 prep - #8

Merged
sunilp merged 15 commits into
fix/test-countsfrom
feat/aip-02
Oct 10, 2026
Merged

sunilp merged 15 commits into
fix/test-countsfrom
feat/aip-02

Conversation

@sunilp

@sunilp sunilp commented Oct 10, 2026

Copy link
Copy Markdown
Owner

Implementation work behind draft-prakash-aip-02. Stacked on #7; retarget to master once that merges.

What changed

  • Shared chain model (aip_token/chain_model.py). V3, V4 and V5 now have one implementation, working on structured link values instead of regexes over Datalog text. Both encodings parse into it.
  • JWT chain encoding (aip_token/jwt_chain.py). Each link is an EdDSA JWS signed by the key the previous link named in cnf, joined with ~. Media type application/aip-chain+jwt, typ aip-link+jwt. A single -01 compact token verifies as a one-link chain.
  • Biscuit on the shared model, with domains. Domains are facts only, since a Datalog check would reject every request that names no domain.
  • Hardening.
    • Empty allowlists are malformed.
    • Duplicate single-valued facts are malformed, and so are blocks missing their issuer or delegate.
    • Compact tokens must carry typ aip+jwt.
    • cnf keys must be strict base64url.
    • Undecodable Biscuit tokens raise TokenError instead of escaping as a 500.
    • The A2A verifier rejects compact tokens.
  • Bindings. The MCP middleware, proxy and A2A verifier dispatch on token format.
  • Identity documents. Signatures decode as base64url per the spec. Standard base64 is still accepted.
  • 0.6.0 prep. Version bump, CHANGELOG, README example and limitation notes.

Known limitations (stated in the CHANGELOG, README and docstrings)

  • Any prefix of a valid JWT chain also verifies and carries an earlier holder's wider authority. Attenuation against downstream holders needs proof of possession of the leaf cnf key. The proxy, middleware and A2A verifier do not check it yet. Biscuit chains are not affected.
  • The JWT verifier does not check that a link's cnf key belongs to its sub identifier.
  • V7 revocation is not implemented.

Tests

  • 323 Python tests pass.
  • 80 Rust tests pass.
  • 26 cross-language conformance tests pass, after building the Rust interop binaries.
  • A new suite runs the same forged-delegation attacks against both encodings and checks they fail with the same code and reason.

CI cannot run until the Actions billing lock on the account is cleared. Do not tag aip-core-v0.6.0 until -02 is posted.

sunilp added 15 commits October 10, 2026 15:48
V3, V4 and V5 now have one implementation that works on structured link
values. Both encodings will parse into it. Also adds TokenError.wire_code,
which always returns the aip_-prefixed code the draft uses.
Each link is a JWS signed by the key the previous link named in cnf, and
carries the SHA-256 of the previous link. Delegator identity is therefore
authenticated, unlike the Biscuit encoding. Verification runs V1 here and
V3 to V5 on the shared chain model.
Adds detect_format so bindings can pick a verifier. CompactToken is kept
and marked deprecated.
Any prefix of a valid chain also verifies and carries wider authority, so a
relying party that needs attenuation to hold against downstream holders must
check proof of possession against the leaf link's cnf key. leaf_holder_key()
exposes it. verify now raises only TokenError for non-string tokens and bad
root keys, and cnf jwk x must be strict base64url.
The regex attenuation walk is gone; V3, V4 and V5 now run in
chain_model.verify_links for both encodings. Domains are encoded as facts
only, since a Datalog check would reject every request that names no
domain, and are checked structurally.
An empty tools or domains list meant different things by encoding: Biscuit
wrote no facts, so it inherited the parent value, while JWT denied every
domain. It is now malformed in both encodings, at verify_links and at mint
time. Biscuit links() also rejects repeated single-valued facts, and forged
block tests cover domain widening, extended expiry and max_depth in a
delegation block.
The spec says base64url; Python decoded standard base64, so roughly
half of spec-conformant signatures failed. Standard base64 is still
accepted for documents written by earlier releases. Rust (hex) is
unchanged and tracked in the draft's implementation status section.
Bindings now pick a verifier by token format. The proxy applies V2 root
binding and the deployment audit to JWT chains the same way it does to
Biscuit chains.
The same forged-delegation cases run through Biscuit and JWT, so the two
encodings cannot drift apart while their own tests keep passing.
A review of draft-prakash-aip-02 found that CompactToken.verify never read the JWT header, so a plain EdDSA JWT with typ JWT or no typ, signed by the root key, was accepted by the MCP middleware. Section 5.1.4 limits legacy acceptance to typ aip+jwt. Verify now rejects any other typ as token_malformed before decoding.
A review of draft-prakash-aip-02 found that links() turned a missing identity fact on block 0 into an empty issuer, and a missing delegator or delegate on a delegation block into an empty or null party, with nothing rejecting it. Table 1 makes issuer required for all links and delegate required for delegation links, so a block missing a required fact is now token_malformed, checked next to the duplicate-fact rejection.
Bump version to 0.6.0 (pyproject and aip_core.__version__), add the 0.6.0 changelog entry, add a JWT chain README example with the truncation and proof-of-possession warning, and refresh the test count.
@coderabbitai

coderabbitai Bot commented Oct 10, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: bc0015d5-bed6-4bbd-9c04-3bb30207cdc1

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sunilp
sunilp merged commit 494a376 into fix/test-counts Oct 10, 2026
1 of 3 checks passed
sunilp added a commit that referenced this pull request Oct 10, 2026
Bring draft-02 implementation (PR #8) onto master
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