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,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.
|