@sanning/proof 0.16.0 → 0.17.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/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 +4 -4
- package/dist/index.js +2 -2
- package/dist/merkle.js +13 -7
- package/dist/verifier.js +24 -19
- package/dist/zip-reader.d.ts +7 -0
- package/dist/zip-reader.js +7 -0
- package/package.json +1 -1
- 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/evidence.ts +109 -43
- package/src/file-match.ts +160 -26
- package/src/grading.ts +15 -2
- package/src/index.ts +6 -0
- package/src/merkle.ts +13 -7
- package/src/verifier.ts +26 -22
- 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/src/case.ts
CHANGED
|
@@ -248,6 +248,7 @@ import {
|
|
|
248
248
|
type DeclarationRole,
|
|
249
249
|
DECLARATION_VERSION,
|
|
250
250
|
DECLARATION_VERSION_V2,
|
|
251
|
+
type PackRecord,
|
|
251
252
|
checkDeclaration,
|
|
252
253
|
declarationClaims,
|
|
253
254
|
packRecords,
|
|
@@ -260,7 +261,7 @@ import {
|
|
|
260
261
|
readHandoffs,
|
|
261
262
|
recordIsMalformed,
|
|
262
263
|
} from "./handoff.js";
|
|
263
|
-
import { coSignaturesPresent, verifyEvidenceBundle } from "./evidence.js";
|
|
264
|
+
import { coSignaturesPresent, resolveTraceEvents, verifyEvidenceBundle } from "./evidence.js";
|
|
264
265
|
import type { EvidenceBundleResult } from "./evidence.js";
|
|
265
266
|
import {
|
|
266
267
|
type PackGrade,
|
|
@@ -784,6 +785,42 @@ export const CASE_EXCEPTION_KINDS = [
|
|
|
784
785
|
|
|
785
786
|
export type CaseExceptionKind = (typeof CASE_EXCEPTION_KINDS)[number];
|
|
786
787
|
|
|
788
|
+
/**
|
|
789
|
+
* The status every record in every pack carries (evidence-bundle 5.5.4
|
|
790
|
+
* v1.35, A43, proof#204), as a closed set. A record is listed for each element
|
|
791
|
+
* of its pack's `body.events` as `packRecords` reads them, or, for a
|
|
792
|
+
* `sanning.evidence.export/v2` pack, of the events of the inline source
|
|
793
|
+
* bundle it wraps (`resolveTraceEvents`), and gets exactly one:
|
|
794
|
+
*
|
|
795
|
+
* - `read_clean`: the record went into the pile, no exception carries its
|
|
796
|
+
* `packIndex` and `recordIndex`, and its pack's declaration check
|
|
797
|
+
* names it by nothing (`declarationFindings` is empty). It says nothing
|
|
798
|
+
* about its PACK: the pack's own grade and exceptions are on the pack, and a
|
|
799
|
+
* surface MUST show that grade beside the status.
|
|
800
|
+
* - `read_flagged`: the record went into the pile, and at least one
|
|
801
|
+
* exception carries its `packIndex` and `recordIndex`, or its pack's
|
|
802
|
+
* declaration check names its `eventId` by a deviation or a content
|
|
803
|
+
* finding (`declarationFindings` is not empty; under an `eventId` the
|
|
804
|
+
* pack carries twice, every record with that id). An exception about the
|
|
805
|
+
* pack alone flags no record.
|
|
806
|
+
* - `not_read`: the record never went into the pile. `reason` is the kind
|
|
807
|
+
* of the exception that says why, on the record
|
|
808
|
+
* (`record_profile_quarantined`, `record_envelope_does_not_verify`,
|
|
809
|
+
* `record_bytes_withheld`, `record_bytes_unreadable`) or on its pack
|
|
810
|
+
* (`pack_key_unreadable`, `pack_does_not_verify`,
|
|
811
|
+
* `pack_body_type_unknown`, `pack_grade_not_verified` where the body
|
|
812
|
+
* could not be read or the verifier graded a different number of records).
|
|
813
|
+
* An export's wrapped records carry the reason of the exception their pack
|
|
814
|
+
* actually got: `pack_body_type_unknown` where its wrapper holds, and
|
|
815
|
+
* otherwise `pack_key_unreadable`, `pack_does_not_verify` or
|
|
816
|
+
* `pack_grade_not_verified`.
|
|
817
|
+
*
|
|
818
|
+
* Before A43 the reading listed only the records it read, so a surface had to
|
|
819
|
+
* work out which records were missing, and why, by a rule of its own.
|
|
820
|
+
*/
|
|
821
|
+
export const CASE_RECORD_STATUSES = ["read_clean", "read_flagged", "not_read"] as const;
|
|
822
|
+
export type CaseRecordStatus = (typeof CASE_RECORD_STATUSES)[number];
|
|
823
|
+
|
|
787
824
|
/**
|
|
788
825
|
* What each exception kind contributes to the case verdict. It is a TABLE and
|
|
789
826
|
* not a chain of branches, so "which exceptions move the verdict" is one thing
|
|
@@ -868,10 +905,25 @@ export interface CasePack {
|
|
|
868
905
|
* `renderException`. */
|
|
869
906
|
export interface CaseException {
|
|
870
907
|
kind: CaseExceptionKind;
|
|
871
|
-
/**
|
|
908
|
+
/**
|
|
909
|
+
* Which pack this is about, by the reader's own name for it. Two packs can
|
|
910
|
+
* carry one `ref`, so it is a label and never the join: `packIndex` is.
|
|
911
|
+
*/
|
|
872
912
|
packRef?: string;
|
|
913
|
+
/**
|
|
914
|
+
* Which pack this is about, by its position in `CaseReading.packs` (0
|
|
915
|
+
* first), present wherever `packRef` is (A43). Unique in the reading.
|
|
916
|
+
*/
|
|
917
|
+
packIndex?: number;
|
|
873
918
|
/** Which record, by its envelope `event_id`. */
|
|
874
919
|
eventId?: string;
|
|
920
|
+
/**
|
|
921
|
+
* Which record, by its position in its pack's `body.events` (0 first),
|
|
922
|
+
* present wherever `eventId` is (A43). One pack can carry one `event_id`
|
|
923
|
+
* twice, so `packIndex` and `recordIndex` together are the join to
|
|
924
|
+
* `CaseReading.records`.
|
|
925
|
+
*/
|
|
926
|
+
recordIndex?: number;
|
|
875
927
|
/**
|
|
876
928
|
* The counterparty key the record named, for the two counterpart kinds.
|
|
877
929
|
* Normalized (`normalizedSigningKey`), so a reader compares what this module
|
|
@@ -904,20 +956,54 @@ export interface CaseException {
|
|
|
904
956
|
}
|
|
905
957
|
|
|
906
958
|
/**
|
|
907
|
-
*
|
|
908
|
-
*
|
|
909
|
-
*
|
|
910
|
-
*
|
|
911
|
-
*
|
|
912
|
-
*
|
|
913
|
-
*
|
|
959
|
+
* One record of one pack, with its status and, where it was read, 4.1's
|
|
960
|
+
* reading of it.
|
|
961
|
+
*
|
|
962
|
+
* EVERY record of every pack gets one of these (A43), in pack order and then
|
|
963
|
+
* in bundle order: the ones this reader read, including the ones that are not
|
|
964
|
+
* hand-off records at all (`findings` carries `not_a_handoff_record`), and the
|
|
965
|
+
* ones it did not, with `status` `not_read` and the `reason`. 4.1 (v1.9, A14)
|
|
966
|
+
* names why: the one thing an attacker wants is for a record to be absent from
|
|
967
|
+
* the output. An export's wrapped records are listed, `not_read`. A pack whose
|
|
968
|
+
* body carries no `events` array, and wraps none, lists none, and its own
|
|
969
|
+
* exception says why.
|
|
914
970
|
*/
|
|
915
971
|
export interface CaseRecordReading {
|
|
916
972
|
packRef: string;
|
|
973
|
+
/** The pack, by its position in `CaseReading.packs` (A43). */
|
|
974
|
+
packIndex: number;
|
|
917
975
|
eventId: string;
|
|
976
|
+
/**
|
|
977
|
+
* The record, by its position in the list of records its pack lists (A43):
|
|
978
|
+
* `body.events`, or for a `sanning.evidence.export/v2` pack the events of the
|
|
979
|
+
* source bundle it wraps.
|
|
980
|
+
*/
|
|
981
|
+
recordIndex: number;
|
|
982
|
+
/** One of `CASE_RECORD_STATUSES` (A43). */
|
|
983
|
+
status: CaseRecordStatus;
|
|
984
|
+
/**
|
|
985
|
+
* For `not_read`, the kind of the exception that says why; `null` for a
|
|
986
|
+
* record that was read.
|
|
987
|
+
*/
|
|
988
|
+
reason: CaseExceptionKind | null;
|
|
989
|
+
/**
|
|
990
|
+
* The kinds its pack's declaration check names this record by, deviations
|
|
991
|
+
* (`DECLARATION_DEVIATION_KINDS`) then content findings
|
|
992
|
+
* (`DECLARATION_CONTENT_KINDS`), each in the check's own order. Matched on
|
|
993
|
+
* `eventId`, so under an `event_id` the pack carries twice every record with
|
|
994
|
+
* that id carries it. A record that went into the pile with any is
|
|
995
|
+
* `read_flagged` (A43).
|
|
996
|
+
*/
|
|
997
|
+
declarationFindings: string[];
|
|
998
|
+
/** 4.1's verdict, or `null` where 4.1 gave none or the record was not read. */
|
|
918
999
|
verdict: string | null;
|
|
919
1000
|
findings: string[];
|
|
920
1001
|
failedChecks: string[];
|
|
1002
|
+
/**
|
|
1003
|
+
* 4.1's candidate counterparts, each as `<pack ref>/<event_id>`. A LABEL for
|
|
1004
|
+
* a person to read and never the join: two packs can carry one ref, and one
|
|
1005
|
+
* pack one `event_id` twice (A43).
|
|
1006
|
+
*/
|
|
921
1007
|
counterparts: string[];
|
|
922
1008
|
/**
|
|
923
1009
|
* The role the pack's v2 declaration signs for this record, `handoff` or
|
|
@@ -935,6 +1021,8 @@ export interface CaseNote {
|
|
|
935
1021
|
kind: CaseNoteKind;
|
|
936
1022
|
/** Which pack, by the reader's own name for it. */
|
|
937
1023
|
packRef: string;
|
|
1024
|
+
/** Which pack, by its position in `CaseReading.packs` (A43). */
|
|
1025
|
+
packIndex: number;
|
|
938
1026
|
/**
|
|
939
1027
|
* For `CONTENT_WITHHELD_BY_DECLARATION`, how many of the pack's records its
|
|
940
1028
|
* declaration withholds.
|
|
@@ -948,6 +1036,13 @@ export interface CaseNote {
|
|
|
948
1036
|
* its wrapper, and never by an organization (D10).
|
|
949
1037
|
*/
|
|
950
1038
|
export interface CasePackReading {
|
|
1039
|
+
/**
|
|
1040
|
+
* The pack's position in the order the reader was given the packs, 0 first
|
|
1041
|
+
* (A43): the one name for a pack that is unique in the reading. Every
|
|
1042
|
+
* exception, note and record names its pack by it as `packIndex`.
|
|
1043
|
+
*/
|
|
1044
|
+
index: number;
|
|
1045
|
+
/** The reader's own name for the pack. Two packs can carry the same one. */
|
|
951
1046
|
ref: string;
|
|
952
1047
|
/** The wrapper key, normalized, or `null` where this reader could not read it. */
|
|
953
1048
|
signingKey: string | null;
|
|
@@ -980,7 +1075,10 @@ export interface CaseReading {
|
|
|
980
1075
|
* reachable only together with `verdict === "verified"`.
|
|
981
1076
|
*/
|
|
982
1077
|
exceptions: CaseException[];
|
|
983
|
-
/**
|
|
1078
|
+
/**
|
|
1079
|
+
* One entry per record of every pack, read or not, in pack order and then
|
|
1080
|
+
* bundle order, each with its status (A43).
|
|
1081
|
+
*/
|
|
984
1082
|
records: CaseRecordReading[];
|
|
985
1083
|
/**
|
|
986
1084
|
* Every `[contributor, verdict]` the case verdict was computed from, in
|
|
@@ -1237,8 +1335,40 @@ export async function readCase(
|
|
|
1237
1335
|
// rather than parsed back out of a reference string. A reference is for a
|
|
1238
1336
|
// human to read; a join rebuilt by splitting one is a join on a value a
|
|
1239
1337
|
// holder can put a separator in.
|
|
1240
|
-
const provenance: {
|
|
1241
|
-
|
|
1338
|
+
const provenance: {
|
|
1339
|
+
position: number;
|
|
1340
|
+
packRef: string;
|
|
1341
|
+
eventId: string;
|
|
1342
|
+
recordIndex: number;
|
|
1343
|
+
}[] = [];
|
|
1344
|
+
// Every record of every pack, in pack order and bundle order (A43): where it
|
|
1345
|
+
// went into the pile, its place there (`heldAt`), and where it did not, the
|
|
1346
|
+
// kind of the exception that says why. Nothing is listed only where the pack
|
|
1347
|
+
// body carries no `events` array, and that pack's own exception says why.
|
|
1348
|
+
const slots: {
|
|
1349
|
+
position: number;
|
|
1350
|
+
packRef: string;
|
|
1351
|
+
recordIndex: number;
|
|
1352
|
+
eventId: string;
|
|
1353
|
+
heldAt: number | null;
|
|
1354
|
+
reason: CaseExceptionKind | null;
|
|
1355
|
+
}[] = [];
|
|
1356
|
+
const unread = (
|
|
1357
|
+
position: number,
|
|
1358
|
+
packRef: string,
|
|
1359
|
+
listed: readonly PackRecord[],
|
|
1360
|
+
reason: CaseExceptionKind,
|
|
1361
|
+
) => {
|
|
1362
|
+
listed.forEach((record, recordIndex) =>
|
|
1363
|
+
slots.push({ position, packRef, recordIndex, eventId: record.eventId, heldAt: null, reason }),
|
|
1364
|
+
);
|
|
1365
|
+
};
|
|
1366
|
+
// The kinds each pack's declaration check names each record by, keyed by the
|
|
1367
|
+
// pack's POSITION and then by `eventId` (A43). The check names a record by
|
|
1368
|
+
// `event_id` alone, so under an id the pack carries twice every record with
|
|
1369
|
+
// that id is named: failing closed, rather than guessing which of the two the
|
|
1370
|
+
// check meant.
|
|
1371
|
+
const declarationFindings = new Map<number, Map<string, string[]>>();
|
|
1242
1372
|
// Keyed by the pack's POSITION and never by its `ref`. `ref` is the reader's
|
|
1243
1373
|
// own name for a pack, and nothing stops two packs in one archive carrying
|
|
1244
1374
|
// the same one: keyed by ref, the second pack's claims silently replaced the
|
|
@@ -1296,9 +1426,11 @@ export async function readCase(
|
|
|
1296
1426
|
// pack whose signer is UNKNOWN, and not a pack nobody signed. Checked
|
|
1297
1427
|
// BEFORE verification, because the two bundle verifiers disagree on some
|
|
1298
1428
|
// of these spellings (proof#135) and the reading must not.
|
|
1299
|
-
note(PACK_KEY_UNREADABLE, { packRef: pack.ref });
|
|
1429
|
+
note(PACK_KEY_UNREADABLE, { packRef: pack.ref, packIndex: position });
|
|
1300
1430
|
unattributed += 1;
|
|
1431
|
+
unread(position, pack.ref, listedRecords(pack.bundle, pack.content), PACK_KEY_UNREADABLE);
|
|
1301
1432
|
packReadings.push({
|
|
1433
|
+
index: position,
|
|
1302
1434
|
ref: pack.ref,
|
|
1303
1435
|
signingKey: null,
|
|
1304
1436
|
grade: null,
|
|
@@ -1330,9 +1462,11 @@ export async function readCase(
|
|
|
1330
1462
|
|
|
1331
1463
|
// WHOSE PACK IS IT. The wrapper, and only the wrapper.
|
|
1332
1464
|
if (!wrapperHolds(verified)) {
|
|
1333
|
-
note(PACK_DOES_NOT_VERIFY, { packRef: pack.ref });
|
|
1465
|
+
note(PACK_DOES_NOT_VERIFY, { packRef: pack.ref, packIndex: position });
|
|
1334
1466
|
unattributed += 1;
|
|
1467
|
+
unread(position, pack.ref, listedRecords(pack.bundle, pack.content), PACK_DOES_NOT_VERIFY);
|
|
1335
1468
|
packReadings.push({
|
|
1469
|
+
index: position,
|
|
1336
1470
|
ref: pack.ref,
|
|
1337
1471
|
signingKey: holder,
|
|
1338
1472
|
grade: null,
|
|
@@ -1349,7 +1483,7 @@ export async function readCase(
|
|
|
1349
1483
|
// the case reading names it rather than moving anything on its account.
|
|
1350
1484
|
caseNotes.push({
|
|
1351
1485
|
position,
|
|
1352
|
-
note: { kind: CO_SIGNATURES_PRESENT, packRef: pack.ref },
|
|
1486
|
+
note: { kind: CO_SIGNATURES_PRESENT, packRef: pack.ref, packIndex: position },
|
|
1353
1487
|
});
|
|
1354
1488
|
}
|
|
1355
1489
|
|
|
@@ -1418,6 +1552,7 @@ export async function readCase(
|
|
|
1418
1552
|
}
|
|
1419
1553
|
note(PACK_GRADE_NOT_VERIFIED, {
|
|
1420
1554
|
packRef: pack.ref,
|
|
1555
|
+
packIndex: position,
|
|
1421
1556
|
status: grade as CaseVerdict,
|
|
1422
1557
|
causes,
|
|
1423
1558
|
reasons,
|
|
@@ -1426,11 +1561,13 @@ export async function readCase(
|
|
|
1426
1561
|
if (verified.status === "failed" && signed.status !== "failed") {
|
|
1427
1562
|
note(PACK_BUNDLE_CONTENT_DEPENDENT, {
|
|
1428
1563
|
packRef: pack.ref,
|
|
1564
|
+
packIndex: position,
|
|
1429
1565
|
reasons: onlyIn(bundleReasons(verified), bundleReasons(signed)),
|
|
1430
1566
|
});
|
|
1431
1567
|
}
|
|
1432
1568
|
const recordsWithheld = full !== null ? full.recordsWithheld : 0;
|
|
1433
1569
|
packReadings.push({
|
|
1570
|
+
index: position,
|
|
1434
1571
|
ref: pack.ref,
|
|
1435
1572
|
signingKey: holder,
|
|
1436
1573
|
grade,
|
|
@@ -1443,12 +1580,20 @@ export async function readCase(
|
|
|
1443
1580
|
// them, so a clean verdict never hides them.
|
|
1444
1581
|
caseNotes.push({
|
|
1445
1582
|
position,
|
|
1446
|
-
note: {
|
|
1583
|
+
note: {
|
|
1584
|
+
kind: CONTENT_WITHHELD_BY_DECLARATION,
|
|
1585
|
+
packRef: pack.ref,
|
|
1586
|
+
packIndex: position,
|
|
1587
|
+
count: recordsWithheld,
|
|
1588
|
+
},
|
|
1447
1589
|
});
|
|
1448
1590
|
}
|
|
1449
1591
|
// The body is one the verifier could not read, so it graded no record, and
|
|
1450
1592
|
// nothing below has a grading to read. Named above.
|
|
1451
|
-
if (signed.status === "malformed")
|
|
1593
|
+
if (signed.status === "malformed") {
|
|
1594
|
+
unread(position, pack.ref, listedRecords(pack.bundle, pack.content), PACK_GRADE_NOT_VERIFIED);
|
|
1595
|
+
continue;
|
|
1596
|
+
}
|
|
1452
1597
|
|
|
1453
1598
|
if (!knownBody) {
|
|
1454
1599
|
// `sanning.evidence.export/v2` lands here, and it is NOT unwrapped: its
|
|
@@ -1457,20 +1602,35 @@ export async function readCase(
|
|
|
1457
1602
|
// a repair (evidence-bundle.md 5.5.1 step 1 says so too). What this exit
|
|
1458
1603
|
// costs is that pack's RECORDS, and it is named every time. It no longer
|
|
1459
1604
|
// costs the pack's KEY: that was read above.
|
|
1460
|
-
note(PACK_BODY_TYPE_UNKNOWN, { packRef: pack.ref });
|
|
1605
|
+
note(PACK_BODY_TYPE_UNKNOWN, { packRef: pack.ref, packIndex: position });
|
|
1606
|
+
// Listed, and not read: the records a `sanning.evidence.export/v2` wraps
|
|
1607
|
+
// are listed from its inline source bundle, so a reading never says "0
|
|
1608
|
+
// not read" of a pack that holds records (A43).
|
|
1609
|
+
unread(position, pack.ref, listedRecords(pack.bundle, pack.content), PACK_BODY_TYPE_UNKNOWN);
|
|
1461
1610
|
continue;
|
|
1462
1611
|
}
|
|
1463
1612
|
// Graded above (`malformed` where the verifier called it `verified`), and
|
|
1464
1613
|
// none of its records are read.
|
|
1465
|
-
if (ungraded)
|
|
1614
|
+
if (ungraded) {
|
|
1615
|
+
unread(position, pack.ref, records, PACK_GRADE_NOT_VERIFIED);
|
|
1616
|
+
continue;
|
|
1617
|
+
}
|
|
1466
1618
|
if (records.length === 0) {
|
|
1467
|
-
note(PACK_HOLDS_NO_RECORDS, { packRef: pack.ref });
|
|
1619
|
+
note(PACK_HOLDS_NO_RECORDS, { packRef: pack.ref, packIndex: position });
|
|
1468
1620
|
continue;
|
|
1469
1621
|
}
|
|
1470
1622
|
|
|
1471
1623
|
const checked = check as DeclarationCheck;
|
|
1472
1624
|
claimsByPack.set(position, declarationClaims(pack.bundle));
|
|
1473
1625
|
rolesByPack.set(position, new Map((checked.roles ?? []).map((r) => [r.eventId, r.role])));
|
|
1626
|
+
const namedByCheck = new Map<string, string[]>();
|
|
1627
|
+
for (const finding of [...checked.deviations, ...checked.contentFindings]) {
|
|
1628
|
+
if (finding.eventId === undefined) continue;
|
|
1629
|
+
const kinds = namedByCheck.get(finding.eventId) ?? [];
|
|
1630
|
+
kinds.push(finding.kind);
|
|
1631
|
+
namedByCheck.set(finding.eventId, kinds);
|
|
1632
|
+
}
|
|
1633
|
+
declarationFindings.set(position, namedByCheck);
|
|
1474
1634
|
if (checked.contentFindings.length > 0) {
|
|
1475
1635
|
// BESIDE the deviation exception and never instead of it: a pack can do
|
|
1476
1636
|
// both, over different records, and the two say different things. This
|
|
@@ -1478,12 +1638,14 @@ export async function readCase(
|
|
|
1478
1638
|
// it turns on (proof#147).
|
|
1479
1639
|
note(PACK_DECLARATION_CONTENT_DEPENDENT, {
|
|
1480
1640
|
packRef: pack.ref,
|
|
1641
|
+
packIndex: position,
|
|
1481
1642
|
count: checked.contentFindings.length,
|
|
1482
1643
|
});
|
|
1483
1644
|
}
|
|
1484
1645
|
if (checked.verdict === "failed") {
|
|
1485
1646
|
note(PACK_DECLARATION_DEVIATES, {
|
|
1486
1647
|
packRef: pack.ref,
|
|
1648
|
+
packIndex: position,
|
|
1487
1649
|
count: checked.deviations.length,
|
|
1488
1650
|
});
|
|
1489
1651
|
} else if (checked.contentFindings.length > 0) {
|
|
@@ -1503,7 +1665,7 @@ export async function readCase(
|
|
|
1503
1665
|
// cannot read", which `gradePack` already carries to `incomplete`. The
|
|
1504
1666
|
// same posture here, for the same reason: the field has no accept-set and
|
|
1505
1667
|
// the holder writes it.
|
|
1506
|
-
note(PACK_DECLARATION_UNREADABLE, { packRef: pack.ref });
|
|
1668
|
+
note(PACK_DECLARATION_UNREADABLE, { packRef: pack.ref, packIndex: position });
|
|
1507
1669
|
} else {
|
|
1508
1670
|
// WHAT IT CONTRIBUTES TURNS ON THE WHOLE READING (5.5.1 step 2 v1.20,
|
|
1509
1671
|
// A23, I4): `partial` where the reading holds a hand-off record, and a
|
|
@@ -1526,30 +1688,34 @@ export async function readCase(
|
|
|
1526
1688
|
const record = records[index];
|
|
1527
1689
|
const graded = verified.events[index];
|
|
1528
1690
|
const bare = signed.events[index];
|
|
1691
|
+
// Which pack and which record, by position: the two names that are
|
|
1692
|
+
// unique in the reading whatever the reader called the pack and whatever
|
|
1693
|
+
// the holder wrote as `event_id` (A43).
|
|
1694
|
+
const at = {
|
|
1695
|
+
packRef: pack.ref,
|
|
1696
|
+
packIndex: position,
|
|
1697
|
+
eventId: record.eventId,
|
|
1698
|
+
recordIndex: index,
|
|
1699
|
+
};
|
|
1700
|
+
// Not read: listed with the kind of the exception that says why.
|
|
1701
|
+
const skip = (kind: CaseExceptionKind, fields: Omit<CaseException, "kind"> = {}) => {
|
|
1702
|
+
note(kind, { ...at, ...fields });
|
|
1703
|
+
slots.push({ position, packRef: pack.ref, recordIndex: index, eventId: record.eventId, heldAt: null, reason: kind });
|
|
1704
|
+
};
|
|
1529
1705
|
if (underQuarantine(recordSpecVersion(pack.bundle, index))) {
|
|
1530
|
-
|
|
1706
|
+
skip(RECORD_PROFILE_QUARANTINED);
|
|
1531
1707
|
continue;
|
|
1532
1708
|
}
|
|
1533
1709
|
if (!graded.envelopeOk) {
|
|
1534
|
-
|
|
1535
|
-
packRef: pack.ref,
|
|
1536
|
-
eventId: record.eventId,
|
|
1537
|
-
reasons: [...graded.errors],
|
|
1538
|
-
});
|
|
1710
|
+
skip(RECORD_ENVELOPE_DOES_NOT_VERIFY, { reasons: [...graded.errors] });
|
|
1539
1711
|
continue;
|
|
1540
1712
|
}
|
|
1541
1713
|
if (!record.bytesSupplied) {
|
|
1542
|
-
|
|
1543
|
-
packRef: pack.ref,
|
|
1544
|
-
eventId: record.eventId,
|
|
1545
|
-
});
|
|
1714
|
+
skip(RECORD_BYTES_WITHHELD);
|
|
1546
1715
|
continue;
|
|
1547
1716
|
}
|
|
1548
1717
|
if (record.record === null) {
|
|
1549
|
-
|
|
1550
|
-
packRef: pack.ref,
|
|
1551
|
-
eventId: record.eventId,
|
|
1552
|
-
});
|
|
1718
|
+
skip(RECORD_BYTES_UNREADABLE);
|
|
1553
1719
|
continue;
|
|
1554
1720
|
}
|
|
1555
1721
|
// Named, and the record stays in the pile (see the kinds). A mismatch
|
|
@@ -1557,14 +1723,12 @@ export async function readCase(
|
|
|
1557
1723
|
// with the side map is `incomplete` (proof#159).
|
|
1558
1724
|
if (bare.contentOk === false) {
|
|
1559
1725
|
note(RECORD_CONTENT_MISMATCH, {
|
|
1560
|
-
|
|
1561
|
-
eventId: record.eventId,
|
|
1726
|
+
...at,
|
|
1562
1727
|
reasons: [...bare.errors],
|
|
1563
1728
|
});
|
|
1564
1729
|
} else if (graded.contentOk === false) {
|
|
1565
1730
|
note(RECORD_CONTENT_DEPENDENT, {
|
|
1566
|
-
|
|
1567
|
-
eventId: record.eventId,
|
|
1731
|
+
...at,
|
|
1568
1732
|
reasons: onlyIn(graded.errors, bare.errors),
|
|
1569
1733
|
});
|
|
1570
1734
|
}
|
|
@@ -1575,17 +1739,24 @@ export async function readCase(
|
|
|
1575
1739
|
(!bare.ok && bare.contentOk !== false)
|
|
1576
1740
|
) {
|
|
1577
1741
|
note(RECORD_DOES_NOT_VERIFY_IN_ITS_PACK, {
|
|
1578
|
-
|
|
1579
|
-
eventId: record.eventId,
|
|
1742
|
+
...at,
|
|
1580
1743
|
reasons: [...bare.errors],
|
|
1581
1744
|
});
|
|
1582
1745
|
}
|
|
1746
|
+
slots.push({
|
|
1747
|
+
position,
|
|
1748
|
+
packRef: pack.ref,
|
|
1749
|
+
recordIndex: index,
|
|
1750
|
+
eventId: record.eventId,
|
|
1751
|
+
heldAt: held.length,
|
|
1752
|
+
reason: null,
|
|
1753
|
+
});
|
|
1583
1754
|
held.push({
|
|
1584
1755
|
publicKey: record.publicKey,
|
|
1585
1756
|
record: record.record,
|
|
1586
1757
|
ref: `${pack.ref}/${record.eventId}`,
|
|
1587
1758
|
});
|
|
1588
|
-
provenance.push({ position, packRef: pack.ref, eventId: record.eventId });
|
|
1759
|
+
provenance.push({ position, packRef: pack.ref, eventId: record.eventId, recordIndex: index });
|
|
1589
1760
|
}
|
|
1590
1761
|
}
|
|
1591
1762
|
|
|
@@ -1596,20 +1767,12 @@ export async function readCase(
|
|
|
1596
1767
|
const readings: HandoffReading[] = readHandoffs(held);
|
|
1597
1768
|
|
|
1598
1769
|
// Step 4.
|
|
1599
|
-
const records: CaseRecordReading[] = [];
|
|
1600
1770
|
let handoffRecords = 0;
|
|
1601
1771
|
for (let index = 0; index < readings.length; index += 1) {
|
|
1602
1772
|
const reading = readings[index];
|
|
1603
|
-
const { position, packRef, eventId } = provenance[index];
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
eventId,
|
|
1607
|
-
verdict: reading.verdict,
|
|
1608
|
-
findings: [...reading.findings],
|
|
1609
|
-
failedChecks: [...reading.failedChecks],
|
|
1610
|
-
counterparts: [...reading.candidates],
|
|
1611
|
-
role: rolesByPack.get(position)?.get(eventId) ?? null,
|
|
1612
|
-
});
|
|
1773
|
+
const { position, packRef, eventId, recordIndex } = provenance[index];
|
|
1774
|
+
// The record this reading is about, by position (A43).
|
|
1775
|
+
const at = { packRef, packIndex: position, eventId, recordIndex };
|
|
1613
1776
|
// proof#137's door, named. Read from the ONE reader of the declaration
|
|
1614
1777
|
// (`declarationClaims`), never from a second parse here. Only a READABLE
|
|
1615
1778
|
// declaration can make a record unclaimed: where the claim itself is absent
|
|
@@ -1632,7 +1795,7 @@ export async function readCase(
|
|
|
1632
1795
|
claims.version === DECLARATION_VERSION &&
|
|
1633
1796
|
!claims.claimedSides.includes(eventId)
|
|
1634
1797
|
) {
|
|
1635
|
-
note(RECORD_UNCLAIMED,
|
|
1798
|
+
note(RECORD_UNCLAIMED, at);
|
|
1636
1799
|
}
|
|
1637
1800
|
|
|
1638
1801
|
if (reading.findings.includes(NOT_A_HANDOFF_FINDING)) {
|
|
@@ -1651,10 +1814,10 @@ export async function readCase(
|
|
|
1651
1814
|
// counterpart, ambiguity and `failedChecks` reporting below all keep
|
|
1652
1815
|
// running over it.
|
|
1653
1816
|
if (recordIsMalformed(held[index])) {
|
|
1654
|
-
note(RECORD_MALFORMED,
|
|
1817
|
+
note(RECORD_MALFORMED, at);
|
|
1655
1818
|
continue;
|
|
1656
1819
|
}
|
|
1657
|
-
note(COUNTERPART_MALFORMED,
|
|
1820
|
+
note(COUNTERPART_MALFORMED, at);
|
|
1658
1821
|
}
|
|
1659
1822
|
|
|
1660
1823
|
if (reading.findings.includes(COUNTERPART_ABSENT)) {
|
|
@@ -1700,16 +1863,15 @@ export async function readCase(
|
|
|
1700
1863
|
note(
|
|
1701
1864
|
kind,
|
|
1702
1865
|
setAside > 0
|
|
1703
|
-
? {
|
|
1704
|
-
: {
|
|
1866
|
+
? { ...at, counterpartyKey: named, count: setAside }
|
|
1867
|
+
: { ...at, counterpartyKey: named },
|
|
1705
1868
|
);
|
|
1706
1869
|
continue;
|
|
1707
1870
|
}
|
|
1708
1871
|
|
|
1709
1872
|
if (reading.findings.includes(COUNTERPART_AMBIGUOUS)) {
|
|
1710
1873
|
note(COUNTERPART_AMBIGUOUS, {
|
|
1711
|
-
|
|
1712
|
-
eventId,
|
|
1874
|
+
...at,
|
|
1713
1875
|
count: reading.candidateReadings.filter((c) => c.accountedFor === null).length,
|
|
1714
1876
|
});
|
|
1715
1877
|
}
|
|
@@ -1722,8 +1884,7 @@ export async function readCase(
|
|
|
1722
1884
|
// check whatever the aggregate word is.
|
|
1723
1885
|
if (reading.failedChecks.length > 0) {
|
|
1724
1886
|
note(PAIR_FAILED, {
|
|
1725
|
-
|
|
1726
|
-
eventId,
|
|
1887
|
+
...at,
|
|
1727
1888
|
failedChecks: [...reading.failedChecks],
|
|
1728
1889
|
});
|
|
1729
1890
|
} else if (reading.verdict === "verified") {
|
|
@@ -1738,10 +1899,17 @@ export async function readCase(
|
|
|
1738
1899
|
// none.
|
|
1739
1900
|
for (const slot of undeclaredSlots) {
|
|
1740
1901
|
if (handoffRecords > 0) {
|
|
1741
|
-
exceptions[slot.exceptionAt] = {
|
|
1902
|
+
exceptions[slot.exceptionAt] = {
|
|
1903
|
+
kind: PACK_UNDECLARED,
|
|
1904
|
+
packRef: slot.ref,
|
|
1905
|
+
packIndex: slot.position,
|
|
1906
|
+
};
|
|
1742
1907
|
contributions[slot.contributionAt] = [slot.ref, EXCEPTION_VERDICT[PACK_UNDECLARED] as CaseVerdict];
|
|
1743
1908
|
} else {
|
|
1744
|
-
caseNotes.push({
|
|
1909
|
+
caseNotes.push({
|
|
1910
|
+
position: slot.position,
|
|
1911
|
+
note: { kind: PACK_UNDECLARED, packRef: slot.ref, packIndex: slot.position },
|
|
1912
|
+
});
|
|
1745
1913
|
}
|
|
1746
1914
|
}
|
|
1747
1915
|
const named = exceptions.filter((e): e is CaseException => e !== null);
|
|
@@ -1753,6 +1921,54 @@ export async function readCase(
|
|
|
1753
1921
|
.sort((a, b) => a.position - b.position || a.i - b.i)
|
|
1754
1922
|
.map((n) => n.note);
|
|
1755
1923
|
|
|
1924
|
+
// Every record of every pack, with its status (A43). A record that went
|
|
1925
|
+
// into the pile is `read_flagged` where any exception names it by pack and
|
|
1926
|
+
// record position, or its pack's declaration check names it (by deviation
|
|
1927
|
+
// or by content finding), and `read_clean` where neither does; every other
|
|
1928
|
+
// record is `not_read`, with the kind of the exception that says why.
|
|
1929
|
+
const flagged = new Set(
|
|
1930
|
+
named
|
|
1931
|
+
.filter((e) => e.packIndex !== undefined && e.recordIndex !== undefined)
|
|
1932
|
+
.map((e) => `${e.packIndex}:${e.recordIndex}`),
|
|
1933
|
+
);
|
|
1934
|
+
const records: CaseRecordReading[] = slots.map((slot) => {
|
|
1935
|
+
const base = {
|
|
1936
|
+
packRef: slot.packRef,
|
|
1937
|
+
packIndex: slot.position,
|
|
1938
|
+
eventId: slot.eventId,
|
|
1939
|
+
recordIndex: slot.recordIndex,
|
|
1940
|
+
};
|
|
1941
|
+
const byCheck = [...(declarationFindings.get(slot.position)?.get(slot.eventId) ?? [])];
|
|
1942
|
+
if (slot.heldAt === null) {
|
|
1943
|
+
return {
|
|
1944
|
+
...base,
|
|
1945
|
+
status: "not_read",
|
|
1946
|
+
reason: slot.reason,
|
|
1947
|
+
declarationFindings: byCheck,
|
|
1948
|
+
verdict: null,
|
|
1949
|
+
findings: [],
|
|
1950
|
+
failedChecks: [],
|
|
1951
|
+
counterparts: [],
|
|
1952
|
+
role: null,
|
|
1953
|
+
};
|
|
1954
|
+
}
|
|
1955
|
+
const reading = readings[slot.heldAt];
|
|
1956
|
+
return {
|
|
1957
|
+
...base,
|
|
1958
|
+
status:
|
|
1959
|
+
flagged.has(`${slot.position}:${slot.recordIndex}`) || byCheck.length > 0
|
|
1960
|
+
? "read_flagged"
|
|
1961
|
+
: "read_clean",
|
|
1962
|
+
reason: null,
|
|
1963
|
+
declarationFindings: byCheck,
|
|
1964
|
+
verdict: reading.verdict,
|
|
1965
|
+
findings: [...reading.findings],
|
|
1966
|
+
failedChecks: [...reading.failedChecks],
|
|
1967
|
+
counterparts: [...reading.candidates],
|
|
1968
|
+
role: rolesByPack.get(slot.position)?.get(slot.eventId) ?? null,
|
|
1969
|
+
};
|
|
1970
|
+
});
|
|
1971
|
+
|
|
1756
1972
|
return {
|
|
1757
1973
|
verdict: verdictOf(named, contributed),
|
|
1758
1974
|
exceptions: named,
|
|
@@ -1765,6 +1981,19 @@ export async function readCase(
|
|
|
1765
1981
|
};
|
|
1766
1982
|
}
|
|
1767
1983
|
|
|
1984
|
+
/**
|
|
1985
|
+
* The records a pack LISTS where this reader does not read them one by one
|
|
1986
|
+
* (A43): the events `resolveTraceEvents` presents, which unwraps a
|
|
1987
|
+
* `sanning.evidence.export/v2` to its inline source bundle, and otherwise
|
|
1988
|
+
* `body.events` as `packRecords` reads it. Listing is not reading: every record
|
|
1989
|
+
* listed here is `not_read`.
|
|
1990
|
+
*/
|
|
1991
|
+
function listedRecords(bundle: unknown, content: CasePack["content"]): PackRecord[] {
|
|
1992
|
+
const events = resolveTraceEvents(bundle);
|
|
1993
|
+
if (events === undefined) return packRecords(bundle, content);
|
|
1994
|
+
return packRecords({ body: { events } }, content);
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1768
1997
|
/**
|
|
1769
1998
|
* `body.declaration.declaration_version` where it is one this reader knows,
|
|
1770
1999
|
* and `null` otherwise.
|