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.
- package/LICENSE +201 -0
- package/README.md +883 -0
- package/cli/abi/ContributionRegistry.json +881 -0
- package/cli/agent.js +2173 -0
- package/cli/anchor-artifact.js +853 -0
- package/cli/anchor.js +400 -0
- package/cli/claim.js +881 -0
- package/cli/core/agent-commit.js +448 -0
- package/cli/core/agent-session.js +598 -0
- package/cli/core/anchor-binding.js +663 -0
- package/cli/core/attestation.js +580 -0
- package/cli/core/evidence-plans.js +495 -0
- package/cli/core/fixtures/evidence-plans/baseline.json +19 -0
- package/cli/core/fulfill-intake.js +1082 -0
- package/cli/core/go-live-preflight.js +481 -0
- package/cli/core/license.js +534 -0
- package/cli/core/manifest.js +243 -0
- package/cli/core/packetseal.js +591 -0
- package/cli/core/registryArtifact.js +49 -0
- package/cli/core/revocation.js +539 -0
- package/cli/core/rfc3161.js +389 -0
- package/cli/core/timestamp.js +482 -0
- package/cli/core/trust-asof.js +479 -0
- package/cli/dataset.js +2950 -0
- package/cli/evidence.js +2227 -0
- package/cli/fulfill-webhook-http.js +438 -0
- package/cli/git.js +220 -0
- package/cli/hash.js +550 -0
- package/cli/identity.js +1072 -0
- package/cli/journal-cli.js +1110 -0
- package/cli/journal-log.js +454 -0
- package/cli/journal.js +334 -0
- package/cli/lineage.js +447 -0
- package/cli/list.js +287 -0
- package/cli/parcel.js +1509 -0
- package/cli/proof.js +578 -0
- package/cli/prove.js +300 -0
- package/cli/receipt.js +631 -0
- package/cli/registry.js +331 -0
- package/cli/reputation.js +344 -0
- package/cli/revocation.js +495 -0
- package/cli/serve-verify-http.js +298 -0
- package/cli/serve-verify.js +333 -0
- package/cli/show.js +339 -0
- package/cli/verify.js +383 -0
- package/cli/vh.js +3927 -0
- package/docs/ADOPT.md +183 -0
- package/docs/ADOPTION.json +11 -0
- package/docs/AGENTTRACE.md +247 -0
- package/docs/ANCHORING.md +167 -0
- package/docs/AUDIT.md +55 -0
- package/docs/CONFORMANCE.md +107 -0
- package/docs/DATALEDGER.md +638 -0
- package/docs/DECIDE.md +47 -0
- package/docs/DECISIONS-PENDING.md +27 -0
- package/docs/DEPLOY-PUBLIC-SITE.md +301 -0
- package/docs/ENGINE-LEDGER.json +12 -0
- package/docs/EVIDENCE.md +519 -0
- package/docs/GO-LIVE.md +66 -0
- package/docs/IDENTITY.md +123 -0
- package/docs/INDEPENDENT-VERIFICATION.md +377 -0
- package/docs/INTEGRITY-JOURNAL.md +337 -0
- package/docs/KEY-LIFECYCLE.md +179 -0
- package/docs/LICENSING.md +46 -0
- package/docs/LINEAGE.md +307 -0
- package/docs/LOOP-AUDIT-2026-07-03.json +580 -0
- package/docs/LOOP-HARDENING-PLAN.md +44 -0
- package/docs/MERKLE-LEAVES.md +113 -0
- package/docs/METRICS.jsonl +31 -0
- package/docs/MORNING.md +204 -0
- package/docs/PILOT.md +444 -0
- package/docs/PROOFPARCEL.md +227 -0
- package/docs/PROOFS.md +262 -0
- package/docs/RECEIPTS.md +341 -0
- package/docs/REPUTATION.md +158 -0
- package/docs/SDK.md +301 -0
- package/docs/STRATEGY-ARCHIVE.md +5055 -0
- package/docs/SUPERVISOR-RUNBOOK.md +52 -0
- package/docs/TRUST-BOUNDARIES.md +335 -0
- package/docs/TRUSTLEDGER.md +1976 -0
- package/docs/USAGE-BUDGET.json +121 -0
- package/docs/VERIFY-SERVICE.md +168 -0
- package/index.js +160 -0
- package/package.json +41 -0
- package/trustledger/build-standalone.js +796 -0
- package/trustledger/cli.js +3179 -0
- package/trustledger/close.js +391 -0
- package/trustledger/corpus.js +159 -0
- package/trustledger/dist/BUILD-PROVENANCE.json +99 -0
- package/trustledger/dist/trustledger-standalone.html +6197 -0
- package/trustledger/dist/trustledger-standalone.html.sha256 +1 -0
- package/trustledger/door-core.js +442 -0
- package/trustledger/fixtures/bank.csv +7 -0
- package/trustledger/fixtures/bank.malformed.csv +3 -0
- package/trustledger/fixtures/bank.noalias.csv +5 -0
- package/trustledger/fixtures/bank.ofx +34 -0
- package/trustledger/fixtures/bank.real.csv +5 -0
- package/trustledger/fixtures/corpus/_shared/prior-close.json +22 -0
- package/trustledger/fixtures/corpus/bank-book-mismatch--benign-twin/inputs.json +14 -0
- package/trustledger/fixtures/corpus/bank-book-mismatch--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/bank-book-mismatch--out-of-trust/inputs.json +14 -0
- package/trustledger/fixtures/corpus/bank-book-mismatch--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/continuity-break--benign-twin/inputs.json +15 -0
- package/trustledger/fixtures/corpus/continuity-break--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/continuity-break--out-of-trust/inputs.json +15 -0
- package/trustledger/fixtures/corpus/continuity-break--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/negative-tenant-ledger--benign-twin/inputs.json +13 -0
- package/trustledger/fixtures/corpus/negative-tenant-ledger--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/negative-tenant-ledger--out-of-trust/inputs.json +13 -0
- package/trustledger/fixtures/corpus/negative-tenant-ledger--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/owner-overdraw--benign-twin/inputs.json +15 -0
- package/trustledger/fixtures/corpus/owner-overdraw--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/owner-overdraw--out-of-trust/inputs.json +15 -0
- package/trustledger/fixtures/corpus/owner-overdraw--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/security-deposit-segregation--benign-twin/inputs.json +16 -0
- package/trustledger/fixtures/corpus/security-deposit-segregation--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/security-deposit-segregation--out-of-trust/inputs.json +13 -0
- package/trustledger/fixtures/corpus/security-deposit-segregation--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/subledger-out-of-balance--benign-twin/inputs.json +13 -0
- package/trustledger/fixtures/corpus/subledger-out-of-balance--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/subledger-out-of-balance--out-of-trust/inputs.json +13 -0
- package/trustledger/fixtures/corpus/subledger-out-of-balance--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/e2e/bank.aliased.csv +4 -0
- package/trustledger/fixtures/e2e/bank.csv +4 -0
- package/trustledger/fixtures/e2e/bank.nsf.csv +4 -0
- package/trustledger/fixtures/e2e/quickbooks.csv +6 -0
- package/trustledger/fixtures/e2e/quickbooks.nsf.csv +8 -0
- package/trustledger/fixtures/e2e/rentroll.csv +6 -0
- package/trustledger/fixtures/e2e/rentroll.nsf.csv +8 -0
- package/trustledger/fixtures/e2e/rentroll.short.csv +5 -0
- package/trustledger/fixtures/plans/baseline.json +25 -0
- package/trustledger/fixtures/plans/price-binding.example.json +27 -0
- package/trustledger/fixtures/policy/ambiguous-deposit-example.json +12 -0
- package/trustledger/fixtures/policy/baseline.json +19 -0
- package/trustledger/fixtures/policy/ca-example.json +12 -0
- package/trustledger/fixtures/policy/negative-tenant-ledger-example.json +12 -0
- package/trustledger/fixtures/policy/owner-overdraw-example.json +12 -0
- package/trustledger/fixtures/quickbooks.csv +7 -0
- package/trustledger/fixtures/quickbooks.real.csv +5 -0
- package/trustledger/fixtures/rentroll.csv +6 -0
- package/trustledger/fixtures/rentroll.real.csv +4 -0
- package/trustledger/ingest.js +1163 -0
- package/trustledger/lib/policy-bundled-loader.js +44 -0
- package/trustledger/lib/sha256-vendored.js +227 -0
- package/trustledger/license.js +563 -0
- package/trustledger/match.js +551 -0
- package/trustledger/plans.js +551 -0
- package/trustledger/policy.js +398 -0
- package/trustledger/public/index.html +512 -0
- package/trustledger/reconcile.js +1486 -0
- package/trustledger/report.js +887 -0
- package/trustledger/seal.js +854 -0
- package/trustledger/server.js +391 -0
- 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>
|