diff --git a/CHANGELOG.md b/CHANGELOG.md index 325170d..56b3838 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,42 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html) --- +## [3.1.0] — 2026-09-13 + +### Added +- **`parties.agent_identity`, `parties.agent_identity_verified`, + `parties.did_resolution_status` and `identity_consistent` are now committed chain + fields**, unconditionally: an + absent identity is committed as `null`. Up to 3.0 the three were served in public proof + responses (§9) but committed nowhere, so they sat outside the Merkle root, outside + `hashes.chain`, outside the Ed25519 signature and therefore outside the RFC 3161 token + and the Rekor entry. An attestor could restate an agent's identity after anchoring and + every external witness still verified. Any party reading `agent_identity_verified` as + evidence was reading the attestor's unbacked word. +- **`disclosed`**: the identity block's `(nonce, value)` pairs, published in the proof + itself. These four nonces are public so that anyone can open the block and check the + served values against the anchored commitments. Hiding is given up on these four + fields and on no other; every remaining nonce stays secret. +- `identity_consistent` is a judgment ON the identity: committing its three neighbours + and leaving it out would rebuild the same hole one field to the left. +- Test vectors 13 and 14 (`identity_verified_did`, + `identity_absent_is_committed_as_null`), with their `published_nonces`. + +### Changed +- `agent_identity_verified` is `true` or `null`, never `false`. One normalisation at the + source, so the committed value and the served value cannot disagree. +- §9: for `spec_version` `"3.1"`, `disclosed` MUST be included in public responses. + +### Notes +- `agent_version` is deliberately NOT committed: it carries no verifiable claim. +- Anchoring the block makes the identity claim **non-repudiable**; it does not make the + binding itself third-party verifiable. No public artefact proves the Ed25519 + challenge-response happened. A verifier needing more MUST resolve the DID itself. +- Proofs at `spec_version` `"3.0"` and below keep their algorithm and stay verifiable. + Their identity fields carry no anchor and MUST NOT be treated as evidence. + +--- + ## [3.0.0] — 2026-09-13 ### Changed diff --git a/README.md b/README.md index 2a779ee..e58a34d 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Want to add yours? Open a PR. ## Test vectors -[`test-vectors.json`](test-vectors.json) contains 12 test cases (7 legacy string-concatenation vectors, 2 canonical-JSON vectors for spec_version 1.2/2.1, 2 per-field commitment vectors for spec_version 3.0, and 1 batch-anchor Merkle vector). Any conformant implementation MUST pass all vectors. +[`test-vectors.json`](test-vectors.json) contains 14 test cases (7 legacy string-concatenation vectors, 2 canonical-JSON vectors for spec_version 1.2/2.1, 2 per-field commitment vectors for spec_version 3.0, 1 batch-anchor Merkle vector, and 2 spec_version 3.1 vectors covering the committed and publicly opened identity triple). Any conformant implementation MUST pass all vectors. ## Composability diff --git a/SPEC.md b/SPEC.md index 08ef95c..243d249 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,4 +1,4 @@ -# ArkForge Proof Specification v3.0.0 +# ArkForge Proof Specification v3.1.0 An open standard for verifiable agent-to-agent execution proofs. @@ -86,7 +86,7 @@ A conformant proof is a JSON object. The following fields are **required**: ```json { "proof_id": "prf_20260225_170950_fdec72", - "spec_version": "3.0", + "spec_version": "3.1", "timestamp": "2026-02-25T17:09:47Z", "hashes": { "request": "sha256:", @@ -171,7 +171,7 @@ All variants produce a valid chain hash. The `payment.transaction_id` value is u The chain hash binds every element of a transaction into a single verifiable seal. -### Algorithm (spec_version "3.0" — current) +### Algorithm (spec_version "3.1" — current) Each chain field is committed to separately, and the chain hash is the RFC 6962 Merkle root of those commitments: @@ -205,14 +205,32 @@ hash is recomputable from public data, and nothing that was private becomes publ ### Fields committed -The committed set is unchanged from spec 2.1: - ``` request_hash, response_hash, transaction_id, timestamp, buyer_fingerprint, seller +agent_identity, agent_identity_verified, did_resolution_status, +identity_consistent (spec 3.1, ALWAYS) [+ upstream_timestamp] when present and non-null [+ receipt_content_hash] when present, stripped of its "sha256:" prefix ``` +**Spec 3.1 added the identity block.** `identity_consistent` belongs to it: it is a +judgment ON the identity, so committing its three neighbours and leaving it out would +rebuild the same hole one field to the left. Up to 3.0 those four fields were served in +public proof responses (section 9) but committed nowhere: outside the Merkle root, +therefore outside `hashes.chain`, the Ed25519 signature, the RFC 3161 token and the +Rekor entry. An attestor could restate an agent's identity after anchoring and every +external witness still verified. Any ranking or audit that reads +`agent_identity_verified` was, up to 3.0, reading the attestor's unbacked word. + +The block is committed **unconditionally**, unlike `upstream_timestamp` and +`receipt_content_hash`. An absent identity is committed as `null`. Committing it only +when present would let an attestor omit the fields and leave a verifier with no +commitment to check against. + +`agent_identity_verified` is `true` or `null`, never `false`: a single normalisation +at the source, so the value committed and the value served cannot disagree. +`agent_version` is NOT committed — it carries no verifiable claim. + #### Reference implementation (Python) ```python @@ -244,6 +262,43 @@ chain_hash = merkle_root([leaf(bytes.fromhex(commitments[f])) for f in sorted(commitments)]).hex() ``` +### Public opening of the identity block (spec 3.1) + +A commitment hides its value: a third party recomputes the root from the published +digests and never learns a field. For the identity block that is not enough, because +the values must be **readable** by whoever reads the proof. So spec 3.1 publishes those +four nonces in the proof itself, under `disclosed`: + +```json +"disclosed": { + "agent_identity": {"nonce": "<64 hex>", "value": "did:web:agent.example"}, + "agent_identity_verified": {"nonce": "<64 hex>", "value": true}, + "did_resolution_status": {"nonce": "<64 hex>", "value": "bound"}, + "identity_consistent": {"nonce": "<64 hex>", "value": true} +} +``` + +Any party checks each triplet against the commitment the anchors cover: + +```python +assert commit(field, bytes.fromhex(item["nonce"]), item["value"]).hex() \ + == proof["commitments"][field] +``` + +Hiding is given up on these four fields and on **no other**: every remaining nonce +stays secret. A verifier MUST treat the flat `agent_identity*` fields of a public +response as informational and take the value from `disclosed`; an attestor MUST serve +the same value in both. + +**What this establishes, and what it does not.** It makes the identity claim +non-repudiable: the attestor committed to it before anchoring and cannot restate it. +It does **not** let a third party verify the binding itself — no public artefact proves +the Ed25519 challenge-response happened. A verifier that needs more MUST resolve the +DID itself. + +A `spec_version` below `"3.1"` carries no anchored identity. A verifier MUST NOT treat +its `agent_identity_verified` as evidence. + ### Selective disclosure The proof owner holds the nonces. To prove one field to a counterparty without revealing @@ -272,7 +327,12 @@ commitment is what makes the pair self-sufficient. | `seller` | `parties.seller` | Target domain (e.g. `arkforge.fr`) | | `upstream_timestamp` | `upstream_timestamp` | Upstream service's HTTP `Date` header. **Committed only when present and non-null** | | `receipt_content_hash` | `provider_payment.receipt_content_hash` | SHA-256 hex of raw receipt bytes. **Committed only when present**. Strip the `sha256:` prefix | +| `agent_identity` | `parties.agent_identity` | Declared or bound agent DID, or `null`. **Committed unconditionally (3.1)** | +| `agent_identity_verified` | `parties.agent_identity_verified` | `true` when the DID is bound via Ed25519 challenge-response, else `null` — never `false`. **Committed unconditionally (3.1)** | +| `did_resolution_status` | `parties.did_resolution_status` | `"bound"`, `"unverified"`, or `null`. **Committed unconditionally (3.1)** | +| `identity_consistent` | `identity_consistent` | `true`/`false`/`null` — whether the declared identity agrees with what the attestor already knows for this key. **Committed unconditionally (3.1)** | | `commitments` | `commitments` | One hex commitment per committed field, published in full | +| `disclosed` | `disclosed` | The identity block's `(nonce, value)` pairs, published for everyone (3.1) | ### Backward compatibility @@ -281,7 +341,8 @@ or re-anchored retroactively. | `spec_version` | Chain hash | |---|---| -| `"3.0"` | Merkle root of per-field commitments (current) | +| `"3.1"` | Merkle root of per-field commitments, identity block included and publicly opened (current) | +| `"3.0"` | Merkle root of per-field commitments; identity served but **not** committed | | `"1.2"`, `"2.1"` | `SHA256(canonical_json(chain_data))` over the **values** | | `"1.1"`, `"2.0"`, absent | `SHA256` of the values concatenated as raw UTF-8, no separator (legacy) | @@ -653,6 +714,7 @@ When a proof is returned via an **unauthenticated** endpoint: - `parties.buyer_fingerprint` SHOULD be omitted (privacy) - `parties.agent_identity`, `parties.agent_identity_verified`, `parties.did_resolution_status`, and `parties.seller` SHOULD be included (third-party auditability) +- For `spec_version` `"3.1"`, `disclosed` MUST be included: it carries the identity block's nonces, without which the anchored identity cannot be opened and the flat fields above are unbacked - `certification_fee` amounts and receipt URLs SHOULD be omitted - `buyer_reputation_score` and `buyer_profile_url` SHOULD be omitted - `provider_payment`: only `type`, `receipt_content_hash`, and `verification_status` SHOULD be retained; `receipt_url` and `parsed_fields` SHOULD be omitted diff --git a/check_consistency.py b/check_consistency.py index a7bc0d2..64479c4 100644 --- a/check_consistency.py +++ b/check_consistency.py @@ -71,6 +71,10 @@ def inclusion_root(leaf: bytes, index: int, size: int, path): return h, i +IDENTITY_FIELDS = ("agent_identity", "agent_identity_verified", "did_resolution_status", + "identity_consistent") + + def commitments_root(commitments: dict) -> str: """Spec 3.0 chain hash: Merkle root over the commitments, fields sorted.""" return merkle_root([leaf_hash(bytes.fromhex(commitments[f])) @@ -181,6 +185,10 @@ def check_test_vectors(): chain_data["upstream_timestamp"] = inp["upstream_timestamp"] if inp.get("receipt_content_hash"): chain_data["receipt_content_hash"] = inp["receipt_content_hash"] + if v.get("spec_version") == "3.1": + # Committed unconditionally: an absent identity is a committed null. + for field in IDENTITY_FIELDS: + chain_data[field] = inp[field] if chain_data != exp["chain_data"]: print(f"FAIL [{name}]: chain_data mismatch") ok = False @@ -195,6 +203,20 @@ def check_test_vectors(): ok = False continue chain_hash = commitments_root(commitments) + # Spec 3.1 publishes the identity nonces. If they stopped opening their + # commitments, an unbacked identity would read as an anchored one. + if v.get("spec_version") == "3.1": + published = exp.get("published_nonces") or {} + if set(published) != set(IDENTITY_FIELDS): + print(f"FAIL [{name}]: published_nonces must cover exactly the identity triple") + ok = False + continue + bad = [f for f in IDENTITY_FIELDS + if commit(f, published[f], chain_data[f]) != exp["commitments"][f]] + if bad: + print(f"FAIL [{name}]: published nonce does not open {', '.join(bad)}") + ok = False + continue elif algo == "canonical_json": chain_data = { "buyer_fingerprint": buyer_fp, diff --git a/test-vectors.json b/test-vectors.json index ca85de6..5a4e6c9 100644 --- a/test-vectors.json +++ b/test-vectors.json @@ -1,9 +1,9 @@ { - "spec_version": "3.0.0", + "spec_version": "3.1.0", "algorithm": "SHA-256", "canonical_json_rule": "json.dumps(data, sort_keys=True, separators=(\",\", \":\"))", "chain_formula_current": "MerkleRoot_RFC6962({ SHA256(field || 0x00 || nonce || canonical_json(value)) for each chain field }, leaves ordered by field name)", - "note": "Vectors 1-7 use the LEGACY string-concatenation algorithm (spec_version 1.1/2.0). Vectors 8-9 hash the canonical JSON of the values themselves (spec_version 1.2/2.1). Vectors 10-11 are spec_version 3.0: the chain hash is the RFC 6962 Merkle root of one commitment per field, so it is recomputable from the published commitments alone, with no field value. Their nonces are fixed here to make the vector reproducible; a real proof draws 32 fresh random bytes per field and per proof. Vector 12 covers batch anchoring: the Merkle root over several chain hashes, and one inclusion path per leaf. transaction_id = payment.transaction_id from the proof JSON. buyer_fingerprint = SHA256(api_key). In test vector inputs, the field is named payment_intent_id for historical reasons.", + "note": "Vectors 1-7 use the LEGACY string-concatenation algorithm (spec_version 1.1/2.0). Vectors 8-9 hash the canonical JSON of the values themselves (spec_version 1.2/2.1). Vectors 10-11 are spec_version 3.0: the chain hash is the RFC 6962 Merkle root of one commitment per field, so it is recomputable from the published commitments alone, with no field value. Their nonces are fixed here to make the vector reproducible; a real proof draws 32 fresh random bytes per field and per proof. Vector 12 covers batch anchoring: the Merkle root over several chain hashes, and one inclusion path per leaf. transaction_id = payment.transaction_id from the proof JSON. buyer_fingerprint = SHA256(api_key). In test vector inputs, the field is named payment_intent_id for historical reasons. Vectors 13-14 are spec_version 3.1: the identity block (agent_identity, agent_identity_verified, did_resolution_status, identity_consistent) joins the committed chain fields, always present, a missing identity committed as null. Their nonces are PUBLISHED in the proof (expected.published_nonces), so any third party opens the block and checks the served values against the anchored commitments. Hiding is given up on these four fields and on no other.", "vectors": [ { "name": "minimal_transaction", @@ -400,6 +400,149 @@ }, "spec_version": "3.0", "algorithm": "batch_merkle" + }, + { + "name": "identity_verified_did", + "spec_version": "3.1", + "algorithm": "commitments", + "input": { + "request": { + "entity_id": "ENT-4417" + }, + "response": { + "agrement": "valide", + "expire_le": "2027-03-31" + }, + "payment_intent_id": "pi_test_identity", + "timestamp": "2026-09-13T12:00:00Z", + "api_key": "mcp_test_identity_key", + "seller": "corpus.arkforge.tech", + "nonces": { + "request_hash": "758c5cb2ecf9cdf65c0eed5ea1de25b660a2c437773bf996bcf0317197fd4fa3", + "response_hash": "a8acdf7386836cf3036832ffeae87986a32cd865d61094f519a6bdfd9210d89d", + "transaction_id": "99406926d17089505690a97d34e6a3ea60b3b7ea2c51791a980754336d9e3e5f", + "timestamp": "ab1ad88a5e60b59f965088710184a8ff785bdd84cd2fd16a75399f871adc1595", + "buyer_fingerprint": "99dffe375991133d011667c99e716daf27654dfef0ed2dc83fe00bbc253b8988", + "seller": "1643109668481c4e6b12bde2df3ae3107d2d93a87885a6d61683cb076fa77eff", + "agent_identity": "79a3fae23bf4238a6dc32ae16519d116b9907180e3cb4be4e8fe58a8f60ad9ce", + "agent_identity_verified": "b81232e2417ccd9cf0f83a6280e217a7349f48ca27bd40b7501eb4ff5d37edeb", + "did_resolution_status": "60603c8278c4e9cbd8ee572cc63b06f2a4bc05ad3d499bba8f3e50336006f679", + "identity_consistent": "f9f0fb076da4747ea93468238a198c7852b77c28fd9c3b52ff995d2e6f0012e0" + }, + "agent_identity": "did:web:agent.example", + "agent_identity_verified": true, + "did_resolution_status": "bound", + "identity_consistent": true + }, + "expected": { + "canonical_request": "{\"entity_id\":\"ENT-4417\"}", + "canonical_response": "{\"agrement\":\"valide\",\"expire_le\":\"2027-03-31\"}", + "request_hash": "6c63eb77ad577dc5288e8c110d4bee7c87b3f6857ac8fe653a1ad42d3198e2ed", + "response_hash": "9d9950bd02d24d12731b58452867e9dabc980c709f97ae64bf43cc1f58ee8d0a", + "buyer_fingerprint": "509a170b6b51db72a66eb0e157cb75c9b8ab2b384b07025bc137ce3e00909558", + "chain_data": { + "request_hash": "6c63eb77ad577dc5288e8c110d4bee7c87b3f6857ac8fe653a1ad42d3198e2ed", + "response_hash": "9d9950bd02d24d12731b58452867e9dabc980c709f97ae64bf43cc1f58ee8d0a", + "transaction_id": "pi_test_identity", + "timestamp": "2026-09-13T12:00:00Z", + "buyer_fingerprint": "509a170b6b51db72a66eb0e157cb75c9b8ab2b384b07025bc137ce3e00909558", + "seller": "corpus.arkforge.tech", + "agent_identity": "did:web:agent.example", + "agent_identity_verified": true, + "did_resolution_status": "bound", + "identity_consistent": true + }, + "commitments": { + "request_hash": "17a415c6a9d6bdf43de2fce2104f3842cfc7e62b8c6c6805f06802c98ba8b53d", + "response_hash": "a016abb615b8cbe0333140a83201af3fcf5836f2f99dcc32e3664db3b069dbc9", + "transaction_id": "7a3bdfe611f3e2dc7e94b9c7c4b8fcd4386b2d6aa3936796caf2d49500025be9", + "timestamp": "f118d63b09d1be16f290450c9043b2df0170f49437721bd25f54fba76ea86a06", + "buyer_fingerprint": "cdc0c387a8592352d85e6395f40d6f7c49a9d9efdc5cca2183fdc69625a0afe7", + "seller": "fcc01302923ae4e817cac3ef737341ac38b409a0c0acc06a42bd3f762db113ec", + "agent_identity": "fabd3b1cc57da13c360493ff2f44ddea591a932f7c9cffcdbd97dd3d870b1156", + "agent_identity_verified": "a0d6e97dc76db97330e5b24390503a046c2e0e44604bde8f5ffb4507c622ae68", + "did_resolution_status": "ee9efb684069ad0172ea95a52b65df6c7cecd9ad9246a3d779ba2122f8c432d5", + "identity_consistent": "e46d6d3bbce8a3e6c556ec05f834ebe6321a407dae1ab8826f5fce21aee6f543" + }, + "published_nonces": { + "agent_identity": "79a3fae23bf4238a6dc32ae16519d116b9907180e3cb4be4e8fe58a8f60ad9ce", + "agent_identity_verified": "b81232e2417ccd9cf0f83a6280e217a7349f48ca27bd40b7501eb4ff5d37edeb", + "did_resolution_status": "60603c8278c4e9cbd8ee572cc63b06f2a4bc05ad3d499bba8f3e50336006f679", + "identity_consistent": "f9f0fb076da4747ea93468238a198c7852b77c28fd9c3b52ff995d2e6f0012e0" + }, + "chain_hash": "8719752410e476e2b90d2ddc0de9acb169737487478b1f1728c9d1efbf20ea89" + } + }, + { + "name": "identity_absent_is_committed_as_null", + "spec_version": "3.1", + "algorithm": "commitments", + "input": { + "request": { + "entity_id": "ENT-0001" + }, + "response": { + "agrement": "absent" + }, + "payment_intent_id": "pi_test_no_identity", + "timestamp": "2026-09-13T12:00:00Z", + "api_key": "mcp_test_no_identity_key", + "seller": "corpus.arkforge.tech", + "nonces": { + "request_hash": "10af35b33ea9747a7671a9b2449b9dfbec7babec11b72f49b2f4ac919c20ca09", + "response_hash": "2524d68c965d7c630e7d9349c75bd178aa73932bce4a600d1d3124349a18907f", + "transaction_id": "543f54024cffb5fa1489bdbd12ab76d22033a5701f7604802387541ce3d7f6e0", + "timestamp": "92755b16aea345b1e8951d79c1395fb5e682ad57d819a1a1d47b08a7c2f5d72e", + "buyer_fingerprint": "1b65680b12aeeaa544517ef61951bcc6e3f1ee6aa4039d86faa958f32414323d", + "seller": "c3954fab2b99aa04f8b6e1fa1ca5ab3e6760dd5eacb0443394e312885a1cf740", + "agent_identity": "9dfb59b283434e3a865efbeeb976a367ef4d3f91677d29d701f9118db2abc4a7", + "agent_identity_verified": "8900d3193539b78eaea5c9f53d0a99c8a968a5bba20c217a0fb64bdddde8921a", + "did_resolution_status": "a7a6b9dbe52c9f8aabfbba1ce952245580095bc0507b0d8ee1c12f8a83b52c86", + "identity_consistent": "e37ee1c1528e87d4f2c8f51cfaf143f464704de42a9e0d1055830e67b8001c5a" + }, + "agent_identity": null, + "agent_identity_verified": null, + "did_resolution_status": null, + "identity_consistent": null + }, + "expected": { + "canonical_request": "{\"entity_id\":\"ENT-0001\"}", + "canonical_response": "{\"agrement\":\"absent\"}", + "request_hash": "08d9e6ab62cdacde40ee55db26aa331ca945bb2eba0225bcd47d88669bc63375", + "response_hash": "8cbf093e28493f052e6823e91709f271cb7f909c5f2345d955b661749e8fe047", + "buyer_fingerprint": "b7c469c00b41e3a7c9db2d7b7ce7ef273993ad73edd69a9243b95c7b43dea727", + "chain_data": { + "request_hash": "08d9e6ab62cdacde40ee55db26aa331ca945bb2eba0225bcd47d88669bc63375", + "response_hash": "8cbf093e28493f052e6823e91709f271cb7f909c5f2345d955b661749e8fe047", + "transaction_id": "pi_test_no_identity", + "timestamp": "2026-09-13T12:00:00Z", + "buyer_fingerprint": "b7c469c00b41e3a7c9db2d7b7ce7ef273993ad73edd69a9243b95c7b43dea727", + "seller": "corpus.arkforge.tech", + "agent_identity": null, + "agent_identity_verified": null, + "did_resolution_status": null, + "identity_consistent": null + }, + "commitments": { + "request_hash": "6cd495cec9f2a57cffd5bf78983acae82c49f568419baa67944083acabfe9705", + "response_hash": "da5dbe1d94161fcf9ff18051f51cd26991324b3197af3435003eb15efe807002", + "transaction_id": "7c929738ac374be0c53f1d639684161c8a2f513d35c455bd0e4573c20a476615", + "timestamp": "ca67037675644082ce3f90fa3bff816ee4b92b65c5e5fc22808d24dbcbade4fa", + "buyer_fingerprint": "49d5ead9a84ef629d9ceb369049272e34a4456e47adc26a38150486809e64a25", + "seller": "7b88ea1af5463ecd3b3344ee6839ab8f881558c7e127794877c722301f39c925", + "agent_identity": "169be79df1432c6fdefc343c91a718ab0ec4088807ad3982e5cf6d6ade2e7f97", + "agent_identity_verified": "98348aaa6790cd123fbbd0b31701f255746ac5d86f7b8392d76a8be8632a128e", + "did_resolution_status": "d0c4647f3108bd52d7ca369963834f79a7277c7ecde956297203df7432fd1d38", + "identity_consistent": "6b7f4afb645968d68e0662f1a112b5fa3d30708ff60e4f1c5f905512ed99497e" + }, + "published_nonces": { + "agent_identity": "9dfb59b283434e3a865efbeeb976a367ef4d3f91677d29d701f9118db2abc4a7", + "agent_identity_verified": "8900d3193539b78eaea5c9f53d0a99c8a968a5bba20c217a0fb64bdddde8921a", + "did_resolution_status": "a7a6b9dbe52c9f8aabfbba1ce952245580095bc0507b0d8ee1c12f8a83b52c86", + "identity_consistent": "e37ee1c1528e87d4f2c8f51cfaf143f464704de42a9e0d1055830e67b8001c5a" + }, + "chain_hash": "a3c27f3e10af92d191fdf0da65efdf297e6d3a94efeffa5b949a2e3d4e99a73d" + } } ], "chain_formula_values": "SHA256(canonical_json({buyer_fingerprint, request_hash, response_hash, seller, timestamp, transaction_id [, upstream_timestamp] [, receipt_content_hash]})) — spec_version 1.2 / 2.1",