@sanning/proof 0.16.0 → 0.18.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 +49 -4
- package/dist/agent-proof.js +50 -23
- package/dist/case-file.d.ts +24 -4
- package/dist/case-file.js +62 -5
- package/dist/case.d.ts +104 -8
- package/dist/case.js +195 -53
- package/dist/cli.d.ts +105 -2
- package/dist/cli.js +664 -123
- package/dist/compose.js +32 -0
- package/dist/crypto.d.ts +17 -0
- package/dist/crypto.js +116 -16
- package/dist/evidence.js +108 -38
- package/dist/file-match.d.ts +64 -5
- package/dist/file-match.js +113 -22
- package/dist/grading.d.ts +7 -0
- package/dist/grading.js +14 -2
- package/dist/index.d.ts +5 -5
- package/dist/index.js +3 -3
- package/dist/merkle.js +13 -7
- package/dist/verifier.js +32 -23
- package/dist/zip-reader.d.ts +7 -0
- package/dist/zip-reader.js +7 -0
- package/package.json +2 -2
- package/src/agent-proof.ts +53 -27
- package/src/case-file.ts +61 -8
- package/src/case.ts +293 -64
- package/src/cli.ts +703 -132
- package/src/compose.ts +33 -0
- package/src/crypto.ts +125 -16
- package/src/evidence.ts +109 -43
- package/src/file-match.ts +160 -26
- package/src/grading.ts +15 -2
- package/src/index.ts +7 -0
- package/src/merkle.ts +13 -7
- package/src/verifier.ts +33 -25
- package/src/zip-reader.ts +7 -0
- package/vectors/README.md +5 -1
- package/vectors/case-file-index.json +88 -2
- package/vectors/file-match.json +135 -1
package/dist/file-match.js
CHANGED
|
@@ -60,6 +60,26 @@ export const NOT_READ_REASONS = [
|
|
|
60
60
|
"handoff_record_malformed",
|
|
61
61
|
"message_withheld",
|
|
62
62
|
];
|
|
63
|
+
/**
|
|
64
|
+
* Why a file was named in the report and never hashed (5.7.5). A skipped file
|
|
65
|
+
* is listed, never dropped: every file a command found is matched, unmatched or
|
|
66
|
+
* skipped, so "no match" is never read as "no file was there".
|
|
67
|
+
*/
|
|
68
|
+
export const SKIPPED_REASONS = [
|
|
69
|
+
"symbolic_link",
|
|
70
|
+
"not_regular",
|
|
71
|
+
"unreadable",
|
|
72
|
+
"too_large",
|
|
73
|
+
"refused",
|
|
74
|
+
];
|
|
75
|
+
const SKIPPED_TEXT = {
|
|
76
|
+
symbolic_link: "a symbolic link: not followed, not read",
|
|
77
|
+
not_regular: "not a regular file: not read",
|
|
78
|
+
unreadable: "could not be read",
|
|
79
|
+
too_large: "past the 1 GiB read from folders in one run: not read",
|
|
80
|
+
// Followed by the reader's own words, `renderSkipped`.
|
|
81
|
+
refused: "a .zip entry the reader refused",
|
|
82
|
+
};
|
|
63
83
|
const REASON_TEXT = {
|
|
64
84
|
no_records: "this bundle carries no records this reading can read",
|
|
65
85
|
record_unreadable: "its record bytes cannot be read",
|
|
@@ -124,17 +144,17 @@ async function messageOf(ev, eventId, messageHash, content) {
|
|
|
124
144
|
}
|
|
125
145
|
return ABSENT;
|
|
126
146
|
}
|
|
127
|
-
async function readPack(bundle, content, packRef) {
|
|
147
|
+
async function readPack(bundle, content, packRef, packIndex) {
|
|
128
148
|
const commitments = [];
|
|
129
149
|
const notRead = [];
|
|
130
150
|
const events = resolveTraceEvents(bundle);
|
|
131
151
|
if (events === undefined) {
|
|
132
|
-
notRead.push({ packRef, eventId: null, reason: "no_records" });
|
|
152
|
+
notRead.push({ packRef, eventId: null, reason: "no_records", packIndex, recordIndex: null });
|
|
133
153
|
return { commitments, notRead };
|
|
134
154
|
}
|
|
135
|
-
for (const ev of events) {
|
|
155
|
+
for (const [recordIndex, ev] of events.entries()) {
|
|
136
156
|
if (!isObject(ev)) {
|
|
137
|
-
notRead.push({ packRef, eventId: null, reason: "record_unreadable" });
|
|
157
|
+
notRead.push({ packRef, eventId: null, reason: "record_unreadable", packIndex, recordIndex });
|
|
138
158
|
continue;
|
|
139
159
|
}
|
|
140
160
|
const envelope = own(ev, "envelope");
|
|
@@ -144,7 +164,7 @@ async function readPack(bundle, content, packRef) {
|
|
|
144
164
|
if (rawBytes !== undefined && rawBytes !== null) {
|
|
145
165
|
eventBytes = typeof rawBytes === "string" ? decode(rawBytes) : undefined;
|
|
146
166
|
if (eventBytes === undefined) {
|
|
147
|
-
notRead.push({ packRef, eventId, reason: "record_unreadable" });
|
|
167
|
+
notRead.push({ packRef, eventId, reason: "record_unreadable", packIndex, recordIndex });
|
|
148
168
|
continue;
|
|
149
169
|
}
|
|
150
170
|
}
|
|
@@ -153,15 +173,15 @@ async function readPack(bundle, content, packRef) {
|
|
|
153
173
|
acceptedSpecMajors: RECORD_PROFILES,
|
|
154
174
|
});
|
|
155
175
|
if (!result.ok || !isObject(envelope) || !envelopeSpelled(envelope)) {
|
|
156
|
-
notRead.push({ packRef, eventId, reason: "record_not_verified" });
|
|
176
|
+
notRead.push({ packRef, eventId, reason: "record_not_verified", packIndex, recordIndex });
|
|
157
177
|
continue;
|
|
158
178
|
}
|
|
159
179
|
if (eventBytes === undefined) {
|
|
160
|
-
notRead.push({ packRef, eventId, reason: "record_withheld" });
|
|
180
|
+
notRead.push({ packRef, eventId, reason: "record_withheld", packIndex, recordIndex });
|
|
161
181
|
continue;
|
|
162
182
|
}
|
|
163
183
|
if (result.payloadHashOk !== true) {
|
|
164
|
-
notRead.push({ packRef, eventId, reason: "record_not_verified" });
|
|
184
|
+
notRead.push({ packRef, eventId, reason: "record_not_verified", packIndex, recordIndex });
|
|
165
185
|
continue;
|
|
166
186
|
}
|
|
167
187
|
let record;
|
|
@@ -172,7 +192,7 @@ async function readPack(bundle, content, packRef) {
|
|
|
172
192
|
record = undefined;
|
|
173
193
|
}
|
|
174
194
|
if (!isObject(record)) {
|
|
175
|
-
notRead.push({ packRef, eventId, reason: "record_unreadable" });
|
|
195
|
+
notRead.push({ packRef, eventId, reason: "record_unreadable", packIndex, recordIndex });
|
|
176
196
|
continue;
|
|
177
197
|
}
|
|
178
198
|
const key = own(envelope, "public_key");
|
|
@@ -188,12 +208,12 @@ async function readPack(bundle, content, packRef) {
|
|
|
188
208
|
if (isHandoffNamespace(held)) {
|
|
189
209
|
const messageHash = own(event, "message_hash");
|
|
190
210
|
if (!isConformantHandoffRecord(held) || !isHexField(messageHash, 64, true)) {
|
|
191
|
-
notRead.push({ packRef, eventId, reason: "handoff_record_malformed" });
|
|
211
|
+
notRead.push({ packRef, eventId, reason: "handoff_record_malformed", packIndex, recordIndex });
|
|
192
212
|
continue;
|
|
193
213
|
}
|
|
194
214
|
const message = await messageOf(ev, eventId, messageHash, content);
|
|
195
215
|
if (message === ABSENT) {
|
|
196
|
-
notRead.push({ packRef, eventId, reason: "message_withheld" });
|
|
216
|
+
notRead.push({ packRef, eventId, reason: "message_withheld", packIndex, recordIndex });
|
|
197
217
|
continue;
|
|
198
218
|
}
|
|
199
219
|
const parts = isObject(message) ? own(message, "parts") : undefined;
|
|
@@ -209,6 +229,8 @@ async function readPack(bundle, content, packRef) {
|
|
|
209
229
|
contentHash: (part.content_hash).toLowerCase(),
|
|
210
230
|
signer,
|
|
211
231
|
signedAt,
|
|
232
|
+
packIndex,
|
|
233
|
+
recordIndex,
|
|
212
234
|
});
|
|
213
235
|
});
|
|
214
236
|
}
|
|
@@ -216,7 +238,7 @@ async function readPack(bundle, content, packRef) {
|
|
|
216
238
|
}
|
|
217
239
|
const contentHash = own(event, "content_hash");
|
|
218
240
|
if (!isHexField(contentHash, 64)) {
|
|
219
|
-
notRead.push({ packRef, eventId, reason: "no_content_hash" });
|
|
241
|
+
notRead.push({ packRef, eventId, reason: "no_content_hash", packIndex, recordIndex });
|
|
220
242
|
continue;
|
|
221
243
|
}
|
|
222
244
|
commitments.push({
|
|
@@ -227,6 +249,8 @@ async function readPack(bundle, content, packRef) {
|
|
|
227
249
|
contentHash: contentHash.toLowerCase(),
|
|
228
250
|
signer,
|
|
229
251
|
signedAt,
|
|
252
|
+
packIndex,
|
|
253
|
+
recordIndex,
|
|
230
254
|
});
|
|
231
255
|
}
|
|
232
256
|
return { commitments, notRead };
|
|
@@ -238,9 +262,16 @@ async function offeredDigests(files) {
|
|
|
238
262
|
for (const entry of files) {
|
|
239
263
|
if (!isObject(entry))
|
|
240
264
|
throw new TypeError("matchFiles: each file is an object {name, bytes}");
|
|
241
|
-
const { name, bytes } = entry;
|
|
265
|
+
const { name, bytes, sha256 } = entry;
|
|
242
266
|
if (typeof name !== "string")
|
|
243
267
|
throw new TypeError("matchFiles: a file's name is a string");
|
|
268
|
+
if (sha256 !== undefined && sha256 !== null) {
|
|
269
|
+
if ((bytes !== undefined && bytes !== null) || !isHexField(sha256, 64, true)) {
|
|
270
|
+
throw new TypeError("matchFiles: a file is {name, bytes} or {name, sha256}, and a sha256 is 64 lowercase hex characters");
|
|
271
|
+
}
|
|
272
|
+
out.push([name, sha256]);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
244
275
|
if (!(bytes instanceof Uint8Array)) {
|
|
245
276
|
throw new TypeError("matchFiles: a file's bytes are a Uint8Array");
|
|
246
277
|
}
|
|
@@ -275,6 +306,7 @@ async function match(commitments, notRead, files) {
|
|
|
275
306
|
notOffered: commitments.filter((_, i) => !used.has(i)),
|
|
276
307
|
notRead,
|
|
277
308
|
scope: FILE_MATCH_SCOPE,
|
|
309
|
+
skipped: [],
|
|
278
310
|
};
|
|
279
311
|
}
|
|
280
312
|
/**
|
|
@@ -284,19 +316,19 @@ async function match(commitments, notRead, files) {
|
|
|
284
316
|
* never an offered file.
|
|
285
317
|
*/
|
|
286
318
|
export async function matchFiles(bundle, files, content) {
|
|
287
|
-
const { commitments, notRead } = await readPack(bundle, content, null);
|
|
319
|
+
const { commitments, notRead } = await readPack(bundle, content, null, null);
|
|
288
320
|
return match(commitments, notRead, files);
|
|
289
321
|
}
|
|
290
322
|
/** The same reading over every pack of a case, each commitment naming its pack's `ref`. */
|
|
291
323
|
export async function matchCaseFiles(packs, files) {
|
|
292
324
|
const commitments = [];
|
|
293
325
|
const notRead = [];
|
|
294
|
-
for (const pack of packs) {
|
|
326
|
+
for (const [packIndex, pack] of packs.entries()) {
|
|
295
327
|
if (!isObject(pack))
|
|
296
328
|
throw new TypeError("matchCaseFiles: each pack is an object {ref, bundle}");
|
|
297
329
|
if (typeof pack.ref !== "string")
|
|
298
330
|
throw new TypeError("matchCaseFiles: a pack's ref is a string");
|
|
299
|
-
const r = await readPack(pack.bundle, pack.content, pack.ref);
|
|
331
|
+
const r = await readPack(pack.bundle, pack.content, pack.ref, packIndex);
|
|
300
332
|
commitments.push(...r.commitments);
|
|
301
333
|
notRead.push(...r.notRead);
|
|
302
334
|
}
|
|
@@ -305,7 +337,9 @@ export async function matchCaseFiles(packs, files) {
|
|
|
305
337
|
function commitmentJson(c) {
|
|
306
338
|
return {
|
|
307
339
|
pack_ref: c.packRef,
|
|
340
|
+
pack_index: c.packIndex,
|
|
308
341
|
event_id: c.eventId,
|
|
342
|
+
record_index: c.recordIndex,
|
|
309
343
|
kind: c.kind,
|
|
310
344
|
part_index: c.partIndex,
|
|
311
345
|
content_hash: c.contentHash,
|
|
@@ -315,6 +349,7 @@ function commitmentJson(c) {
|
|
|
315
349
|
}
|
|
316
350
|
/** The reading as the `files` member of `--json`, identical in both kernels. */
|
|
317
351
|
export function fileMatchToJson(reading) {
|
|
352
|
+
const skipped = reading.skipped ?? [];
|
|
318
353
|
return {
|
|
319
354
|
offered: reading.offered,
|
|
320
355
|
matched: reading.matched.map((m) => ({
|
|
@@ -326,13 +361,32 @@ export function fileMatchToJson(reading) {
|
|
|
326
361
|
not_offered: reading.notOffered.map(commitmentJson),
|
|
327
362
|
not_read: reading.notRead.map((n) => ({
|
|
328
363
|
pack_ref: n.packRef,
|
|
364
|
+
pack_index: n.packIndex,
|
|
329
365
|
event_id: n.eventId,
|
|
366
|
+
record_index: n.recordIndex,
|
|
330
367
|
reason: n.reason,
|
|
331
368
|
rendered: renderNotRead(n),
|
|
332
369
|
})),
|
|
370
|
+
// Present only when something was skipped, so a reading with no skipped
|
|
371
|
+
// file is the object it was before a folder could be offered (A40).
|
|
372
|
+
...(skipped.length > 0
|
|
373
|
+
? {
|
|
374
|
+
skipped: skipped.map((k) => ({
|
|
375
|
+
name: k.name,
|
|
376
|
+
reason: k.reason,
|
|
377
|
+
...(k.detail !== undefined ? { detail: k.detail } : {}),
|
|
378
|
+
rendered: renderSkipped(k),
|
|
379
|
+
})),
|
|
380
|
+
}
|
|
381
|
+
: {}),
|
|
333
382
|
scope: reading.scope,
|
|
334
383
|
};
|
|
335
384
|
}
|
|
385
|
+
function renderSkipped(k) {
|
|
386
|
+
if (k.detail !== undefined)
|
|
387
|
+
return `${k.name} (${SKIPPED_TEXT[k.reason]}: ${k.detail}; not read)`;
|
|
388
|
+
return `${k.name} (${SKIPPED_TEXT[k.reason]})`;
|
|
389
|
+
}
|
|
336
390
|
function recordName(eventId) {
|
|
337
391
|
return eventId !== null ? `record ${eventId}` : "a record with no event_id";
|
|
338
392
|
}
|
|
@@ -350,12 +404,41 @@ function renderNotRead(n) {
|
|
|
350
404
|
return `no record read${where}: ${REASON_TEXT[n.reason]}`;
|
|
351
405
|
return `${recordName(n.eventId)}${where} not read: ${REASON_TEXT[n.reason]}`;
|
|
352
406
|
}
|
|
353
|
-
/**
|
|
354
|
-
|
|
407
|
+
/**
|
|
408
|
+
* The commitments no offered file matched and no disclosed log answers.
|
|
409
|
+
*
|
|
410
|
+
* `contents` is each pack's side content by `pack_ref`. A record whose own pack
|
|
411
|
+
* discloses bytes that hash to its `content_hash` is accounted for by the pack,
|
|
412
|
+
* and is not returned. Only the text report's count reads this; the reading, and
|
|
413
|
+
* `--json`, still list every commitment no file matched.
|
|
414
|
+
*/
|
|
415
|
+
export async function notAccountedFor(reading, contents) {
|
|
416
|
+
const out = [];
|
|
417
|
+
for (const c of reading.notOffered) {
|
|
418
|
+
const held = c.packRef !== null ? contents.get(c.packRef) : undefined;
|
|
419
|
+
const raw = c.kind === "record" && held !== undefined && held !== null && c.eventId !== null
|
|
420
|
+
? decode(sideContentEntry(held, c.eventId).value)
|
|
421
|
+
: undefined;
|
|
422
|
+
if (raw !== undefined && (await sha256Hex(raw)) === c.contentHash)
|
|
423
|
+
continue;
|
|
424
|
+
out.push(c);
|
|
425
|
+
}
|
|
426
|
+
return out;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* The text report's lines for the reading, identical in both kernels.
|
|
430
|
+
*
|
|
431
|
+
* By default every commitment no file matched is a line. `notAmong` is the case
|
|
432
|
+
* command's report when no `--files` was given: one count line for the recorded
|
|
433
|
+
* files no loose file matched, and no line for each (A41).
|
|
434
|
+
*/
|
|
435
|
+
export function renderFileMatch(reading, notAmong) {
|
|
436
|
+
const skipped = reading.skipped ?? [];
|
|
355
437
|
const lines = [
|
|
356
438
|
`files ${reading.offered} offered: ${reading.matched.length} vouched for, ` +
|
|
357
|
-
`${reading.unmatched.length} match no record
|
|
358
|
-
|
|
439
|
+
`${reading.unmatched.length} match no record` +
|
|
440
|
+
(notAmong === undefined ? `; ${reading.notOffered.length} commitment(s) had no file offered` : "") +
|
|
441
|
+
(skipped.length > 0 ? `; ${skipped.length} skipped, not read` : ""),
|
|
359
442
|
];
|
|
360
443
|
for (const m of reading.matched) {
|
|
361
444
|
lines.push(` vouched for ${m.name} sha256 ${m.sha256}`);
|
|
@@ -364,8 +447,16 @@ export function renderFileMatch(reading) {
|
|
|
364
447
|
}
|
|
365
448
|
for (const u of reading.unmatched)
|
|
366
449
|
lines.push(` no match ${u.name} sha256 ${u.sha256}`);
|
|
367
|
-
for (const
|
|
368
|
-
lines.push(`
|
|
450
|
+
for (const k of skipped)
|
|
451
|
+
lines.push(` skipped ${renderSkipped(k)}`);
|
|
452
|
+
if (notAmong === undefined) {
|
|
453
|
+
for (const c of reading.notOffered) {
|
|
454
|
+
lines.push(` not offered ${c.contentHash}, ${renderCommitment(c)}`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
else if (notAmong > 0) {
|
|
458
|
+
lines.push(` ${notAmong} recorded commitment(s) matched no loose file and no log in their own pack ` +
|
|
459
|
+
"(--json lists every commitment no file matched, these included)");
|
|
369
460
|
}
|
|
370
461
|
for (const n of reading.notRead)
|
|
371
462
|
lines.push(` not read ${renderNotRead(n)}`);
|
package/dist/grading.d.ts
CHANGED
|
@@ -145,6 +145,13 @@ export interface PackGrade {
|
|
|
145
145
|
* bundle, precisely so the transport can say things the map cannot.
|
|
146
146
|
*/
|
|
147
147
|
export declare function accountDisclosure(bundle: unknown, content: SideContentMap): DisclosureAccount;
|
|
148
|
+
/**
|
|
149
|
+
* A JSON number read by its value: an integer within 2**53 - 1, or null.
|
|
150
|
+
* `Number.isSafeInteger`, mirrored by `_js_safe_integer` in
|
|
151
|
+
* `src/sanning_proof/chain.py`, so `2.0` is 2 in both kernels and
|
|
152
|
+
* `9007199254740993`, which this parser rounds, is unreadable in both.
|
|
153
|
+
*/
|
|
154
|
+
export declare function safeInteger(value: unknown): number | null;
|
|
148
155
|
/** The `leaf_count` a checkpoint's SIGNED record commits to, or null if unreadable. */
|
|
149
156
|
export declare function signedLeafCount(checkpoint: {
|
|
150
157
|
event_bytes?: unknown;
|
package/dist/grading.js
CHANGED
|
@@ -323,6 +323,16 @@ content) {
|
|
|
323
323
|
const extra = sideContentKeys(content).filter((id) => !accounted.has(id));
|
|
324
324
|
return { expected, present, missing, unreadable, extra };
|
|
325
325
|
}
|
|
326
|
+
/**
|
|
327
|
+
* A JSON number read by its value: an integer within 2**53 - 1, or null.
|
|
328
|
+
* `Number.isSafeInteger`, mirrored by `_js_safe_integer` in
|
|
329
|
+
* `src/sanning_proof/chain.py`, so `2.0` is 2 in both kernels and
|
|
330
|
+
* `9007199254740993`, which this parser rounds, is unreadable in both.
|
|
331
|
+
*/
|
|
332
|
+
export function safeInteger(value) {
|
|
333
|
+
// `+ 0` folds -0 into 0, which Python's `int(-0.0)` already is.
|
|
334
|
+
return typeof value === "number" && Number.isSafeInteger(value) ? value + 0 : null;
|
|
335
|
+
}
|
|
326
336
|
/** The `leaf_count` a checkpoint's SIGNED record commits to, or null if unreadable. */
|
|
327
337
|
export function signedLeafCount(checkpoint) {
|
|
328
338
|
const hex = checkpoint?.event_bytes;
|
|
@@ -336,8 +346,10 @@ export function signedLeafCount(checkpoint) {
|
|
|
336
346
|
// `BOM + JCS(checkpoint)` yielded a signed leaf count here and unreadable
|
|
337
347
|
// there, and that count decides a completeness note (proof#140 gate 5).
|
|
338
348
|
const record = jcsParse(hexToBytes(hex));
|
|
339
|
-
|
|
340
|
-
|
|
349
|
+
// Read BY VALUE, the model JCS signs: a record spelling `3.0`, `3e0` or
|
|
350
|
+
// `30e-1` commits to 3, and since A38 (proof#197) this count decides a
|
|
351
|
+
// verdict, not only a note. Past 2**53 - 1 it is unreadable, as in Python.
|
|
352
|
+
return safeInteger(record?.event?.leaf_count);
|
|
341
353
|
}
|
|
342
354
|
catch {
|
|
343
355
|
return null;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { ACCEPTED_SPEC_MAJORS, contentHashes, jcs, specVersionSupported, verifyEnvelope, } from "./verifier.js";
|
|
2
|
-
export { bytesToHex, deriveOperatorAddress, ed25519Verify, hexToBytes, sha256Bytes, sha256Hex, utf8, verifyRsaPssSha256, } from "./crypto.js";
|
|
2
|
+
export { bytesToHex, deriveOperatorAddress, ed25519Refusal, ed25519Verify, hexToBytes, sha256Bytes, sha256Hex, utf8, verifyRsaPssSha256, } from "./crypto.js";
|
|
3
3
|
export { EMPTY_TREE_ROOT_HEX, auditPath, leafHash, merkleRoot, nodeHash, verifyInclusion, } from "./merkle.js";
|
|
4
4
|
export type { ContentRole, Envelope, Subject, VerificationResult, VerifyOptions } from "./types.js";
|
|
5
5
|
export { ACCEPTED_EVIDENCE_MAJORS, ANCHOR_TRACE_BODY_TYPE, CO_SIGNATURES_NOTE, EXPORT_BODY_TYPE, STAMP_BODY_VERSION, STAMP_TRACE_BODY_TYPE, STAMP_EVIDENCE_BODY_TYPE, coSignaturesPresent, disclosedContent, recomputeExportVerdict, verifyEvidenceBundle, } from "./evidence.js";
|
|
@@ -8,16 +8,16 @@ export { PACK_GRADE_CAUSE_KINDS, PACK_VERDICTS, accountDisclosure, completenessN
|
|
|
8
8
|
export type { CompletenessNote, DisclosureAccount, GradeInput, PackGrade, PackGradeCause, PackGradeCauseKind, PackVerdict, } from "./grading.js";
|
|
9
9
|
export { DECLARATION_CONTENT_KINDS, DECLARATION_DEVIATION_KINDS, DECLARATION_NOTE_KINDS, DECLARATION_ROLES, DECLARATION_VERDICTS, DECLARATION_VERSION, DECLARATION_VERSION_V2, DECLARATION_VERSIONS, checkDeclaration, declarationClaims, packRecords, renderContentFinding, renderDeclarationSummary, renderDeviation, renderNote, } from "./declaration.js";
|
|
10
10
|
export type { DeclarationCheck, DeclarationClaims, DeclarationContentFinding, DeclarationContentKind, DeclarationDeviation, DeclarationDeviationKind, DeclarationNote, DeclarationNoteKind, DeclarationRole, DeclarationVerdict, PackRecord, } from "./declaration.js";
|
|
11
|
-
export { CASE_BODY_TYPES, CASE_EXCEPTION_KINDS, CASE_QUARANTINED_PROFILES, CASE_RECORD_PROFILES, CASE_NOTE_KINDS, CASE_RELIANCE, CASE_RELIANCE_HANDOFF_CLAUSE, CASE_VERDICTS, CONTENT_WITHHELD_BY_DECLARATION, CO_SIGNATURES_PRESENT, VERDICT_DISPLAY_TITLES, WITNESS_NOT_LOOKED_UP, WITNESS_STATES, WITNESS_TIME_CACHED, WITNESS_TIME_RECORDED, caseReliance, readCase, renderCaseNote, renderCaseSummary, renderException, renderWitnessLine, } from "./case.js";
|
|
11
|
+
export { CASE_BODY_TYPES, CASE_EXCEPTION_KINDS, CASE_QUARANTINED_PROFILES, CASE_RECORD_PROFILES, CASE_RECORD_STATUSES, CASE_NOTE_KINDS, CASE_RELIANCE, CASE_RELIANCE_HANDOFF_CLAUSE, CASE_VERDICTS, CONTENT_WITHHELD_BY_DECLARATION, CO_SIGNATURES_PRESENT, VERDICT_DISPLAY_TITLES, WITNESS_NOT_LOOKED_UP, WITNESS_STATES, WITNESS_TIME_CACHED, WITNESS_TIME_RECORDED, caseReliance, readCase, renderCaseNote, renderCaseSummary, renderException, renderWitnessLine, } from "./case.js";
|
|
12
12
|
export { MAX_BLOCK_TIME, MAX_WITNESS_CACHE_DEPTH, WITNESS_CACHE_NAME, WITNESS_CACHE_VERSION, checkWitnessCache, formatBlockTime, readWitnessCache, renderWitnessTime, witnessTimeQuery, } from "./witness-cache.js";
|
|
13
13
|
export type { CachedWitnessTime, WitnessTimeAnswer, WitnessTimeCheck } from "./witness-cache.js";
|
|
14
14
|
export { lookupPublicRecord, onChainHalf, witnessLine } from "./witness-lookup.js";
|
|
15
15
|
export type { OnChainHalf, OnChainRow, PublicRecordPack, PublicRecordResult, PublicRecordState, WitnessLookupOptions, WitnessTimeRow, } from "./witness-lookup.js";
|
|
16
|
-
export type { CaseException, CaseExceptionKind, CaseNote, CaseNoteKind, CasePack, CasePackReading, CaseReading, CaseRecordReading, CaseVerdict, WitnessLine, WitnessState, } from "./case.js";
|
|
16
|
+
export type { CaseException, CaseExceptionKind, CaseNote, CaseNoteKind, CasePack, CasePackReading, CaseReading, CaseRecordReading, CaseRecordStatus, CaseVerdict, WitnessLine, WitnessState, } from "./case.js";
|
|
17
17
|
export { CASE_FILE_FINDING_KINDS, CASE_FILE_INDEX_NAME, CASE_FILE_VERSION, HOW_TO_VERIFY_NAME, REPORT_README_FIRST_LINE, howToVerifyText, isPackRef, readCaseFileIndex, renderCaseFileFinding, } from "./case-file.js";
|
|
18
18
|
export type { CaseFileFinding, CaseFileFindingKind, CaseFileIndexReading, } from "./case-file.js";
|
|
19
|
-
export { FILE_MATCH_SCOPE, NOT_READ_REASONS, fileMatchToJson, matchCaseFiles, matchFiles, renderFileMatch, } from "./file-match.js";
|
|
20
|
-
export type { FileCommitment, FileMatchPack, FileMatchReading, MatchedFile, NotRead, NotReadReason, OfferedFile, UnmatchedFile, } from "./file-match.js";
|
|
19
|
+
export { FILE_MATCH_SCOPE, NOT_READ_REASONS, SKIPPED_REASONS, fileMatchToJson, matchCaseFiles, matchFiles, notAccountedFor, renderFileMatch, } from "./file-match.js";
|
|
20
|
+
export type { FileCommitment, FileMatchPack, FileMatchReading, MatchedFile, NotRead, NotReadReason, OfferedFile, SkippedFile, SkippedReason, UnmatchedFile, } from "./file-match.js";
|
|
21
21
|
export { ADAPTER_CHAIN_NAMESPACES, CHAIN_READING_SCOPE, readChains, renderChainFinding, } from "./chain.js";
|
|
22
22
|
export type { ChainFinding, ChainFindingKind, ChainReading, ChainSource, ChainSummary, } from "./chain.js";
|
|
23
23
|
export { EXIT_CODES, EXIT_CODES_HELP, EXIT_FAILED, EXIT_MALFORMED, EXIT_MIXED, EXIT_UNDETERMINED, EXIT_VERIFIED, exitCodeFor, } from "./exit-codes.js";
|
package/dist/index.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
// mutable accept-set would be a runtime-widenable kernel. Additive — no
|
|
19
19
|
// existing export changes.
|
|
20
20
|
export { ACCEPTED_SPEC_MAJORS, contentHashes, jcs, specVersionSupported, verifyEnvelope, } from "./verifier.js";
|
|
21
|
-
export { bytesToHex, deriveOperatorAddress, ed25519Verify, hexToBytes, sha256Bytes, sha256Hex, utf8, verifyRsaPssSha256, } from "./crypto.js";
|
|
21
|
+
export { bytesToHex, deriveOperatorAddress, ed25519Refusal, ed25519Verify, hexToBytes, sha256Bytes, sha256Hex, utf8, verifyRsaPssSha256, } from "./crypto.js";
|
|
22
22
|
export { EMPTY_TREE_ROOT_HEX, auditPath, leafHash, merkleRoot, nodeHash, verifyInclusion, } from "./merkle.js";
|
|
23
23
|
// Evidence-bundle verification (specs/evidence-bundle.md): the signed
|
|
24
24
|
// sanning.evidence/v1 wrapper carrying the sanning.anchor.trace/v3 body. The CLI
|
|
@@ -85,7 +85,7 @@ export { DECLARATION_CONTENT_KINDS, DECLARATION_DEVIATION_KINDS, DECLARATION_NOT
|
|
|
85
85
|
//
|
|
86
86
|
// Pure: data in, data out, no I/O. Additive: a new module, no existing export
|
|
87
87
|
// changes, and no new verdict word anywhere.
|
|
88
|
-
export { CASE_BODY_TYPES, CASE_EXCEPTION_KINDS, CASE_QUARANTINED_PROFILES, CASE_RECORD_PROFILES, CASE_NOTE_KINDS, CASE_RELIANCE, CASE_RELIANCE_HANDOFF_CLAUSE, CASE_VERDICTS, CONTENT_WITHHELD_BY_DECLARATION, CO_SIGNATURES_PRESENT, VERDICT_DISPLAY_TITLES, WITNESS_NOT_LOOKED_UP, WITNESS_STATES, WITNESS_TIME_CACHED, WITNESS_TIME_RECORDED, caseReliance, readCase, renderCaseNote, renderCaseSummary, renderException, renderWitnessLine, } from "./case.js";
|
|
88
|
+
export { CASE_BODY_TYPES, CASE_EXCEPTION_KINDS, CASE_QUARANTINED_PROFILES, CASE_RECORD_PROFILES, CASE_RECORD_STATUSES, CASE_NOTE_KINDS, CASE_RELIANCE, CASE_RELIANCE_HANDOFF_CLAUSE, CASE_VERDICTS, CONTENT_WITHHELD_BY_DECLARATION, CO_SIGNATURES_PRESENT, VERDICT_DISPLAY_TITLES, WITNESS_NOT_LOOKED_UP, WITNESS_STATES, WITNESS_TIME_CACHED, WITNESS_TIME_RECORDED, caseReliance, readCase, renderCaseNote, renderCaseSummary, renderException, renderWitnessLine, } from "./case.js";
|
|
89
89
|
// The witnessed-time cache (evidence-bundle 5.6.6, v1.25, A30): read it, look it
|
|
90
90
|
// up, print it. Never an input to a verdict.
|
|
91
91
|
export { MAX_BLOCK_TIME, MAX_WITNESS_CACHE_DEPTH, WITNESS_CACHE_NAME, WITNESS_CACHE_VERSION, checkWitnessCache, formatBlockTime, readWitnessCache, renderWitnessTime, witnessTimeQuery, } from "./witness-cache.js";
|
|
@@ -111,7 +111,7 @@ export { CASE_FILE_FINDING_KINDS, CASE_FILE_INDEX_NAME, CASE_FILE_VERSION, HOW_T
|
|
|
111
111
|
// Files handed in beside a pack (src/file-match.ts, evidence-bundle.md §5.7,
|
|
112
112
|
// A29, proof#92): each file's SHA-256 matched against the commitments the
|
|
113
113
|
// pack's SIGNED records make, by hash alone. Never moves a verdict.
|
|
114
|
-
export { FILE_MATCH_SCOPE, NOT_READ_REASONS, fileMatchToJson, matchCaseFiles, matchFiles, renderFileMatch, } from "./file-match.js";
|
|
114
|
+
export { FILE_MATCH_SCOPE, NOT_READ_REASONS, SKIPPED_REASONS, fileMatchToJson, matchCaseFiles, matchFiles, notAccountedFor, renderFileMatch, } from "./file-match.js";
|
|
115
115
|
// Chain reading (src/chain.ts): following the pointer every sanning.events/v2
|
|
116
116
|
// record carries to the record before it, over the records ONE bundle presents.
|
|
117
117
|
//
|
package/dist/merkle.js
CHANGED
|
@@ -66,26 +66,32 @@ export async function verifyInclusion(leaf, leafIndex, totalLeaves, auditPath, e
|
|
|
66
66
|
}
|
|
67
67
|
if (totalLeaves === 1)
|
|
68
68
|
return auditPath.length === 0 && bytesEqual(leaf, expectedRoot);
|
|
69
|
+
// Arithmetic, not bitwise: JavaScript's `&` and `>>=` truncate to 32 bits,
|
|
70
|
+
// so a tree past 2**31 leaves walked a different tree here than in the
|
|
71
|
+
// Python kernel, whose integers are unbounded. `% 2` and a halving floor
|
|
72
|
+
// are exact for every safe integer, and the stated index and count reach
|
|
73
|
+
// this walk as safe integers.
|
|
69
74
|
let fn = leafIndex;
|
|
70
75
|
let sn = totalLeaves - 1;
|
|
71
76
|
let r = leaf;
|
|
77
|
+
const half = (v) => Math.floor(v / 2);
|
|
72
78
|
for (const p of auditPath) {
|
|
73
79
|
if (sn === 0)
|
|
74
80
|
return false; // path longer than the tree depth — malformed
|
|
75
|
-
if (
|
|
81
|
+
if (fn % 2 === 1 || fn === sn) {
|
|
76
82
|
r = await nodeHash(p, r);
|
|
77
|
-
if (
|
|
78
|
-
while (
|
|
79
|
-
fn
|
|
80
|
-
sn
|
|
83
|
+
if (fn % 2 === 0) {
|
|
84
|
+
while (fn % 2 === 0 && fn !== 0) {
|
|
85
|
+
fn = half(fn);
|
|
86
|
+
sn = half(sn);
|
|
81
87
|
}
|
|
82
88
|
}
|
|
83
89
|
}
|
|
84
90
|
else {
|
|
85
91
|
r = await nodeHash(r, p);
|
|
86
92
|
}
|
|
87
|
-
fn
|
|
88
|
-
sn
|
|
93
|
+
fn = half(fn);
|
|
94
|
+
sn = half(sn);
|
|
89
95
|
}
|
|
90
96
|
return sn === 0 && bytesEqual(r, expectedRoot);
|
|
91
97
|
}
|
package/dist/verifier.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// Conformance is enforced by test/conformance.test.ts, which runs every
|
|
8
8
|
// corpus test vector through this code and asserts byte-for-byte agreement.
|
|
9
9
|
import canonicalize from "canonicalize";
|
|
10
|
-
import {
|
|
10
|
+
import { SIGNATURE_FAILED, ed25519Refusal, isHexField, sha256Hex, utf8 } from "./crypto.js";
|
|
11
11
|
// Fail-closed accepted-profile registry (envelope-spec §2): exactly the
|
|
12
12
|
// accepted profile majors, nothing else. Minors within an accepted
|
|
13
13
|
// major ("sanning.agent/v1.<minor>") are additive and tolerated — matching the Go
|
|
@@ -690,7 +690,7 @@ export async function verifyEnvelope(env, options) {
|
|
|
690
690
|
try {
|
|
691
691
|
return await verifyEnvelopeChecked(env, opts);
|
|
692
692
|
}
|
|
693
|
-
catch
|
|
693
|
+
catch {
|
|
694
694
|
return {
|
|
695
695
|
ok: false,
|
|
696
696
|
specVersionOk: false,
|
|
@@ -700,7 +700,7 @@ export async function verifyEnvelope(env, options) {
|
|
|
700
700
|
signatureOk: false,
|
|
701
701
|
contentHashOk: opts.expectedContentHash === undefined ? null : false,
|
|
702
702
|
contentRole: null,
|
|
703
|
-
errors: [
|
|
703
|
+
errors: ["envelope could not be read"],
|
|
704
704
|
};
|
|
705
705
|
}
|
|
706
706
|
}
|
|
@@ -737,8 +737,13 @@ async function verifyEnvelopeChecked(env, opts) {
|
|
|
737
737
|
};
|
|
738
738
|
}
|
|
739
739
|
const specVersionOk = specVersionSupported(env.spec_version, opts.acceptedSpecMajors);
|
|
740
|
-
if (!specVersionOk)
|
|
741
|
-
|
|
740
|
+
if (!specVersionOk) {
|
|
741
|
+
// Absent or null reads "missing", as in the Python kernel. Everything else
|
|
742
|
+
// is quoted the way both kernels quote a value in a finding.
|
|
743
|
+
errors.push(env.spec_version === undefined || env.spec_version === null
|
|
744
|
+
? "missing spec_version"
|
|
745
|
+
: `unsupported spec_version: ${JSON.stringify(env.spec_version)}`);
|
|
746
|
+
}
|
|
742
747
|
// Skeleton field format (envelope-spec §2, proof#95): a present `signed_at`
|
|
743
748
|
// or `environment` that is not what §2 says fails the verdict. There is no
|
|
744
749
|
// result field for it, so the verdict shape is unchanged for every envelope
|
|
@@ -766,8 +771,8 @@ async function verifyEnvelopeChecked(env, opts) {
|
|
|
766
771
|
// line, or an ANSI escape that paints `VERIFIED` green, into what the
|
|
767
772
|
// verifier prints. No other field in this file echoes its value, the Python
|
|
768
773
|
// twin names the same failure without one, and the value is in the envelope
|
|
769
|
-
// the auditor already holds.
|
|
770
|
-
//
|
|
774
|
+
// the auditor already holds. Neither is the recomputed digest: the reason is
|
|
775
|
+
// the Python kernel's sentence, word for word (proof#150).
|
|
771
776
|
let payloadHashOk;
|
|
772
777
|
if (typeof env.payload_hash !== "string") {
|
|
773
778
|
payloadHashOk = false;
|
|
@@ -779,23 +784,23 @@ async function verifyEnvelopeChecked(env, opts) {
|
|
|
779
784
|
try {
|
|
780
785
|
const recomputed = await sha256Hex(utf8(jcs(env.payload)));
|
|
781
786
|
checks.push(recomputed === env.payload_hash);
|
|
782
|
-
if (recomputed !== env.payload_hash) {
|
|
783
|
-
errors.push(`payload_hash mismatch: the envelope's payload_hash is not SHA-256(JCS(payload)), recomputed=${recomputed}`);
|
|
784
|
-
}
|
|
785
787
|
}
|
|
786
|
-
catch
|
|
788
|
+
catch {
|
|
787
789
|
checks.push(false);
|
|
788
|
-
errors.push(
|
|
790
|
+
errors.push("payload is not JCS-canonicalizable");
|
|
789
791
|
}
|
|
790
792
|
}
|
|
791
793
|
if (opts.payloadBytes !== undefined) {
|
|
792
794
|
const external = await sha256Hex(opts.payloadBytes);
|
|
793
795
|
checks.push(external === env.payload_hash);
|
|
794
|
-
if (external !== env.payload_hash) {
|
|
795
|
-
errors.push(`payload_hash does not match the committed bytes: bytes=${external}`);
|
|
796
|
-
}
|
|
797
796
|
}
|
|
798
797
|
payloadHashOk = checks.length > 0 ? checks.every(Boolean) : null;
|
|
798
|
+
// One sentence for a mismatch against the inline payload, the supplied
|
|
799
|
+
// bytes or both, and none when the payload could not be canonicalized: the
|
|
800
|
+
// Python kernel's rule, word for word (proof#150).
|
|
801
|
+
if (payloadHashOk === false && !errors.some((e) => e.startsWith("payload is not"))) {
|
|
802
|
+
errors.push("payload_hash does not match the committed bytes");
|
|
803
|
+
}
|
|
799
804
|
}
|
|
800
805
|
// Check 2 — Signature Confirmed: Ed25519 over the signed scope, which is
|
|
801
806
|
// JCS(envelope minus `signature` minus `co_signatures`) per envelope-spec §2.
|
|
@@ -803,15 +808,22 @@ async function verifyEnvelopeChecked(env, opts) {
|
|
|
803
808
|
// invalidating the primary signature; the field is reserved/default-absent.
|
|
804
809
|
// The corpus has no co-signed vectors, so this strip is pinned by an explicit
|
|
805
810
|
// unit test rather than by conformance.
|
|
811
|
+
// The refusal names the clause of the Ed25519 rule a signature breaks
|
|
812
|
+
// (envelope-spec §7, A44), so a forged shape reads differently from a
|
|
813
|
+
// corrupted byte; an equation that does not hold reads the bare sentence.
|
|
806
814
|
let signatureOk = false;
|
|
807
815
|
try {
|
|
808
816
|
const { signature: _signature, co_signatures: _coSignatures, ...envelopeForSig } = env;
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
817
|
+
const refusal = await ed25519Refusal(utf8(jcs(envelopeForSig)), env.signature, env.public_key);
|
|
818
|
+
signatureOk = refusal === null;
|
|
819
|
+
if (refusal !== null)
|
|
820
|
+
errors.push(refusal);
|
|
812
821
|
}
|
|
813
|
-
catch
|
|
814
|
-
|
|
822
|
+
catch {
|
|
823
|
+
// The sentence the Python kernel writes for the same failure. What was
|
|
824
|
+
// thrown is the host's wording (`jcs: input nesting exceeds ...`), and a
|
|
825
|
+
// public result carries the kernel's own words only (proof#150).
|
|
826
|
+
errors.push(SIGNATURE_FAILED);
|
|
815
827
|
}
|
|
816
828
|
// Check 3 (optional) — Content Bind: the user's bytes match a hash this
|
|
817
829
|
// envelope commits to. The tag got us here; only this proves the bytes match.
|
|
@@ -865,6 +877,3 @@ async function verifyEnvelopeChecked(env, opts) {
|
|
|
865
877
|
errors,
|
|
866
878
|
};
|
|
867
879
|
}
|
|
868
|
-
function stringifyErr(e) {
|
|
869
|
-
return e instanceof Error ? e.message : String(e);
|
|
870
|
-
}
|
package/dist/zip-reader.d.ts
CHANGED
|
@@ -27,6 +27,13 @@
|
|
|
27
27
|
export declare const MAX_ZIP_ENTRIES = 50000;
|
|
28
28
|
/** More uncompressed bytes than this, across every entry, refuse the archive. */
|
|
29
29
|
export declare const MAX_ZIP_BYTES: number;
|
|
30
|
+
/**
|
|
31
|
+
* An archive file larger than this is refused before it is read: the
|
|
32
|
+
* uncompressed cap, and room for the headers, the central directory and the
|
|
33
|
+
* deflate framing of an archive that holds that much. The caller checks the
|
|
34
|
+
* size on disk, so the archive is never read whole to find out it is too big.
|
|
35
|
+
*/
|
|
36
|
+
export declare const MAX_ZIP_ARCHIVE_BYTES: number;
|
|
30
37
|
/** A name nested deeper than this many folders refuses the archive. */
|
|
31
38
|
export declare const MAX_ZIP_DEPTH = 32;
|
|
32
39
|
/** The archive cannot be read as a case file. `message` is the whole report, identical in both kernels. */
|
package/dist/zip-reader.js
CHANGED
|
@@ -29,6 +29,13 @@ import { utf8Strict } from "./verifier.js";
|
|
|
29
29
|
export const MAX_ZIP_ENTRIES = 50_000;
|
|
30
30
|
/** More uncompressed bytes than this, across every entry, refuse the archive. */
|
|
31
31
|
export const MAX_ZIP_BYTES = 2 ** 30;
|
|
32
|
+
/**
|
|
33
|
+
* An archive file larger than this is refused before it is read: the
|
|
34
|
+
* uncompressed cap, and room for the headers, the central directory and the
|
|
35
|
+
* deflate framing of an archive that holds that much. The caller checks the
|
|
36
|
+
* size on disk, so the archive is never read whole to find out it is too big.
|
|
37
|
+
*/
|
|
38
|
+
export const MAX_ZIP_ARCHIVE_BYTES = MAX_ZIP_BYTES + 256 * 2 ** 20;
|
|
32
39
|
/** A name nested deeper than this many folders refuses the archive. */
|
|
33
40
|
export const MAX_ZIP_DEPTH = 32;
|
|
34
41
|
const EOCD = 0x06054b50;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanning/proof",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "TypeScript kernel of the Sanning verification stack: RFC 8785 (JCS) canonicalization, SHA-256, and Ed25519 verification for Verifiable Event Envelopes. Full-family verifier (inline + external commitment); accepts sanning.agent/v1 and sanning.events/v2. Conformance-gated byte-for-byte against the test-vectors corpus.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"prepublishOnly": "npm run build"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@noble/ed25519": "^2.
|
|
33
|
+
"@noble/ed25519": "^3.2.0",
|
|
34
34
|
"canonicalize": "^2.0.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|