verifyhash 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +883 -0
  3. package/cli/abi/ContributionRegistry.json +881 -0
  4. package/cli/agent.js +2173 -0
  5. package/cli/anchor-artifact.js +853 -0
  6. package/cli/anchor.js +400 -0
  7. package/cli/claim.js +881 -0
  8. package/cli/core/agent-commit.js +448 -0
  9. package/cli/core/agent-session.js +598 -0
  10. package/cli/core/anchor-binding.js +663 -0
  11. package/cli/core/attestation.js +580 -0
  12. package/cli/core/evidence-plans.js +495 -0
  13. package/cli/core/fixtures/evidence-plans/baseline.json +19 -0
  14. package/cli/core/fulfill-intake.js +1082 -0
  15. package/cli/core/go-live-preflight.js +481 -0
  16. package/cli/core/license.js +534 -0
  17. package/cli/core/manifest.js +243 -0
  18. package/cli/core/packetseal.js +591 -0
  19. package/cli/core/registryArtifact.js +49 -0
  20. package/cli/core/revocation.js +539 -0
  21. package/cli/core/rfc3161.js +389 -0
  22. package/cli/core/timestamp.js +482 -0
  23. package/cli/core/trust-asof.js +479 -0
  24. package/cli/dataset.js +2950 -0
  25. package/cli/evidence.js +2227 -0
  26. package/cli/fulfill-webhook-http.js +438 -0
  27. package/cli/git.js +220 -0
  28. package/cli/hash.js +550 -0
  29. package/cli/identity.js +1072 -0
  30. package/cli/journal-cli.js +1110 -0
  31. package/cli/journal-log.js +454 -0
  32. package/cli/journal.js +334 -0
  33. package/cli/lineage.js +447 -0
  34. package/cli/list.js +287 -0
  35. package/cli/parcel.js +1509 -0
  36. package/cli/proof.js +578 -0
  37. package/cli/prove.js +300 -0
  38. package/cli/receipt.js +631 -0
  39. package/cli/registry.js +331 -0
  40. package/cli/reputation.js +344 -0
  41. package/cli/revocation.js +495 -0
  42. package/cli/serve-verify-http.js +298 -0
  43. package/cli/serve-verify.js +333 -0
  44. package/cli/show.js +339 -0
  45. package/cli/verify.js +383 -0
  46. package/cli/vh.js +3927 -0
  47. package/docs/ADOPT.md +183 -0
  48. package/docs/ADOPTION.json +11 -0
  49. package/docs/AGENTTRACE.md +247 -0
  50. package/docs/ANCHORING.md +167 -0
  51. package/docs/AUDIT.md +55 -0
  52. package/docs/CONFORMANCE.md +107 -0
  53. package/docs/DATALEDGER.md +638 -0
  54. package/docs/DECIDE.md +47 -0
  55. package/docs/DECISIONS-PENDING.md +27 -0
  56. package/docs/DEPLOY-PUBLIC-SITE.md +301 -0
  57. package/docs/ENGINE-LEDGER.json +12 -0
  58. package/docs/EVIDENCE.md +519 -0
  59. package/docs/GO-LIVE.md +66 -0
  60. package/docs/IDENTITY.md +123 -0
  61. package/docs/INDEPENDENT-VERIFICATION.md +377 -0
  62. package/docs/INTEGRITY-JOURNAL.md +337 -0
  63. package/docs/KEY-LIFECYCLE.md +179 -0
  64. package/docs/LICENSING.md +46 -0
  65. package/docs/LINEAGE.md +307 -0
  66. package/docs/LOOP-AUDIT-2026-07-03.json +580 -0
  67. package/docs/LOOP-HARDENING-PLAN.md +44 -0
  68. package/docs/MERKLE-LEAVES.md +113 -0
  69. package/docs/METRICS.jsonl +31 -0
  70. package/docs/MORNING.md +204 -0
  71. package/docs/PILOT.md +444 -0
  72. package/docs/PROOFPARCEL.md +227 -0
  73. package/docs/PROOFS.md +262 -0
  74. package/docs/RECEIPTS.md +341 -0
  75. package/docs/REPUTATION.md +158 -0
  76. package/docs/SDK.md +301 -0
  77. package/docs/STRATEGY-ARCHIVE.md +5055 -0
  78. package/docs/SUPERVISOR-RUNBOOK.md +52 -0
  79. package/docs/TRUST-BOUNDARIES.md +335 -0
  80. package/docs/TRUSTLEDGER.md +1976 -0
  81. package/docs/USAGE-BUDGET.json +121 -0
  82. package/docs/VERIFY-SERVICE.md +168 -0
  83. package/index.js +160 -0
  84. package/package.json +41 -0
  85. package/trustledger/build-standalone.js +796 -0
  86. package/trustledger/cli.js +3179 -0
  87. package/trustledger/close.js +391 -0
  88. package/trustledger/corpus.js +159 -0
  89. package/trustledger/dist/BUILD-PROVENANCE.json +99 -0
  90. package/trustledger/dist/trustledger-standalone.html +6197 -0
  91. package/trustledger/dist/trustledger-standalone.html.sha256 +1 -0
  92. package/trustledger/door-core.js +442 -0
  93. package/trustledger/fixtures/bank.csv +7 -0
  94. package/trustledger/fixtures/bank.malformed.csv +3 -0
  95. package/trustledger/fixtures/bank.noalias.csv +5 -0
  96. package/trustledger/fixtures/bank.ofx +34 -0
  97. package/trustledger/fixtures/bank.real.csv +5 -0
  98. package/trustledger/fixtures/corpus/_shared/prior-close.json +22 -0
  99. package/trustledger/fixtures/corpus/bank-book-mismatch--benign-twin/inputs.json +14 -0
  100. package/trustledger/fixtures/corpus/bank-book-mismatch--benign-twin/meta.json +7 -0
  101. package/trustledger/fixtures/corpus/bank-book-mismatch--out-of-trust/inputs.json +14 -0
  102. package/trustledger/fixtures/corpus/bank-book-mismatch--out-of-trust/meta.json +7 -0
  103. package/trustledger/fixtures/corpus/continuity-break--benign-twin/inputs.json +15 -0
  104. package/trustledger/fixtures/corpus/continuity-break--benign-twin/meta.json +7 -0
  105. package/trustledger/fixtures/corpus/continuity-break--out-of-trust/inputs.json +15 -0
  106. package/trustledger/fixtures/corpus/continuity-break--out-of-trust/meta.json +7 -0
  107. package/trustledger/fixtures/corpus/negative-tenant-ledger--benign-twin/inputs.json +13 -0
  108. package/trustledger/fixtures/corpus/negative-tenant-ledger--benign-twin/meta.json +7 -0
  109. package/trustledger/fixtures/corpus/negative-tenant-ledger--out-of-trust/inputs.json +13 -0
  110. package/trustledger/fixtures/corpus/negative-tenant-ledger--out-of-trust/meta.json +7 -0
  111. package/trustledger/fixtures/corpus/owner-overdraw--benign-twin/inputs.json +15 -0
  112. package/trustledger/fixtures/corpus/owner-overdraw--benign-twin/meta.json +7 -0
  113. package/trustledger/fixtures/corpus/owner-overdraw--out-of-trust/inputs.json +15 -0
  114. package/trustledger/fixtures/corpus/owner-overdraw--out-of-trust/meta.json +7 -0
  115. package/trustledger/fixtures/corpus/security-deposit-segregation--benign-twin/inputs.json +16 -0
  116. package/trustledger/fixtures/corpus/security-deposit-segregation--benign-twin/meta.json +7 -0
  117. package/trustledger/fixtures/corpus/security-deposit-segregation--out-of-trust/inputs.json +13 -0
  118. package/trustledger/fixtures/corpus/security-deposit-segregation--out-of-trust/meta.json +7 -0
  119. package/trustledger/fixtures/corpus/subledger-out-of-balance--benign-twin/inputs.json +13 -0
  120. package/trustledger/fixtures/corpus/subledger-out-of-balance--benign-twin/meta.json +7 -0
  121. package/trustledger/fixtures/corpus/subledger-out-of-balance--out-of-trust/inputs.json +13 -0
  122. package/trustledger/fixtures/corpus/subledger-out-of-balance--out-of-trust/meta.json +7 -0
  123. package/trustledger/fixtures/e2e/bank.aliased.csv +4 -0
  124. package/trustledger/fixtures/e2e/bank.csv +4 -0
  125. package/trustledger/fixtures/e2e/bank.nsf.csv +4 -0
  126. package/trustledger/fixtures/e2e/quickbooks.csv +6 -0
  127. package/trustledger/fixtures/e2e/quickbooks.nsf.csv +8 -0
  128. package/trustledger/fixtures/e2e/rentroll.csv +6 -0
  129. package/trustledger/fixtures/e2e/rentroll.nsf.csv +8 -0
  130. package/trustledger/fixtures/e2e/rentroll.short.csv +5 -0
  131. package/trustledger/fixtures/plans/baseline.json +25 -0
  132. package/trustledger/fixtures/plans/price-binding.example.json +27 -0
  133. package/trustledger/fixtures/policy/ambiguous-deposit-example.json +12 -0
  134. package/trustledger/fixtures/policy/baseline.json +19 -0
  135. package/trustledger/fixtures/policy/ca-example.json +12 -0
  136. package/trustledger/fixtures/policy/negative-tenant-ledger-example.json +12 -0
  137. package/trustledger/fixtures/policy/owner-overdraw-example.json +12 -0
  138. package/trustledger/fixtures/quickbooks.csv +7 -0
  139. package/trustledger/fixtures/quickbooks.real.csv +5 -0
  140. package/trustledger/fixtures/rentroll.csv +6 -0
  141. package/trustledger/fixtures/rentroll.real.csv +4 -0
  142. package/trustledger/ingest.js +1163 -0
  143. package/trustledger/lib/policy-bundled-loader.js +44 -0
  144. package/trustledger/lib/sha256-vendored.js +227 -0
  145. package/trustledger/license.js +563 -0
  146. package/trustledger/match.js +551 -0
  147. package/trustledger/plans.js +551 -0
  148. package/trustledger/policy.js +398 -0
  149. package/trustledger/public/index.html +512 -0
  150. package/trustledger/reconcile.js +1486 -0
  151. package/trustledger/report.js +887 -0
  152. package/trustledger/seal.js +854 -0
  153. package/trustledger/server.js +391 -0
  154. package/trustledger/valueproof.js +350 -0
@@ -0,0 +1,227 @@
1
+ # ProofParcel — B2B data-delivery receipts
2
+
3
+ ProofParcel turns a data hand-off between two parties into a **portable, independently-verifiable
4
+ proof-of-delivery receipt**: a tamper-evident manifest that pins **exactly which files (names AND
5
+ bytes) were delivered** for a parcel, plus a signable attestation over that parcel's identity. It is a
6
+ **thin adapter over the same path-bound Merkle + signed-attestation core** as
7
+ [DataLedger](DATALEDGER.md) (`cli/parcel.js` consumes `cli/core/manifest.js` and
8
+ `cli/core/attestation.js`), so every claim it makes is independently re-derivable.
9
+
10
+ Every ProofParcel command is **offline, needs NO private key, and needs NO network**. You can hand a
11
+ manifest, a verify result, an attestation, or a signed container to the other party and they can
12
+ re-derive the result on an air-gapped machine with only the `vh` CLI — they do not have to trust your
13
+ server, your build machine, or you.
14
+
15
+ > **Read this first:** the trust posture below is the SAME wording carried in-band in every artifact
16
+ > (`cli/parcel.js` › `TRUST_NOTE` / `PARCEL_TRUST_NOTE`, shared verbatim with DataLedger) and in
17
+ > [`docs/TRUST-BOUNDARIES.md`](TRUST-BOUNDARIES.md). Do not overclaim past it.
18
+
19
+ ---
20
+
21
+ ## Who buys this, and why
22
+
23
+ **B2B data exchange has an expensive failure mode: a delivery dispute.** "You never sent file X." "The
24
+ file you sent was altered." "That is not the parcel we agreed to." When a contract has a
25
+ delivery-acceptance clause, resolving such a dispute is slow and costly. ProofParcel issues a
26
+ contractual **proof-of-delivery receipt** that makes the dispute re-derivable instead of arguable:
27
+ either party can re-compute the same Merkle root from the files on disk and detect any
28
+ edit/rename/add/remove, and a signed attestation lets a sender **vouch** for exactly which parcel they
29
+ handed over.
30
+
31
+ Buyers are data vendors, market-data redistributors, ML-data marketplaces, and any contract with a
32
+ delivery-acceptance clause — a **different paying buyer** than DataLedger's data-provenance reviewer,
33
+ with a different budgeted reason to pay.
34
+
35
+ ---
36
+
37
+ ## What ProofParcel PROVES (and what it does NOT)
38
+
39
+ A parcel manifest commits to a Merkle root over the full set of `(relPath, content)` pairs delivered —
40
+ **file names AND bytes**. From that root and the manifest, anyone can re-derive, offline:
41
+
42
+ 1. **Exactly which files were delivered — names and bytes.** Any edit, rename, add, or remove changes
43
+ the root. A re-computed root that matches the recorded root means the files on disk are
44
+ byte-for-byte (and name-for-name) the parcel that was committed to. A hand-edited manifest `root`
45
+ cannot fake a `MATCH` — `vh parcel verify` re-derives the root from the actual file bytes.
46
+
47
+ 2. **A signable parcel IDENTITY.** `vh parcel attest` emits a deterministic, byte-canonical UNSIGNED
48
+ payload (root + fileCount + a canonical `manifestDigest` over the delivered file set) that a sender
49
+ can sign. `vh parcel verify-attest` recovers the signer offline and confirms it.
50
+
51
+ **It does NOT, by itself, prove:**
52
+
53
+ - **A trusted delivery TIMESTAMP.** "Delivered ON date T" / "unaltered since date T" rides the
54
+ **human-owned signing/timestamp trust-root** ([`STRATEGY.md` P-3](../STRATEGY.md), `needs-human`).
55
+ The loop ships the **FORMAT, the OFFLINE VERIFIER, AND the `vh parcel sign` command** — but `vh parcel
56
+ sign` only ever reads a key the human PROVISIONED outside the loop (it never generates/persists/logs a
57
+ key); PROVISIONING the key / standing up a timestamp anchor is the human step. This is the **same honest
58
+ trust posture as DataLedger** — a receipt binds the file SET and is signable, but a signature is not a
59
+ timestamp.
60
+ - **That the self-asserted `parcel` metadata is true.** The optional `parcel` block
61
+ (`parcelId` / `sender` / `recipient`) is **UNTRUSTED, self-asserted metadata**: it is **NOT bound
62
+ into the Merkle root**, editing it does not change the root, and it is **EXCLUDED** from the
63
+ attestation `manifestDigest` (a signer commits to the file SET, never the labels). The same applies
64
+ to the per-file `{source, license}` hints.
65
+
66
+ ---
67
+
68
+ ## Commands
69
+
70
+ > **Run it, don't just read it.** [`examples/run.js`](../examples/run.js) is the executable companion to
71
+ > the worked example below: `node examples/run.js` drives the ProofParcel pipeline (`parcel build → verify
72
+ > → attest`, alongside the DataLedger side) against tiny committed sample data, offline and with no key,
73
+ > and prints a PASS/FAIL summary — including a caught delivery tamper. It writes only to an OS temp dir,
74
+ > references (does not run) the human-gated sign/timestamp steps, and is test-gated by
75
+ > `test/cli.examples.test.js`. See [`examples/README.md`](../examples/README.md).
76
+
77
+ | Command | What it produces | Property |
78
+ | --- | --- | --- |
79
+ | `vh parcel build <dir> --out <p>` | a tamper-evident parcel manifest (Merkle root + per-file `{relPath,contentHash,leaf}` + optional untrusted `parcel` block) | offline, no key, no network |
80
+ | `vh parcel verify <dir> --manifest <p>` | re-derives the root from a fresh copy on disk + a precise per-file `ADDED/REMOVED/CHANGED` diff | offline, no key, no network; **CI-gateable exit 0 MATCH / 3 MISMATCH** |
81
+ | `vh parcel attest <manifest> [--out <p>] [--json]` | the deterministic, byte-canonical **UNSIGNED** attestation payload a sender signs (root + fileCount + `manifestDigest`; `signed:false`) | offline, no key, no network |
82
+ | `vh parcel sign <manifest> --key-env <VAR>\|--key-file <p> [--out <p>] [--json]` | signs the UNSIGNED attestation with a key YOU provisioned → the signed container `verify-attest` accepts. Read-only of YOUR key; never generates/persists/logs a key | offline, **caller-supplied key**, no network |
83
+ | `vh parcel verify-attest <signed> [--manifest <m>] [--signer <addr>] [--json]` | recovers the signer, optionally pins the expected sender (`--signer`) and binds the signature to your parcel (`--manifest`) | **offline, no key, no network, CI-gateable exit 0 ACCEPTED / 3 REJECTED** |
84
+ | `vh parcel timestamp-request <manifest> [--out <p>] [--json]` | the SHA-256 digest of the canonical attestation bytes — the exact `messageImprint` you submit to your RFC-3161 TSA | offline, no key, no network |
85
+ | `vh parcel timestamp-wrap <manifest> --token <p> [--out <p>] [--json]` | wraps the TSA's returned RFC-3161 token into a verifiable `verifyhash.parcel-attestation-timestamped` container (binds it to the re-derived SHA-256 digest) | offline, no key, no network |
86
+ | `vh parcel verify-timestamp <container> [--manifest <m>] [--json]` | OFFLINE-verifies a timestamped container: re-derives the digest, confirms the RFC-3161 token binds it, optionally binds to your parcel; ACCEPTED (with genTime / TSA serial / policy OID) or REJECTED | **offline, no key, no network, CI-gateable exit 0 ACCEPTED / 3 REJECTED** |
87
+
88
+ The signed container uses ProofParcel's own `kind: "verifyhash.parcel-attestation-signed"`, distinct
89
+ from DataLedger's `verifyhash.dataset-attestation-signed`, so a dataset signed-container does **not**
90
+ cross-verify as a parcel one (and vice-versa) — even though the two products' UNSIGNED identity bytes
91
+ can coincide for the same files. The scheme is `eip191-personal-sign` (EIP-191 `personal_sign` over the
92
+ EXACT canonical UNSIGNED bytes).
93
+
94
+ `vh parcel verify-attest` performs up to three checks, ACCEPTED only when **every requested** one
95
+ passes:
96
+
97
+ - **signature recovers to the claimed signer** (always): the embedded signature must recover to the
98
+ address the container claims as `signer`.
99
+ - **`--signer <addr>` pins the expected sender** (optional): the recovered signer must equal a specific
100
+ address you expected.
101
+ - **`--manifest <m>` binds your parcel** (optional): the canonical UNSIGNED bytes re-computed from YOUR
102
+ parcel manifest must be byte-identical to the signed payload — proving the signature vouches for the
103
+ parcel **you** hold.
104
+
105
+ ### `vh parcel sign` — the one-command signing leg (reads a key YOU provisioned)
106
+
107
+ `vh parcel sign <manifest> --key-env <VAR> | --key-file <path> [--out <p>] [--json]` is the **one command
108
+ that turns "the sender has a key" into a signed container the recipient can verify.** It builds the UNSIGNED
109
+ payload exactly as `vh parcel attest` does (no re-implementation), constructs an in-process ethers `Wallet`
110
+ from the key YOU supply, signs the canonical bytes (`eip191-personal-sign`), and **wraps WITHOUT editing**
111
+ the payload into the `verifyhash.parcel-attestation-signed` container the existing `vh parcel verify-attest`
112
+ accepts.
113
+
114
+ It performs a **read-only of a key YOU provisioned outside this tool**; it **never generates, never
115
+ persists, and never logs (or echoes) a key**, and it is **OFFLINE — no provider, no network**. The key is
116
+ read from EXACTLY ONE of `--key-env <VAR>` or `--key-file <path>`, used in-process ONLY to sign, then
117
+ discarded. **Neither source, both sources, a missing env var, an unreadable file, or a malformed/all-zero
118
+ key HARD-ERRORS before any signing**, naming only the SOURCE — **never the key material**. On success the
119
+ output prints ONLY the PUBLIC signer address, the output path, and the scheme.
120
+
121
+ > **Trust posture (inherited verbatim — a signature is NOT a timestamp).** This is the SHARED in-band
122
+ > `SIGN_TRUST_NOTE` (`cli/parcel.js`), the same wording the `sign` command prints and the **SAME honest
123
+ > posture as DataLedger**, so the caveat can never drift from the code:
124
+ >
125
+ > > This signs the parcel IDENTITY (root, fileCount, manifestDigest) with the key YOU supplied. A self-managed key attests "the signer says so" — it is NOT an independent, trusted TIMESTAMP: "delivered/unaltered since a date T" still needs the human-owned signing/timestamp trust-root (needs-human, P-3). The key must be one YOU provisioned OUTSIDE this tool.
126
+
127
+ ---
128
+
129
+ ## Worked example: sender builds a parcel → signs (P-3, ONE command) → recipient verify-attests
130
+
131
+ ```sh
132
+ # --- SENDER ---
133
+ # 1. Build the tamper-evident delivery receipt over the files being delivered.
134
+ vh parcel build ./delivery --out parcel.json \
135
+ --parcel-id PX-42 --sender "Acme Data" --recipient "Beta Corp"
136
+ # (parcelId/sender/recipient are UNTRUSTED self-asserted metadata, NOT bound into the root)
137
+
138
+ # 2. Emit the canonical UNSIGNED attestation payload (the signing-ready bytes).
139
+ vh parcel attest parcel.json --out attest.json
140
+ # attest.json carries `signed:false` — it is NOT yet a vouch and NOT a timestamp.
141
+
142
+ # 3. [HUMAN step, STRATEGY.md P-3 — PROVISION ONLY] The sender PROVISIONS a real key OUTSIDE the loop, then
143
+ # SIGNs with ONE command: `vh parcel sign` reads that key, signs the canonical attest bytes
144
+ # (eip191-personal-sign), and wraps them into a signed container WITHOUT editing the payload (it stays
145
+ # signed:false). The loop NEVER generates/persists/logs the key. (In tests this uses an EPHEMERAL
146
+ # throwaway `Wallet.createRandom()` key — test-only, never a real key.)
147
+ vh parcel sign parcel.json --key-env PARCEL_SIGNING_KEY --out signed.json
148
+ # signed by 0x<sender's public address> scheme: eip191-personal-sign
149
+ # signed parcel attestation written: /abs/path/signed.json
150
+
151
+ # --- RECIPIENT (offline, no key, no network) ---
152
+ # 4. Verify the signed container binds the parcel actually received, by the expected sender.
153
+ vh parcel verify-attest signed.json --manifest parcel.json --signer 0x<sender-address>
154
+ # Exit 0 ACCEPTED only if: the signature recovers to the claimed signer, the recovered signer is the
155
+ # expected sender, AND the signature binds the recipient's own parcel manifest. Exit 3 REJECTED
156
+ # otherwise — a recipient's CI can gate on this.
157
+
158
+ # 5. Independently, confirm the delivered bytes still match the receipt.
159
+ vh parcel verify ./received --manifest parcel.json # exit 0 MATCH / 3 MISMATCH
160
+ ```
161
+
162
+ The wrap step is **wrap-don't-edit**: the signed container embeds the EXACT canonical UNSIGNED bytes as
163
+ a string, and the embedded payload stays strictly `signed:false` — wrapping adds a vouch, it never
164
+ edits the thing vouched for.
165
+
166
+ ---
167
+
168
+ ## The independent delivery timestamp (P-3 Option B): an RFC-3161 TSA proves "existed by date T"
169
+
170
+ A self-managed signature attests only "the sender **says so**". For the stronger claim that an
171
+ **independent** third party saw this exact parcel identity **by time T**, ProofParcel ships the same P-3
172
+ Option (B) machinery as DataLedger — the `verifyhash.parcel-attestation-timestamped` **FORMAT** and the
173
+ OFFLINE **VERIFIER** `vh parcel verify-timestamp` — proved end-to-end with **self-minted test tokens** (a
174
+ test-only mock TSA with an ephemeral key — **NEVER a real TSA**). Obtaining a real token is a human/network
175
+ step. The flow is **`timestamp-request` → (obtain a token from your TSA) → `timestamp-wrap` →
176
+ `verify-timestamp`**:
177
+
178
+ ```sh
179
+ # 1. REQUEST: emit the SHA-256 digest of the canonical parcel-attestation bytes (the TSA's messageImprint).
180
+ vh parcel timestamp-request parcel.json
181
+ # sha256 digest (the messageImprint to stamp): 9f12…ab
182
+
183
+ # 2. [HUMAN, P-3 Option B] Pick a TSA you trust and obtain a token over that digest (network step).
184
+ # The loop NEVER calls a TSA, holds no token, and generates none.
185
+
186
+ # 3. WRAP: bind the returned RFC-3161 token to the re-derived digest, WITHOUT editing the payload.
187
+ vh parcel timestamp-wrap parcel.json --token token.der --out parcel.timestamped.json
188
+
189
+ # 4. The RECIPIENT verifies offline — no key, no network — and (optionally) binds it to THEIR parcel.
190
+ vh parcel verify-timestamp parcel.timestamped.json --manifest parcel.json
191
+ # verify-timestamp: ACCEPTED
192
+ # ACCEPTED: an RFC-3161 TSA asserted this parcel identity existed by:
193
+ # genTime (ISO UTC): 2026-01-01T00:00:00Z TSA serial: 2a policy OID: 1.2.3.4.5
194
+ # (exit 0; exit 3 if a tampered token / mismatched digest / edited payload / different manifest fails)
195
+ ```
196
+
197
+ > **The exact bounded trust claim (never overclaims).** ACCEPTED means **an RFC-3161 TSA asserted this exact
198
+ > parcel identity (the SHA-256 digest of the canonical attestation bytes) existed by `<genTime>`** — and this
199
+ > is **as trustworthy as the TSA whose certificate YOU trust**. `verify-timestamp` does **NOT** validate the
200
+ > TSA's X.509 certificate chain or the token's CMS signature — use a CMS verifier (`openssl ts -verify`) for
201
+ > full PKI validation. It NEVER claims "delivered/unaltered since date T" without that qualification. A
202
+ > tampered token, a mismatched digest, or an edited embedded attestation **REJECTS** — never a false ACCEPT.
203
+
204
+ P-3 Option (B)'s human handoff collapses to: **(1)** pick a TSA you trust; **(2)** run
205
+ `vh parcel timestamp-request` to get the digest; **(3)** obtain a token from your TSA over that digest;
206
+ **(4)** run `vh parcel timestamp-wrap` — **done**; recipients verify offline with `vh parcel verify-timestamp`.
207
+
208
+ ---
209
+
210
+ ## Trust boundary (the same honest posture as DataLedger)
211
+
212
+ - The receipt **binds the file SET** to a Merkle root and is **signable** — but it is **NOT by itself a
213
+ trusted delivery TIMESTAMP**. "Delivered ON date T" rides the human-owned trust-root
214
+ ([`STRATEGY.md` P-3](../STRATEGY.md), `needs-human`).
215
+ - The `parcel` metadata (`parcelId` / `sender` / `recipient`) and the per-file `{source, license}`
216
+ hints are **UNTRUSTED, self-asserted**: not bound into the root, excluded from the attestation digest,
217
+ and proving nothing on their own.
218
+ - A valid signature proves the **holder of `signer`'s key vouched for THIS parcel identity** — not a
219
+ timestamp, not the truth of the metadata.
220
+
221
+ See [`docs/TRUST-BOUNDARIES.md`](TRUST-BOUNDARIES.md) and DataLedger's
222
+ [trust posture](DATALEDGER.md) — ProofParcel reuses the SAME in-band `TRUST_NOTE` verbatim so the
223
+ caveats never drift between products.
224
+
225
+
226
+ ---
227
+ <sub>© 2026 verifyhash.com · Licensed under Apache-2.0 (SPDX-License-Identifier: Apache-2.0) — see the [LICENSE](https://verifyhash.com/LICENSE) and [NOTICE](https://verifyhash.com/NOTICE) served with this file.</sub>
package/docs/PROOFS.md ADDED
@@ -0,0 +1,262 @@
1
+ # verifyhash portable proofs — artifact schema, verification steps, worked example
2
+
3
+ This is the canonical spec for the **portable Merkle-proof artifact** that `cli/proof.js` writes and
4
+ verifies (task **T-9.2**), exported by `vh prove <file> --root <dir> --out <p>` and consumed by the
5
+ read-only `vh verify-proof <p>`. A proof artifact is a versioned, strictly-validated JSON file that
6
+ lets a third party **independently** confirm a single file is part of an anchored repository Merkle
7
+ root — needing **only the artifact + an RPC URL**, never the original repo, no working tree, no key.
8
+
9
+ That portability is the missing half of the project's core promise ("anyone can later prove that some
10
+ content is byte-for-byte what was anchored, without trusting any server"). `vh prove` already builds a
11
+ genuine proof, but on its own that proof only lives in the prover's terminal. `--out` exports it so it
12
+ can leave the prover's machine; `vh verify-proof` lets the recipient verify it with **no trust in the
13
+ prover** (it re-derives and re-folds everything itself).
14
+
15
+ > **Trust posture (read this first).** The artifact is an **UNTRUSTED transport container**, in exactly
16
+ > the spirit of [`docs/TRUST-BOUNDARIES.md`](TRUST-BOUNDARIES.md) and the receipt posture in
17
+ > [`docs/RECEIPTS.md`](RECEIPTS.md). `vh verify-proof` **never trusts the file's claims** — it
18
+ > RE-DERIVES the leaf from `contentHash` + `relPath`, RE-FOLDS the `proof` itself, and checks the root
19
+ > the *fold produced* on-chain (not the `root` field the file claims). Every field below is therefore
20
+ > labelled **UNTRUSTED transport — verification re-derives**: tampering with any of them is either
21
+ > caught offline or produces a non-`ACCEPTED` verdict, never a false accept.
22
+
23
+ > **What an `ACCEPTED` verdict proves: SET-MEMBERSHIP — not authorship, not the `uri`.** It binds the
24
+ > file's path + bytes to an anchored Merkle **root**, exactly the boundary the contract's `verifyLeaf`
25
+ > draws. It says nothing about who anchored that root, what `contributor` means (see the `authorBound`
26
+ > rule in [`docs/TRUST-BOUNDARIES.md`](TRUST-BOUNDARIES.md)), or any `uri`. `vh verify-proof` leads its
27
+ > human-readable output with this caveat verbatim.
28
+
29
+ ---
30
+
31
+ ## Schema
32
+
33
+ `cli/proof.js` defines the discriminators `kind: "verifyhash.merkle-proof"` and `schemaVersion: 1`
34
+ (distinct from the receipt kinds in [`docs/RECEIPTS.md`](RECEIPTS.md), so a random JSON file, a
35
+ receipt, or a future/foreign artifact is never misread as a current proof). `readProofArtifact`
36
+ validates **strictly** and throws on ANY deviation rather than filling defaults — a malformed/short
37
+ hash or a non-hex `proof` hard-errors, so `vh verify-proof` can never silently accept a structurally
38
+ bogus file. This mirrors the receipt schema's strict-validation posture.
39
+
40
+ Every field is **UNTRUSTED transport**: verification re-derives, it never relies on a field being
41
+ honest. The columns below name what each field is *for* and how verification *re-checks* it.
42
+
43
+ | Field | Type | Required | How verification re-checks it (UNTRUSTED transport) |
44
+ |-------|------|----------|------------------------------------------------------|
45
+ | `kind` | string | yes | Must equal `"verifyhash.merkle-proof"` exactly, else rejected as not-a-proof-artifact. A structural discriminator only. |
46
+ | `schemaVersion` | integer | yes | Must be a version this build understands (currently `1`); any other is rejected so a future/foreign file is never misread. |
47
+ | `contentHash` | `0x`+64 hex (32 bytes) | yes | The bare `keccak256` of the file's bytes. The leaf is RE-DERIVED from this + `relPath`; a tampered `contentHash` breaks the re-derived leaf and is REJECTED offline. |
48
+ | `relPath` | non-empty string | yes | The file's repo-relative POSIX path, the value bound into the leaf. The leaf is RE-DERIVED from `contentHash` + `relPath`; changing it changes the re-derived leaf and is REJECTED offline. |
49
+ | `leaf` | `0x`+64 hex (32 bytes) | yes | The path-bound leaf `pathLeaf(relPath, contentHash) = keccak256(DIR_LEAF_DOMAIN ‖ relPath ‖ 0x00 ‖ contentHash)`. Verification RE-DERIVES this from `contentHash`+`relPath` and rejects if the stored `leaf` does not equal the re-derived one — a forged `leaf` alone cannot fool it. |
50
+ | `root` | `0x`+64 hex (32 bytes) | yes | The directory's anchored Merkle root the proof folds to. Verification computes its OWN root by folding `leaf` through `proof`; the `root` field is only confirmed to equal that computed root, and it is the *computed* root that is checked on-chain. |
51
+ | `proof` | array of `0x`+64 hex | yes | The sorted-pair Merkle siblings. RE-FOLDED with the same `nodeHash` convention the contract's `verifyLeaf` uses; a tampered sibling no longer folds to `root` and is REJECTED offline. May be `[]` for a single-file tree (`leaf == root`). |
52
+ | `contractAddress` | `0x`+40 hex (address) | optional | A hint of WHERE the prover expects the root anchored. Recorded when the artifact is built on the on-chain prove path. An explicit `--contract` always overrides it; it is never trusted blindly. |
53
+ | `chainId` | non-negative integer | optional | A hint of WHICH chain the root is anchored on. Informational/self-describing; recorded on the on-chain build path, never required. |
54
+
55
+ An artifact built with the no-key `--dry-run`/build path legitimately omits `contractAddress`/`chainId`
56
+ (there was no chain context); the verifier then supplies `--contract`/`--rpc`. An artifact built on the
57
+ on-chain prove path records both so `vh verify-proof` can run with **no** `--contract` flag.
58
+
59
+ ### The leaf and fold convention (same as the contract)
60
+
61
+ The proof artifact reuses the **exact** Merkle machinery `vh hash <dir>` and the contract's
62
+ `verifyLeaf` agree on — there is no second scheme (see [`docs/MERKLE-LEAVES.md`](MERKLE-LEAVES.md)):
63
+
64
+ - the **path-bound leaf** is `pathLeaf(relPath, contentHash) = keccak256(DIR_LEAF_DOMAIN ‖ relPath ‖ 0x00 ‖ contentHash)`,
65
+ so the proof is tied to the file's **location**, not just its bytes (renaming or moving the file
66
+ changes the leaf and the proof no longer folds);
67
+ - the **fold** tags the leaf with `LEAF_TAG`, then walks `proof` applying `NODE_TAG` sorted-pair
68
+ hashing (`nodeHash`) — byte-identically to the contract's `verifyLeaf`.
69
+
70
+ `vh verify-proof` reuses `hash.js`'s `pathLeaf` / `leafHash` / `nodeHash` directly (not a
71
+ re-implementation), so the offline fold and the on-chain `verifyLeaf` can never silently diverge.
72
+
73
+ ---
74
+
75
+ ## Verification steps
76
+
77
+ `vh verify-proof <p>` is **read-only and needs no key, no repo, and no working tree** — just the
78
+ artifact and an RPC URL. It never constructs a signer. It runs two stages, and prints `ACCEPTED`
79
+ **only** when the offline fold **and** both on-chain checks pass:
80
+
81
+ ### 1. Offline fold (no network)
82
+
83
+ The internal-consistency gate. Purely offline (no network even needed), it:
84
+
85
+ 1. **RE-DERIVES the leaf** from `contentHash` + `relPath`: `derivedLeaf = pathLeaf(relPath, contentHash)`,
86
+ and checks `leafMatches = (artifact.leaf == derivedLeaf)`. A forged `leaf`, or a tampered
87
+ `contentHash`/`relPath`, fails here.
88
+ 2. **RE-FOLDS the proof**: `computed = leafHash(leaf)`, then for each sibling `s`,
89
+ `computed = nodeHash(computed, s)`; checks `foldsToRoot = (computed == artifact.root)`. A tampered
90
+ `proof` sibling (or `root`) fails here.
91
+
92
+ If either check fails the verdict is **`REJECTED`** immediately, **with no network call** — there is
93
+ nothing meaningful to ask the chain about a proof that does not even fold to its own claimed root.
94
+ The CLI names exactly which check failed.
95
+
96
+ ### 2. On-chain check (one read-only call set)
97
+
98
+ Only when the offline fold holds, and only if a provider is supplied, `vh verify-proof` makes one
99
+ read-only check set against the root the **offline fold produced** (`computedRoot`, which equals the
100
+ artifact `root` since the fold held — so the file's `root` is never trusted unchecked):
101
+
102
+ 1. `isAnchored(root)` — is the root actually anchored on-chain? If not, the verdict is **`NOT ANCHORED`**
103
+ (a distinct, non-zero exit), NOT a false accept: the proof is internally valid but there is nothing
104
+ on-chain to prove it against (it was never anchored, or you are pointed at the wrong contract/chain).
105
+ 2. `verifyLeaf(root, leaf, proof)` — the contract's own verdict (defense in depth: even if the offline
106
+ fold had a bug, the chain decides). `verifyLeaf` tags the supplied `leaf` itself and replays the
107
+ sorted-pair fold.
108
+
109
+ The contract address resolves as: explicit `--contract` (or `VH_CONTRACT`) > the artifact's recorded
110
+ `contractAddress`. With no provider at all, `vh verify-proof` reports the offline fold result but does
111
+ **not** claim `ACCEPTED` (acceptance requires the on-chain leg) — a script reading the status never
112
+ mistakes an offline-only pass for a full accept.
113
+
114
+ ### Verdicts
115
+
116
+ | Verdict | Meaning | Exit |
117
+ |---------|---------|------|
118
+ | `ACCEPTED` | Offline fold held AND root is anchored AND on-chain `verifyLeaf` accepted. The file's path + bytes are a leaf of an anchored root. | 0 |
119
+ | `REJECTED` | An offline or on-chain check failed (tampered `leaf`/`contentHash`/`proof`/`root`, or the chain rejected the proof). Never a false accept. | non-zero |
120
+ | `NOT ANCHORED` | The proof folds offline, but its root was never anchored on-chain. Distinct from a tamper. | non-zero |
121
+
122
+ ---
123
+
124
+ ## Worked end-to-end example (prove → hand over → verify-proof)
125
+
126
+ The whole point is that the **prover** and the **verifier** can be different people on different
127
+ machines: the only thing that crosses between them is the artifact file.
128
+
129
+ **Prover** — build and export the artifact (no key needed; works on the `--dry-run` build path):
130
+
131
+ ```
132
+ $ vh prove src/index.js --root ./repo --out proof.json --dry-run
133
+ Wrote portable proof artifact: /work/repo/proof.json
134
+ repo root dir: /work/repo (5 files)
135
+ file: src/index.js
136
+ merkle root: 0x9f8c…a1
137
+ content hash: 0x4b2e…7c
138
+ leaf (path-bound): 0x77ad…02
139
+ proof (2 siblings):
140
+ 0x1c3d…9e
141
+ 0xa0f5…44
142
+ ```
143
+
144
+ The exported `proof.json` (every field UNTRUSTED transport — `vh verify-proof` re-derives):
145
+
146
+ ```json
147
+ {
148
+ "kind": "verifyhash.merkle-proof",
149
+ "schemaVersion": 1,
150
+ "root": "0x9f8c00000000000000000000000000000000000000000000000000000000a100",
151
+ "leaf": "0x77ad00000000000000000000000000000000000000000000000000000000a200",
152
+ "contentHash": "0x4b2e00000000000000000000000000000000000000000000000000000000a300",
153
+ "relPath": "src/index.js",
154
+ "proof": [
155
+ "0x1c3d00000000000000000000000000000000000000000000000000000000a400",
156
+ "0xa0f500000000000000000000000000000000000000000000000000000000a500"
157
+ ],
158
+ "contractAddress": "0x5FbDB2315678afecb367f032d93F642f64180aa3",
159
+ "chainId": 31337
160
+ }
161
+ ```
162
+
163
+ > An artifact built on the **on-chain** prove path (with `--contract`/`--rpc`) records
164
+ > `contractAddress` + `chainId`, so the verifier needs no `--contract`. An artifact built with bare
165
+ > `--dry-run` and no chain context omits both; the verifier then passes `--contract`/`--rpc`.
166
+
167
+ **Hand over** the file (email, attach to a PR, drop in a bucket — it is read-only public-membership
168
+ evidence; it holds **no secret**, unlike a claim receipt). The recipient does **not** need the repo.
169
+
170
+ **Verifier** — confirm it with only the artifact + an RPC URL, no repo, no key:
171
+
172
+ ```
173
+ $ vh verify-proof proof.json --rpc https://…
174
+ NOTE: this proves SET-MEMBERSHIP only — that the named file (its path + bytes) is a leaf of an
175
+ anchored repo Merkle root. It does NOT prove authorship, who anchored the root, or anything about
176
+ any `uri`. The artifact is an UNTRUSTED transport container: verify-proof RE-DERIVES the leaf and
177
+ RE-FOLDS the proof itself (it never trusts the file's claims), then confirms the root is anchored
178
+ on-chain. Set-membership in an anchored root is exactly what the contract's verifyLeaf attests.
179
+
180
+ proof artifact: proof.json
181
+ relPath: src/index.js
182
+ contentHash: 0x4b2e…7c
183
+ leaf: 0x77ad…02
184
+ root: 0x9f8c…a1
185
+ proof siblings: 2
186
+
187
+ offline recompute (no network):
188
+ leaf re-derived from contentHash+relPath: yes
189
+ proof folds to the claimed root: yes
190
+
191
+ registry authenticated: REGISTRY_ID ok (v1), chainId 137
192
+
193
+ on-chain checks (one read-only call set):
194
+ root is anchored (isAnchored): yes
195
+ contract verifyLeaf accepts the proof: yes
196
+
197
+ result: ACCEPTED
198
+ ```
199
+
200
+ If the verifier (or anyone in transit) had altered the artifact's `proof`, `leaf`, or `contentHash`,
201
+ the offline fold would not hold and the verdict would be `REJECTED` — caught offline, no network even
202
+ needed. If the `root` was never anchored, the verdict would be `NOT ANCHORED` rather than a false
203
+ accept.
204
+
205
+ **The on-chain leg authenticates the registry first, on the artifact's recorded chain** (T-11.2).
206
+ Before any on-chain check, verify-proof runs the shared `assertRegistry` preflight: it confirms a
207
+ contract is actually deployed at the address, that it self-identifies as a genuine verifyhash registry
208
+ (`REGISTRY_ID` / `REGISTRY_VERSION`), AND — because the artifact records the `chainId` it was anchored
209
+ on — that the provider is on **that same chain**. An artifact that says "anchored on chainId 137"
210
+ **hard-errors** rather than be "verified" against a different chain that returns fakes. That is the
211
+ portability promise made trustworthy: the consumer no longer trusts the prover's RPC blindly. The
212
+ human verdict prints a `registry authenticated: REGISTRY_ID ok (vN), chainId N` line (above) before
213
+ the on-chain checks; a loud, non-default `--skip-identity-check` bypasses the preflight for a known
214
+ local-dev contract. See the README's
215
+ [authenticated reads](../README.md#authenticated-reads-registry-identity--chainid) section.
216
+
217
+ `--json` emits the same verdict + per-check booleans (`offline.{leafMatches,foldsToRoot,ok}`,
218
+ `onChain.{checked,rootAnchored,verifyLeaf}`, `accepted`, `status`) plus the trust note, for tooling. It
219
+ also carries a top-level `registry: { id, version, chainId }` block proving the on-chain leg ran
220
+ against an authenticated registry on the artifact's recorded chain (or `{ "skipped": true, "note": … }`
221
+ under `--skip-identity-check`, or `null` when no on-chain leg ran — an offline-only / rejected-early
222
+ verdict).
223
+
224
+ ---
225
+
226
+ ## What this does NOT prove
227
+
228
+ Consistent with [`docs/TRUST-BOUNDARIES.md`](TRUST-BOUNDARIES.md) and the contract's `verifyLeaf`
229
+ boundary:
230
+
231
+ - **Not authorship.** `ACCEPTED` says the file is in an anchored root; it says nothing about who
232
+ anchored it or what `contributor` means (that is the `authorBound` distinction — one-shot `anchor`
233
+ is front-runnable, only commit-reveal binds an author).
234
+ - **Not the `uri`.** The artifact carries no `uri`; even where one exists on the record, it is an
235
+ untrusted hint the contract never fetched or validated.
236
+ - **Not "this is the latest/only version."** It proves membership in *the* root in the artifact; a
237
+ different snapshot has a different root.
238
+
239
+ > Rule of thumb: **`vh verify-proof` binds a file's path + bytes to an anchored root; that is all.**
240
+
241
+ ---
242
+
243
+ ## Tests
244
+
245
+ `test/cli.verifyproof.test.js` proves the behaviour this doc describes:
246
+
247
+ - the offline fold re-derives the leaf and folds to the root with **no network**, and a tampered
248
+ `proof` / `leaf` / `contentHash` is caught offline (`offlineOk === false`);
249
+ - strict validation hard-errors on a wrong `kind`, an unsupported `schemaVersion`, a short/malformed
250
+ hash, a non-hex/non-array `proof`, an empty `relPath`, and non-JSON input;
251
+ - end-to-end against a live hardhat node: build `--out` then `vh verify-proof` (artifact + RPC only,
252
+ **no repo**) `ACCEPTED`s a genuine proof, the on-chain build path records `contractAddress` so
253
+ verify needs no `--contract`, tampering `proof`/`leaf`/`contentHash` `REJECTED`s, a never-anchored
254
+ root reports `NOT ANCHORED`, and `--json` round-trips the verdict + per-check booleans.
255
+
256
+ `test/cli.proofs.docs.test.js` is the docs-rot guard for this file + the README CLI block: it pins the
257
+ schema fields, the `kind`/`schemaVersion`, the verification stages, and the trust caveats to the real
258
+ `cli/proof.js` exports, so the prose cannot silently drift from the code.
259
+
260
+
261
+ ---
262
+ <sub>© 2026 verifyhash.com · Licensed under Apache-2.0 (SPDX-License-Identifier: Apache-2.0) — see the [LICENSE](https://verifyhash.com/LICENSE) and [NOTICE](https://verifyhash.com/NOTICE) served with this file.</sub>