burnledger 0.8.1 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +144 -2
- package/dist/cjs/anchor.d.ts +5 -3
- package/dist/cjs/anchor.d.ts.map +1 -1
- package/dist/cjs/anchor.js +10 -4
- package/dist/cjs/anchor.js.map +1 -1
- package/dist/cjs/client.d.ts +95 -0
- package/dist/cjs/client.d.ts.map +1 -1
- package/dist/cjs/client.js +175 -54
- package/dist/cjs/client.js.map +1 -1
- package/dist/cjs/customer-keys.d.ts +73 -1
- package/dist/cjs/customer-keys.d.ts.map +1 -1
- package/dist/cjs/customer-keys.js +329 -3
- package/dist/cjs/customer-keys.js.map +1 -1
- package/dist/cjs/enclave-registration.d.ts +165 -0
- package/dist/cjs/enclave-registration.d.ts.map +1 -0
- package/dist/cjs/enclave-registration.js +287 -0
- package/dist/cjs/enclave-registration.js.map +1 -0
- package/dist/cjs/enclave-seal.d.ts +43 -0
- package/dist/cjs/enclave-seal.d.ts.map +1 -1
- package/dist/cjs/enclave-seal.js +62 -1
- package/dist/cjs/enclave-seal.js.map +1 -1
- package/dist/cjs/errors.d.ts +10 -0
- package/dist/cjs/errors.d.ts.map +1 -1
- package/dist/cjs/errors.js +11 -1
- package/dist/cjs/errors.js.map +1 -1
- package/dist/cjs/index.browser.d.ts +7 -3
- package/dist/cjs/index.browser.d.ts.map +1 -1
- package/dist/cjs/index.browser.js +6 -3
- package/dist/cjs/index.browser.js.map +1 -1
- package/dist/cjs/index.d.ts +22 -10
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +75 -6
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/key-group.d.ts +80 -0
- package/dist/cjs/key-group.d.ts.map +1 -0
- package/dist/cjs/key-group.js +136 -0
- package/dist/cjs/key-group.js.map +1 -0
- package/dist/cjs/models.d.ts +60 -0
- package/dist/cjs/models.d.ts.map +1 -1
- package/dist/cjs/models.js +63 -0
- package/dist/cjs/models.js.map +1 -1
- package/dist/cjs/node-runtime.d.ts +40 -0
- package/dist/cjs/node-runtime.d.ts.map +1 -0
- package/dist/cjs/node-runtime.js +42 -0
- package/dist/cjs/node-runtime.js.map +1 -0
- package/dist/cjs/status-document.d.ts +25 -0
- package/dist/cjs/status-document.d.ts.map +1 -0
- package/dist/cjs/status-document.js +62 -0
- package/dist/cjs/status-document.js.map +1 -0
- package/dist/cjs/verify.d.ts +108 -3
- package/dist/cjs/verify.d.ts.map +1 -1
- package/dist/cjs/verify.js +451 -171
- package/dist/cjs/verify.js.map +1 -1
- package/dist/cjs/web-verifier.d.ts +23 -3
- package/dist/cjs/web-verifier.d.ts.map +1 -1
- package/dist/cjs/web-verifier.js +31 -5
- package/dist/cjs/web-verifier.js.map +1 -1
- package/dist/cjs/webhooks.d.ts +9 -2
- package/dist/cjs/webhooks.d.ts.map +1 -1
- package/dist/cjs/webhooks.js +29 -11
- package/dist/cjs/webhooks.js.map +1 -1
- package/dist/esm/anchor.d.ts +5 -3
- package/dist/esm/anchor.d.ts.map +1 -1
- package/dist/esm/anchor.js +10 -4
- package/dist/esm/anchor.js.map +1 -1
- package/dist/esm/cli.d.ts +30 -17
- package/dist/esm/cli.d.ts.map +1 -1
- package/dist/esm/cli.js +142 -83
- package/dist/esm/cli.js.map +1 -1
- package/dist/esm/client.d.ts +95 -0
- package/dist/esm/client.d.ts.map +1 -1
- package/dist/esm/client.js +175 -21
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/customer-keys.d.ts +73 -1
- package/dist/esm/customer-keys.d.ts.map +1 -1
- package/dist/esm/customer-keys.js +323 -3
- package/dist/esm/customer-keys.js.map +1 -1
- package/dist/esm/enclave-registration.d.ts +165 -0
- package/dist/esm/enclave-registration.d.ts.map +1 -0
- package/dist/esm/enclave-registration.js +277 -0
- package/dist/esm/enclave-registration.js.map +1 -0
- package/dist/esm/enclave-seal.d.ts +43 -0
- package/dist/esm/enclave-seal.d.ts.map +1 -1
- package/dist/esm/enclave-seal.js +61 -1
- package/dist/esm/enclave-seal.js.map +1 -1
- package/dist/esm/errors.d.ts +10 -0
- package/dist/esm/errors.d.ts.map +1 -1
- package/dist/esm/errors.js +10 -0
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.browser.d.ts +7 -3
- package/dist/esm/index.browser.d.ts.map +1 -1
- package/dist/esm/index.browser.js +6 -3
- package/dist/esm/index.browser.js.map +1 -1
- package/dist/esm/index.d.ts +22 -10
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +30 -8
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/key-group.d.ts +80 -0
- package/dist/esm/key-group.d.ts.map +1 -0
- package/dist/esm/key-group.js +130 -0
- package/dist/esm/key-group.js.map +1 -0
- package/dist/esm/models.d.ts +60 -0
- package/dist/esm/models.d.ts.map +1 -1
- package/dist/esm/models.js +59 -0
- package/dist/esm/models.js.map +1 -1
- package/dist/esm/node-runtime.d.ts +40 -0
- package/dist/esm/node-runtime.d.ts.map +1 -0
- package/dist/esm/node-runtime.js +38 -0
- package/dist/esm/node-runtime.js.map +1 -0
- package/dist/esm/status-document.d.ts +25 -0
- package/dist/esm/status-document.d.ts.map +1 -0
- package/dist/esm/status-document.js +59 -0
- package/dist/esm/status-document.js.map +1 -0
- package/dist/esm/verify.d.ts +108 -3
- package/dist/esm/verify.d.ts.map +1 -1
- package/dist/esm/verify.js +448 -171
- package/dist/esm/verify.js.map +1 -1
- package/dist/esm/web-verifier.d.ts +23 -3
- package/dist/esm/web-verifier.d.ts.map +1 -1
- package/dist/esm/web-verifier.js +27 -5
- package/dist/esm/web-verifier.js.map +1 -1
- package/dist/esm/webhooks.d.ts +9 -2
- package/dist/esm/webhooks.d.ts.map +1 -1
- package/dist/esm/webhooks.js +29 -11
- package/dist/esm/webhooks.js.map +1 -1
- package/package.json +1 -1
- package/src/anchor.ts +10 -4
- package/src/cli.ts +145 -78
- package/src/client.ts +241 -25
- package/src/customer-keys.ts +373 -3
- package/src/enclave-registration.ts +402 -0
- package/src/enclave-seal.ts +108 -1
- package/src/errors.ts +11 -0
- package/src/index.browser.ts +9 -3
- package/src/index.ts +48 -6
- package/src/key-group.ts +181 -0
- package/src/models.ts +131 -0
- package/src/node-runtime.ts +57 -0
- package/src/status-document.ts +59 -0
- package/src/verify.ts +565 -177
- package/src/web-verifier.ts +36 -3
- package/src/webhooks.ts +28 -11
package/src/cli.ts
CHANGED
|
@@ -9,7 +9,9 @@ import { nodeCrypto } from "./crypto-node.js";
|
|
|
9
9
|
import { CERTIFICATE_REVOKED, VerificationError } from "./errors.js";
|
|
10
10
|
import {
|
|
11
11
|
formatTimestamp,
|
|
12
|
+
issuingImage,
|
|
12
13
|
publicKeyFromHex,
|
|
14
|
+
registrationQualifier,
|
|
13
15
|
verifyCertificateWithStatus,
|
|
14
16
|
verifyTransparency,
|
|
15
17
|
KNOWN_FORMAT_VERSIONS,
|
|
@@ -24,23 +26,34 @@ import {
|
|
|
24
26
|
type KeyListEvidence,
|
|
25
27
|
} from "./keys.js";
|
|
26
28
|
import { ANCHOR_INCONSISTENT, checkStatementAnchor, type AnchorResult } from "./anchor.js";
|
|
27
|
-
import
|
|
29
|
+
import { unwrapStatusStatement } from "./status-document.js";
|
|
30
|
+
import type { AuthorizationPolicy, PublicKeyInfo } from "./verify.js";
|
|
31
|
+
|
|
32
|
+
// Exported from here too: tests/cli-revocation-unknown.test.ts reads it as the
|
|
33
|
+
// CLI's own parser.
|
|
34
|
+
export { unwrapStatusStatement };
|
|
28
35
|
|
|
29
36
|
type Cert = Record<string, unknown>;
|
|
30
37
|
/** A published status statement, exactly as GET /v1/certificates/{id}/status
|
|
31
38
|
* serves it: hex strings, not decoded bytes. verifyCertificateWithStatus reads
|
|
32
39
|
* this shape, so nothing here re-encodes a field the signature covers. */
|
|
33
40
|
type StatusStatement = Record<string, unknown>;
|
|
34
|
-
const USAGE =
|
|
41
|
+
const USAGE =
|
|
42
|
+
"Usage: burnledger check --cert <file> --keys <file> [--online --api-url <url>]\n" +
|
|
43
|
+
" [[--require-certified | --require-certified-system <system_id>...] --customer-key-id <id>...]\n";
|
|
35
44
|
|
|
36
45
|
function die(msg: string): never { process.stderr.write(msg); process.exit(2); }
|
|
37
46
|
|
|
38
47
|
function parseArgs(argv: string[]): {
|
|
39
48
|
certPath: string; keysPath: string; online: boolean; apiUrl: string;
|
|
49
|
+
policy: AuthorizationPolicy | undefined;
|
|
40
50
|
} {
|
|
41
51
|
const args = argv.slice(2);
|
|
42
52
|
if (args[0] !== "check") die(USAGE);
|
|
43
53
|
let certPath = "", keysPath = "", online = false, apiUrl = "";
|
|
54
|
+
let requireCertified = false;
|
|
55
|
+
const systems: string[] = [];
|
|
56
|
+
const keyIds: string[] = [];
|
|
44
57
|
for (let i = 1; i < args.length; i++) {
|
|
45
58
|
const a = args[i]!;
|
|
46
59
|
if (a === "--cert") { certPath = args[++i] ?? die("--cert requires a value\n"); }
|
|
@@ -51,9 +64,29 @@ function parseArgs(argv: string[]): {
|
|
|
51
64
|
// buys nothing — but it is a published flag that scripts already pass, and
|
|
52
65
|
// failing on an unknown flag would break them for no gain.
|
|
53
66
|
else if (a === "--api-key") { void (args[++i] ?? die("--api-key requires a value\n")); }
|
|
67
|
+
// ADR-025. Opt-in: without these, authorization: none verifies as it always
|
|
68
|
+
// has. The Go CLI has no equivalent yet; these names are the proposal for it.
|
|
69
|
+
else if (a === "--require-certified") { requireCertified = true; }
|
|
70
|
+
else if (a === "--require-certified-system") {
|
|
71
|
+
systems.push(args[++i] ?? die("--require-certified-system requires a value\n"));
|
|
72
|
+
}
|
|
73
|
+
else if (a === "--customer-key-id") { keyIds.push(args[++i] ?? die("--customer-key-id requires a value\n")); }
|
|
54
74
|
else die(`Unknown argument: ${a}\n`);
|
|
55
75
|
}
|
|
56
76
|
if (!certPath || !keysPath) die("Both --cert and --keys are required.\n");
|
|
77
|
+
// Every system, or these systems: asking for both is a mistake to name, not
|
|
78
|
+
// one to resolve silently in either direction.
|
|
79
|
+
if (requireCertified && systems.length > 0) {
|
|
80
|
+
die("--require-certified and --require-certified-system are alternatives; use one\n");
|
|
81
|
+
}
|
|
82
|
+
// Certified under a key id you did not name proves nothing to you: the relay
|
|
83
|
+
// can enroll a key group of its own, and the record would name that one.
|
|
84
|
+
if ((requireCertified || systems.length > 0) && keyIds.length === 0) {
|
|
85
|
+
die(
|
|
86
|
+
"--require-certified and --require-certified-system need --customer-key-id <id>: " +
|
|
87
|
+
"certified under a key you did not name proves nothing\n",
|
|
88
|
+
);
|
|
89
|
+
}
|
|
57
90
|
if (online) {
|
|
58
91
|
try {
|
|
59
92
|
apiUrl = resolveApiBaseUrl(apiUrl, process.env);
|
|
@@ -61,7 +94,9 @@ function parseArgs(argv: string[]): {
|
|
|
61
94
|
die((e as Error).message + "\n");
|
|
62
95
|
}
|
|
63
96
|
}
|
|
64
|
-
|
|
97
|
+
const policy: AuthorizationPolicy | undefined =
|
|
98
|
+
keyIds.length > 0 ? { ...(systems.length > 0 ? { systems } : {}), customerKeyIds: keyIds } : undefined;
|
|
99
|
+
return { certPath, keysPath, online, apiUrl, policy };
|
|
65
100
|
}
|
|
66
101
|
|
|
67
102
|
/** Flag first, then environment — the precedence docs/cli.md promises and the
|
|
@@ -86,8 +121,8 @@ export function resolveApiBaseUrl(
|
|
|
86
121
|
* carries a signed statement — key_id == sha256(public_key) makes every
|
|
87
122
|
* identity self-checking — so an unsigned file is not refused. What changes is
|
|
88
123
|
* what this CLI is entitled to say about the statuses and dates. Mirrors
|
|
89
|
-
* loadKeysFromFile in cmd/cli/keys.go. */
|
|
90
|
-
async function loadKeys(path: string): Promise<{ keys: Map<string, PublicKeyInfo>; keyList: KeyListEvidence }> {
|
|
124
|
+
* loadKeysFromFile in cmd/cli/keys.go. Exported for testing. */
|
|
125
|
+
export async function loadKeys(path: string): Promise<{ keys: Map<string, PublicKeyInfo>; keyList: KeyListEvidence }> {
|
|
91
126
|
let entries: KeyEntry[];
|
|
92
127
|
let keyList: KeyListEvidence;
|
|
93
128
|
try {
|
|
@@ -147,64 +182,61 @@ async function loadKeys(path: string): Promise<{ keys: Map<string, PublicKeyInfo
|
|
|
147
182
|
* issuer's signature.
|
|
148
183
|
*
|
|
149
184
|
* No Authorization header is sent. The endpoint takes none, and sending an API
|
|
150
|
-
* key to an arbitrary --api-url is a credential leak the check never needed.
|
|
151
|
-
async function fetchStatusStatement(apiUrl: string, certId: string): Promise<StatusStatement> {
|
|
152
|
-
const res = await fetch(`${apiUrl.replace(/\/+$/, "")}/v1/certificates/${certId}/status`);
|
|
153
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
154
|
-
return unwrapStatusStatement(await res.json());
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/** The API wraps every response in {"data": ...}; a caller may hand over either
|
|
158
|
-
* the envelope or the document inside it. Mirrors
|
|
159
|
-
* core.ParseCertificateStatusDocument, so this CLI and the Go one cannot
|
|
160
|
-
* disagree about which shape the endpoint returns. Exported for testing.
|
|
161
|
-
*
|
|
162
|
-
* A document that does not parse THROWS, and the caller turns that into a
|
|
163
|
-
* warning and a VALID_REVOCATION_UNKNOWN verdict. That is deliberate: an answer
|
|
164
|
-
* this build cannot read is silence about revocation, not a finding about the
|
|
165
|
-
* certificate. Handing a half-read document to the verifier instead would
|
|
166
|
-
* report "status statement signature is invalid" — a claim about the issuer —
|
|
167
|
-
* when what actually happened is that something returned nonsense.
|
|
185
|
+
* key to an arbitrary --api-url is a credential leak the check never needed.
|
|
168
186
|
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
export function
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
187
|
+
* Read under a 1 MiB cap and a total deadline, matching the anchor fetch
|
|
188
|
+
* (anchor.ts) and the Go CLI (cmd/cli/online.go): a status statement is a few
|
|
189
|
+
* hundred bytes, so anything near the cap is a misbehaving or hostile server.
|
|
190
|
+
* The AbortController bounds the whole fetch including the body read, which a
|
|
191
|
+
* server dripping bytes could otherwise hold open. Exported for testing. */
|
|
192
|
+
export async function fetchStatusStatement(apiUrl: string, certId: string): Promise<StatusStatement> {
|
|
193
|
+
const url = `${apiUrl.replace(/\/+$/, "")}/v1/certificates/${certId}/status`;
|
|
194
|
+
const controller = new AbortController();
|
|
195
|
+
const timer = setTimeout(() => { controller.abort(); }, STATUS_FETCH_TIMEOUT_MS);
|
|
196
|
+
try {
|
|
197
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
198
|
+
const body = await readCapped(res);
|
|
199
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
200
|
+
try {
|
|
201
|
+
return unwrapStatusStatement(JSON.parse(body));
|
|
202
|
+
} catch (e) {
|
|
203
|
+
throw new Error(`parse response: ${e instanceof Error ? e.message : String(e)}`);
|
|
204
|
+
}
|
|
205
|
+
} finally {
|
|
206
|
+
clearTimeout(timer);
|
|
184
207
|
}
|
|
185
|
-
for (const f of ["statement_issued_at", "statement_expires_at"]) requireRfc3339(doc, f);
|
|
186
|
-
if (doc.revoked_at != null) requireRfc3339(doc, "revoked_at");
|
|
187
|
-
if (typeof doc.sth_tree_size !== "number") throw new Error("status statement: sth_tree_size is not a number");
|
|
188
|
-
// Refuse a short or long value rather than letting it through: a truncated
|
|
189
|
-
// signature or root hash that reaches the verifier is checked against bytes
|
|
190
|
-
// nobody sent.
|
|
191
|
-
requireHex(doc, "sth_root_hash", 32);
|
|
192
|
-
requireHex(doc, "signature", 64);
|
|
193
|
-
return doc;
|
|
194
208
|
}
|
|
195
209
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
210
|
+
/** How much of the status response to buffer (1 MiB) and how long the whole
|
|
211
|
+
* fetch may take (10 s), matching cmd/cli/online.go and anchor.ts. */
|
|
212
|
+
const MAX_STATUS_BYTES = 1 << 20;
|
|
213
|
+
const STATUS_FETCH_TIMEOUT_MS = 10_000;
|
|
214
|
+
|
|
215
|
+
/** Read a response body, stopping at the cap instead of buffering whatever a
|
|
216
|
+
* hostile server decides to send. The stream is cancelled on the way out, so an
|
|
217
|
+
* endless body costs one megabyte and not the process. */
|
|
218
|
+
async function readCapped(res: Response): Promise<string> {
|
|
219
|
+
const reader = res.body?.getReader();
|
|
220
|
+
if (reader === undefined) return "";
|
|
221
|
+
const chunks: Uint8Array[] = [];
|
|
222
|
+
let total = 0;
|
|
223
|
+
for (;;) {
|
|
224
|
+
const { done, value } = await reader.read();
|
|
225
|
+
if (done) break;
|
|
226
|
+
total += value.length;
|
|
227
|
+
if (total > MAX_STATUS_BYTES) {
|
|
228
|
+
await reader.cancel();
|
|
229
|
+
throw new Error(`read response: body exceeds ${MAX_STATUS_BYTES} bytes`);
|
|
230
|
+
}
|
|
231
|
+
chunks.push(value);
|
|
200
232
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
throw new Error(`status statement: ${field} is not ${bytes} bytes of hex`);
|
|
233
|
+
const joined = new Uint8Array(total);
|
|
234
|
+
let at = 0;
|
|
235
|
+
for (const c of chunks) {
|
|
236
|
+
joined.set(c, at);
|
|
237
|
+
at += c.length;
|
|
207
238
|
}
|
|
239
|
+
return new TextDecoder().decode(joined);
|
|
208
240
|
}
|
|
209
241
|
|
|
210
242
|
/** The verdicts a reader can act on, and therefore the ones that exit 0.
|
|
@@ -321,8 +353,15 @@ function pushRecordCounts(L: string[], sys: Record<string, unknown>[]): void {
|
|
|
321
353
|
}
|
|
322
354
|
|
|
323
355
|
/**
|
|
324
|
-
* Per system: did the enclave check it against a
|
|
325
|
-
* and
|
|
356
|
+
* Per system: did the enclave check it against a registration, under which key,
|
|
357
|
+
* and what does the image that signed the record make of that?
|
|
358
|
+
*
|
|
359
|
+
* Printed for exactly the verdicts that say the record's own signature
|
|
360
|
+
* verified: VALID, VALID_REVOCATION_UNKNOWN, the two soft key verdicts and
|
|
361
|
+
* REVOKED, as in the Go and Python CLIs and the /verify page. The qualifier is
|
|
362
|
+
* read from issuer.enclave_pcr0, which that signature covers, and
|
|
363
|
+
* verifyCertificate checks it before any of those verdicts is reached. Beside
|
|
364
|
+
* INVALID the field is unverified bytes, so nothing is printed there.
|
|
326
365
|
*
|
|
327
366
|
* This line is the WHOLE POINT of the field. ADR-025 §2: the enclave cannot
|
|
328
367
|
* refuse a second enrollment for a team — that needs durable state — so a parent
|
|
@@ -344,14 +383,11 @@ function pushRegistrationEvidence(L: string[], cert: Record<string, unknown>): v
|
|
|
344
383
|
);
|
|
345
384
|
return;
|
|
346
385
|
}
|
|
386
|
+
const qualifier = registrationQualifier(issuingImage(cert));
|
|
347
387
|
for (const sys of (cert.systems as Record<string, unknown>[]) ?? []) {
|
|
348
388
|
const name = sys.system_name as string;
|
|
349
389
|
if (sys.authorization === "certified") {
|
|
350
|
-
L.push(
|
|
351
|
-
` Registration [${name}]: certified under ${sys.customer_key_id as string} — ` +
|
|
352
|
-
"the enclave checked this system against a customer-signed registration. " +
|
|
353
|
-
"If that key id is not yours, someone else authorized this verification.",
|
|
354
|
-
);
|
|
390
|
+
L.push(` Registration [${name}]: certified under ${sys.customer_key_id as string} — ${qualifier}`);
|
|
355
391
|
} else if (sys.authorization === "none") {
|
|
356
392
|
L.push(
|
|
357
393
|
` Registration [${name}]: none — this verification was NOT checked against ` +
|
|
@@ -388,8 +424,9 @@ export function formatOutput(
|
|
|
388
424
|
status: StatusStatement | undefined, online: boolean, anchor?: AnchorResult,
|
|
389
425
|
keyList?: KeyListEvidence,
|
|
390
426
|
): string {
|
|
391
|
-
|
|
392
|
-
|
|
427
|
+
// This line prints for records that FAILED verification too, so it must not
|
|
428
|
+
// assume the shape verification would have checked.
|
|
429
|
+
const id = typeof cert.certificate_id === "string" ? cert.certificate_id.slice(0, 6) : "unknown";
|
|
393
430
|
const att = cert.attestation as Record<string, unknown>;
|
|
394
431
|
const L: string[] = [];
|
|
395
432
|
|
|
@@ -407,22 +444,33 @@ export function formatOutput(
|
|
|
407
444
|
return L.join("\n") + "\n";
|
|
408
445
|
}
|
|
409
446
|
|
|
410
|
-
// A soft key verdict is not a forgery: every signature verified, and what
|
|
411
|
-
// could not be established is the key's authority. "INVALID" would send an
|
|
412
|
-
// auditor hunting for tampering that is not there, and "VALID" — which this
|
|
413
|
-
// CLI printed — claims a check that did not happen.
|
|
414
|
-
const note = SOFT_KEY_VERDICTS[sigResult];
|
|
415
|
-
if (note !== undefined) return `Verification record dc_${id}: ${sigResult} (${note})\n`;
|
|
416
447
|
// Revocation is now a signed fact, so the reader gets the evidence with the
|
|
417
448
|
// verdict: who said it, when, and which log entry it points at.
|
|
418
449
|
if (sigResult === REVOKED) {
|
|
419
450
|
L.push(`Verification record dc_${id}: INVALID (REVOKED)`);
|
|
451
|
+
pushRegistrationEvidence(L, cert);
|
|
420
452
|
pushStatementEvidence(L, status);
|
|
421
453
|
pushAnchorEvidence(L, anchor);
|
|
422
454
|
return L.join("\n") + "\n";
|
|
423
455
|
}
|
|
424
|
-
|
|
456
|
+
const note = SOFT_KEY_VERDICTS[sigResult];
|
|
457
|
+
if (!USABLE_VERDICTS.has(sigResult) && note === undefined) {
|
|
458
|
+
return `Verification record dc_${id}: INVALID (${sigResult})\n`;
|
|
459
|
+
}
|
|
460
|
+
// A failed proof refuses the record, and it outranks a soft key verdict: a
|
|
461
|
+
// verdict that asks for a human must not hide one that refuses. The Python
|
|
462
|
+
// CLI and the /verify page already put it first; this CLI and Go printed
|
|
463
|
+
// the soft verdict instead.
|
|
425
464
|
if (transparencyFailed) return `Verification record dc_${id}: INVALID (transparency: ${transparency})\n`;
|
|
465
|
+
// A soft key verdict is not a forgery: every signature verified, and what
|
|
466
|
+
// could not be established is the key's authority. "INVALID" would send an
|
|
467
|
+
// auditor hunting for tampering that is not there, and "VALID" — which this
|
|
468
|
+
// CLI printed — claims a check that did not happen.
|
|
469
|
+
if (note !== undefined) {
|
|
470
|
+
L.push(`Verification record dc_${id}: ${sigResult} (${note})`);
|
|
471
|
+
pushRegistrationEvidence(L, cert);
|
|
472
|
+
return L.join("\n") + "\n";
|
|
473
|
+
}
|
|
426
474
|
|
|
427
475
|
// The two usable verdicts. VALID is sayable without a qualifier for the first
|
|
428
476
|
// time: it is reached only when a fresh statement, signed by a key from the
|
|
@@ -433,6 +481,7 @@ export function formatOutput(
|
|
|
433
481
|
? `${VALID_REVOCATION_UNKNOWN} (${online ? REVOCATION_UNKNOWN_ONLINE : REVOCATION_UNKNOWN_OFFLINE})`
|
|
434
482
|
: "VALID (a signed status statement says this record is live)";
|
|
435
483
|
L.push(`Verification record dc_${id}: ${tag}`);
|
|
484
|
+
const sub = displayHash((cert.subject as Record<string, unknown>).identifier_hash);
|
|
436
485
|
L.push(` Subject: sha256:${sub.slice(0, 6)}...`);
|
|
437
486
|
L.push(` Format version: ${cert.certificate_format_version as string}`);
|
|
438
487
|
|
|
@@ -583,7 +632,13 @@ export function unwrapCertificate(parsed: Cert): Cert {
|
|
|
583
632
|
|
|
584
633
|
async function main(): Promise<void> {
|
|
585
634
|
const args = parseArgs(process.argv);
|
|
586
|
-
const
|
|
635
|
+
const parsed: unknown = JSON.parse(await readFile(args.certPath, "utf-8"));
|
|
636
|
+
// No record to give a verdict about. Go's loader refuses the same file at
|
|
637
|
+
// unmarshal and exits 2, as this does.
|
|
638
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
639
|
+
die(`${args.certPath}: not a JSON object\n`);
|
|
640
|
+
}
|
|
641
|
+
const cert = unwrapCertificate(parsed as Cert);
|
|
587
642
|
const { keys, keyList } = await loadKeys(args.keysPath);
|
|
588
643
|
|
|
589
644
|
// Fetched first, because the verdict now depends on it. A status statement is
|
|
@@ -623,11 +678,19 @@ async function main(): Promise<void> {
|
|
|
623
678
|
// and VALID_KEY_COMPROMISED_LATER are cases verifyCertificateWithStatus
|
|
624
679
|
// deliberately refused to call VALID, and this is the tool an auditor runs in
|
|
625
680
|
// a script — the one whose exit code gets trusted.
|
|
681
|
+
//
|
|
682
|
+
// Only a VerificationError is a verdict. Anything else is a defect in this
|
|
683
|
+
// CLI or the SDK under it; printing it as INVALID dressed a crash up as a
|
|
684
|
+
// finding about the record, so it propagates and exits 2 instead.
|
|
626
685
|
let sigResult = "VALID";
|
|
627
|
-
try {
|
|
686
|
+
try {
|
|
687
|
+
sigResult = await verifyCertificateWithStatus(nodeCrypto, cert, keys, status ?? null, undefined, {
|
|
688
|
+
requireAuthorization: args.policy,
|
|
689
|
+
});
|
|
690
|
+
}
|
|
628
691
|
catch (e) {
|
|
629
|
-
if (e instanceof VerificationError
|
|
630
|
-
|
|
692
|
+
if (!(e instanceof VerificationError)) throw e;
|
|
693
|
+
sigResult = e.code === CERTIFICATE_REVOKED ? REVOKED : e.message;
|
|
631
694
|
}
|
|
632
695
|
|
|
633
696
|
// NOT_AVAILABLE (no proof) is a legitimate result; only a thrown error is a
|
|
@@ -635,7 +698,11 @@ async function main(): Promise<void> {
|
|
|
635
698
|
let transparency = "NOT_AVAILABLE";
|
|
636
699
|
let transparencyFailed = false;
|
|
637
700
|
try { transparency = await verifyTransparency(nodeCrypto, cert, keys); }
|
|
638
|
-
catch (e) {
|
|
701
|
+
catch (e) {
|
|
702
|
+
if (!(e instanceof VerificationError)) throw e;
|
|
703
|
+
transparency = e.message;
|
|
704
|
+
transparencyFailed = true;
|
|
705
|
+
}
|
|
639
706
|
|
|
640
707
|
const valid = certVerdict(sigResult, transparencyFailed, anchor);
|
|
641
708
|
|