From 3024a486ecec8f92ab2cc555718a69c7ad405847 Mon Sep 17 00:00:00 2001 From: desiorac Date: Mon, 14 Sep 2026 16:34:31 +0200 Subject: [PATCH] docs: 3.1.1, portee exacte du masquage et de la couverture La 3.1.0 disait le masquage abandonne sur les quatre champs d'identite et aucun autre. Les champs servis en clair a nonce secret (hashes.request, hashes.response, timestamp, seller...) ne sont ni masques ni lies a la racine pour un tiers. Texte seul, format et vecteurs inchanges. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01473z218uifiN7pN3LVoR9M --- CHANGELOG.md | 18 ++++++++++++++++++ SPEC.md | 22 +++++++++++++++++++--- test-vectors.json | 4 ++-- 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56b3838..59d147f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,24 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html) --- +## [3.1.1] — 2026-09-14 + +Wording only. No change to the proof format, the algorithms or the test vectors. + +### Fixed +- The 3.1.0 text said hiding was given up on the identity block's four fields "and on + no other". That overstated it: a public response serves `hashes.request`, + `hashes.response`, `timestamp`, `parties.seller`, `upstream_timestamp` and + `provider_payment.receipt_content_hash` in clear, with their nonces kept secret. Those + values are neither hidden nor tied to the anchored root for a third party: altering one + in a public response leaves every public check passing. The section now says so, and + requires a verifier that relies on one of them to open its commitment with the pair + obtained from the owner. +- "What the signature covers" now states that covered means tamper-evident for the + attestor, not checkable by a third party from the public response. + +--- + ## [3.1.0] — 2026-09-13 ### Added diff --git a/SPEC.md b/SPEC.md index 243d249..64ce945 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,4 +1,4 @@ -# ArkForge Proof Specification v3.1.0 +# ArkForge Proof Specification v3.1.1 An open standard for verifiable agent-to-agent execution proofs. @@ -285,11 +285,25 @@ 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 +These four are the only nonces published: every other nonce stays with the proof's +owner. 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. +**A secret nonce does not make a served value hidden, nor bound.** A public response +(§9) also serves `hashes.request`, `hashes.response`, `timestamp`, `parties.seller`, +`upstream_timestamp` and `provider_payment.receipt_content_hash` in clear. Their +nonces are not published, so: + +- those values are **not hidden**: anyone reads them; +- a third party **cannot tie them to the anchored root**: recomputing the root uses + the commitments only, and nothing links the flat value to its commitment. Altering + one of these fields in a public response leaves every public check passing. + +A verifier that relies on one of them MUST open its commitment with the +`(nonce, value)` pair obtained from the proof's owner (the authenticated response +carries every nonce), and MUST NOT treat the flat value alone as evidence. + **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 @@ -651,6 +665,8 @@ pub.verify(b64url_decode(sig_b64), chain_hash.encode("utf-8")) **Covered** (via the chain hash): `hashes.request`, `hashes.response`, `payment.transaction_id`, `timestamp`, `parties.buyer_fingerprint`, `parties.seller`, `upstream_timestamp` (if present), `provider_payment.receipt_content_hash` (if present). +Covered means the attestor cannot change these values after signing without breaking the signature. It does not mean a third party can check the values it is shown: for `spec_version` `"3.0"` and above, the chain hash is a Merkle root over commitments, and a flat value is tied to its commitment only by a nonce the public response does not carry (§2, *Public opening of the identity block*). For earlier versions, a public response filtered as §9 recommends lacks `parties.buyer_fingerprint` and the payment transaction id, so the chain hash cannot be recomputed from it either. + **Not covered** (mutable metadata): `identity_consistent`, `timestamp_authority` status, `transaction_success`, `upstream_status_code`, `disputed`, `dispute_id`. These fields are informational and may change after proof creation. ### Key distribution diff --git a/test-vectors.json b/test-vectors.json index 5a4e6c9..a831d7b 100644 --- a/test-vectors.json +++ b/test-vectors.json @@ -1,9 +1,9 @@ { - "spec_version": "3.1.0", + "spec_version": "3.1.1", "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 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.", + "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. These four are the only published nonces; values served in clear with a secret nonce (hashes.request, hashes.response, timestamp, seller, ...) are neither hidden nor tied to the root for a third party (SPEC.md §2).", "vectors": [ { "name": "minimal_transaction",