Skip to content

Converge implementations on the draft-01 canonical encoding - #3

Merged
sunilp merged 2 commits into
masterfrom
canonical-encoding-draft01
Aug 19, 2026
Merged

Converge implementations on the draft-01 canonical encoding#3
sunilp merged 2 commits into
masterfrom
canonical-encoding-draft01

Conversation

@sunilp

@sunilp sunilp commented Aug 19, 2026

Copy link
Copy Markdown
Owner

The Rust and Python reference implementations rejected each other's chained tokens in both directions whenever a delegation block was present. Reproduced before the fix:

  • Rust mints, Python verifies: delegation block 1 does not grant right("tool:search")
  • Python mints, Rust verifies: Check n°1 in block n°1: check if budget($b), $b <= 100

Each implementation matched the spec on one axis and neither on both. Rust emitted the canonical tool check but an inverted budget check; Python emitted the canonical budget direction but check if right(...) per scope, plus a block-source scan to compensate.

What this changes

Both now emit the canonical encoding from draft-prakash-aip-01 §3.4.1 and verify with the algorithm in §4.

  • One self-contained check if per block. Scope narrowing follows from conjunction, so a widened block authorizes nothing.
  • Budget is a budget_ceiling fact, not a check. A Datalog check cannot express "narrower than my parent": Python's verifier injected budget(0) making every such check vacuous, and Rust injected nothing so the check bound to the authority's ceiling and rejected valid chains.
  • V4 structural attenuation walk at verification, covering scope, budget, expiry and principal invariance. Mint-time checks remain as a convenience to the delegator; an attacker appending a block does not run them.

Wildcards

tool:* worked in neither implementation. It compared literally, so a wildcard authority did not authorize tool:search. test_delegation_chain_three_hops used tool:* but only asserted depth and never authorized, so it never caught this.

Patterns now compile to starts_with clauses joined with or, under the Standard profile. The Simple profile keeps exact string equality exactly as draft-01 §3.4.1 prints it, so nothing here contradicts the published draft.

Security note

A block's scope constraint must be a single self-contained check, never a shared rule name. Rules do not attenuate: the authority block's rule stays in scope and is unioned with a delegation block's narrower rule of the same name, and the broader one then satisfies the delegation's own check. Verified against biscuit-auth before choosing the encoding, and now normative in spec/aip-tokens.md §7.2.

Why nothing caught this

The cross-language conformance tests at tests/conformance/ were outside CI's working-directory: python, CI only fired on aip-core-v* tags, and the tests skipped silently without biscuit-python. The Python to Rust test also used an authority-only token, so it would not have exercised delegation even if it ran.

All three now fixed: the tests exercise delegation in both directions, a new workflow runs them on push and pull request, and the job fails outright if biscuit-python is missing rather than skipping.

Verification

Python      131 passed
Rust         72 passed
Conformance   6 passed

sunilp added 2 commits August 19, 2026 21:15
The two reference implementations rejected each other's chained tokens in
both directions whenever a delegation block was present. Rust emitted
`check if tool($t), [...].contains($t)` and `check if budget($b), $b >= N`;
Python emitted `check if right("...")` per scope and `$b <= N`, then
compensated with a block-source scan. Each matched the spec on one axis and
neither on both.

Both now emit the canonical encoding from draft-prakash-aip-01 Section 3.4.1
and verify with the algorithm in Section 4:

- One self-contained `check if` per block. Scope narrowing follows from
  conjunction, so widening authorizes nothing.
- Budget is a `budget_ceiling` fact, not a check. A Datalog check cannot
  express "narrower than my parent": Python's verifier injected budget(0),
  making every such check vacuous, while Rust injected nothing and evaluated
  against the authority's ceiling, rejecting valid chains.
- V4 structural attenuation walk at verification, covering scope, budget,
  expiry and principal invariance. Mint-time checks are a convenience for the
  delegator; an attacker appending a block does not run them.

Wildcard scopes worked in neither implementation. `tool:*` compared literally,
so a wildcard authority did not authorize `tool:search`. The existing three-hop
test used `tool:*` but only asserted depth, never authorizing. Wildcards now
compile to `starts_with` clauses joined with `or`.

The check must stay self-contained. A rule-based encoding does not attenuate:
a delegation block declaring a narrower rule is unioned with the authority
block's broader rule of the same name, and the broader one then satisfies the
delegation's check. Verified against biscuit-auth before choosing the form.

Conformance tests now exercise delegation in both directions, and run in CI on
push and pull request. They previously lived outside CI's working-directory and
skipped silently when biscuit-python was absent, which is how this drifted.

Python 131 passed, Rust 72 passed, conformance 6 passed.
The markdown specs still described draft-00: a budget Datalog check, no
verification algorithm, and canonical templates neither implementation
followed. That drift is what let the two implementations diverge.

- New spec/aip-verification.md carrying the V1 to V7 algorithm, the error code
  for each failure, and the reason the container format does not establish
  attenuation on its own.
- aip-tokens.md: budget is a budget_ceiling fact, declared and verified
  structurally, never a Datalog check, and never an ambient fact the verifier
  supplies. Block fact tables gain principal and drop the budget check.
- aip-tokens.md section 7: the Simple profile now carries the full canonical
  block encoding and matches scopes by exact string equality. Scope patterns
  move to the Standard profile, which is where curated Datalog is already
  permitted, so wildcard support contradicts nothing in the published draft.
- Standard profile gains a normative requirement that a block's scope
  constraint be a single self-contained check. Rules shared across blocks do
  not attenuate: the authority block's rule stays in scope and is unioned with
  a delegation block's narrower rule of the same name, and the broader one then
  satisfies the delegation's own check.
- aip-delegation.md: principal invariance, and mint-time checking is called
  out as insufficient on its own.
- aip-bindings-mcp.md defers to the algorithm rather than restating it.
- README points at draft-01.

Python 131 passed, Rust 72 passed, conformance 6 passed.
@coderabbitai

coderabbitai Bot commented Aug 19, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: dc64e3ba-e468-4eb4-82db-4744a7fac68a


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 e069371 into master Aug 19, 2026
1 of 3 checks passed
@sunilp
sunilp deleted the canonical-encoding-draft01 branch August 19, 2026 16:41
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