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,337 @@
1
+ # The integrity journal — tamper-evident verification OVER TIME (`vh journal`)
2
+
3
+ Every other verifyhash surface — `vh verify`, `vh evidence verify`, `vh serve-verify`, the SDK, the GitHub
4
+ Action — answers one question: **"do these exact bytes match this seal RIGHT NOW?"** and then exits. The
5
+ **integrity journal** is the structurally-new capability: an **append-only, hash-chained log of verify
6
+ verdicts**. Each run appends one entry; the log is **itself tamper-evident**, so a deleted / edited /
7
+ reordered / inserted past entry **breaks the chain** and `vh journal verify` **localizes the first break**.
8
+
9
+ That is the *"verified **continuously** from run A to run B, and here is the exact entry where one drifted"*
10
+ artifact a one-shot verify cannot produce — a standing record a recipient **re-runs**, not a one-time event.
11
+
12
+ It reuses the **same** hash-chain shape the project already trusts for seals (keccak256 over canonical bytes)
13
+ — **no new crypto** is introduced. The core (`cli/journal.js`) is **pure**: no disk I/O, no socket, no key.
14
+
15
+ ---
16
+
17
+ ## The command
18
+
19
+ ```bash
20
+ # Record ONE new verdict as a hash-chained line (strictly additive; prior lines are never rewritten):
21
+ vh journal append <artifact> --to <journalfile> [--dir <d>] [--ts <ISO>] [--json]
22
+
23
+ # Walk the whole on-disk chain and report PASS / BROKEN / DRIFTED:
24
+ vh journal verify <journalfile> [--json]
25
+ ```
26
+
27
+ - `append` **verifies** `<artifact>` (a `*.vhevidence.json` seal / signed container) through the **existing**
28
+ composed verify path and records the resulting verdict as one new line. **Recording a `REJECTED` verdict is
29
+ a successful append** (exit 0) — the journal's job is to faithfully record what it saw; the drift surfaces
30
+ at `verify` time.
31
+ - `verify` re-derives every entry hash and walks the chain from genesis to head.
32
+
33
+ The on-disk format is **newline-delimited JSON (JSONL)** — one entry per line — chosen precisely because an
34
+ append is **strictly additive**: `fs.appendFileSync` writes only the new line's bytes and never rewrites a
35
+ prior line, so the pre-existing bytes are preserved byte-for-byte.
36
+
37
+ ---
38
+
39
+ ## The entry schema
40
+
41
+ Each line is one entry:
42
+
43
+ ```json
44
+ { "seq": 0,
45
+ "prevHash": "0x…(32 bytes)",
46
+ "ts": "2026-07-01T00:00:00.000Z",
47
+ "artifact": "dist/release.vhevidence.json",
48
+ "verdict": { "verdict": "ACCEPTED", "…": "the full composed verify envelope, VERBATIM" },
49
+ "entryHash": "0x…(32 bytes)" }
50
+ ```
51
+
52
+ | field | meaning |
53
+ |-------------|---------|
54
+ | `seq` | 0-based position in the journal (a genesis append is `seq` 0). Must equal the line's index. |
55
+ | `prevHash` | the **prior** entry's `entryHash`, or the genesis constant for `seq` 0. |
56
+ | `ts` | a **self-asserted** wall-clock instant the caller supplies (see the honesty boundary below). |
57
+ | `artifact` | a caller-supplied label for **what** was observed (a path / id). Stored verbatim. |
58
+ | `verdict` | the verify verdict recorded, stored **verbatim** (deep-equal to the composed verify output). |
59
+ | `entryHash` | `keccak256(canonical({ schema, seq, prevHash, ts, artifact, verdict }))` — the chain link. |
60
+
61
+ Constants (stable; a schema bump requires a breaking-change version):
62
+
63
+ - **schema tag** folded into every `entryHash`: `vh.integrity-journal/v1`
64
+ - **genesis domain** (the `seq` 0 `prevHash` is `keccak256` of this fixed string): `vh.integrity-journal/v1:genesis`
65
+
66
+ The preimage is serialized with a **recursive, key-sorted, deterministic** JSON encoder, so two logically
67
+ identical observations hash identically regardless of key insertion order, while remaining a total, injective
68
+ encoding of the value.
69
+
70
+ ---
71
+
72
+ ## The chain guarantee
73
+
74
+ Because each `entryHash` folds in `prevHash`, **every `entryHash` commits to the entire prefix before it**.
75
+ Therefore:
76
+
77
+ - **Editing any past field** (a `verdict`, `ts`, `artifact`, `seq`, or `prevHash`) makes that entry's
78
+ `entryHash` no longer re-derive from its contents → **break localized at that `seq`**.
79
+ - **Deleting / reordering / inserting** an entry shifts a `seq` or a `prevHash` → **break localized** at the
80
+ first offending index.
81
+ - A **hand-edit into non-JSON** on a line is caught as a malformed entry → **BROKEN**.
82
+
83
+ `vh journal verify` **never returns a false PASS**: any deviation yields a non-zero exit naming the drifted
84
+ artifact + the `seq` where it drifted + `brokenAt` (the index). A false positive is treated as a security bug.
85
+
86
+ Two distinct failure modes, both non-zero (exit 3):
87
+
88
+ - **BROKEN** — the hash-chain itself was tampered (a deleted / reordered / inserted / hand-edited past line).
89
+ A broken chain means **none** of the recorded verdicts can be trusted; this takes precedence.
90
+ - **DRIFTED** — the chain is **intact** (every recorded observation is authentic + in order) but some
91
+ recorded observation's verdict was **not `ACCEPTED`**. This is the "integrity over time" signal: the
92
+ artifact was recorded continuously and one observation FAILED. A one-shot verify cannot produce this.
93
+
94
+ **PASS** requires **both**: the chain is unbroken **and** every recorded observation was `ACCEPTED`.
95
+
96
+ ---
97
+
98
+ ## The 0/3 exit-code contract
99
+
100
+ `vh journal` uses the **same** `0` / `3` CI-exit contract as `vh verify` / `vh evidence verify`, so it drops
101
+ into an existing pipeline unchanged:
102
+
103
+ | exit | name | meaning |
104
+ |------|------|---------|
105
+ | `0` | PASS | `verify`: unbroken chain, every observation `ACCEPTED`. `append`: recorded cleanly (of any verdict). |
106
+ | `3` | BROKEN / DRIFTED | `verify`: the chain was tampered **or** a recorded observation was `REJECTED`. Block the merge. |
107
+ | `2` | USAGE | misconfiguration (missing argument / bad flag). Never a silent pass. |
108
+ | `1` | IO | a file could not be read/written. Never a silent pass. |
109
+
110
+ A green pipeline therefore MEANS "the artifact has verified continuously across every recorded run, and the
111
+ record itself is tamper-evident."
112
+
113
+ ---
114
+
115
+ ## Transparency-log proofs (publish a tree head; auditors verify offline)
116
+
117
+ The chain above answers *"is my copy of the whole log intact?"* — but the checker must hold (and re-walk)
118
+ the **entire** journal. The transparency-log surface adds the second half a real transparency log needs: an
119
+ **RFC-6962 / Certificate-Transparency-style ordered Merkle tree** over the journal's entry hashes — the same
120
+ lineage as CT's certificate logs and Sigstore's **Rekor** — so that:
121
+
122
+ - a single **tree head** `{ size, root }` (one 32-byte root + a count) commits to the **whole ordered log**;
123
+ - **inclusion** of any one entry under that head is provable with an O(log n) path — the auditor never needs
124
+ the log;
125
+ - **consistency** between an old head (size *m*) and a new head (size *n*) is provable with an O(log n)
126
+ path — proving the size-*n* log is an **append-only extension** of the size-*m* log, i.e. **no history was
127
+ rewritten** between the two heads. A hash-chain alone cannot prove that compactly.
128
+
129
+ **This is a deliberately different tree from the file-set tree in `cli/hash.js`.** The seal tree is a
130
+ *sorted-leaf, sorted-pair* Merkle root: it commits to a **SET** of files and is intentionally
131
+ order-independent. A journal is the opposite — **order is meaning** — so the log tree is
132
+ **position-preserving**: leaves stay at their `seq`, interior nodes fold their children in tree order
133
+ (never min/max-sorted), with RFC-6962 domain separation (`leaf = keccak256(0x00 ‖ entryHash)`,
134
+ `node = keccak256(0x01 ‖ left ‖ right)`). Only a position-binding tree can prove
135
+ inclusion-at-a-position or append-only consistency. Same `keccak256` primitive as everything else here —
136
+ **no new crypto**; the core is the pure [`cli/journal-log.js`](../cli/journal-log.js) (no fs, no socket,
137
+ no key, no clock).
138
+
139
+ ### The four commands
140
+
141
+ ```bash
142
+ vh journal tree-head <journalfile> [--json] # print the publishable head { size, root }
143
+ vh journal prove-inclusion <journalfile> --seq <i> [--out <f>] [--json] # emit an inclusion-proof artifact
144
+ vh journal prove-consistency <journalfile> --from <m> [--out <f>] [--json] # emit a consistency-proof artifact
145
+ vh journal check-proof <prooffile> [--json] # OFFLINE auditor: ACCEPTED / REJECTED
146
+ ```
147
+
148
+ All four are **read-only** and **verify-only** (the only write is the `--out` proof artifact you name); they
149
+ hold **no key** and bind **no network**. `tree-head` / `prove-*` first re-verify the hash chain and **refuse**
150
+ (exit 3, `BROKEN`) to emit anything over a tampered journal. `check-proof` is the **third-party AUDITOR**
151
+ command: it reads **only** the proof artifact — never the journal, never a key, never a socket — so you can
152
+ hand an auditor a published tree head plus a proof file and they confirm inclusion / append-only-ness
153
+ **without ever holding your log**.
154
+
155
+ > **What "OFFLINE" means here (independence caveat — read this before selling `check-proof` as "independent").**
156
+ > `check-proof` — and `tree-head` / `prove-*` — run in the **producer package**
157
+ > (`cli/journal-cli.js` → [`cli/journal-log.js`](../cli/journal-log.js), which `require`s **ethers**), so
158
+ > `npm i verifyhash` pulls in the producer stack. "OFFLINE" here means **no network and no log** (the auditor
159
+ > holds only the proof artifact) — it does **NOT** mean "no producer stack." These self-contained proof
160
+ > artifacts are **not yet** checkable with the zero-dependency standalone [`verifier/`](../verifier/) bundle a
161
+ > **seal** enjoys, so a counterparty's security team cannot (today) verify a proof with a light/independent
162
+ > client the way a CT/Rekor client can. See the
163
+ > [**Independence scope**](#independence-scope--journal-verification-currently-needs-the-producer-package)
164
+ > section below.
165
+
166
+ All four ride the **same 0/3 exit contract** as the table above: `0` = head printed / proof emitted /
167
+ proof `ACCEPTED`; `3` = `BROKEN` chain or `REJECTED` proof (fail closed — a forged, edited, or unknown-kind
168
+ artifact is always `REJECTED`, never a silent pass); `2` = usage; `1` = IO.
169
+
170
+ ### The proof-artifact schemas
171
+
172
+ `prove-inclusion` emits `kind: "vh-journal-inclusion"` — everything `check-proof` needs, and **nothing of
173
+ the log itself**:
174
+
175
+ ```json
176
+ { "kind": "vh-journal-inclusion",
177
+ "journal": "journal.jsonl",
178
+ "leaf": "0x…(the entryHash being proven)",
179
+ "seq": 1,
180
+ "size": 3,
181
+ "root": "0x…(the head this proof verifies against)",
182
+ "path": ["0x…", "0x…"],
183
+ "note": "…the self-asserted-head note, verbatim…" }
184
+ ```
185
+
186
+ `prove-consistency` emits `kind: "vh-journal-consistency"` — the two heads plus the RFC-6962 §2.1.2 proof
187
+ that the first is a prefix of the second:
188
+
189
+ ```json
190
+ { "kind": "vh-journal-consistency",
191
+ "journal": "journal.jsonl",
192
+ "first": { "size": 3, "root": "0x…" },
193
+ "second": { "size": 5, "root": "0x…" },
194
+ "proof": ["0x…", "0x…", "0x…", "0x…"],
195
+ "note": "…the self-asserted-head note, verbatim…" }
196
+ ```
197
+
198
+ `check-proof` dispatches on `kind`, re-derives the root(s) from the artifact's own fields, and prints
199
+ `ACCEPTED` (exit 0) only when the proof verifies against the head **embedded in the artifact** — so the
200
+ auditor must compare that embedded head against a head they trust. Every accept carries this reminder,
201
+ verbatim:
202
+
203
+ > ACCEPTED means the proof verifies against the head EMBEDDED in the artifact; compare that head (size + root) against a tree head you trust (e.g. one the operator published/signed) before relying on it
204
+
205
+ ### Worked example (copy-pasteable, end-to-end)
206
+
207
+ ```bash
208
+ # 0) something to observe: seal a directory into an evidence packet (any *.vhevidence.json works)
209
+ mkdir -p bundle && printf 'hello\n' > bundle/a.txt && printf 'world\n' > bundle/b.txt
210
+ vh evidence seal ./bundle --out ./bundle/release.vhevidence.json
211
+
212
+ # 1) append THREE observations (three hash-chained verify verdicts)
213
+ vh journal append ./bundle/release.vhevidence.json --to journal.jsonl
214
+ vh journal append ./bundle/release.vhevidence.json --to journal.jsonl
215
+ vh journal append ./bundle/release.vhevidence.json --to journal.jsonl
216
+
217
+ # 2) publish the head — one { size, root } line commits to the whole ordered log
218
+ vh journal tree-head journal.jsonl
219
+ # tree head of journal.jsonl: { size: 3, root: 0x49714d…e409d2 }
220
+
221
+ # 3) prove entry seq 1 is committed at that position under that head
222
+ vh journal prove-inclusion journal.jsonl --seq 1 --out seq1.inclusion.json
223
+
224
+ # 4) the AUDITOR checks it OFFLINE — the proof file is ALL they get (no journal, no key, no network;
225
+ # "OFFLINE" = no network/log, still the PRODUCER package (installs ethers), NOT the standalone verifier/ —
226
+ # see "Independence scope" below)
227
+ vh journal check-proof seq1.inclusion.json # ACCEPTED (exit 0)
228
+
229
+ # 5) keep working: append TWO more observations (the log grows 3 → 5)
230
+ vh journal append ./bundle/release.vhevidence.json --to journal.jsonl
231
+ vh journal append ./bundle/release.vhevidence.json --to journal.jsonl
232
+
233
+ # 6) prove the size-5 log is an APPEND-ONLY extension of the size-3 log the auditor already saw
234
+ vh journal prove-consistency journal.jsonl --from 3 --out 3-to-5.consistency.json
235
+
236
+ # 7) the auditor checks THAT offline too — no history was rewritten between the two heads
237
+ vh journal check-proof 3-to-5.consistency.json # ACCEPTED (exit 0)
238
+ ```
239
+
240
+ (Your `root` values will differ from any printed here: every `entryHash` folds in the self-asserted `ts` of
241
+ that run. Tamper with any byte of a proof artifact — or hand `check-proof` a proof forged against a
242
+ different head — and it prints `REJECTED`, exit 3.)
243
+
244
+ ### Honesty boundary — the head is SELF-ASSERTED (what these proofs do and do NOT mean)
245
+
246
+ - **Inclusion** proves an observation is **committed at a position (`seq`) under a given head** — nothing
247
+ more.
248
+ - **Consistency** proves the log is **append-only between two heads** — the second head's log extends the
249
+ first head's log without rewriting it.
250
+ - The **tree head itself is SELF-ASSERTED** — it is the verifier's (the log holder's) **own** commitment,
251
+ exactly like the journal's `ts`. Every `tree-head` / `prove-*` output carries this note, verbatim:
252
+
253
+ > this tree head is SELF-ASSERTED (the log holder's own commitment to its journal as it stands now); it does NOT by itself prove "existed at / unaltered since date T" until a trust-root signs/timestamps the head (P-3)
254
+
255
+ So a tree head does **not** prove *"existed / unaltered since date T"* on its own — that claim still requires
256
+ the **STRATEGY.md P-3** signing/timestamp trust-root, exactly as for the journal's `ts` below. What the
257
+ tree head changes is **how little** P-3 has to sign: signing the head **is** the P-3 collapse of
258
+ "sign the whole log" down to "sign 32 bytes" — once a trust-root signs/timestamps one head, every inclusion
259
+ and consistency proof under it inherits that anchor. This is **NO new gate and NO relaxed gate**:
260
+ P-3's and P-9's human-owned steps are **unchanged**; the loop still never holds a real key.
261
+
262
+ The whole surface is test-gated by [`test/journal-log.core.test.js`](../test/journal-log.core.test.js),
263
+ [`test/cli.journal-log.test.js`](../test/cli.journal-log.test.js), and the docs-rot guard
264
+ [`test/journal-log.docs.test.js`](../test/journal-log.docs.test.js) on every `npx hardhat test`.
265
+
266
+ ---
267
+
268
+ ## Honesty boundary — the `ts` is SELF-ASSERTED, and is NOT a timestamp
269
+
270
+ The journal proves **ordering + continuity of the verifier's OWN observations** and the **tamper-evidence of
271
+ the record**. It does **not** prove *when* an observation happened: **the `ts` is SELF-ASSERTED — the
272
+ verifier's own wall clock — and is NOT a trusted timestamp.** A caller can supply any `ts`; the journal only
273
+ commits to whatever value it was given, in order.
274
+
275
+ Consequently **the journal NEVER claims "unaltered since date T" on its own.** That claim requires a
276
+ **trust-root** that independently signs and/or timestamps the `ts` — the human-owned step in **STRATEGY.md
277
+ P-3** (a self-managed signing key, an RFC-3161 timestamp authority, or an on-chain anchor). Until that
278
+ trust-root is applied, the honest reading is: *"these observations occurred in this order and the record has
279
+ not been tampered with"*, **not** *"unaltered since date T"*.
280
+
281
+ To upgrade to a stronger claim, sign/timestamp the journal head (or the individual `entryHash`es) with your
282
+ provisioned P-3 trust-root; the journal's ordering guarantee then rides on top of an independent "existed by
283
+ date T" attestation. That provisioning is a **human** step (the loop never holds a real key), documented in
284
+ STRATEGY.md **P-3**.
285
+
286
+ This is the same trust boundary the rest of the toolkit carries: a seal proves **tamper-evidence**, a
287
+ signature proves **who vouched**, and **neither is a trusted timestamp** without P-3. See
288
+ [docs/TRUST-BOUNDARIES.md](./TRUST-BOUNDARIES.md).
289
+
290
+ ---
291
+
292
+ ## Independence scope — journal verification currently needs the producer package
293
+
294
+ A **seal** is independently re-verifiable **offline** with the **zero-dependency standalone verifier**
295
+ ([`verifier/verify-vh.js`](../verifier/verify-vh.js) + its vendored keccak) — no ethers, no hardhat, no
296
+ producer stack; that is the "check it yourself" promise of the [`verifier/`](../verifier/) tree.
297
+
298
+ **A journal does not yet inherit that.** The `vh journal append` / `vh journal verify` commands — **and the
299
+ four transparency-log commands `tree-head` / `prove-inclusion` / `prove-consistency` / `check-proof`** — live
300
+ in the **producer package** (`cli/journal.js`, and `cli/journal-cli.js` →
301
+ [`cli/journal-log.js`](../cli/journal-log.js), which `require`s **ethers**), and `npm i verifyhash` installs
302
+ **ethers** as a runtime dependency. So today a recipient who **re-runs** a journal — **or an auditor who runs
303
+ `check-proof` on a self-contained proof artifact** — is running the **producer's** package, **not** the buyer-
304
+ installable standalone verifier — the standalone tree has **no journal or transparency-log capability yet**. In
305
+ particular `check-proof` is **OFFLINE** only in the sense of *no network and no log*, **not** in the sense of
306
+ *no producer stack*: the proof artifacts a seal's [`verifier/`](../verifier/) bundle would let a counterparty
307
+ check with **zero dependencies** are **not yet** checkable that way. Be honest about this when you hand a
308
+ journal — or a proof — to a counterparty: *the chain / proof is verifiable, but with the producer package, not
309
+ (yet) with the independent offline bundle a seal enjoys.*
310
+
311
+ The chain — and the RFC-6962 tree the proofs ride on — is plain `keccak256` over canonical bytes, both of
312
+ which the standalone tree **already vendors**, so this gap is closeable: a follow-up adding
313
+ `verify-vh journal verify` and a standalone `check-proof` to the [`verifier/`](../verifier/) tree would give
314
+ the journal and its transparency-log proofs the same offline, no-ethers independence as seals. Until then,
315
+ treat "re-runs the journal" and "checks a proof" as "does so with `verifyhash` installed."
316
+
317
+ ---
318
+
319
+ ## Drop it into CI
320
+
321
+ The journal is a **continuous-integrity** gate: each run appends this build's verdict, then verifies the
322
+ whole chain, and fails the build on a broken chain or a recorded drift.
323
+
324
+ - a dependency-free runnable step — [`examples/journal-ci.js`](../examples/journal-ci.js)
325
+ (appends two hash-chained entries, verifies an unbroken chain, exits 0);
326
+ - a shell CI gate — [`verifier/ci/journal.generic.sh`](../verifier/ci/journal.generic.sh)
327
+ (`bash -n` valid; exits 0 on an unbroken chain, non-zero (3) after a tampered artifact appends a REJECT);
328
+ - a GitHub Actions gate — [`verifier/ci/journal.github-actions.yml`](../verifier/ci/journal.github-actions.yml)
329
+ (persists the journal across runs via a **rolling** `actions/cache` key + `restore-keys` fallback, so the
330
+ chain genuinely accumulates — a **static** cache key would freeze the journal at one entry, because GitHub
331
+ caches are immutable and a same-key hit skips the post-job save).
332
+
333
+ **Persist the journal file between runs** (a cache, a committed file, or a stored build artifact) so the
334
+ chain accumulates — a fresh journal each run only ever proves a single observation.
335
+
336
+ The whole surface is test-gated by [`test/journal.example.test.js`](../test/journal.example.test.js) and
337
+ [`test/cli.journal.test.js`](../test/cli.journal.test.js) on every `npx hardhat test`, so it can never rot.
@@ -0,0 +1,179 @@
1
+ # Key lifecycle: publish → pin → verify, and revoking a key (`vh revocation`)
2
+
3
+ Every sealed/signed artifact this family mints (an evidence seal, a signed license, a dataset/parcel
4
+ attestation, an identity card) is trusted because a vendor's signing **key** backs it. A key, though, has a
5
+ lifecycle: it is generated, **published** (so recipients learn its address), used to sign for as long as it
6
+ is good, and — eventually — **rotated, retired, or compromised**. Until now there was no first-class,
7
+ offline-verifiable way for a vendor to say "this key is **revoked** as of D", so every artifact the key ever
8
+ signed kept verifying as ACCEPTED forever, and a recipient had no way to ask "was this key still good when
9
+ **this** exhibit was sealed?". The producer **KEY REVOCATION** (`vh revocation publish` / `vh revocation
10
+ verify`) closes that gap, reusing the shared attestation core verbatim — **no new crypto, no new scheme, no
11
+ new dependency**.
12
+
13
+ This doc walks the whole **publish → pin → verify** key lifecycle and states the load-bearing boundary
14
+ **verbatim**.
15
+
16
+ ## The honest boundary (stated verbatim)
17
+
18
+ The honest boundary, stated verbatim (the same words STRATEGY.md pins):
19
+
20
+ > a revocation is a SIGNED CLAIM by the key-holder (it proves the key-holder SAID "revoked as of D"); it is NOT a trusted wall-clock timestamp without P-3
21
+
22
+ So `--as-of` is **recipient-chosen evidence, not an oracle**: a revocation tells you what the key-holder
23
+ *declared*, and from *when they say*. It is **NOT** a legal opinion. Anchoring "revoked at a wall-clock
24
+ instant T anyone can trust" to a real, independently-trustworthy timestamp still rides the human-owned
25
+ signing/timestamp trust-root (needs-human, **STRATEGY.md P-3**) — exactly the same boundary the identity
26
+ card, the signed seal, and the signed attestation carry.
27
+
28
+ The publish/verify paths LEAD with this caveat verbatim — the standing `REVOCATION_TRUST_NOTE` /
29
+ `SIGNED_REVOCATION_TRUST_NOTE` the core exports, so the prose here can never drift from the code:
30
+
31
+ ```
32
+ This is a verifyhash producer KEY REVOCATION: the holder of `vendorAddress`'s key SIGNED it, declaring that address REVOKED as of `revokedAt` for `reason` (optionally superseded by `supersededBy`). verify RE-DERIVES the signer from these exact bytes and REQUIRES it to equal `vendorAddress` — a key revokes ITSELF; a third party cannot revoke a key it does not control. It proves the KEY-HOLDER's SIGNED CLAIM ONLY: `revokedAt` is the holder's self-asserted instant, NOT a trusted TIMESTAMP (it rides the human-owned timestamp trust-root, STRATEGY.md P-3), and this is NOT a legal opinion.
33
+ ```
34
+
35
+ ## The lifecycle in three moves: publish → pin → verify
36
+
37
+ 1. **Publish.** A vendor generates a keypair OUTSIDE the loop and publishes a signed
38
+ [producer identity card](IDENTITY.md) (`vh identity publish`) binding their **`vendorAddress`** to the
39
+ bounded claim set they attest. They sign their evidence/licenses/attestations with **that same key**.
40
+ 2. **Pin.** A recipient (or a cold prospect) does the address-to-vendor trust step **ONCE** — `vh identity
41
+ verify vendor.vhidentity.json --signer <addr-you-were-given>` → ACCEPTED — and then reuses that pinned
42
+ `vendorAddress` across every later signed handoff (`vh evidence verify-signed <p> --signer <addr>`, etc.)
43
+ with **no new out-of-band step**.
44
+ 3. **Verify (and re-check the key is still good).** When that key is compromised, rotated, retired, or
45
+ superseded, the vendor publishes a signed **revocation** of that same `vendorAddress`. Recipients pin it
46
+ next to the identity card and pass it to any signed-verify command via **`--revocations <f>`**
47
+ `[--as-of <ISO>]`. An exhibit signed under a key that was **revoked-before-as-of** then downgrades from
48
+ ACCEPTED to **REVOKED**; an exhibit signed while the key was still good keeps its ACCEPTED verdict (with
49
+ an informational "this key is revoked *now*" note) — the precise forensic value.
50
+
51
+ The whole point: **pin once, then keep believing the same vendorAddress** across every handoff — and a
52
+ revocation is how that pinned key is honestly *retired* when its day comes.
53
+
54
+ ## Commands
55
+
56
+ ```
57
+ vh revocation publish --address <0xaddr> --reason <reason> (--key-env <VAR> | --key-file <path>) [--superseded-by <0xaddr>] [--revoked-at <ISO>] [--out <p>] [--json]
58
+ vh revocation verify <revocation> [--signer <0xaddr>] [--json]
59
+ ```
60
+
61
+ ### `vh revocation publish` — mint the revocation
62
+
63
+ `publish` MINTS a signed `*.vhrevocation.json` revocation marking `--address` **REVOKED** as of
64
+ `--revoked-at` (default now) for `--reason`, OPTIONALLY naming a `--superseded-by` successor key. It signs
65
+ with a **HUMAN-provisioned key** (EXACTLY ONE of `--key-env` / `--key-file`, **read-used-discarded** via the
66
+ shared `loadSigningWallet` — the loop **NEVER** generates, persists, or logs a key, and the key never appears
67
+ in any output).
68
+
69
+ - **The load-bearing self-control invariant — a key revokes ITSELF.** `publish` mints **ONLY** when the provisioned key's address **EQUALS** `--address`.
70
+ A key that does **NOT** control `--address` **hard-errors (exit 2) BEFORE writing anything** (never a
71
+ mis-minted statement) — a **third party cannot revoke a key it does not control**, otherwise anyone could
72
+ grief a vendor by "revoking" their key.
73
+ - **`--reason`** is one of the closed set `["compromised", "retired", "rotated", "superseded"]` (an out-of-set
74
+ reason is a usage error): a small, fixed vocabulary a recipient can reason about — `compromised`/`retired`
75
+ make the key's past signatures suspect, `rotated`/`superseded` simply move on to a new key.
76
+ - **`--superseded-by`** (optional) names the successor key the vendor moved to; absent, the revocation
77
+ supersedes the key with nothing.
78
+ - **Filesystem hygiene.** Default **prints the revocation + writes NOTHING**; `--out <p>` writes ONLY to the
79
+ caller-chosen path — **never silently to cwd**.
80
+ - The output **LEADS with the trust line**; `--json` carries the PUBLIC revocation summary (vendorAddress,
81
+ signer, reason, revokedAt, supersededBy) + the artifact — and **never the key**.
82
+ - **Exit:** **0** ok / **2** usage (missing/invalid field, key-source error, key does not control
83
+ `--address`) / **1** IO (`--out` write).
84
+
85
+ ### `vh revocation verify` — check + pin the revocation
86
+
87
+ `verify <revocation>` is the **OFFLINE / key-free / network-free** read path. It RECOVERS the signer from the
88
+ embedded canonical revocation bytes + signature and:
89
+
90
+ 1. confirms the signature **backs the claimed signer** (Check 1, always);
91
+ 2. confirms the recovered signer **IS the revocation's own `vendorAddress`** (the load-bearing self-control
92
+ check, always — a key revokes ITSELF);
93
+ 3. OPTIONALLY pins it to an expected `--signer` (run only when given).
94
+
95
+ It prints the **reason / revokedAt / supersededBy** + per-check PASS/FAIL, and **LEADS with the trust line**.
96
+ A **forged / tampered / wrong-key** revocation (one whose signature does not recover to its own
97
+ `vendorAddress`), or a wrong `--signer`, is a clean **REJECTED** — **never a silent pass**.
98
+
99
+ - **Exit:** **0** ACCEPTED / **3** REJECTED / **2** usage / **1** IO.
100
+
101
+ ## Worked example: publish a rotation → pin → verify
102
+
103
+ ```
104
+ # The vendor rotates: they mint a signed revocation of their OLD key, naming the NEW one (offline, key read-used-discarded):
105
+ $ vh revocation publish --address 0x<old-vendor> --reason rotated \
106
+ --superseded-by 0x<new-vendor> --key-env VENDOR_OLD_KEY --out ./old.vhrevocation.json
107
+ This is a verifyhash producer KEY REVOCATION: … # caveat first
108
+ published a signed key revocation for 0x<old-vendor> (signed by 0x<old-vendor>)
109
+ reason: rotated
110
+ revokedAt: 2026-06-26T00:00:00.000Z
111
+ supersededBy: 0x<new-vendor>
112
+ written: /abs/path/old.vhrevocation.json # exit 0
113
+
114
+ # A recipient verifies the revocation itself — recover (always) + self-control (always) + pin (--signer):
115
+ $ vh revocation verify ./old.vhrevocation.json --signer 0x<old-vendor>
116
+ TRUST: This is a SIGNED verifyhash key-revocation container: … # caveat first
117
+ revocation: ACCEPTED
118
+ [PASS] signature recovers to the claimed signer
119
+ [PASS] the recovered signer IS the revocation's vendorAddress (a key revokes ITSELF; …)
120
+ [PASS] recovered signer matches the expected signer (0x<old-vendor>)
121
+ ACCEPTED: every requested check passed — the key-holder SIGNED this revocation of the address it controls. # exit 0
122
+
123
+ # A THIRD-PARTY "revocation" (signed by some OTHER key) is a clean REJECTED — it can never grief a vendor:
124
+ $ vh revocation verify ./forged.vhrevocation.json
125
+
126
+ REJECTED: failed check(s): vendorAddressMatchesSigner. # exit 3
127
+ ```
128
+
129
+ ## Using a revocation as a recipient: `--revocations` on the verify commands
130
+
131
+ Running `vh revocation verify` **on its own** proves the revocation is **genuine** (signed by the key it
132
+ claims to revoke) — but proving a revocation is genuine is not the same as *acting* on it. To actually
133
+ downgrade an exhibit, a recipient passes the revocation to a **signed-verify** command:
134
+
135
+ ```
136
+ # An evidence exhibit signed under a key that was revoked-BEFORE the as-of instant downgrades to REVOKED:
137
+ $ vh evidence verify-signed ./bundle/b.vhevidence.json --signer 0x<old-vendor> --dir ./bundle \
138
+ --revocations ./old.vhrevocation.json --as-of 2026-07-01T00:00:00.000Z
139
+
140
+ revocation check (as of 2026-07-01T00:00:00.000Z):
141
+ [REVOKED] the signing key (0x<old-vendor>) was REVOKED as of 2026-06-26T00:00:00.000Z (reason: rotated), superseded by 0x<new-vendor> — at or before the as-of instant. This artifact is NOT trustworthy as of 2026-07-01T00:00:00.000Z.
142
+ REJECTED: … # exit 3
143
+ ```
144
+
145
+ The same `--revocations <f>` / `--as-of <ISO>` flags work on `vh evidence verify-signed`,
146
+ `vh dataset verify-attest`, `vh parcel verify-attest`, and `vh identity verify`. The
147
+ [`cli/core/trust-asof.js`](../cli/core/trust-asof.js) recipient core enforces the **strongest possible
148
+ non-loosening invariant**: with **NO `--revocations` supplied, every existing verify command behaves
149
+ byte-for-byte as today** — a revocation can ONLY turn an ACCEPTED into a REVOKED, never the reverse, and a
150
+ **forged / tampered / third-party** revocation is **IGNORED with a warning**, never trusted to downgrade.
151
+
152
+ ## The independent verifier (`verify-vh`) is revocation-aware too
153
+
154
+ The `--revocations <file-or-dir>` / `--as-of <ISO>` downgrade above is **also** in the standalone independent
155
+ verifier — [`verifier/`](../verifier/) (`verify-vh.js` + `dist/verify-vh-standalone.js`), the deliverable that
156
+ lets a counterparty recompute **without installing the producer's stack** (T-51.4). `verify-vh` consults the
157
+ producer's signed revocations with an **offline EIP-191 recovery** of each revocation (its own pure-JS
158
+ secp256k1 — **no `ethers`**) plus the **same non-loosening as-of comparison** the producer stack uses, so on
159
+ identical inputs `verify-vh --revocations <f> --as-of <T>` reaches the **same REVOKED verdict and exit code
160
+ (3)** the producer's `vh ... verify-signed --revocations <f> --as-of <T>` reaches. A revocation dated *after*
161
+ `--as-of` stays ACCEPTED with a later-revoked note; a **forged / tampered / third-party** revocation is
162
+ **IGNORED with a warning**, never trusted to downgrade (a key revokes itself); and with **NO `--revocations`,
163
+ `verify-vh` is byte-for-byte as before**. A directory is read as a flat pool of revocation files; a single
164
+ file may be one revocation or a JSON array. This parity is stated the same way in
165
+ [`docs/INDEPENDENT-VERIFICATION.md`](INDEPENDENT-VERIFICATION.md) §3 and
166
+ [`verifier/README.md`](../verifier/README.md) §4, and proven in
167
+ [`test/verifier.revocation.test.js`](../test/verifier.revocation.test.js).
168
+
169
+ ## See also
170
+
171
+ - [`docs/IDENTITY.md`](IDENTITY.md) — the producer identity card (the **publish** + **pin** moves above).
172
+ - [`docs/EVIDENCE.md`](EVIDENCE.md) — the recipient `verify-signed … --revocations` step in context.
173
+ - [`docs/TRUST-BOUNDARIES.md`](TRUST-BOUNDARIES.md) — what each artifact proves and does not.
174
+ - **STRATEGY.md P-7 step 1** — where the evidence-product vendor key is generated, pinned, and (now)
175
+ honestly retired via a revocation.
176
+
177
+
178
+ ---
179
+ <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>
@@ -0,0 +1,46 @@
1
+ # Licensing — decision record
2
+
3
+ **Status: DECIDED (Apache-2.0, repo-wide) — 2026-06-26. The paid-surface source-available split is DEFERRED (needs counsel).**
4
+
5
+ ## What is in force now
6
+
7
+ The entire repository is licensed under **Apache License 2.0** (`LICENSE` + `NOTICE`).
8
+ `package.json` (root and `verifier/`) declares `"license": "Apache-2.0"`, and the Solidity
9
+ files carry `SPDX-License-Identifier: Apache-2.0`.
10
+
11
+ ### Why Apache-2.0 (not bare MIT, not proprietary)
12
+
13
+ - The prior state was `"license": "MIT"` in `package.json` with **no LICENSE file at all** — a gap.
14
+ - Apache-2.0 is permissive like MIT but additionally grants an explicit **patent license** and
15
+ reserves **trademark/brand** rights — both valuable for a crypto/provenance product and a named
16
+ brand ("verifyhash").
17
+ - It **keeps the trust pitch intact**: the standalone verifier (`verifier/`, published on
18
+ verifyhash.com) is *meant* to be freely downloaded, audited, and **reproduced from source**.
19
+ A permissive license is what makes "don't trust us — rebuild it yourself" legally true.
20
+ - Fully-proprietary / all-rights-reserved was rejected: it would contradict the already-shipped
21
+ permissive metadata and the public invitation to redistribute the verifier, and gut the trust story.
22
+
23
+ ### The moat is not source secrecy
24
+
25
+ Even fully open, the business stays defensible. The real moat is:
26
+ 1. the **vendor signing key** — only the holder can mint valid seals / entitlements;
27
+ 2. the **brand** ("verifyhash") — reserved by the trademark clause above;
28
+ 3. the **customer relationship** and the **hosted/paid entitlement** service.
29
+ A code license gives none of those away.
30
+
31
+ ## Deferred — needs human + counsel (do NOT auto-apply)
32
+
33
+ A **source-available split** remains a future option: keep the verifier + spec/conformance +
34
+ on-chain contracts + free CLI verbs under Apache-2.0, and move the **paid** producer/sealing path,
35
+ `trustledger/*`, and the evidence paid cores under a source-available license (e.g. **BSL 1.1** with
36
+ a chosen Change Date / Change License / Additional Use Grant, or **PolyForm Noncommercial** for a
37
+ permanent non-commercial line) to block commercial resale while staying auditable.
38
+
39
+ This requires business/legal decisions (license choice, BSL parameters, confirming no inbound
40
+ third-party code blocks relicensing) and is left as a human/counsel decision. Nothing about the split
41
+ has been applied.
42
+
43
+ ## Note on the product's "license" feature
44
+
45
+ The `*.vhlicense.json` entitlement (`cli/core/license.js`, `trustledger/license.js`) is a signed,
46
+ offline-verifiable **paid-tier access credential** — unrelated to this copyright license.