From ce8b0cf0182762836bbf5e17326c2fce165a2d20 Mon Sep 17 00:00:00 2001 From: desiorac Date: Sun, 13 Sep 2026 19:30:45 +0200 Subject: [PATCH 1/2] spec 3.1 : le triplet d identite entre dans les champs engages Jusqu a 3.0 agent_identity, agent_identity_verified et did_resolution_status etaient servis dans les reponses publiques et engages nulle part : hors racine Merkle, donc hors hashes.chain, hors signature Ed25519, donc hors jeton RFC 3161 et hors Rekor. L emetteur pouvait les reecrire apres ancrage sans qu aucun temoin externe ne bouge, et tout classement lisant agent_identity_verified lisait sa parole. 3.1 les engage inconditionnellement, une identite absente etant engagee a null, et publie leurs nonces dans la preuve (disclosed) pour que la valeur servie soit ouvrable par n importe qui. Le masquage est abandonne sur ces trois champs et sur aucun autre. Ce que ca etablit : la non-repudiation. Ce que ca n etablit pas : la verification du binding lui-meme par un tiers, aucun artefact public ne prouve le challenge-response. Vecteurs 13-14 generes par une reimplementation independante et recoupes avec trust_layer ; check_consistency.py verifie que les nonces publies ouvrent bien leurs engagements. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CzXFq94XGK7vcDgoSRsPFp --- CHANGELOG.md | 33 +++++++++++ README.md | 2 +- SPEC.md | 69 ++++++++++++++++++++-- check_consistency.py | 21 +++++++ test-vectors.json | 137 ++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 253 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 325170d..86f142a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,39 @@ 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` and + `parties.did_resolution_status` 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 triple's `(nonce, value)` pairs, published in the proof + itself. These three nonces are public so that anyone can open the triple and check the + served values against the anchored commitments. Hiding is given up on these three + fields and on no other; every remaining nonce stays secret. +- 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 triple 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..b63e961 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,29 @@ 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 (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 triple.** Up to 3.0 those three 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 triple 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 +259,42 @@ chain_hash = merkle_root([leaf(bytes.fromhex(commitments[f])) for f in sorted(commitments)]).hex() ``` +### Public opening of the identity triple (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 triple that is not enough, because +the values must be **readable** by whoever reads the proof. So spec 3.1 publishes those +three 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"} +} +``` + +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 three 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 +323,11 @@ 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)** | | `commitments` | `commitments` | One hex commitment per committed field, published in full | +| `disclosed` | `disclosed` | The identity triple's `(nonce, value)` pairs, published for everyone (3.1) | ### Backward compatibility @@ -281,7 +336,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 triple 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 +709,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 triple'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..6452d7c 100644 --- a/check_consistency.py +++ b/check_consistency.py @@ -71,6 +71,9 @@ def inclusion_root(leaf: bytes, index: int, size: int, path): return h, i +IDENTITY_FIELDS = ("agent_identity", "agent_identity_verified", "did_resolution_status") + + 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 +184,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 +202,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..77277b3 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 triple (agent_identity, agent_identity_verified, did_resolution_status) 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 triple and checks the served values against the anchored commitments. Hiding is given up on these three fields and on no other.", "vectors": [ { "name": "minimal_transaction", @@ -400,6 +400,139 @@ }, "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" + }, + "agent_identity": "did:web:agent.example", + "agent_identity_verified": true, + "did_resolution_status": "bound" + }, + "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" + }, + "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" + }, + "published_nonces": { + "agent_identity": "79a3fae23bf4238a6dc32ae16519d116b9907180e3cb4be4e8fe58a8f60ad9ce", + "agent_identity_verified": "b81232e2417ccd9cf0f83a6280e217a7349f48ca27bd40b7501eb4ff5d37edeb", + "did_resolution_status": "60603c8278c4e9cbd8ee572cc63b06f2a4bc05ad3d499bba8f3e50336006f679" + }, + "chain_hash": "7087e7b61c27a3fe9e7b5d433d738e3e46c9c50f63e073602e01d81531e454a2" + } + }, + { + "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" + }, + "agent_identity": null, + "agent_identity_verified": null, + "did_resolution_status": 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 + }, + "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" + }, + "published_nonces": { + "agent_identity": "9dfb59b283434e3a865efbeeb976a367ef4d3f91677d29d701f9118db2abc4a7", + "agent_identity_verified": "8900d3193539b78eaea5c9f53d0a99c8a968a5bba20c217a0fb64bdddde8921a", + "did_resolution_status": "a7a6b9dbe52c9f8aabfbba1ce952245580095bc0507b0d8ee1c12f8a83b52c86" + }, + "chain_hash": "4a4b11b5647ee01ef749fbecde897772d97d3bd8cd9adb900e20ad4367cfa8b8" + } } ], "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", From b1f256dcb971bd7bf2897bc085879fb7a72eb1f5 Mon Sep 17 00:00:00 2001 From: desiorac Date: Sun, 13 Sep 2026 19:36:00 +0200 Subject: [PATCH 2/2] spec 3.1 : identity_consistent rejoint le bloc d identite engage Un jugement SUR l identite, servi publiquement, engage nulle part. Ancrer ses trois voisins en le laissant dehors reconstruit le meme trou un champ plus a gauche. Vecteurs 13-14 regeneres et recoupes par reimplementation independante. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CzXFq94XGK7vcDgoSRsPFp --- CHANGELOG.md | 15 +++++++++------ SPEC.md | 27 ++++++++++++++++----------- check_consistency.py | 3 ++- test-vectors.json | 36 +++++++++++++++++++++++------------- 4 files changed, 50 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86f142a..56b3838 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,18 +14,21 @@ 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` and - `parties.did_resolution_status` are now committed chain fields**, unconditionally: an +- **`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 triple's `(nonce, value)` pairs, published in the proof - itself. These three nonces are public so that anyone can open the triple and check the - served values against the anchored commitments. Hiding is given up on these three +- **`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`. @@ -36,7 +39,7 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html) ### Notes - `agent_version` is deliberately NOT committed: it carries no verifiable claim. -- Anchoring the triple makes the identity claim **non-repudiable**; it does not make the +- 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. diff --git a/SPEC.md b/SPEC.md index b63e961..243d249 100644 --- a/SPEC.md +++ b/SPEC.md @@ -207,19 +207,22 @@ hash is recomputable from public data, and nothing that was private becomes publ ``` request_hash, response_hash, transaction_id, timestamp, buyer_fingerprint, seller -agent_identity, agent_identity_verified, did_resolution_status (spec 3.1, ALWAYS) +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 triple.** Up to 3.0 those three fields were served in +**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 triple is committed **unconditionally**, unlike `upstream_timestamp` and +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. @@ -259,18 +262,19 @@ chain_hash = merkle_root([leaf(bytes.fromhex(commitments[f])) for f in sorted(commitments)]).hex() ``` -### Public opening of the identity triple (spec 3.1) +### 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 triple that is not enough, because +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 -three nonces in the proof itself, under `disclosed`: +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"} + "did_resolution_status": {"nonce": "<64 hex>", "value": "bound"}, + "identity_consistent": {"nonce": "<64 hex>", "value": true} } ``` @@ -281,7 +285,7 @@ assert commit(field, bytes.fromhex(item["nonce"]), item["value"]).hex() \ == proof["commitments"][field] ``` -Hiding is given up on these three fields and on **no other**: every remaining nonce +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. @@ -326,8 +330,9 @@ commitment is what makes the pair self-sufficient. | `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 triple's `(nonce, value)` pairs, published for everyone (3.1) | +| `disclosed` | `disclosed` | The identity block's `(nonce, value)` pairs, published for everyone (3.1) | ### Backward compatibility @@ -336,7 +341,7 @@ or re-anchored retroactively. | `spec_version` | Chain hash | |---|---| -| `"3.1"` | Merkle root of per-field commitments, identity triple included and publicly opened (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) | @@ -709,7 +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 triple's nonces, without which the anchored identity cannot be opened and the flat fields above are unbacked +- 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 6452d7c..64479c4 100644 --- a/check_consistency.py +++ b/check_consistency.py @@ -71,7 +71,8 @@ def inclusion_root(leaf: bytes, index: int, size: int, path): return h, i -IDENTITY_FIELDS = ("agent_identity", "agent_identity_verified", "did_resolution_status") +IDENTITY_FIELDS = ("agent_identity", "agent_identity_verified", "did_resolution_status", + "identity_consistent") def commitments_root(commitments: dict) -> str: diff --git a/test-vectors.json b/test-vectors.json index 77277b3..5a4e6c9 100644 --- a/test-vectors.json +++ b/test-vectors.json @@ -3,7 +3,7 @@ "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. Vectors 13-14 are spec_version 3.1: the identity triple (agent_identity, agent_identity_verified, did_resolution_status) 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 triple and checks the served values against the anchored commitments. Hiding is given up on these three fields and on no other.", + "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", @@ -426,11 +426,13 @@ "seller": "1643109668481c4e6b12bde2df3ae3107d2d93a87885a6d61683cb076fa77eff", "agent_identity": "79a3fae23bf4238a6dc32ae16519d116b9907180e3cb4be4e8fe58a8f60ad9ce", "agent_identity_verified": "b81232e2417ccd9cf0f83a6280e217a7349f48ca27bd40b7501eb4ff5d37edeb", - "did_resolution_status": "60603c8278c4e9cbd8ee572cc63b06f2a4bc05ad3d499bba8f3e50336006f679" + "did_resolution_status": "60603c8278c4e9cbd8ee572cc63b06f2a4bc05ad3d499bba8f3e50336006f679", + "identity_consistent": "f9f0fb076da4747ea93468238a198c7852b77c28fd9c3b52ff995d2e6f0012e0" }, "agent_identity": "did:web:agent.example", "agent_identity_verified": true, - "did_resolution_status": "bound" + "did_resolution_status": "bound", + "identity_consistent": true }, "expected": { "canonical_request": "{\"entity_id\":\"ENT-4417\"}", @@ -447,7 +449,8 @@ "seller": "corpus.arkforge.tech", "agent_identity": "did:web:agent.example", "agent_identity_verified": true, - "did_resolution_status": "bound" + "did_resolution_status": "bound", + "identity_consistent": true }, "commitments": { "request_hash": "17a415c6a9d6bdf43de2fce2104f3842cfc7e62b8c6c6805f06802c98ba8b53d", @@ -458,14 +461,16 @@ "seller": "fcc01302923ae4e817cac3ef737341ac38b409a0c0acc06a42bd3f762db113ec", "agent_identity": "fabd3b1cc57da13c360493ff2f44ddea591a932f7c9cffcdbd97dd3d870b1156", "agent_identity_verified": "a0d6e97dc76db97330e5b24390503a046c2e0e44604bde8f5ffb4507c622ae68", - "did_resolution_status": "ee9efb684069ad0172ea95a52b65df6c7cecd9ad9246a3d779ba2122f8c432d5" + "did_resolution_status": "ee9efb684069ad0172ea95a52b65df6c7cecd9ad9246a3d779ba2122f8c432d5", + "identity_consistent": "e46d6d3bbce8a3e6c556ec05f834ebe6321a407dae1ab8826f5fce21aee6f543" }, "published_nonces": { "agent_identity": "79a3fae23bf4238a6dc32ae16519d116b9907180e3cb4be4e8fe58a8f60ad9ce", "agent_identity_verified": "b81232e2417ccd9cf0f83a6280e217a7349f48ca27bd40b7501eb4ff5d37edeb", - "did_resolution_status": "60603c8278c4e9cbd8ee572cc63b06f2a4bc05ad3d499bba8f3e50336006f679" + "did_resolution_status": "60603c8278c4e9cbd8ee572cc63b06f2a4bc05ad3d499bba8f3e50336006f679", + "identity_consistent": "f9f0fb076da4747ea93468238a198c7852b77c28fd9c3b52ff995d2e6f0012e0" }, - "chain_hash": "7087e7b61c27a3fe9e7b5d433d738e3e46c9c50f63e073602e01d81531e454a2" + "chain_hash": "8719752410e476e2b90d2ddc0de9acb169737487478b1f1728c9d1efbf20ea89" } }, { @@ -492,11 +497,13 @@ "seller": "c3954fab2b99aa04f8b6e1fa1ca5ab3e6760dd5eacb0443394e312885a1cf740", "agent_identity": "9dfb59b283434e3a865efbeeb976a367ef4d3f91677d29d701f9118db2abc4a7", "agent_identity_verified": "8900d3193539b78eaea5c9f53d0a99c8a968a5bba20c217a0fb64bdddde8921a", - "did_resolution_status": "a7a6b9dbe52c9f8aabfbba1ce952245580095bc0507b0d8ee1c12f8a83b52c86" + "did_resolution_status": "a7a6b9dbe52c9f8aabfbba1ce952245580095bc0507b0d8ee1c12f8a83b52c86", + "identity_consistent": "e37ee1c1528e87d4f2c8f51cfaf143f464704de42a9e0d1055830e67b8001c5a" }, "agent_identity": null, "agent_identity_verified": null, - "did_resolution_status": null + "did_resolution_status": null, + "identity_consistent": null }, "expected": { "canonical_request": "{\"entity_id\":\"ENT-0001\"}", @@ -513,7 +520,8 @@ "seller": "corpus.arkforge.tech", "agent_identity": null, "agent_identity_verified": null, - "did_resolution_status": null + "did_resolution_status": null, + "identity_consistent": null }, "commitments": { "request_hash": "6cd495cec9f2a57cffd5bf78983acae82c49f568419baa67944083acabfe9705", @@ -524,14 +532,16 @@ "seller": "7b88ea1af5463ecd3b3344ee6839ab8f881558c7e127794877c722301f39c925", "agent_identity": "169be79df1432c6fdefc343c91a718ab0ec4088807ad3982e5cf6d6ade2e7f97", "agent_identity_verified": "98348aaa6790cd123fbbd0b31701f255746ac5d86f7b8392d76a8be8632a128e", - "did_resolution_status": "d0c4647f3108bd52d7ca369963834f79a7277c7ecde956297203df7432fd1d38" + "did_resolution_status": "d0c4647f3108bd52d7ca369963834f79a7277c7ecde956297203df7432fd1d38", + "identity_consistent": "6b7f4afb645968d68e0662f1a112b5fa3d30708ff60e4f1c5f905512ed99497e" }, "published_nonces": { "agent_identity": "9dfb59b283434e3a865efbeeb976a367ef4d3f91677d29d701f9118db2abc4a7", "agent_identity_verified": "8900d3193539b78eaea5c9f53d0a99c8a968a5bba20c217a0fb64bdddde8921a", - "did_resolution_status": "a7a6b9dbe52c9f8aabfbba1ce952245580095bc0507b0d8ee1c12f8a83b52c86" + "did_resolution_status": "a7a6b9dbe52c9f8aabfbba1ce952245580095bc0507b0d8ee1c12f8a83b52c86", + "identity_consistent": "e37ee1c1528e87d4f2c8f51cfaf143f464704de42a9e0d1055830e67b8001c5a" }, - "chain_hash": "4a4b11b5647ee01ef749fbecde897772d97d3bd8cd9adb900e20ad4367cfa8b8" + "chain_hash": "a3c27f3e10af92d191fdf0da65efdf297e6d3a94efeffa5b949a2e3d4e99a73d" } } ],