verifyhash 0.1.0 → 0.1.1

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 (63) hide show
  1. package/README.md +5 -3
  2. package/cli/agent-hook.js +431 -0
  3. package/docs/ADOPT.md +15 -5
  4. package/docs/AGENT-HOOK.md +111 -0
  5. package/docs/PUBLISH-VERIFY-VH.md +45 -0
  6. package/examples/README.md +185 -0
  7. package/examples/policy.lenient.json +5 -0
  8. package/examples/policy.strict.json +6 -0
  9. package/examples/run.js +366 -0
  10. package/examples/sample-dataset/README.txt +10 -0
  11. package/examples/sample-dataset/corpus/cc-by-poem.txt +8 -0
  12. package/examples/sample-dataset/corpus/mit-notes.txt +4 -0
  13. package/examples/sample-dataset/data/unlabeled.txt +5 -0
  14. package/examples/sample-dataset/vendored/gpl-snippet.txt +5 -0
  15. package/examples/sample-dataset.hints.json +7 -0
  16. package/examples/sample-parcel/data/manifest-of-contents.txt +7 -0
  17. package/examples/sample-parcel/data/records.csv +4 -0
  18. package/examples/sample-parcel/delivery-note.txt +9 -0
  19. package/package.json +25 -3
  20. package/verifier/README.md +555 -0
  21. package/verifier/action/README.md +87 -0
  22. package/verifier/action/action.yml +146 -0
  23. package/verifier/build-standalone-html.js +1287 -0
  24. package/verifier/build-standalone.js +989 -0
  25. package/verifier/ci/journal.generic.sh +96 -0
  26. package/verifier/ci/journal.github-actions.yml +99 -0
  27. package/verifier/ci/reproduce-vh.generic.sh +59 -0
  28. package/verifier/ci/reproduce-vh.github-actions.yml +49 -0
  29. package/verifier/ci/verify-service.generic.sh +96 -0
  30. package/verifier/ci/verify-service.github-actions.yml +88 -0
  31. package/verifier/ci/verify-vh.generic.sh +75 -0
  32. package/verifier/ci/verify-vh.github-actions.yml +56 -0
  33. package/verifier/dist/BUILD-PROVENANCE.json +210 -0
  34. package/verifier/dist/seal-vh-standalone.js +876 -0
  35. package/verifier/dist/seal-vh-standalone.js.sha256 +1 -0
  36. package/verifier/dist/verify-vh-standalone.html +3373 -0
  37. package/verifier/dist/verify-vh-standalone.html.sha256 +1 -0
  38. package/verifier/dist/verify-vh-standalone.js +4121 -0
  39. package/verifier/dist/verify-vh-standalone.js.sha256 +1 -0
  40. package/verifier/lib/canonical.js +141 -0
  41. package/verifier/lib/keccak.js +30 -0
  42. package/verifier/lib/keccak256-vendored.js +206 -0
  43. package/verifier/lib/merkle.js +145 -0
  44. package/verifier/lib/revocation-core.js +606 -0
  45. package/verifier/lib/revocation.js +200 -0
  46. package/verifier/lib/seal-cli.js +374 -0
  47. package/verifier/lib/seal-evidence.js +237 -0
  48. package/verifier/lib/secp256k1-recover.js +249 -0
  49. package/verifier/package.json +39 -0
  50. package/verifier/verify-vh.js +2374 -0
  51. package/docs/ADOPTION.json +0 -11
  52. package/docs/AUDIT.md +0 -55
  53. package/docs/DECIDE.md +0 -47
  54. package/docs/DECISIONS-PENDING.md +0 -27
  55. package/docs/DEPLOY-PUBLIC-SITE.md +0 -301
  56. package/docs/ENGINE-LEDGER.json +0 -12
  57. package/docs/LOOP-AUDIT-2026-07-03.json +0 -580
  58. package/docs/LOOP-HARDENING-PLAN.md +0 -44
  59. package/docs/METRICS.jsonl +0 -31
  60. package/docs/MORNING.md +0 -204
  61. package/docs/STRATEGY-ARCHIVE.md +0 -5055
  62. package/docs/SUPERVISOR-RUNBOOK.md +0 -52
  63. package/docs/USAGE-BUDGET.json +0 -121
@@ -0,0 +1,7 @@
1
+ Contents of this delivery:
2
+ - delivery-note.txt human-readable cover note
3
+ - data/records.csv the delivered records
4
+ - data/manifest-of-contents.txt this file
5
+
6
+ ProofParcel's tamper-evident manifest is the machine-checkable counterpart to this
7
+ human note: it commits to the exact bytes of every file above.
@@ -0,0 +1,4 @@
1
+ id,label,value
2
+ 1,alpha,100
3
+ 2,beta,200
4
+ 3,gamma,300
@@ -0,0 +1,9 @@
1
+ Sample B2B data-delivery parcel for the verifyhash / ProofParcel end-to-end example.
2
+
3
+ This directory stands in for a real data hand-off between two parties (a sender and a
4
+ recipient). ProofParcel pins exactly which files (names AND bytes) were delivered, so a
5
+ later "you never sent X" / "the file you sent was altered" dispute is resolved by
6
+ re-deriving the root from the delivered bytes.
7
+
8
+ The parcel metadata (parcelId, sender, recipient) is UNTRUSTED, self-asserted, and is
9
+ NOT bound into the Merkle root. Only the file set (names + bytes) is bound.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "verifyhash",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Tamper-evident, permissionless on-chain registry of code-contribution hashes, plus an offline data-provenance toolkit (the `vh` CLI).",
5
5
  "license": "Apache-2.0",
6
6
  "main": "index.js",
@@ -9,14 +9,36 @@
9
9
  "./package.json": "./package.json"
10
10
  },
11
11
  "bin": {
12
- "vh": "cli/vh.js"
12
+ "vh": "cli/vh.js",
13
+ "vh-agent-hook": "cli/agent-hook.js"
13
14
  },
14
15
  "files": [
15
16
  "index.js",
16
17
  "cli/",
17
18
  "trustledger/",
19
+ "verifier/",
20
+ "examples/README.md",
21
+ "examples/run.js",
22
+ "examples/sample-dataset/",
23
+ "examples/sample-dataset.hints.json",
24
+ "examples/policy.lenient.json",
25
+ "examples/policy.strict.json",
26
+ "examples/sample-parcel/",
18
27
  "README.md",
19
- "docs/"
28
+ "docs/",
29
+ "!docs/METRICS.jsonl",
30
+ "!docs/USAGE-BUDGET.json",
31
+ "!docs/ENGINE-LEDGER.json",
32
+ "!docs/LOOP-AUDIT-*.json",
33
+ "!docs/LOOP-HARDENING-PLAN.md",
34
+ "!docs/SUPERVISOR-RUNBOOK.md",
35
+ "!docs/DECISIONS-PENDING.md",
36
+ "!docs/STRATEGY-ARCHIVE.md",
37
+ "!docs/MORNING.md",
38
+ "!docs/ADOPTION.json",
39
+ "!docs/DECIDE.md",
40
+ "!docs/DEPLOY-PUBLIC-SITE.md",
41
+ "!docs/AUDIT.md"
20
42
  ],
21
43
  "engines": {
22
44
  "node": ">=18"
@@ -0,0 +1,555 @@
1
+ # `verify-vh` — the independent, offline verifier
2
+
3
+ **You received a sealed verifyhash artifact and you are NOT a verifyhash customer.** This directory
4
+ is everything you need to check it yourself: a single command, near-zero dependencies, **no network,
5
+ and no back-edge into the producer's stack**. You do not need our `ethers`/`hardhat` toolchain, an
6
+ account, a license, or our permission. Read this file, `npm install`, run one command, and decide.
7
+
8
+ `verify-vh` is **free**. There is no paid tier for verification — the producer pays to *seal*; anyone
9
+ may *verify* forever, offline, at zero cost. That is deliberate: a proof a counterparty cannot
10
+ independently check is not a proof.
11
+
12
+ ---
13
+
14
+ ## 0. Get it in 10 seconds (zero-install — start here)
15
+
16
+ The fastest way to check a seal needs **no clone, no `npm install`, no `node_modules`, no account**:
17
+ save ONE self-contained file — [`dist/verify-vh-standalone.js`](dist/verify-vh-standalone.js) — and run
18
+ it with `node`. It depends on **nothing but Node core** (the keccak provider is a vendored pure-JS one,
19
+ cross-checked against `js-sha3` and `ethers`):
20
+
21
+ ```bash
22
+ # 1. Save the single file dist/verify-vh-standalone.js next to the packet you were handed.
23
+
24
+ # 2. (Optional, recommended) check its PUBLISHED checksum so you know the file wasn't swapped in transit.
25
+ # We ship it beside the bundle as dist/verify-vh-standalone.js.sha256 (standard `sha256sum` format):
26
+ sha256sum -c verify-vh-standalone.js.sha256 # -> "verify-vh-standalone.js: OK"
27
+ # (macOS: shasum -a 256 -c verify-vh-standalone.js.sha256)
28
+
29
+ # 3. Run it — no install:
30
+ node verify-vh-standalone.js <packet> --vendor 0xPRODUCER_ADDRESS
31
+ # exit 0 = verifies; exit 3 = REJECTED (names the changed file / wrong signer).
32
+ ```
33
+
34
+ That one file is **byte-for-byte the same verifier** described in the rest of this README — it is built
35
+ deterministically from these sources, and a stale bundle FAILS CI
36
+ (`../test/verifier.standalone.test.js`). The split-source path below (`npm install` the `verifier/` tree
37
+ and run `verify-vh.js`) stays for auditors who want to read each `lib/*` file on its own; **both compute
38
+ the identical verdict and exit code.** The checksum is a transport-integrity check pinned to a hex you
39
+ get out-of-band from the producer — like `--vendor`; the real trust anchor is the source audit in §6.
40
+ **Don't want to trust our checksum either? Reproduce the bundle from source yourself — see §0b.**
41
+
42
+ **The easier path changes nothing about what is proven:** whether you run the one-file bundle or the
43
+ split tree, the seal proves **tamper-evidence + signer-pin**, NOT a trusted "sealed at T" (that still
44
+ requires **P-3** — see §4). The convenience is in the *install*, never in the *claim*.
45
+
46
+ ---
47
+
48
+ ## 0y. No Node at all? Verify (and try to fool it) in your browser — one offline page
49
+
50
+ Everything in §0 still assumes `node` on a PATH. If you — or the counterparty you are convincing —
51
+ have **no terminal at all**, the same verifier ships as **one committed, fully offline HTML file**:
52
+ [`dist/verify-vh-standalone.html`](dist/verify-vh-standalone.html) (integrity sidecar:
53
+ [`dist/verify-vh-standalone.html.sha256`](dist/verify-vh-standalone.html.sha256)). Save it and
54
+ double-click it; the page opens with the **60-second challenge built in**: click **"Load the sample
55
+ packet & verify"** (ACCEPT), then change ONE character of the editable sample file and re-verify
56
+ (**REJECT** — the page names the file you changed) — then drag a REAL packet + its files in and read
57
+ the same verdict + per-file localization this README describes (optional vendor pin and revocations
58
+ drop included). The page also carries a built-in **agent-session demo** (§2c): a sample
59
+ `*.vhagent.json` packet with one tool_call payload already REDACTED behind its hash commitment —
60
+ load it (ACCEPT — redaction is not tamper), tamper one payload byte in the page, and watch the
61
+ REJECT name the offending event `seq`. The page contains **NO network API at all** (no `fetch`, no `XMLHttpRequest`, no
62
+ WebSocket), so your packet bytes never leave your machine — check the browser **devtools Network tab**:
63
+ it stays empty. Like the node bundle, it is built deterministically from these same sources
64
+ (`node build-standalone-html.js --check` reproduces it byte-for-byte, pinned in
65
+ [`dist/BUILD-PROVENANCE.json`](dist/BUILD-PROVENANCE.json)).
66
+
67
+ The boundary on the page is the same one this README carries, verbatim: **ACCEPT is tamper-evidence
68
+ that these exact bytes match the seal — and, for a signed seal, WHO vouched (signer recovery + optional
69
+ vendor pin). It is NOT a trusted timestamp and NOT proof of WHEN without the P-3 trust-root. For
70
+ CI/production gating use the node standalone (`verify-vh-standalone.js`).** The browser page is the
71
+ first-contact convenience; your pipeline gates on the node standalone (§2b).
72
+
73
+ ---
74
+
75
+ ## 0z. The 5-second proof — one command, no flags, no key (`demo`)
76
+
77
+ **Never run this tool before? Start here.** Before you have a packet, an address, or any idea what a "seal"
78
+ is, run the **zero-config demo** — it takes a brand-new user from *nothing* to a *verified packet* in one
79
+ command, with **no flags, no `--vendor` to paste, and no key knowledge**:
80
+
81
+ ```bash
82
+ node verify-vh-standalone.js demo # (or, from the split tree: node verify-vh.js demo)
83
+ # or, with nothing checked out at all: npx --yes <package> demo
84
+ ```
85
+
86
+ It ships a tiny, **genuinely-signed** evidence packet baked into the file, plays it through the **exact same
87
+ verify path** every real check uses, and prints the honest verdict:
88
+
89
+ ```
90
+ STEP 1 — verify the genuine packet (signer recovered from the bytes, then pinned):
91
+ ACCEPT — the artifact verifies. signer: 0x70997970c51812dc3a010c7d01b50e0d17dc79c8
92
+ ...
93
+ STEP 2 — tamper ONE byte of a referenced file, then re-verify the SAME packet:
94
+ REJECT (CHANGED) — the tampered copy is caught:
95
+ CHANGED model-card.md: sealed 0x1aeca0… != on-disk 0xb71fba…
96
+ ```
97
+
98
+ A genuine packet is **ACCEPTED and its signer named**; a one-byte change is **REJECTED**. The demo's signature
99
+ is a real EIP-191 signature by a **fixed, well-known TEST-ONLY key** (hardhat account #1 — never a real key,
100
+ never real funds); the address above is genuinely *recovered* from the bytes by the same pure-JS secp256k1
101
+ routine a real verify uses, not echoed. The demo writes only a throwaway temp dir it deletes, opens **no
102
+ network**, and exits `0`. It proves exactly what §4 says — **tamper-evidence + signer-pin**, NOT a trusted
103
+ "sealed at T" — and nothing more.
104
+
105
+ **Want to poke at it with your own hands?** The bare `demo` runs in a throwaway dir and is gone when it exits —
106
+ you can *watch* it but not *touch* it. Add a directory name and it **writes the same genuinely-signed packet
107
+ into a folder you keep**, then prints the exact copy-paste commands to verify, tamper, and restore it yourself:
108
+
109
+ ```bash
110
+ node verify-vh-standalone.js demo ./vh-demo # writes ./vh-demo/{demo-packet.vhevidence.json, model-card.md, weights.txt}
111
+ # It then prints, ready to paste:
112
+ node verify-vh-standalone.js ./vh-demo/demo-packet.vhevidence.json --vendor 0x7099...79C8 # exit 0 = ACCEPT
113
+ printf 'X' >> ./vh-demo/model-card.md # tamper one byte
114
+ node verify-vh-standalone.js ./vh-demo/demo-packet.vhevidence.json --vendor 0x7099...79C8 # exit 3 = REJECT (CHANGED)
115
+ ```
116
+
117
+ That is the working on-ramp from *watched a demo* to *verified my own bytes on disk* — the packet it writes is
118
+ the same real artifact a producer would hand you (`mechanically tested in ../test/verifier.demo.test.js`), not a
119
+ toy. Once it clicks, point the tool at a **real** packet you were handed
120
+ (`node verify-vh.js <packet> --vendor 0xPRODUCER_ADDRESS`); and when you want a counterparty to be able to pin
121
+ **you**, that is the paid producer side — **sign your own files** with `vh evidence seal --sign` (see §0a).
122
+
123
+ ---
124
+
125
+ ## 0b. "Who verifies the verifier?" — reproduce the bundle from source yourself (zero-trust bootstrap)
126
+
127
+ The published checksum in §0 proves the file survived transport — but it comes **from the same place as
128
+ the bundle**, so on its own it cannot prove the bundle is the source you can read here (if our
129
+ distribution were compromised, both would swap together). The answer to *"who verifies the verifier?"* is
130
+ to **reproduce the bundle from the in-tree source** and confirm the published checksum is exactly what
131
+ that source compiles to. It is offline, Node-core-only (no `npm install`, no `hardhat`), and writes
132
+ nothing:
133
+
134
+ ```bash
135
+ # From the verifier/ tree you can READ end to end (the builder + every lib/*.js it inlines):
136
+ node build-standalone.js --check
137
+ # -> per-target MATCH/MISMATCH for each bundle, its .sha256 sidecar, AND every inlined source file.
138
+ # exit 0 = every committed bundle, sidecar, and the build-provenance manifest reproduce byte-for-byte
139
+ # from source, and every source file hashes to its manifest-pinned sha256.
140
+ # exit 1 = something does not reproduce — the line NAMES the offending file (bundle, sidecar, or a
141
+ # specific lib/*.js source).
142
+ ```
143
+
144
+ The build is **deterministic** (no timestamp, no randomness, a hand-fixed module list), so the bundle
145
+ bytes are a pure function of the committed sources. `--check` recompiles both bundles in memory, recomputes
146
+ their checksums, and compares against the committed files — and cross-checks each inlined source against the
147
+ committed **build-provenance manifest**, [`dist/BUILD-PROVENANCE.json`](dist/BUILD-PROVENANCE.json). That
148
+ manifest maps each published bundle's sha256 to the **ordered, individually-hashed** `lib/*.js` files it
149
+ inlines — so you can `sha256` the exact files you audited and find their hashes there, then see they compose
150
+ (in that order) the bundle whose hash is in the `.sha256` sidecar. Trust roots in **reading source**, not in
151
+ trusting our hex.
152
+
153
+ This proves **build integrity** — the bundle faithfully reproduces the audited source. It is NOT a claim
154
+ that the source's *logic* is correct (read it, and run the conformance corpus, for that), and NOT a trusted
155
+ timestamp/identity (that is **P-3**). `--check` opens **no network** and writes nothing under the tree
156
+ (proven by `../test/verifier.reproduce.test.js`).
157
+
158
+ Reproducing the bundle changes **nothing** about the trust boundary in §4: whether you run the one-file
159
+ bundle or the split tree, the seal proves **tamper-evidence + signer-pin**, NOT a trusted "sealed at T"
160
+ (that still requires **P-3** — see §4). The reproduce step moves trust from *our hex* to *the source you
161
+ read*; it does not widen the *claim*.
162
+
163
+ **Make it a RENEWING control, not a one-time read — wire `--check` into your own CI.** Auditing the
164
+ verifier once is good; re-confirming it on *every* build is better, because a supply-chain swap of the
165
+ verifier itself (a stale bundle, a one-byte source edit) then **fails your pipeline** instead of slipping
166
+ past. Two shipped, copy-paste snippets do exactly that — they run `--check` and pass its exit code
167
+ straight through, so any drift blocks the merge:
168
+
169
+ - **[`ci/reproduce-vh.generic.sh`](ci/reproduce-vh.generic.sh)** — a portable `set -e` shell gate for
170
+ GitLab CI, CircleCI, Jenkins, a Makefile recipe, or a git hook. No config, no install: `./verifier/ci/reproduce-vh.generic.sh`.
171
+ - **[`ci/reproduce-vh.github-actions.yml`](ci/reproduce-vh.github-actions.yml)** — a GitHub Actions
172
+ workflow you drop at `.github/workflows/reproduce-vh.yml`; a green check then *means* "the verifier we
173
+ depend on is still the exact source we audited."
174
+
175
+ These are the verifier-integrity twins of the seal-gate snippets in §2b (that gate your *seals*; these
176
+ gate the *verifier*). They are **examples the loop never runs**, but their exact gate command is
177
+ mechanically tested (`../test/verifier.reproduce-ci-snippet.test.js`): it must exit `0` on a clean
178
+ checkout and **non-zero, naming the offending source file,** when one byte of an inlined `lib/*.js`
179
+ changes — so the snippet you copy is known-good, not aspirational. Wiring the gate widens **nothing**
180
+ about the trust boundary in §4; it just makes the §0b reproduce answer *renew* on every build.
181
+
182
+ ---
183
+
184
+ ## 0a. Produce your OWN seal in 10 seconds, then hand it off (the free self-service round-trip)
185
+
186
+ §0 is the FREE **verify** side. There is a matching FREE **produce** side, so you can run the *whole*
187
+ loop yourself — seal your own files, hand the result to a counterparty, watch them verify it — with **no
188
+ clone, no `npm install`, no account, no key**, on either side. Save ONE file —
189
+ [`dist/seal-vh-standalone.js`](dist/seal-vh-standalone.js) — and run it with `node`. Like the verifier,
190
+ it depends on **nothing but Node core** (the keccak provider is the same vendored pure-JS one):
191
+
192
+ ```bash
193
+ # 1. Save the single file dist/seal-vh-standalone.js (optionally check dist/seal-vh-standalone.js.sha256
194
+ # the same way as the verifier in §0).
195
+
196
+ # 2. Seal up to 25 of YOUR OWN files into one tamper-evident packet — no install, no key, no account:
197
+ node seal-vh-standalone.js <your-folder> -o packet.vhevidence.json # exit 0 = sealed
198
+
199
+ # 3. Hand packet.vhevidence.json + your folder to a counterparty. They run the FREE verifier from §0:
200
+ node verify-vh-standalone.js packet.vhevidence.json --dir <your-folder> # exit 0 = verifies; 3 = REJECTED
201
+ ```
202
+
203
+ That is the entire organic adoption loop, self-service and free on both ends, before any sales call: one
204
+ file to **seal**, one file to **verify**, and the `.vhevidence.json` is the only thing that has to change
205
+ hands. The standalone sealer is built deterministically from these sources and a stale bundle FAILS CI
206
+ (`../test/freeseal.standalone.test.js`); its seal bytes are byte-for-byte identical to the producer's own
207
+ `cli/evidence.js` seal over the same folder, so a free seal is the *same* artifact the paid tool wraps —
208
+ never a toy.
209
+
210
+ **The honest scope boundary is exactly the same as §0 — and the free seal is *narrower* still.** A
211
+ standalone seal proves **tamper-evidence + offline-recompute** — the referenced files are byte-for-byte
212
+ the ones sealed, independently re-derivable by anyone — and **NOT** a trusted "sealed at T" without
213
+ **P-3** (see §4). On top of that, the FREE seal is **UNSIGNED** (no signer to pin — there is no
214
+ `--sign`/`--license`/`--key` flag here at all) and **capped at 25 files** (a folder of more than 25
215
+ hard-errors and writes nothing). **SIGNING** (an EIP-191 signer-pin so a counterparty can pin you with
216
+ `--vendor`) and **UNLIMITED** sealing are the PAID upgrade — `vh evidence seal --sign` / the
217
+ `evidence_unlimited` entitlement (`--license`), routed through the full producer CLI. The free loop is
218
+ the funnel; the paid upgrade adds *who signed it* and *no file cap*.
219
+
220
+ ---
221
+
222
+ ## 1. What you have, in one minute
223
+
224
+ A counterparty (the "producer") ran a paid verifyhash tool over some files and handed you:
225
+
226
+ 1. **The artifact** — a small JSON file (`*.vhevidence.json`, `*.vhseal`, `*.vhdataset.json`, or a
227
+ proof bundle). It lists, for each file, a `relPath` and a keccak-256 `contentHash`, folds those
228
+ into one keccak Merkle **root**, and (if signed) carries a 65-byte secp256k1 signature over the
229
+ canonical bytes of that root.
230
+ 2. **The referenced files** themselves (e.g. `model-card.md`, `weights.bin`). By default they sit
231
+ next to the artifact; otherwise point `--dir` at them.
232
+ 3. **The producer's signer address** (`0x…`, 20 bytes) — out-of-band: a contract, an email
233
+ signature, a website. You pin it with `--vendor` so a *different* key cannot impersonate them.
234
+
235
+ `verify-vh` recomputes the root from **the bytes you actually hold**, recovers **who signed it**, and
236
+ tells you in one line whether both match.
237
+
238
+ ---
239
+
240
+ ## 2. Install & run
241
+
242
+ ```bash
243
+ cd verifier
244
+ npm install # pulls ONE runtime dependency: js-sha3 (keccak). Nothing else.
245
+ node verify-vh.js <artifact> [--vendor 0xADDR] [--dir <files-dir>] [--json]
246
+ # or, after `npm link` / global install:
247
+ verify-vh <artifact> --vendor 0xADDR
248
+ ```
249
+
250
+ Requires Node ≥ 18. No build step, no native modules, no compiler.
251
+
252
+ **Exit codes** (so you can gate CI on them):
253
+
254
+ | code | meaning |
255
+ |------|---------|
256
+ | `0` | **OK** — every referenced byte matches the seal, signature valid, signer == `--vendor` |
257
+ | `3` | **REJECTED** — a clean, expected NO verdict (file changed/missing, bad signature, wrong issuer) |
258
+ | `2` | usage error (bad flags) |
259
+ | `1` | I/O error (artifact unreadable) |
260
+
261
+ ---
262
+
263
+ ## 2a. Gate a whole release in one command — batch / manifest mode
264
+
265
+ A release produces *many* artifacts (an evidence packet per dataset, a reconciliation seal per report, a
266
+ proof bundle per claim). You should not have to call the verifier once per file and `&&` the exit codes
267
+ by hand. Pass several artifacts — or a **manifest** listing them — and get **ONE** verdict and **ONE** CI
268
+ exit code:
269
+
270
+ ```bash
271
+ # Repeated artifacts (each inherits the one --vendor/--dir you pass):
272
+ verify-vh a.vhevidence.json b.vhseal c.vhevidence.json --vendor 0xADDR --dir ./out
273
+
274
+ # A manifest file (newline list OR JSON array), each entry with its OWN optional --vendor/--dir:
275
+ verify-vh --manifest release.manifest --json
276
+ ```
277
+
278
+ **The aggregate exit contract** — the same four codes, now over the *whole set*:
279
+
280
+ | code | meaning |
281
+ |------|---------|
282
+ | `0` | **OK** — and only if — **every** artifact in the batch verifies |
283
+ | `3` | **REJECTED** — **any** artifact is rejected; the report names **which** artifact failed and why |
284
+ | `2` | usage error (bad flag, malformed per-entry `--vendor`, empty manifest, `--manifest` + a positional) |
285
+ | `1` | I/O error (the manifest, or any listed artifact, is unreadable) — the batch never "passes" while an artifact could not be evaluated |
286
+
287
+ **Manifest format.** Either a **newline list** (one entry per line; blank lines and `#` comments are
288
+ skipped) or a **JSON array**. Each entry is an artifact path with an optional per-entry `--vendor` /
289
+ `--dir`. Paths resolve relative to the **manifest file's own directory** (a release ships its manifest
290
+ next to its artifacts); a top-level `--vendor`/`--dir` is a **default** each entry may override.
291
+
292
+ ```text
293
+ # release.manifest (newline form)
294
+ datasets/march.vhevidence.json --vendor 0xb463…3221 --dir datasets/march
295
+ recon/q2.vhseal --vendor 0xb463…3221
296
+ proofs/claim-7.vhproof.json
297
+ ```
298
+
299
+ ```json
300
+ [
301
+ "proofs/claim-7.vhproof.json",
302
+ { "artifact": "recon/q2.vhseal", "vendor": "0xb463…3221" },
303
+ { "artifact": "datasets/march.vhevidence.json", "vendor": "0xb463…3221", "dir": "datasets/march" }
304
+ ]
305
+ ```
306
+
307
+ `--json` emits a **stable aggregate**:
308
+
309
+ ```json
310
+ { "ok": false, "total": 3, "passed": 2, "failed": 1,
311
+ "results": [ /* …one entry PER artifact, each the SAME shape the single-artifact --json emits… */ ] }
312
+ ```
313
+
314
+ Each `results[]` entry is byte-identical in shape to the single-artifact `--json` object (the same core
315
+ verifies every entry — no divergence). Gate your release CI on `ok` (or the process exit code). The
316
+ batch path adds **no new crypto and no new artifact kind**, and every entry keeps the same per-entry
317
+ **path-escape / no-network** guarantees as a lone verify. The **single-artifact** invocation
318
+ (`verify-vh <artifact>`) is unchanged — a lone positional still emits the single-artifact object, not an
319
+ aggregate.
320
+
321
+ ---
322
+
323
+ ## 2b. Wire it into your pipeline — a copy-paste CI merge gate
324
+
325
+ A pilot becomes a renewal when the gate is *wired in*: the build fails the moment a sealed artifact is
326
+ tampered, forged, or signed by the wrong key. Two shipped snippets make that one paste:
327
+
328
+ - **[`ci/verify-vh.generic.sh`](ci/verify-vh.generic.sh)** — a portable `set -e` shell gate for **GitLab
329
+ CI, CircleCI, Jenkins, a Makefile recipe, or a git hook**. It is configured entirely by environment
330
+ variables (no in-file editing), runs the standalone verifier in single-artifact *or* manifest mode, and
331
+ passes the `0/3/2/1` exit code straight through so any non-zero verdict **fails the job**:
332
+
333
+ ```bash
334
+ # gate one artifact:
335
+ VH_VENDOR=0xPRODUCER VH_ARTIFACTS="dist/packet.vhevidence.json" ./verifier/ci/verify-vh.generic.sh
336
+ # gate a WHOLE release in one invocation:
337
+ VH_VENDOR=0xPRODUCER VH_MANIFEST=release.manifest ./verifier/ci/verify-vh.generic.sh
338
+ ```
339
+
340
+ | env | meaning |
341
+ |-----|---------|
342
+ | `VH_VENDOR` | **required** — the producer's signer address (`0x` + 20 bytes), pinned out-of-band |
343
+ | `VH_MANIFEST` | a release manifest (gate every artifact at once) |
344
+ | `VH_ARTIFACTS` | space-separated artifact paths (when no manifest) |
345
+ | `VH_DIR` | optional dir holding the referenced files |
346
+ | `VERIFY_VH` | path to `verify-vh.js` (default `./verifier/verify-vh.js`) |
347
+
348
+ - **[`ci/verify-vh.github-actions.yml`](ci/verify-vh.github-actions.yml)** — a GitHub Actions workflow you
349
+ drop at `.github/workflows/verify-vh.yml`. It installs **only** the standalone verifier (`js-sha3`, no
350
+ ethers/hardhat) and runs the gate on every push / pull request; a green check then *means* every sealed
351
+ artifact still matches the bytes the producer signed.
352
+
353
+ Both ship as **examples the loop never runs**, but their exact gate command is mechanically tested
354
+ (`../test/verifier.ci-snippet.test.js`): it must exit `0` on a good release and `3` on a tampered one, so
355
+ the snippet you copy is known-good, not aspirational.
356
+
357
+ **The boundary holds in CI too: verification is FREE, sealing is PAID.** Running this gate — like every
358
+ `verify-vh` call — costs nothing, needs no licence, and opens no network. The licence gates only the
359
+ **producer's** paid sealing surface; your pipeline gates on the proofs for free. A green gate is a
360
+ *renewing* dependency precisely because checking the producer's seal never costs you anything, while
361
+ producing a valid one is what the producer pays for.
362
+
363
+ ---
364
+
365
+ ## 2c. Verify an AGENT-SESSION packet (`*.vhagent.json`) — AgentTrace, free
366
+
367
+ The producer's `vh agent seal` turns an ordered AI-agent session log (prompts, completions, tool
368
+ calls/results, notes) into ONE tamper-evident, selectively-REDACTABLE packet. `verify-vh`
369
+ auto-detects it like every other artifact kind — same command, same exit codes, zero install via the
370
+ standalone bundle or the offline browser page (§0y has a built-in agent demo):
371
+
372
+ ```bash
373
+ node verify-vh.js session.vhagent.json # unsigned packet (the FREE surface)
374
+ node verify-vh.js session.vhagent.json --vendor 0xPRODUCER # signed packet, signer pinned
375
+ ```
376
+
377
+ What is INDEPENDENTLY re-derived (this verifier imports **nothing** from the producer stack — the
378
+ whole convention is re-implemented against the verifier's own keccak):
379
+
380
+ - **Every event leaf.** For a FULL event the payload's keccak-256 hash commitment is recomputed from
381
+ the payload bytes (and cross-checked against the carried commitment); for a REDACTED event the
382
+ well-formed carried commitment is what the tree binds. A one-byte payload edit — or a **forged
383
+ commitment on a redacted event** — is a REJECT that **names the offending event `seq`**. The
384
+ payload's UTF-8 encoding matches the producer **byte-for-byte** (a lone low surrogate encodes to its
385
+ literal 3-byte form; only a lone HIGH surrogate — which has no UTF-8 encoding — is rejected), so a
386
+ genuine packet the producer sealed is never falsely rejected here.
387
+ - **The ordered head.** An RFC-6962-style, position-bound Merkle root (leaf `0x00` / node `0x01`
388
+ domain separation, children in tree order — NEVER sorted) over the event leaves. Reordering,
389
+ dropping, or inserting events changes the root: `root_mismatch`.
390
+ - **The head signature, when present.** A signed packet carries a detached EIP-191 attestation over
391
+ the HEAD `{ size, root }` (so ONE signature stays valid for every redacted copy). The signer is
392
+ recovered with the same vendored secp256k1 routine and pinned to `--vendor`; a signature pasted
393
+ from a different session is `head_not_bound`, a forged one `bad_signature`, and a `--vendor` pin
394
+ on an UNSIGNED packet is a clean REJECT (`unsigned_cannot_pin_vendor`) — a stripped signature
395
+ never passes a pinned verify.
396
+
397
+ The packet is SELF-CONTAINED (no sibling files, so `--dir` is irrelevant), and REDACTION IS NOT
398
+ TAMPER: a packet whose payloads were withheld behind their commitments still verifies with the
399
+ IDENTICAL head — the verdict lists exactly which seqs are withheld. The same honest boundary as
400
+ everything else here: ACCEPT proves the LOG is unaltered since seal — **not** that the log
401
+ faithfully records what the agent actually did, not a trusted timestamp, and `ts` fields are
402
+ self-asserted (the packet's own in-band trust note says the same).
403
+
404
+ ---
405
+
406
+ ## 3. The exact bytes verified, and the scheme
407
+
408
+ Nothing here is magic; it is two standard primitives you can re-implement in an afternoon.
409
+
410
+ ### 3a. Per-file content hash
411
+ For each referenced file, `contentHash = keccak256(file_bytes)`, the raw file bytes with no framing,
412
+ no normalization, no encoding step. Change one byte → a different hash. The verifier reports that file
413
+ as `CHANGED` and prints both the sealed and the on-disk hash.
414
+
415
+ ### 3b. The keccak Merkle root
416
+ The per-file `(relPath, contentHash)` leaves (plus, for reconciliation seals, a synthetic
417
+ `verdict`/role header leaf so a verdict edit also moves the root) are folded into one **keccak-256
418
+ Merkle root**. The verifier re-derives this root from the files on disk and compares it, byte-for-byte,
419
+ to the `root` embedded in the artifact. (See `lib/merkle.js` for the exact leaf encoding and pairing
420
+ order — it is short and dependency-free.)
421
+
422
+ ### 3c. The signature: EIP-191 `personal_sign` over keccak
423
+ A signed artifact carries a 65-byte `r(32) || s(32) || v(1)` secp256k1 signature. The signed message
424
+ is the **canonical UTF-8 bytes** of the artifact's unsigned payload (the same bytes the verifier
425
+ re-derives in `lib/canonical.js` — it does NOT trust a "signature" field that just echoes a hash). The
426
+ digest is the standard EIP-191 personal-sign pre-image:
427
+
428
+ ```
429
+ keccak256( "\x19Ethereum Signed Message:\n" + <decimal byte length> + <canonical message bytes> )
430
+ ```
431
+
432
+ `verify-vh` recovers the signer **address** from `(message, signature)` using a tiny vendored
433
+ secp256k1 public-key recovery (SEC 1 §4.1.6) over `js-sha3` keccak — **no `ethers`**. The address is
434
+ `"0x" + last-20-bytes( keccak256( X32 || Y32 ) )`, lowercased. If you pass `--vendor 0xADDR`, the
435
+ recovered address must equal it (compared as 20 raw bytes; checksum casing is ignored), or the verdict
436
+ is `wrong_issuer`.
437
+
438
+ ---
439
+
440
+ ## 4. The trust boundary — read this before you rely on it
441
+
442
+ `verify-vh` is honest about what a recomputation can and cannot prove. It proves, **purely from the
443
+ bytes in your hands**:
444
+
445
+ - ✅ **Tamper-evidence** — the referenced files are byte-for-byte the ones the producer sealed (if any
446
+ file changed, you see exactly which one, sealed-hash vs on-disk-hash).
447
+ - ✅ **Offline recompute** — the root is independently re-derivable; you are not trusting our software,
448
+ our servers, or a "trust us, it matched" claim. No network call happens (proven mechanically — see
449
+ §6 and `test/verifier.isolation.test.js`).
450
+ - ✅ **Signer-pin** — *which key* vouched for this artifact, pinned to an address you supply
451
+ out-of-band, so a different key cannot impersonate the producer.
452
+ - ✅ **Revocation-aware (opt-in)** — with `--revocations <file-or-dir> [--as-of <ISO>]` `verify-vh`
453
+ consults the producer's signed key revocations and **downgrades** an otherwise-ACCEPTED artifact to
454
+ **REVOKED** (exit 3) when the signing key was revoked **at or before** the as-of instant (default:
455
+ now). A revocation dated *after* the as-of leaves it ACCEPTED with an informational later-revoked note;
456
+ a forged / tampered / third-party revocation is **ignored** with a warning (a revocation only ever
457
+ *removes* trust, never adds it — a key revokes itself). This reaches the **same** downgrade the
458
+ producer-stack `vh ... verify-signed --revocations <f> --as-of <T>` reaches on the identical inputs —
459
+ fully OFFLINE, no producer stack, no network, no key (see
460
+ [`../docs/KEY-LIFECYCLE.md`](../docs/KEY-LIFECYCLE.md)). A directory is read as a flat pool of
461
+ revocation files; a single file may be one revocation or a JSON array.
462
+
463
+ It deliberately does **NOT** prove:
464
+
465
+ - ❌ **A trusted "sealed at time T".** The signature says *this key vouched for these bytes*, not *on
466
+ this date*. Any `timestamp`/`sealedAt` field inside an artifact is producer-asserted and rides the
467
+ human-owned signing/timestamp trust-root (proposal **P-3** in `../STRATEGY.md`). For an *independent*
468
+ time anchor, the family offers a separate **RFC-3161** timestamp path (`vh … verify-timestamp`,
469
+ also offline) — that is a different deliverable, not something `verify-vh` asserts.
470
+ - ❌ **A legal or accounting opinion.** A green verdict means the bytes and the signer check out. It is
471
+ not an attestation that the underlying claim (a reconciliation, a model's provenance) is *correct* —
472
+ that judgement belongs to the producer and their reviewers.
473
+
474
+ In one sentence: **`verify-vh` tells you the bytes are unchanged and which key signed them — not when,
475
+ and not whether the producer's conclusion is true.**
476
+
477
+ ---
478
+
479
+ ## 5. Worked example: producer seals → hands over packet → you run `verify-vh`
480
+
481
+ This is a real, end-to-end run (test-only ephemeral keys; never a real key or real funds).
482
+
483
+ **Step 1 — the producer seals** a directory of files into a signed evidence packet with their paid
484
+ tool, then publishes their signer address `0xb463…3221` somewhere you trust:
485
+
486
+ ```
487
+ data/
488
+ model-card.md
489
+ weights.bin
490
+ packet.vhevidence.json ← the signed seal the producer hands you, alongside the two files
491
+ ```
492
+
493
+ **Step 2 — you, the counterparty, verify** (you did NOT install the producer's stack):
494
+
495
+ ```bash
496
+ cd verifier && npm install
497
+ node verify-vh.js ../data/packet.vhevidence.json --vendor 0xb463f30cf53d1e0365130363ae9b9867998c3221
498
+ ```
499
+
500
+ Output (exit `0`):
501
+
502
+ ```
503
+ # verify-vh — .../data/packet.vhevidence.json
504
+ kind: vh.evidence-seal-signed
505
+ embedded kind: vh.evidence-seal
506
+ signed: yes
507
+ recovered signer: 0xb463f30cf53d1e0365130363ae9b9867998c3221
508
+ claimed signer: 0xb463f30cf53d1e0365130363ae9b9867998c3221
509
+ pinned --vendor: 0xb463f30cf53d1e0365130363ae9b9867998c3221
510
+ signer matches vendor: yes
511
+ sealed root: 0x51004f29ea5b0081be2943d377b2c1572b0543af4bfea724642fa73db3589dd5
512
+ recomputed root: 0x51004f29ea5b0081be2943d377b2c1572b0543af4bfea724642fa73db3589dd5
513
+ root matches: yes
514
+ files: 2 matched, 0 changed, 0 missing, 0 rejected, 0 unexpected
515
+
516
+ OK — the artifact verifies.
517
+ ```
518
+
519
+ **Step 3 — tamper detection.** Suppose `model-card.md` was altered by one byte in transit. Re-running
520
+ exits `3` and names the file:
521
+
522
+ ```
523
+ recomputed root: 0xb2dd6f94… (≠ sealed root)
524
+ root matches: NO
525
+ REJECTED (CHANGED):
526
+ CHANGED model-card.md: sealed 0x59396c16… != on-disk 0xd241bee9…
527
+ ```
528
+
529
+ A wrong `--vendor` yields `wrong_issuer`; a corrupted signature yields `bad_signature` — both clean
530
+ exit `3` verdicts, never a crash. Add `--json` for a stable machine verdict object
531
+ (`{ verdict, reason, accepted, rootMatches, signerMatchesVendor, counts, … }`) to gate CI.
532
+
533
+ ---
534
+
535
+ ## 6. Why you can trust *this verifier* itself
536
+
537
+ Independence is **mechanically enforced**, not just promised:
538
+
539
+ - **No producer stack.** Every `require(` in this whole tree (`verify-vh.js` + `lib/*`) is grepped by
540
+ `../test/verifier.isolation.test.js`; it must never pull `ethers`, `hardhat`, `@nomicfoundation/*`,
541
+ or anything under `../cli/` or `../trustledger/`. The only runtime dependency is `js-sha3`.
542
+ - **No network, no back-edge.** The same test runs a real verify and asserts the process opens **no
543
+ socket and no network handle** — `verify-vh` never `require`s `http`/`https`/`net`/`dns`. It cannot
544
+ phone home, because it has nothing to phone home *with*.
545
+ - **Read-only.** It holds no key, writes nothing, and leaves your working tree byte-for-byte untouched.
546
+ - **Cross-checked crypto.** Its secp256k1 recovery is independently re-implemented and continuously
547
+ cross-checked against the production path (`../test/verifier.crypto.test.js`) so the two can never
548
+ silently drift.
549
+
550
+ See [`../docs/INDEPENDENT-VERIFICATION.md`](../docs/INDEPENDENT-VERIFICATION.md) for the full
551
+ counterparty-facing specification.
552
+
553
+
554
+ ---
555
+ <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>