jcp: JCR-1 receipts bind their payload — jcp/2 + version dispatch (SPEC §8, C23) #23

Merged
dcharlot merged 1 commit from jcp/receipt-payload-hash into main 2026-07-18 03:16:43 -04:00
7 changed files with 295 additions and 28 deletions

View file

@ -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.

View file

@ -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",

View file

@ -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"
}
}
]
}

View file

@ -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",

View file

@ -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",

View file

@ -64,6 +64,7 @@ pub fn checks() -> Vec<Check> {
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));

View file

@ -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<String, ReceiptError> {
let v: Value = jcp_schema::from_canonical_cbor(payload)
.map_err(|_| ReceiptError::UnknownProfile("<undecodable>".into()))?;
v.get("jcp_receipt")
.and_then(Value::as_str)
.map(str::to_string)
.ok_or_else(|| ReceiptError::UnknownProfile("<absent>".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<String>,
/// 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<String>,
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<Rece
action: field("action").ok_or_else(|| bad("action"))?,
verdict: field("verdict").ok_or_else(|| bad("verdict"))?,
deny_reason: field("deny_reason"),
payload_hash: field("payload_hash"),
cost_uj: uint("cost_uj").ok_or_else(|| bad("cost_uj"))?,
budget_remaining_uj: uint("budget_remaining_uj").ok_or_else(|| bad("budget_remaining_uj"))?,
steward: field("steward").ok_or_else(|| bad("steward"))?,
@ -430,6 +498,87 @@ mod tests {
(frame, grant, d)
}
/// The exact scenario `gen-vectors` sealed for the PUBLISHED v1 vectors
/// (nonce `[9;16]`, cost 30 µJ, `now = 10`) — needed to re-verify the real
/// legacy bytes below.
fn published_v1_scenario() -> (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();