Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
29 changes: 29 additions & 0 deletions conformance.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand Down
1 change: 1 addition & 0 deletions ctk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
54 changes: 54 additions & 0 deletions ctk/vectors/token.json
Original file line number Diff line number Diff line change
@@ -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)"
}
]
}
3 changes: 3 additions & 0 deletions ctk/vectors/token_signing_key.pub
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAA6EHv/POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg=
-----END PUBLIC KEY-----
8 changes: 5 additions & 3 deletions spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down