From bf4212bb7f5db06b6f429feeb0983c36471f50b1 Mon Sep 17 00:00:00 2001 From: Koishore Roy Date: Thu, 11 Jun 2026 03:13:11 +0530 Subject: [PATCH] =?UTF-8?q?CTK:=20=C2=A79=20authorization-token=20verifier?= =?UTF-8?q?=20vectors;=20mark=20=C2=A79=20reference-backed=20(0.3.3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reference implements the §9 token profile from delego 0.3.3. Adds ctk/vectors/token.json (+ token_signing_key.pub) — verifier vectors for §9.1: valid token + alg=none (algorithm confusion), tampered signature, wrong audience, expired — wired into conformance.py (skipped on reference < 0.3.3). Flips §9's status tag from 'draft — not yet in reference' to reference-backed; no normative design text changed. Protocol version unchanged (token is additive). --- CHANGELOG.md | 10 ++++++ conformance.py | 29 +++++++++++++++++ ctk/README.md | 1 + ctk/vectors/token.json | 54 +++++++++++++++++++++++++++++++ ctk/vectors/token_signing_key.pub | 3 ++ spec.md | 8 +++-- 6 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 ctk/vectors/token.json create mode 100644 ctk/vectors/token_signing_key.pub diff --git a/CHANGELOG.md b/CHANGELOG.md index 561b75c..c5dff9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,16 @@ reference implements it. See [`spec.md` §2.1](spec.md) for the version matrix. ## [Unreleased] +### Changed (§9 authorization token — now reference-backed) +- The reference implements the **§9 authorization-token profile** as of + **delego 0.3.3**. §9's status flips from *draft / not yet in reference* to + *reference-backed (optional profile)*; no normative design text changed. +- `ctk/vectors/token.json` + `ctk/vectors/token_signing_key.pub` — new verifier + (PEP) vectors for §9.1: a valid token plus `alg=none` (algorithm confusion), + tampered signature, wrong audience, and expired — each marked accept/reject. + Wired into `conformance.py` (skipped on a reference < 0.3.3). Protocol version + unchanged (the token is additive; no hashed/signed bytes change). + ### Changed (CTK — reference 0.3.0 implements the §4.2 query-fold) - `ctk/vectors/hashing.json` regenerated by reference **0.3.0** on the **0.3 preimage**: the fingerprint preimage now carries the canonical `query` diff --git a/conformance.py b/conformance.py index 01c805d..71929af 100644 --- a/conformance.py +++ b/conformance.py @@ -146,6 +146,35 @@ def verify_chain(jsonl: str): else: fail(f"tampered chain: got valid={tvalid} problems={tprobs}") +# --- §9 authorization token (optional profile, reference >= 0.3.3) ------------ # +# The reference implements the §9 token profile from 0.3.3; replay the verifier +# vectors. Skipped (not failed) on an older reference that lacks verify_token. +try: + from delego.token import TokenError, verify_token +except ImportError: + print("token (§9): reference < 0.3.3 — token profile not implemented, skipping") +else: + print("token (§9 / §9.1):") + from cryptography.hazmat.primitives import serialization as _ser + + _pub = _ser.load_pem_public_key((VEC / "token_signing_key.pub").read_bytes()) + _tok = json.loads((VEC / "token.json").read_text()) + _leeway = _tok.get("_leeway_seconds", 60) + for c in _tok["cases"]: + try: + claims = verify_token( + c["token"], public_key=_pub, audience=c["audience"], now=c["now"], leeway=_leeway + ) + got = "accept" + except TokenError: + claims, got = None, "reject" + if got != c["expect"]: + fail(f"token: {c['name']}: got {got}, want {c['expect']}") + elif got == "accept" and not all(claims.get(k) == v for k, v in c.get("expected_claims", {}).items()): + fail(f"token: {c['name']}: claims mismatch") + else: + ok(f"{got:6} {c['name']}") + print() if fails: print(f"{fails} conformance failure(s) — reference does not match the spec's CTK.") diff --git a/ctk/README.md b/ctk/README.md index 1844716..895bc02 100644 --- a/ctk/README.md +++ b/ctk/README.md @@ -15,6 +15,7 @@ conformant implementation MUST reproduce it. | [`vectors/chain.jsonl`](vectors/chain.jsonl) + [`vectors/chain.expected.json`](vectors/chain.expected.json) | Verify the chain (spec §8.1) using [`vectors/signing_key.pub`](vectors/signing_key.pub); it MUST be valid with the listed `seqs`. | | [`vectors/chain.tampered.jsonl`](vectors/chain.tampered.jsonl) + [`vectors/chain.tampered.expected.json`](vectors/chain.tampered.expected.json) | The same chain with `seq 0` edited and not re-signed; verification MUST fail with a content-hash mismatch at `seq 0`. | | [`vectors/resolve.json`](vectors/resolve.json) *(0.2; + 0.3 §7.1 P1/P3)* | For each case, given a parked `approval` (its `action_fingerprint`, `intent_hash`, `status`, `rule`) and a `presented_action`, apply the §7 resolution rules (fingerprint guard → intent guard → status) and match `expected.outcome`; the emitted reason MUST contain `expected.reason_contains`. Exercises the confused-deputy guard, the intent guard, and single-use replay refusal — and now the §7.1 authorization properties **P1** (fingerprint mismatch refused even when `status = approved`) and **P3** (a `denied` approval is not resurrected). The P1/P3 cases carry a `"property"` tag; they are already enforced by the reference and are wired into `conformance.py`. | +| [`vectors/token.json`](vectors/token.json) *(§9, reference ≥ 0.3.3)* | Verifier (PEP) vectors for the authorization-token profile. For each case, verify the `token` JWS against [`vectors/token_signing_key.pub`](vectors/token_signing_key.pub) requiring `aud == audience` at wall-clock `now`, with the alg **pinned to EdDSA** (never taken from the token header). `accept` → claims valid; `reject` → verification MUST fail (the PEP MUST NOT inject a credential). Covers a valid token, `alg=none` (algorithm confusion), a tampered signature, a wrong audience, and an expired token. Wired into `conformance.py` (skipped on a reference < 0.3.3). | The policy used for the decision and chain vectors is [`../examples/policy.example.yaml`](../examples/policy.example.yaml). The public diff --git a/ctk/vectors/token.json b/ctk/vectors/token.json new file mode 100644 index 0000000..75c79a3 --- /dev/null +++ b/ctk/vectors/token.json @@ -0,0 +1,54 @@ +{ + "_about": "CTK vectors for spec §9 / §9.1 — the authorization token (optional profile). A conformant verifier (PEP) MUST accept/reject each token as marked, verifying the JWS against token_signing_key.pub with alg pinned to EdDSA. Reproduced by the reference's delego.verify_token at reference >= 0.3.3.", + "_spec": "§9 (token), §9.1 (broker verification steps 1–4), §11 (algorithm confusion)", + "_signing_key": "token_signing_key.pub (Ed25519 SubjectPublicKeyInfo PEM)", + "_how_to_apply": "For each case, verify `token` against the public key requiring aud == `audience`, at wall-clock `now` (epoch seconds), leeway 60s. accept → claims valid (and contain `expected_claims`); reject → verification MUST fail (the verifier MUST NOT inject a credential). The key/alg come from verifier config, never the token header.", + "_leeway_seconds": 60, + "cases": [ + { + "name": "valid token verifies", + "token": "eyJhbGciOiJFZERTQSIsImtpZCI6IjI1ZjU3OGVlNDBmZDkwM2QiLCJ0eXAiOiJKV1QifQ.eyJhcHIiOiJhcHJfNGM5MTgzZjc2MDZmIiwiYXVkIjoiYnJva2VyOm9uZWNsaSIsImNucyI6IjAxSkJRSzlaNlg4TjNNMlAwUjVUN1Y5VzJaIiwiZXhwIjoxNzU5MDAwMDQ1LCJmcHIiOiJjNzBkNGVlNTc5NTcyMDIwODc4ODdjYjVlOWQzMjIyMjk3N2I3MjhiZDA2OTQ3Yjc3NjFjMjgzYjZkNGVkMzk0IiwiaWF0IjoxNzU5MDAwMDAwLCJpaHQiOiI3NmY4ZWVmMWI5N2UxMjEzYTU5ZWVjMjhjZWRmMTViYjk5OWZkYjAwYTNmZDE3ZjgzNDNiYzQ2NzZmZGJiNGYzIiwiaXNzIjoiZGVsZWdvOmxvY2FsIiwianRpIjoiMDFKQlFLOVo2WDhOM00yUDBSNVQ3VjlXMVkiLCJwb2wiOnsicnVsZSI6InBsYWNlLW9yZGVyIiwidmVyc2lvbiI6MX0sInN1YiI6ImFnZW50Om9uZWNsaS9zZXNzaW9uLTdmM2EifQ.qp-T_pAcKYpSR_i83yGe9H03X1YpWvJzI0oIzcGxiASpANMw7SfkjkLdY7thbvfksYlJwP3wT3PmHYTQ0ir7Ag", + "audience": "broker:onecli", + "now": 1759000010, + "expect": "accept", + "expected_claims": { + "fpr": "c70d4ee57957202087887cb5e9d32222977b728bd06947b7761c283b6d4ed394", + "iht": "76f8eef1b97e1213a59eec28cedf15bb999fdb00a3fd17f8343bc4676fdbb4f3", + "aud": "broker:onecli", + "apr": "apr_4c9183f7606f" + } + }, + { + "name": "alg=none is rejected (algorithm confusion)", + "token": "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJhcHIiOiJhcHJfNGM5MTgzZjc2MDZmIiwiYXVkIjoiYnJva2VyOm9uZWNsaSIsImNucyI6IjAxSkJRSzlaNlg4TjNNMlAwUjVUN1Y5VzJaIiwiZXhwIjoxNzU5MDAwMDQ1LCJmcHIiOiJjNzBkNGVlNTc5NTcyMDIwODc4ODdjYjVlOWQzMjIyMjk3N2I3MjhiZDA2OTQ3Yjc3NjFjMjgzYjZkNGVkMzk0IiwiaWF0IjoxNzU5MDAwMDAwLCJpaHQiOiI3NmY4ZWVmMWI5N2UxMjEzYTU5ZWVjMjhjZWRmMTViYjk5OWZkYjAwYTNmZDE3ZjgzNDNiYzQ2NzZmZGJiNGYzIiwiaXNzIjoiZGVsZWdvOmxvY2FsIiwianRpIjoiMDFKQlFLOVo2WDhOM00yUDBSNVQ3VjlXMVkiLCJwb2wiOnsicnVsZSI6InBsYWNlLW9yZGVyIiwidmVyc2lvbiI6MX0sInN1YiI6ImFnZW50Om9uZWNsaS9zZXNzaW9uLTdmM2EifQ.", + "audience": "broker:onecli", + "now": 1759000010, + "expect": "reject", + "reason": "alg must be EdDSA" + }, + { + "name": "tampered signature is rejected", + "token": "eyJhbGciOiJFZERTQSIsImtpZCI6IjI1ZjU3OGVlNDBmZDkwM2QiLCJ0eXAiOiJKV1QifQ.eyJhcHIiOiJhcHJfNGM5MTgzZjc2MDZmIiwiYXVkIjoiYnJva2VyOm9uZWNsaSIsImNucyI6IjAxSkJRSzlaNlg4TjNNMlAwUjVUN1Y5VzJaIiwiZXhwIjoxNzU5MDAwMDQ1LCJmcHIiOiJjNzBkNGVlNTc5NTcyMDIwODc4ODdjYjVlOWQzMjIyMjk3N2I3MjhiZDA2OTQ3Yjc3NjFjMjgzYjZkNGVkMzk0IiwiaWF0IjoxNzU5MDAwMDAwLCJpaHQiOiI3NmY4ZWVmMWI5N2UxMjEzYTU5ZWVjMjhjZWRmMTViYjk5OWZkYjAwYTNmZDE3ZjgzNDNiYzQ2NzZmZGJiNGYzIiwiaXNzIjoiZGVsZWdvOmxvY2FsIiwianRpIjoiMDFKQlFLOVo2WDhOM00yUDBSNVQ3VjlXMVkiLCJwb2wiOnsicnVsZSI6InBsYWNlLW9yZGVyIiwidmVyc2lvbiI6MX0sInN1YiI6ImFnZW50Om9uZWNsaS9zZXNzaW9uLTdmM2EifQ.Ap-T_pAcKYpSR_i83yGe9H03X1YpWvJzI0oIzcGxiASpANMw7SfkjkLdY7thbvfksYlJwP3wT3PmHYTQ0ir7Ag", + "audience": "broker:onecli", + "now": 1759000010, + "expect": "reject", + "reason": "bad signature" + }, + { + "name": "wrong audience is rejected (exact match)", + "token": "eyJhbGciOiJFZERTQSIsImtpZCI6IjI1ZjU3OGVlNDBmZDkwM2QiLCJ0eXAiOiJKV1QifQ.eyJhcHIiOiJhcHJfNGM5MTgzZjc2MDZmIiwiYXVkIjoiYnJva2VyOm9uZWNsaSIsImNucyI6IjAxSkJRSzlaNlg4TjNNMlAwUjVUN1Y5VzJaIiwiZXhwIjoxNzU5MDAwMDQ1LCJmcHIiOiJjNzBkNGVlNTc5NTcyMDIwODc4ODdjYjVlOWQzMjIyMjk3N2I3MjhiZDA2OTQ3Yjc3NjFjMjgzYjZkNGVkMzk0IiwiaWF0IjoxNzU5MDAwMDAwLCJpaHQiOiI3NmY4ZWVmMWI5N2UxMjEzYTU5ZWVjMjhjZWRmMTViYjk5OWZkYjAwYTNmZDE3ZjgzNDNiYzQ2NzZmZGJiNGYzIiwiaXNzIjoiZGVsZWdvOmxvY2FsIiwianRpIjoiMDFKQlFLOVo2WDhOM00yUDBSNVQ3VjlXMVkiLCJwb2wiOnsicnVsZSI6InBsYWNlLW9yZGVyIiwidmVyc2lvbiI6MX0sInN1YiI6ImFnZW50Om9uZWNsaS9zZXNzaW9uLTdmM2EifQ.qp-T_pAcKYpSR_i83yGe9H03X1YpWvJzI0oIzcGxiASpANMw7SfkjkLdY7thbvfksYlJwP3wT3PmHYTQ0ir7Ag", + "audience": "broker:evil", + "now": 1759000010, + "expect": "reject", + "reason": "aud mismatch" + }, + { + "name": "expired token is rejected", + "token": "eyJhbGciOiJFZERTQSIsImtpZCI6IjI1ZjU3OGVlNDBmZDkwM2QiLCJ0eXAiOiJKV1QifQ.eyJhcHIiOiJhcHJfNGM5MTgzZjc2MDZmIiwiYXVkIjoiYnJva2VyOm9uZWNsaSIsImNucyI6IjAxSkJRSzlaNlg4TjNNMlAwUjVUN1Y5VzJaIiwiZXhwIjoxNzU5MDAwMDQ1LCJmcHIiOiJjNzBkNGVlNTc5NTcyMDIwODc4ODdjYjVlOWQzMjIyMjk3N2I3MjhiZDA2OTQ3Yjc3NjFjMjgzYjZkNGVkMzk0IiwiaWF0IjoxNzU5MDAwMDAwLCJpaHQiOiI3NmY4ZWVmMWI5N2UxMjEzYTU5ZWVjMjhjZWRmMTViYjk5OWZkYjAwYTNmZDE3ZjgzNDNiYzQ2NzZmZGJiNGYzIiwiaXNzIjoiZGVsZWdvOmxvY2FsIiwianRpIjoiMDFKQlFLOVo2WDhOM00yUDBSNVQ3VjlXMVkiLCJwb2wiOnsicnVsZSI6InBsYWNlLW9yZGVyIiwidmVyc2lvbiI6MX0sInN1YiI6ImFnZW50Om9uZWNsaS9zZXNzaW9uLTdmM2EifQ.qp-T_pAcKYpSR_i83yGe9H03X1YpWvJzI0oIzcGxiASpANMw7SfkjkLdY7thbvfksYlJwP3wT3PmHYTQ0ir7Ag", + "audience": "broker:onecli", + "now": 1759001000, + "expect": "reject", + "reason": "expired (now > exp + leeway)" + } + ] +} diff --git a/ctk/vectors/token_signing_key.pub b/ctk/vectors/token_signing_key.pub new file mode 100644 index 0000000..a0c6a77 --- /dev/null +++ b/ctk/vectors/token_signing_key.pub @@ -0,0 +1,3 @@ +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEAA6EHv/POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg= +-----END PUBLIC KEY----- diff --git a/spec.md b/spec.md index b12c487..68d3163 100644 --- a/spec.md +++ b/spec.md @@ -493,10 +493,12 @@ anchor is advanced as the chain grows. See the §10 conformance line. -## 9. Authorization Token (OPTIONAL PROFILE) *(0.3, draft — not yet in reference)* +## 9. Authorization Token (OPTIONAL PROFILE) *(0.3 — reference-backed since delego 0.3.3)* -> **Status: draft, not yet implemented in the reference.** This section defines -> an **optional profile** layered on the 0.2 preimage. The token is **not** the +> **Status: reference-backed (optional profile).** The reference implements this +> profile from **delego 0.3.3**; its verifier reproduces the +> [`ctk/vectors/token.json`](ctk/vectors/token.json) vectors (§10). This section +> defines an **optional profile** layered on the 0.2 preimage. The token is **not** the > protocol's load-bearing control — the load-bearing controls are the > deterministic decision (§6), the fingerprint/intent binding and single-use > approval (§7, §7.1), the audit chain (§8), and the PDP/PEP split (§2) in which