Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 19 additions & 3 deletions SPEC.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions test-vectors.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Loading