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,495 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// cli/revocation.js — the THIN I/O shell for `vh revocation publish` + `vh revocation verify` (EPIC-51 /
|
|
4
|
+
// T-51.3). ALL crypto/validation lives in the PURE core (cli/core/revocation.js, T-51.1); this layer ONLY
|
|
5
|
+
// parses argv, reads/writes files, and renders. There is NO new crypto, NO new scheme, NO new dependency
|
|
6
|
+
// here — `cmdRevocation` is the publish/verify CLI surface over `buildRevocation` / `verifyRevocation`.
|
|
7
|
+
//
|
|
8
|
+
// WHAT THIS SHIPS
|
|
9
|
+
// `vh revocation publish --address <a> --reason <r> (--key-env <VAR>|--key-file <p>) [--superseded-by <a>]
|
|
10
|
+
// [--revoked-at <ISO>] [--out <p>] [--json]`
|
|
11
|
+
// MINTS a signed producer KEY REVOCATION — a vendor SIGNS, with the SAME key it signs its evidence/
|
|
12
|
+
// licenses/cards with, a self-describing container marking that key's OWN `vendorAddress` revoked as of
|
|
13
|
+
// `revokedAt` for a bounded `reason` (and OPTIONALLY naming a `supersededBy` successor). The mint is
|
|
14
|
+
// REFUSED (a clean usage error, BEFORE any --out write) when the provisioned key does NOT control
|
|
15
|
+
// --address — a key revokes ITSELF; a third party cannot revoke a key it does not control. Default
|
|
16
|
+
// prints the revocation + writes NOTHING; --out writes a caller-chosen path (never cwd).
|
|
17
|
+
// `vh revocation verify <revocation> [--signer <0xaddr>] [--json]`
|
|
18
|
+
// OFFLINE / key-free / network-free: RECOVERS the signer from a signed revocation, confirms the
|
|
19
|
+
// signature backs the claimed signer AND that the recovered signer IS the revocation's own
|
|
20
|
+
// vendorAddress (the load-bearing SELF-CONTROL check), OPTIONALLY pins --signer, and prints the
|
|
21
|
+
// reason/revokedAt/supersededBy + per-check PASS/FAIL. A forged/tampered/wrong-key revocation, or a
|
|
22
|
+
// wrong --signer, is a clean REJECTED — never a silent pass.
|
|
23
|
+
//
|
|
24
|
+
// THE LOAD-BEARING POSTURE — a SIGNED CLAIM, NOT a trusted timestamp without P-3.
|
|
25
|
+
// A revocation proves the KEY-HOLDER SAID "revoked as of D"; the `revokedAt` instant is the holder's
|
|
26
|
+
// self-asserted instant, NOT a trusted wall-clock timestamp (it rides the human-owned timestamp
|
|
27
|
+
// trust-root, STRATEGY.md P-3). The publish/verify paths LEAD with that caveat verbatim (the standing
|
|
28
|
+
// REVOCATION/SIGNED_REVOCATION trust note the core exports), so the human + JSON boundary can never drift.
|
|
29
|
+
|
|
30
|
+
const fs = require("fs");
|
|
31
|
+
const path = require("path");
|
|
32
|
+
const coreAttestation = require("./core/attestation");
|
|
33
|
+
const coreRevocation = require("./core/revocation");
|
|
34
|
+
const { isAddress } = require("ethers");
|
|
35
|
+
|
|
36
|
+
// Exit contract shared with the rest of the family: 0 ok/ACCEPTED / 1 IO / 2 usage / 3 gate-fail (verify
|
|
37
|
+
// REJECTED). Mirrors cli/identity.js's EXIT so every gate reads the same.
|
|
38
|
+
const EXIT = Object.freeze({ OK: 0, IO: 1, USAGE: 2, FAIL: 3 });
|
|
39
|
+
|
|
40
|
+
// Real "now" as a canonical ISO-8601 UTC instant — the publish default clock, isolated + injectable
|
|
41
|
+
// (io.nowISO) so the command stays deterministic under test. revokedAt defaults to this when omitted.
|
|
42
|
+
function nowISO() {
|
|
43
|
+
return new Date().toISOString();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Parse `revocation publish` argv. EXACTLY-ONE-of key sources is enforced downstream by loadSigningWallet
|
|
47
|
+
// (so neither/both error key-free); the parser only collects flags. A flag without its value, or an unknown
|
|
48
|
+
// flag, is a usage error (e.usage=true) — a typo never silently changes the revocation. `publish` takes NO
|
|
49
|
+
// positional arguments (the address rides --address) so a stray positional is a clean usage error.
|
|
50
|
+
function parsePublishArgs(argv) {
|
|
51
|
+
const opts = {
|
|
52
|
+
address: undefined,
|
|
53
|
+
reason: undefined,
|
|
54
|
+
supersededBy: undefined,
|
|
55
|
+
revokedAt: undefined,
|
|
56
|
+
keyEnv: undefined,
|
|
57
|
+
keyFile: undefined,
|
|
58
|
+
out: undefined,
|
|
59
|
+
json: false,
|
|
60
|
+
};
|
|
61
|
+
for (let i = 0; i < argv.length; i++) {
|
|
62
|
+
const a = argv[i];
|
|
63
|
+
const need = (flag) => {
|
|
64
|
+
const v = argv[++i];
|
|
65
|
+
if (v === undefined) {
|
|
66
|
+
const e = new Error(`${flag} requires a value`);
|
|
67
|
+
e.usage = true;
|
|
68
|
+
throw e;
|
|
69
|
+
}
|
|
70
|
+
return v;
|
|
71
|
+
};
|
|
72
|
+
switch (a) {
|
|
73
|
+
case "--address":
|
|
74
|
+
opts.address = need("--address");
|
|
75
|
+
break;
|
|
76
|
+
case "--reason":
|
|
77
|
+
opts.reason = need("--reason");
|
|
78
|
+
break;
|
|
79
|
+
case "--superseded-by":
|
|
80
|
+
opts.supersededBy = need("--superseded-by");
|
|
81
|
+
break;
|
|
82
|
+
case "--revoked-at":
|
|
83
|
+
opts.revokedAt = need("--revoked-at");
|
|
84
|
+
break;
|
|
85
|
+
case "--key-env":
|
|
86
|
+
opts.keyEnv = need("--key-env");
|
|
87
|
+
break;
|
|
88
|
+
case "--key-file":
|
|
89
|
+
opts.keyFile = need("--key-file");
|
|
90
|
+
break;
|
|
91
|
+
case "--out":
|
|
92
|
+
opts.out = need("--out");
|
|
93
|
+
break;
|
|
94
|
+
case "--json":
|
|
95
|
+
opts.json = true;
|
|
96
|
+
break;
|
|
97
|
+
default: {
|
|
98
|
+
const e = new Error(`unknown flag: ${a} (revocation publish takes no positional arguments)`);
|
|
99
|
+
e.usage = true;
|
|
100
|
+
throw e;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return opts;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* `vh revocation publish` — MINT a signed key revocation. PURE core + the only I/O being the OPTIONAL --out
|
|
109
|
+
* write (the signing is offline/key-free in the sense that the loop holds no key; the human's key lives ONLY
|
|
110
|
+
* inside the in-process Wallet loadSigningWallet builds and is discarded).
|
|
111
|
+
*
|
|
112
|
+
* The mint is REFUSED (a clean usage error, BEFORE any write) when the provisioned key's address != --address
|
|
113
|
+
* — a key revokes ITSELF; a third party cannot revoke a key it does not control. The output LEADS with the
|
|
114
|
+
* trust line. Default prints the revocation + writes NOTHING; --out writes a caller-chosen path (never cwd).
|
|
115
|
+
*
|
|
116
|
+
* Exit: 0 ok / 2 usage (missing/invalid field, key-source error, key does not control --address) / 1 IO.
|
|
117
|
+
*/
|
|
118
|
+
async function runRevocationPublish(opts, io = {}) {
|
|
119
|
+
const write = io.write || ((s) => process.stdout.write(s));
|
|
120
|
+
const writeErr = io.writeErr || ((s) => process.stderr.write(s));
|
|
121
|
+
|
|
122
|
+
// Required fields up front (a missing one is a clean usage error, never a confusing core throw).
|
|
123
|
+
if (opts.address == null) {
|
|
124
|
+
writeErr(
|
|
125
|
+
"error: `vh revocation publish` requires --address <0xaddr> (the vendor address the key revokes — itself)\n"
|
|
126
|
+
);
|
|
127
|
+
return EXIT.USAGE;
|
|
128
|
+
}
|
|
129
|
+
if (opts.reason == null) {
|
|
130
|
+
writeErr(
|
|
131
|
+
`error: \`vh revocation publish\` requires --reason <reason> (one of ${JSON.stringify(coreRevocation.REVOCATION_REASON_SET)})\n`
|
|
132
|
+
);
|
|
133
|
+
return EXIT.USAGE;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Validate the --address SHAPE up front so a malformed address is a usage error (2), never a runtime throw
|
|
137
|
+
// mid-mint. buildRevocationPayload also normalizes/validates, but failing fast here gives the clean exit-2
|
|
138
|
+
// the contract promises. (isAddress accepts checksummed/lowercase 0x-addresses.)
|
|
139
|
+
if (!isAddress(opts.address)) {
|
|
140
|
+
writeErr(`error: invalid --address: ${opts.address} (expected a 20-byte 0x-hex address)\n`);
|
|
141
|
+
return EXIT.USAGE;
|
|
142
|
+
}
|
|
143
|
+
// Same up-front shape check for the OPTIONAL --superseded-by successor (when given).
|
|
144
|
+
if (opts.supersededBy !== undefined && !isAddress(opts.supersededBy)) {
|
|
145
|
+
writeErr(
|
|
146
|
+
`error: invalid --superseded-by: ${opts.supersededBy} (expected a 20-byte 0x-hex successor address)\n`
|
|
147
|
+
);
|
|
148
|
+
return EXIT.USAGE;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Resolve the HUMAN-supplied key into an in-process Wallet FIRST — neither/both sources, a missing env var,
|
|
152
|
+
// an unreadable file, or a malformed/zero key HARD-ERRORS here with a KEY-FREE message (the SAME core +
|
|
153
|
+
// posture as `vh evidence seal --sign` / `vh identity publish`). The loop NEVER holds/generates/persists/
|
|
154
|
+
// logs a key.
|
|
155
|
+
let wallet;
|
|
156
|
+
try {
|
|
157
|
+
({ wallet } = coreAttestation.loadSigningWallet({ keyEnv: opts.keyEnv, keyFile: opts.keyFile }));
|
|
158
|
+
} catch (e) {
|
|
159
|
+
writeErr(`error: ${e.message}\n`);
|
|
160
|
+
return EXIT.USAGE;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// revokedAt defaults to the injectable clock (a real ISO instant at runtime; a pinned one in tests).
|
|
164
|
+
const revokedAt = opts.revokedAt != null ? opts.revokedAt : (io.nowISO || nowISO)();
|
|
165
|
+
|
|
166
|
+
// Build + sign + enforce the self-control invariant in the PURE core. A malformed field (out-of-set reason,
|
|
167
|
+
// non-canonical date, malformed successor) OR the key NOT controlling --address throws RevocationError — a
|
|
168
|
+
// usage error (2), BEFORE any --out write. The message never includes the key.
|
|
169
|
+
let container;
|
|
170
|
+
try {
|
|
171
|
+
container = await coreRevocation.buildRevocation(
|
|
172
|
+
{
|
|
173
|
+
vendorAddress: opts.address,
|
|
174
|
+
reason: opts.reason,
|
|
175
|
+
revokedAt,
|
|
176
|
+
...(opts.supersededBy !== undefined ? { supersededBy: opts.supersededBy } : {}),
|
|
177
|
+
},
|
|
178
|
+
wallet
|
|
179
|
+
);
|
|
180
|
+
} catch (e) {
|
|
181
|
+
writeErr(`error: ${e.message}\n`);
|
|
182
|
+
return EXIT.USAGE;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const canonical = coreRevocation.serializeSignedRevocation(container);
|
|
186
|
+
const payload = JSON.parse(container.attestation);
|
|
187
|
+
// The PUBLIC vendor address — recovered from the signature, never the key. By the self-control invariant it
|
|
188
|
+
// equals payload.vendorAddress; we recover it to PROVE that (and to print "signed by" from the signature).
|
|
189
|
+
const signedBy = coreAttestation.recoverSigner(container);
|
|
190
|
+
|
|
191
|
+
// Write to --out (caller-chosen path; NEVER cwd) or print to stdout (writes nothing).
|
|
192
|
+
let outAbs = null;
|
|
193
|
+
if (opts.out) {
|
|
194
|
+
outAbs = path.resolve(opts.out);
|
|
195
|
+
try {
|
|
196
|
+
fs.writeFileSync(outAbs, canonical);
|
|
197
|
+
} catch (e) {
|
|
198
|
+
writeErr(`error: cannot write --out file ${opts.out}: ${e.message}\n`);
|
|
199
|
+
return EXIT.IO;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (opts.json) {
|
|
204
|
+
// ONLY public fields: the vendor ADDRESS (recovered), the revocation summary, the path — NEVER the key.
|
|
205
|
+
// With no --out the canonical bytes ride in `container` so --json never drops the artifact (family parity).
|
|
206
|
+
write(
|
|
207
|
+
JSON.stringify(
|
|
208
|
+
{
|
|
209
|
+
published: true,
|
|
210
|
+
note: coreRevocation.REVOCATION_TRUST_NOTE,
|
|
211
|
+
kind: coreRevocation.SIGNED_REVOCATION_KIND,
|
|
212
|
+
vendorAddress: payload.vendorAddress,
|
|
213
|
+
signer: signedBy,
|
|
214
|
+
reason: payload.reason,
|
|
215
|
+
revokedAt: payload.revokedAt,
|
|
216
|
+
supersededBy: Object.prototype.hasOwnProperty.call(payload, "supersededBy")
|
|
217
|
+
? payload.supersededBy
|
|
218
|
+
: null,
|
|
219
|
+
out: outAbs,
|
|
220
|
+
container: outAbs ? null : canonical,
|
|
221
|
+
},
|
|
222
|
+
null,
|
|
223
|
+
2
|
|
224
|
+
) + "\n"
|
|
225
|
+
);
|
|
226
|
+
} else {
|
|
227
|
+
write(coreRevocation.REVOCATION_TRUST_NOTE + "\n\n");
|
|
228
|
+
write(`published a signed key revocation for ${payload.vendorAddress} (signed by ${signedBy})\n`);
|
|
229
|
+
write(` reason: ${payload.reason}\n`);
|
|
230
|
+
write(` revokedAt: ${payload.revokedAt}\n`);
|
|
231
|
+
if (Object.prototype.hasOwnProperty.call(payload, "supersededBy")) {
|
|
232
|
+
write(` supersededBy: ${payload.supersededBy}\n`);
|
|
233
|
+
}
|
|
234
|
+
if (outAbs) {
|
|
235
|
+
write(` written: ${outAbs}\n`);
|
|
236
|
+
} else {
|
|
237
|
+
// Default: print the revocation bytes so a publisher can eyeball/redirect them — still writes nothing.
|
|
238
|
+
write(canonical);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return EXIT.OK;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Parse `revocation verify` argv. Takes exactly one positional <revocation> + OPTIONAL --signer/--json. A
|
|
245
|
+
// flag without its value, an unknown flag, or a second positional is a clean usage error.
|
|
246
|
+
function parseVerifyArgs(argv) {
|
|
247
|
+
const opts = { revocation: undefined, signer: undefined, json: false, _positionals: [] };
|
|
248
|
+
for (let i = 0; i < argv.length; i++) {
|
|
249
|
+
const a = argv[i];
|
|
250
|
+
const need = (flag) => {
|
|
251
|
+
const v = argv[++i];
|
|
252
|
+
if (v === undefined) {
|
|
253
|
+
const e = new Error(`${flag} requires a value`);
|
|
254
|
+
e.usage = true;
|
|
255
|
+
throw e;
|
|
256
|
+
}
|
|
257
|
+
return v;
|
|
258
|
+
};
|
|
259
|
+
switch (a) {
|
|
260
|
+
case "--signer":
|
|
261
|
+
opts.signer = need("--signer");
|
|
262
|
+
break;
|
|
263
|
+
case "--json":
|
|
264
|
+
opts.json = true;
|
|
265
|
+
break;
|
|
266
|
+
default:
|
|
267
|
+
if (a && a.startsWith("--")) {
|
|
268
|
+
const e = new Error(`unknown flag: ${a}`);
|
|
269
|
+
e.usage = true;
|
|
270
|
+
throw e;
|
|
271
|
+
}
|
|
272
|
+
opts._positionals.push(a);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (opts._positionals.length > 1) {
|
|
276
|
+
const e = new Error(
|
|
277
|
+
`unexpected extra argument: ${opts._positionals[1]} (revocation verify takes exactly one <revocation>)`
|
|
278
|
+
);
|
|
279
|
+
e.usage = true;
|
|
280
|
+
throw e;
|
|
281
|
+
}
|
|
282
|
+
opts.revocation = opts._positionals[0];
|
|
283
|
+
return opts;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// The standing trust line the verify path LEADS with — reuses the SIGNED-revocation note verbatim (so the
|
|
287
|
+
// human + JSON caveats can NEVER drift). It is the load-bearing honesty of the read: an ACCEPT proves the
|
|
288
|
+
// KEY-HOLDER's SIGNED CLAIM, NOT a trusted timestamp (P-3), NOT a legal opinion.
|
|
289
|
+
const VERIFY_TRUST_NOTE = coreRevocation.SIGNED_REVOCATION_TRUST_NOTE;
|
|
290
|
+
|
|
291
|
+
// Render the human verify report. PURE. LEADS with the trust line, prints the verdict, the recovered/claimed/
|
|
292
|
+
// vendor address, the per-check PASS/FAIL (Check 1 + the SELF-CONTROL vendorAddress check ALWAYS; the
|
|
293
|
+
// --signer pin only when requested), then the revocation's reason/revokedAt/supersededBy. A REJECTED verdict
|
|
294
|
+
// NAMES the failing check(s).
|
|
295
|
+
function renderVerify(r, ctx) {
|
|
296
|
+
const L = [];
|
|
297
|
+
// TRUST FIRST.
|
|
298
|
+
L.push("TRUST: " + VERIFY_TRUST_NOTE);
|
|
299
|
+
L.push("");
|
|
300
|
+
L.push(`# vh revocation verify — ${ctx.revocation}`);
|
|
301
|
+
L.push(`revocation: ${r.verdict}`);
|
|
302
|
+
L.push(`scheme: ${r.scheme}`);
|
|
303
|
+
L.push(`vendorAddress: ${r.vendorAddress} (the address this key revokes — itself)`);
|
|
304
|
+
L.push(`recovered signer: ${r.recoveredSigner} (from the embedded canonical revocation bytes + signature)`);
|
|
305
|
+
L.push(`claimed signer: ${r.claimedSigner} (the container's \`signer\` field)`);
|
|
306
|
+
L.push(`reason: ${r.reason}`);
|
|
307
|
+
L.push(`revokedAt: ${r.revokedAt} (the holder's self-asserted instant — NOT a trusted timestamp without P-3)`);
|
|
308
|
+
if (r.supersededBy) {
|
|
309
|
+
L.push(`supersededBy: ${r.supersededBy}`);
|
|
310
|
+
}
|
|
311
|
+
// Check 1 (ALWAYS): the signature recovers to the claimed signer.
|
|
312
|
+
L.push(` [${r.checks.signatureMatchesSigner ? "PASS" : "FAIL"}] signature recovers to the claimed signer`);
|
|
313
|
+
// The load-bearing SELF-CONTROL check (ALWAYS): the recovered signer IS the revocation's own vendorAddress.
|
|
314
|
+
L.push(
|
|
315
|
+
` [${r.checks.vendorAddressMatchesSigner ? "PASS" : "FAIL"}] the recovered signer IS the revocation's ` +
|
|
316
|
+
"vendorAddress (a key revokes ITSELF; a third party cannot revoke a key it does not control)"
|
|
317
|
+
);
|
|
318
|
+
// Check 3 (only under --signer): the recovered signer equals the expected, out-of-band signer.
|
|
319
|
+
if (r.checks.signerMatchesExpected === null) {
|
|
320
|
+
L.push(" [skip] expected-signer pin: not requested (pass --signer <0xaddr> to pin the signer)");
|
|
321
|
+
} else {
|
|
322
|
+
L.push(
|
|
323
|
+
` [${r.checks.signerMatchesExpected ? "PASS" : "FAIL"}] recovered signer matches the expected ` +
|
|
324
|
+
`signer (${r.expectedSigner})`
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
if (r.accepted) {
|
|
328
|
+
L.push(
|
|
329
|
+
"ACCEPTED: every requested check passed — the key-holder SIGNED this revocation of the address it controls."
|
|
330
|
+
);
|
|
331
|
+
} else {
|
|
332
|
+
L.push(`REJECTED: failed check(s): ${r.failedChecks.join(", ")}.`);
|
|
333
|
+
if (r.failedChecks.includes("signatureMatchesSigner")) {
|
|
334
|
+
L.push(
|
|
335
|
+
" forged/tampered: the signature does NOT recover to the claimed `signer` — this revocation is UNBACKED."
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
if (r.failedChecks.includes("vendorAddressMatchesSigner")) {
|
|
339
|
+
L.push(
|
|
340
|
+
" third-party: the recovered signer is NOT the revocation's vendorAddress — a key revokes ITSELF;"
|
|
341
|
+
);
|
|
342
|
+
L.push(" this revocation was NOT signed by the key it claims to revoke, so it never downgrades trust.");
|
|
343
|
+
}
|
|
344
|
+
if (r.failedChecks.includes("signerMatchesExpected")) {
|
|
345
|
+
L.push(
|
|
346
|
+
" pin-mismatch: the signature is genuine but the signer is NOT the address you pinned with --signer."
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
L.push("");
|
|
351
|
+
return L.join("\n");
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* `vh revocation verify <revocation> [--signer <0xaddr>] [--json]` — OFFLINE / key-free / network-free.
|
|
356
|
+
* RECOVERS the signer from a signed revocation and confirms (1) the signature backs the claimed signer,
|
|
357
|
+
* (2) the recovered signer IS the revocation's own vendorAddress (the load-bearing self-control check), and
|
|
358
|
+
* OPTIONALLY (3) pins it to an expected --signer. LEADS with the trust line; prints reason/revokedAt/
|
|
359
|
+
* supersededBy + per-check PASS/FAIL. A forged/tampered/wrong-key revocation, or a wrong --signer, is a
|
|
360
|
+
* clean REJECTED — NEVER a silent pass. Writes NOTHING. Exit: 0 ACCEPTED / 3 REJECTED / 2 usage / 1 IO.
|
|
361
|
+
*/
|
|
362
|
+
function runRevocationVerify(opts, io = {}) {
|
|
363
|
+
const write = io.write || ((s) => process.stdout.write(s));
|
|
364
|
+
const writeErr = io.writeErr || ((s) => process.stderr.write(s));
|
|
365
|
+
|
|
366
|
+
if (!opts.revocation) {
|
|
367
|
+
writeErr("error: `vh revocation verify` requires a <revocation> (a signed key-revocation file path)\n");
|
|
368
|
+
return EXIT.USAGE;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Validate the --signer SHAPE up front (when given) so a malformed pin is a usage error (2), never a
|
|
372
|
+
// runtime throw inside verifyRevocation (which normalizes via getAddress and would throw). OFFLINE.
|
|
373
|
+
if (opts.signer !== undefined && opts.signer !== null) {
|
|
374
|
+
if (!isAddress(opts.signer)) {
|
|
375
|
+
writeErr(`error: invalid --signer address: ${opts.signer} (expected a 20-byte 0x-hex address)\n`);
|
|
376
|
+
return EXIT.USAGE;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Read + STRICT-validate the container BEFORE any recovery — a malformed/edited/foreign container (or a
|
|
381
|
+
// non-revocation file) hard-errors (exit 1), never half-accepted. A forged signature is NOT a parse error:
|
|
382
|
+
// readRevocation proves the bytes are canonical; the recovery (the verdict) runs below in the PURE core.
|
|
383
|
+
let container;
|
|
384
|
+
try {
|
|
385
|
+
const text = fs.readFileSync(path.resolve(opts.revocation), "utf8");
|
|
386
|
+
container = coreRevocation.readRevocation(text);
|
|
387
|
+
} catch (e) {
|
|
388
|
+
writeErr(`error: cannot read signed key revocation ${opts.revocation}: ${e.message}\n`);
|
|
389
|
+
return EXIT.IO;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Run the PURE, OFFLINE verify. No I/O, no key, no network. A structurally-sound-but-forged/mismatched
|
|
393
|
+
// revocation is a clean REJECTED verdict (not a throw); only a genuinely broken read would throw (above).
|
|
394
|
+
let result;
|
|
395
|
+
try {
|
|
396
|
+
result = coreRevocation.verifyRevocation({ container, expectedSigner: opts.signer });
|
|
397
|
+
} catch (e) {
|
|
398
|
+
writeErr(`error: ${e.message}\n`);
|
|
399
|
+
return EXIT.IO;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (opts.json) {
|
|
403
|
+
write(
|
|
404
|
+
JSON.stringify(
|
|
405
|
+
{ ...result, revocation: opts.revocation, note: VERIFY_TRUST_NOTE },
|
|
406
|
+
null,
|
|
407
|
+
2
|
|
408
|
+
) + "\n"
|
|
409
|
+
);
|
|
410
|
+
} else {
|
|
411
|
+
write(renderVerify(result, { revocation: opts.revocation }));
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Exit non-zero on REJECTED so a recipient's CI can gate (0 ACCEPTED / 3 REJECTED — the family's read contract).
|
|
415
|
+
return result.accepted ? EXIT.OK : EXIT.FAIL;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function revocationUsage() {
|
|
419
|
+
return [
|
|
420
|
+
"vh revocation — publish + verify a producer KEY REVOCATION (a key declares ITSELF revoked as of a date)",
|
|
421
|
+
"",
|
|
422
|
+
"Usage:",
|
|
423
|
+
" vh revocation publish --address <0xaddr> --reason <reason> (--key-env <VAR> | --key-file <path>)",
|
|
424
|
+
" [--superseded-by <0xaddr>] [--revoked-at <ISO>] [--out <p>] [--json]",
|
|
425
|
+
" vh revocation verify <revocation> [--signer <0xaddr>] [--json]",
|
|
426
|
+
"",
|
|
427
|
+
"publish MINTS a signed revocation marking --address REVOKED as of --revoked-at (default now) for",
|
|
428
|
+
" --reason (one of " + JSON.stringify(coreRevocation.REVOCATION_REASON_SET) + "), optionally naming a",
|
|
429
|
+
" --superseded-by successor key. It signs with a HUMAN-provisioned key (EXACTLY ONE of --key-env/",
|
|
430
|
+
" --key-file, read-used-discarded; the loop sets/holds NO key) and MINTS ONLY when that key's address",
|
|
431
|
+
" EQUALS --address — a key revokes ITSELF; a third party cannot revoke a key it does not control (else",
|
|
432
|
+
" it hard-errors BEFORE writing). Default prints the revocation + writes NOTHING; --out writes to a",
|
|
433
|
+
" caller-chosen path (never cwd). Exit: 0 ok / 2 usage (missing/invalid field, key-source error, key",
|
|
434
|
+
" does not control --address) / 1 IO.",
|
|
435
|
+
"verify is OFFLINE/key-free/network-free: it RECOVERS the signer, confirms the signature backs it AND that",
|
|
436
|
+
" the recovered signer IS the revocation's vendorAddress (a key revokes ITSELF), OPTIONALLY pins --signer,",
|
|
437
|
+
" and prints the reason/revokedAt/supersededBy + per-check PASS/FAIL. A forged/tampered/wrong-key",
|
|
438
|
+
" revocation, or a wrong --signer, is a clean REJECTED — never a silent pass. Exit: 0 ACCEPTED / 3",
|
|
439
|
+
" REJECTED / 2 usage / 1 IO.",
|
|
440
|
+
"",
|
|
441
|
+
"A revocation is a SIGNED CLAIM by the key-holder (it proves the key-holder SAID \"revoked as of D\"): the",
|
|
442
|
+
"revokedAt instant is self-asserted, NOT a trusted TIMESTAMP without P-3, and this is NOT a legal opinion.",
|
|
443
|
+
"Pin the revocation alongside your identity card; recipients pass it to any signed-verify command via",
|
|
444
|
+
"--revocations <f> [--as-of <ISO>] to downgrade an exhibit signed under a key that was revoked-before-as-of.",
|
|
445
|
+
"",
|
|
446
|
+
].join("\n");
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* CLI dispatch: `vh revocation <publish|verify> ...`. An UNKNOWN subcommand is a USAGE error (2) — the loop
|
|
451
|
+
* never silently accepts a typo'd subcommand. `-h`/`--help`/`help`/no-subcommand prints usage.
|
|
452
|
+
*/
|
|
453
|
+
async function cmdRevocation(argv, io = {}) {
|
|
454
|
+
const writeErr = io.writeErr || ((s) => process.stderr.write(s));
|
|
455
|
+
const [sub, ...rest] = argv;
|
|
456
|
+
if (sub === "publish") {
|
|
457
|
+
let opts;
|
|
458
|
+
try {
|
|
459
|
+
opts = parsePublishArgs(rest);
|
|
460
|
+
} catch (e) {
|
|
461
|
+
writeErr(`error: ${e.message}\n`);
|
|
462
|
+
return EXIT.USAGE;
|
|
463
|
+
}
|
|
464
|
+
return runRevocationPublish(opts, io);
|
|
465
|
+
}
|
|
466
|
+
if (sub === "verify") {
|
|
467
|
+
let opts;
|
|
468
|
+
try {
|
|
469
|
+
opts = parseVerifyArgs(rest);
|
|
470
|
+
} catch (e) {
|
|
471
|
+
writeErr(`error: ${e.message}\n`);
|
|
472
|
+
return EXIT.USAGE;
|
|
473
|
+
}
|
|
474
|
+
return runRevocationVerify(opts, io);
|
|
475
|
+
}
|
|
476
|
+
if (sub === undefined || sub === "-h" || sub === "--help" || sub === "help") {
|
|
477
|
+
io.write ? io.write(revocationUsage()) : process.stdout.write(revocationUsage());
|
|
478
|
+
return sub === undefined ? EXIT.USAGE : EXIT.OK;
|
|
479
|
+
}
|
|
480
|
+
writeErr(`error: unknown revocation subcommand: ${sub} (expected: publish, verify)\n`);
|
|
481
|
+
return EXIT.USAGE;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
module.exports = {
|
|
485
|
+
EXIT,
|
|
486
|
+
nowISO,
|
|
487
|
+
VERIFY_TRUST_NOTE,
|
|
488
|
+
parsePublishArgs,
|
|
489
|
+
parseVerifyArgs,
|
|
490
|
+
runRevocationPublish,
|
|
491
|
+
runRevocationVerify,
|
|
492
|
+
renderVerify,
|
|
493
|
+
revocationUsage,
|
|
494
|
+
cmdRevocation,
|
|
495
|
+
};
|