@sanning/proof 0.19.1 → 0.20.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/README.md +30 -29
- package/dist/agent-record.d.ts +62 -0
- package/dist/agent-record.js +527 -0
- package/dist/agent-schemas.generated.d.ts +15 -0
- package/dist/agent-schemas.generated.js +5759 -0
- package/dist/case-file.d.ts +32 -89
- package/dist/case-file.js +41 -93
- package/dist/case.d.ts +339 -822
- package/dist/case.js +667 -1160
- package/dist/chain.d.ts +19 -47
- package/dist/chain.js +78 -205
- package/dist/claims.d.ts +211 -0
- package/dist/claims.js +1081 -0
- package/dist/cli.d.ts +45 -68
- package/dist/cli.js +618 -1022
- package/dist/compose.js +23 -65
- package/dist/crypto.d.ts +7 -8
- package/dist/crypto.js +60 -147
- package/dist/declaration.d.ts +68 -194
- package/dist/declaration.js +175 -518
- package/dist/environment.d.ts +9 -30
- package/dist/environment.js +17 -66
- package/dist/evidence.d.ts +119 -160
- package/dist/evidence.js +744 -1103
- package/dist/exit-codes.d.ts +11 -27
- package/dist/exit-codes.js +24 -59
- package/dist/file-match.d.ts +33 -79
- package/dist/file-match.js +33 -69
- package/dist/grading.d.ts +98 -135
- package/dist/grading.js +224 -380
- package/dist/handoff.d.ts +62 -160
- package/dist/handoff.js +153 -393
- package/dist/index.d.ts +14 -10
- package/dist/index.js +60 -167
- package/dist/json-schema.d.ts +29 -0
- package/dist/json-schema.js +298 -0
- package/dist/merkle.js +16 -28
- package/dist/message-parts.d.ts +56 -212
- package/dist/message-parts.js +142 -502
- package/dist/side-content.d.ts +11 -22
- package/dist/side-content.js +22 -65
- package/dist/types.d.ts +17 -0
- package/dist/types.js +19 -5
- package/dist/verifier.d.ts +40 -114
- package/dist/verifier.js +196 -535
- package/dist/witness-cache.d.ts +24 -45
- package/dist/witness-cache.js +38 -81
- package/dist/witness-lookup.d.ts +36 -57
- package/dist/witness-lookup.js +89 -139
- package/dist/zip-reader.d.ts +18 -33
- package/dist/zip-reader.js +19 -34
- package/package.json +11 -3
- package/src/agent-record.ts +608 -0
- package/src/agent-schemas.generated.ts +5764 -0
- package/src/case-file.ts +45 -109
- package/src/case.ts +814 -1328
- package/src/chain.ts +85 -224
- package/src/claims.ts +1253 -0
- package/src/cli.ts +714 -1176
- package/src/compose.ts +33 -79
- package/src/crypto.ts +60 -147
- package/src/declaration.ts +219 -656
- package/src/environment.ts +17 -65
- package/src/evidence.ts +958 -1381
- package/src/exit-codes.ts +24 -63
- package/src/file-match.ts +40 -89
- package/src/grading.ts +287 -421
- package/src/handoff.ts +175 -427
- package/src/index.ts +131 -177
- package/src/json-schema.ts +295 -0
- package/src/merkle.ts +16 -28
- package/src/message-parts.ts +155 -533
- package/src/side-content.ts +25 -68
- package/src/types.ts +55 -36
- package/src/verifier.ts +202 -526
- package/src/witness-cache.ts +45 -94
- package/src/witness-lookup.ts +107 -177
- package/src/zip-reader.ts +20 -38
- package/vectors/README.md +52 -34
- package/vectors/agent-record.json +2175 -0
- package/vectors/case-file-index.json +1 -1
- package/vectors/case-split.json +2 -2
- package/vectors/chain-reading.json +3 -3
- package/vectors/file-match.json +285 -148
- package/vectors/jcs-reader.json +6 -6
- package/vectors/pack-grading.json +102 -25
- package/vectors/sandbox-evidence.json +573 -581
- package/vectors/stamp-on-chain.json +18 -18
- package/vectors/witness-cache.json +49 -49
- package/dist/agent-proof.d.ts +0 -28
- package/dist/agent-proof.js +0 -226
- package/dist/run-adapters.d.ts +0 -10
- package/dist/run-adapters.js +0 -35
- package/dist/run.d.ts +0 -209
- package/dist/run.js +0 -1405
- package/src/agent-proof.ts +0 -264
- package/src/run-adapters.ts +0 -44
- package/src/run.ts +0 -1598
package/src/witness-lookup.ts
CHANGED
|
@@ -1,26 +1,14 @@
|
|
|
1
1
|
// The public-record (witness) lookup the CLI runs with `--gateways`
|
|
2
|
-
// (evidence-bundle.md §5.5.6
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// a browser app, call the SAME implementation rather than each keeping its
|
|
6
|
-
// own copy that can drift.
|
|
2
|
+
// (evidence-bundle.md §5.5.6): re-fetch each pack's witness on the public record
|
|
3
|
+
// and report what it said. The CLI and non-CLI callers (the verify page, a
|
|
4
|
+
// browser app) share this one implementation.
|
|
7
5
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// caller supplies one (a browser page's own `fetch`, or a test's fake), that
|
|
12
|
-
// is the ONE this module calls; where none is given, it falls back to the
|
|
13
|
-
// ambient global `fetch`, the same fallback `cli.ts` already applied at its
|
|
14
|
-
// own call site before this module existed.
|
|
15
|
-
//
|
|
16
|
-
// `onChainHalf` and `witnessLine` are the CLI's own building blocks,
|
|
17
|
-
// UNCHANGED (proof#200 is additive: the CLI's `case` output and exit codes are
|
|
18
|
-
// byte-for-byte what they were). `lookupPublicRecord` is the new export:
|
|
19
|
-
// one pack (or several) in, one four-state result out, with a pack's
|
|
20
|
-
// `witness-cache.json` read as part of the pack rather than as a side file a
|
|
21
|
-
// caller must remember to also pass in.
|
|
6
|
+
// The network call is the one exception to the kernel's purity (specs/architecture.md §3).
|
|
7
|
+
// It never runs unless a caller hands in a gateway, and `options.fetchImpl`
|
|
8
|
+
// is the seam: the caller's `fetch` if given, else the ambient global.
|
|
22
9
|
|
|
23
10
|
import { SIMULATED_WITNESS_KIND, verifyEvidenceBundle } from "./evidence.js";
|
|
11
|
+
import type { EvidenceBundleResult } from "./evidence.js";
|
|
24
12
|
import type { WitnessLine, WitnessState } from "./case.js";
|
|
25
13
|
import { WITNESS_NOT_LOOKED_UP, renderWitnessLine } from "./case.js";
|
|
26
14
|
import {
|
|
@@ -32,7 +20,7 @@ import {
|
|
|
32
20
|
renderWitnessTime,
|
|
33
21
|
} from "./witness-cache.js";
|
|
34
22
|
|
|
35
|
-
/** The seam every network call
|
|
23
|
+
/** The seam every network call goes through: the caller's `fetch` if given, else the ambient global. */
|
|
36
24
|
export interface WitnessLookupOptions {
|
|
37
25
|
fetchImpl?: typeof fetch;
|
|
38
26
|
}
|
|
@@ -44,13 +32,9 @@ export interface OnChainRow {
|
|
|
44
32
|
mismatched: number;
|
|
45
33
|
unreachable: number;
|
|
46
34
|
not_checked: number;
|
|
47
|
-
/**
|
|
48
|
-
* How many of the `not_checked` witnesses are simulated: a sandbox plane's
|
|
49
|
-
* stand-in for a chain, never looked up. Present only where there
|
|
50
|
-
* is one, so a production pack's row is unchanged.
|
|
51
|
-
*/
|
|
35
|
+
/** How many of the `not_checked` witnesses are simulated (a sandbox plane's stand-in for a chain, never looked up). Present only where there is one. */
|
|
52
36
|
simulated?: number;
|
|
53
|
-
/** Present only where the pack caches a witness time (5.6.6
|
|
37
|
+
/** Present only where the pack caches a witness time (5.6.6). */
|
|
54
38
|
witness_times?: WitnessTimeRow[];
|
|
55
39
|
}
|
|
56
40
|
|
|
@@ -86,29 +70,68 @@ export interface OnChainHalf {
|
|
|
86
70
|
checked: boolean;
|
|
87
71
|
gateways: string[];
|
|
88
72
|
packs: OnChainRow[];
|
|
89
|
-
verdict: "not_checked" | "confirmed" | "
|
|
73
|
+
verdict: "not_checked" | "confirmed" | "differs" | "not_settled";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** One pack's witnesses as a lookup left them: how many, and each one's rollup. */
|
|
77
|
+
export function onChainRow(
|
|
78
|
+
ref: string,
|
|
79
|
+
result: EvidenceBundleResult,
|
|
80
|
+
): { row: OnChainRow; answered: Set<string> } {
|
|
81
|
+
const witnesses = [...result.checkpoints, ...(result.stamps ?? [])];
|
|
82
|
+
const row: OnChainRow = {
|
|
83
|
+
ref,
|
|
84
|
+
witnesses: witnesses.length,
|
|
85
|
+
confirmed: 0,
|
|
86
|
+
mismatched: 0,
|
|
87
|
+
unreachable: 0,
|
|
88
|
+
not_checked: 0,
|
|
89
|
+
};
|
|
90
|
+
for (const w of witnesses) {
|
|
91
|
+
const rollup = w.onChain?.rollup ?? null;
|
|
92
|
+
if (rollup === "confirm") row.confirmed++;
|
|
93
|
+
else if (rollup === "mismatch") row.mismatched++;
|
|
94
|
+
else if (rollup === "unreachable") row.unreachable++;
|
|
95
|
+
else row.not_checked++;
|
|
96
|
+
}
|
|
97
|
+
const simulated = witnesses.filter((w) => w.anchorRef?.kind === SIMULATED_WITNESS_KIND).length;
|
|
98
|
+
if (simulated > 0) row.simulated = simulated;
|
|
99
|
+
const answered = new Set<string>();
|
|
100
|
+
for (const w of witnesses) {
|
|
101
|
+
for (const outcome of w.onChain?.perGateway ?? []) {
|
|
102
|
+
if (outcome.outcome === "confirm" || outcome.outcome === "mismatch") {
|
|
103
|
+
answered.add(outcome.gateway);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return { row, answered };
|
|
90
108
|
}
|
|
91
109
|
|
|
92
110
|
/**
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
111
|
+
* `differs` where a lookup found other bytes or another time, `not_settled` where one
|
|
112
|
+
* did not settle, `confirmed` otherwise. Only `differs` moves a verdict.
|
|
113
|
+
*/
|
|
114
|
+
export function onChainOutcome(rows: readonly OnChainRow[]): "differs" | "not_settled" | "confirmed" {
|
|
115
|
+
if (
|
|
116
|
+
rows.some((r) => r.mismatched > 0 || (r.witness_times ?? []).some((t) => t.outcome === "differs"))
|
|
117
|
+
) {
|
|
118
|
+
return "differs";
|
|
119
|
+
}
|
|
120
|
+
if (rows.some((r) => r.witnesses === 0 || r.unreachable > 0 || r.not_checked > 0)) {
|
|
121
|
+
return "not_settled";
|
|
122
|
+
}
|
|
123
|
+
return "confirmed";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Each pack's witnesses, re-fetched through `verifyEvidenceBundle`. Outside the
|
|
128
|
+
* case reading on purpose: `readCase` makes no network call, so the case verdict
|
|
129
|
+
* is the same on every surface. This adds the half the reading names as not checked.
|
|
101
130
|
*/
|
|
102
131
|
export async function onChainHalf(
|
|
103
132
|
packs: readonly (readonly [string, unknown, readonly CachedWitnessTime[]])[],
|
|
104
133
|
gateways: readonly string[],
|
|
105
134
|
options: WitnessLookupOptions = {},
|
|
106
|
-
/**
|
|
107
|
-
* Where given, receives for each pack the `stamp_ref` of every stamp whose
|
|
108
|
-
* witness this lookup confirmed: the run reading evaluates an enclave token
|
|
109
|
-
* at a stamp's interval end only there.
|
|
110
|
-
*/
|
|
111
|
-
witnessed?: string[][],
|
|
112
135
|
): Promise<OnChainHalf> {
|
|
113
136
|
if (gateways.length === 0) return { checked: false, gateways: [], packs: [], verdict: "not_checked" };
|
|
114
137
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
@@ -116,53 +139,19 @@ export async function onChainHalf(
|
|
|
116
139
|
const answered = new Set<string>();
|
|
117
140
|
for (const [ref, bundle, cached] of packs) {
|
|
118
141
|
const result = await verifyEvidenceBundle(bundle, { gateways: [...gateways], fetchImpl });
|
|
119
|
-
const
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
witnesses: witnesses.length,
|
|
123
|
-
confirmed: 0,
|
|
124
|
-
mismatched: 0,
|
|
125
|
-
unreachable: 0,
|
|
126
|
-
not_checked: 0,
|
|
127
|
-
};
|
|
128
|
-
witnessed?.push(
|
|
129
|
-
(result.stamps ?? []).filter((st) => st.ok && st.onChain?.rollup === "confirm").map((st) => st.stampRef),
|
|
130
|
-
);
|
|
131
|
-
for (const w of witnesses) {
|
|
132
|
-
const rollup = w.onChain?.rollup ?? null;
|
|
133
|
-
if (rollup === "confirm") row.confirmed++;
|
|
134
|
-
else if (rollup === "mismatch") row.mismatched++;
|
|
135
|
-
else if (rollup === "unreachable") row.unreachable++;
|
|
136
|
-
else row.not_checked++;
|
|
137
|
-
}
|
|
138
|
-
const simulated = witnesses.filter((w) => w.anchorRef?.kind === SIMULATED_WITNESS_KIND).length;
|
|
139
|
-
if (simulated > 0) row.simulated = simulated;
|
|
140
|
-
// The witness times the pack's witness-cache.json gives, looked up where
|
|
141
|
-
// it gives any (5.6.6, v1.25, A30). A pack with no cache gets no key and
|
|
142
|
-
// no request.
|
|
142
|
+
const { row, answered: heard } = onChainRow(ref, result);
|
|
143
|
+
for (const g of heard) answered.add(g);
|
|
144
|
+
// The witness times the pack's witness-cache.json gives (5.6.6). A pack with no cache gets no key and no request.
|
|
143
145
|
const times = await checkWitnessCache(cached, gateways, fetchImpl);
|
|
144
146
|
if (times.length > 0) row.witness_times = times.map(witnessTimeRow);
|
|
145
147
|
rows.push(row);
|
|
146
|
-
for (const w of witnesses) {
|
|
147
|
-
for (const outcome of w.onChain?.perGateway ?? []) {
|
|
148
|
-
if (outcome.outcome === "confirm" || outcome.outcome === "mismatch") {
|
|
149
|
-
answered.add(outcome.gateway);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
148
|
for (const t of times) {
|
|
154
149
|
for (const a of t.answers) {
|
|
155
150
|
if (a.outcome === "matches" || a.outcome === "differs") answered.add(a.gateway);
|
|
156
151
|
}
|
|
157
152
|
}
|
|
158
153
|
}
|
|
159
|
-
const verdict = rows
|
|
160
|
-
(r) => r.mismatched > 0 || (r.witness_times ?? []).some((t) => t.outcome === "differs"),
|
|
161
|
-
)
|
|
162
|
-
? "failed"
|
|
163
|
-
: rows.some((r) => r.witnesses === 0 || r.unreachable > 0 || r.not_checked > 0)
|
|
164
|
-
? "undetermined"
|
|
165
|
-
: "confirmed";
|
|
154
|
+
const verdict = onChainOutcome(rows);
|
|
166
155
|
return {
|
|
167
156
|
answered: gateways.filter((g) => answered.has(g)),
|
|
168
157
|
checked: true,
|
|
@@ -179,8 +168,7 @@ function distinctTimes(times: Iterable<[number, number]>): [number, number][] {
|
|
|
179
168
|
return [...seen.values()].sort((a, b) => a[1] - b[1] || a[0] - b[0]);
|
|
180
169
|
}
|
|
181
170
|
|
|
182
|
-
// Only the keys that carry something
|
|
183
|
-
// is the line 0.13.0 printed, field for field.
|
|
171
|
+
// Only the keys that carry something.
|
|
184
172
|
function withTimes(
|
|
185
173
|
line: WitnessLine,
|
|
186
174
|
cached: [number, number][],
|
|
@@ -194,15 +182,11 @@ function withTimes(
|
|
|
194
182
|
}
|
|
195
183
|
|
|
196
184
|
/**
|
|
197
|
-
* `cached` is every witness time the packs cache (5.6.6
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
* Moved from `cli.ts` unchanged: same inputs, same three states
|
|
204
|
-
* (`WitnessState`), same rendering. The CLI's `case` command still calls this
|
|
205
|
-
* exact function, so its witness line is unchanged.
|
|
185
|
+
* `cached` is every witness time the packs cache (5.6.6). Where no lookup ran,
|
|
186
|
+
* each is printed labelled not checked. Where one ran, each time the public
|
|
187
|
+
* record matched is printed as the record's, each time no lookup settled stays
|
|
188
|
+
* labelled not checked, and a time the record contradicts makes the line read
|
|
189
|
+
* `differs`.
|
|
206
190
|
*/
|
|
207
191
|
export function witnessLine(onChain: OnChainHalf, cached: readonly CachedWitnessTime[] = []): WitnessLine {
|
|
208
192
|
if (!onChain.checked) {
|
|
@@ -239,7 +223,7 @@ export function witnessLine(onChain: OnChainHalf, cached: readonly CachedWitness
|
|
|
239
223
|
`, and the public record dates it ${renderWitnessTime(a.block_height as number, a.block_time as number)}`;
|
|
240
224
|
if (!contradicted.includes(reason)) contradicted.push(reason);
|
|
241
225
|
}
|
|
242
|
-
if (onChain.verdict === "
|
|
226
|
+
if (onChain.verdict === "differs") {
|
|
243
227
|
return withTimes({ state: "differs", reasons: contradicted, services }, unsettled, recorded);
|
|
244
228
|
}
|
|
245
229
|
if (onChain.verdict === "confirmed") {
|
|
@@ -258,39 +242,20 @@ export function witnessLine(onChain: OnChainHalf, cached: readonly CachedWitness
|
|
|
258
242
|
return withTimes({ state: "not_checked", reasons, services }, unsettled, recorded);
|
|
259
243
|
}
|
|
260
244
|
|
|
261
|
-
// ─── The public-entry-point export
|
|
262
|
-
//
|
|
263
|
-
// `onChainHalf` and `witnessLine` above are the CLI's own two-step shape:
|
|
264
|
-
// gateways in, an `OnChainHalf` (every row, every gateway) out, then a
|
|
265
|
-
// `WitnessLine` (three states: `confirmed`, `not_checked`, `differs`) rendered
|
|
266
|
-
// from it. A second caller, the verify page, does not want the CLI's
|
|
267
|
-
// internal row shape and does want a fourth state the CLI's three-state
|
|
268
|
-
// `WitnessLine` cannot name: a pack that carries NO witness at all yet is not
|
|
269
|
-
// "not checked" (a lookup that could have found something and didn't), it is
|
|
270
|
-
// "there is nothing on the public record to check". Folding that pack into
|
|
271
|
-
// `not_checked` is not wrong for the CLI (`case` already says so in the
|
|
272
|
-
// reason text: "a pack names no witness"), but a tag that only ever reads
|
|
273
|
-
// "Not checked" hides the one distinction an auditor most wants: did we look
|
|
274
|
-
// and find nothing, or is there nothing to look for yet.
|
|
245
|
+
// ─── The public-entry-point export ─────────────────────────────
|
|
275
246
|
//
|
|
276
|
-
// `lookupPublicRecord`
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
// rather than raise, and this function adds no new path to the network or the
|
|
282
|
-
// parser.
|
|
247
|
+
// `lookupPublicRecord` adds a fourth state the CLI's three-state `WitnessLine`
|
|
248
|
+
// cannot name: a pack that names no witness at all is not "not checked" (a
|
|
249
|
+
// lookup that found nothing), it is "nothing on the public record to check". It
|
|
250
|
+
// calls `onChainHalf` and `witnessLine` rather than reimplementing them, so the
|
|
251
|
+
// network call and the verdict arithmetic live in one place.
|
|
283
252
|
|
|
284
|
-
/** The
|
|
253
|
+
/** The states a lookup can carry: the CLI's `WitnessState` plus `no_public_record`. */
|
|
285
254
|
export type PublicRecordState = WitnessState | "no_public_record";
|
|
286
255
|
|
|
287
256
|
/**
|
|
288
|
-
* One pack
|
|
289
|
-
*
|
|
290
|
-
* of the pack, per proof#200's acceptance criteria, rather than as a
|
|
291
|
-
* separately-loaded side file. `null` and `undefined` both mean "this pack has
|
|
292
|
-
* no witness-cache.json", the same reading `readWitnessCache` gives a missing
|
|
293
|
-
* file.
|
|
257
|
+
* One pack: the parsed `bundle.json` and, where it has one, the raw bytes of its
|
|
258
|
+
* `witness-cache.json`. `null` and `undefined` both mean no cache file.
|
|
294
259
|
*/
|
|
295
260
|
export interface PublicRecordPack {
|
|
296
261
|
/** How this pack is named in the result; carries no meaning beyond that. */
|
|
@@ -303,36 +268,27 @@ export interface PublicRecordResult {
|
|
|
303
268
|
state: PublicRecordState;
|
|
304
269
|
reasons: string[];
|
|
305
270
|
services: string[];
|
|
306
|
-
/** Witness times a pack caches that no lookup settled (5.6.6
|
|
271
|
+
/** Witness times a pack caches that no lookup settled (5.6.6). */
|
|
307
272
|
cached?: [number, number][];
|
|
308
273
|
/** Witness times the public record returned that matched a pack's cache. */
|
|
309
274
|
recorded?: [number, number][];
|
|
310
|
-
/** The line as
|
|
275
|
+
/** The line as the CLI and a page print it. */
|
|
311
276
|
rendered: string;
|
|
312
277
|
}
|
|
313
278
|
|
|
314
279
|
/**
|
|
315
|
-
* The public-record lookup the CLI runs with `--gateways`,
|
|
316
|
-
* consumer that is not the CLI can run the SAME lookup rather than copy it
|
|
317
|
-
* (proof#200; the copy this closes is verify-page#70's `src/lib/verify.ts`).
|
|
280
|
+
* The public-record lookup the CLI runs with `--gateways`, for non-CLI callers.
|
|
318
281
|
*
|
|
319
|
-
* `packs` is every pack in the drop
|
|
320
|
-
*
|
|
321
|
-
* `not_checked` or `no_public_record`. `options.fetchImpl` is the one seam a
|
|
322
|
-
* network call goes through, a browser page supplies its own `fetchImpl`;
|
|
323
|
-
* with none given this falls back to the ambient global `fetch`, so a
|
|
324
|
-
* browser page and a test can each supply their own and Node's `fetch` is
|
|
325
|
-
* never assumed where a caller passed something else.
|
|
282
|
+
* `packs` is every pack in the drop; `gateways` is empty for an offline reading,
|
|
283
|
+
* which makes no network call and answers `not_checked` or `no_public_record`.
|
|
326
284
|
*
|
|
327
|
-
* `state` is `confirmed` only about the
|
|
328
|
-
* pack names. It says nothing about whether the pack itself verifies
|
|
329
|
-
*
|
|
330
|
-
* `confirmed` apart from the pack's own verdict (proof#201).
|
|
285
|
+
* `state` is `confirmed` only about the bytes: the public record holds what a
|
|
286
|
+
* pack names. It says nothing about whether the pack itself verifies, so a
|
|
287
|
+
* caller must never show `confirmed` apart from the pack's own verdict.
|
|
331
288
|
*
|
|
332
|
-
* Never throws. Every input
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
* `not_checked` rather than escape as an exception.
|
|
289
|
+
* Never throws. Every input (a malformed pack, a non-string gateway, a
|
|
290
|
+
* misbehaving `fetchImpl`) is untrusted, and anything it cannot make sense of
|
|
291
|
+
* settles as `not_checked`.
|
|
336
292
|
*/
|
|
337
293
|
export async function lookupPublicRecord(
|
|
338
294
|
packs: readonly PublicRecordPack[],
|
|
@@ -340,10 +296,7 @@ export async function lookupPublicRecord(
|
|
|
340
296
|
options: WitnessLookupOptions = {},
|
|
341
297
|
): Promise<PublicRecordResult> {
|
|
342
298
|
try {
|
|
343
|
-
//
|
|
344
|
-
// check, which is not the same claim as "the packs it handed in name no
|
|
345
|
-
// witness" (`no_public_record`, below). Checked first, and unconditionally,
|
|
346
|
-
// so a caller than mishandles an empty drop never reads it as confirmed.
|
|
299
|
+
// Zero packs is the caller handing in nothing, not packs that name no witness (`no_public_record`). Checked first so an empty drop never reads as confirmed.
|
|
347
300
|
if (packs.length === 0) {
|
|
348
301
|
const reasons = [...WITNESS_NOT_LOOKED_UP.reasons];
|
|
349
302
|
return {
|
|
@@ -363,23 +316,12 @@ export async function lookupPublicRecord(
|
|
|
363
316
|
const onChain = await onChainHalfForWitnessCount(rows, gateways, options);
|
|
364
317
|
const line = witnessLine(onChain.half, caches.flat());
|
|
365
318
|
|
|
366
|
-
//
|
|
367
|
-
//
|
|
368
|
-
//
|
|
369
|
-
//
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
// a second reading of the bundle.
|
|
373
|
-
//
|
|
374
|
-
// A pack that does NOT verify (a broken wrapper signature, `bundle: null`,
|
|
375
|
-
// garbage, an unrecognised body type) also names zero witnesses, by
|
|
376
|
-
// construction: `verifyEvidenceBundle` never reports a checkpoint or a
|
|
377
|
-
// stamp for a bundle it could not authenticate. That is not "nothing to
|
|
378
|
-
// check yet", it is a pack this lookup could not read, so `no_public_record`
|
|
379
|
-
// is reserved for a pack whose wrapper genuinely verifies
|
|
380
|
-
// (`signatureOk && bodyHashOk && status !== "failed"`) and that carries no
|
|
381
|
-
// cached witness time either (a cache entry names something to check).
|
|
382
|
-
// Anything else that would otherwise qualify reads `not_checked` instead.
|
|
319
|
+
// `no_public_record`: every pack names zero witnesses, so nothing could be
|
|
320
|
+
// confirmed or contradicted. Reserved for packs whose wrapper verifies
|
|
321
|
+
// (`signatureOk && bodyHashOk && status !== "failed"`) and that carry no
|
|
322
|
+
// cached witness time: a pack that fails to verify also names zero
|
|
323
|
+
// witnesses, and that is a pack this lookup could not read, so it reads
|
|
324
|
+
// `not_checked`.
|
|
383
325
|
const noWitnesses = onChain.witnessCounts.every((n) => n === 0);
|
|
384
326
|
const everyPackVerifies = onChain.verifies.every((v) => v);
|
|
385
327
|
const noCacheRead = caches.every((c) => c.length === 0);
|
|
@@ -392,18 +334,12 @@ export async function lookupPublicRecord(
|
|
|
392
334
|
reasons: line.reasons,
|
|
393
335
|
services: line.services,
|
|
394
336
|
...(line.cached ? { cached: line.cached } : {}),
|
|
395
|
-
// `recorded`
|
|
396
|
-
// time; it never rides alongside anything but a `confirmed` verdict, so
|
|
397
|
-
// a pack whose wrapper does not verify can never surface a cached time
|
|
398
|
-
// as though the record had confirmed it.
|
|
337
|
+
// `recorded` rides only with a `confirmed` verdict, so a pack whose wrapper does not verify never surfaces a cached time as record-confirmed.
|
|
399
338
|
...(state === "confirmed" && line.recorded ? { recorded: line.recorded } : {}),
|
|
400
339
|
rendered: renderWitnessLine({ ...line, state: state === "no_public_record" ? "not_checked" : state }),
|
|
401
340
|
};
|
|
402
341
|
} catch (e) {
|
|
403
|
-
// Untrusted input all the way down
|
|
404
|
-
// caller's `fetchImpl`. Anything this function or what it calls cannot
|
|
405
|
-
// make sense of settles `not_checked` rather than throw out of an export a
|
|
406
|
-
// browser page calls directly.
|
|
342
|
+
// Untrusted input all the way down (a gateway's answer, a pack's shape, a `fetchImpl`): settle `not_checked` rather than throw out of an export a page calls directly.
|
|
407
343
|
const reason =
|
|
408
344
|
e instanceof Error ? `the lookup did not settle: ${e.message}` : "the lookup did not settle";
|
|
409
345
|
const reasons = [reason];
|
|
@@ -416,15 +352,9 @@ export async function lookupPublicRecord(
|
|
|
416
352
|
}
|
|
417
353
|
}
|
|
418
354
|
|
|
419
|
-
//
|
|
420
|
-
//
|
|
421
|
-
// `
|
|
422
|
-
// OFFLINE reading. This wrapper reads each pack's witness count, and whether
|
|
423
|
-
// its wrapper verifies, the same way `onChainHalf` does, via
|
|
424
|
-
// `verifyEvidenceBundle`: an OFFLINE parse (`gateways: []`), so it costs no
|
|
425
|
-
// extra request whether or not a lookup runs, and `verifies` is never itself
|
|
426
|
-
// a statement about the public record, only about the pack's own signature
|
|
427
|
-
// and `body_hash`.
|
|
355
|
+
// Reads each pack's witness count and whether its wrapper verifies, via an
|
|
356
|
+
// offline `verifyEvidenceBundle` parse (`gateways: []`), so it costs no request.
|
|
357
|
+
// `verifies` is about the pack's own signature and `body_hash`, never the public record.
|
|
428
358
|
async function onChainHalfForWitnessCount(
|
|
429
359
|
packs: readonly (readonly [string, unknown, readonly CachedWitnessTime[]])[],
|
|
430
360
|
gateways: readonly string[],
|
package/src/zip-reader.ts
CHANGED
|
@@ -1,42 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The one ZIP reader `case` uses, for a case file named on the command line.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* a central directory that contradicts a local header, a name that is not
|
|
8
|
-
* UTF-8, data after the end of a deflate stream. Where either library would
|
|
9
|
-
* have made a choice, this module refuses instead.
|
|
4
|
+
* Node has no ZIP reader. This one refuses where two libraries would disagree
|
|
5
|
+
* (evidence-bundle.md 5.6.1). It reads the central directory and each entry
|
|
6
|
+
* through its local header, stored and deflate only.
|
|
10
7
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* reads, entry by entry: an encrypted entry, a symbolic link, a method other
|
|
20
|
-
* than stored or deflate, and a name that is absolute, holds a backslash or a
|
|
21
|
-
* NUL, or has an empty, `.` or `..` segment.
|
|
8
|
+
* It refuses the whole archive for: no end-of-central-directory record, ZIP64,
|
|
9
|
+
* more than one disk, an entry placed outside the archive, a local header that
|
|
10
|
+
* contradicts its central entry, a size or CRC-32 that does not match, two
|
|
11
|
+
* entries with one name, a name that is not UTF-8, more than MAX_ZIP_ENTRIES
|
|
12
|
+
* entries or MAX_ZIP_BYTES bytes, or a name nested deeper than MAX_ZIP_DEPTH.
|
|
13
|
+
* It reports without reading, per entry: an encrypted entry, a symbolic link,
|
|
14
|
+
* another method, and a name that is absolute, holds a backslash or NUL, or has
|
|
15
|
+
* an empty, `.` or `..` segment.
|
|
22
16
|
*
|
|
23
|
-
* Pure: bytes in, entries out.
|
|
24
|
-
* splits.
|
|
17
|
+
* Pure: bytes in, entries out.
|
|
25
18
|
*/
|
|
26
19
|
|
|
27
20
|
import { inflateRawSync } from "node:zlib";
|
|
28
21
|
|
|
29
22
|
import { utf8Strict } from "./verifier.js";
|
|
30
23
|
|
|
31
|
-
/** More entries than this refuse the archive
|
|
24
|
+
/** More entries than this refuse the archive. */
|
|
32
25
|
export const MAX_ZIP_ENTRIES = 50_000;
|
|
33
26
|
/** More uncompressed bytes than this, across every entry, refuse the archive. */
|
|
34
27
|
export const MAX_ZIP_BYTES = 2 ** 30;
|
|
35
28
|
/**
|
|
36
29
|
* An archive file larger than this is refused before it is read: the
|
|
37
|
-
* uncompressed cap
|
|
38
|
-
*
|
|
39
|
-
* size on disk, so the archive is never read whole to find out it is too big.
|
|
30
|
+
* uncompressed cap plus room for headers and framing. The caller checks the
|
|
31
|
+
* size on disk.
|
|
40
32
|
*/
|
|
41
33
|
export const MAX_ZIP_ARCHIVE_BYTES = MAX_ZIP_BYTES + 256 * 2 ** 20;
|
|
42
34
|
/** A name nested deeper than this many folders refuses the archive. */
|
|
@@ -49,7 +41,7 @@ const S_IFMT = 0o170000;
|
|
|
49
41
|
const S_IFLNK = 0o120000;
|
|
50
42
|
const UNIX = 3;
|
|
51
43
|
|
|
52
|
-
/** The archive cannot be read as a case file. `message` is the whole report
|
|
44
|
+
/** The archive cannot be read as a case file. `message` is the whole report. */
|
|
53
45
|
export class ZipReadError extends Error {
|
|
54
46
|
constructor(message: string) {
|
|
55
47
|
super(message);
|
|
@@ -57,10 +49,7 @@ export class ZipReadError extends Error {
|
|
|
57
49
|
}
|
|
58
50
|
}
|
|
59
51
|
|
|
60
|
-
/**
|
|
61
|
-
* One file entry. `refused` is why the entry is reported and not read, or
|
|
62
|
-
* `null`; `data` is its bytes where it was read.
|
|
63
|
-
*/
|
|
52
|
+
/** One file entry. `refused` is why it is reported and not read, or `null`; `data` is set where it was read. */
|
|
64
53
|
export interface ZipEntry {
|
|
65
54
|
name: string;
|
|
66
55
|
refused: string | null;
|
|
@@ -101,18 +90,13 @@ function sameBytes(a: Uint8Array, b: Uint8Array): boolean {
|
|
|
101
90
|
return true;
|
|
102
91
|
}
|
|
103
92
|
|
|
104
|
-
/**
|
|
105
|
-
* Every FILE entry of a ZIP archive, in central-directory order. A directory
|
|
106
|
-
* entry (a name ending in `/`) is not a file and is not returned. Throws
|
|
107
|
-
* `ZipReadError`.
|
|
108
|
-
*/
|
|
93
|
+
/** Every file entry of a ZIP archive, in central-directory order (directory entries are skipped). Throws `ZipReadError`. */
|
|
109
94
|
export function readZip(data: Uint8Array): ZipEntry[] {
|
|
110
95
|
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
111
96
|
const n = data.length;
|
|
112
97
|
const u32 = (at: number) => view.getUint32(at, true);
|
|
113
98
|
const u16 = (at: number) => view.getUint16(at, true);
|
|
114
|
-
//
|
|
115
|
-
// 65535, searched backwards so the LAST signature wins.
|
|
99
|
+
// 22 bytes plus a comment of at most 65535, searched backwards so the last signature wins.
|
|
116
100
|
let eocd = -1;
|
|
117
101
|
const lowest = Math.max(0, n - 22 - 0xffff);
|
|
118
102
|
for (let at = n - 22; at >= lowest; at--) {
|
|
@@ -214,9 +198,7 @@ export function readZip(data: Uint8Array): ZipEntry[] {
|
|
|
214
198
|
if (method === 0) {
|
|
215
199
|
body = packed;
|
|
216
200
|
} else {
|
|
217
|
-
//
|
|
218
|
-
// input bytes the stream consumed. Anything left over is data after the
|
|
219
|
-
// end of the deflate stream, which Python's `unused_data` refuses too.
|
|
201
|
+
// Bytes left over after the end of the deflate stream are refused.
|
|
220
202
|
let result: { buffer: Buffer; engine: { bytesWritten: number } };
|
|
221
203
|
try {
|
|
222
204
|
result = inflateRawSync(packed, {
|