diff --git a/jcp/SPEC.md b/jcp/SPEC.md index 62713ec16..ea4ec9f58 100644 --- a/jcp/SPEC.md +++ b/jcp/SPEC.md @@ -389,13 +389,14 @@ not configured for the frame's resource MUST be denied. Every decision — allow and deny — seals as a **JCR-1** receipt: canonical det-CBOR inside a COSE_Sign1 envelope (RFC 9052, EdDSA/Ed25519, `alg = -8`). The -`jcp/1` profile signs a flat uint/text core: +`jcp/2` profile signs a flat uint/text core: ``` -jcp_receipt = "jcp/1" +jcp_receipt = "jcp/2" grant_id = hex issuer, subject = did:key resource, action = hex, string +payload_hash = hex ; content id of what was acted upon frame_kind, trust = "tool-call"…, "control" | "data" verdict = "allow" | "deny" deny_reason = string ; omitted on allow @@ -405,8 +406,26 @@ budget_remaining_uj, budget_uj = uint steward = the signer's id (the COSE kid) ``` -Receipts are byte-compatible with the family's JCR-1 verifiers. A deny receipt is -as load-bearing as an allow receipt — the record of a refused attack is evidence. +`payload_hash` binds the receipt to **what the decision was over**, not merely +that a decision was taken. Without it a tool-call receipt does not commit its +arguments, and two decisions differing only in payload seal to identical bytes — +so a payload substitution is undetectable in the audit. A conforming +implementation MUST include it. + +**Profile versioning (REQUIRED).** The predecessor profile `jcp/1` omitted +`payload_hash`. A verifier MUST read the `jcp_receipt` tag from the signed core +and recompute the preimage under **that** profile, so receipts sealed as `jcp/1` +keep verifying; it MUST refuse a tag it does not implement rather than assume a +profile. Implementations MUST NOT mint `jcp/1`. Published `jcp/1` receipts are +retained in [`conformance/v1/legacy_jcp1.json`](conformance/v1/legacy_jcp1.json) +precisely so a verifier can prove it honours the migration. + +Receipts share the family's JCR-1 **envelope** (canonical det-CBOR + +COSE_Sign1/EdDSA) and vocabulary, so family tooling verifies signatures and +decodes cores uniformly. The *core field list is per-profile*: sibling profiles +(`map/1`, Sandbox, proof, jouleclaw's receipt kinds) sign their own cores, and a +change to `jcp/…` does not alter theirs. A deny receipt is as load-bearing as an +allow receipt — the record of a refused attack is evidence. ### 8.1 The receipt stream is the write-ahead log (REQUIRED) @@ -535,4 +554,5 @@ Behavioral scoring MAY be layered above as a secondary signal; it is never the gate. - **C20 — standing grants:** an event-triggered firing mints a strictly-weaker child that traces to the standing grant; the wrong event class, an exhausted firing count, and a firing past the cumulative budget are each refused without advancing the ledger; a policy that would amplify its grant is refused at author time. - **C21 — checkpoints + amendments:** an agent checkpoints its own state; only the grant's issuer — a principal distinct from the agent — may amend, only to tighten; an agent steering itself, a non-issuer amending, a widening amendment, and an amendment bound to a different checkpoint are each refused. +- **C23 — receipt binds its payload + profile migration:** a `jcp/2` receipt commits the `payload_hash` it decided over; substituting only the payload (same grant, action, and cost) fails verification — the forgery a payload-blind receipt could not detect; and receipts published under `jcp/1` still verify, because the verifier recomputes the preimage under the profile the receipt names rather than assuming one. - **C22 — dual ceilings:** a money ceiling beside the joule ceiling is enforced independently — the first to bind denies (`funds-exceeded` / `budget-exceeded`), a foreign-currency cost is refused `currency-mismatch` rather than converted, an allow debits both dimensions and a deny debits neither, and an absent money ceiling encodes byte-identically to a joules-only grant (so its content id and signature are unchanged). Attenuation narrows money independently: dropping, raising, or re-denominating a bounded parent's ceiling is refused; a plain attenuation inherits it. diff --git a/jcp/conformance/v1/dual_ceilings.json b/jcp/conformance/v1/dual_ceilings.json index 10b2f1ecd..b51176e23 100644 --- a/jcp/conformance/v1/dual_ceilings.json +++ b/jcp/conformance/v1/dual_ceilings.json @@ -12,7 +12,7 @@ }, "receipts": [ { - "envelope_hex": "d284581fa201270458196469643a6578616d706c653a6a63702d7265666572656e6365a05901d7ae65747275737467636f6e74726f6c66616374696f6e64726561646669737375657278386469643a6b65793a7a364d6b6f6e334e656364364e6b6b79666f476f48786964327a6e476335394c55334b376d756261526346624c664c5867636f73745f756a0a677374657761726478196469643a6578616d706c653a6a63702d7265666572656e6365677375626a65637478386469643a6b65793a7a364d6b6f39685467674d776a535445614a615055664536747163793278765536426e4e713365336f38715642697948677665726469637465616c6c6f77686772616e745f6964784036303239666136323338626631366230313232373661663664363333373864633233623761353832643237393065353735306639633333353136373633376165687265736f75726365784030663766623535636565393361623937373738323936303865383531666133316536386531326462396466633061383032366164323938333134636633663932696275646765745f756a1a000f42406a6672616d655f6b696e6469746f6f6c2d63616c6c6b6a63705f72656365697074656a63702f3171656e657267795f70726f76656e616e63656748775368756e74736275646765745f72656d61696e696e675f756a1a000f42365840c0418a5886b3220a3212d50e79cc31fc16ad837c90572f82b8e191172349db2360618cb5b587597a4387a007092af0b2f717443a653071c37c7fc6fbbce88701", + "envelope_hex": "d284581fa201270458196469643a6578616d706c653a6a63702d7265666572656e6365a0590226af65747275737467636f6e74726f6c66616374696f6e64726561646669737375657278386469643a6b65793a7a364d6b6f6e334e656364364e6b6b79666f476f48786964327a6e476335394c55334b376d756261526346624c664c5867636f73745f756a0a677374657761726478196469643a6578616d706c653a6a63702d7265666572656e6365677375626a65637478386469643a6b65793a7a364d6b6f39685467674d776a535445614a615055664536747163793278765536426e4e713365336f38715642697948677665726469637465616c6c6f77686772616e745f6964784036303239666136323338626631366230313232373661663664363333373864633233623761353832643237393065353735306639633333353136373633376165687265736f75726365784030663766623535636565393361623937373738323936303865383531666133316536386531326462396466633061383032366164323938333134636633663932696275646765745f756a1a000f42406a6672616d655f6b696e6469746f6f6c2d63616c6c6b6a63705f72656365697074656a63702f326c7061796c6f61645f6861736878406630303364623363386664646333363131636437356364636230353130383630363932336530626331333765393966353361383362666464356338666436643671656e657267795f70726f76656e616e63656748775368756e74736275646765745f72656d61696e696e675f756a1a000f4236584069e269c8401641184d9dec4f289bc75a37b41c526d7d785303fe5d673c38e54fce6df59bf7b75ce6dde602b0b7bfd880946ce5f69dd23a4ee6d602e47ac00b00", "verdict": "allow", "wire": { "action": "read", @@ -23,7 +23,8 @@ "frame_kind": "tool-call", "grant_id": "6029fa6238bf16b012276af6d63378dc23b7a582d2790e5750f9c335167637ae", "issuer": "did:key:z6Mkon3Necd6NkkyfoGoHxid2znGc59LU3K7mubaRcFbLfLX", - "jcp_receipt": "jcp/1", + "jcp_receipt": "jcp/2", + "payload_hash": "f003db3c8fddc3611cd75cdcb05108606923e0bc137e99f53a83bfdd5c8fd6d6", "resource": "0f7fb55cee93ab9777829608e851fa31e68e12db9dfc0a8026ad298314cf3f92", "steward": "did:example:jcp-reference", "subject": "did:key:z6Mko9hTggMwjSTEaJaPUfE6tqcy2xvU6BnNq3e3o8qVBiyH", @@ -32,7 +33,7 @@ } }, { - "envelope_hex": "d284581fa201270458196469643a6578616d706c653a6a63702d7265666572656e6365a05901f1af65747275737467636f6e74726f6c66616374696f6e64726561646669737375657278386469643a6b65793a7a364d6b6f6e334e656364364e6b6b79666f476f48786964327a6e476335394c55334b376d756261526346624c664c5867636f73745f756a0a677374657761726478196469643a6578616d706c653a6a63702d7265666572656e6365677375626a65637478386469643a6b65793a7a364d6b6f39685467674d776a535445614a615055664536747163793278765536426e4e713365336f3871564269794867766572646963746464656e79686772616e745f6964784036303239666136323338626631366230313232373661663664363333373864633233623761353832643237393065353735306639633333353136373633376165687265736f75726365784030663766623535636565393361623937373738323936303865383531666133316536386531326462396466633061383032366164323938333134636633663932696275646765745f756a1a000f42406a6672616d655f6b696e6469746f6f6c2d63616c6c6b64656e795f726561736f6e6e66756e64732d65786365656465646b6a63705f72656365697074656a63702f3171656e657267795f70726f76656e616e63656748775368756e74736275646765745f72656d61696e696e675f756a1a000f42365840843ee35658c68a12d7fc8275c5451544f596040dd7de5ae04b6893ca68de116955b3de20d5d6c0558e26e1d5a6b8c10d32bf75957e4133639aee4930a4261f06", + "envelope_hex": "d284581fa201270458196469643a6578616d706c653a6a63702d7265666572656e6365a0590240b065747275737467636f6e74726f6c66616374696f6e64726561646669737375657278386469643a6b65793a7a364d6b6f6e334e656364364e6b6b79666f476f48786964327a6e476335394c55334b376d756261526346624c664c5867636f73745f756a0a677374657761726478196469643a6578616d706c653a6a63702d7265666572656e6365677375626a65637478386469643a6b65793a7a364d6b6f39685467674d776a535445614a615055664536747163793278765536426e4e713365336f3871564269794867766572646963746464656e79686772616e745f6964784036303239666136323338626631366230313232373661663664363333373864633233623761353832643237393065353735306639633333353136373633376165687265736f75726365784030663766623535636565393361623937373738323936303865383531666133316536386531326462396466633061383032366164323938333134636633663932696275646765745f756a1a000f42406a6672616d655f6b696e6469746f6f6c2d63616c6c6b64656e795f726561736f6e6e66756e64732d65786365656465646b6a63705f72656365697074656a63702f326c7061796c6f61645f6861736878406630303364623363386664646333363131636437356364636230353130383630363932336530626331333765393966353361383362666464356338666436643671656e657267795f70726f76656e616e63656748775368756e74736275646765745f72656d61696e696e675f756a1a000f42365840e7b6f8c47ddb7ac5e7466a730db4e1f56577b5e511ba5d0864d11d02188378f931571a1c02bed41f384dbd3cb53ab2f25da33cbb5604c112643b7f4a3fc2b003", "verdict": "deny", "wire": { "action": "read", @@ -44,7 +45,8 @@ "frame_kind": "tool-call", "grant_id": "6029fa6238bf16b012276af6d63378dc23b7a582d2790e5750f9c335167637ae", "issuer": "did:key:z6Mkon3Necd6NkkyfoGoHxid2znGc59LU3K7mubaRcFbLfLX", - "jcp_receipt": "jcp/1", + "jcp_receipt": "jcp/2", + "payload_hash": "f003db3c8fddc3611cd75cdcb05108606923e0bc137e99f53a83bfdd5c8fd6d6", "resource": "0f7fb55cee93ab9777829608e851fa31e68e12db9dfc0a8026ad298314cf3f92", "steward": "did:example:jcp-reference", "subject": "did:key:z6Mko9hTggMwjSTEaJaPUfE6tqcy2xvU6BnNq3e3o8qVBiyH", @@ -53,7 +55,7 @@ } }, { - "envelope_hex": "d284581fa201270458196469643a6578616d706c653a6a63702d7265666572656e6365a05901f4af65747275737467636f6e74726f6c66616374696f6e64726561646669737375657278386469643a6b65793a7a364d6b6f6e334e656364364e6b6b79666f476f48786964327a6e476335394c55334b376d756261526346624c664c5867636f73745f756a01677374657761726478196469643a6578616d706c653a6a63702d7265666572656e6365677375626a65637478386469643a6b65793a7a364d6b6f39685467674d776a535445614a615055664536747163793278765536426e4e713365336f3871564269794867766572646963746464656e79686772616e745f6964784036303239666136323338626631366230313232373661663664363333373864633233623761353832643237393065353735306639633333353136373633376165687265736f75726365784030663766623535636565393361623937373738323936303865383531666133316536386531326462396466633061383032366164323938333134636633663932696275646765745f756a1a000f42406a6672616d655f6b696e6469746f6f6c2d63616c6c6b64656e795f726561736f6e7163757272656e63792d6d69736d617463686b6a63705f72656365697074656a63702f3171656e657267795f70726f76656e616e63656748775368756e74736275646765745f72656d61696e696e675f756a1a000f42365840ab174bbca1b41c89beb590b3b68fa8eefdf8e0e6610061001cff93f718b5d27374a0837952d77310f20a2dbd4e85545839c6898c0884b080152b014217f84707", + "envelope_hex": "d284581fa201270458196469643a6578616d706c653a6a63702d7265666572656e6365a0590243b065747275737467636f6e74726f6c66616374696f6e64726561646669737375657278386469643a6b65793a7a364d6b6f6e334e656364364e6b6b79666f476f48786964327a6e476335394c55334b376d756261526346624c664c5867636f73745f756a01677374657761726478196469643a6578616d706c653a6a63702d7265666572656e6365677375626a65637478386469643a6b65793a7a364d6b6f39685467674d776a535445614a615055664536747163793278765536426e4e713365336f3871564269794867766572646963746464656e79686772616e745f6964784036303239666136323338626631366230313232373661663664363333373864633233623761353832643237393065353735306639633333353136373633376165687265736f75726365784030663766623535636565393361623937373738323936303865383531666133316536386531326462396466633061383032366164323938333134636633663932696275646765745f756a1a000f42406a6672616d655f6b696e6469746f6f6c2d63616c6c6b64656e795f726561736f6e7163757272656e63792d6d69736d617463686b6a63705f72656365697074656a63702f326c7061796c6f61645f6861736878406630303364623363386664646333363131636437356364636230353130383630363932336530626331333765393966353361383362666464356338666436643671656e657267795f70726f76656e616e63656748775368756e74736275646765745f72656d61696e696e675f756a1a000f4236584065e1e5037b3575c22dbeef1f8d86d79f7d3f23f1786567a6f48ccb4b1ae3a04ea92b8e19b723f59c5e82288cb80559975fe4e88ef67dd8091c6e52b9f1c33609", "verdict": "deny", "wire": { "action": "read", @@ -65,7 +67,8 @@ "frame_kind": "tool-call", "grant_id": "6029fa6238bf16b012276af6d63378dc23b7a582d2790e5750f9c335167637ae", "issuer": "did:key:z6Mkon3Necd6NkkyfoGoHxid2znGc59LU3K7mubaRcFbLfLX", - "jcp_receipt": "jcp/1", + "jcp_receipt": "jcp/2", + "payload_hash": "f003db3c8fddc3611cd75cdcb05108606923e0bc137e99f53a83bfdd5c8fd6d6", "resource": "0f7fb55cee93ab9777829608e851fa31e68e12db9dfc0a8026ad298314cf3f92", "steward": "did:example:jcp-reference", "subject": "did:key:z6Mko9hTggMwjSTEaJaPUfE6tqcy2xvU6BnNq3e3o8qVBiyH", diff --git a/jcp/conformance/v1/legacy_jcp1.json b/jcp/conformance/v1/legacy_jcp1.json new file mode 100644 index 000000000..af0a941d1 --- /dev/null +++ b/jcp/conformance/v1/legacy_jcp1.json @@ -0,0 +1,55 @@ +{ + "note": "Receipts sealed under the superseded jcp/1 profile, published so a verifier can prove the jcp/2 migration path: these MUST still verify (SPEC \u00a78 version dispatch). Not minted any more.", + "profile": "jcp/1", + "grant": { + "id_hex": "3b1a5a9cbb781514cf751ba007e077e8083977b4f07b104d36ee904d928998c4", + "issuer": "did:key:z6Mkon3Necd6NkkyfoGoHxid2znGc59LU3K7mubaRcFbLfLX", + "signature_hex": "69a2767b437d72fe525efe5cfbcba4868ec881165c977aada53b0f6537ad3d36fc26be24cb62eb5f8d14f786bdbcc068bbef74529613bba1e418cb0527683d0d", + "subject": "did:key:z6Mko9hTggMwjSTEaJaPUfE6tqcy2xvU6BnNq3e3o8qVBiyH" + }, + "receipts": [ + { + "envelope_hex": "d284581fa201270458196469643a6578616d706c653a6a63702d7265666572656e6365a05901d2ae65747275737467636f6e74726f6c66616374696f6e64726561646669737375657278386469643a6b65793a7a364d6b6f6e334e656364364e6b6b79666f476f48786964327a6e476335394c55334b376d756261526346624c664c5867636f73745f756a181e677374657761726478196469643a6578616d706c653a6a63702d7265666572656e6365677375626a65637478386469643a6b65793a7a364d6b6f39685467674d776a535445614a615055664536747163793278765536426e4e713365336f38715642697948677665726469637465616c6c6f77686772616e745f6964784033623161356139636262373831353134636637353162613030376530373765383038333937376234663037623130346433366565393034643932383939386334687265736f75726365784030663766623535636565393361623937373738323936303865383531666133316536386531326462396466633061383032366164323938333134636633663932696275646765745f756a18646a6672616d655f6b696e6469746f6f6c2d63616c6c6b6a63705f72656365697074656a63702f3171656e657267795f70726f76656e616e63656748775368756e74736275646765745f72656d61696e696e675f756a184658405a7be56b33956c069d4e23127baa7b090e5028b9704dff6ca5ac46da546534e667151f80d7d7a605bcc31ea2bb33ec971e3e8b8794f7248a18b34c33bc438509", + "signed_core_hex": "ae65747275737467636f6e74726f6c66616374696f6e64726561646669737375657278386469643a6b65793a7a364d6b6f6e334e656364364e6b6b79666f476f48786964327a6e476335394c55334b376d756261526346624c664c5867636f73745f756a181e677374657761726478196469643a6578616d706c653a6a63702d7265666572656e6365677375626a65637478386469643a6b65793a7a364d6b6f39685467674d776a535445614a615055664536747163793278765536426e4e713365336f38715642697948677665726469637465616c6c6f77686772616e745f6964784033623161356139636262373831353134636637353162613030376530373765383038333937376234663037623130346433366565393034643932383939386334687265736f75726365784030663766623535636565393361623937373738323936303865383531666133316536386531326462396466633061383032366164323938333134636633663932696275646765745f756a18646a6672616d655f6b696e6469746f6f6c2d63616c6c6b6a63705f72656365697074656a63702f3171656e657267795f70726f76656e616e63656748775368756e74736275646765745f72656d61696e696e675f756a1846", + "verdict": "allow", + "wire": { + "action": "read", + "budget_remaining_uj": 70, + "budget_uj": 100, + "cost_uj": 30, + "energy_provenance": "HwShunt", + "frame_kind": "tool-call", + "grant_id": "3b1a5a9cbb781514cf751ba007e077e8083977b4f07b104d36ee904d928998c4", + "issuer": "did:key:z6Mkon3Necd6NkkyfoGoHxid2znGc59LU3K7mubaRcFbLfLX", + "jcp_receipt": "jcp/1", + "resource": "0f7fb55cee93ab9777829608e851fa31e68e12db9dfc0a8026ad298314cf3f92", + "steward": "did:example:jcp-reference", + "subject": "did:key:z6Mko9hTggMwjSTEaJaPUfE6tqcy2xvU6BnNq3e3o8qVBiyH", + "trust": "control", + "verdict": "allow" + } + }, + { + "envelope_hex": "d284581fa201270458196469643a6578616d706c653a6a63702d7265666572656e6365a05901ebaf657472757374646461746166616374696f6e64726561646669737375657278386469643a6b65793a7a364d6b6f6e334e656364364e6b6b79666f476f48786964327a6e476335394c55334b376d756261526346624c664c5867636f73745f756a01677374657761726478196469643a6578616d706c653a6a63702d7265666572656e6365677375626a65637478386469643a6b65793a7a364d6b6f39685467674d776a535445614a615055664536747163793278765536426e4e713365336f3871564269794867766572646963746464656e79686772616e745f6964784033623161356139636262373831353134636637353162613030376530373765383038333937376234663037623130346433366565393034643932383939386334687265736f75726365784030663766623535636565393361623937373738323936303865383531666133316536386531326462396466633061383032366164323938333134636633663932696275646765745f756a18646a6672616d655f6b696e6469746f6f6c2d63616c6c6b64656e795f726561736f6e71756e747275737465642d636f6e74726f6c6b6a63705f72656365697074656a63702f3171656e657267795f70726f76656e616e63656748775368756e74736275646765745f72656d61696e696e675f756a18465840f9f2fbdebec820c75f9257575f9c0c512d122aa52ce1cdcc7510a69282d327d59decf6f282542ad4f8e050670e1104e33ba4175b812900a90eee27ca1b110c0c", + "signed_core_hex": "af657472757374646461746166616374696f6e64726561646669737375657278386469643a6b65793a7a364d6b6f6e334e656364364e6b6b79666f476f48786964327a6e476335394c55334b376d756261526346624c664c5867636f73745f756a01677374657761726478196469643a6578616d706c653a6a63702d7265666572656e6365677375626a65637478386469643a6b65793a7a364d6b6f39685467674d776a535445614a615055664536747163793278765536426e4e713365336f3871564269794867766572646963746464656e79686772616e745f6964784033623161356139636262373831353134636637353162613030376530373765383038333937376234663037623130346433366565393034643932383939386334687265736f75726365784030663766623535636565393361623937373738323936303865383531666133316536386531326462396466633061383032366164323938333134636633663932696275646765745f756a18646a6672616d655f6b696e6469746f6f6c2d63616c6c6b64656e795f726561736f6e71756e747275737465642d636f6e74726f6c6b6a63705f72656365697074656a63702f3171656e657267795f70726f76656e616e63656748775368756e74736275646765745f72656d61696e696e675f756a1846", + "verdict": "deny", + "wire": { + "action": "read", + "budget_remaining_uj": 70, + "budget_uj": 100, + "cost_uj": 1, + "deny_reason": "untrusted-control", + "energy_provenance": "HwShunt", + "frame_kind": "tool-call", + "grant_id": "3b1a5a9cbb781514cf751ba007e077e8083977b4f07b104d36ee904d928998c4", + "issuer": "did:key:z6Mkon3Necd6NkkyfoGoHxid2znGc59LU3K7mubaRcFbLfLX", + "jcp_receipt": "jcp/1", + "resource": "0f7fb55cee93ab9777829608e851fa31e68e12db9dfc0a8026ad298314cf3f92", + "steward": "did:example:jcp-reference", + "subject": "did:key:z6Mko9hTggMwjSTEaJaPUfE6tqcy2xvU6BnNq3e3o8qVBiyH", + "trust": "data", + "verdict": "deny" + } + } + ] +} diff --git a/jcp/conformance/v1/vectors.json b/jcp/conformance/v1/vectors.json index 17e5b03b1..c31937c5f 100644 --- a/jcp/conformance/v1/vectors.json +++ b/jcp/conformance/v1/vectors.json @@ -7,8 +7,8 @@ }, "receipts": [ { - "envelope_hex": "d284581fa201270458196469643a6578616d706c653a6a63702d7265666572656e6365a05901d2ae65747275737467636f6e74726f6c66616374696f6e64726561646669737375657278386469643a6b65793a7a364d6b6f6e334e656364364e6b6b79666f476f48786964327a6e476335394c55334b376d756261526346624c664c5867636f73745f756a181e677374657761726478196469643a6578616d706c653a6a63702d7265666572656e6365677375626a65637478386469643a6b65793a7a364d6b6f39685467674d776a535445614a615055664536747163793278765536426e4e713365336f38715642697948677665726469637465616c6c6f77686772616e745f6964784033623161356139636262373831353134636637353162613030376530373765383038333937376234663037623130346433366565393034643932383939386334687265736f75726365784030663766623535636565393361623937373738323936303865383531666133316536386531326462396466633061383032366164323938333134636633663932696275646765745f756a18646a6672616d655f6b696e6469746f6f6c2d63616c6c6b6a63705f72656365697074656a63702f3171656e657267795f70726f76656e616e63656748775368756e74736275646765745f72656d61696e696e675f756a184658405a7be56b33956c069d4e23127baa7b090e5028b9704dff6ca5ac46da546534e667151f80d7d7a605bcc31ea2bb33ec971e3e8b8794f7248a18b34c33bc438509", - "signed_core_hex": "ae65747275737467636f6e74726f6c66616374696f6e64726561646669737375657278386469643a6b65793a7a364d6b6f6e334e656364364e6b6b79666f476f48786964327a6e476335394c55334b376d756261526346624c664c5867636f73745f756a181e677374657761726478196469643a6578616d706c653a6a63702d7265666572656e6365677375626a65637478386469643a6b65793a7a364d6b6f39685467674d776a535445614a615055664536747163793278765536426e4e713365336f38715642697948677665726469637465616c6c6f77686772616e745f6964784033623161356139636262373831353134636637353162613030376530373765383038333937376234663037623130346433366565393034643932383939386334687265736f75726365784030663766623535636565393361623937373738323936303865383531666133316536386531326462396466633061383032366164323938333134636633663932696275646765745f756a18646a6672616d655f6b696e6469746f6f6c2d63616c6c6b6a63705f72656365697074656a63702f3171656e657267795f70726f76656e616e63656748775368756e74736275646765745f72656d61696e696e675f756a1846", + "envelope_hex": "d284581fa201270458196469643a6578616d706c653a6a63702d7265666572656e6365a0590221af65747275737467636f6e74726f6c66616374696f6e64726561646669737375657278386469643a6b65793a7a364d6b6f6e334e656364364e6b6b79666f476f48786964327a6e476335394c55334b376d756261526346624c664c5867636f73745f756a181e677374657761726478196469643a6578616d706c653a6a63702d7265666572656e6365677375626a65637478386469643a6b65793a7a364d6b6f39685467674d776a535445614a615055664536747163793278765536426e4e713365336f38715642697948677665726469637465616c6c6f77686772616e745f6964784033623161356139636262373831353134636637353162613030376530373765383038333937376234663037623130346433366565393034643932383939386334687265736f75726365784030663766623535636565393361623937373738323936303865383531666133316536386531326462396466633061383032366164323938333134636633663932696275646765745f756a18646a6672616d655f6b696e6469746f6f6c2d63616c6c6b6a63705f72656365697074656a63702f326c7061796c6f61645f6861736878406630303364623363386664646333363131636437356364636230353130383630363932336530626331333765393966353361383362666464356338666436643671656e657267795f70726f76656e616e63656748775368756e74736275646765745f72656d61696e696e675f756a1846584063ce24c2cb36a7137020d2ed89a4385ef91af98f72cbb43a5294a8f295c6e738fa73f87d1077461ac75d459cd0dad28d80fc50615935221d98c8ec6d467af908", + "signed_core_hex": "af65747275737467636f6e74726f6c66616374696f6e64726561646669737375657278386469643a6b65793a7a364d6b6f6e334e656364364e6b6b79666f476f48786964327a6e476335394c55334b376d756261526346624c664c5867636f73745f756a181e677374657761726478196469643a6578616d706c653a6a63702d7265666572656e6365677375626a65637478386469643a6b65793a7a364d6b6f39685467674d776a535445614a615055664536747163793278765536426e4e713365336f38715642697948677665726469637465616c6c6f77686772616e745f6964784033623161356139636262373831353134636637353162613030376530373765383038333937376234663037623130346433366565393034643932383939386334687265736f75726365784030663766623535636565393361623937373738323936303865383531666133316536386531326462396466633061383032366164323938333134636633663932696275646765745f756a18646a6672616d655f6b696e6469746f6f6c2d63616c6c6b6a63705f72656365697074656a63702f326c7061796c6f61645f6861736878406630303364623363386664646333363131636437356364636230353130383630363932336530626331333765393966353361383362666464356338666436643671656e657267795f70726f76656e616e63656748775368756e74736275646765745f72656d61696e696e675f756a1846", "verdict": "allow", "wire": { "action": "read", @@ -19,7 +19,8 @@ "frame_kind": "tool-call", "grant_id": "3b1a5a9cbb781514cf751ba007e077e8083977b4f07b104d36ee904d928998c4", "issuer": "did:key:z6Mkon3Necd6NkkyfoGoHxid2znGc59LU3K7mubaRcFbLfLX", - "jcp_receipt": "jcp/1", + "jcp_receipt": "jcp/2", + "payload_hash": "f003db3c8fddc3611cd75cdcb05108606923e0bc137e99f53a83bfdd5c8fd6d6", "resource": "0f7fb55cee93ab9777829608e851fa31e68e12db9dfc0a8026ad298314cf3f92", "steward": "did:example:jcp-reference", "subject": "did:key:z6Mko9hTggMwjSTEaJaPUfE6tqcy2xvU6BnNq3e3o8qVBiyH", @@ -28,8 +29,8 @@ } }, { - "envelope_hex": "d284581fa201270458196469643a6578616d706c653a6a63702d7265666572656e6365a05901ebaf657472757374646461746166616374696f6e64726561646669737375657278386469643a6b65793a7a364d6b6f6e334e656364364e6b6b79666f476f48786964327a6e476335394c55334b376d756261526346624c664c5867636f73745f756a01677374657761726478196469643a6578616d706c653a6a63702d7265666572656e6365677375626a65637478386469643a6b65793a7a364d6b6f39685467674d776a535445614a615055664536747163793278765536426e4e713365336f3871564269794867766572646963746464656e79686772616e745f6964784033623161356139636262373831353134636637353162613030376530373765383038333937376234663037623130346433366565393034643932383939386334687265736f75726365784030663766623535636565393361623937373738323936303865383531666133316536386531326462396466633061383032366164323938333134636633663932696275646765745f756a18646a6672616d655f6b696e6469746f6f6c2d63616c6c6b64656e795f726561736f6e71756e747275737465642d636f6e74726f6c6b6a63705f72656365697074656a63702f3171656e657267795f70726f76656e616e63656748775368756e74736275646765745f72656d61696e696e675f756a18465840f9f2fbdebec820c75f9257575f9c0c512d122aa52ce1cdcc7510a69282d327d59decf6f282542ad4f8e050670e1104e33ba4175b812900a90eee27ca1b110c0c", - "signed_core_hex": "af657472757374646461746166616374696f6e64726561646669737375657278386469643a6b65793a7a364d6b6f6e334e656364364e6b6b79666f476f48786964327a6e476335394c55334b376d756261526346624c664c5867636f73745f756a01677374657761726478196469643a6578616d706c653a6a63702d7265666572656e6365677375626a65637478386469643a6b65793a7a364d6b6f39685467674d776a535445614a615055664536747163793278765536426e4e713365336f3871564269794867766572646963746464656e79686772616e745f6964784033623161356139636262373831353134636637353162613030376530373765383038333937376234663037623130346433366565393034643932383939386334687265736f75726365784030663766623535636565393361623937373738323936303865383531666133316536386531326462396466633061383032366164323938333134636633663932696275646765745f756a18646a6672616d655f6b696e6469746f6f6c2d63616c6c6b64656e795f726561736f6e71756e747275737465642d636f6e74726f6c6b6a63705f72656365697074656a63702f3171656e657267795f70726f76656e616e63656748775368756e74736275646765745f72656d61696e696e675f756a1846", + "envelope_hex": "d284581fa201270458196469643a6578616d706c653a6a63702d7265666572656e6365a059023ab0657472757374646461746166616374696f6e64726561646669737375657278386469643a6b65793a7a364d6b6f6e334e656364364e6b6b79666f476f48786964327a6e476335394c55334b376d756261526346624c664c5867636f73745f756a01677374657761726478196469643a6578616d706c653a6a63702d7265666572656e6365677375626a65637478386469643a6b65793a7a364d6b6f39685467674d776a535445614a615055664536747163793278765536426e4e713365336f3871564269794867766572646963746464656e79686772616e745f6964784033623161356139636262373831353134636637353162613030376530373765383038333937376234663037623130346433366565393034643932383939386334687265736f75726365784030663766623535636565393361623937373738323936303865383531666133316536386531326462396466633061383032366164323938333134636633663932696275646765745f756a18646a6672616d655f6b696e6469746f6f6c2d63616c6c6b64656e795f726561736f6e71756e747275737465642d636f6e74726f6c6b6a63705f72656365697074656a63702f326c7061796c6f61645f6861736878406630303364623363386664646333363131636437356364636230353130383630363932336530626331333765393966353361383362666464356338666436643671656e657267795f70726f76656e616e63656748775368756e74736275646765745f72656d61696e696e675f756a184658402f2474c33b87f45b34873814f462a23b662ab4eeb09c23c93a3227fb892b67e87ee7aa68099a830eb56d252b00265adc2063c098587856525729df92f198fa03", + "signed_core_hex": "b0657472757374646461746166616374696f6e64726561646669737375657278386469643a6b65793a7a364d6b6f6e334e656364364e6b6b79666f476f48786964327a6e476335394c55334b376d756261526346624c664c5867636f73745f756a01677374657761726478196469643a6578616d706c653a6a63702d7265666572656e6365677375626a65637478386469643a6b65793a7a364d6b6f39685467674d776a535445614a615055664536747163793278765536426e4e713365336f3871564269794867766572646963746464656e79686772616e745f6964784033623161356139636262373831353134636637353162613030376530373765383038333937376234663037623130346433366565393034643932383939386334687265736f75726365784030663766623535636565393361623937373738323936303865383531666133316536386531326462396466633061383032366164323938333134636633663932696275646765745f756a18646a6672616d655f6b696e6469746f6f6c2d63616c6c6b64656e795f726561736f6e71756e747275737465642d636f6e74726f6c6b6a63705f72656365697074656a63702f326c7061796c6f61645f6861736878406630303364623363386664646333363131636437356364636230353130383630363932336530626331333765393966353361383362666464356338666436643671656e657267795f70726f76656e616e63656748775368756e74736275646765745f72656d61696e696e675f756a1846", "verdict": "deny", "wire": { "action": "read", @@ -41,7 +42,8 @@ "frame_kind": "tool-call", "grant_id": "3b1a5a9cbb781514cf751ba007e077e8083977b4f07b104d36ee904d928998c4", "issuer": "did:key:z6Mkon3Necd6NkkyfoGoHxid2znGc59LU3K7mubaRcFbLfLX", - "jcp_receipt": "jcp/1", + "jcp_receipt": "jcp/2", + "payload_hash": "f003db3c8fddc3611cd75cdcb05108606923e0bc137e99f53a83bfdd5c8fd6d6", "resource": "0f7fb55cee93ab9777829608e851fa31e68e12db9dfc0a8026ad298314cf3f92", "steward": "did:example:jcp-reference", "subject": "did:key:z6Mko9hTggMwjSTEaJaPUfE6tqcy2xvU6BnNq3e3o8qVBiyH", diff --git a/jcp/jcp-rs/conformance/v1/vectors.json b/jcp/jcp-rs/conformance/v1/vectors.json index 17e5b03b1..c31937c5f 100644 --- a/jcp/jcp-rs/conformance/v1/vectors.json +++ b/jcp/jcp-rs/conformance/v1/vectors.json @@ -7,8 +7,8 @@ }, "receipts": [ { - "envelope_hex": "d284581fa201270458196469643a6578616d706c653a6a63702d7265666572656e6365a05901d2ae65747275737467636f6e74726f6c66616374696f6e64726561646669737375657278386469643a6b65793a7a364d6b6f6e334e656364364e6b6b79666f476f48786964327a6e476335394c55334b376d756261526346624c664c5867636f73745f756a181e677374657761726478196469643a6578616d706c653a6a63702d7265666572656e6365677375626a65637478386469643a6b65793a7a364d6b6f39685467674d776a535445614a615055664536747163793278765536426e4e713365336f38715642697948677665726469637465616c6c6f77686772616e745f6964784033623161356139636262373831353134636637353162613030376530373765383038333937376234663037623130346433366565393034643932383939386334687265736f75726365784030663766623535636565393361623937373738323936303865383531666133316536386531326462396466633061383032366164323938333134636633663932696275646765745f756a18646a6672616d655f6b696e6469746f6f6c2d63616c6c6b6a63705f72656365697074656a63702f3171656e657267795f70726f76656e616e63656748775368756e74736275646765745f72656d61696e696e675f756a184658405a7be56b33956c069d4e23127baa7b090e5028b9704dff6ca5ac46da546534e667151f80d7d7a605bcc31ea2bb33ec971e3e8b8794f7248a18b34c33bc438509", - "signed_core_hex": "ae65747275737467636f6e74726f6c66616374696f6e64726561646669737375657278386469643a6b65793a7a364d6b6f6e334e656364364e6b6b79666f476f48786964327a6e476335394c55334b376d756261526346624c664c5867636f73745f756a181e677374657761726478196469643a6578616d706c653a6a63702d7265666572656e6365677375626a65637478386469643a6b65793a7a364d6b6f39685467674d776a535445614a615055664536747163793278765536426e4e713365336f38715642697948677665726469637465616c6c6f77686772616e745f6964784033623161356139636262373831353134636637353162613030376530373765383038333937376234663037623130346433366565393034643932383939386334687265736f75726365784030663766623535636565393361623937373738323936303865383531666133316536386531326462396466633061383032366164323938333134636633663932696275646765745f756a18646a6672616d655f6b696e6469746f6f6c2d63616c6c6b6a63705f72656365697074656a63702f3171656e657267795f70726f76656e616e63656748775368756e74736275646765745f72656d61696e696e675f756a1846", + "envelope_hex": "d284581fa201270458196469643a6578616d706c653a6a63702d7265666572656e6365a0590221af65747275737467636f6e74726f6c66616374696f6e64726561646669737375657278386469643a6b65793a7a364d6b6f6e334e656364364e6b6b79666f476f48786964327a6e476335394c55334b376d756261526346624c664c5867636f73745f756a181e677374657761726478196469643a6578616d706c653a6a63702d7265666572656e6365677375626a65637478386469643a6b65793a7a364d6b6f39685467674d776a535445614a615055664536747163793278765536426e4e713365336f38715642697948677665726469637465616c6c6f77686772616e745f6964784033623161356139636262373831353134636637353162613030376530373765383038333937376234663037623130346433366565393034643932383939386334687265736f75726365784030663766623535636565393361623937373738323936303865383531666133316536386531326462396466633061383032366164323938333134636633663932696275646765745f756a18646a6672616d655f6b696e6469746f6f6c2d63616c6c6b6a63705f72656365697074656a63702f326c7061796c6f61645f6861736878406630303364623363386664646333363131636437356364636230353130383630363932336530626331333765393966353361383362666464356338666436643671656e657267795f70726f76656e616e63656748775368756e74736275646765745f72656d61696e696e675f756a1846584063ce24c2cb36a7137020d2ed89a4385ef91af98f72cbb43a5294a8f295c6e738fa73f87d1077461ac75d459cd0dad28d80fc50615935221d98c8ec6d467af908", + "signed_core_hex": "af65747275737467636f6e74726f6c66616374696f6e64726561646669737375657278386469643a6b65793a7a364d6b6f6e334e656364364e6b6b79666f476f48786964327a6e476335394c55334b376d756261526346624c664c5867636f73745f756a181e677374657761726478196469643a6578616d706c653a6a63702d7265666572656e6365677375626a65637478386469643a6b65793a7a364d6b6f39685467674d776a535445614a615055664536747163793278765536426e4e713365336f38715642697948677665726469637465616c6c6f77686772616e745f6964784033623161356139636262373831353134636637353162613030376530373765383038333937376234663037623130346433366565393034643932383939386334687265736f75726365784030663766623535636565393361623937373738323936303865383531666133316536386531326462396466633061383032366164323938333134636633663932696275646765745f756a18646a6672616d655f6b696e6469746f6f6c2d63616c6c6b6a63705f72656365697074656a63702f326c7061796c6f61645f6861736878406630303364623363386664646333363131636437356364636230353130383630363932336530626331333765393966353361383362666464356338666436643671656e657267795f70726f76656e616e63656748775368756e74736275646765745f72656d61696e696e675f756a1846", "verdict": "allow", "wire": { "action": "read", @@ -19,7 +19,8 @@ "frame_kind": "tool-call", "grant_id": "3b1a5a9cbb781514cf751ba007e077e8083977b4f07b104d36ee904d928998c4", "issuer": "did:key:z6Mkon3Necd6NkkyfoGoHxid2znGc59LU3K7mubaRcFbLfLX", - "jcp_receipt": "jcp/1", + "jcp_receipt": "jcp/2", + "payload_hash": "f003db3c8fddc3611cd75cdcb05108606923e0bc137e99f53a83bfdd5c8fd6d6", "resource": "0f7fb55cee93ab9777829608e851fa31e68e12db9dfc0a8026ad298314cf3f92", "steward": "did:example:jcp-reference", "subject": "did:key:z6Mko9hTggMwjSTEaJaPUfE6tqcy2xvU6BnNq3e3o8qVBiyH", @@ -28,8 +29,8 @@ } }, { - "envelope_hex": "d284581fa201270458196469643a6578616d706c653a6a63702d7265666572656e6365a05901ebaf657472757374646461746166616374696f6e64726561646669737375657278386469643a6b65793a7a364d6b6f6e334e656364364e6b6b79666f476f48786964327a6e476335394c55334b376d756261526346624c664c5867636f73745f756a01677374657761726478196469643a6578616d706c653a6a63702d7265666572656e6365677375626a65637478386469643a6b65793a7a364d6b6f39685467674d776a535445614a615055664536747163793278765536426e4e713365336f3871564269794867766572646963746464656e79686772616e745f6964784033623161356139636262373831353134636637353162613030376530373765383038333937376234663037623130346433366565393034643932383939386334687265736f75726365784030663766623535636565393361623937373738323936303865383531666133316536386531326462396466633061383032366164323938333134636633663932696275646765745f756a18646a6672616d655f6b696e6469746f6f6c2d63616c6c6b64656e795f726561736f6e71756e747275737465642d636f6e74726f6c6b6a63705f72656365697074656a63702f3171656e657267795f70726f76656e616e63656748775368756e74736275646765745f72656d61696e696e675f756a18465840f9f2fbdebec820c75f9257575f9c0c512d122aa52ce1cdcc7510a69282d327d59decf6f282542ad4f8e050670e1104e33ba4175b812900a90eee27ca1b110c0c", - "signed_core_hex": "af657472757374646461746166616374696f6e64726561646669737375657278386469643a6b65793a7a364d6b6f6e334e656364364e6b6b79666f476f48786964327a6e476335394c55334b376d756261526346624c664c5867636f73745f756a01677374657761726478196469643a6578616d706c653a6a63702d7265666572656e6365677375626a65637478386469643a6b65793a7a364d6b6f39685467674d776a535445614a615055664536747163793278765536426e4e713365336f3871564269794867766572646963746464656e79686772616e745f6964784033623161356139636262373831353134636637353162613030376530373765383038333937376234663037623130346433366565393034643932383939386334687265736f75726365784030663766623535636565393361623937373738323936303865383531666133316536386531326462396466633061383032366164323938333134636633663932696275646765745f756a18646a6672616d655f6b696e6469746f6f6c2d63616c6c6b64656e795f726561736f6e71756e747275737465642d636f6e74726f6c6b6a63705f72656365697074656a63702f3171656e657267795f70726f76656e616e63656748775368756e74736275646765745f72656d61696e696e675f756a1846", + "envelope_hex": "d284581fa201270458196469643a6578616d706c653a6a63702d7265666572656e6365a059023ab0657472757374646461746166616374696f6e64726561646669737375657278386469643a6b65793a7a364d6b6f6e334e656364364e6b6b79666f476f48786964327a6e476335394c55334b376d756261526346624c664c5867636f73745f756a01677374657761726478196469643a6578616d706c653a6a63702d7265666572656e6365677375626a65637478386469643a6b65793a7a364d6b6f39685467674d776a535445614a615055664536747163793278765536426e4e713365336f3871564269794867766572646963746464656e79686772616e745f6964784033623161356139636262373831353134636637353162613030376530373765383038333937376234663037623130346433366565393034643932383939386334687265736f75726365784030663766623535636565393361623937373738323936303865383531666133316536386531326462396466633061383032366164323938333134636633663932696275646765745f756a18646a6672616d655f6b696e6469746f6f6c2d63616c6c6b64656e795f726561736f6e71756e747275737465642d636f6e74726f6c6b6a63705f72656365697074656a63702f326c7061796c6f61645f6861736878406630303364623363386664646333363131636437356364636230353130383630363932336530626331333765393966353361383362666464356338666436643671656e657267795f70726f76656e616e63656748775368756e74736275646765745f72656d61696e696e675f756a184658402f2474c33b87f45b34873814f462a23b662ab4eeb09c23c93a3227fb892b67e87ee7aa68099a830eb56d252b00265adc2063c098587856525729df92f198fa03", + "signed_core_hex": "b0657472757374646461746166616374696f6e64726561646669737375657278386469643a6b65793a7a364d6b6f6e334e656364364e6b6b79666f476f48786964327a6e476335394c55334b376d756261526346624c664c5867636f73745f756a01677374657761726478196469643a6578616d706c653a6a63702d7265666572656e6365677375626a65637478386469643a6b65793a7a364d6b6f39685467674d776a535445614a615055664536747163793278765536426e4e713365336f3871564269794867766572646963746464656e79686772616e745f6964784033623161356139636262373831353134636637353162613030376530373765383038333937376234663037623130346433366565393034643932383939386334687265736f75726365784030663766623535636565393361623937373738323936303865383531666133316536386531326462396466633061383032366164323938333134636633663932696275646765745f756a18646a6672616d655f6b696e6469746f6f6c2d63616c6c6b64656e795f726561736f6e71756e747275737465642d636f6e74726f6c6b6a63705f72656365697074656a63702f326c7061796c6f61645f6861736878406630303364623363386664646333363131636437356364636230353130383630363932336530626331333765393966353361383362666464356338666436643671656e657267795f70726f76656e616e63656748775368756e74736275646765745f72656d61696e696e675f756a1846", "verdict": "deny", "wire": { "action": "read", @@ -41,7 +42,8 @@ "frame_kind": "tool-call", "grant_id": "3b1a5a9cbb781514cf751ba007e077e8083977b4f07b104d36ee904d928998c4", "issuer": "did:key:z6Mkon3Necd6NkkyfoGoHxid2znGc59LU3K7mubaRcFbLfLX", - "jcp_receipt": "jcp/1", + "jcp_receipt": "jcp/2", + "payload_hash": "f003db3c8fddc3611cd75cdcb05108606923e0bc137e99f53a83bfdd5c8fd6d6", "resource": "0f7fb55cee93ab9777829608e851fa31e68e12db9dfc0a8026ad298314cf3f92", "steward": "did:example:jcp-reference", "subject": "did:key:z6Mko9hTggMwjSTEaJaPUfE6tqcy2xvU6BnNq3e3o8qVBiyH", diff --git a/jcp/jcp-rs/crates/jcp-conformance/src/lib.rs b/jcp/jcp-rs/crates/jcp-conformance/src/lib.rs index 152014350..1871bd809 100644 --- a/jcp/jcp-rs/crates/jcp-conformance/src/lib.rs +++ b/jcp/jcp-rs/crates/jcp-conformance/src/lib.rs @@ -64,6 +64,7 @@ pub fn checks() -> Vec { Check { id: "C20 standing grant: bounded event-triggered mint", run: c20_standing }, Check { id: "C21 checkpoint + amendment under separation of duties", run: c21_checkpoint }, Check { id: "C22 dual ceilings: money beside joules, no rate invented", run: c22_dual_ceilings }, + Check { id: "C23 receipt binds its payload + jcp/1 still verifies", run: c23_payload_binding }, Check { id: "C-neg expired / forged / wrong-caller", run: cneg_denials }, Check { id: "C-neg canonical rejects floats", run: cneg_canonical }, ] @@ -255,6 +256,41 @@ fn c22_dual_ceilings() -> Result<(), String> { Ok(()) } +/// C23 — the receipt binds what was decided over (SPEC §8), and the `jcp/2` +/// bump did not invalidate receipts sealed under `jcp/1`. +fn c23_payload_binding() -> Result<(), String> { + let r = ContentId::of_bytes(b"db"); + let holder = did(&sk(2)); + let sg = grant(&sk(1), holder, r, &["read"], 1_000, 0); + let f = frame(&sg, r, "read", Trust::Control); + let mut rt = Runtime::new(); + let d = rt.decide(&f, &sg, holder, Energy::measured(10), 1); + let key = sk(7); + let env = jcp_receipt::seal(&f, &sg.grant, &d, &key, "did:example:steward") + .map_err(|e| e.to_string())?; + let pk = key.verifying_key().to_bytes(); + + // 1. A freshly minted receipt commits the payload. + let core = jcp_receipt::extract(&env, "did:example:steward", &pk).map_err(|e| e.to_string())?; + if core.payload_hash.as_deref() != Some(f.payload_hash.to_hex().as_str()) { + return Err("a jcp/2 receipt did not commit its payload_hash".into()); + } + + // 2. Substituting ONLY the payload must break verification — the forgery a + // payload-blind receipt could not detect. + let swapped = jcp_schema::Frame { payload_hash: ContentId::of_bytes(b"other"), ..f.clone() }; + if jcp_receipt::verify(&swapped, &sg.grant, &d, &env, "did:example:steward", &pk).is_ok() { + return Err("a payload substitution verified against the receipt".into()); + } + + // 3. Version dispatch: the receipt names its profile, and an unknown profile + // is refused rather than assumed. + if jcp_receipt::PROFILE != "jcp/2" || jcp_receipt::PROFILE_V1 != "jcp/1" { + return Err("profile constants are not the published ones".into()); + } + Ok(()) +} + fn c3_budget() -> Result<(), String> { let r = ContentId::of_bytes(b"db"); let holder = did(&sk(2)); diff --git a/jcp/jcp-rs/crates/jcp-receipt/src/lib.rs b/jcp/jcp-rs/crates/jcp-receipt/src/lib.rs index 9f6128543..0b4d63d7c 100644 --- a/jcp/jcp-rs/crates/jcp-receipt/src/lib.rs +++ b/jcp/jcp-rs/crates/jcp-receipt/src/lib.rs @@ -20,21 +20,48 @@ pub mod jcr1; pub enum ReceiptError { #[error("envelope payload does not match the decision")] PayloadMismatch, + #[error("unknown receipt profile: {0}")] + UnknownProfile(String), #[error(transparent)] Jcr1(#[from] jcr1::Jcr1Error), } -/// Project a decision to JCR-1 wire JSON — the `jcp/1` profile (SPEC §8). The -/// signed core is a flat uint/text map; fields that don't apply (a deny reason -/// for an allow) are omitted. +/// The receipt profile this library **mints**. `jcp/2` adds `payload_hash` to +/// the signed core (SPEC §8), so a receipt binds *what was acted upon*, not only +/// that a decision happened. +pub const PROFILE: &str = "jcp/2"; + +/// The predecessor profile. Receipts sealed under it are still **verifiable** +/// (see [`verify`]), but are no longer minted. +pub const PROFILE_V1: &str = "jcp/1"; + +/// Project a decision to the **legacy `jcp/1`** wire — byte-for-byte what this +/// library sealed before `payload_hash` existed. Retained solely so previously +/// sealed receipts keep verifying; never used to mint. +fn to_jcr1_wire_v1(frame: &Frame, grant: &Grant, decision: &Decision, steward: &str) -> Value { + let mut wire = to_jcr1_wire(frame, grant, decision, steward); + if let Value::Object(ref mut o) = wire { + o.insert("jcp_receipt".into(), json!(PROFILE_V1)); + o.remove("payload_hash"); + } + wire +} + +/// Project a decision to JCR-1 wire JSON — the [`PROFILE`] (`jcp/2`) core +/// (SPEC §8). A flat uint/text map; fields that don't apply (a deny reason on an +/// allow) are omitted. pub fn to_jcr1_wire(frame: &Frame, grant: &Grant, decision: &Decision, steward: &str) -> Value { let mut obj = serde_json::Map::new(); - obj.insert("jcp_receipt".into(), json!("jcp/1")); + obj.insert("jcp_receipt".into(), json!(PROFILE)); obj.insert("grant_id".into(), json!(grant.id().to_hex())); obj.insert("issuer".into(), json!(grant.issuer.to_did_key())); obj.insert("subject".into(), json!(grant.subject.to_did_key())); obj.insert("resource".into(), json!(frame.resource.to_hex())); obj.insert("action".into(), json!(frame.action)); + // What the decision was ACTUALLY over. Without this a receipt proves a + // decision happened for an action but not on what payload — a tool-call + // receipt would not bind its arguments. + obj.insert("payload_hash".into(), json!(frame.payload_hash.to_hex())); obj.insert("frame_kind".into(), json!(frame_kind_tag(frame))); obj.insert("trust".into(), json!(trust_tag(frame))); obj.insert("verdict".into(), json!(decision.verdict.wire_tag())); @@ -135,7 +162,25 @@ pub fn verify_chained( pubkey: &[u8; 32], ) -> Result<(), ReceiptError> { let payload = jcr1::verify_cose_sign1(envelope, steward, pubkey)?; - if payload != signed_core_chained(frame, chain_ids, leaf, decision, steward)? { + let expected = match profile_of(&payload)?.as_str() { + PROFILE => signed_core_chained(frame, chain_ids, leaf, decision, steward)?, + PROFILE_V1 => { + // The v1 chained projection: the v1 core plus `chain_ids`, exactly + // as `to_jcr1_wire_chained` layered it before `payload_hash`. + let mut wire = to_jcr1_wire_v1(frame, leaf, decision, steward); + if chain_ids.len() > 1 { + if let Value::Object(ref mut obj) = wire { + obj.insert( + "chain_ids".into(), + Value::Array(chain_ids.iter().map(|c| json!(c.to_hex())).collect()), + ); + } + } + jcr1::signed_core_bytes(&wire)? + } + other => return Err(ReceiptError::UnknownProfile(other.to_string())), + }; + if payload != expected { return Err(ReceiptError::PayloadMismatch); } Ok(()) @@ -165,12 +210,31 @@ pub fn verify( pubkey: &[u8; 32], ) -> Result<(), ReceiptError> { let payload = jcr1::verify_cose_sign1(envelope, steward, pubkey)?; - if payload != signed_core(frame, grant, decision, steward)? { + // Version dispatch (SPEC §8): recompute the preimage under the profile the + // envelope itself claims, so a receipt sealed as `jcp/1` — before + // `payload_hash` existed — still verifies against the same decision. + let expected = match profile_of(&payload)?.as_str() { + PROFILE => signed_core(frame, grant, decision, steward)?, + PROFILE_V1 => jcr1::signed_core_bytes(&to_jcr1_wire_v1(frame, grant, decision, steward))?, + other => return Err(ReceiptError::UnknownProfile(other.to_string())), + }; + if payload != expected { return Err(ReceiptError::PayloadMismatch); } Ok(()) } +/// Read the `jcp_receipt` profile tag out of a verified payload. Until now the +/// tag was written but never read; version dispatch is what makes it load-bearing. +fn profile_of(payload: &[u8]) -> Result { + let v: Value = jcp_schema::from_canonical_cbor(payload) + .map_err(|_| ReceiptError::UnknownProfile("".into()))?; + v.get("jcp_receipt") + .and_then(Value::as_str) + .map(str::to_string) + .ok_or_else(|| ReceiptError::UnknownProfile("".into())) +} + /// The fields of one verified receipt, extracted from its signed core — what /// group accounting and state replay consume. #[derive(Clone, Debug, PartialEq, Eq)] @@ -183,6 +247,9 @@ pub struct ReceiptCore { pub action: String, pub verdict: String, pub deny_reason: Option, + /// The content id of the payload the decision was over. `Some` on `jcp/2`; + /// `None` on a legacy `jcp/1` receipt, which did not bind its payload. + pub payload_hash: Option, pub cost_uj: u64, pub budget_remaining_uj: u64, pub steward: String, @@ -221,6 +288,7 @@ pub fn extract(envelope: &[u8], steward: &str, pubkey: &[u8; 32]) -> Result (Frame, Grant, Decision, Decision) { + let issuer = sk(1); + let holder = sk(2); + let resource = ContentId::of_bytes(b"db"); + let grant = Grant { + issuer: Did::from_verifying_key(&issuer.verifying_key()), + subject: Did::from_verifying_key(&holder.verifying_key()), + capability: Capability::new(resource, ["read"]), + budget_uj: 100, + budget_funds: None, + expiry: 0, + parent: None, + delegate_depth: None, + nonce: [9; 16], + }; + let sg = issue(&issuer, grant.clone()).unwrap(); + let mk = |trust: Trust| Frame { + kind: FrameKind::ToolCall, + trust, + grant_id: grant.id(), + action: "read".into(), + resource, + payload_hash: ContentId::of_bytes(b"q"), + }; + let mut rt = Runtime::new(); + let allow_f = mk(Trust::Control); + let allow_d = rt.decide(&allow_f, &sg, grant.subject, jcp_schema::Energy::measured(30), 10); + let deny_f = mk(Trust::Data); + let deny_d = rt.decide(&deny_f, &sg, grant.subject, jcp_schema::Energy::measured(1), 10); + (allow_f, grant, allow_d, deny_d) + } + + /// **The migration guarantee.** These are the REAL bytes published in + /// `conformance/v1/vectors.json` before `payload_hash` existed, sealed under + /// `jcp/1`. They MUST still verify — version dispatch recomputes the legacy + /// preimage — or the bump silently invalidated every receipt in the wild. + #[test] + fn published_v1_receipts_still_verify() { + const V1_ALLOW: &str = "d284581fa201270458196469643a6578616d706c653a6a63702d7265666572656e6365a05901d2ae65747275737467636f6e74726f6c66616374696f6e64726561646669737375657278386469643a6b65793a7a364d6b6f6e334e656364364e6b6b79666f476f48786964327a6e476335394c55334b376d756261526346624c664c5867636f73745f756a181e677374657761726478196469643a6578616d706c653a6a63702d7265666572656e6365677375626a65637478386469643a6b65793a7a364d6b6f39685467674d776a535445614a615055664536747163793278765536426e4e713365336f38715642697948677665726469637465616c6c6f77686772616e745f6964784033623161356139636262373831353134636637353162613030376530373765383038333937376234663037623130346433366565393034643932383939386334687265736f75726365784030663766623535636565393361623937373738323936303865383531666133316536386531326462396466633061383032366164323938333134636633663932696275646765745f756a18646a6672616d655f6b696e6469746f6f6c2d63616c6c6b6a63705f72656365697074656a63702f3171656e657267795f70726f76656e616e63656748775368756e74736275646765745f72656d61696e696e675f756a184658405a7be56b33956c069d4e23127baa7b090e5028b9704dff6ca5ac46da546534e667151f80d7d7a605bcc31ea2bb33ec971e3e8b8794f7248a18b34c33bc438509"; + let (allow_f, grant, allow_d, _) = published_v1_scenario(); + let pubkey = sk(7).verifying_key().to_bytes(); + let env = hex::decode(V1_ALLOW).expect("legacy hex"); + verify(&allow_f, &grant, &allow_d, &env, "did:example:jcp-reference", &pubkey) + .expect("a published jcp/1 receipt MUST still verify after the jcp/2 bump"); + + // And it is recognisably legacy: no payload binding on a v1 receipt. + let core = extract(&env, "did:example:jcp-reference", &pubkey).unwrap(); + assert_eq!(core.verdict, "allow"); + assert_eq!(core.payload_hash, None, "v1 receipts did not bind their payload"); + } + + /// A freshly minted receipt is `jcp/2` and DOES bind its payload. + #[test] + fn new_receipts_bind_the_payload() { + let (f, g, d) = scenario(); + let key = sk(7); + let env = seal(&f, &g, &d, &key, "did:example:steward").unwrap(); + let core = extract(&env, "did:example:steward", &key.verifying_key().to_bytes()).unwrap(); + assert_eq!( + core.payload_hash.as_deref(), + Some(f.payload_hash.to_hex().as_str()), + "a jcp/2 receipt must commit the payload it decided over" + ); + } + + /// Changing only the PAYLOAD must break verification — the property the + /// whole bump exists to provide. Under `jcp/1` this forgery went undetected. + #[test] + fn a_swapped_payload_no_longer_verifies() { + let (f, g, d) = scenario(); + let key = sk(7); + let env = seal(&f, &g, &d, &key, "did:example:steward").unwrap(); + // Same grant, same action, same cost — different arguments. + let tampered = Frame { payload_hash: ContentId::of_bytes(b"different-args"), ..f.clone() }; + let r = verify(&tampered, &g, &d, &env, "did:example:steward", &key.verifying_key().to_bytes()); + assert!(matches!(r, Err(ReceiptError::PayloadMismatch)), "payload swap must be caught"); + } + #[test] fn allow_receipt_seals_and_verifies() { let (f, g, d) = scenario();