@sanning/proof 0.12.0 → 0.14.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 +25 -12
- package/dist/agent-proof.d.ts +2 -2
- package/dist/agent-proof.js +4 -4
- package/dist/case-file.d.ts +126 -0
- package/dist/case-file.js +353 -0
- package/dist/case.d.ts +217 -26
- package/dist/case.js +535 -124
- package/dist/cli.d.ts +64 -8
- package/dist/cli.js +799 -219
- package/dist/compose.js +6 -1
- package/dist/crypto.d.ts +1 -1
- package/dist/crypto.js +5 -1
- package/dist/declaration.d.ts +58 -52
- package/dist/declaration.js +256 -33
- package/dist/evidence.d.ts +38 -0
- package/dist/evidence.js +238 -22
- package/dist/exit-codes.d.ts +17 -0
- package/dist/exit-codes.js +50 -0
- package/dist/file-match.d.ts +105 -0
- package/dist/file-match.js +374 -0
- package/dist/grading.d.ts +28 -13
- package/dist/grading.js +90 -31
- package/dist/index.d.ts +14 -7
- package/dist/index.js +20 -6
- package/dist/message-parts.d.ts +18 -10
- package/dist/message-parts.js +25 -15
- package/dist/types.d.ts +1 -0
- package/dist/verifier.d.ts +63 -4
- package/dist/verifier.js +223 -15
- package/dist/witness-cache.d.ts +85 -0
- package/dist/witness-cache.js +264 -0
- package/dist/zip-reader.d.ts +50 -0
- package/dist/zip-reader.js +220 -0
- package/package.json +2 -2
- package/src/agent-proof.ts +4 -4
- package/src/case-file.ts +422 -0
- package/src/case.ts +671 -131
- package/src/cli.ts +932 -217
- package/src/compose.ts +8 -1
- package/src/crypto.ts +5 -1
- package/src/declaration.ts +248 -52
- package/src/evidence.ts +257 -23
- package/src/exit-codes.ts +54 -0
- package/src/file-match.ts +461 -0
- package/src/grading.ts +109 -52
- package/src/index.ts +101 -2
- package/src/message-parts.ts +25 -15
- package/src/types.ts +5 -0
- package/src/verifier.ts +225 -16
- package/src/witness-cache.ts +306 -0
- package/src/zip-reader.ts +229 -0
- package/vectors/README.md +66 -8
- package/vectors/case-file-index.json +668 -0
- package/vectors/case-split.json +218 -9
- package/vectors/file-match.json +1808 -0
- package/vectors/jcs-reader.json +82 -3
- package/vectors/pack-grading.json +68 -4
- package/vectors/stamp-on-chain.json +20 -5
- package/vectors/witness-cache.json +1304 -0
package/dist/case.js
CHANGED
|
@@ -166,7 +166,9 @@
|
|
|
166
166
|
* gets, since that reads wrapper keys.
|
|
167
167
|
*
|
|
168
168
|
* Not exits: a record whose envelope verifies and whose content does not
|
|
169
|
-
* match its commitment is `RECORD_CONTENT_MISMATCH
|
|
169
|
+
* match its commitment is `RECORD_CONTENT_MISMATCH` where that content is
|
|
170
|
+
* inside the signed body and `RECORD_CONTENT_DEPENDENT` where it was handed
|
|
171
|
+
* in beside the pack (proof#159), and one that fails
|
|
170
172
|
* another check inside its pack is `RECORD_DOES_NOT_VERIFY_IN_ITS_PACK`.
|
|
171
173
|
* Both are named and both STAY in the pile, so neither unsigned content
|
|
172
174
|
* handed in beside a pack nor a broken inclusion proof removes a record
|
|
@@ -207,7 +209,9 @@
|
|
|
207
209
|
* verdict, exactly as `counterpart_absent` and `not_a_handoff_record` are
|
|
208
210
|
* findings beside a verdict in 4.1.
|
|
209
211
|
*
|
|
210
|
-
* A case with
|
|
212
|
+
* A case with any `failed` contribution is `failed`: a proven failure keeps
|
|
213
|
+
* the headline (5.5.4 v1.16, A17, proof#158). Otherwise a case with more than
|
|
214
|
+
* one outcome is `mixed` (anchor#198, criterion 4).
|
|
211
215
|
*
|
|
212
216
|
* WHAT THIS READER DOES NOT CLAIM
|
|
213
217
|
* ===============================
|
|
@@ -237,9 +241,11 @@
|
|
|
237
241
|
* sequence. Narrowing any of it here with `handoffs[]` is precisely the move
|
|
238
242
|
* that would reopen proof#137's door.
|
|
239
243
|
*/
|
|
240
|
-
import { checkDeclaration, declarationClaims, packRecords, } from "./declaration.js";
|
|
244
|
+
import { DECLARATION_VERSION, DECLARATION_VERSION_V2, checkDeclaration, declarationClaims, packRecords, } from "./declaration.js";
|
|
241
245
|
import { NOT_A_HANDOFF_FINDING, normalizedSigningKey, readHandoffs, recordIsMalformed, } from "./handoff.js";
|
|
242
|
-
import { verifyEvidenceBundle } from "./evidence.js";
|
|
246
|
+
import { coSignaturesPresent, verifyEvidenceBundle } from "./evidence.js";
|
|
247
|
+
import { accountDisclosure, completenessNotes, gradePack, } from "./grading.js";
|
|
248
|
+
import { renderWitnessTime } from "./witness-cache.js";
|
|
243
249
|
// ---- The vocabulary ---------------------------------------------------------
|
|
244
250
|
/**
|
|
245
251
|
* The verdicts a case can reach. Every one is already in use in
|
|
@@ -578,17 +584,26 @@ export const PACK_KEY_UNREADABLE = "pack_key_unreadable";
|
|
|
578
584
|
*/
|
|
579
585
|
export const PACK_DOES_NOT_VERIFY = "pack_does_not_verify";
|
|
580
586
|
/**
|
|
581
|
-
* The pack's wrapper HOLDS and its
|
|
582
|
-
*
|
|
583
|
-
*
|
|
584
|
-
*
|
|
585
|
-
*
|
|
586
|
-
*
|
|
587
|
-
*
|
|
588
|
-
*
|
|
589
|
-
*
|
|
587
|
+
* The pack's wrapper HOLDS and its CASE GRADE is not `verified`
|
|
588
|
+
* (evidence-bundle 5.5.1 step 1 v1.20, A23; it replaces
|
|
589
|
+
* `pack_bundle_not_verified`, which carried the bundle status alone). The case
|
|
590
|
+
* grade is the pack grade of 5.4.5, taken twice: the SIGNED grade over the run
|
|
591
|
+
* with the side map empty, and the FULL grade over the run with it. The signed
|
|
592
|
+
* grade decides where it is `failed` or `malformed`; a failure only the full
|
|
593
|
+
* grade reaches is `incomplete`, because it rests on bytes nobody signed;
|
|
594
|
+
* otherwise the full grade. A body the verifier could not read is `malformed`,
|
|
595
|
+
* a body that is not one of the two event-carrying bodies takes the signed
|
|
596
|
+
* run's status, and a verifier that graded a different number of records than
|
|
597
|
+
* the pack holds takes the signed status, or `malformed` where that status is
|
|
598
|
+
* `verified`.
|
|
599
|
+
*
|
|
600
|
+
* The exception carries `status`, the case grade; `causes`, the 5.4.5 cause
|
|
601
|
+
* kinds of the grade that decided it (none in the first three branches); and
|
|
602
|
+
* `reasons`, the verifier's reasons from the run with the side map empty. It
|
|
603
|
+
* CONTRIBUTES the grade, and `incomplete` for `pending`, `not_found` or
|
|
604
|
+
* `mixed`, so the case verdict never reads above its weakest pack.
|
|
590
605
|
*/
|
|
591
|
-
export const
|
|
606
|
+
export const PACK_GRADE_NOT_VERIFIED = "pack_grade_not_verified";
|
|
592
607
|
/**
|
|
593
608
|
* A record in a pack whose wrapper holds, whose OWN envelope does not verify
|
|
594
609
|
* (`EventResult.envelopeOk` is false). Not read by 4.1, and NAMED with the
|
|
@@ -609,12 +624,39 @@ export const RECORD_ENVELOPE_DOES_NOT_VERIFY = "record_envelope_does_not_verify"
|
|
|
609
624
|
export const RECORD_DOES_NOT_VERIFY_IN_ITS_PACK = "record_does_not_verify_in_its_pack";
|
|
610
625
|
/**
|
|
611
626
|
* A record whose envelope verifies and whose disclosed content does not hash to
|
|
612
|
-
* the
|
|
613
|
-
*
|
|
627
|
+
* the hash it commits to, its `content_hash` or a hand-off record's
|
|
628
|
+
* `message_hash` (evidence-bundle v1.19, A20) (`EventResult.contentOk` is false), with the
|
|
629
|
+
* side map EMPTY: the content is inside the signed body. The same mismatch from
|
|
630
|
+
* content handed in beside the pack is `RECORD_CONTENT_DEPENDENT` (proof#159).
|
|
631
|
+
* `failed`, and the record STAYS in the pile: content beside a pack is unsigned and often
|
|
614
632
|
* supplied by whoever assembled the case, and gate 3 on proof#140 showed that
|
|
615
633
|
* party erasing an honest verified pair by swapping it.
|
|
616
634
|
*/
|
|
617
635
|
export const RECORD_CONTENT_MISMATCH = "record_content_mismatch";
|
|
636
|
+
/**
|
|
637
|
+
* The two readings proof#147 did not reach, given its shape (proof#159, ruled
|
|
638
|
+
* by Phil 2026-09-22, evidence-bundle.md 5.5.4 v1.16, A17). A pack whose
|
|
639
|
+
* bundle verdict is `failed` ONLY when read with the unsigned content handed
|
|
640
|
+
* in beside it, and `failed` no longer when read with that map empty, is
|
|
641
|
+
* `PACK_BUNDLE_CONTENT_DEPENDENT`; a record whose content mismatch comes from
|
|
642
|
+
* that map alone is `RECORD_CONTENT_DEPENDENT`. Both are `incomplete`: the
|
|
643
|
+
* reader established that the bytes it was handed are not the bytes the record
|
|
644
|
+
* commits to, and nothing about the party that signed the record, because
|
|
645
|
+
* whoever assembled the case chose those bytes with no key. Up to v1.15 both
|
|
646
|
+
* readings carried `failed`, and one flipped nibble of accuser-supplied content
|
|
647
|
+
* put a `failed` against an intact pack; under A17's headline rule that would
|
|
648
|
+
* have become the case's headline.
|
|
649
|
+
*
|
|
650
|
+
* The test that separates the two is the one proof#159 states for the inverse:
|
|
651
|
+
* every deviation a signature covers still reads `failed` with the side map
|
|
652
|
+
* EMPTY. So the pack is verified twice, with the map and without it, and what
|
|
653
|
+
* fails without it is `PACK_GRADE_NOT_VERIFIED`, `RECORD_CONTENT_MISMATCH`
|
|
654
|
+
* (content inside the signed body) or `RECORD_DOES_NOT_VERIFY_IN_ITS_PACK`,
|
|
655
|
+
* each still `failed`. Emptying the map is the most a party that signed
|
|
656
|
+
* nothing can do to starve the check, and it cannot remove any of those.
|
|
657
|
+
*/
|
|
658
|
+
export const PACK_BUNDLE_CONTENT_DEPENDENT = "pack_bundle_content_dependent";
|
|
659
|
+
export const RECORD_CONTENT_DEPENDENT = "record_content_dependent";
|
|
618
660
|
/**
|
|
619
661
|
* A record signed under a ratified profile that is under quarantine
|
|
620
662
|
* (`CASE_QUARANTINED_PROFILES`, pending DL-63). Not read, in any kernel.
|
|
@@ -624,11 +666,25 @@ export const RECORD_PROFILE_QUARANTINED = "record_profile_quarantined";
|
|
|
624
666
|
/** The reader was handed no packs at all. */
|
|
625
667
|
export const CASE_HOLDS_NO_PACKS = "case_holds_no_packs";
|
|
626
668
|
/**
|
|
627
|
-
*
|
|
628
|
-
*
|
|
629
|
-
*
|
|
669
|
+
* The three NOTEs (5.5.3 v1.20, extended proof#148): findings on the reading
|
|
670
|
+
* with no contribution, printed in the scope sentence so a clean verdict
|
|
671
|
+
* never hides any of them. `pack_undeclared` is a note only where the
|
|
672
|
+
* reading holds no hand-off record; where it holds one, it is the exception
|
|
673
|
+
* of that name.
|
|
630
674
|
*/
|
|
631
|
-
export const
|
|
675
|
+
export const CONTENT_WITHHELD_BY_DECLARATION = "content_withheld_by_declaration";
|
|
676
|
+
/**
|
|
677
|
+
* A pack's wrapper carries a present `co_signatures` (evidence-bundle.md §2,
|
|
678
|
+
* decided by Phil 2026-09-24 on proof#148). The field verifies nothing and
|
|
679
|
+
* sits outside the wrapper's signed scope, so the case reading names it
|
|
680
|
+
* rather than presenting it as evidence, and it contributes no verdict.
|
|
681
|
+
*/
|
|
682
|
+
export const CO_SIGNATURES_PRESENT = "co_signatures_present";
|
|
683
|
+
export const CASE_NOTE_KINDS = [
|
|
684
|
+
PACK_UNDECLARED,
|
|
685
|
+
CONTENT_WITHHELD_BY_DECLARATION,
|
|
686
|
+
CO_SIGNATURES_PRESENT,
|
|
687
|
+
];
|
|
632
688
|
/**
|
|
633
689
|
* Every exception this reader can report, as a closed set. A surface may render
|
|
634
690
|
* them and a test may assert over them; nothing outside this array is emitted.
|
|
@@ -652,13 +708,14 @@ export const CASE_EXCEPTION_KINDS = [
|
|
|
652
708
|
PACK_HOLDS_NO_RECORDS,
|
|
653
709
|
PACK_KEY_UNREADABLE,
|
|
654
710
|
PACK_DOES_NOT_VERIFY,
|
|
655
|
-
|
|
711
|
+
PACK_GRADE_NOT_VERIFIED,
|
|
712
|
+
PACK_BUNDLE_CONTENT_DEPENDENT,
|
|
656
713
|
RECORD_ENVELOPE_DOES_NOT_VERIFY,
|
|
657
714
|
RECORD_DOES_NOT_VERIFY_IN_ITS_PACK,
|
|
658
715
|
RECORD_CONTENT_MISMATCH,
|
|
716
|
+
RECORD_CONTENT_DEPENDENT,
|
|
659
717
|
RECORD_PROFILE_QUARANTINED,
|
|
660
718
|
CASE_HOLDS_NO_PACKS,
|
|
661
|
-
CASE_HOLDS_NO_HANDOFF_RECORDS,
|
|
662
719
|
];
|
|
663
720
|
/**
|
|
664
721
|
* What each exception kind contributes to the case verdict. It is a TABLE and
|
|
@@ -671,10 +728,10 @@ export const CASE_EXCEPTION_KINDS = [
|
|
|
671
728
|
* contributes a clean verdict, which is what makes the invariant in `verdictOf`
|
|
672
729
|
* hold.
|
|
673
730
|
*
|
|
674
|
-
* `
|
|
675
|
-
* contributes the pack's
|
|
676
|
-
*
|
|
677
|
-
*
|
|
731
|
+
* `PACK_GRADE_NOT_VERIFIED` is the one row that is not a constant: it
|
|
732
|
+
* contributes the pack's case grade, which is never `verified` (the exception
|
|
733
|
+
* is emitted only when it is not). `null` marks that row, and `readCase` fills
|
|
734
|
+
* it from `status`.
|
|
678
735
|
*/
|
|
679
736
|
const EXCEPTION_VERDICT = {
|
|
680
737
|
[COUNTERPART_ABSENT]: "incomplete",
|
|
@@ -695,14 +752,26 @@ const EXCEPTION_VERDICT = {
|
|
|
695
752
|
[PACK_HOLDS_NO_RECORDS]: "incomplete",
|
|
696
753
|
[PACK_KEY_UNREADABLE]: "incomplete",
|
|
697
754
|
[PACK_DOES_NOT_VERIFY]: "failed",
|
|
698
|
-
[
|
|
755
|
+
[PACK_GRADE_NOT_VERIFIED]: null,
|
|
756
|
+
[PACK_BUNDLE_CONTENT_DEPENDENT]: "incomplete",
|
|
699
757
|
[RECORD_ENVELOPE_DOES_NOT_VERIFY]: "failed",
|
|
700
758
|
[RECORD_DOES_NOT_VERIFY_IN_ITS_PACK]: "failed",
|
|
701
759
|
[RECORD_CONTENT_MISMATCH]: "failed",
|
|
760
|
+
[RECORD_CONTENT_DEPENDENT]: "incomplete",
|
|
702
761
|
[RECORD_PROFILE_QUARANTINED]: "incomplete",
|
|
703
762
|
[CASE_HOLDS_NO_PACKS]: "partial",
|
|
704
|
-
[CASE_HOLDS_NO_HANDOFF_RECORDS]: "partial",
|
|
705
763
|
};
|
|
764
|
+
/**
|
|
765
|
+
* The case grades `PACK_GRADE_NOT_VERIFIED` contributes as themselves. Every
|
|
766
|
+
* other grade a pack can reach (`pending`, `not_found`, `mixed`, which a pack
|
|
767
|
+
* grade passes through and a case verdict never issues) is `incomplete`.
|
|
768
|
+
*/
|
|
769
|
+
const GRADE_CONTRIBUTES = new Set([
|
|
770
|
+
"partial",
|
|
771
|
+
"incomplete",
|
|
772
|
+
"failed",
|
|
773
|
+
"malformed",
|
|
774
|
+
]);
|
|
706
775
|
// ---- The reading ------------------------------------------------------------
|
|
707
776
|
const bodyTypeOf = (bundle) => typeof bundle === "object" && bundle !== null
|
|
708
777
|
? bundle["body_type"]
|
|
@@ -740,6 +809,23 @@ const wrapperHolds = (result) => result.signatureOk && result.bodyHashOk;
|
|
|
740
809
|
* bundle-level error at all, and an exception that named the verdict and no
|
|
741
810
|
* reason would send a reader looking for a defect in the pack as a whole.
|
|
742
811
|
*/
|
|
812
|
+
/**
|
|
813
|
+
* The reasons a reading carries only because of the side map, in the order the
|
|
814
|
+
* verifier wrote them: what the content-dependent kinds are about, and nothing
|
|
815
|
+
* a signature covers.
|
|
816
|
+
*/
|
|
817
|
+
const onlyIn = (withMap, withoutMap) => withMap.filter((reason) => !withoutMap.includes(reason));
|
|
818
|
+
/**
|
|
819
|
+
* Whether a side-content map holds any key. Python's `if pack.content` is
|
|
820
|
+
* the twin: `None` and an empty map both mean the two verify runs are one run.
|
|
821
|
+
*/
|
|
822
|
+
function sideMapHasKeys(content) {
|
|
823
|
+
if (content === null || content === undefined)
|
|
824
|
+
return false;
|
|
825
|
+
if (content instanceof Map)
|
|
826
|
+
return content.size > 0;
|
|
827
|
+
return Object.keys(content).length > 0;
|
|
828
|
+
}
|
|
743
829
|
const bundleReasons = (result) => [
|
|
744
830
|
...result.errors,
|
|
745
831
|
...(result.stamps ?? []).flatMap((stamp) => stamp.errors),
|
|
@@ -775,8 +861,13 @@ function underQuarantine(specVersion) {
|
|
|
775
861
|
/**
|
|
776
862
|
* The case verdict, from the contributions alone.
|
|
777
863
|
*
|
|
778
|
-
*
|
|
779
|
-
*
|
|
864
|
+
* **A proven failure keeps the headline** (`evidence-bundle.md` 5.5.4 v1.16,
|
|
865
|
+
* A17, proof#158): if any contribution is `failed`, the case is `failed`,
|
|
866
|
+
* whatever else contributes, and every other exception stays named beside it.
|
|
867
|
+
* Otherwise one outcome is that outcome, and more than one is `mixed`
|
|
868
|
+
* (anchor#198, criterion 4), so `mixed` never holds a `failed`. `malformed`
|
|
869
|
+
* does NOT keep the headline: it says a record cannot be read, not that a
|
|
870
|
+
* check ran and did not pass. `mixed` is in the 3 vocabulary and coins no word.
|
|
780
871
|
*
|
|
781
872
|
* THE INVARIANT, stated so a mutation test has something to kill: `verified` is
|
|
782
873
|
* reachable ONLY when `exceptions` is empty. Every exception kind carries a
|
|
@@ -785,14 +876,25 @@ function underQuarantine(specVersion) {
|
|
|
785
876
|
* The assertion is here anyway, because that is the property every one of the
|
|
786
877
|
* seventeen findings on this program violated.
|
|
787
878
|
*/
|
|
788
|
-
function verdictOf(exceptions, contributions) {
|
|
789
|
-
//
|
|
790
|
-
//
|
|
791
|
-
//
|
|
792
|
-
|
|
793
|
-
|
|
879
|
+
function verdictOf(exceptions, contributions, holdsPacks = true) {
|
|
880
|
+
// A reading that holds at least one pack and no contribution at all is
|
|
881
|
+
// `verified` (5.5.4 v1.20, A23): every pack in it graded `verified` and
|
|
882
|
+
// nothing in it was an exception. Every exception contributes, so no
|
|
883
|
+
// contribution means no exception. A reading with no packs always carries
|
|
884
|
+
// `case_holds_no_packs`, and is defended here anyway: an empty aggregate over
|
|
885
|
+
// nothing reading as `verified` is the defect class itself.
|
|
886
|
+
if (contributions.length === 0) {
|
|
887
|
+
if (exceptions.length > 0) {
|
|
888
|
+
throw new Error(`exceptions with no contribution: ${exceptions.map((e) => e.kind).join(", ")}`);
|
|
889
|
+
}
|
|
890
|
+
return holdsPacks ? "verified" : "partial";
|
|
891
|
+
}
|
|
794
892
|
const outcomes = new Set(contributions.map(([, v]) => v));
|
|
795
|
-
const verdict = outcomes.
|
|
893
|
+
const verdict = outcomes.has("failed")
|
|
894
|
+
? "failed"
|
|
895
|
+
: outcomes.size === 1
|
|
896
|
+
? [...outcomes][0]
|
|
897
|
+
: "mixed";
|
|
796
898
|
if (verdict === "verified" && exceptions.length > 0) {
|
|
797
899
|
throw new Error(`verified over a non-empty exception list: ${exceptions.map((e) => e.kind).join(", ")}`);
|
|
798
900
|
}
|
|
@@ -826,23 +928,35 @@ function verdictOf(exceptions, contributions) {
|
|
|
826
928
|
* see is the counterpart that is missing.
|
|
827
929
|
*/
|
|
828
930
|
export async function readCase(packs) {
|
|
931
|
+
// A `null` slot is held for each pack that carries no declaration, and
|
|
932
|
+
// settled once the pile says whether the reading holds a hand-off record.
|
|
829
933
|
const exceptions = [];
|
|
830
934
|
const contributions = [];
|
|
831
935
|
const note = (kind, fields = {}) => {
|
|
832
936
|
exceptions.push({ kind, ...fields });
|
|
833
|
-
// `null` is
|
|
834
|
-
|
|
937
|
+
// `null` is PACK_GRADE_NOT_VERIFIED: the pack's case grade, never
|
|
938
|
+
// verified, and `incomplete` for a word a pack grade passes through and a
|
|
939
|
+
// case verdict never issues (5.5.1 step 1 v1.20).
|
|
940
|
+
let contribution = EXCEPTION_VERDICT[kind];
|
|
941
|
+
if (contribution === null) {
|
|
942
|
+
const status = fields.status;
|
|
943
|
+
contribution = GRADE_CONTRIBUTES.has(status) ? status : "incomplete";
|
|
944
|
+
}
|
|
835
945
|
contributions.push([fields.eventId ?? fields.packRef ?? "case", contribution]);
|
|
836
946
|
};
|
|
837
947
|
if (packs.length === 0) {
|
|
838
948
|
note(CASE_HOLDS_NO_PACKS);
|
|
949
|
+
const named = exceptions;
|
|
950
|
+
const contributed = contributions;
|
|
839
951
|
return {
|
|
840
|
-
verdict: verdictOf(
|
|
841
|
-
exceptions,
|
|
952
|
+
verdict: verdictOf(named, contributed, false),
|
|
953
|
+
exceptions: named,
|
|
842
954
|
records: [],
|
|
843
|
-
contributions,
|
|
955
|
+
contributions: contributed,
|
|
844
956
|
handoffRecords: 0,
|
|
845
957
|
onChainChecked: false,
|
|
958
|
+
notes: [],
|
|
959
|
+
packs: [],
|
|
846
960
|
};
|
|
847
961
|
}
|
|
848
962
|
// Step 1 and 2, pack by pack, in the order the reader was given them.
|
|
@@ -884,6 +998,15 @@ export async function readCase(packs) {
|
|
|
884
998
|
// below, from the pack envelope and never from the body, so no body type, no
|
|
885
999
|
// disclosure choice and no record content decides whether it is read.
|
|
886
1000
|
const holderKeys = new Set();
|
|
1001
|
+
// One reading per pack, in the order the reader was given them (5.5.4 v1.20).
|
|
1002
|
+
const packReadings = [];
|
|
1003
|
+
// The NOTEs (5.5.3 v1.20), with the pack position they are about.
|
|
1004
|
+
const caseNotes = [];
|
|
1005
|
+
// The role each pack's readable v2 declaration signs for each record, keyed
|
|
1006
|
+
// by the pack's POSITION for the reason `claimsByPack` is.
|
|
1007
|
+
const rolesByPack = new Map();
|
|
1008
|
+
// The undeclared packs' slots, settled after the pile is read.
|
|
1009
|
+
const undeclaredSlots = [];
|
|
887
1010
|
// How many packs this reader could not tie to a key: an unreadable wrapper
|
|
888
1011
|
// key, or a wrapper that does not hold. While it is not zero, "no pack in this
|
|
889
1012
|
// case is signed by the named key" is not established, so the innocent
|
|
@@ -899,6 +1022,13 @@ export async function readCase(packs) {
|
|
|
899
1022
|
// of these spellings (proof#135) and the reading must not.
|
|
900
1023
|
note(PACK_KEY_UNREADABLE, { packRef: pack.ref });
|
|
901
1024
|
unattributed += 1;
|
|
1025
|
+
packReadings.push({
|
|
1026
|
+
ref: pack.ref,
|
|
1027
|
+
signingKey: null,
|
|
1028
|
+
grade: null,
|
|
1029
|
+
recordsWithheld: 0,
|
|
1030
|
+
declarationVersion: null,
|
|
1031
|
+
});
|
|
902
1032
|
continue;
|
|
903
1033
|
}
|
|
904
1034
|
// The precondition `HeldRecord` states, met HERE rather than trusted to a
|
|
@@ -910,28 +1040,130 @@ export async function readCase(packs) {
|
|
|
910
1040
|
content: (pack.content ?? undefined),
|
|
911
1041
|
});
|
|
912
1042
|
witnessChecked.push(verified.onChainChecked);
|
|
1043
|
+
// The same pack with the side map EMPTY (proof#159): what fails here is
|
|
1044
|
+
// what a signature covers, and what fails only above rests on bytes nobody
|
|
1045
|
+
// signed. Skipped where there is no map, because the two runs are then the
|
|
1046
|
+
// same run.
|
|
1047
|
+
const signed = sideMapHasKeys(pack.content)
|
|
1048
|
+
? await verifyEvidenceBundle(pack.bundle, {})
|
|
1049
|
+
: verified;
|
|
913
1050
|
// WHOSE PACK IS IT. The wrapper, and only the wrapper.
|
|
914
1051
|
if (!wrapperHolds(verified)) {
|
|
915
1052
|
note(PACK_DOES_NOT_VERIFY, { packRef: pack.ref });
|
|
916
1053
|
unattributed += 1;
|
|
1054
|
+
packReadings.push({
|
|
1055
|
+
ref: pack.ref,
|
|
1056
|
+
signingKey: holder,
|
|
1057
|
+
grade: null,
|
|
1058
|
+
recordsWithheld: 0,
|
|
1059
|
+
declarationVersion: null,
|
|
1060
|
+
});
|
|
917
1061
|
continue;
|
|
918
1062
|
}
|
|
919
1063
|
holderKeys.add(holder);
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
1064
|
+
if (coSignaturesPresent(pack.bundle)) {
|
|
1065
|
+
// A NOTE, with no contribution (5.5.3 v1.20, proof#148): the wrapper's
|
|
1066
|
+
// co_signatures verifies nothing and sits outside its signed scope, so
|
|
1067
|
+
// the case reading names it rather than moving anything on its account.
|
|
1068
|
+
caseNotes.push({
|
|
1069
|
+
position,
|
|
1070
|
+
note: { kind: CO_SIGNATURES_PRESENT, packRef: pack.ref },
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
const bodyType = bodyTypeOf(pack.bundle);
|
|
1074
|
+
const records = packRecords(pack.bundle, pack.content);
|
|
1075
|
+
// The per-record gradings below are joined to `records` BY INDEX, so a
|
|
1076
|
+
// verifier that graded a different number of records than the pack holds
|
|
1077
|
+
// leaves this loop with no grading to read. That is not hypothetical: a
|
|
1078
|
+
// pack whose `body_hash` does not cover its body reads `failed` with
|
|
1079
|
+
// `events` EMPTY while `packRecords` reads every record in it, and only
|
|
1080
|
+
// `wrapperHolds` above stands between that pack and this loop. A reader
|
|
1081
|
+
// must never crash on a pack a party can build, so the gap is graded rather
|
|
1082
|
+
// than indexed into, and the pack's records are dropped exactly as a
|
|
1083
|
+
// `malformed` pack's are (gate 4 on proof#140, finding 3).
|
|
1084
|
+
const ungraded = verified.events.length !== records.length || signed.events.length !== records.length;
|
|
1085
|
+
const knownBody = CASE_BODY_TYPES.includes(bodyType);
|
|
1086
|
+
// WHAT IS WRONG INSIDE IT, from here on. First the pack's own GRADE, which
|
|
1087
|
+
// propagates: the case never reads above its weakest pack (5.5.1 step 1
|
|
1088
|
+
// v1.20, A23; up to v1.19 it was the bundle status).
|
|
1089
|
+
let check = null;
|
|
1090
|
+
let full = null;
|
|
1091
|
+
let causes = [];
|
|
1092
|
+
let grade;
|
|
1093
|
+
if (signed.status === "malformed") {
|
|
1094
|
+
grade = "malformed";
|
|
1095
|
+
}
|
|
1096
|
+
else if (!knownBody) {
|
|
1097
|
+
grade = signed.status;
|
|
1098
|
+
}
|
|
1099
|
+
else if (ungraded) {
|
|
1100
|
+
grade = signed.status === "verified" ? "malformed" : signed.status;
|
|
1101
|
+
}
|
|
1102
|
+
else {
|
|
1103
|
+
check = checkDeclaration(pack.bundle, pack.content);
|
|
1104
|
+
const completeness = completenessNotes(pack.bundle);
|
|
1105
|
+
full = gradePack(verified, accountDisclosure(pack.bundle, pack.content ?? {}), completeness, check);
|
|
1106
|
+
const bareGrade = gradePack(signed, accountDisclosure(pack.bundle, {}), completeness, checkDeclaration(pack.bundle, null));
|
|
1107
|
+
// NO `failed` HERE RESTS ON BYTES NOBODY SIGNED. The signed grade decides
|
|
1108
|
+
// where it is `failed` or `malformed`; a failure only the side map
|
|
1109
|
+
// produced is `incomplete`; otherwise the full grade.
|
|
1110
|
+
if (bareGrade.verdict === "failed" || bareGrade.verdict === "malformed") {
|
|
1111
|
+
grade = bareGrade.verdict;
|
|
1112
|
+
causes = bareGrade.causes.map((c) => c.kind);
|
|
1113
|
+
}
|
|
1114
|
+
else if (full.verdict === "failed" || full.verdict === "malformed") {
|
|
1115
|
+
grade = "incomplete";
|
|
1116
|
+
causes = full.causes.map((c) => c.kind);
|
|
1117
|
+
}
|
|
1118
|
+
else {
|
|
1119
|
+
grade = full.verdict;
|
|
1120
|
+
causes = full.causes.map((c) => c.kind);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
if (grade !== "verified") {
|
|
1124
|
+
const reasons = bundleReasons(signed);
|
|
1125
|
+
if (knownBody && ungraded && signed.status !== "malformed") {
|
|
1126
|
+
// Said, because nothing else says it: the verifier's own reasons are
|
|
1127
|
+
// about the bundle, and this pack's records are dropped for a reason
|
|
1128
|
+
// of this reader's.
|
|
1129
|
+
reasons.push(`this verifier graded ${verified.events.length} of the ${records.length} ` +
|
|
1130
|
+
"record(s) the pack holds, so none of them is read");
|
|
1131
|
+
}
|
|
1132
|
+
note(PACK_GRADE_NOT_VERIFIED, {
|
|
924
1133
|
packRef: pack.ref,
|
|
925
|
-
status:
|
|
926
|
-
|
|
1134
|
+
status: grade,
|
|
1135
|
+
causes,
|
|
1136
|
+
reasons,
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
if (verified.status === "failed" && signed.status !== "failed") {
|
|
1140
|
+
note(PACK_BUNDLE_CONTENT_DEPENDENT, {
|
|
1141
|
+
packRef: pack.ref,
|
|
1142
|
+
reasons: onlyIn(bundleReasons(verified), bundleReasons(signed)),
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
const recordsWithheld = full !== null ? full.recordsWithheld : 0;
|
|
1146
|
+
packReadings.push({
|
|
1147
|
+
ref: pack.ref,
|
|
1148
|
+
signingKey: holder,
|
|
1149
|
+
grade,
|
|
1150
|
+
recordsWithheld,
|
|
1151
|
+
declarationVersion: knownBody ? declarationVersionOf(pack.bundle) : null,
|
|
1152
|
+
});
|
|
1153
|
+
if (recordsWithheld > 0) {
|
|
1154
|
+
// A NOTE, with no contribution (5.5.3 v1.20): the holder signed that
|
|
1155
|
+
// these records' content does not travel, and the scope sentence counts
|
|
1156
|
+
// them, so a clean verdict never hides them.
|
|
1157
|
+
caseNotes.push({
|
|
1158
|
+
position,
|
|
1159
|
+
note: { kind: CONTENT_WITHHELD_BY_DECLARATION, packRef: pack.ref, count: recordsWithheld },
|
|
927
1160
|
});
|
|
928
1161
|
}
|
|
929
1162
|
// The body is one the verifier could not read, so it graded no record, and
|
|
930
1163
|
// nothing below has a grading to read. Named above.
|
|
931
|
-
if (
|
|
1164
|
+
if (signed.status === "malformed")
|
|
932
1165
|
continue;
|
|
933
|
-
|
|
934
|
-
if (!CASE_BODY_TYPES.includes(bodyType)) {
|
|
1166
|
+
if (!knownBody) {
|
|
935
1167
|
// `sanning.evidence.export/v2` lands here, and it is NOT unwrapped: its
|
|
936
1168
|
// records sit under `body.source_bundle`, and reading them is a change to
|
|
937
1169
|
// which packs this reader grades, so it is a decision of its own and not
|
|
@@ -941,58 +1173,39 @@ export async function readCase(packs) {
|
|
|
941
1173
|
note(PACK_BODY_TYPE_UNKNOWN, { packRef: pack.ref });
|
|
942
1174
|
continue;
|
|
943
1175
|
}
|
|
944
|
-
|
|
945
|
-
//
|
|
946
|
-
|
|
947
|
-
// leaves this loop with no grading to read. That is not hypothetical: a
|
|
948
|
-
// pack whose `body_hash` does not cover its body reads `failed` with
|
|
949
|
-
// `events` EMPTY while `packRecords` reads every record in it, and only
|
|
950
|
-
// `wrapperHolds` above stands between that pack and this loop. A reader
|
|
951
|
-
// must never crash on a pack a party can build, so the gap is NAMED rather
|
|
952
|
-
// than indexed into, and the pack's records are dropped exactly as a
|
|
953
|
-
// `malformed` pack's are (gate 4 on proof#140, finding 3: under the
|
|
954
|
-
// mutation that dropped `bodyHashOk` from the wrapper test, this kernel
|
|
955
|
-
// threw `Cannot read properties of undefined (reading 'envelopeOk')` here
|
|
956
|
-
// and nothing in the ladder said so).
|
|
957
|
-
if (verified.events.length !== records.length) {
|
|
958
|
-
note(PACK_BUNDLE_NOT_VERIFIED, {
|
|
959
|
-
packRef: pack.ref,
|
|
960
|
-
status: verified.status,
|
|
961
|
-
reasons: [
|
|
962
|
-
`this verifier graded ${verified.events.length} of the ` +
|
|
963
|
-
`${records.length} record(s) the pack holds, so none of them is read`,
|
|
964
|
-
],
|
|
965
|
-
});
|
|
1176
|
+
// Graded above (`malformed` where the verifier called it `verified`), and
|
|
1177
|
+
// none of its records are read.
|
|
1178
|
+
if (ungraded)
|
|
966
1179
|
continue;
|
|
967
|
-
}
|
|
968
1180
|
if (records.length === 0) {
|
|
969
1181
|
note(PACK_HOLDS_NO_RECORDS, { packRef: pack.ref });
|
|
970
1182
|
continue;
|
|
971
1183
|
}
|
|
972
|
-
const
|
|
1184
|
+
const checked = check;
|
|
973
1185
|
claimsByPack.set(position, declarationClaims(pack.bundle));
|
|
974
|
-
|
|
1186
|
+
rolesByPack.set(position, new Map((checked.roles ?? []).map((r) => [r.eventId, r.role])));
|
|
1187
|
+
if (checked.contentFindings.length > 0) {
|
|
975
1188
|
// BESIDE the deviation exception and never instead of it: a pack can do
|
|
976
1189
|
// both, over different records, and the two say different things. This
|
|
977
1190
|
// one is `incomplete` because whoever assembled the case chose the bytes
|
|
978
1191
|
// it turns on (proof#147).
|
|
979
1192
|
note(PACK_DECLARATION_CONTENT_DEPENDENT, {
|
|
980
1193
|
packRef: pack.ref,
|
|
981
|
-
count:
|
|
1194
|
+
count: checked.contentFindings.length,
|
|
982
1195
|
});
|
|
983
1196
|
}
|
|
984
|
-
if (
|
|
1197
|
+
if (checked.verdict === "failed") {
|
|
985
1198
|
note(PACK_DECLARATION_DEVIATES, {
|
|
986
1199
|
packRef: pack.ref,
|
|
987
|
-
count:
|
|
1200
|
+
count: checked.deviations.length,
|
|
988
1201
|
});
|
|
989
1202
|
}
|
|
990
|
-
else if (
|
|
1203
|
+
else if (checked.contentFindings.length > 0) {
|
|
991
1204
|
// Already named above. `partial` here is not "the reader cannot read the
|
|
992
1205
|
// claim": the claim was read, and part of it rests on bytes nobody
|
|
993
1206
|
// signed.
|
|
994
1207
|
}
|
|
995
|
-
else if (
|
|
1208
|
+
else if (checked.verdict === "verified") {
|
|
996
1209
|
// A pack that agrees with its own claim contributes NOTHING, rather than
|
|
997
1210
|
// a `verified` of its own. The case verdict is over the hand-off
|
|
998
1211
|
// readings, and criterion 4's "mixed" is about mixed HAND-OFF outcomes: a
|
|
@@ -1001,7 +1214,7 @@ export async function readCase(packs) {
|
|
|
1001
1214
|
// read `mixed` and the failure would be the quieter half of a two-word
|
|
1002
1215
|
// answer. A pack-level PROBLEM still contributes, through its exception.
|
|
1003
1216
|
}
|
|
1004
|
-
else if (
|
|
1217
|
+
else if (checked.present) {
|
|
1005
1218
|
// `partial` with a declaration present is 5.4.3's "a claim this reader
|
|
1006
1219
|
// cannot read", which `gradePack` already carries to `incomplete`. The
|
|
1007
1220
|
// same posture here, for the same reason: the field has no accept-set and
|
|
@@ -1009,7 +1222,18 @@ export async function readCase(packs) {
|
|
|
1009
1222
|
note(PACK_DECLARATION_UNREADABLE, { packRef: pack.ref });
|
|
1010
1223
|
}
|
|
1011
1224
|
else {
|
|
1012
|
-
|
|
1225
|
+
// WHAT IT CONTRIBUTES TURNS ON THE WHOLE READING (5.5.1 step 2 v1.20,
|
|
1226
|
+
// A23, I4): `partial` where the reading holds a hand-off record, and a
|
|
1227
|
+
// NOTE where it holds none. That is not known until the pile is read, so
|
|
1228
|
+
// the slot is held HERE, in pack order, and settled below.
|
|
1229
|
+
undeclaredSlots.push({
|
|
1230
|
+
exceptionAt: exceptions.length,
|
|
1231
|
+
contributionAt: contributions.length,
|
|
1232
|
+
position,
|
|
1233
|
+
ref: pack.ref,
|
|
1234
|
+
});
|
|
1235
|
+
exceptions.push(null);
|
|
1236
|
+
contributions.push(null);
|
|
1013
1237
|
}
|
|
1014
1238
|
// Each record on its OWN envelope. `packRecords` and the verifier both walk
|
|
1015
1239
|
// `body.events` in order and emit one entry per element, so the index is
|
|
@@ -1017,6 +1241,7 @@ export async function readCase(packs) {
|
|
|
1017
1241
|
for (let index = 0; index < records.length; index += 1) {
|
|
1018
1242
|
const record = records[index];
|
|
1019
1243
|
const graded = verified.events[index];
|
|
1244
|
+
const bare = signed.events[index];
|
|
1020
1245
|
if (underQuarantine(recordSpecVersion(pack.bundle, index))) {
|
|
1021
1246
|
note(RECORD_PROFILE_QUARANTINED, { packRef: pack.ref, eventId: record.eventId });
|
|
1022
1247
|
continue;
|
|
@@ -1043,20 +1268,31 @@ export async function readCase(packs) {
|
|
|
1043
1268
|
});
|
|
1044
1269
|
continue;
|
|
1045
1270
|
}
|
|
1046
|
-
// Named, and the record stays in the pile (see the kinds).
|
|
1047
|
-
|
|
1271
|
+
// Named, and the record stays in the pile (see the kinds). A mismatch
|
|
1272
|
+
// the signed body carries on its own is `failed`; one that appears only
|
|
1273
|
+
// with the side map is `incomplete` (proof#159).
|
|
1274
|
+
if (bare.contentOk === false) {
|
|
1048
1275
|
note(RECORD_CONTENT_MISMATCH, {
|
|
1049
1276
|
packRef: pack.ref,
|
|
1050
1277
|
eventId: record.eventId,
|
|
1051
|
-
reasons: [...
|
|
1278
|
+
reasons: [...bare.errors],
|
|
1052
1279
|
});
|
|
1053
1280
|
}
|
|
1054
|
-
if (
|
|
1055
|
-
(
|
|
1281
|
+
else if (graded.contentOk === false) {
|
|
1282
|
+
note(RECORD_CONTENT_DEPENDENT, {
|
|
1283
|
+
packRef: pack.ref,
|
|
1284
|
+
eventId: record.eventId,
|
|
1285
|
+
reasons: onlyIn(graded.errors, bare.errors),
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
// Every other check on the SIGNED reading, so side content can neither
|
|
1289
|
+
// add a failure here nor take one away.
|
|
1290
|
+
if (!(bare.inclusionOk && bare.checkpointBound) ||
|
|
1291
|
+
(!bare.ok && bare.contentOk !== false)) {
|
|
1056
1292
|
note(RECORD_DOES_NOT_VERIFY_IN_ITS_PACK, {
|
|
1057
1293
|
packRef: pack.ref,
|
|
1058
1294
|
eventId: record.eventId,
|
|
1059
|
-
reasons: [...
|
|
1295
|
+
reasons: [...bare.errors],
|
|
1060
1296
|
});
|
|
1061
1297
|
}
|
|
1062
1298
|
held.push({
|
|
@@ -1085,6 +1321,7 @@ export async function readCase(packs) {
|
|
|
1085
1321
|
findings: [...reading.findings],
|
|
1086
1322
|
failedChecks: [...reading.failedChecks],
|
|
1087
1323
|
counterparts: [...reading.candidates],
|
|
1324
|
+
role: rolesByPack.get(position)?.get(eventId) ?? null,
|
|
1088
1325
|
});
|
|
1089
1326
|
// proof#137's door, named. Read from the ONE reader of the declaration
|
|
1090
1327
|
// (`declarationClaims`), never from a second parse here. Only a READABLE
|
|
@@ -1098,9 +1335,13 @@ export async function readCase(packs) {
|
|
|
1098
1335
|
// `not_a_handoff_record` exit it was a check the audited party switched off
|
|
1099
1336
|
// by respelling two fields, and behind the `malformed` exit it was one a
|
|
1100
1337
|
// party switched off by breaking its own record.
|
|
1338
|
+
//
|
|
1339
|
+
// v1's ALONE (5.5.1 v1.20, A21). A v2 pack's claim check is rule 9, which
|
|
1340
|
+
// the declaration check already ran on the signed body.
|
|
1101
1341
|
const claims = claimsByPack.get(position);
|
|
1102
1342
|
if (claims !== undefined &&
|
|
1103
1343
|
claims.readable &&
|
|
1344
|
+
claims.version === DECLARATION_VERSION &&
|
|
1104
1345
|
!claims.claimedSides.includes(eventId)) {
|
|
1105
1346
|
note(RECORD_UNCLAIMED, { packRef, eventId });
|
|
1106
1347
|
}
|
|
@@ -1190,17 +1431,54 @@ export async function readCase(packs) {
|
|
|
1190
1431
|
contributions.push([`${packRef}/${eventId}`, "verified"]);
|
|
1191
1432
|
}
|
|
1192
1433
|
}
|
|
1193
|
-
|
|
1194
|
-
|
|
1434
|
+
// A reading with no hand-off record in it is not an exception (5.5.3 v1.20,
|
|
1435
|
+
// A23, D6). The undeclared packs, settled: the exception `pack_undeclared`,
|
|
1436
|
+
// contributing `partial`, in the slot it was given in pack order where the
|
|
1437
|
+
// reading holds a hand-off record; a NOTE with no contribution where it holds
|
|
1438
|
+
// none.
|
|
1439
|
+
for (const slot of undeclaredSlots) {
|
|
1440
|
+
if (handoffRecords > 0) {
|
|
1441
|
+
exceptions[slot.exceptionAt] = { kind: PACK_UNDECLARED, packRef: slot.ref };
|
|
1442
|
+
contributions[slot.contributionAt] = [slot.ref, EXCEPTION_VERDICT[PACK_UNDECLARED]];
|
|
1443
|
+
}
|
|
1444
|
+
else {
|
|
1445
|
+
caseNotes.push({ position: slot.position, note: { kind: PACK_UNDECLARED, packRef: slot.ref } });
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
const named = exceptions.filter((e) => e !== null);
|
|
1449
|
+
const contributed = contributions.filter((c) => c !== null);
|
|
1450
|
+
// Pack order, stable. A pack carries at most one note: one with no
|
|
1451
|
+
// declaration withholds nothing by one.
|
|
1452
|
+
const notes = caseNotes
|
|
1453
|
+
.map((n, i) => ({ ...n, i }))
|
|
1454
|
+
.sort((a, b) => a.position - b.position || a.i - b.i)
|
|
1455
|
+
.map((n) => n.note);
|
|
1195
1456
|
return {
|
|
1196
|
-
verdict: verdictOf(
|
|
1197
|
-
exceptions,
|
|
1457
|
+
verdict: verdictOf(named, contributed),
|
|
1458
|
+
exceptions: named,
|
|
1198
1459
|
records,
|
|
1199
|
-
contributions,
|
|
1460
|
+
contributions: contributed,
|
|
1200
1461
|
handoffRecords,
|
|
1201
1462
|
onChainChecked: witnessChecked.length > 0 && witnessChecked.every((c) => c),
|
|
1463
|
+
notes,
|
|
1464
|
+
packs: packReadings,
|
|
1202
1465
|
};
|
|
1203
1466
|
}
|
|
1467
|
+
/**
|
|
1468
|
+
* `body.declaration.declaration_version` where it is one this reader knows,
|
|
1469
|
+
* and `null` otherwise.
|
|
1470
|
+
*/
|
|
1471
|
+
function declarationVersionOf(bundle) {
|
|
1472
|
+
const at = (v, k) => typeof v === "object" && v !== null && !Array.isArray(v)
|
|
1473
|
+
? v[k]
|
|
1474
|
+
: undefined;
|
|
1475
|
+
const declaration = at(at(bundle, "body"), "declaration");
|
|
1476
|
+
if (typeof declaration !== "object" || declaration === null || Array.isArray(declaration)) {
|
|
1477
|
+
return null;
|
|
1478
|
+
}
|
|
1479
|
+
const version = declaration["declaration_version"];
|
|
1480
|
+
return version === DECLARATION_VERSION || version === DECLARATION_VERSION_V2 ? version : null;
|
|
1481
|
+
}
|
|
1204
1482
|
// ---- Presentation (optional) ------------------------------------------------
|
|
1205
1483
|
/**
|
|
1206
1484
|
* The house wording for one exception.
|
|
@@ -1285,13 +1563,20 @@ export function renderException(exception) {
|
|
|
1285
1563
|
return (`${exception.packRef} carries a wrapper signature over its body that ` +
|
|
1286
1564
|
"does not verify, so none of its records are read and the key it " +
|
|
1287
1565
|
"names is not counted as its signer");
|
|
1288
|
-
case
|
|
1289
|
-
let head = `${exception.packRef}
|
|
1290
|
-
|
|
1566
|
+
case PACK_GRADE_NOT_VERIFIED: {
|
|
1567
|
+
let head = `${exception.packRef} grades ${(exception.status ?? "").toUpperCase()} as a pack`;
|
|
1568
|
+
if (exception.causes !== undefined && exception.causes.length > 0) {
|
|
1569
|
+
head += ` (${exception.causes.join(", ")})`;
|
|
1570
|
+
}
|
|
1291
1571
|
if (exception.status === "malformed")
|
|
1292
1572
|
head += ", so none of its records are read";
|
|
1293
1573
|
return head + because(exception.reasons);
|
|
1294
1574
|
}
|
|
1575
|
+
case PACK_BUNDLE_CONTENT_DEPENDENT:
|
|
1576
|
+
return (`${exception.packRef} reads FAILED as a bundle only together with ` +
|
|
1577
|
+
"the unsigned content handed in beside it, which anyone could have " +
|
|
1578
|
+
"supplied, so whether that failure is the pack's is UNKNOWN" +
|
|
1579
|
+
because(exception.reasons));
|
|
1295
1580
|
case RECORD_ENVELOPE_DOES_NOT_VERIFY:
|
|
1296
1581
|
return (`${where} is in a pack whose wrapper verifies, and its own envelope ` +
|
|
1297
1582
|
"does not verify, so it is not read as a hand-off record" +
|
|
@@ -1304,45 +1589,171 @@ export function renderException(exception) {
|
|
|
1304
1589
|
return (`${where} is read with content that does not match what it commits ` +
|
|
1305
1590
|
"to, and it is still read as a hand-off record" +
|
|
1306
1591
|
because(exception.reasons));
|
|
1592
|
+
case RECORD_CONTENT_DEPENDENT:
|
|
1593
|
+
return (`${where} commits to content that the unsigned content handed in ` +
|
|
1594
|
+
"beside its pack does not match, and anyone could have supplied " +
|
|
1595
|
+
"that content, so what it says about the record is UNKNOWN; the " +
|
|
1596
|
+
"record is still read as a hand-off record" +
|
|
1597
|
+
because(exception.reasons));
|
|
1307
1598
|
case RECORD_PROFILE_QUARANTINED:
|
|
1308
1599
|
return (`${where} is signed under a profile this case reader does not read ` +
|
|
1309
1600
|
"while its admission is undecided, so what it says is UNKNOWN");
|
|
1310
1601
|
case CASE_HOLDS_NO_PACKS:
|
|
1311
1602
|
return "this case holds no packs";
|
|
1312
|
-
case CASE_HOLDS_NO_HANDOFF_RECORDS:
|
|
1313
|
-
return ("no record this verifier read in this case is a hand-off record, " +
|
|
1314
|
-
"so no hand-off was verified");
|
|
1315
1603
|
default:
|
|
1316
1604
|
return exception.kind;
|
|
1317
1605
|
}
|
|
1318
1606
|
}
|
|
1607
|
+
/** The house wording for one NOTE. Offered, not imposed, as `renderException` is. */
|
|
1608
|
+
export function renderCaseNote(note) {
|
|
1609
|
+
switch (note.kind) {
|
|
1610
|
+
case CONTENT_WITHHELD_BY_DECLARATION:
|
|
1611
|
+
return (`${note.packRef} withholds the content of ${note.count} record(s) by ` +
|
|
1612
|
+
"its own signed declaration: their signatures and commitments were " +
|
|
1613
|
+
"checked, their content was not");
|
|
1614
|
+
case PACK_UNDECLARED:
|
|
1615
|
+
return `${note.packRef} does not say what it holds`;
|
|
1616
|
+
case CO_SIGNATURES_PRESENT:
|
|
1617
|
+
return (`${note.packRef} carries unverified co-signatures in its wrapper; ` +
|
|
1618
|
+
"they are not part of the evidence");
|
|
1619
|
+
default:
|
|
1620
|
+
return note.kind;
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1319
1623
|
/**
|
|
1320
|
-
*
|
|
1624
|
+
* The scope sentence, printed directly under the headline (evidence-bundle
|
|
1625
|
+
* 5.5.6 v1.20, A26), byte for byte the text `render_case_summary` returns.
|
|
1626
|
+
*
|
|
1627
|
+
* The packs in the reading; the distinct keys, compared as keys, that signed
|
|
1628
|
+
* the wrapper of a pack whose wrapper held; the hand-off records; the
|
|
1629
|
+
* exceptions; and that the reading was made offline. Where any pack holds
|
|
1630
|
+
* records withheld by its declaration, their total; where any pack is noted
|
|
1631
|
+
* `pack_undeclared`, how many. Those two lines are part of what the headline
|
|
1632
|
+
* MEANS, and a surface MUST print them with it.
|
|
1321
1633
|
*
|
|
1322
|
-
*
|
|
1323
|
-
* the
|
|
1324
|
-
* that differ by a word.
|
|
1634
|
+
* What was not checked on the public record is no longer a clause here. It is
|
|
1635
|
+
* the witness line (`renderWitnessLine`), printed with every headline.
|
|
1325
1636
|
*/
|
|
1326
1637
|
export function renderCaseSummary(reading) {
|
|
1327
|
-
const
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1638
|
+
const keys = new Set(reading.packs.filter((p) => p.grade !== null).map((p) => p.signingKey));
|
|
1639
|
+
const withheld = reading.packs.reduce((n, p) => n + p.recordsWithheld, 0);
|
|
1640
|
+
const undeclared = reading.notes.filter((n) => n.kind === PACK_UNDECLARED).length;
|
|
1641
|
+
const lines = [
|
|
1642
|
+
`${reading.packs.length} pack(s), ${keys.size} signing key(s), ` +
|
|
1643
|
+
`${reading.handoffRecords} hand-off record(s), ` +
|
|
1644
|
+
`${reading.exceptions.length} exception(s), read offline.`,
|
|
1645
|
+
];
|
|
1646
|
+
if (withheld > 0) {
|
|
1647
|
+
lines.push(`${withheld} record(s)' content withheld by their holder: their ` +
|
|
1648
|
+
"signatures and commitments were checked, their content was not.");
|
|
1649
|
+
}
|
|
1650
|
+
if (undeclared > 0)
|
|
1651
|
+
lines.push(`${undeclared} pack(s) do not say what they hold.`);
|
|
1652
|
+
return lines.join(" ");
|
|
1653
|
+
}
|
|
1654
|
+
// ---- The headline and the witness line (evidence-bundle 5.5.6 v1.20) -------
|
|
1655
|
+
/**
|
|
1656
|
+
* The one display title of each of the eight 3 words. A surface prints the
|
|
1657
|
+
* title, never the word, and never a title of its own (#173, A18). The wording
|
|
1658
|
+
* is Will's to change through the glossary (DL-71); what each MEANS is pinned
|
|
1659
|
+
* in the spec.
|
|
1660
|
+
*/
|
|
1661
|
+
export const VERDICT_DISPLAY_TITLES = {
|
|
1662
|
+
verified: "Verified",
|
|
1663
|
+
partial: "Partial",
|
|
1664
|
+
pending: "Pending",
|
|
1665
|
+
incomplete: "Incomplete",
|
|
1666
|
+
failed: "Failed",
|
|
1667
|
+
malformed: "Not readable",
|
|
1668
|
+
not_found: "Not found",
|
|
1669
|
+
mixed: "Mixed",
|
|
1670
|
+
};
|
|
1671
|
+
/**
|
|
1672
|
+
* What a reader may rely on, for each of the six words a case reading reaches
|
|
1673
|
+
* (5.5.6 v1.20, A26; the `verified` text at v1.28, A35), the same for every
|
|
1674
|
+
* drop. `pending` and `not_found` never head a case reading: they are
|
|
1675
|
+
* witness states. `verified` is the base sentence, printed alone where the
|
|
1676
|
+
* drop holds no hand-off record; a caller reads `caseReliance` for
|
|
1677
|
+
* `verified`, never this mapping directly.
|
|
1678
|
+
*/
|
|
1679
|
+
export const CASE_RELIANCE = {
|
|
1680
|
+
verified: "Every pack in this drop is genuine, unaltered, and matches its own " +
|
|
1681
|
+
"signed declaration. Every record in it was signed by its holder's " +
|
|
1682
|
+
"key. This was read offline, on this device. It does not say this " +
|
|
1683
|
+
"drop holds everything that was recorded.",
|
|
1684
|
+
partial: "Nothing here was proven wrong, and a check could not be completed: a " +
|
|
1685
|
+
"pack names no witness, or a pack in a hand-off reading does not say " +
|
|
1686
|
+
"what it holds. The exceptions say which.",
|
|
1687
|
+
incomplete: "Nothing here was proven wrong, and part of it could not be checked: " +
|
|
1688
|
+
"the other side of a hand-off is not in this drop, bytes were not " +
|
|
1689
|
+
"handed over, or a claim could not be read. Each exception says what " +
|
|
1690
|
+
"is missing and, where a key is known, whose pack would supply it.",
|
|
1691
|
+
failed: "A check ran and did not pass. Do not rely on this drop until the " +
|
|
1692
|
+
"named failure is explained. The failure is named on the pack or " +
|
|
1693
|
+
"record it is about, and every other exception is listed beside it.",
|
|
1694
|
+
malformed: "Part of this drop could not be read as evidence, and nothing was " +
|
|
1695
|
+
"proven wrong. The exceptions name what could not be read.",
|
|
1696
|
+
mixed: "The readings here differ and none of them failed: some are verified " +
|
|
1697
|
+
"and some could not be checked. Read each pack's row and each " +
|
|
1698
|
+
"exception before relying on any of it.",
|
|
1699
|
+
};
|
|
1700
|
+
/**
|
|
1701
|
+
* The clause a `verified` drop that holds a hand-off record gains, inserted
|
|
1702
|
+
* after `CASE_RELIANCE`'s first sentence (5.5.6, A35). Never printed alone,
|
|
1703
|
+
* and never where `handoffRecords` is zero: most drops hold one agent's
|
|
1704
|
+
* pack and no hand-off to say anything about.
|
|
1705
|
+
*/
|
|
1706
|
+
export const CASE_RELIANCE_HANDOFF_CLAUSE = "Every hand-off with both sides in this drop is intact.";
|
|
1707
|
+
/**
|
|
1708
|
+
* What a reader may rely on for one case reading (5.5.6 v1.28, A35), byte
|
|
1709
|
+
* for byte the text `case_reliance`'s Python sibling returns.
|
|
1710
|
+
*
|
|
1711
|
+
* For every word but `verified` this is `CASE_RELIANCE` unchanged. For
|
|
1712
|
+
* `verified`, the hand-off clause is inserted after the first sentence
|
|
1713
|
+
* where `handoffRecords` is not zero, and left out where it is zero: the
|
|
1714
|
+
* base sentence never claims a hand-off the drop does not hold.
|
|
1715
|
+
*/
|
|
1716
|
+
export function caseReliance(verdict, handoffRecords) {
|
|
1717
|
+
if (verdict === "verified" && handoffRecords > 0) {
|
|
1718
|
+
return ("Every pack in this drop is genuine, unaltered, and matches its own " +
|
|
1719
|
+
"signed declaration. Every hand-off with both sides in this drop is " +
|
|
1720
|
+
"intact. Every record in it was signed by its holder's key. This " +
|
|
1721
|
+
"was read offline, on this device. It does not say this drop holds " +
|
|
1722
|
+
"everything that was recorded.");
|
|
1723
|
+
}
|
|
1724
|
+
return CASE_RELIANCE[verdict];
|
|
1725
|
+
}
|
|
1726
|
+
/** The three witness states, and how each is printed (5.5.6, D7). No other. */
|
|
1727
|
+
export const WITNESS_STATES = {
|
|
1728
|
+
confirmed: "Confirmed on the public record",
|
|
1729
|
+
not_checked: "Not checked",
|
|
1730
|
+
differs: "The public record holds different bytes",
|
|
1731
|
+
};
|
|
1732
|
+
/** The line printed where no lookup ran, which is every reading this kernel produces on its own. */
|
|
1733
|
+
export const WITNESS_NOT_LOOKED_UP = Object.freeze({
|
|
1734
|
+
state: "not_checked",
|
|
1735
|
+
reasons: ["no lookup was run"],
|
|
1736
|
+
services: [],
|
|
1737
|
+
});
|
|
1738
|
+
/** The witness line as both kernels print it. */
|
|
1739
|
+
/** How a witness time a pack caches is printed where nothing checked it (5.5.6, 5.6.6, v1.25, A30). */
|
|
1740
|
+
export const WITNESS_TIME_CACHED = "the pack dates the witness, not checked";
|
|
1741
|
+
/** How a witness time the public record returned is printed where it matched the cache. */
|
|
1742
|
+
export const WITNESS_TIME_RECORDED = "the public record dates the witness";
|
|
1743
|
+
function witnessTimes(times) {
|
|
1744
|
+
return times.map(([h, t]) => renderWitnessTime(h, t)).join(", ");
|
|
1745
|
+
}
|
|
1746
|
+
export function renderWitnessLine(line) {
|
|
1747
|
+
let text = `Witness: ${WITNESS_STATES[line.state]}`;
|
|
1748
|
+
const parts = [...line.reasons];
|
|
1749
|
+
if (line.cached && line.cached.length > 0)
|
|
1750
|
+
parts.push(`${WITNESS_TIME_CACHED}: ${witnessTimes(line.cached)}`);
|
|
1751
|
+
if (line.recorded && line.recorded.length > 0) {
|
|
1752
|
+
parts.push(`${WITNESS_TIME_RECORDED}: ${witnessTimes(line.recorded)}`);
|
|
1344
1753
|
}
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1754
|
+
if (parts.length > 0)
|
|
1755
|
+
text += `: ${parts.join("; ")}`;
|
|
1756
|
+
if (line.services.length > 0)
|
|
1757
|
+
text += ` (answered: ${line.services.join(", ")})`;
|
|
1758
|
+
return text;
|
|
1348
1759
|
}
|