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
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,23 @@ reference implements it. See [`spec.md` §2.1](spec.md) for the version matrix.

## [Unreleased]

### 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`
(`[]` for a bare URL), so every `action_fingerprint` changed. New query
cases: substitution (`?to=me` vs `?to=attacker` no longer share a
fingerprint), encoding equivalence (`%20` vs `+`, pair order), duplicate and
valueless pairs, fragment exclusion. The prior fixtures are preserved as
`ctk/vectors/hashing-v0.2.json` for ≤ 0.2-preimage implementations (the
`versions/` documents now point there).
- `ctk/vectors/resolve.json`: embedded fingerprints/intent hashes recomputed on
the 0.3 preimage (cases and expectations unchanged).
- `ctk/vectors/broker_query.json` marked **superseded** at reference ≥ 0.3.0:
with the query fingerprint-bound, a broker forwards it (refusing only a
`#fragment`); the reconstruction cases apply only to ≤ 0.2-preimage brokers.
- `spec.md` is untouched (frozen at 0.3); the §4.1 worked example remains the
0.2-preimage illustration and matches `hashing-v0.2.json`.

### Added (editorial — no protocol change)
- `versions/` — standalone documents of record for prior protocol versions:
[`versions/spec-v0.1.md`](versions/spec-v0.1.md) and
Expand Down
29 changes: 14 additions & 15 deletions ctk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ conformant implementation MUST reproduce it.

| File | What a conformant implementation must do |
|------|------------------------------------------|
| [`vectors/hashing.json`](vectors/hashing.json) | For each action, compute `intent_hash` and `action_fingerprint` (spec §4) and match. Each entry includes the exact canonical-JSON that was hashed, so you can debug a mismatch byte-for-byte. |
| [`vectors/hashing.json`](vectors/hashing.json) *(0.3 preimage)* | For each action, compute `intent_hash` and `action_fingerprint` (spec §4, with the §4.2 query-fold: the preimage carries the canonical `query`, `[]` for a bare URL) and match. Includes query cases — substitution (`?to=me` vs `?to=attacker` differ), encoding equivalence (`%20` vs `+`, pair order), duplicate/valueless pairs, fragment exclusion. Each entry includes the exact canonical-JSON that was hashed, so you can debug a mismatch byte-for-byte. |
| [`vectors/hashing-v0.2.json`](vectors/hashing-v0.2.json) *(0.1–0.2 preimage snapshot)* | The same check for an implementation on the ≤ 0.2 preimage (no `query` key). Frozen — see [`../versions/`](../versions/README.md). |
| [`vectors/decisions.json`](vectors/decisions.json) | Load the example policy, evaluate each action (spec §5–§6), and match `outcome` / `rule` / `reasons`. |
| [`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`. |
Expand All @@ -20,19 +21,16 @@ The policy used for the decision and chain vectors is
key for verifying the chain signatures is `vectors/signing_key.pub` (the private
key is never published).

## Pending vectors (documented, not yet wired)
## Documented-only vectors (not wired)

These vectors describe **0.3 additive behaviour that the current reference
(protocol 0.2) does not yet expose**, so `conformance.py` does **not** replay them
— wiring them now would fail against the unfixed reference. They are committed as
documentation and **activate (get wired into `conformance.py`) when the reference
is ≥ 0.2.3**. Each file carries its own `_status`, `_activate_at_reference`,
`_spec`, and `_how_to_apply` keys.
These files are committed as documentation and are **not** replayed by
`conformance.py`. Each carries its own `_status`, `_spec`, and `_how_to_apply`
keys.

| File | What it will check | Activate at |
|------|--------------------|-------------|
| [`vectors/policy_invalid.json`](vectors/policy_invalid.json) | Policy-schema validation and **fail-closed** loading (spec §5.1): each policy is invalid (unknown `match`/`constraints` key, unknown `decision`, missing `default`); a conformant Authorizer MUST reject it and MUST NOT silently skip an unknown constraint. | reference 0.2.3 |
| [`vectors/broker_query.json`](vectors/broker_query.json) | The Broker (PEP) **query obligation** (spec §4.2): the Broker MUST reconstruct the outgoing URL from the authorized `host`/`path`/`params`, MUST NOT forward the agent-supplied query verbatim, and MUST refuse an action whose URL it cannot reconstruct. (This is the additive obligation; folding the query into the fingerprint is a deferred breaking change and is **not** tested here.) | reference ≥ 0.2.3 |
| File | Status |
|------|--------|
| [`vectors/policy_invalid.json`](vectors/policy_invalid.json) | **Pending.** Policy-schema validation and **fail-closed** loading (spec §5.1): each policy is invalid (unknown `match`/`constraints` key, unknown `decision`, missing `default`); a conformant Authorizer MUST reject it and MUST NOT silently skip an unknown constraint. The reference enforces this since 0.2.3. |
| [`vectors/broker_query.json`](vectors/broker_query.json) | **Superseded at reference ≥ 0.3.0.** The Broker (PEP) query-reconstruction obligation as it applies to a **0.2-preimage** implementation. With the 0.3 query-fold, the whole query of an authorised action is fingerprint-bound and a broker forwards it (refusing only a `#fragment`), so these cases apply only to implementations that remain on the ≤ 0.2 preimage. |

## Schema validation

Expand All @@ -43,6 +41,7 @@ runs it on every change.
## Regenerating

The vectors are fixtures produced by the reference implementation. A port in any
language passes the CTK when it reproduces every value above. The pending vectors
above become fixtures (and get wired into `conformance.py`) once the reference
implements the corresponding §4.2 / §5.1 behaviour at ≥ 0.2.3.
language passes the CTK when it reproduces every value above. `hashing.json` and
the hashes embedded in `resolve.json` were regenerated by reference **0.3.0**
when the §4.2 query-fold shipped; `hashing-v0.2.json` preserves the prior
preimage's fixtures for ≤ 0.2 implementations.
114 changes: 59 additions & 55 deletions ctk/vectors/broker_query.json
Original file line number Diff line number Diff line change
@@ -1,58 +1,62 @@
{
"_about": "CTK vectors for spec §4.2 — the Broker (PEP) query obligation. These exercise NOT-YET-RELEASED behaviour: query reconstruction at the Broker is a 0.3 additive obligation that the current reference (protocol 0.2) does not yet expose a Broker for, so these vectors are PENDING and are NOT wired into conformance.py. Activate (wire into conformance.py) when a conformant Broker is available at reference >= 0.2.3.",
"_status": "pending",
"_activate_at_reference": ">=0.2.3",
"_spec": "§4.2 (Broker query obligation, additive) and §10 (0.3 additive hardening)",
"_how_to_apply": "For each case, the Broker has authorized the action described by `authorized.host`/`authorized.path`/`authorized.params`. The Agent presents `agent_supplied_url`. A conformant Broker MUST reconstruct the outgoing URL from the authorized host/path/params and MUST NOT forward the agent-supplied query verbatim. `expected.outgoing_url` is the URL the Broker MUST send (null when it MUST refuse). `expected.action` is `reconstruct` or `refuse`.",
"_note_deferred": "Folding the query into the action_fingerprint preimage is a DEFERRED breaking change (§4.2) and is NOT covered here; these vectors test only the additive Broker obligation, which changes no hashed bytes.",
"cases": [
{
"name": "agent-supplied query is dropped; URL reconstructed from authorized fields",
"property": "no-verbatim-query-forward",
"authorized": {
"method": "GET",
"host": "api.example.com",
"path": "/orders",
"params": {}
},
"agent_supplied_url": "https://api.example.com/orders?to=attacker&limit=100",
"expected": {
"action": "reconstruct",
"outgoing_url": "https://api.example.com/orders",
"reason_contains": "query not derivable from authorized action"
}
},
{
"name": "decision-relevant value rides the query and is not in params: dropped",
"property": "no-decision-relevant-from-query",
"authorized": {
"method": "POST",
"host": "api.example.com",
"path": "/orders",
"params": { "amount": 2400, "currency": "USD", "destination": "internal" }
},
"agent_supplied_url": "https://api.example.com/orders?destination=attacker",
"expected": {
"action": "reconstruct",
"outgoing_url": "https://api.example.com/orders",
"reason_contains": "query parameter not represented in params"
}
},
{
"name": "Broker that cannot reconstruct the URL refuses the action",
"property": "refuse-when-irreconstructible",
"authorized": {
"method": "GET",
"host": null,
"path": null,
"params": {}
},
"agent_supplied_url": "https://api.example.com/orders?to=me",
"expected": {
"action": "refuse",
"outgoing_url": null,
"reason_contains": "cannot reconstruct URL"
}
"_about": "CTK vectors for spec §4.2 — the Broker (PEP) query obligation as it applies to a 0.2-preimage implementation. SUPERSEDED at reference >= 0.3.0: the query is folded into the action_fingerprint preimage, so the entire query of an authorised action is derivable from it and a 0.3 broker forwards it (refusing only a #fragment) — the reconstruction behaviour these cases describe no longer applies. Kept for implementations that remain on the 0.2 preimage, where the Broker obligation is the interim defense. Never wired into conformance.py.",
"_status": "superseded",
"_spec": "§4.2 (Broker query obligation, additive) and §10 (0.3 additive hardening)",
"_how_to_apply": "For each case, the Broker has authorized the action described by `authorized.host`/`authorized.path`/`authorized.params`. The Agent presents `agent_supplied_url`. A conformant Broker MUST reconstruct the outgoing URL from the authorized host/path/params and MUST NOT forward the agent-supplied query verbatim. `expected.outgoing_url` is the URL the Broker MUST send (null when it MUST refuse). `expected.action` is `reconstruct` or `refuse`.",
"_note_deferred": "The query-fold shipped in reference 0.3.0: see ctk/vectors/hashing.json (0.3 preimage, including query cases) and hashing-v0.2.json (the 0.2-preimage snapshot).",
"cases": [
{
"name": "agent-supplied query is dropped; URL reconstructed from authorized fields",
"property": "no-verbatim-query-forward",
"authorized": {
"method": "GET",
"host": "api.example.com",
"path": "/orders",
"params": {}
},
"agent_supplied_url": "https://api.example.com/orders?to=attacker&limit=100",
"expected": {
"action": "reconstruct",
"outgoing_url": "https://api.example.com/orders",
"reason_contains": "query not derivable from authorized action"
}
},
{
"name": "decision-relevant value rides the query and is not in params: dropped",
"property": "no-decision-relevant-from-query",
"authorized": {
"method": "POST",
"host": "api.example.com",
"path": "/orders",
"params": {
"amount": 2400,
"currency": "USD",
"destination": "internal"
}
]
},
"agent_supplied_url": "https://api.example.com/orders?destination=attacker",
"expected": {
"action": "reconstruct",
"outgoing_url": "https://api.example.com/orders",
"reason_contains": "query parameter not represented in params"
}
},
{
"name": "Broker that cannot reconstruct the URL refuses the action",
"property": "refuse-when-irreconstructible",
"authorized": {
"method": "GET",
"host": null,
"path": null,
"params": {}
},
"agent_supplied_url": "https://api.example.com/orders?to=me",
"expected": {
"action": "refuse",
"outgoing_url": null,
"reason_contains": "cannot reconstruct URL"
}
}
],
"_superseded_at_reference": ">=0.3.0"
}
47 changes: 47 additions & 0 deletions ctk/vectors/hashing-v0.2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
[
{
"action": {
"instruction": "read my account details",
"method": "GET",
"url": "https://api.example.com/accounts/me",
"params": {}
},
"intent_canonical_json": "{\"instruction\":\"read my account details\"}",
"intent_hash": "ec949034e985a92f3bcd9f9ab8313a80005157698f748f2b8df6163c04af4619",
"fingerprint_canonical_json": "{\"host\":\"api.example.com\",\"method\":\"GET\",\"params\":{},\"path\":\"/accounts/me\"}",
"action_fingerprint": "497e02606ea157ca8ca885cbbd33d1a8a70c40fdaf6f15c5154d858f870b8b61"
},
{
"action": {
"instruction": "place a small order",
"method": "POST",
"url": "https://api.example.com/orders",
"params": {
"amount": 2400,
"currency": "USD",
"destination": "internal"
}
},
"intent_canonical_json": "{\"instruction\":\"place a small order\"}",
"intent_hash": "76f8eef1b97e1213a59eec28cedf15bb999fdb00a3fd17f8343bc4676fdbb4f3",
"fingerprint_canonical_json": "{\"host\":\"api.example.com\",\"method\":\"POST\",\"params\":{\"amount\":2400,\"currency\":\"USD\",\"destination\":\"internal\"},\"path\":\"/orders\"}",
"action_fingerprint": "c70d4ee57957202087887cb5e9d32222977b728bd06947b7761c283b6d4ed394"
},
{
"action": {
"instruction": "place a small order",
"method": "POST",
"url": "https://api.example.com/orders",
"params": {
"amount": 2400,
"currency": "USD",
"destination": "internal",
"recipient": "attacker"
}
},
"intent_canonical_json": "{\"instruction\":\"place a small order\"}",
"intent_hash": "76f8eef1b97e1213a59eec28cedf15bb999fdb00a3fd17f8343bc4676fdbb4f3",
"fingerprint_canonical_json": "{\"host\":\"api.example.com\",\"method\":\"POST\",\"params\":{\"amount\":2400,\"currency\":\"USD\",\"destination\":\"internal\",\"recipient\":\"attacker\"},\"path\":\"/orders\"}",
"action_fingerprint": "dabddc8fc7e8fb30bdec6fb796a336b7897d4a2a12ae386727e2110d7e0e9572"
}
]
Loading
Loading