@sanning/proof 0.19.1 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -29
- package/dist/agent-record.d.ts +62 -0
- package/dist/agent-record.js +527 -0
- package/dist/agent-schemas.generated.d.ts +15 -0
- package/dist/agent-schemas.generated.js +5759 -0
- package/dist/case-file.d.ts +32 -89
- package/dist/case-file.js +41 -93
- package/dist/case.d.ts +339 -822
- package/dist/case.js +667 -1160
- package/dist/chain.d.ts +19 -47
- package/dist/chain.js +78 -205
- package/dist/claims.d.ts +211 -0
- package/dist/claims.js +1081 -0
- package/dist/cli.d.ts +45 -68
- package/dist/cli.js +618 -1022
- package/dist/compose.js +23 -65
- package/dist/crypto.d.ts +7 -8
- package/dist/crypto.js +60 -147
- package/dist/declaration.d.ts +68 -194
- package/dist/declaration.js +175 -518
- package/dist/environment.d.ts +9 -30
- package/dist/environment.js +17 -66
- package/dist/evidence.d.ts +119 -160
- package/dist/evidence.js +744 -1103
- package/dist/exit-codes.d.ts +11 -27
- package/dist/exit-codes.js +24 -59
- package/dist/file-match.d.ts +33 -79
- package/dist/file-match.js +33 -69
- package/dist/grading.d.ts +98 -135
- package/dist/grading.js +224 -380
- package/dist/handoff.d.ts +62 -160
- package/dist/handoff.js +153 -393
- package/dist/index.d.ts +14 -10
- package/dist/index.js +60 -167
- package/dist/json-schema.d.ts +29 -0
- package/dist/json-schema.js +298 -0
- package/dist/merkle.js +16 -28
- package/dist/message-parts.d.ts +56 -212
- package/dist/message-parts.js +142 -502
- package/dist/side-content.d.ts +11 -22
- package/dist/side-content.js +22 -65
- package/dist/types.d.ts +17 -0
- package/dist/types.js +19 -5
- package/dist/verifier.d.ts +40 -114
- package/dist/verifier.js +196 -535
- package/dist/witness-cache.d.ts +24 -45
- package/dist/witness-cache.js +38 -81
- package/dist/witness-lookup.d.ts +36 -57
- package/dist/witness-lookup.js +89 -139
- package/dist/zip-reader.d.ts +18 -33
- package/dist/zip-reader.js +19 -34
- package/package.json +11 -3
- package/src/agent-record.ts +608 -0
- package/src/agent-schemas.generated.ts +5764 -0
- package/src/case-file.ts +45 -109
- package/src/case.ts +814 -1328
- package/src/chain.ts +85 -224
- package/src/claims.ts +1253 -0
- package/src/cli.ts +714 -1176
- package/src/compose.ts +33 -79
- package/src/crypto.ts +60 -147
- package/src/declaration.ts +219 -656
- package/src/environment.ts +17 -65
- package/src/evidence.ts +958 -1381
- package/src/exit-codes.ts +24 -63
- package/src/file-match.ts +40 -89
- package/src/grading.ts +287 -421
- package/src/handoff.ts +175 -427
- package/src/index.ts +131 -177
- package/src/json-schema.ts +295 -0
- package/src/merkle.ts +16 -28
- package/src/message-parts.ts +155 -533
- package/src/side-content.ts +25 -68
- package/src/types.ts +55 -36
- package/src/verifier.ts +202 -526
- package/src/witness-cache.ts +45 -94
- package/src/witness-lookup.ts +107 -177
- package/src/zip-reader.ts +20 -38
- package/vectors/README.md +52 -34
- package/vectors/agent-record.json +2175 -0
- package/vectors/case-file-index.json +1 -1
- package/vectors/case-split.json +2 -2
- package/vectors/chain-reading.json +3 -3
- package/vectors/file-match.json +285 -148
- package/vectors/jcs-reader.json +6 -6
- package/vectors/pack-grading.json +102 -25
- package/vectors/sandbox-evidence.json +573 -581
- package/vectors/stamp-on-chain.json +18 -18
- package/vectors/witness-cache.json +49 -49
- package/dist/agent-proof.d.ts +0 -28
- package/dist/agent-proof.js +0 -226
- package/dist/run-adapters.d.ts +0 -10
- package/dist/run-adapters.js +0 -35
- package/dist/run.d.ts +0 -209
- package/dist/run.js +0 -1405
- package/src/agent-proof.ts +0 -264
- package/src/run-adapters.ts +0 -44
- package/src/run.ts +0 -1598
package/src/case.ts
CHANGED
|
@@ -1,245 +1,64 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* One verdict for a
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* (anchor#198, and DL-60: producer identity gates what the plane accepts and
|
|
63
|
-
* never whether evidence verifies). The union pile is not partitioned by pack,
|
|
64
|
-
* by issuer, by `tenant_id` or by organization anywhere in this module, so a
|
|
65
|
-
* pair assembled by two organizations reads exactly as a pair assembled by
|
|
66
|
-
* one. There is no code path here that reads a tenant.
|
|
67
|
-
*
|
|
68
|
-
* THE DEFECT CLASS THIS MODULE IS WRITTEN AGAINST
|
|
69
|
-
* ===============================================
|
|
70
|
-
*
|
|
71
|
-
* Seventeen findings on this program share one generator: **a deviation
|
|
72
|
-
* designed as a comparison between two present things, where the deviation
|
|
73
|
-
* itself removes one of them, so the code takes the "nothing to compare"
|
|
74
|
-
* branch written for the honest case.** A rule shaped that way reads the
|
|
75
|
-
* absence of a signal as "fine" rather than as "I could not tell", and a real
|
|
76
|
-
* deviation grades clean.
|
|
77
|
-
*
|
|
78
|
-
* Acceptance criterion 3 of anchor#198 is made of exactly that shape: *a
|
|
79
|
-
* one-sided hand-off is an exception, not a failure*. So the four rules below
|
|
80
|
-
* are structural and are the thing to preserve in any change here.
|
|
81
|
-
*
|
|
82
|
-
* 1. **Every exception is NAMED and appears in the output, and every exception
|
|
83
|
-
* contributes a verdict.** There is no path through this module that
|
|
84
|
-
* removes a record, a pack or a claim from the reading. An empty
|
|
85
|
-
* `exceptions` is the ONLY shape a reader may read as clean, and `readCase`
|
|
86
|
-
* is invariant: `verified` is reachable only when that array is empty.
|
|
87
|
-
* 2. **Nothing a party writes decides whether grading happens.** No graded
|
|
88
|
-
* value is part of any key that selects a code path. The pile is everything
|
|
89
|
-
* every pack supplied; there is no filter.
|
|
90
|
-
* 3. **Every field that selects a code path and is chosen by the party being
|
|
91
|
-
* audited has an accept-set, and an unknown value goes to a named exception
|
|
92
|
-
* rather than to silence.** `body_type` is `CASE_BODY_TYPES`;
|
|
93
|
-
* `declaration_version` is `checkDeclaration`'s; `event_type` and
|
|
94
|
-
* `event.counterparty_public_key` are 4.1's, through `readHandoffs`. A pack
|
|
95
|
-
* that supplies zero records is `PACK_HOLDS_NO_RECORDS` and a case with no
|
|
96
|
-
* hand-off record in it at all is `CASE_HOLDS_NO_HANDOFF_RECORDS`, because
|
|
97
|
-
* "nothing to grade" is never "nothing is wrong".
|
|
98
|
-
* 4. **The 4.1 predicates are CALLED, never restated.** `readHandoffs`,
|
|
99
|
-
* `normalizedSigningKey` and `packRecords` are the one implementation of
|
|
100
|
-
* each rule. A validation copied beside the one that enforces it drifts,
|
|
101
|
-
* and a copy that is looser blesses a value the real one then rejects.
|
|
102
|
-
*
|
|
103
|
-
* EVERY EARLY EXIT IN THIS MODULE, AND WHAT IT SKIPS
|
|
104
|
-
* ==================================================
|
|
105
|
-
*
|
|
106
|
-
* Three defects on this layer were one shape: an exit between a record
|
|
107
|
-
* entering the pile and a check running over it, with the audited party
|
|
108
|
-
* choosing whether the exit was taken. So the exits are enumerated here rather
|
|
109
|
-
* than left to be rediscovered a fourth time. `src/sanning_proof/case.py` has
|
|
110
|
-
* the same exits in the same order, and the pull request that added this list
|
|
111
|
-
* carries the same table.
|
|
112
|
-
*
|
|
113
|
-
* 1. **No packs** (`return`). Skips everything. Reached by the reader, who
|
|
114
|
-
* chose what to hand in. Named: `CASE_HOLDS_NO_PACKS`.
|
|
115
|
-
* 2. **A wrapper key this reader cannot read** (`continue`). Skips the bundle
|
|
116
|
-
* verification, the declaration check and every record in the pack. The
|
|
117
|
-
* holder writes the key, so it steers here freely, and the pack is NAMED
|
|
118
|
-
* (`PACK_KEY_UNREADABLE`). Both bundle verifiers now refuse every such
|
|
119
|
-
* spelling in the same words (proof#142), so this exit and exit 3 agree on
|
|
120
|
-
* which packs they take; it stays first because it names WHY. While any
|
|
121
|
-
* pack takes this exit, a one-sided record whose named key signed no pack
|
|
122
|
-
* reads `COUNTERPART_ABSENT_SIGNER_UNKNOWN` and never the innocent
|
|
123
|
-
* `COUNTERPART_ABSENT`.
|
|
124
|
-
* 3. **The wrapper does not hold** (`continue`): `signatureOk` and
|
|
125
|
-
* `bodyHashOk` are not both true. Skips the declaration check and every
|
|
126
|
-
* record in the pack, and the key it claims is never counted as a pack
|
|
127
|
-
* signer. **This is the forger's case and the only exit that turns on
|
|
128
|
-
* provenance**: those two fields are the whole of what ties a pack to a
|
|
129
|
-
* key. Reached by whoever built the pack, and by anyone who alters its
|
|
130
|
-
* unsigned `signature` field, which is indistinguishable from a forgery.
|
|
131
|
-
* Named: `PACK_DOES_NOT_VERIFY`, and it moves one-sided records to
|
|
132
|
-
* `COUNTERPART_ABSENT_SIGNER_UNKNOWN` exactly as exit 2 does.
|
|
133
|
-
*
|
|
134
|
-
* A pack whose wrapper HOLDS is never sent here, whatever is wrong inside
|
|
135
|
-
* it (gate 3 on proof#140): its key is counted, its own bundle verdict is
|
|
136
|
-
* `PACK_BUNDLE_NOT_VERIFIED` when that verdict is not `verified`, and each
|
|
137
|
-
* record is graded on its own envelope below.
|
|
138
|
-
* 3b. **The bundle verdict is `malformed`** (`continue`, after exit 3 and after
|
|
139
|
-
* `PACK_BUNDLE_NOT_VERIFIED`). The body is one the verifier could not read,
|
|
140
|
-
* so it graded no record. The key is already counted.
|
|
141
|
-
* 4. **`body_type` outside `CASE_BODY_TYPES`** (`continue`). Skips the
|
|
142
|
-
* declaration check and every record in the pack. The holder writes
|
|
143
|
-
* `body_type`, so the holder steers here freely, which is why it is named
|
|
144
|
-
* every time (`PACK_BODY_TYPE_UNKNOWN`) and why the pack's wrapper key is
|
|
145
|
-
* counted ABOVE it: a counterparty cannot leave the damning counterpart
|
|
146
|
-
* line by choosing a body (F3). `sanning.evidence.export/v2` lands here and
|
|
147
|
-
* is not unwrapped.
|
|
148
|
-
* 5. **No records** (`continue`). Skips the declaration check, over nothing.
|
|
149
|
-
* The holder steers here by handing over an empty `events[]`. Named:
|
|
150
|
-
* `PACK_HOLDS_NO_RECORDS`; the wrapper key is already counted.
|
|
151
|
-
* 5b. **A record under a quarantined profile** and 5c. **a record whose own
|
|
152
|
-
* envelope does not verify** (`continue`). Each skips 4.1 for that record,
|
|
153
|
-
* its candidacy for any other record, and the claim check, because 4.1
|
|
154
|
-
* compares only envelopes that verified (`HeldRecord`). Named:
|
|
155
|
-
* `RECORD_PROFILE_QUARANTINED` (`incomplete`, pending DL-63) and
|
|
156
|
-
* `RECORD_ENVELOPE_DOES_NOT_VERIFY` (`failed`). Neither removes a named
|
|
157
|
-
* failure from another record: the other records of the pack are still
|
|
158
|
-
* graded, and pairs still form over every record that verified.
|
|
159
|
-
* 6. **`event_bytes` withheld** and 7. **`event_bytes` unreadable**
|
|
160
|
-
* (`continue`). Each skips 4.1 for that record, its candidacy for any other
|
|
161
|
-
* record, and the claim check. The holder steers into both by what it
|
|
162
|
-
* discloses. Each is named in the claim check's place
|
|
163
|
-
* (`RECORD_BYTES_WITHHELD` `incomplete`, `RECORD_BYTES_UNREADABLE`
|
|
164
|
-
* `malformed`), so neither buys a cleaner reading than an unclaimed record
|
|
165
|
-
* would get, and neither decides which counterpart kind another record
|
|
166
|
-
* gets, since that reads wrapper keys.
|
|
167
|
-
*
|
|
168
|
-
* Not exits: a record whose envelope verifies and whose content does not
|
|
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
|
|
172
|
-
* another check inside its pack is `RECORD_DOES_NOT_VERIFY_IN_ITS_PACK`.
|
|
173
|
-
* Both are named and both STAY in the pile, so neither unsigned content
|
|
174
|
-
* handed in beside a pack nor a broken inclusion proof removes a record
|
|
175
|
-
* from pairing.
|
|
176
|
-
* 8. **`not_a_handoff_record`** (`continue`). Skips the hand-off count and the
|
|
177
|
-
* counterpart, ambiguity and pair reporting, which 4.1 does not give a
|
|
178
|
-
* record outside its scope. The holder steers here by respelling a field.
|
|
179
|
-
* It no longer skips the claim check, which runs ABOVE it because it asks
|
|
180
|
-
* what the pack SAID and not what the record is (F1).
|
|
181
|
-
* 9. **THIS record is malformed** (`continue`). Skips the counterpart,
|
|
182
|
-
* ambiguity and pair reporting, and 4.1 computed none of them for a
|
|
183
|
-
* malformed subject, so nothing is lost. A party steers here only by
|
|
184
|
-
* breaking a record it signed, and the record is named `RECORD_MALFORMED`.
|
|
185
|
-
* A well formed record whose CANDIDATE is malformed does not take this
|
|
186
|
-
* exit: it is `COUNTERPART_MALFORMED` and the reporting below still runs
|
|
187
|
-
* (F2).
|
|
188
|
-
* 10. **`counterpart_absent`** (`continue`). Skips the ambiguity and pair
|
|
189
|
-
* reporting, and there is nothing to skip: 4.1 emits that finding only
|
|
190
|
-
* where no candidate is left to read, an EMPTY candidate list or one whose
|
|
191
|
-
* every candidate another record accounts for (4.1 v1.17, A20). Which of the three counterpart kinds it
|
|
192
|
-
* gets reads the keys of the packs that verified, counted before exit 4,
|
|
193
|
-
* and whether any pack took exit 2 or 3.
|
|
194
|
-
*
|
|
195
|
-
* Not an exit, and listed because it is one more place this shape could
|
|
196
|
-
* hide: the claim check runs only where the pack's declaration is READABLE. An
|
|
197
|
-
* absent or unreadable one is named once for the pack (`PACK_UNDECLARED`,
|
|
198
|
-
* `PACK_DECLARATION_UNREADABLE`) rather than once per record.
|
|
199
|
-
*
|
|
200
|
-
* THE VERDICT VOCABULARY IS CLOSED
|
|
201
|
-
* ================================
|
|
202
|
-
*
|
|
203
|
-
* `verified`, `failed`, `partial`, `mixed`, `incomplete` and `malformed`,
|
|
204
|
-
* every one of them already in use (`evidence-bundle.md` 3 and
|
|
205
|
-
* `PACK_VERDICTS`). **No new word is coined anywhere in this module**, and
|
|
206
|
-
* `CASE_VERDICTS` is asserted against `PACK_VERDICTS` by the test suite so a
|
|
207
|
-
* future addition here cannot invent one quietly. Everything this module has
|
|
208
|
-
* to say that is not one of those six is an EXCEPTION KIND reported beside the
|
|
209
|
-
* verdict, exactly as `counterpart_absent` and `not_a_handoff_record` are
|
|
210
|
-
* findings beside a verdict in 4.1.
|
|
211
|
-
*
|
|
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).
|
|
215
|
-
*
|
|
216
|
-
* WHAT THIS READER DOES NOT CLAIM
|
|
217
|
-
* ===============================
|
|
218
|
-
*
|
|
219
|
-
* * **Not that the case is complete.** Joining is voluntary. A counterparty
|
|
220
|
-
* that never handed over a pack leaves `COUNTERPART_ABSENT`, which is an
|
|
221
|
-
* exception and never a failure. It says that no pack in the case is signed
|
|
222
|
-
* by the named key, and nothing about what that party recorded.
|
|
223
|
-
* * **Not that a party which presented no pack under the named key recorded
|
|
224
|
-
* nothing.** See the top of this comment, and proof#143.
|
|
225
|
-
* * **Not that two organizations are independent.** One team holding both keys
|
|
226
|
-
* proves which agent said what, not that two parties agreed (4.1, 5.4.4).
|
|
227
|
-
* * **Not that a `verified` case has no other problem.** It reads hand-offs.
|
|
228
|
-
* It runs `verifyEvidenceBundle` over every pack itself, offline and with no
|
|
229
|
-
* gateways, and a pack whose own bundle verdict is not `verified` is named
|
|
230
|
-
* with that verdict and its reasons, so the case never reads above its
|
|
231
|
-
* weakest pack. On-chain re-fetch and `gradePack` are a caller's to run.
|
|
232
|
-
*
|
|
233
|
-
* THE COST OF 4.1's "GRADE EVERY CANDIDATE", AND WHAT IS LEFT OF IT (4.1
|
|
234
|
-
* v1.17, amendment A20, which reopens proof#134). Up to v1.16 a pile holding
|
|
235
|
-
* two or more records of the complementary type signed by one counterparty key
|
|
236
|
-
* could not reach `verified`. 4.1 now sets such a candidate aside where a
|
|
237
|
-
* record the subject's own key signed pairs with it verified at another
|
|
238
|
-
* sequence, so two messages in one direction, and the demo's A to B, B to A,
|
|
239
|
-
* A to B, read clean. What is left surfaces as `COUNTERPART_AMBIGUOUS` beside
|
|
240
|
-
* the failure: fan-in, fan-out, and the same pair in two cases at the same
|
|
241
|
-
* sequence. Narrowing any of it here with `handoffs[]` is precisely the move
|
|
242
|
-
* that would reopen proof#137's door.
|
|
2
|
+
* One verdict for a case: several packs, possibly from several organizations.
|
|
3
|
+
* Conformance: `test-vectors/case/`.
|
|
4
|
+
*
|
|
5
|
+
* A pack a party assembles about itself is not self-proving about what that
|
|
6
|
+
* party left out: a holder can declare a record and claim it nowhere, and the
|
|
7
|
+
* pack still reads `verified` (`declaration-unclaimed-record-01`). Reading
|
|
8
|
+
* several parties' packs at once closes that. An `agent.handoff.sent` with no
|
|
9
|
+
* counterpart among the case's records is always named, and when a pack signed
|
|
10
|
+
* by the key that record names is in the case, the reading says that pack
|
|
11
|
+
* holds no counterpart. It does not establish that a party which presented no
|
|
12
|
+
* such pack recorded nothing: nothing binds a pack's wrapper key to its
|
|
13
|
+
* records' keys.
|
|
14
|
+
*
|
|
15
|
+
* How the join works:
|
|
16
|
+
* - Records pair with records, through `readHandoffs`, over the union of every
|
|
17
|
+
* record every pack supplied. `handoffs[]` is written by the audited party,
|
|
18
|
+
* so it never selects a pair; it is read only to name a declared record that
|
|
19
|
+
* is claimed nowhere (`RECORD_UNCLAIMED`).
|
|
20
|
+
* - Matching is on hashes and identifiers only, never tenancy. Nothing here
|
|
21
|
+
* partitions the pile by pack, issuer, `tenant_id` or organization.
|
|
22
|
+
*
|
|
23
|
+
* Structural rules, to preserve in any change:
|
|
24
|
+
* 1. Every exception is named in the output and contributes a verdict.
|
|
25
|
+
* Nothing removes a record, pack or claim from the reading. `verified` is
|
|
26
|
+
* reachable only when `exceptions` is empty.
|
|
27
|
+
* 2. Nothing a party writes decides whether grading happens. The pile is
|
|
28
|
+
* everything every pack supplied.
|
|
29
|
+
* 3. Every party-chosen field that selects a code path has an accept-set, and
|
|
30
|
+
* an unknown value is a named exception. "Nothing to grade" is never
|
|
31
|
+
* "nothing is wrong".
|
|
32
|
+
* 4. The §4.1 predicates (`readHandoffs`, `normalizedSigningKey`,
|
|
33
|
+
* `packRecords`) are called, never restated.
|
|
34
|
+
*
|
|
35
|
+
* Every early exit in `readCase` names its exception, so no exit buys a
|
|
36
|
+
* cleaner reading than the record or pack would otherwise get. In particular:
|
|
37
|
+
* - A pack with an unreadable key or a wrapper that does not hold is skipped
|
|
38
|
+
* and its key never counted; while any pack takes that exit, a one-sided
|
|
39
|
+
* record reads `COUNTERPART_ABSENT_SIGNER_UNKNOWN`, never `COUNTERPART_ABSENT`.
|
|
40
|
+
* - A wrapper key is counted before the body-type and no-records exits, so
|
|
41
|
+
* choosing a body cannot dodge the counterpart line.
|
|
42
|
+
* - The claim check runs before the not-a-hand-off and malformed exits, so
|
|
43
|
+
* respelling or breaking a record cannot switch it off.
|
|
44
|
+
* - Content and in-pack failures are named and the record stays in the pile.
|
|
45
|
+
*
|
|
46
|
+
* The verdict vocabulary is closed: `verified`, `failed`, `incomplete`
|
|
47
|
+
* (evidence-bundle.md section 3). Every exception maps to one claim and one
|
|
48
|
+
* state (`claims.ts`), and the case reads its worst pack: any `failed`
|
|
49
|
+
* contribution makes the case `failed`, otherwise any `incomplete` makes it
|
|
50
|
+
* `incomplete`, and a reading with no contribution is `verified`.
|
|
51
|
+
*
|
|
52
|
+
* Not claimed: that the case is complete (joining is voluntary), that two
|
|
53
|
+
* organizations are independent (one team can hold both keys), or that a
|
|
54
|
+
* `verified` case has no other problem. Every pack is verified offline here
|
|
55
|
+
* and a pack whose bundle is not `verified` is named; on-chain re-fetch and
|
|
56
|
+
* `gradePack` are the caller's.
|
|
57
|
+
*
|
|
58
|
+
* Several records of the complementary type under one counterparty key can
|
|
59
|
+
* still be ambiguous (fan-in, fan-out, one pair in two cases at one sequence):
|
|
60
|
+
* that surfaces as `COUNTERPART_AMBIGUOUS`. Narrowing it with `handoffs[]`
|
|
61
|
+
* would reopen the hole above.
|
|
243
62
|
*/
|
|
244
63
|
|
|
245
64
|
import {
|
|
@@ -253,6 +72,7 @@ import {
|
|
|
253
72
|
declarationClaims,
|
|
254
73
|
packRecords,
|
|
255
74
|
} from "./declaration.js";
|
|
75
|
+
import { readAgentRecords, type AgentHeldRecord, type AgentRecordReading } from "./agent-record.js";
|
|
256
76
|
import { readChainLinks } from "./chain.js";
|
|
257
77
|
import {
|
|
258
78
|
type HandoffReading,
|
|
@@ -261,6 +81,7 @@ import {
|
|
|
261
81
|
normalizedSigningKey,
|
|
262
82
|
readHandoffs,
|
|
263
83
|
recordIsMalformed,
|
|
84
|
+
recordIsNonconforming,
|
|
264
85
|
} from "./handoff.js";
|
|
265
86
|
import { coSignaturesPresent, resolveTraceEvents, verifyEvidenceBundle } from "./evidence.js";
|
|
266
87
|
import type { EvidenceBundleResult } from "./evidence.js";
|
|
@@ -275,56 +96,34 @@ import {
|
|
|
275
96
|
import type { EnvironmentMarker } from "./environment.js";
|
|
276
97
|
import type { SideContentMap } from "./side-content.js";
|
|
277
98
|
import { renderWitnessTime } from "./witness-cache.js";
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
99
|
+
import {
|
|
100
|
+
KIND_CLAIMS,
|
|
101
|
+
VERDICTS,
|
|
102
|
+
VERDICT_TITLES,
|
|
103
|
+
bundleProblems,
|
|
104
|
+
claimsOf,
|
|
105
|
+
problem,
|
|
106
|
+
restOnSigned,
|
|
107
|
+
verdictOf as claimsVerdictOf,
|
|
108
|
+
withItems,
|
|
109
|
+
type Problem,
|
|
110
|
+
type ProblemItem,
|
|
111
|
+
type ReasonKind,
|
|
112
|
+
} from "./claims.js";
|
|
113
|
+
|
|
114
|
+
/** The three verdicts (evidence-bundle.md section 3). A case reads its worst pack. */
|
|
115
|
+
export const CASE_VERDICTS = VERDICTS;
|
|
294
116
|
|
|
295
117
|
export type CaseVerdict = (typeof CASE_VERDICTS)[number];
|
|
296
118
|
|
|
297
119
|
/**
|
|
298
|
-
*
|
|
299
|
-
*
|
|
120
|
+
* The pack bodies that carry `events[]` and may carry a `declaration`
|
|
121
|
+
* (evidence-bundle.md §5.4.1). An accept-set: `body_type` is party-written, so
|
|
122
|
+
* an unknown value is `PACK_BODY_TYPE_UNKNOWN`, never zero records read clean.
|
|
300
123
|
*
|
|
301
|
-
* `
|
|
302
|
-
* `
|
|
303
|
-
*
|
|
304
|
-
* containment at run time against `grading.PACK_VERDICTS`, which is that
|
|
305
|
-
* language's equivalent gate.
|
|
306
|
-
*/
|
|
307
|
-
const _caseVerdictsAreSharedVocabulary: PackVerdict[] = [...CASE_VERDICTS];
|
|
308
|
-
void _caseVerdictsAreSharedVocabulary;
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* The two pack bodies that carry `events[]` and may carry a `declaration`
|
|
312
|
-
* (`evidence-bundle.md` 5.4.1). This is an ACCEPT-SET and not a hint.
|
|
313
|
-
*
|
|
314
|
-
* `body_type` selects a code path and is written by the party assembling the
|
|
315
|
-
* pack. Without this set, a pack whose records live under a key this reader
|
|
316
|
-
* does not know yields zero records, contributes nothing, and lets the rest of
|
|
317
|
-
* the case read clean over it: the exact defect class this module is written
|
|
318
|
-
* against. An unknown value is `PACK_BODY_TYPE_UNKNOWN`.
|
|
319
|
-
*
|
|
320
|
-
* **`sanning.evidence.export/v2` is NOT in this set and is NOT unwrapped.** It
|
|
321
|
-
* verifies as a bundle and carries a whole `sanning.anchor.trace/v3` pack under
|
|
322
|
-
* `body.source_bundle`, so its records do not reach the pile and the export is
|
|
323
|
-
* named `PACK_BODY_TYPE_UNKNOWN`, `incomplete`. Reading inside it changes which
|
|
324
|
-
* packs this reader grades, so it is a decision of its own. A reader who needs
|
|
325
|
-
* the records hands in the source bundle as a pack. The export's wrapper key is
|
|
326
|
-
* still read, so wrapping a pack in one does not change which counterpart kind
|
|
327
|
-
* another record gets.
|
|
124
|
+
* `sanning.evidence.export/v2` is not unwrapped: it is named
|
|
125
|
+
* `PACK_BODY_TYPE_UNKNOWN`, and a reader who needs its records hands in the
|
|
126
|
+
* source bundle. Its wrapper key is still read.
|
|
328
127
|
*/
|
|
329
128
|
export const CASE_BODY_TYPES = [
|
|
330
129
|
"sanning.anchor.trace/v3",
|
|
@@ -332,398 +131,224 @@ export const CASE_BODY_TYPES = [
|
|
|
332
131
|
] as const;
|
|
333
132
|
|
|
334
133
|
/**
|
|
335
|
-
* The
|
|
336
|
-
* accept-set
|
|
337
|
-
*
|
|
338
|
-
* from that file, and a test here pins this one equal to this kernel's own
|
|
339
|
-
* `ACCEPTED_SPEC_MAJORS`, which is what `verifyEvidenceBundle` reads under:
|
|
340
|
-
* the Python kernel narrows its verifier to this set for the case reader, and
|
|
341
|
-
* this kernel's verifier already reads exactly it. If this kernel ever admits
|
|
342
|
-
* a quarantined profile, that test fails, and the case reader has to narrow
|
|
343
|
-
* its verifier the way the Python one does before it can pass.
|
|
134
|
+
* The profiles records are read under: the accept-set (envelope-spec.md §5)
|
|
135
|
+
* minus quarantined profiles in `cross-kernel/accept-set.json`. A test pins
|
|
136
|
+
* this equal to `ACCEPTED_SPEC_MAJORS`, which `verifyEvidenceBundle` reads under.
|
|
344
137
|
*/
|
|
345
|
-
export const CASE_RECORD_PROFILES = ["sanning.
|
|
138
|
+
export const CASE_RECORD_PROFILES = ["sanning.events/v2"] as const;
|
|
346
139
|
|
|
347
|
-
/**
|
|
140
|
+
/** Accepted profiles under quarantine. */
|
|
348
141
|
export const CASE_QUARANTINED_PROFILES = ["sanning.mlflow/v1"] as const;
|
|
349
142
|
|
|
350
|
-
// ---- The exceptions ---------------------------------------------------------
|
|
351
|
-
//
|
|
352
|
-
// Each of these is a NAME for something a reader would otherwise have to infer
|
|
353
|
-
// from an absence. None of them is a verdict word.
|
|
354
|
-
|
|
355
143
|
/**
|
|
356
|
-
* A conformant hand-off record with no counterpart
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
* That is ALL it says. It does not say the counterparty's account is absent,
|
|
364
|
-
* and it does not say nothing was tampered with: a counterparty that signs its
|
|
365
|
-
* own pack under a second key, or hands over an export an issuer signed,
|
|
366
|
-
* reaches this reading too, and nothing offline binds a pack's key to the keys
|
|
367
|
-
* of the records inside it (proof#143). The rendered text states the fact and
|
|
368
|
-
* stops.
|
|
369
|
-
*
|
|
370
|
-
* It says nothing about RECORDS signed by that key either. A party may hold records
|
|
371
|
-
* its counterparty signed, and those are still graded as candidates, but they
|
|
372
|
-
* are not the counterparty's account of itself, so they do not move this
|
|
373
|
-
* record to `COUNTERPART_ABSENT_FROM_COUNTERPARTY_RECORDS`.
|
|
144
|
+
* A conformant hand-off record with no counterpart, where every pack verified
|
|
145
|
+
* under a readable key and none is signed by the key the record names, or every
|
|
146
|
+
* candidate was accounted for by another record (then `count` is set and no
|
|
147
|
+
* pack is named). An exception, never a failure. It says only that no pack is
|
|
148
|
+
* signed by the named key; records that key signed and others hold are not that
|
|
149
|
+
* party's own account and do not change it.
|
|
374
150
|
*/
|
|
375
151
|
export const COUNTERPART_ABSENT = "counterpart_absent";
|
|
376
152
|
|
|
377
153
|
/**
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
*
|
|
383
|
-
*
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
*
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
* nothing at all.
|
|
396
|
-
*
|
|
397
|
-
* What it does NOT prove, stated so no surface claims it: that B handed the
|
|
398
|
-
* pack over for THIS case. A pack B signed for some other purpose and a third
|
|
399
|
-
* party replayed reads the same, because the only values that could say
|
|
400
|
-
* otherwise (the declaration's `case_id` and `period`) are written by B and
|
|
401
|
-
* would let B leave this line by writing them differently. The cost the other
|
|
402
|
-
* way: a pack B assembles under any key other than the one its records name, a
|
|
403
|
-
* per-organization hand-over key (anchor#196) included, reads
|
|
404
|
-
* `COUNTERPART_ABSENT`, because nothing in the case binds that key to B. That
|
|
405
|
-
* reading is still true: no pack signed by the named key is here. So a
|
|
406
|
-
* counterparty can AVOID this line by choosing the key it signs under; what it
|
|
407
|
-
* cannot do is make the missing counterpart read clean.
|
|
408
|
-
*
|
|
409
|
-
* It is deliberately NOT also conditioned on a record signed by B being in the
|
|
410
|
-
* pile. B's pack can hold none this reader reads (a body it does not unwrap,
|
|
411
|
-
* bytes B withheld, an empty `events[]`), and each of those is a choice B makes
|
|
412
|
-
* and is named on its own. A second conjunct on them hands B the innocent line
|
|
413
|
-
* for making one.
|
|
414
|
-
*
|
|
415
|
-
* It is a DISTINCT kind from `COUNTERPART_ABSENT` and not a louder spelling of
|
|
416
|
-
* it. Collapsing the two would be the defect class again: "the counterparty
|
|
417
|
-
* never joined" and "the counterparty joined and its own account does not
|
|
418
|
-
* contain this hand-off" are the honest reading and the damning one, and one
|
|
419
|
-
* name for both lets the second read as the first.
|
|
420
|
-
*
|
|
421
|
-
* It is still an EXCEPTION and not a failure, which criterion 3 requires and
|
|
422
|
-
* which is also the honest call: the counterparty's pack is a selection over
|
|
423
|
-
* its own interval, and a hand-off outside that interval is legitimately not
|
|
424
|
-
* in it. What this names is that the question can now be ASKED of the other
|
|
425
|
-
* party's own evidence, which is what no single pack could ever support.
|
|
154
|
+
* A conformant hand-off record with no counterpart, where a verified pack
|
|
155
|
+
* signed by the key the record names is in the case and holds no counterpart.
|
|
156
|
+
* The closure this module exists for (`declaration-unclaimed-record-01`).
|
|
157
|
+
*
|
|
158
|
+
* "Its own account" is read from the key that signed the pack, never from
|
|
159
|
+
* record keys: a wrapper signed by B is provenance an accuser cannot forge, but
|
|
160
|
+
* records signed by B that A holds are not B's account.
|
|
161
|
+
*
|
|
162
|
+
* Not proven: that B handed the pack over for this case. A pack B signs under
|
|
163
|
+
* any other key reads `COUNTERPART_ABSENT`, which is still true; B can avoid
|
|
164
|
+
* this line by choice of key, but cannot make the missing counterpart read
|
|
165
|
+
* clean. Not conditioned on B's records being readable, since each way B could
|
|
166
|
+
* make them unreadable is its own named choice.
|
|
167
|
+
*
|
|
168
|
+
* A distinct kind from `COUNTERPART_ABSENT`: one name for both would let the
|
|
169
|
+
* damning reading pass as the honest one. Still `incomplete`, not a failure: a
|
|
170
|
+
* hand-off outside the interval B's pack covers is legitimately absent.
|
|
426
171
|
*/
|
|
427
172
|
export const COUNTERPART_ABSENT_FROM_COUNTERPARTY_RECORDS =
|
|
428
173
|
"counterpart_absent_from_counterparty_records";
|
|
429
174
|
|
|
430
175
|
/**
|
|
431
|
-
* A conformant hand-off record with no counterpart, no
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
*
|
|
435
|
-
* Whether a pack signed by the named key is in the case is UNKNOWN, so neither
|
|
436
|
-
* of the other two kinds is true.
|
|
437
|
-
*
|
|
438
|
-
* The gate on the previous version of this reader showed why it exists. A
|
|
439
|
-
* counterparty that signed its own pack under `" " + key` got
|
|
440
|
-
* `PACK_KEY_UNREADABLE` for the pack and the innocent `COUNTERPART_ABSENT` for
|
|
441
|
-
* the record, and the output contradicted itself: whose pack it was is
|
|
442
|
-
* UNKNOWN, and then no pack is signed by the named key. The second sentence
|
|
443
|
-
* was not established.
|
|
444
|
-
*
|
|
445
|
-
* `incomplete`, like the other two one-sided kinds: "I could not tell".
|
|
176
|
+
* A conformant hand-off record with no counterpart, no verified pack signed by
|
|
177
|
+
* the key it names, and at least one pack whose signer is unknown
|
|
178
|
+
* (`PACK_KEY_UNREADABLE` or `PACK_DOES_NOT_VERIFY`). Whether the named key
|
|
179
|
+
* signed a pack is unknown, so neither other kind is true. `incomplete`.
|
|
446
180
|
*/
|
|
447
181
|
export const COUNTERPART_ABSENT_SIGNER_UNKNOWN = "counterpart_absent_signer_unknown";
|
|
448
182
|
|
|
449
183
|
/**
|
|
450
|
-
* More than one candidate counterpart
|
|
451
|
-
*
|
|
452
|
-
*
|
|
453
|
-
* record. `count` is how many of the candidates are live, which is the number
|
|
454
|
-
* the ambiguity is about.
|
|
184
|
+
* More than one live candidate counterpart (§4.1). The reading is the worst of
|
|
185
|
+
* them and the ambiguity is named, so an honest decoy cannot hide a tampered
|
|
186
|
+
* record. `count` is the number of live candidates.
|
|
455
187
|
*/
|
|
456
188
|
export const COUNTERPART_AMBIGUOUS = "counterpart_ambiguous";
|
|
457
189
|
|
|
190
|
+
/** A formed pair that failed checks, listed in §4.1's fixed order. */
|
|
191
|
+
export const PAIR_FAILED = "pair_failed";
|
|
192
|
+
|
|
193
|
+
/** This record itself is malformed under §4.1 (`recordIsMalformed`). */
|
|
194
|
+
export const RECORD_MALFORMED = "record_malformed";
|
|
195
|
+
|
|
458
196
|
/**
|
|
459
|
-
* A
|
|
460
|
-
*
|
|
197
|
+
* A candidate counterpart is malformed and this record is not. §4.1 reports
|
|
198
|
+
* the worst candidate, so without this kind one junk record signed by the
|
|
199
|
+
* counterparty's key would blame the honest record and drop its other failures.
|
|
200
|
+
* `incomplete`: the defect is the candidate's, which carries its own
|
|
201
|
+
* `RECORD_MALFORMED`.
|
|
461
202
|
*/
|
|
462
|
-
export const
|
|
203
|
+
export const COUNTERPART_MALFORMED = "counterpart_malformed";
|
|
463
204
|
|
|
464
205
|
/**
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
*
|
|
206
|
+
* This record is one of the two hand-off types, signed and intact, and does
|
|
207
|
+
* not follow its vocabulary (`recordIsNonconforming`). A producer's error,
|
|
208
|
+
* never a proven alteration.
|
|
468
209
|
*/
|
|
469
|
-
export const
|
|
210
|
+
export const RECORD_NONCONFORMING = "record_nonconforming";
|
|
470
211
|
|
|
471
212
|
/**
|
|
472
|
-
* A
|
|
473
|
-
*
|
|
474
|
-
*
|
|
475
|
-
* outranks `failed`, so one junk record signed by the counterparty's key makes
|
|
476
|
-
* the reading of a perfectly well formed record `malformed` too. Filed under
|
|
477
|
-
* `RECORD_MALFORMED`, that read as "this record is not a well formed hand-off
|
|
478
|
-
* record" about the HONEST record, and the branch it took skipped the
|
|
479
|
-
* counterpart, ambiguity and `failedChecks` reporting entirely: one junk record
|
|
480
|
-
* both blamed the wrong party and stripped a named `message_hash` failure out
|
|
481
|
-
* of the case. That is proof#133 ruling 6, which 4.1 closed by keeping the
|
|
482
|
-
* UNION of `failedChecks`, reappearing one layer out because this layer
|
|
483
|
-
* discarded that union.
|
|
484
|
-
*
|
|
485
|
-
* A record's own conformance and its candidate's are different facts and do
|
|
486
|
-
* not share a kind. This one is `incomplete` and not `malformed`: the proven
|
|
487
|
-
* defect belongs to the candidate, which carries its own `RECORD_MALFORMED`
|
|
488
|
-
* line, and what is true of THIS record is that its pairing could not be
|
|
489
|
-
* determined. Contributing `malformed` here would attribute the candidate's
|
|
490
|
-
* defect to the record a party being accused did not write.
|
|
213
|
+
* A candidate counterpart is nonconforming and this record is not.
|
|
214
|
+
* `incomplete` for the reason `COUNTERPART_MALFORMED` is: the defect is the
|
|
215
|
+
* candidate's, which carries its own `RECORD_NONCONFORMING`.
|
|
491
216
|
*/
|
|
492
|
-
export const
|
|
217
|
+
export const COUNTERPART_NONCONFORMING = "counterpart_nonconforming";
|
|
493
218
|
|
|
494
219
|
/**
|
|
495
|
-
* A record
|
|
496
|
-
*
|
|
497
|
-
*
|
|
498
|
-
*
|
|
499
|
-
*
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
* with an `ff` ligature in `event_type` and a Cyrillic `е` in
|
|
504
|
-
* `counterparty_public_key`, left 4.1's scope, took the `not_a_handoff_record`
|
|
505
|
-
* branch, and bought a case reading of `verified` with zero exceptions where
|
|
506
|
-
* the conformant spelling read `mixed` with four. proof#136 ratified that a
|
|
507
|
-
* respelling buys a DIFFERENT LINE rather than no line; at this layer it
|
|
508
|
-
* bought no line at all, and a strictly better outcome than honesty.
|
|
509
|
-
*
|
|
510
|
-
* It is not an accusation. `handoffs[]` MAY be empty and an unclaimed record
|
|
511
|
-
* may be entirely innocent. What it is, is the end of the silence: the reader
|
|
512
|
-
* cannot tell whether an unclaimed record is part of the hand-over the holder
|
|
513
|
-
* meant to give or one it is keeping out of its own account, and that is "I
|
|
514
|
-
* could not tell", which is why it carries `incomplete` and not `verified`.
|
|
515
|
-
*
|
|
516
|
-
* THE COST, stated rather than left to be found: a pack that declares records
|
|
517
|
-
* which are not hand-off sides at all, an ordinary log line among them, is
|
|
518
|
-
* named once per such record, so a case holding one cannot read `verified`.
|
|
519
|
-
* That is the price of a rule with no branch the holder selects, and the only
|
|
520
|
-
* narrower rule available keys on a value the holder writes. A surface that
|
|
521
|
-
* wants the two apart reads `CaseRecordReading.findings`, where
|
|
522
|
-
* `not_a_handoff_record` still says which is which.
|
|
220
|
+
* A record its pack declares and its pack's readable declaration claims in no
|
|
221
|
+
* `handoffs[]` entry (§5.4.2 rules 2 and 7 cannot reach it). Turns on what the
|
|
222
|
+
* pack said, never on whether §4.1 governs the record, so respelling
|
|
223
|
+
* `event_type` cannot move a record out of this check.
|
|
224
|
+
*
|
|
225
|
+
* Not an accusation: `handoffs[]` may be empty and the record innocent, so it
|
|
226
|
+
* is `incomplete`. The cost is that a declared non-hand-off record (a log line)
|
|
227
|
+
* is named too; `CaseRecordReading.findings` tells the two apart.
|
|
523
228
|
*/
|
|
524
229
|
export const RECORD_UNCLAIMED = "record_unclaimed_by_its_pack";
|
|
525
230
|
|
|
526
231
|
/**
|
|
527
|
-
* The pack
|
|
528
|
-
*
|
|
529
|
-
*
|
|
530
|
-
* `event_bytes` is OPTIONAL (5.1, 5.3) and withholding it is minimum
|
|
531
|
-
* disclosure, which the corpus calls a customer's right and not a defect
|
|
532
|
-
* (proof#137, defect 3), so this is not a deviation and does not fail. It is
|
|
533
|
-
* still SAID, because a withheld record is exactly where a missing counterpart
|
|
534
|
-
* would hide, and a reader that dropped it silently would read a trimmed pack
|
|
535
|
-
* as an honest one.
|
|
232
|
+
* The pack withheld `event_bytes` for a record. Withholding is minimal
|
|
233
|
+
* disclosure (§5.1, §5.3), not a deviation, but it is said: a withheld record is
|
|
234
|
+
* where a missing counterpart would hide.
|
|
536
235
|
*/
|
|
537
236
|
export const RECORD_BYTES_WITHHELD = "record_bytes_withheld";
|
|
538
237
|
|
|
539
238
|
/**
|
|
540
|
-
* The pack
|
|
541
|
-
*
|
|
542
|
-
* `packRecords`). Bytes the holder handed over that do not decode are a defect
|
|
543
|
-
* in what was handed over, not an absence, so this one is `malformed`.
|
|
239
|
+
* The pack supplied `event_bytes` that do not read as `JCS(event record)`.
|
|
240
|
+
* A defect in what was handed over, so `malformed`.
|
|
544
241
|
*/
|
|
545
242
|
export const RECORD_BYTES_UNREADABLE = "record_bytes_unreadable";
|
|
546
243
|
|
|
547
|
-
/**
|
|
548
|
-
* The pack deviates from its own declaration (5.4.2). A proven mismatch, so it
|
|
549
|
-
* fails: a case cannot be clean over a pack that is proven to be trimmed.
|
|
550
|
-
*/
|
|
244
|
+
/** The pack deviates from its own declaration (§5.4.2). A proven mismatch: `failed`. */
|
|
551
245
|
export const PACK_DECLARATION_DEVIATES = "pack_declaration_deviates";
|
|
552
246
|
|
|
553
247
|
/**
|
|
554
|
-
* The pack and the
|
|
555
|
-
*
|
|
556
|
-
*
|
|
557
|
-
* declares `content_withheld` for which bytes reached this reader from the
|
|
558
|
-
* side-content map alone.
|
|
559
|
-
*
|
|
560
|
-
* `incomplete` and never `failed`, which is the whole point of the kind. 5.4
|
|
561
|
-
* grades a pack's declaration partly against content the READER was handed,
|
|
562
|
-
* and whoever assembles a case fills that map in. Under
|
|
563
|
-
* `PACK_DECLARATION_DEVIATES` an accuser could put an intact pack into a
|
|
564
|
-
* `failed` case WITH NO KEY AT ALL: supply bytes for a record its holder
|
|
565
|
-
* declared withheld, or drop the bytes of one it declared disclosed. The pack
|
|
566
|
-
* was intact, its wrapper verified, and its holder had done nothing. This is
|
|
567
|
-
* the mirror of proof#143, where a counterparty makes ITSELF look better by
|
|
568
|
-
* choosing what it hands over.
|
|
248
|
+
* The pack and the unsigned content beside it do not line up (§5.4.2 rules 3
|
|
249
|
+
* and 4): a record declared `disclosed` with no bytes, or declared
|
|
250
|
+
* `content_withheld` with bytes from the side-content map alone.
|
|
569
251
|
*
|
|
570
|
-
*
|
|
571
|
-
*
|
|
252
|
+
* `incomplete`, never `failed`: whoever assembles a case fills in that map, so
|
|
253
|
+
* otherwise an accuser could fail an intact pack with no key. Emitted beside
|
|
254
|
+
* `PACK_DECLARATION_DEVIATES` when a pack does both.
|
|
572
255
|
*/
|
|
573
256
|
export const PACK_DECLARATION_CONTENT_DEPENDENT = "pack_declaration_content_dependent";
|
|
574
257
|
|
|
575
258
|
/**
|
|
576
|
-
* The pack
|
|
577
|
-
*
|
|
578
|
-
* does not satisfy 5.4.1. `declaration_version` is chosen by the same party
|
|
579
|
-
* that assembles the pack and has no accept-set of its own, so this is UNKNOWN
|
|
580
|
-
* and never clean.
|
|
259
|
+
* The pack's `declaration` cannot be read (§5.4.3). Its version is chosen by
|
|
260
|
+
* the pack's assembler with no accept-set, so this is unknown, never clean.
|
|
581
261
|
*/
|
|
582
262
|
export const PACK_DECLARATION_UNREADABLE = "pack_declaration_unreadable";
|
|
583
263
|
|
|
584
264
|
/**
|
|
585
|
-
* The pack carries no `declaration`
|
|
586
|
-
*
|
|
587
|
-
* `verified`, because there is nothing to check the pack against, and never
|
|
588
|
-
* `failed`, because nothing is wrong.
|
|
265
|
+
* The pack carries no `declaration`. `partial` (§5.4.3): nothing to check
|
|
266
|
+
* against, and nothing wrong.
|
|
589
267
|
*/
|
|
590
268
|
export const PACK_UNDECLARED = "pack_undeclared";
|
|
591
269
|
|
|
592
|
-
/**
|
|
593
|
-
* The pack's `body_type` is not one this reader knows to carry `events[]`
|
|
594
|
-
* (`CASE_BODY_TYPES`).
|
|
595
|
-
*/
|
|
270
|
+
/** The pack's `body_type` is not in `CASE_BODY_TYPES`. */
|
|
596
271
|
export const PACK_BODY_TYPE_UNKNOWN = "pack_body_type_unknown";
|
|
597
272
|
|
|
598
|
-
/**
|
|
599
|
-
* The pack carries a body this reader knows and no records in it. A pack that
|
|
600
|
-
* contributes nothing is named rather than passed over.
|
|
601
|
-
*/
|
|
273
|
+
/** The pack carries a known body with no records. */
|
|
602
274
|
export const PACK_HOLDS_NO_RECORDS = "pack_holds_no_records";
|
|
603
275
|
|
|
604
276
|
/**
|
|
605
|
-
* The
|
|
606
|
-
*
|
|
607
|
-
*
|
|
608
|
-
*
|
|
609
|
-
* `
|
|
610
|
-
*
|
|
611
|
-
* Found by constructing it, while enumerating this module's exits: the wrapper
|
|
612
|
-
* key is the ONE input that separates the two counterpart kinds, and a key this
|
|
613
|
-
* reader could not read used to drop out of that set in silence. The Python
|
|
614
|
-
* `verify_evidence_bundle` accepts a wrapper `public_key` with ASCII
|
|
615
|
-
* whitespace inside it or around it (`bytes.fromhex` skips it; this kernel's
|
|
616
|
-
* verifier rejects it), so a counterparty that signed its own pack under
|
|
617
|
-
* `" " + key` handed a Python reader a pack that verified, contributed no key,
|
|
618
|
-
* and moved the subject from `COUNTERPART_ABSENT_FROM_COUNTERPARTY_RECORDS` to
|
|
619
|
-
* the innocent `COUNTERPART_ABSENT`. The same repair 4.1 made for a RECORD
|
|
620
|
-
* whose envelope key it cannot read (proof#133, fourth hunt): a key a reader
|
|
621
|
-
* holds and cannot read is named, never treated as absent. It is `incomplete`
|
|
622
|
-
* rather than `malformed` because whether the spelling verifies is the envelope
|
|
623
|
-
* layer's call (proof#135), and this reader only knows it cannot read it.
|
|
277
|
+
* The pack's wrapper key cannot be read (`normalizedSigningKey` returns
|
|
278
|
+
* nothing), so its signer is unknown. None of its records are read, and a
|
|
279
|
+
* one-sided record whose named key signed no verified pack reads
|
|
280
|
+
* `COUNTERPART_ABSENT_SIGNER_UNKNOWN`. A key a reader holds and cannot read is
|
|
281
|
+
* named, never treated as absent. `incomplete`: whether the spelling verifies
|
|
282
|
+
* is the envelope layer's call.
|
|
624
283
|
*/
|
|
625
284
|
export const PACK_KEY_UNREADABLE = "pack_key_unreadable";
|
|
626
285
|
|
|
627
286
|
/**
|
|
628
|
-
* The pack
|
|
629
|
-
*
|
|
630
|
-
*
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
*
|
|
636
|
-
*
|
|
637
|
-
*
|
|
638
|
-
*
|
|
639
|
-
*
|
|
640
|
-
* `failed`: a pack in the case is proven broken.
|
|
287
|
+
* The pack's `bundle.json` cannot be read as JSON: it does not parse, nests too deep, or
|
|
288
|
+
* names one member twice. The pack is not verified and none of its records are read, and
|
|
289
|
+
* every other pack in the case still is. `incomplete`: the signature could not be checked.
|
|
290
|
+
*/
|
|
291
|
+
export const PACK_UNREADABLE = "pack_unreadable";
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* `verifyEvidenceBundle`, run offline here, returned `failed` or `malformed`.
|
|
295
|
+
* None of its records are read (§4.1 requires verified envelopes), and its
|
|
296
|
+
* wrapper key is never counted as a pack signer: an unverified wrapper claiming
|
|
297
|
+
* the counterparty's key is what an accuser would forge. `failed`.
|
|
641
298
|
*/
|
|
642
299
|
export const PACK_DOES_NOT_VERIFY = "pack_does_not_verify";
|
|
643
300
|
|
|
644
301
|
/**
|
|
645
|
-
* The pack's wrapper
|
|
646
|
-
* (evidence-bundle 5.5.1
|
|
647
|
-
*
|
|
648
|
-
*
|
|
649
|
-
*
|
|
650
|
-
*
|
|
651
|
-
*
|
|
652
|
-
* otherwise the full grade. A body the verifier could not read is `malformed`,
|
|
653
|
-
* a body that is not one of the two event-carrying bodies takes the signed
|
|
654
|
-
* run's status, and a verifier that graded a different number of records than
|
|
655
|
-
* the pack holds takes the signed status, or `malformed` where that status is
|
|
302
|
+
* The pack's wrapper holds and its case grade is not `verified`
|
|
303
|
+
* (evidence-bundle §5.5.1). The case grade is the §5.4.5 pack grade taken
|
|
304
|
+
* twice: signed (side map empty) and full (with it). The signed grade decides
|
|
305
|
+
* where it is `failed` or `malformed`; a failure only the full grade reaches is
|
|
306
|
+
* `incomplete`, since it rests on unsigned bytes; otherwise the full grade.
|
|
307
|
+
* An unreadable body is `malformed`; a non-event body takes the signed status;
|
|
308
|
+
* a record-count mismatch takes the signed status, or `malformed` where that is
|
|
656
309
|
* `verified`.
|
|
657
310
|
*
|
|
658
|
-
*
|
|
659
|
-
*
|
|
660
|
-
* `
|
|
661
|
-
* CONTRIBUTES the grade, and `incomplete` for `pending`, `not_found` or
|
|
662
|
-
* `mixed`, so the case verdict never reads above its weakest pack.
|
|
311
|
+
* Carries `status`, `causes` (the deciding grade's cause kinds) and `reasons`
|
|
312
|
+
* (from the signed run). Contributes the grade, or `incomplete` for `pending`,
|
|
313
|
+
* `not_found` or `mixed`, so the case never reads above its weakest pack.
|
|
663
314
|
*/
|
|
664
315
|
export const PACK_GRADE_NOT_VERIFIED = "pack_grade_not_verified";
|
|
665
316
|
|
|
666
317
|
/**
|
|
667
|
-
* A record
|
|
668
|
-
*
|
|
669
|
-
*
|
|
670
|
-
* is proven not to verify. As a reason to drop the whole pack it let the
|
|
671
|
-
* counterparty add one junk event to its own pack, re-seal it, and strip a
|
|
672
|
-
* proven `message_hash` failure out of the case (gate 3 on proof#140).
|
|
318
|
+
* A record whose own envelope does not verify, in a pack whose wrapper holds.
|
|
319
|
+
* Not read by §4.1. `failed`. Scoped to the record so one junk event cannot
|
|
320
|
+
* drop the whole pack and a proven failure with it.
|
|
673
321
|
*/
|
|
674
322
|
export const RECORD_ENVELOPE_DOES_NOT_VERIFY = "record_envelope_does_not_verify";
|
|
675
323
|
|
|
676
324
|
/**
|
|
677
|
-
* A record whose envelope verifies
|
|
678
|
-
*
|
|
679
|
-
*
|
|
680
|
-
*
|
|
681
|
-
* remove its own tampered receipt from pairing by breaking that receipt's
|
|
682
|
-
* inclusion proof would be choosing its own reading.
|
|
325
|
+
* A record whose envelope verifies but which fails another check in its pack
|
|
326
|
+
* (inclusion proof, checkpoint or stamp join, a retired field). `failed`, and
|
|
327
|
+
* the record stays in the pile, so breaking its own proof cannot remove a
|
|
328
|
+
* tampered receipt from pairing.
|
|
683
329
|
*/
|
|
684
330
|
export const RECORD_DOES_NOT_VERIFY_IN_ITS_PACK = "record_does_not_verify_in_its_pack";
|
|
685
331
|
|
|
686
332
|
/**
|
|
687
|
-
* A record whose
|
|
688
|
-
*
|
|
689
|
-
*
|
|
690
|
-
* side map EMPTY: the content is inside the signed body. The same mismatch from
|
|
691
|
-
* content handed in beside the pack is `RECORD_CONTENT_DEPENDENT` (proof#159).
|
|
692
|
-
* `failed`, and the record STAYS in the pile: content beside a pack is unsigned and often
|
|
693
|
-
* supplied by whoever assembled the case, and gate 3 on proof#140 showed that
|
|
694
|
-
* party erasing an honest verified pair by swapping it.
|
|
333
|
+
* A record whose disclosed content does not hash to its `content_hash` (or a
|
|
334
|
+
* hand-off record's `message_hash`) with the side map empty: the content is in
|
|
335
|
+
* the signed body. `failed`, and the record stays in the pile.
|
|
695
336
|
*/
|
|
696
337
|
export const RECORD_CONTENT_MISMATCH = "record_content_mismatch";
|
|
697
338
|
|
|
698
339
|
/**
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
* bundle
|
|
702
|
-
*
|
|
703
|
-
*
|
|
704
|
-
* that map alone is `RECORD_CONTENT_DEPENDENT`. Both are `incomplete`: the
|
|
705
|
-
* reader established that the bytes it was handed are not the bytes the record
|
|
706
|
-
* commits to, and nothing about the party that signed the record, because
|
|
707
|
-
* whoever assembled the case chose those bytes with no key. Up to v1.15 both
|
|
708
|
-
* readings carried `failed`, and one flipped nibble of accuser-supplied content
|
|
709
|
-
* put a `failed` against an intact pack; under A17's headline rule that would
|
|
710
|
-
* have become the case's headline.
|
|
711
|
-
*
|
|
712
|
-
* The test that separates the two is the one proof#159 states for the inverse:
|
|
713
|
-
* every deviation a signature covers still reads `failed` with the side map
|
|
714
|
-
* EMPTY. So the pack is verified twice, with the map and without it, and what
|
|
715
|
-
* fails without it is `PACK_GRADE_NOT_VERIFIED`, `RECORD_CONTENT_MISMATCH`
|
|
716
|
-
* (content inside the signed body) or `RECORD_DOES_NOT_VERIFY_IN_ITS_PACK`,
|
|
717
|
-
* each still `failed`. Emptying the map is the most a party that signed
|
|
718
|
-
* nothing can do to starve the check, and it cannot remove any of those.
|
|
340
|
+
* A pack whose bundle verdict is `failed` only with the unsigned side content,
|
|
341
|
+
* and a record whose content mismatch comes from that content alone
|
|
342
|
+
* (evidence-bundle §5.5.4). Both `incomplete`: whoever assembled the case chose
|
|
343
|
+
* those bytes with no key. Every deviation a signature covers still fails with
|
|
344
|
+
* the side map empty, so starving the check cannot hide one.
|
|
719
345
|
*/
|
|
720
346
|
export const PACK_BUNDLE_CONTENT_DEPENDENT = "pack_bundle_content_dependent";
|
|
721
347
|
export const RECORD_CONTENT_DEPENDENT = "record_content_dependent";
|
|
722
348
|
|
|
723
349
|
/**
|
|
724
|
-
* A record
|
|
725
|
-
*
|
|
726
|
-
* `incomplete`: whether it verifies is what the kernels disagree about.
|
|
350
|
+
* A record under a quarantined profile (`CASE_QUARANTINED_PROFILES`). Not read.
|
|
351
|
+
* `incomplete`.
|
|
727
352
|
*/
|
|
728
353
|
export const RECORD_PROFILE_QUARANTINED = "record_profile_quarantined";
|
|
729
354
|
|
|
@@ -731,18 +356,16 @@ export const RECORD_PROFILE_QUARANTINED = "record_profile_quarantined";
|
|
|
731
356
|
export const CASE_HOLDS_NO_PACKS = "case_holds_no_packs";
|
|
732
357
|
|
|
733
358
|
/**
|
|
734
|
-
*
|
|
735
|
-
*
|
|
736
|
-
*
|
|
737
|
-
*
|
|
738
|
-
* of that name.
|
|
359
|
+
* Notes (§5.5.3): findings with no verdict contribution, printed in the scope
|
|
360
|
+
* sentence so a clean verdict never hides them. `pack_undeclared` is a note
|
|
361
|
+
* only where the reading holds no hand-off record; otherwise it is the
|
|
362
|
+
* exception.
|
|
739
363
|
*/
|
|
740
364
|
export const CONTENT_WITHHELD_BY_DECLARATION = "content_withheld_by_declaration";
|
|
741
365
|
/**
|
|
742
|
-
* A pack's wrapper carries
|
|
743
|
-
*
|
|
744
|
-
*
|
|
745
|
-
* rather than presenting it as evidence, and it contributes no verdict.
|
|
366
|
+
* A pack's wrapper carries `co_signatures` (evidence-bundle section 2). It verifies
|
|
367
|
+
* nothing and sits outside the signed scope, so it is named, not presented as
|
|
368
|
+
* evidence.
|
|
746
369
|
*/
|
|
747
370
|
export const CO_SIGNATURES_PRESENT = "co_signatures_present";
|
|
748
371
|
export const CASE_NOTE_KINDS = [
|
|
@@ -752,18 +375,17 @@ export const CASE_NOTE_KINDS = [
|
|
|
752
375
|
] as const;
|
|
753
376
|
export type CaseNoteKind = (typeof CASE_NOTE_KINDS)[number];
|
|
754
377
|
|
|
755
|
-
/**
|
|
756
|
-
* Every exception this reader can report, as a closed set. A surface may render
|
|
757
|
-
* them and a test may assert over them; nothing outside this array is emitted.
|
|
758
|
-
*/
|
|
378
|
+
/** Every exception this reader can report. Nothing outside this set is emitted. */
|
|
759
379
|
export const CASE_EXCEPTION_KINDS = [
|
|
760
380
|
COUNTERPART_ABSENT,
|
|
761
381
|
COUNTERPART_ABSENT_FROM_COUNTERPARTY_RECORDS,
|
|
762
382
|
COUNTERPART_ABSENT_SIGNER_UNKNOWN,
|
|
763
383
|
COUNTERPART_AMBIGUOUS,
|
|
764
384
|
COUNTERPART_MALFORMED,
|
|
385
|
+
COUNTERPART_NONCONFORMING,
|
|
765
386
|
PAIR_FAILED,
|
|
766
387
|
RECORD_MALFORMED,
|
|
388
|
+
RECORD_NONCONFORMING,
|
|
767
389
|
RECORD_UNCLAIMED,
|
|
768
390
|
RECORD_BYTES_WITHHELD,
|
|
769
391
|
RECORD_BYTES_UNREADABLE,
|
|
@@ -773,6 +395,7 @@ export const CASE_EXCEPTION_KINDS = [
|
|
|
773
395
|
PACK_UNDECLARED,
|
|
774
396
|
PACK_BODY_TYPE_UNKNOWN,
|
|
775
397
|
PACK_HOLDS_NO_RECORDS,
|
|
398
|
+
PACK_UNREADABLE,
|
|
776
399
|
PACK_KEY_UNREADABLE,
|
|
777
400
|
PACK_DOES_NOT_VERIFY,
|
|
778
401
|
PACK_GRADE_NOT_VERIFIED,
|
|
@@ -788,354 +411,219 @@ export const CASE_EXCEPTION_KINDS = [
|
|
|
788
411
|
export type CaseExceptionKind = (typeof CASE_EXCEPTION_KINDS)[number];
|
|
789
412
|
|
|
790
413
|
/**
|
|
791
|
-
* The status
|
|
792
|
-
*
|
|
793
|
-
*
|
|
794
|
-
* `
|
|
795
|
-
*
|
|
796
|
-
*
|
|
797
|
-
* - `
|
|
798
|
-
*
|
|
799
|
-
*
|
|
800
|
-
*
|
|
801
|
-
*
|
|
802
|
-
* - `read_flagged`: the record went into the pile, and at least one
|
|
803
|
-
* exception carries its `packIndex` and `recordIndex`, or its pack's
|
|
804
|
-
* declaration check names its `eventId` by a deviation or a content
|
|
805
|
-
* finding (`declarationFindings` is not empty; under an `eventId` the
|
|
806
|
-
* pack carries twice, every record with that id). An exception about the
|
|
807
|
-
* pack alone flags no record.
|
|
808
|
-
* - `not_read`: the record never went into the pile. `reason` is the kind
|
|
809
|
-
* of the exception that says why, on the record
|
|
810
|
-
* (`record_profile_quarantined`, `record_envelope_does_not_verify`,
|
|
811
|
-
* `record_bytes_withheld`, `record_bytes_unreadable`) or on its pack
|
|
812
|
-
* (`pack_key_unreadable`, `pack_does_not_verify`,
|
|
813
|
-
* `pack_body_type_unknown`, `pack_grade_not_verified` where the body
|
|
814
|
-
* could not be read or the verifier graded a different number of records).
|
|
815
|
-
* An export's wrapped records carry the reason of the exception their pack
|
|
816
|
-
* actually got: `pack_body_type_unknown` where its wrapper holds, and
|
|
817
|
-
* otherwise `pack_key_unreadable`, `pack_does_not_verify` or
|
|
818
|
-
* `pack_grade_not_verified`.
|
|
819
|
-
*
|
|
820
|
-
* Before A43 the reading listed only the records it read, so a surface had to
|
|
821
|
-
* work out which records were missing, and why, by a rule of its own.
|
|
414
|
+
* The status each record of each pack gets (evidence-bundle §5.5.4), one per
|
|
415
|
+
* element of `body.events` (or, for an export, its source bundle's events):
|
|
416
|
+
*
|
|
417
|
+
* - `read_clean`: in the pile, named by no exception and no declaration
|
|
418
|
+
* finding. Says nothing about its pack; a surface must show the pack grade
|
|
419
|
+
* beside it.
|
|
420
|
+
* - `read_flagged`: in the pile, and named by an exception or a declaration
|
|
421
|
+
* finding (every record sharing a duplicated `eventId`). A pack-only
|
|
422
|
+
* exception flags no record.
|
|
423
|
+
* - `not_read`: never in the pile. `reason` is the record or pack exception
|
|
424
|
+
* that says why; an export's wrapped records take their pack's exception.
|
|
822
425
|
*/
|
|
823
426
|
export const CASE_RECORD_STATUSES = ["read_clean", "read_flagged", "not_read"] as const;
|
|
824
427
|
export type CaseRecordStatus = (typeof CASE_RECORD_STATUSES)[number];
|
|
825
428
|
|
|
826
429
|
/**
|
|
827
|
-
* What each exception kind contributes to the case verdict
|
|
828
|
-
*
|
|
829
|
-
*
|
|
830
|
-
*
|
|
831
|
-
*
|
|
832
|
-
* Read the `incomplete` rows as "I could not tell", which is the distinction
|
|
833
|
-
* this whole module turns on. None of them is `verified`: an exception never
|
|
834
|
-
* contributes a clean verdict, which is what makes the invariant in `verdictOf`
|
|
835
|
-
* hold.
|
|
836
|
-
*
|
|
837
|
-
* `PACK_GRADE_NOT_VERIFIED` is the one row that is not a constant: it
|
|
838
|
-
* contributes the pack's case grade, which is never `verified` (the exception
|
|
839
|
-
* is emitted only when it is not). `null` marks that row, and `readCase` fills
|
|
840
|
-
* it from `status`.
|
|
841
|
-
*/
|
|
842
|
-
const EXCEPTION_VERDICT: Record<CaseExceptionKind, CaseVerdict | null> = {
|
|
843
|
-
[COUNTERPART_ABSENT]: "incomplete",
|
|
844
|
-
[COUNTERPART_ABSENT_FROM_COUNTERPARTY_RECORDS]: "incomplete",
|
|
845
|
-
[COUNTERPART_ABSENT_SIGNER_UNKNOWN]: "incomplete",
|
|
846
|
-
[COUNTERPART_AMBIGUOUS]: "incomplete",
|
|
847
|
-
[COUNTERPART_MALFORMED]: "incomplete",
|
|
848
|
-
[PAIR_FAILED]: "failed",
|
|
849
|
-
[RECORD_MALFORMED]: "malformed",
|
|
850
|
-
[RECORD_UNCLAIMED]: "incomplete",
|
|
851
|
-
[RECORD_BYTES_WITHHELD]: "incomplete",
|
|
852
|
-
[RECORD_BYTES_UNREADABLE]: "malformed",
|
|
853
|
-
[PACK_DECLARATION_DEVIATES]: "failed",
|
|
854
|
-
[PACK_DECLARATION_CONTENT_DEPENDENT]: "incomplete",
|
|
855
|
-
[PACK_DECLARATION_UNREADABLE]: "incomplete",
|
|
856
|
-
[PACK_UNDECLARED]: "partial",
|
|
857
|
-
[PACK_BODY_TYPE_UNKNOWN]: "incomplete",
|
|
858
|
-
[PACK_HOLDS_NO_RECORDS]: "incomplete",
|
|
859
|
-
[PACK_KEY_UNREADABLE]: "incomplete",
|
|
860
|
-
[PACK_DOES_NOT_VERIFY]: "failed",
|
|
861
|
-
[PACK_GRADE_NOT_VERIFIED]: null,
|
|
862
|
-
[PACK_BUNDLE_CONTENT_DEPENDENT]: "incomplete",
|
|
863
|
-
[RECORD_ENVELOPE_DOES_NOT_VERIFY]: "failed",
|
|
864
|
-
[RECORD_DOES_NOT_VERIFY_IN_ITS_PACK]: "failed",
|
|
865
|
-
[RECORD_CONTENT_MISMATCH]: "failed",
|
|
866
|
-
[RECORD_CONTENT_DEPENDENT]: "incomplete",
|
|
867
|
-
[RECORD_PROFILE_QUARANTINED]: "incomplete",
|
|
868
|
-
[CASE_HOLDS_NO_PACKS]: "partial",
|
|
869
|
-
};
|
|
870
|
-
|
|
871
|
-
/**
|
|
872
|
-
* The case grades `PACK_GRADE_NOT_VERIFIED` contributes as themselves. Every
|
|
873
|
-
* other grade a pack can reach (`pending`, `not_found`, `mixed`, which a pack
|
|
874
|
-
* grade passes through and a case verdict never issues) is `incomplete`.
|
|
430
|
+
* What each exception kind contributes to the case verdict, read off the claim it maps to
|
|
431
|
+
* (`KIND_CLAIMS` in `claims.ts`): a disproof is `failed`, anything not proven
|
|
432
|
+
* `incomplete`. A kind with no mapping throws rather than reading as a quiet `verified`.
|
|
433
|
+
* `null` marks `PACK_GRADE_NOT_VERIFIED`, which contributes the pack's case grade from
|
|
434
|
+
* `status`.
|
|
875
435
|
*/
|
|
876
|
-
const
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
]
|
|
882
|
-
|
|
883
|
-
|
|
436
|
+
const EXCEPTION_VERDICT: Record<CaseExceptionKind, CaseVerdict | null> = Object.fromEntries(
|
|
437
|
+
CASE_EXCEPTION_KINDS.map((kind) => {
|
|
438
|
+
if (kind === PACK_GRADE_NOT_VERIFIED) return [kind, null];
|
|
439
|
+
const mapping = KIND_CLAIMS[kind];
|
|
440
|
+
if (mapping === undefined) throw new Error(`KeyError: '${kind}'`);
|
|
441
|
+
return [kind, mapping[1] === "disproven" ? "failed" : "incomplete"];
|
|
442
|
+
}),
|
|
443
|
+
) as Record<CaseExceptionKind, CaseVerdict | null>;
|
|
884
444
|
|
|
885
445
|
/**
|
|
886
|
-
* One pack in a case, as
|
|
887
|
-
*
|
|
888
|
-
* `
|
|
889
|
-
*
|
|
890
|
-
* the party that assembled it, and two packs are free to claim one name.
|
|
891
|
-
*
|
|
892
|
-
* A caller passes packs as it received them. `readCase` runs
|
|
893
|
-
* `verifyEvidenceBundle` over each one itself, so `HeldRecord`'s precondition
|
|
894
|
-
* (every envelope it compares has verified) is met by this module and cannot
|
|
895
|
-
* be skipped by a caller. A pack that does not verify is
|
|
896
|
-
* `PACK_DOES_NOT_VERIFY`.
|
|
446
|
+
* One pack in a case, as handed to the reader. `ref` is the reader's own name
|
|
447
|
+
* for it, never read from the pack, since two packs can claim one name.
|
|
448
|
+
* `readCase` verifies each pack itself, so the verified-envelope precondition
|
|
449
|
+
* of `HeldRecord` cannot be skipped by a caller.
|
|
897
450
|
*/
|
|
898
451
|
export interface CasePack {
|
|
899
452
|
ref: string;
|
|
900
453
|
bundle: unknown;
|
|
901
454
|
content?: SideContentMap | null;
|
|
455
|
+
/** Why the pack's `bundle.json` could not be read, where it could not; `bundle` is then ignored (`PACK_UNREADABLE`). */
|
|
456
|
+
unreadable?: string | null;
|
|
902
457
|
}
|
|
903
458
|
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
/** One named exception, as data. A surface renders its own prose or calls
|
|
907
|
-
* `renderException`. */
|
|
459
|
+
/** One named exception, as data. A surface renders it or calls `renderException`. */
|
|
908
460
|
export interface CaseException {
|
|
909
461
|
kind: CaseExceptionKind;
|
|
910
|
-
/**
|
|
911
|
-
* Which pack this is about, by the reader's own name for it. Two packs can
|
|
912
|
-
* carry one `ref`, so it is a label and never the join: `packIndex` is.
|
|
913
|
-
*/
|
|
462
|
+
/** The reader's name for the pack: a label, never the join (`packIndex` is). */
|
|
914
463
|
packRef?: string;
|
|
915
|
-
/**
|
|
916
|
-
* Which pack this is about, by its position in `CaseReading.packs` (0
|
|
917
|
-
* first), present wherever `packRef` is (A43). Unique in the reading.
|
|
918
|
-
*/
|
|
464
|
+
/** The pack's position in `CaseReading.packs`, wherever `packRef` is set. */
|
|
919
465
|
packIndex?: number;
|
|
920
466
|
/** Which record, by its envelope `event_id`. */
|
|
921
467
|
eventId?: string;
|
|
922
468
|
/**
|
|
923
|
-
*
|
|
924
|
-
*
|
|
925
|
-
*
|
|
926
|
-
* `CaseReading.records`.
|
|
469
|
+
* The record's position in its pack's events, wherever `eventId` is set.
|
|
470
|
+
* With `packIndex`, the join to `CaseReading.records`, since one pack can
|
|
471
|
+
* carry an `event_id` twice.
|
|
927
472
|
*/
|
|
928
473
|
recordIndex?: number;
|
|
929
|
-
/**
|
|
930
|
-
* The counterparty key the record named, for the two counterpart kinds.
|
|
931
|
-
* Normalized (`normalizedSigningKey`), so a reader compares what this module
|
|
932
|
-
* compared.
|
|
933
|
-
*/
|
|
474
|
+
/** The counterparty key the record named, normalized as this module compared it. */
|
|
934
475
|
counterpartyKey?: string | null;
|
|
935
|
-
/** Which checks a formed pair failed, in 4.1's fixed order. */
|
|
476
|
+
/** Which checks a formed pair failed, in §4.1's fixed order. */
|
|
936
477
|
failedChecks?: string[];
|
|
937
478
|
/**
|
|
938
|
-
*
|
|
939
|
-
*
|
|
940
|
-
* (v1.17, A20), and it is how many were set aside.
|
|
479
|
+
* The count the exception is about. On `COUNTERPART_ABSENT`, set only where
|
|
480
|
+
* every candidate was set aside, and it is how many were.
|
|
941
481
|
*/
|
|
942
482
|
count?: number;
|
|
943
483
|
/** The pack's case grade, for `PACK_GRADE_NOT_VERIFIED`. */
|
|
944
484
|
status?: CaseVerdict;
|
|
945
485
|
/**
|
|
946
|
-
*
|
|
947
|
-
*
|
|
948
|
-
* not computed (an unreadable body, a body that is not one of the two
|
|
949
|
-
* event-carrying ones, a verifier that graded too few records).
|
|
486
|
+
* For `PACK_GRADE_NOT_VERIFIED`, the §5.4.5 cause kinds of the deciding grade.
|
|
487
|
+
* Empty where no grade was computed.
|
|
950
488
|
*/
|
|
951
489
|
causes?: PackGradeCauseKind[];
|
|
952
490
|
/**
|
|
953
|
-
*
|
|
954
|
-
*
|
|
955
|
-
* graded on the record's own envelope and content.
|
|
491
|
+
* The bundle verifier's own reasons, for `PACK_GRADE_NOT_VERIFIED` and the
|
|
492
|
+
* record kinds graded on the record's envelope and content.
|
|
956
493
|
*/
|
|
957
494
|
reasons?: string[];
|
|
495
|
+
/**
|
|
496
|
+
* For `PACK_GRADE_NOT_VERIFIED`, every problem behind the grade, each mapped to one
|
|
497
|
+
* claim and one state (`claims.ts`), at this pack's position. A grade that is not
|
|
498
|
+
* `failed` carries no disproof: one that only the unsigned side map reached reads
|
|
499
|
+
* `not_proven`.
|
|
500
|
+
*/
|
|
501
|
+
problems?: Problem[];
|
|
958
502
|
}
|
|
959
503
|
|
|
960
504
|
/**
|
|
961
|
-
* One record of one pack, with its status and, where
|
|
962
|
-
*
|
|
963
|
-
*
|
|
964
|
-
*
|
|
965
|
-
* in bundle order: the ones this reader read, including the ones that are not
|
|
966
|
-
* hand-off records at all (`findings` carries `not_a_handoff_record`), and the
|
|
967
|
-
* ones it did not, with `status` `not_read` and the `reason`. 4.1 (v1.9, A14)
|
|
968
|
-
* names why: the one thing an attacker wants is for a record to be absent from
|
|
969
|
-
* the output. An export's wrapped records are listed, `not_read`. A pack whose
|
|
970
|
-
* body carries no `events` array, and wraps none, lists none, and its own
|
|
971
|
-
* exception says why.
|
|
505
|
+
* One record of one pack, with its status and, where read, §4.1's reading.
|
|
506
|
+
* Every record of every pack is listed, in pack then bundle order, read or
|
|
507
|
+
* not, because the one thing an attacker wants is a record absent from the
|
|
508
|
+
* output. A pack with no `events` array lists none; its exception says why.
|
|
972
509
|
*/
|
|
973
510
|
export interface CaseRecordReading {
|
|
974
511
|
packRef: string;
|
|
975
|
-
/** The pack
|
|
512
|
+
/** The pack's position in `CaseReading.packs`. */
|
|
976
513
|
packIndex: number;
|
|
977
514
|
eventId: string;
|
|
978
515
|
/**
|
|
979
|
-
* The record
|
|
980
|
-
*
|
|
981
|
-
* source bundle it wraps.
|
|
516
|
+
* The record's position in its pack's events (for an export, its source
|
|
517
|
+
* bundle's events).
|
|
982
518
|
*/
|
|
983
519
|
recordIndex: number;
|
|
984
|
-
/** One of `CASE_RECORD_STATUSES` (A43). */
|
|
985
520
|
status: CaseRecordStatus;
|
|
986
|
-
/**
|
|
987
|
-
* For `not_read`, the kind of the exception that says why; `null` for a
|
|
988
|
-
* record that was read.
|
|
989
|
-
*/
|
|
521
|
+
/** For `not_read`, the exception kind that says why; else `null`. */
|
|
990
522
|
reason: CaseExceptionKind | null;
|
|
991
523
|
/**
|
|
992
|
-
* The
|
|
993
|
-
*
|
|
994
|
-
*
|
|
995
|
-
* `eventId`, so under an `event_id` the pack carries twice every record with
|
|
996
|
-
* that id carries it. A record that went into the pile with any is
|
|
997
|
-
* `read_flagged` (A43).
|
|
524
|
+
* The declaration check's deviations then content findings for this record's
|
|
525
|
+
* `eventId` (every record sharing a duplicated id). Any makes a read record
|
|
526
|
+
* `read_flagged`.
|
|
998
527
|
*/
|
|
999
528
|
declarationFindings: string[];
|
|
1000
|
-
/** 4.1's verdict, or `null` where
|
|
529
|
+
/** §4.1's verdict, or `null` where it gave none or the record was not read. */
|
|
1001
530
|
verdict: string | null;
|
|
1002
531
|
findings: string[];
|
|
1003
532
|
failedChecks: string[];
|
|
1004
533
|
/**
|
|
1005
|
-
* 4.1's candidate counterparts
|
|
1006
|
-
*
|
|
1007
|
-
* pack one `event_id` twice (A43).
|
|
534
|
+
* §4.1's candidate counterparts as `<pack ref>/<event_id>`: labels for a
|
|
535
|
+
* person, never the join.
|
|
1008
536
|
*/
|
|
1009
537
|
counterparts: string[];
|
|
1010
538
|
/**
|
|
1011
|
-
* The role the pack's v2 declaration signs for this record, `
|
|
1012
|
-
*
|
|
1013
|
-
* v1, or does not declare the record (5.5.1 v1.20).
|
|
539
|
+
* The role the pack's v2 declaration signs for this record, or `null` where
|
|
540
|
+
* the declaration is absent, unreadable, v1 or silent on it (§5.5.1).
|
|
1014
541
|
*/
|
|
1015
542
|
role: DeclarationRole | null;
|
|
543
|
+
/**
|
|
544
|
+
* The `sanning.agent` reading of this record (agent-events.md section 10), read with
|
|
545
|
+
* every record of every pack in the case, or `null` where step 1 gives it no
|
|
546
|
+
* verdict or it was not read. Its findings move no verdict; a `nonconforming`
|
|
547
|
+
* or `partial` reading moves its pack's grade (section 5.4.5).
|
|
548
|
+
*/
|
|
549
|
+
agent: AgentRecordReading | null;
|
|
1016
550
|
}
|
|
1017
551
|
|
|
1018
|
-
/**
|
|
1019
|
-
* One NOTE on the reading (5.5.3 v1.20): something true a reader needs and
|
|
1020
|
-
* nothing a reader could not tell, so it carries no contribution.
|
|
1021
|
-
*/
|
|
552
|
+
/** A note on the reading (§5.5.3): true and needed, with no contribution. */
|
|
1022
553
|
export interface CaseNote {
|
|
1023
554
|
kind: CaseNoteKind;
|
|
1024
|
-
/** Which pack, by the reader's own name for it. */
|
|
1025
555
|
packRef: string;
|
|
1026
|
-
/** Which pack, by its position in `CaseReading.packs` (A43). */
|
|
1027
556
|
packIndex: number;
|
|
1028
|
-
/**
|
|
1029
|
-
* For `CONTENT_WITHHELD_BY_DECLARATION`, how many of the pack's records its
|
|
1030
|
-
* declaration withholds.
|
|
1031
|
-
*/
|
|
557
|
+
/** For `CONTENT_WITHHELD_BY_DECLARATION`, how many records are withheld. */
|
|
1032
558
|
count?: number;
|
|
1033
559
|
}
|
|
1034
560
|
|
|
1035
561
|
/**
|
|
1036
|
-
* One pack as
|
|
1037
|
-
*
|
|
1038
|
-
* its wrapper, and never by an organization (D10).
|
|
562
|
+
* One pack as read, in the order given (§5.5.4). A surface names a pack by
|
|
563
|
+
* `signingKey`, never by an organization.
|
|
1039
564
|
*/
|
|
1040
565
|
export interface CasePackReading {
|
|
1041
|
-
/**
|
|
1042
|
-
* The pack's position in the order the reader was given the packs, 0 first
|
|
1043
|
-
* (A43): the one name for a pack that is unique in the reading. Every
|
|
1044
|
-
* exception, note and record names its pack by it as `packIndex`.
|
|
1045
|
-
*/
|
|
566
|
+
/** The pack's position in the order given: its one unique name in the reading. */
|
|
1046
567
|
index: number;
|
|
1047
|
-
/** The reader's own name for the pack.
|
|
568
|
+
/** The reader's own name for the pack. Not unique. */
|
|
1048
569
|
ref: string;
|
|
1049
570
|
/** The wrapper key, normalized, or `null` where this reader could not read it. */
|
|
1050
571
|
signingKey: string | null;
|
|
1051
|
-
/**
|
|
1052
|
-
* The case grade (5.5.1 step 1), or `null` where the wrapper did not hold or
|
|
1053
|
-
* the key could not be read.
|
|
1054
|
-
*/
|
|
572
|
+
/** The case grade (§5.5.1), or `null` where the wrapper did not hold or the key is unreadable. */
|
|
1055
573
|
grade: PackVerdict | null;
|
|
1056
|
-
/**
|
|
1057
|
-
* Records withheld by the pack's declaration (5.4.5): in scope for their
|
|
1058
|
-
* signature and commitment only. Never counted as verified records.
|
|
1059
|
-
*/
|
|
574
|
+
/** Records withheld by the declaration (§5.4.5). Never counted as verified. */
|
|
1060
575
|
recordsWithheld: number;
|
|
1061
|
-
/**
|
|
1062
|
-
* `sanning.declaration/v1`, `sanning.declaration/v2`, or `null` where the
|
|
1063
|
-
* pack carries no declaration, one this reader does not know, or a body this
|
|
1064
|
-
* reader does not read.
|
|
1065
|
-
*/
|
|
576
|
+
/** The declaration version, or `null` where absent, unknown or in an unread body. */
|
|
1066
577
|
declarationVersion: string | null;
|
|
1067
578
|
}
|
|
1068
579
|
|
|
1069
580
|
export interface CaseReading {
|
|
1070
581
|
verdict: CaseVerdict;
|
|
582
|
+
/** `changed` or `nonconforming` where `verdict` is `failed`. */
|
|
583
|
+
reasonKind?: ReasonKind;
|
|
1071
584
|
/**
|
|
1072
|
-
* Every
|
|
1073
|
-
* order
|
|
1074
|
-
* order and bundle order.
|
|
1075
|
-
*
|
|
1076
|
-
* **An empty array is the only shape a reader may read as clean**, and it is
|
|
1077
|
-
* reachable only together with `verdict === "verified"`.
|
|
585
|
+
* Every problem behind the verdict, each mapped to one claim and one state, in
|
|
586
|
+
* exception order. Empty exactly where `verdict` is `verified`.
|
|
1078
587
|
*/
|
|
1079
|
-
|
|
588
|
+
problems: Problem[];
|
|
1080
589
|
/**
|
|
1081
|
-
*
|
|
1082
|
-
* bundle order
|
|
590
|
+
* Every exception: pack-level ones in pack order, then record-level ones in
|
|
591
|
+
* pack and bundle order. Empty is the only clean shape, and only with
|
|
592
|
+
* `verdict === "verified"`.
|
|
1083
593
|
*/
|
|
594
|
+
exceptions: CaseException[];
|
|
595
|
+
/** One entry per record of every pack, read or not. */
|
|
1084
596
|
records: CaseRecordReading[];
|
|
1085
597
|
/**
|
|
1086
|
-
* Every `[contributor, verdict]` the
|
|
1087
|
-
*
|
|
1088
|
-
* kernels compare the derivation and not only its answer.
|
|
598
|
+
* Every `[contributor, verdict]` the verdict was computed from, so the
|
|
599
|
+
* verdict is auditable rather than asserted.
|
|
1089
600
|
*/
|
|
1090
601
|
contributions: [string, CaseVerdict][];
|
|
1091
|
-
/**
|
|
602
|
+
/** Hand-off records in the case. Zero is its own exception. */
|
|
1092
603
|
handoffRecords: number;
|
|
1093
604
|
/**
|
|
1094
|
-
* Whether any witness
|
|
1095
|
-
*
|
|
1096
|
-
*
|
|
1097
|
-
*
|
|
1098
|
-
*
|
|
1099
|
-
* is ABSENT, so the honest omission is named and the unverifiable assertion
|
|
1100
|
-
* is not: a holder that writes any `anchor_ref` at all reads `verified`,
|
|
1101
|
-
* because this reader makes no network call. That is the standing lens
|
|
1102
|
-
* inverted, and the answer is not to refuse the assertion, which no offline
|
|
1103
|
-
* reader can grade. It is to say what was not checked, so nobody reads a
|
|
1104
|
-
* clean case as "a witness held". Re-fetching on chain stays the caller's
|
|
1105
|
-
* job, exactly as `evidence-bundle.md` 5.5's does-not-claim list says.
|
|
1106
|
-
*
|
|
1107
|
-
* Derived from each pack's own `EvidenceBundleResult.onChainChecked` rather
|
|
1108
|
-
* than written as a constant, so it stays true of what was actually run:
|
|
1109
|
-
* this reader passes no gateways, and it is the ALL of an empty list of
|
|
1110
|
-
* checked packs, which is false.
|
|
605
|
+
* Whether any witness was re-fetched on chain. Always false here: this reader
|
|
606
|
+
* makes no network call, so a pack with any `anchor_ref` reads `verified`
|
|
607
|
+
* without the witness being checked. Stated so nobody reads a clean case as
|
|
608
|
+
* "a witness held"; re-fetching is the caller's (evidence-bundle §5.5).
|
|
609
|
+
* Derived from each pack's `onChainChecked`, not written as a constant.
|
|
1111
610
|
*/
|
|
1112
611
|
onChainChecked: boolean;
|
|
1113
|
-
/**
|
|
612
|
+
/** Notes, in pack order (§5.5.3). No contribution. */
|
|
1114
613
|
notes: CaseNote[];
|
|
1115
|
-
/** One reading per pack, in the order
|
|
614
|
+
/** One reading per pack, in the order given. */
|
|
1116
615
|
packs: CasePackReading[];
|
|
1117
616
|
}
|
|
1118
617
|
|
|
1119
|
-
// ---- The reading ------------------------------------------------------------
|
|
1120
|
-
|
|
1121
618
|
const bodyTypeOf = (bundle: unknown): unknown =>
|
|
1122
619
|
typeof bundle === "object" && bundle !== null
|
|
1123
620
|
? (bundle as Record<string, unknown>)["body_type"]
|
|
1124
621
|
: undefined;
|
|
1125
622
|
|
|
1126
623
|
/**
|
|
1127
|
-
* The key that
|
|
1128
|
-
*
|
|
1129
|
-
*
|
|
1130
|
-
* The wrapper signature covers `body_hash` and `body` carries the
|
|
1131
|
-
* `declaration` (`evidence-bundle.md` 5.1, 5.4.1), so this key is the party
|
|
1132
|
-
* whose signed account of itself this pack is. Read from the ENVELOPE of the
|
|
1133
|
-
* pack and never from `issuer`: `issuer` is a label and DL-60 keeps it out of
|
|
1134
|
-
* every reading, while `public_key` is the value the signature binds.
|
|
1135
|
-
*
|
|
1136
|
-
* It means something only for a pack whose wrapper HOLDS, which is why
|
|
1137
|
-
* `readCase` counts it only after its own `verifyEvidenceBundle` call over the
|
|
1138
|
-
* pack returned `signatureOk` and `bodyHashOk`.
|
|
624
|
+
* The key that signed the pack wrapper, and so the pack's declaration
|
|
625
|
+
* (evidence-bundle §5.1, §5.4.1). Read from `public_key`, never `issuer`, which
|
|
626
|
+
* is a label. Meaningful only once the wrapper is verified to hold.
|
|
1139
627
|
*/
|
|
1140
628
|
const holderKeyOf = (bundle: unknown): unknown =>
|
|
1141
629
|
typeof bundle === "object" && bundle !== null
|
|
@@ -1143,41 +631,32 @@ const holderKeyOf = (bundle: unknown): unknown =>
|
|
|
1143
631
|
: undefined;
|
|
1144
632
|
|
|
1145
633
|
/**
|
|
1146
|
-
* Provenance
|
|
1147
|
-
*
|
|
1148
|
-
*
|
|
634
|
+
* Provenance only: the wrapper signature verified and `body_hash` matches the
|
|
635
|
+
* body (evidence-bundle §5.5.1). Under a version this verifier does not know, the
|
|
636
|
+
* signature alone ties the pack to its key, and the body is not read.
|
|
1149
637
|
*/
|
|
1150
638
|
const wrapperHolds = (result: EvidenceBundleResult): boolean =>
|
|
1151
|
-
result.signatureOk && result.bodyHashOk;
|
|
639
|
+
result.signatureOk && (result.bodyHashOk || result.versionUnknown === true);
|
|
1152
640
|
|
|
1153
641
|
/**
|
|
1154
|
-
*
|
|
1155
|
-
*
|
|
1156
|
-
* event's own id.
|
|
1157
|
-
*
|
|
1158
|
-
* The event errors are here as well as on the records' own exceptions on
|
|
1159
|
-
* purpose. A pack whose only defect is one record reads `failed` with no
|
|
1160
|
-
* bundle-level error at all, and an exception that named the verdict and no
|
|
1161
|
-
* reason would send a reader looking for a defect in the pack as a whole.
|
|
1162
|
-
*/
|
|
1163
|
-
/**
|
|
1164
|
-
* The reasons a reading carries only because of the side map, in the order the
|
|
1165
|
-
* verifier wrote them: what the content-dependent kinds are about, and nothing
|
|
1166
|
-
* a signature covers.
|
|
642
|
+
* Reasons present only with the side map: what the content-dependent kinds are
|
|
643
|
+
* about, and nothing a signature covers.
|
|
1167
644
|
*/
|
|
1168
645
|
const onlyIn = (withMap: readonly string[], withoutMap: readonly string[]): string[] =>
|
|
1169
646
|
withMap.filter((reason) => !withoutMap.includes(reason));
|
|
1170
647
|
|
|
1171
|
-
/**
|
|
1172
|
-
* Whether a side-content map holds any key. Python's `if pack.content` is
|
|
1173
|
-
* the twin: `None` and an empty map both mean the two verify runs are one run.
|
|
1174
|
-
*/
|
|
648
|
+
/** `null`, `undefined` and an empty map all mean the two verify runs are one. */
|
|
1175
649
|
function sideMapHasKeys(content: CasePack["content"]): boolean {
|
|
1176
650
|
if (content === null || content === undefined) return false;
|
|
1177
651
|
if (content instanceof Map) return content.size > 0;
|
|
1178
652
|
return Object.keys(content as object).length > 0;
|
|
1179
653
|
}
|
|
1180
654
|
|
|
655
|
+
/**
|
|
656
|
+
* The verifier's reasons for a pack: bundle, then stamps, checkpoints and
|
|
657
|
+
* events (prefixed with the event id). Event errors are included because a pack
|
|
658
|
+
* whose only defect is one record has no bundle-level error.
|
|
659
|
+
*/
|
|
1181
660
|
const bundleReasons = (result: EvidenceBundleResult): string[] => [
|
|
1182
661
|
...result.errors,
|
|
1183
662
|
...(result.stamps ?? []).flatMap((stamp) => stamp.errors),
|
|
@@ -1186,9 +665,8 @@ const bundleReasons = (result: EvidenceBundleResult): string[] => [
|
|
|
1186
665
|
];
|
|
1187
666
|
|
|
1188
667
|
/**
|
|
1189
|
-
* The `spec_version` of
|
|
1190
|
-
*
|
|
1191
|
-
* `index` is the same record in both.
|
|
668
|
+
* The `spec_version` of `body.events[index]`'s envelope. `packRecords` and the
|
|
669
|
+
* verifier walk events in the same order.
|
|
1192
670
|
*/
|
|
1193
671
|
function recordSpecVersion(bundle: unknown, index: number): unknown {
|
|
1194
672
|
const at = (v: unknown, k: string | number): unknown =>
|
|
@@ -1198,8 +676,8 @@ function recordSpecVersion(bundle: unknown, index: number): unknown {
|
|
|
1198
676
|
}
|
|
1199
677
|
|
|
1200
678
|
/**
|
|
1201
|
-
* `specVersion` is
|
|
1202
|
-
*
|
|
679
|
+
* Whether `specVersion` is quarantined, on the token boundary
|
|
680
|
+
* `specVersionSupported` uses.
|
|
1203
681
|
*/
|
|
1204
682
|
function underQuarantine(specVersion: unknown): boolean {
|
|
1205
683
|
if (typeof specVersion !== "string") return false;
|
|
@@ -1212,46 +690,32 @@ function underQuarantine(specVersion: unknown): boolean {
|
|
|
1212
690
|
}
|
|
1213
691
|
|
|
1214
692
|
/**
|
|
1215
|
-
* The case verdict, from the contributions alone
|
|
693
|
+
* The case verdict, from the contributions alone: `failed` where any contribution is,
|
|
694
|
+
* else `incomplete` where any is, else `verified`. A case reads its worst pack, and every
|
|
695
|
+
* exception is still named beside it.
|
|
1216
696
|
*
|
|
1217
|
-
*
|
|
1218
|
-
*
|
|
1219
|
-
* whatever else contributes, and every other exception stays named beside it.
|
|
1220
|
-
* Otherwise one outcome is that outcome, and more than one is `mixed`
|
|
1221
|
-
* (anchor#198, criterion 4), so `mixed` never holds a `failed`. `malformed`
|
|
1222
|
-
* does NOT keep the headline: it says a record cannot be read, not that a
|
|
1223
|
-
* check ran and did not pass. `mixed` is in the 3 vocabulary and coins no word.
|
|
1224
|
-
*
|
|
1225
|
-
* THE INVARIANT, stated so a mutation test has something to kill: `verified` is
|
|
1226
|
-
* reachable ONLY when `exceptions` is empty. Every exception kind carries a
|
|
1227
|
-
* non-`verified` contribution (`EXCEPTION_VERDICT`), so this falls out of the
|
|
1228
|
-
* table rather than being enforced by a second rule that could drift from it.
|
|
1229
|
-
* The assertion is here anyway, because that is the property every one of the
|
|
1230
|
-
* seventeen findings on this program violated.
|
|
697
|
+
* Invariant: `verified` only when `exceptions` is empty. It follows from
|
|
698
|
+
* `EXCEPTION_VERDICT` and is asserted anyway.
|
|
1231
699
|
*/
|
|
1232
700
|
function verdictOf(
|
|
1233
701
|
exceptions: readonly CaseException[],
|
|
1234
702
|
contributions: readonly [string, CaseVerdict][],
|
|
1235
703
|
holdsPacks = true,
|
|
1236
704
|
): CaseVerdict {
|
|
1237
|
-
//
|
|
1238
|
-
//
|
|
1239
|
-
// nothing in it was an exception. Every exception contributes, so no
|
|
1240
|
-
// contribution means no exception. A reading with no packs always carries
|
|
1241
|
-
// `case_holds_no_packs`, and is defended here anyway: an empty aggregate over
|
|
1242
|
-
// nothing reading as `verified` is the defect class itself.
|
|
705
|
+
// No contribution means every pack graded `verified` and no exception. A
|
|
706
|
+
// reading with no packs is `incomplete`, never `verified` over nothing.
|
|
1243
707
|
if (contributions.length === 0) {
|
|
1244
708
|
if (exceptions.length > 0) {
|
|
1245
709
|
throw new Error(`exceptions with no contribution: ${exceptions.map((e) => e.kind).join(", ")}`);
|
|
1246
710
|
}
|
|
1247
|
-
return holdsPacks ? "verified" : "
|
|
711
|
+
return holdsPacks ? "verified" : "incomplete";
|
|
1248
712
|
}
|
|
1249
713
|
const outcomes = new Set(contributions.map(([, v]) => v));
|
|
1250
714
|
const verdict: CaseVerdict = outcomes.has("failed")
|
|
1251
715
|
? "failed"
|
|
1252
|
-
: outcomes.
|
|
1253
|
-
?
|
|
1254
|
-
: "
|
|
716
|
+
: outcomes.has("incomplete")
|
|
717
|
+
? "incomplete"
|
|
718
|
+
: "verified";
|
|
1255
719
|
if (verdict === "verified" && exceptions.length > 0) {
|
|
1256
720
|
throw new Error(
|
|
1257
721
|
`verified over a non-empty exception list: ${exceptions.map((e) => e.kind).join(", ")}`,
|
|
@@ -1260,38 +724,224 @@ function verdictOf(
|
|
|
1260
724
|
return verdict;
|
|
1261
725
|
}
|
|
1262
726
|
|
|
727
|
+
function at(pack: number | null, record: number | null = null): ProblemItem[] {
|
|
728
|
+
return [[pack, record]];
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// The envelope faults a record's pack already names with their own claim, so the case does
|
|
732
|
+
// not also call the envelope one whose signature fails.
|
|
733
|
+
const ENVELOPE_FAULTS_OF_THE_PACK: readonly string[] = [
|
|
734
|
+
"payload_unbound",
|
|
735
|
+
"not_an_envelope",
|
|
736
|
+
"key_unreadable",
|
|
737
|
+
"spec_version_unknown",
|
|
738
|
+
"nonconforming",
|
|
739
|
+
];
|
|
740
|
+
|
|
741
|
+
/** A pack's own problems (read at position 0) at its position in the case. */
|
|
742
|
+
function moved(problems: readonly Problem[], position: number): Problem[] {
|
|
743
|
+
return problems.map((p) => withItems(p, p.items.map(([, ...rest]) => [position, ...rest] as ProblemItem)));
|
|
744
|
+
}
|
|
745
|
+
|
|
1263
746
|
/**
|
|
1264
|
-
*
|
|
1265
|
-
*
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
747
|
+
* Every exception, mapped to its claim and state, in exception order. A pack grade
|
|
748
|
+
* carries its own problems; a pack grade with none (a defence) reads as its status.
|
|
749
|
+
*/
|
|
750
|
+
export function caseProblems(exceptions: readonly CaseException[]): Problem[] {
|
|
751
|
+
const out: Problem[] = [];
|
|
752
|
+
for (const e of exceptions) {
|
|
753
|
+
let items: ProblemItem[];
|
|
754
|
+
if (e.recordIndex !== undefined) items = at(e.packIndex ?? null, e.recordIndex);
|
|
755
|
+
else if (e.packIndex !== undefined) items = at(e.packIndex);
|
|
756
|
+
else items = at(null);
|
|
757
|
+
if (e.kind === PACK_GRADE_NOT_VERIFIED) {
|
|
758
|
+
if (e.problems !== undefined && e.problems.length > 0) out.push(...e.problems);
|
|
759
|
+
else if (e.status === "failed") out.push(problem("bundle_does_not_verify", items));
|
|
760
|
+
else out.push(problem("bundle_not_checked", items));
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
if (e.kind === PAIR_FAILED && e.failedChecks !== undefined && e.failedChecks.length > 0) {
|
|
764
|
+
// One problem per check that failed, so each is said in its own words.
|
|
765
|
+
for (const c of e.failedChecks) out.push(problem(e.kind, items, { rule: c }));
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
out.push(
|
|
769
|
+
problem(e.kind, items, {
|
|
770
|
+
key: e.kind.startsWith("counterpart_absent") ? (e.counterpartyKey ?? null) : null,
|
|
771
|
+
}),
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
return out;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* The reading, with its problems and the verdict they give, which must be the verdict
|
|
779
|
+
* the contributions give: two derivations of one answer, held to agree.
|
|
780
|
+
*/
|
|
781
|
+
function withProblems(reading: Omit<CaseReading, "problems" | "reasonKind">): CaseReading {
|
|
782
|
+
const problems = caseProblems(reading.exceptions);
|
|
783
|
+
const outcome = claimsVerdictOf(claimsOf(problems));
|
|
784
|
+
if (outcome.status !== reading.verdict) {
|
|
785
|
+
throw new Error(
|
|
786
|
+
`two derivations of one verdict disagree: ${outcome.status} and ${reading.verdict}`,
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
return {
|
|
790
|
+
...reading,
|
|
791
|
+
...(outcome.reason_kind === undefined ? {} : { reasonKind: outcome.reason_kind }),
|
|
792
|
+
problems,
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/** One pack's grade in a case, and what it rests on. */
|
|
797
|
+
interface PackGrading {
|
|
798
|
+
grade: PackVerdict;
|
|
799
|
+
problems: Problem[];
|
|
800
|
+
causes: PackGradeCauseKind[];
|
|
801
|
+
check: DeclarationCheck | null;
|
|
802
|
+
full: PackGrade | null;
|
|
803
|
+
records: ReturnType<typeof packRecords>;
|
|
804
|
+
ungraded: boolean;
|
|
805
|
+
knownBody: boolean;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* The pack's grade, read as `verify --logs` reads it: no disproof rests on bytes nobody
|
|
810
|
+
* signed.
|
|
811
|
+
*/
|
|
812
|
+
async function gradeInCase(
|
|
813
|
+
pack: CasePack,
|
|
814
|
+
verified: EvidenceBundleResult,
|
|
815
|
+
signed: EvidenceBundleResult,
|
|
816
|
+
position: number,
|
|
817
|
+
): Promise<PackGrading> {
|
|
818
|
+
const bodyType = bodyTypeOf(pack.bundle);
|
|
819
|
+
const records = packRecords(pack.bundle, pack.content);
|
|
820
|
+
// Per-record gradings are joined to `records` by index. A pack whose
|
|
821
|
+
// `body_hash` does not cover its body reads `failed` with no events, so a
|
|
822
|
+
// count mismatch is graded, never indexed into.
|
|
823
|
+
const ungraded =
|
|
824
|
+
verified.events.length !== records.length || signed.events.length !== records.length;
|
|
825
|
+
const knownBody = (CASE_BODY_TYPES as readonly unknown[]).includes(bodyType);
|
|
826
|
+
|
|
827
|
+
// The pack's grade propagates: the case never reads above its weakest pack.
|
|
828
|
+
let check: DeclarationCheck | null = null;
|
|
829
|
+
let full: PackGrade | null = null;
|
|
830
|
+
let causes: PackGradeCauseKind[] = [];
|
|
831
|
+
let grade: PackVerdict;
|
|
832
|
+
let packProblems: Problem[] = bundleProblems(signed, position);
|
|
833
|
+
if (signed.unreadable === true) {
|
|
834
|
+
// A body that cannot be read under a wrapper whose time breaks its format fails.
|
|
835
|
+
grade = signed.status === "failed" ? "failed" : "incomplete";
|
|
836
|
+
} else if (!knownBody) {
|
|
837
|
+
grade = signed.status as PackVerdict;
|
|
838
|
+
} else if (ungraded) {
|
|
839
|
+
grade = signed.status === "verified" ? "incomplete" : (signed.status as PackVerdict);
|
|
840
|
+
if (signed.status === "verified") packProblems = [problem("bundle_not_checked", at(position))];
|
|
841
|
+
} else {
|
|
842
|
+
// The declaration's chain rule reads the same signed chain links for both
|
|
843
|
+
// checks (evidence-bundle §5.4.2).
|
|
844
|
+
const chainLinks = await readChainLinks(pack.bundle);
|
|
845
|
+
check = checkDeclaration(pack.bundle, pack.content, chainLinks);
|
|
846
|
+
const completeness = completenessNotes(pack.bundle);
|
|
847
|
+
full = gradePack(verified, accountDisclosure(pack.bundle, pack.content ?? {}), completeness, check);
|
|
848
|
+
const bareGrade = gradePack(
|
|
849
|
+
signed,
|
|
850
|
+
accountDisclosure(pack.bundle, {}),
|
|
851
|
+
completeness,
|
|
852
|
+
checkDeclaration(pack.bundle, null, chainLinks),
|
|
853
|
+
);
|
|
854
|
+
// No disproof here rests on unsigned bytes: each disproof the pack read with the
|
|
855
|
+
// bytes beside it makes and the signed reading does not is not proven, as `verify`
|
|
856
|
+
// reads it, and everything else is the reading with those bytes, so a pack's logs/
|
|
857
|
+
// count in `case` as they do in `verify --logs`.
|
|
858
|
+
packProblems = moved(restOnSigned(full.problems, bareGrade.problems), position);
|
|
859
|
+
grade = claimsVerdictOf(claimsOf(packProblems)).status;
|
|
860
|
+
causes = full.causes.map((c) => c.kind);
|
|
861
|
+
}
|
|
862
|
+
return { grade, problems: packProblems, causes, check, full, records, ungraded, knownBody };
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* Each pack that reads as a pack, verified twice: with the side map, and with
|
|
867
|
+
* it empty. What fails only in the first rests on bytes nobody
|
|
868
|
+
* signed. One run where there is no map. Offline, no gateways.
|
|
869
|
+
*/
|
|
870
|
+
async function verifyPacks(
|
|
871
|
+
packs: readonly CasePack[],
|
|
872
|
+
): Promise<Map<number, [EvidenceBundleResult, EvidenceBundleResult]>> {
|
|
873
|
+
const runs = new Map<number, [EvidenceBundleResult, EvidenceBundleResult]>();
|
|
874
|
+
for (const [position, pack] of packs.entries()) {
|
|
875
|
+
if (typeof pack.unreadable === "string") continue;
|
|
876
|
+
const verified = await verifyEvidenceBundle(pack.bundle, {
|
|
877
|
+
content: (pack.content ?? undefined) as
|
|
878
|
+
| Record<string, Uint8Array | string>
|
|
879
|
+
| ReadonlyMap<string, Uint8Array | string>
|
|
880
|
+
| undefined,
|
|
881
|
+
});
|
|
882
|
+
const signed = sideMapHasKeys(pack.content)
|
|
883
|
+
? await verifyEvidenceBundle(pack.bundle, {})
|
|
884
|
+
: verified;
|
|
885
|
+
runs.set(position, [verified, signed]);
|
|
886
|
+
}
|
|
887
|
+
return runs;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* Read the agent records of every pack as one pile and put each reading on
|
|
892
|
+
* both runs of its pack, in place of the reading over its own pack alone
|
|
893
|
+
* (agent-events.md section 10 step 7): pairing, chains and closing lists span packs.
|
|
894
|
+
* A record takes part where its pack's wrapper holds, its pack's records are
|
|
895
|
+
* all graded, and its envelope verified with its record bytes bound to it.
|
|
896
|
+
*/
|
|
897
|
+
async function readAgentRecordsAcross(
|
|
898
|
+
packs: readonly CasePack[],
|
|
899
|
+
runs: Map<number, [EvidenceBundleResult, EvidenceBundleResult]>,
|
|
900
|
+
): Promise<void> {
|
|
901
|
+
const items: AgentHeldRecord[] = [];
|
|
902
|
+
const where: [number, number][] = [];
|
|
903
|
+
for (const [position, [verified, signed]] of runs) {
|
|
904
|
+
if (!wrapperHolds(verified)) continue;
|
|
905
|
+
const records = packRecords(packs[position]!.bundle, packs[position]!.content);
|
|
906
|
+
if (verified.events.length !== records.length || signed.events.length !== records.length) continue;
|
|
907
|
+
for (const [index, record] of records.entries()) {
|
|
908
|
+
const graded = verified.events[index]!;
|
|
909
|
+
graded.agent = null;
|
|
910
|
+
signed.events[index]!.agent = null;
|
|
911
|
+
if (graded.envelopeOk && graded.payloadBindingOk === true && record.record !== null) {
|
|
912
|
+
items.push({ event_id: record.eventId, public_key: record.publicKey, record: record.record });
|
|
913
|
+
where.push([position, index]);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
const readings = await readAgentRecords(items);
|
|
918
|
+
for (const [i, [position, index]] of where.entries()) {
|
|
919
|
+
const [verified, signed] = runs.get(position)!;
|
|
920
|
+
verified.events[index]!.agent = readings[i]!;
|
|
921
|
+
signed.events[index]!.agent = readings[i]!;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* Reads a case: packs in, one verdict and its exceptions out.
|
|
927
|
+
*
|
|
928
|
+
* 1. Each pack is read into records (`packRecords`); an unknown body or no
|
|
929
|
+
* records is named.
|
|
930
|
+
* 2. Each pack is graded against its own declaration (`checkDeclaration`).
|
|
931
|
+
* 3. Every record goes into one pile, unpartitioned, read by `readHandoffs`.
|
|
932
|
+
* 4. Each reading becomes an exception or a clean contribution. The
|
|
933
|
+
* counterpart kinds are told apart by whether a pack signed by the
|
|
934
|
+
* counterparty's key is in the case, read from every wrapper before any exit.
|
|
935
|
+
*
|
|
936
|
+
* Before step 1 the agent records of every pack are read as one pile
|
|
937
|
+
* (agent-events.md section 10), and each pack is graded with those readings
|
|
938
|
+
* (evidence-bundle.md section 5.4.5, section 5.5.1).
|
|
1289
939
|
*/
|
|
1290
940
|
export async function readCase(
|
|
1291
941
|
packs: readonly CasePack[],
|
|
1292
942
|
): Promise<CaseReading> {
|
|
1293
|
-
// A `null` slot is held for each pack
|
|
1294
|
-
//
|
|
943
|
+
// A `null` slot is held for each undeclared pack until the pile says whether
|
|
944
|
+
// the reading holds a hand-off record.
|
|
1295
945
|
const exceptions: (CaseException | null)[] = [];
|
|
1296
946
|
const contributions: ([string, CaseVerdict] | null)[] = [];
|
|
1297
947
|
|
|
@@ -1300,14 +950,10 @@ export async function readCase(
|
|
|
1300
950
|
fields: Omit<CaseException, "kind"> = {},
|
|
1301
951
|
) => {
|
|
1302
952
|
exceptions.push({ kind, ...fields });
|
|
1303
|
-
// `null` is PACK_GRADE_NOT_VERIFIED: the pack's case grade, never
|
|
1304
|
-
// verified
|
|
1305
|
-
// case verdict never issues (5.5.1 step 1 v1.20).
|
|
953
|
+
// `null` is PACK_GRADE_NOT_VERIFIED: contribute the pack's case grade, never
|
|
954
|
+
// verified (section 5.5.1 step 1).
|
|
1306
955
|
let contribution = EXCEPTION_VERDICT[kind];
|
|
1307
|
-
if (contribution === null)
|
|
1308
|
-
const status = fields.status as string;
|
|
1309
|
-
contribution = GRADE_CONTRIBUTES.has(status) ? (status as CaseVerdict) : "incomplete";
|
|
1310
|
-
}
|
|
956
|
+
if (contribution === null) contribution = fields.status as CaseVerdict;
|
|
1311
957
|
contributions.push([fields.eventId ?? fields.packRef ?? "case", contribution]);
|
|
1312
958
|
};
|
|
1313
959
|
|
|
@@ -1315,7 +961,7 @@ export async function readCase(
|
|
|
1315
961
|
note(CASE_HOLDS_NO_PACKS);
|
|
1316
962
|
const named = exceptions as CaseException[];
|
|
1317
963
|
const contributed = contributions as [string, CaseVerdict][];
|
|
1318
|
-
return {
|
|
964
|
+
return withProblems({
|
|
1319
965
|
verdict: verdictOf(named, contributed, false),
|
|
1320
966
|
exceptions: named,
|
|
1321
967
|
records: [],
|
|
@@ -1324,29 +970,22 @@ export async function readCase(
|
|
|
1324
970
|
onChainChecked: false,
|
|
1325
971
|
notes: [],
|
|
1326
972
|
packs: [],
|
|
1327
|
-
};
|
|
973
|
+
});
|
|
1328
974
|
}
|
|
1329
975
|
|
|
1330
|
-
//
|
|
1331
|
-
// Whether every pack's witness was re-fetched on chain, ALL over the packs
|
|
1332
|
-
// this reader verified. It passes no gateways, so this stays false and the
|
|
1333
|
-
// reading says so (see `CaseReading.onChainChecked`).
|
|
976
|
+
// Steps 1 and 2, pack by pack. No gateways, so `witnessChecked` stays false.
|
|
1334
977
|
const witnessChecked: boolean[] = [];
|
|
1335
978
|
const held: HeldRecord[] = [];
|
|
1336
|
-
//
|
|
1337
|
-
//
|
|
1338
|
-
// human to read; a join rebuilt by splitting one is a join on a value a
|
|
1339
|
-
// holder can put a separator in.
|
|
979
|
+
// Pack and `event_id` per pile member, kept beside the pile rather than
|
|
980
|
+
// parsed from a reference string a holder could put a separator in.
|
|
1340
981
|
const provenance: {
|
|
1341
982
|
position: number;
|
|
1342
983
|
packRef: string;
|
|
1343
984
|
eventId: string;
|
|
1344
985
|
recordIndex: number;
|
|
1345
986
|
}[] = [];
|
|
1346
|
-
// Every record of every pack, in
|
|
1347
|
-
//
|
|
1348
|
-
// kind of the exception that says why. Nothing is listed only where the pack
|
|
1349
|
-
// body carries no `events` array, and that pack's own exception says why.
|
|
987
|
+
// Every record of every pack, in order: its pile position, or the exception
|
|
988
|
+
// kind that kept it out.
|
|
1350
989
|
const slots: {
|
|
1351
990
|
position: number;
|
|
1352
991
|
packRef: string;
|
|
@@ -1365,71 +1004,75 @@ export async function readCase(
|
|
|
1365
1004
|
slots.push({ position, packRef, recordIndex, eventId: record.eventId, heldAt: null, reason }),
|
|
1366
1005
|
);
|
|
1367
1006
|
};
|
|
1368
|
-
//
|
|
1369
|
-
//
|
|
1370
|
-
// `event_id` alone, so under an id the pack carries twice every record with
|
|
1371
|
-
// that id is named: failing closed, rather than guessing which of the two the
|
|
1372
|
-
// check meant.
|
|
1007
|
+
// Declaration-check findings per pack position, then `eventId`. A duplicated
|
|
1008
|
+
// id names every record with it: fail closed rather than guess.
|
|
1373
1009
|
const declarationFindings = new Map<number, Map<string, string[]>>();
|
|
1374
|
-
// Keyed by
|
|
1375
|
-
//
|
|
1376
|
-
// the same one: keyed by ref, the second pack's claims silently replaced the
|
|
1377
|
-
// first's and records from one pack were graded against another pack's
|
|
1378
|
-
// declaration. Found by a constructed archive rather than by reading, which
|
|
1379
|
-
// is the only way this class of defect is ever found.
|
|
1010
|
+
// Keyed by pack position, never `ref`: two packs can carry one ref, and a
|
|
1011
|
+
// record must never be graded against another pack's declaration.
|
|
1380
1012
|
const claimsByPack = new Map<number, DeclarationClaims>();
|
|
1381
1013
|
|
|
1382
|
-
// Every key that
|
|
1383
|
-
//
|
|
1384
|
-
//
|
|
1385
|
-
//
|
|
1386
|
-
// case.
|
|
1387
|
-
//
|
|
1388
|
-
// The record-key set was wrong twice over, and both times in the direction
|
|
1389
|
-
// that matters. It let the ACCUSER manufacture the damning line: one pack,
|
|
1390
|
-
// plus one unrelated record the counterparty signed at some other time, read
|
|
1391
|
-
// "the counterparty's OWN records ARE here" when the counterparty had handed
|
|
1392
|
-
// over nothing (F4). And it let the COUNTERPARTY choose the innocent line: it
|
|
1393
|
-
// was filled inside the record loop, which the `body_type` and no-records
|
|
1394
|
-
// exits `continue` past, so a counterparty that wrapped its own pack in a body
|
|
1395
|
-
// this reader does not read took its keys out of the set (F3), the third
|
|
1396
|
-
// instance of a route already closed once for withheld `event_bytes`.
|
|
1397
|
-
//
|
|
1398
|
-
// A wrapper key closes both. The accuser cannot forge one without the
|
|
1399
|
-
// counterparty's private key, and it is read HERE, first, before any exit
|
|
1400
|
-
// below, from the pack envelope and never from the body, so no body type, no
|
|
1401
|
-
// disclosure choice and no record content decides whether it is read.
|
|
1014
|
+
// Every key that signed a pack wrapper (and so its declaration): the one
|
|
1015
|
+
// input that separates the counterpart kinds. Wrapper keys, not record keys:
|
|
1016
|
+
// an accuser cannot forge one, and it is read before any exit, from the
|
|
1017
|
+
// envelope, so no body type or disclosure choice decides whether it counts.
|
|
1402
1018
|
const holderKeys = new Set<string>();
|
|
1403
|
-
// One reading per pack, in the order the reader was given them (5.5.4 v1.20).
|
|
1404
1019
|
const packReadings: CasePackReading[] = [];
|
|
1405
|
-
// The NOTEs (5.5.3 v1.20), with the pack position they are about.
|
|
1406
1020
|
const caseNotes: { position: number; note: CaseNote }[] = [];
|
|
1407
|
-
//
|
|
1408
|
-
// by the pack's POSITION for the reason `claimsByPack` is.
|
|
1021
|
+
// Keyed by pack position, as `claimsByPack` is.
|
|
1409
1022
|
const rolesByPack = new Map<number, Map<string, DeclarationRole>>();
|
|
1410
|
-
//
|
|
1023
|
+
// Undeclared packs' held slots, settled after the pile is read.
|
|
1411
1024
|
const undeclaredSlots: {
|
|
1412
1025
|
exceptionAt: number;
|
|
1413
1026
|
contributionAt: number;
|
|
1414
1027
|
position: number;
|
|
1415
1028
|
ref: string;
|
|
1416
1029
|
}[] = [];
|
|
1417
|
-
//
|
|
1418
|
-
//
|
|
1419
|
-
// case is signed by the named key" is not established, so the innocent
|
|
1420
|
-
// one-sided kind is not available (COUNTERPART_ABSENT_SIGNER_UNKNOWN).
|
|
1030
|
+
// Packs not tied to a key (unreadable key, or wrapper does not hold). While
|
|
1031
|
+
// non-zero, the innocent `COUNTERPART_ABSENT` is not established.
|
|
1421
1032
|
let unattributed = 0;
|
|
1422
1033
|
|
|
1034
|
+
const runs = await verifyPacks(packs);
|
|
1035
|
+
await readAgentRecordsAcross(packs, runs);
|
|
1036
|
+
// The agent reading of each record that went into the pile, by pack and record position.
|
|
1037
|
+
const agentReadings = new Map<string, AgentRecordReading | null>();
|
|
1038
|
+
|
|
1423
1039
|
for (let position = 0; position < packs.length; position += 1) {
|
|
1424
1040
|
const pack = packs[position];
|
|
1041
|
+
if (typeof pack.unreadable === "string") {
|
|
1042
|
+
note(PACK_UNREADABLE, { packRef: pack.ref, packIndex: position, reasons: [pack.unreadable] });
|
|
1043
|
+
unattributed += 1;
|
|
1044
|
+
packReadings.push({
|
|
1045
|
+
index: position,
|
|
1046
|
+
ref: pack.ref,
|
|
1047
|
+
signingKey: null,
|
|
1048
|
+
grade: null,
|
|
1049
|
+
recordsWithheld: 0,
|
|
1050
|
+
declarationVersion: null,
|
|
1051
|
+
});
|
|
1052
|
+
continue;
|
|
1053
|
+
}
|
|
1425
1054
|
const holder = normalizedSigningKey(holderKeyOf(pack.bundle));
|
|
1426
1055
|
if (holder === null) {
|
|
1427
|
-
//
|
|
1428
|
-
//
|
|
1429
|
-
// BEFORE verification, because the two bundle verifiers disagree on some
|
|
1430
|
-
// of these spellings (proof#135) and the reading must not.
|
|
1056
|
+
// Signer unknown, not unsigned. Checked before verification so verifier
|
|
1057
|
+
// differences over key spelling cannot change the reading.
|
|
1431
1058
|
note(PACK_KEY_UNREADABLE, { packRef: pack.ref, packIndex: position });
|
|
1432
1059
|
unattributed += 1;
|
|
1060
|
+
// `signed` is not proven on that claim only: a pack that can be read is still graded,
|
|
1061
|
+
// so a forged record inside it, or a `body_hash` that does not hold, fails it. Its
|
|
1062
|
+
// records do not join the pile, since no key ties them to a party.
|
|
1063
|
+
const [keyless, keylessSigned] = runs.get(position)!;
|
|
1064
|
+
const keylessGrade = await gradeInCase(pack, keyless, keylessSigned, position);
|
|
1065
|
+
const inside = keylessGrade.problems.filter((p) => p.kind !== PACK_KEY_UNREADABLE);
|
|
1066
|
+
if (inside.length > 0 && keylessSigned.unreadable !== true) {
|
|
1067
|
+
note(PACK_GRADE_NOT_VERIFIED, {
|
|
1068
|
+
packRef: pack.ref,
|
|
1069
|
+
packIndex: position,
|
|
1070
|
+
status: claimsVerdictOf(claimsOf(inside)).status as CaseVerdict,
|
|
1071
|
+
causes: keylessGrade.causes,
|
|
1072
|
+
reasons: bundleReasons(keylessSigned),
|
|
1073
|
+
problems: inside,
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1433
1076
|
unread(position, pack.ref, listedRecords(pack.bundle, pack.content), PACK_KEY_UNREADABLE);
|
|
1434
1077
|
packReadings.push({
|
|
1435
1078
|
index: position,
|
|
@@ -1442,27 +1085,10 @@ export async function readCase(
|
|
|
1442
1085
|
continue;
|
|
1443
1086
|
}
|
|
1444
1087
|
|
|
1445
|
-
|
|
1446
|
-
// caller: every key this module compares is an envelope `public_key`, and
|
|
1447
|
-
// "unforgeable" is a property of a VERIFIED envelope. Offline, no
|
|
1448
|
-
// gateways: this reader makes no network call. This kernel's verifier
|
|
1449
|
-
// reads envelopes under exactly CASE_RECORD_PROFILES (pinned by a test).
|
|
1450
|
-
const verified = await verifyEvidenceBundle(pack.bundle, {
|
|
1451
|
-
content: (pack.content ?? undefined) as
|
|
1452
|
-
| Record<string, Uint8Array | string>
|
|
1453
|
-
| ReadonlyMap<string, Uint8Array | string>
|
|
1454
|
-
| undefined,
|
|
1455
|
-
});
|
|
1088
|
+
const [verified, signed] = runs.get(position)!;
|
|
1456
1089
|
witnessChecked.push(verified.onChainChecked);
|
|
1457
|
-
// The same pack with the side map EMPTY (proof#159): what fails here is
|
|
1458
|
-
// what a signature covers, and what fails only above rests on bytes nobody
|
|
1459
|
-
// signed. Skipped where there is no map, because the two runs are then the
|
|
1460
|
-
// same run.
|
|
1461
|
-
const signed = sideMapHasKeys(pack.content)
|
|
1462
|
-
? await verifyEvidenceBundle(pack.bundle, {})
|
|
1463
|
-
: verified;
|
|
1464
1090
|
|
|
1465
|
-
//
|
|
1091
|
+
// Whose pack it is: the wrapper, and only the wrapper.
|
|
1466
1092
|
if (!wrapperHolds(verified)) {
|
|
1467
1093
|
note(PACK_DOES_NOT_VERIFY, { packRef: pack.ref, packIndex: position });
|
|
1468
1094
|
unattributed += 1;
|
|
@@ -1480,77 +1106,20 @@ export async function readCase(
|
|
|
1480
1106
|
holderKeys.add(holder);
|
|
1481
1107
|
|
|
1482
1108
|
if (coSignaturesPresent(pack.bundle)) {
|
|
1483
|
-
// A
|
|
1484
|
-
// co_signatures verifies nothing and sits outside its signed scope, so
|
|
1485
|
-
// the case reading names it rather than moving anything on its account.
|
|
1109
|
+
// A note, no contribution: `co_signatures` verifies nothing.
|
|
1486
1110
|
caseNotes.push({
|
|
1487
1111
|
position,
|
|
1488
1112
|
note: { kind: CO_SIGNATURES_PRESENT, packRef: pack.ref, packIndex: position },
|
|
1489
1113
|
});
|
|
1490
1114
|
}
|
|
1491
1115
|
|
|
1492
|
-
const
|
|
1493
|
-
const records =
|
|
1494
|
-
|
|
1495
|
-
// verifier that graded a different number of records than the pack holds
|
|
1496
|
-
// leaves this loop with no grading to read. That is not hypothetical: a
|
|
1497
|
-
// pack whose `body_hash` does not cover its body reads `failed` with
|
|
1498
|
-
// `events` EMPTY while `packRecords` reads every record in it, and only
|
|
1499
|
-
// `wrapperHolds` above stands between that pack and this loop. A reader
|
|
1500
|
-
// must never crash on a pack a party can build, so the gap is graded rather
|
|
1501
|
-
// than indexed into, and the pack's records are dropped exactly as a
|
|
1502
|
-
// `malformed` pack's are (gate 4 on proof#140, finding 3).
|
|
1503
|
-
const ungraded =
|
|
1504
|
-
verified.events.length !== records.length || signed.events.length !== records.length;
|
|
1505
|
-
const knownBody = (CASE_BODY_TYPES as readonly unknown[]).includes(bodyType);
|
|
1506
|
-
|
|
1507
|
-
// WHAT IS WRONG INSIDE IT, from here on. First the pack's own GRADE, which
|
|
1508
|
-
// propagates: the case never reads above its weakest pack (5.5.1 step 1
|
|
1509
|
-
// v1.20, A23; up to v1.19 it was the bundle status).
|
|
1510
|
-
let check: DeclarationCheck | null = null;
|
|
1511
|
-
let full: PackGrade | null = null;
|
|
1512
|
-
let causes: PackGradeCauseKind[] = [];
|
|
1513
|
-
let grade: PackVerdict;
|
|
1514
|
-
if (signed.status === "malformed") {
|
|
1515
|
-
grade = "malformed";
|
|
1516
|
-
} else if (!knownBody) {
|
|
1517
|
-
grade = signed.status as PackVerdict;
|
|
1518
|
-
} else if (ungraded) {
|
|
1519
|
-
grade = signed.status === "verified" ? "malformed" : (signed.status as PackVerdict);
|
|
1520
|
-
} else {
|
|
1521
|
-
// Rule 11 reads the chain links of the same pack, and the reader reads
|
|
1522
|
-
// only the signed body, so the bare check below takes the same reading
|
|
1523
|
-
// (evidence-bundle.md 5.4.2 v1.36, A45).
|
|
1524
|
-
const chainLinks = await readChainLinks(pack.bundle);
|
|
1525
|
-
check = checkDeclaration(pack.bundle, pack.content, chainLinks);
|
|
1526
|
-
const completeness = completenessNotes(pack.bundle);
|
|
1527
|
-
full = gradePack(verified, accountDisclosure(pack.bundle, pack.content ?? {}), completeness, check);
|
|
1528
|
-
const bareGrade = gradePack(
|
|
1529
|
-
signed,
|
|
1530
|
-
accountDisclosure(pack.bundle, {}),
|
|
1531
|
-
completeness,
|
|
1532
|
-
checkDeclaration(pack.bundle, null, chainLinks),
|
|
1533
|
-
);
|
|
1534
|
-
// NO `failed` HERE RESTS ON BYTES NOBODY SIGNED. The signed grade decides
|
|
1535
|
-
// where it is `failed` or `malformed`; a failure only the side map
|
|
1536
|
-
// produced is `incomplete`; otherwise the full grade.
|
|
1537
|
-
if (bareGrade.verdict === "failed" || bareGrade.verdict === "malformed") {
|
|
1538
|
-
grade = bareGrade.verdict;
|
|
1539
|
-
causes = bareGrade.causes.map((c) => c.kind);
|
|
1540
|
-
} else if (full.verdict === "failed" || full.verdict === "malformed") {
|
|
1541
|
-
grade = "incomplete";
|
|
1542
|
-
causes = full.causes.map((c) => c.kind);
|
|
1543
|
-
} else {
|
|
1544
|
-
grade = full.verdict;
|
|
1545
|
-
causes = full.causes.map((c) => c.kind);
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1116
|
+
const graded = await gradeInCase(pack, verified, signed, position);
|
|
1117
|
+
const { grade, check, full, causes, records, ungraded, knownBody } = graded;
|
|
1118
|
+
const packProblems = graded.problems;
|
|
1548
1119
|
if (grade !== "verified") {
|
|
1549
1120
|
const reasons = bundleReasons(signed);
|
|
1550
|
-
if (knownBody && ungraded && signed.
|
|
1551
|
-
//
|
|
1552
|
-
// about the bundle, and this pack's records are dropped for a reason
|
|
1553
|
-
// of this reader's.
|
|
1121
|
+
if (knownBody && ungraded && signed.unreadable !== true) {
|
|
1122
|
+
// The verifier's reasons are about the bundle; this one is ours.
|
|
1554
1123
|
reasons.push(
|
|
1555
1124
|
`this verifier graded ${verified.events.length} of the ${records.length} ` +
|
|
1556
1125
|
"record(s) the pack holds, so none of them is read",
|
|
@@ -1562,6 +1131,7 @@ export async function readCase(
|
|
|
1562
1131
|
status: grade as CaseVerdict,
|
|
1563
1132
|
causes,
|
|
1564
1133
|
reasons,
|
|
1134
|
+
problems: packProblems,
|
|
1565
1135
|
});
|
|
1566
1136
|
}
|
|
1567
1137
|
if (verified.status === "failed" && signed.status !== "failed") {
|
|
@@ -1581,9 +1151,8 @@ export async function readCase(
|
|
|
1581
1151
|
declarationVersion: knownBody ? declarationVersionOf(pack.bundle) : null,
|
|
1582
1152
|
});
|
|
1583
1153
|
if (recordsWithheld > 0) {
|
|
1584
|
-
// A
|
|
1585
|
-
//
|
|
1586
|
-
// them, so a clean verdict never hides them.
|
|
1154
|
+
// A note, no contribution: the holder signed that this content does not
|
|
1155
|
+
// travel, and the scope sentence counts it.
|
|
1587
1156
|
caseNotes.push({
|
|
1588
1157
|
position,
|
|
1589
1158
|
note: {
|
|
@@ -1594,29 +1163,21 @@ export async function readCase(
|
|
|
1594
1163
|
},
|
|
1595
1164
|
});
|
|
1596
1165
|
}
|
|
1597
|
-
// The
|
|
1598
|
-
|
|
1599
|
-
if (signed.status === "malformed") {
|
|
1166
|
+
// The verifier graded no record; named above.
|
|
1167
|
+
if (signed.unreadable === true) {
|
|
1600
1168
|
unread(position, pack.ref, listedRecords(pack.bundle, pack.content), PACK_GRADE_NOT_VERIFIED);
|
|
1601
1169
|
continue;
|
|
1602
1170
|
}
|
|
1603
1171
|
|
|
1604
1172
|
if (!knownBody) {
|
|
1605
|
-
//
|
|
1606
|
-
//
|
|
1607
|
-
// which packs this reader grades, so it is a decision of its own and not
|
|
1608
|
-
// a repair (evidence-bundle.md 5.5.1 step 1 says so too). What this exit
|
|
1609
|
-
// costs is that pack's RECORDS, and it is named every time. It no longer
|
|
1610
|
-
// costs the pack's KEY: that was read above.
|
|
1173
|
+
// Not unwrapped (evidence-bundle §5.5.1). Costs the pack's records, never
|
|
1174
|
+
// its key, which was read above.
|
|
1611
1175
|
note(PACK_BODY_TYPE_UNKNOWN, { packRef: pack.ref, packIndex: position });
|
|
1612
|
-
// Listed,
|
|
1613
|
-
// are listed from its inline source bundle, so a reading never says "0
|
|
1614
|
-
// not read" of a pack that holds records (A43).
|
|
1176
|
+
// Listed, not read, so a pack holding records never reads "0 not read".
|
|
1615
1177
|
unread(position, pack.ref, listedRecords(pack.bundle, pack.content), PACK_BODY_TYPE_UNKNOWN);
|
|
1616
1178
|
continue;
|
|
1617
1179
|
}
|
|
1618
|
-
// Graded above
|
|
1619
|
-
// none of its records are read.
|
|
1180
|
+
// Graded above; none of its records are read.
|
|
1620
1181
|
if (ungraded) {
|
|
1621
1182
|
unread(position, pack.ref, records, PACK_GRADE_NOT_VERIFIED);
|
|
1622
1183
|
continue;
|
|
@@ -1638,10 +1199,8 @@ export async function readCase(
|
|
|
1638
1199
|
}
|
|
1639
1200
|
declarationFindings.set(position, namedByCheck);
|
|
1640
1201
|
if (checked.contentFindings.length > 0) {
|
|
1641
|
-
//
|
|
1642
|
-
//
|
|
1643
|
-
// one is `incomplete` because whoever assembled the case chose the bytes
|
|
1644
|
-
// it turns on (proof#147).
|
|
1202
|
+
// Beside the deviation exception, never instead: they concern different
|
|
1203
|
+
// records. `incomplete` because the case assembler chose the bytes.
|
|
1645
1204
|
note(PACK_DECLARATION_CONTENT_DEPENDENT, {
|
|
1646
1205
|
packRef: pack.ref,
|
|
1647
1206
|
packIndex: position,
|
|
@@ -1649,34 +1208,29 @@ export async function readCase(
|
|
|
1649
1208
|
});
|
|
1650
1209
|
}
|
|
1651
1210
|
if (checked.verdict === "failed") {
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1211
|
+
// A declaration that does not parse deviates from nothing: the pack grade names it as
|
|
1212
|
+
// a declaration that breaks its own format.
|
|
1213
|
+
if (
|
|
1214
|
+
checked.deviations.length === 0 ||
|
|
1215
|
+
checked.deviations.some((d) => d.kind !== "declaration_malformed")
|
|
1216
|
+
) {
|
|
1217
|
+
note(PACK_DECLARATION_DEVIATES, {
|
|
1218
|
+
packRef: pack.ref,
|
|
1219
|
+
packIndex: position,
|
|
1220
|
+
count: checked.deviations.length,
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1657
1223
|
} else if (checked.contentFindings.length > 0) {
|
|
1658
|
-
//
|
|
1659
|
-
// claim": the claim was read, and part of it rests on bytes nobody
|
|
1660
|
-
// signed.
|
|
1224
|
+
// Named above: the claim was read, and part of it rests on unsigned bytes.
|
|
1661
1225
|
} else if (checked.verdict === "verified") {
|
|
1662
|
-
// A pack
|
|
1663
|
-
// a `verified`
|
|
1664
|
-
// readings, and criterion 4's "mixed" is about mixed HAND-OFF outcomes: a
|
|
1665
|
-
// pack-level clean contribution would put `verified` into the set on
|
|
1666
|
-
// every case, so one broken pair beside two honest declarations would
|
|
1667
|
-
// read `mixed` and the failure would be the quieter half of a two-word
|
|
1668
|
-
// answer. A pack-level PROBLEM still contributes, through its exception.
|
|
1226
|
+
// A clean pack contributes nothing, so the case verdict reflects hand-off
|
|
1227
|
+
// outcomes: a `verified` per pack would turn one broken pair into `mixed`.
|
|
1669
1228
|
} else if (checked.present) {
|
|
1670
|
-
//
|
|
1671
|
-
// cannot read", which `gradePack` already carries to `incomplete`. The
|
|
1672
|
-
// same posture here, for the same reason: the field has no accept-set and
|
|
1673
|
-
// the holder writes it.
|
|
1229
|
+
// A claim this reader cannot read (§5.4.3); the holder writes the field.
|
|
1674
1230
|
note(PACK_DECLARATION_UNREADABLE, { packRef: pack.ref, packIndex: position });
|
|
1675
1231
|
} else {
|
|
1676
|
-
//
|
|
1677
|
-
//
|
|
1678
|
-
// NOTE where it holds none. That is not known until the pile is read, so
|
|
1679
|
-
// the slot is held HERE, in pack order, and settled below.
|
|
1232
|
+
// `partial` if the reading holds a hand-off record, else a note (§5.5.1).
|
|
1233
|
+
// Not known until the pile is read, so hold the slot.
|
|
1680
1234
|
undeclaredSlots.push({
|
|
1681
1235
|
exceptionAt: exceptions.length,
|
|
1682
1236
|
contributionAt: contributions.length,
|
|
@@ -1687,25 +1241,29 @@ export async function readCase(
|
|
|
1687
1241
|
contributions.push(null);
|
|
1688
1242
|
}
|
|
1689
1243
|
|
|
1690
|
-
// Each record on its
|
|
1691
|
-
// `body.events` in order and emit one entry per element, so the index is
|
|
1692
|
-
// the join, exactly as `held`, `provenance` and `readings` below.
|
|
1244
|
+
// Each record on its own envelope, joined by index.
|
|
1693
1245
|
for (let index = 0; index < records.length; index += 1) {
|
|
1694
1246
|
const record = records[index];
|
|
1695
1247
|
const graded = verified.events[index];
|
|
1696
1248
|
const bare = signed.events[index];
|
|
1697
|
-
//
|
|
1698
|
-
// unique in the reading whatever the reader called the pack and whatever
|
|
1699
|
-
// the holder wrote as `event_id` (A43).
|
|
1249
|
+
// Pack and record by position: unique whatever `ref` or `event_id` say.
|
|
1700
1250
|
const at = {
|
|
1701
1251
|
packRef: pack.ref,
|
|
1702
1252
|
packIndex: position,
|
|
1703
1253
|
eventId: record.eventId,
|
|
1704
1254
|
recordIndex: index,
|
|
1705
1255
|
};
|
|
1706
|
-
// Not read: listed with the kind of the exception that says why.
|
|
1256
|
+
// Not read: listed with the kind of the exception that says why. Signed bytes that
|
|
1257
|
+
// bind and are not JSON, and an envelope that cannot be read, names a version this
|
|
1258
|
+
// verifier does not know or breaks its format under a signature that holds, are their
|
|
1259
|
+
// pack's own problems, so no exception is noted here.
|
|
1707
1260
|
const skip = (kind: CaseExceptionKind, fields: Omit<CaseException, "kind"> = {}) => {
|
|
1708
|
-
|
|
1261
|
+
if (
|
|
1262
|
+
!(kind === RECORD_BYTES_UNREADABLE && bare.recordNotJson === true) &&
|
|
1263
|
+
!(kind === RECORD_ENVELOPE_DOES_NOT_VERIFY && ENVELOPE_FAULTS_OF_THE_PACK.includes(graded.envelopeFault ?? ""))
|
|
1264
|
+
) {
|
|
1265
|
+
note(kind, { ...at, ...fields });
|
|
1266
|
+
}
|
|
1709
1267
|
slots.push({ position, packRef: pack.ref, recordIndex: index, eventId: record.eventId, heldAt: null, reason: kind });
|
|
1710
1268
|
};
|
|
1711
1269
|
if (underQuarantine(recordSpecVersion(pack.bundle, index))) {
|
|
@@ -1724,9 +1282,8 @@ export async function readCase(
|
|
|
1724
1282
|
skip(RECORD_BYTES_UNREADABLE);
|
|
1725
1283
|
continue;
|
|
1726
1284
|
}
|
|
1727
|
-
// Named, and the record stays in the pile
|
|
1728
|
-
//
|
|
1729
|
-
// with the side map is `incomplete` (proof#159).
|
|
1285
|
+
// Named, and the record stays in the pile. A mismatch in the signed body
|
|
1286
|
+
// is `failed`; one only with the side map is `incomplete`.
|
|
1730
1287
|
if (bare.contentOk === false) {
|
|
1731
1288
|
note(RECORD_CONTENT_MISMATCH, {
|
|
1732
1289
|
...at,
|
|
@@ -1738,11 +1295,13 @@ export async function readCase(
|
|
|
1738
1295
|
reasons: onlyIn(graded.errors, bare.errors),
|
|
1739
1296
|
});
|
|
1740
1297
|
}
|
|
1741
|
-
// Every other check on the
|
|
1742
|
-
// add a failure here nor
|
|
1298
|
+
// Every other check on the signed reading, so side content can neither
|
|
1299
|
+
// add a failure here nor remove one. A stamp the pack does not hold, and signed
|
|
1300
|
+
// bytes that are not JSON, are their pack's own problems (not proven, and a
|
|
1301
|
+
// producer's error), never a record that does not match its pack.
|
|
1743
1302
|
if (
|
|
1744
|
-
!(bare.
|
|
1745
|
-
(!bare.ok && bare.contentOk !== false)
|
|
1303
|
+
!(bare.stampAbsent === true || bare.recordNotJson === true) &&
|
|
1304
|
+
(!(bare.inclusionOk && bare.checkpointBound) || (!bare.ok && bare.contentOk !== false))
|
|
1746
1305
|
) {
|
|
1747
1306
|
note(RECORD_DOES_NOT_VERIFY_IN_ITS_PACK, {
|
|
1748
1307
|
...at,
|
|
@@ -1757,19 +1316,19 @@ export async function readCase(
|
|
|
1757
1316
|
heldAt: held.length,
|
|
1758
1317
|
reason: null,
|
|
1759
1318
|
});
|
|
1319
|
+
const agent = graded.agent;
|
|
1320
|
+
agentReadings.set(`${position}:${index}`, agent);
|
|
1760
1321
|
held.push({
|
|
1761
1322
|
publicKey: record.publicKey,
|
|
1762
1323
|
record: record.record,
|
|
1763
1324
|
ref: `${pack.ref}/${record.eventId}`,
|
|
1325
|
+
agentNonconforming: agent !== null && agent.verdict === "failed",
|
|
1764
1326
|
});
|
|
1765
1327
|
provenance.push({ position, packRef: pack.ref, eventId: record.eventId, recordIndex: index });
|
|
1766
1328
|
}
|
|
1767
1329
|
}
|
|
1768
1330
|
|
|
1769
|
-
// Step 3
|
|
1770
|
-
// the line DL-60 constrains: there is nothing here that reads a tenant, an
|
|
1771
|
-
// issuer or an organization, so a pair across two organizations' packs is
|
|
1772
|
-
// read exactly as a pair inside one.
|
|
1331
|
+
// Step 3: one pile, no partition by tenant, issuer or organization.
|
|
1773
1332
|
const readings: HandoffReading[] = readHandoffs(held);
|
|
1774
1333
|
|
|
1775
1334
|
// Step 4.
|
|
@@ -1777,23 +1336,12 @@ export async function readCase(
|
|
|
1777
1336
|
for (let index = 0; index < readings.length; index += 1) {
|
|
1778
1337
|
const reading = readings[index];
|
|
1779
1338
|
const { position, packRef, eventId, recordIndex } = provenance[index];
|
|
1780
|
-
// The record this reading is about, by position (A43).
|
|
1781
1339
|
const at = { packRef, packIndex: position, eventId, recordIndex };
|
|
1782
|
-
//
|
|
1783
|
-
//
|
|
1784
|
-
//
|
|
1785
|
-
//
|
|
1786
|
-
//
|
|
1787
|
-
//
|
|
1788
|
-
// ABOVE the two `continue`s below, deliberately. This check asks what the
|
|
1789
|
-
// PACK said about the record, which is independent of whether 4.1 governs
|
|
1790
|
-
// the record and of whether the record is well formed. Behind the
|
|
1791
|
-
// `not_a_handoff_record` exit it was a check the audited party switched off
|
|
1792
|
-
// by respelling two fields, and behind the `malformed` exit it was one a
|
|
1793
|
-
// party switched off by breaking its own record.
|
|
1794
|
-
//
|
|
1795
|
-
// v1's ALONE (5.5.1 v1.20, A21). A v2 pack's claim check is rule 9, which
|
|
1796
|
-
// the declaration check already ran on the signed body.
|
|
1340
|
+
// Read from `declarationClaims`, never a second parse. Only a readable
|
|
1341
|
+
// declaration makes a record unclaimed; otherwise the pack's own exception
|
|
1342
|
+
// covers it. Runs above both `continue`s below, so neither respelling a
|
|
1343
|
+
// record out of §4.1 nor breaking it switches the check off. v1 only: a v2
|
|
1344
|
+
// pack's claim check already ran inside the declaration check.
|
|
1797
1345
|
const claims = claimsByPack.get(position);
|
|
1798
1346
|
if (
|
|
1799
1347
|
claims !== undefined &&
|
|
@@ -1805,33 +1353,32 @@ export async function readCase(
|
|
|
1805
1353
|
}
|
|
1806
1354
|
|
|
1807
1355
|
if (reading.findings.includes(NOT_A_HANDOFF_FINDING)) {
|
|
1808
|
-
//
|
|
1809
|
-
// is not a hand-off exception, and it is still in `records` and still
|
|
1810
|
-
// subject to the claim check above.
|
|
1356
|
+
// Not a hand-off exception; still listed and still claim-checked above.
|
|
1811
1357
|
continue;
|
|
1812
1358
|
}
|
|
1813
1359
|
handoffRecords += 1;
|
|
1814
1360
|
|
|
1815
|
-
if (reading.
|
|
1816
|
-
//
|
|
1817
|
-
//
|
|
1818
|
-
//
|
|
1819
|
-
// CANDIDATE, so it is a different kind, it does not `continue`, and the
|
|
1820
|
-
// counterpart, ambiguity and `failedChecks` reporting below all keep
|
|
1821
|
-
// running over it.
|
|
1361
|
+
if (reading.findings.includes("malformed_record")) {
|
|
1362
|
+
// `recordIsMalformed` answers about this record. A malformed aggregate
|
|
1363
|
+
// over a well-formed record is a malformed candidate, and reporting
|
|
1364
|
+
// continues for it.
|
|
1822
1365
|
if (recordIsMalformed(held[index])) {
|
|
1823
1366
|
note(RECORD_MALFORMED, at);
|
|
1824
1367
|
continue;
|
|
1825
1368
|
}
|
|
1826
1369
|
note(COUNTERPART_MALFORMED, at);
|
|
1370
|
+
} else if (reading.findings.includes("nonconforming_record")) {
|
|
1371
|
+
// The same question for the other defect: this record's own, or a candidate's.
|
|
1372
|
+
if (recordIsNonconforming(held[index])) {
|
|
1373
|
+
note(RECORD_NONCONFORMING, at);
|
|
1374
|
+
continue;
|
|
1375
|
+
}
|
|
1376
|
+
note(COUNTERPART_NONCONFORMING, at);
|
|
1827
1377
|
}
|
|
1828
1378
|
|
|
1829
1379
|
if (reading.findings.includes(COUNTERPART_ABSENT)) {
|
|
1830
|
-
// `held`, `provenance` and `readings`
|
|
1831
|
-
//
|
|
1832
|
-
// Deliberately not a lookup by reference string: a reference is for a
|
|
1833
|
-
// person to read, and a join rebuilt by splitting one is a join on a
|
|
1834
|
-
// value a holder can put a separator inside.
|
|
1380
|
+
// `held`, `provenance` and `readings` share one order, never filtered, so
|
|
1381
|
+
// the index is the join.
|
|
1835
1382
|
const subject = held[index];
|
|
1836
1383
|
const event = (subject.record as Record<string, unknown>)["event"];
|
|
1837
1384
|
const named = normalizedSigningKey(
|
|
@@ -1839,20 +1386,11 @@ export async function readCase(
|
|
|
1839
1386
|
? (event as Record<string, unknown>)["counterparty_public_key"]
|
|
1840
1387
|
: undefined,
|
|
1841
1388
|
);
|
|
1842
|
-
//
|
|
1843
|
-
//
|
|
1844
|
-
//
|
|
1845
|
-
//
|
|
1846
|
-
//
|
|
1847
|
-
//
|
|
1848
|
-
// EXCEPT where 4.1 set every candidate aside (v1.17, A20). There the
|
|
1849
|
-
// absence is a finding about the candidates the reader DID read, each
|
|
1850
|
-
// paired `verified` with another record the subject's own signer made,
|
|
1851
|
-
// and not a finding about any pack. 4.1 and evidence-bundle A16 name the
|
|
1852
|
-
// neutral kind for it, and the damning one would put the audited party's
|
|
1853
|
-
// own unfaithful record on the counterparty's pack as a statement
|
|
1854
|
-
// against it. `count` is how many candidates were set aside, which is
|
|
1855
|
-
// also what tells the rendered sentence which absence this is.
|
|
1389
|
+
// The damning kind needs a verified pack signed by the named key; with
|
|
1390
|
+
// any unattributed pack, the reading says it could not tell. Where every
|
|
1391
|
+
// candidate was set aside (each paired with another record by the
|
|
1392
|
+
// subject's own signer), the absence is about those candidates, not any
|
|
1393
|
+
// pack, so the neutral kind carries `count`.
|
|
1856
1394
|
const graded = reading.candidateReadings;
|
|
1857
1395
|
const setAside =
|
|
1858
1396
|
graded.length > 0 && graded.every((c) => c.accountedFor !== null)
|
|
@@ -1881,13 +1419,9 @@ export async function readCase(
|
|
|
1881
1419
|
count: reading.candidateReadings.filter((c) => c.accountedFor === null).length,
|
|
1882
1420
|
});
|
|
1883
1421
|
}
|
|
1884
|
-
//
|
|
1885
|
-
//
|
|
1886
|
-
//
|
|
1887
|
-
// verdict instead threw that union away: `malformed` outranks `failed`, so
|
|
1888
|
-
// one junk candidate took the branch below and a proven `message_hash`
|
|
1889
|
-
// failure disappeared from the case. A named failed check is a named failed
|
|
1890
|
-
// check whatever the aggregate word is.
|
|
1422
|
+
// On `failedChecks`, not the verdict word: `malformed` outranks `failed`,
|
|
1423
|
+
// and §4.1 keeps the union of failed checks so a junk candidate cannot hide
|
|
1424
|
+
// a proven failure.
|
|
1891
1425
|
if (reading.failedChecks.length > 0) {
|
|
1892
1426
|
note(PAIR_FAILED, {
|
|
1893
1427
|
...at,
|
|
@@ -1898,11 +1432,8 @@ export async function readCase(
|
|
|
1898
1432
|
}
|
|
1899
1433
|
}
|
|
1900
1434
|
|
|
1901
|
-
//
|
|
1902
|
-
//
|
|
1903
|
-
// contributing `partial`, in the slot it was given in pack order where the
|
|
1904
|
-
// reading holds a hand-off record; a NOTE with no contribution where it holds
|
|
1905
|
-
// none.
|
|
1435
|
+
// Undeclared packs: the `pack_undeclared` exception, contributing `incomplete`,
|
|
1436
|
+
// where the reading holds a hand-off record; otherwise a note (§5.5.3).
|
|
1906
1437
|
for (const slot of undeclaredSlots) {
|
|
1907
1438
|
if (handoffRecords > 0) {
|
|
1908
1439
|
exceptions[slot.exceptionAt] = {
|
|
@@ -1920,18 +1451,14 @@ export async function readCase(
|
|
|
1920
1451
|
}
|
|
1921
1452
|
const named = exceptions.filter((e): e is CaseException => e !== null);
|
|
1922
1453
|
const contributed = contributions.filter((c): c is [string, CaseVerdict] => c !== null);
|
|
1923
|
-
// Pack order, stable.
|
|
1924
|
-
// declaration withholds nothing by one.
|
|
1454
|
+
// Pack order, stable.
|
|
1925
1455
|
const notes = caseNotes
|
|
1926
1456
|
.map((n, i) => ({ ...n, i }))
|
|
1927
1457
|
.sort((a, b) => a.position - b.position || a.i - b.i)
|
|
1928
1458
|
.map((n) => n.note);
|
|
1929
1459
|
|
|
1930
|
-
//
|
|
1931
|
-
//
|
|
1932
|
-
// record position, or its pack's declaration check names it (by deviation
|
|
1933
|
-
// or by content finding), and `read_clean` where neither does; every other
|
|
1934
|
-
// record is `not_read`, with the kind of the exception that says why.
|
|
1460
|
+
// Read records are `read_flagged` when an exception or declaration finding
|
|
1461
|
+
// names them, else `read_clean`; the rest are `not_read`.
|
|
1935
1462
|
const flagged = new Set(
|
|
1936
1463
|
named
|
|
1937
1464
|
.filter((e) => e.packIndex !== undefined && e.recordIndex !== undefined)
|
|
@@ -1956,6 +1483,7 @@ export async function readCase(
|
|
|
1956
1483
|
failedChecks: [],
|
|
1957
1484
|
counterparts: [],
|
|
1958
1485
|
role: null,
|
|
1486
|
+
agent: null,
|
|
1959
1487
|
};
|
|
1960
1488
|
}
|
|
1961
1489
|
const reading = readings[slot.heldAt];
|
|
@@ -1972,10 +1500,11 @@ export async function readCase(
|
|
|
1972
1500
|
failedChecks: [...reading.failedChecks],
|
|
1973
1501
|
counterparts: [...reading.candidates],
|
|
1974
1502
|
role: rolesByPack.get(slot.position)?.get(slot.eventId) ?? null,
|
|
1503
|
+
agent: agentReadings.get(`${slot.position}:${slot.recordIndex}`) ?? null,
|
|
1975
1504
|
};
|
|
1976
1505
|
});
|
|
1977
1506
|
|
|
1978
|
-
return {
|
|
1507
|
+
return withProblems({
|
|
1979
1508
|
verdict: verdictOf(named, contributed),
|
|
1980
1509
|
exceptions: named,
|
|
1981
1510
|
records,
|
|
@@ -1984,15 +1513,12 @@ export async function readCase(
|
|
|
1984
1513
|
onChainChecked: witnessChecked.length > 0 && witnessChecked.every((c) => c),
|
|
1985
1514
|
notes,
|
|
1986
1515
|
packs: packReadings,
|
|
1987
|
-
};
|
|
1516
|
+
});
|
|
1988
1517
|
}
|
|
1989
1518
|
|
|
1990
1519
|
/**
|
|
1991
|
-
* The records a pack
|
|
1992
|
-
*
|
|
1993
|
-
* `sanning.evidence.export/v2` to its inline source bundle, and otherwise
|
|
1994
|
-
* `body.events` as `packRecords` reads it. Listing is not reading: every record
|
|
1995
|
-
* listed here is `not_read`.
|
|
1520
|
+
* The records a pack lists when this reader does not read them one by one,
|
|
1521
|
+
* unwrapping an export to its source bundle. Every one is `not_read`.
|
|
1996
1522
|
*/
|
|
1997
1523
|
function listedRecords(bundle: unknown, content: CasePack["content"]): PackRecord[] {
|
|
1998
1524
|
const events = resolveTraceEvents(bundle);
|
|
@@ -2000,10 +1526,7 @@ function listedRecords(bundle: unknown, content: CasePack["content"]): PackRecor
|
|
|
2000
1526
|
return packRecords({ body: { events } }, content);
|
|
2001
1527
|
}
|
|
2002
1528
|
|
|
2003
|
-
/**
|
|
2004
|
-
* `body.declaration.declaration_version` where it is one this reader knows,
|
|
2005
|
-
* and `null` otherwise.
|
|
2006
|
-
*/
|
|
1529
|
+
/** The pack's known `declaration_version`, or `null`. */
|
|
2007
1530
|
function declarationVersionOf(bundle: unknown): string | null {
|
|
2008
1531
|
const at = (v: unknown, k: string): unknown =>
|
|
2009
1532
|
typeof v === "object" && v !== null && !Array.isArray(v)
|
|
@@ -2019,18 +1542,13 @@ function declarationVersionOf(bundle: unknown): string | null {
|
|
|
2019
1542
|
: null;
|
|
2020
1543
|
}
|
|
2021
1544
|
|
|
2022
|
-
// ---- Presentation (optional) ------------------------------------------------
|
|
2023
|
-
|
|
2024
|
-
/**
|
|
2025
|
-
* The house wording for one exception.
|
|
2026
|
-
*
|
|
2027
|
-
* Offered, not imposed, exactly as `renderDeviation` is: what must not vary
|
|
2028
|
-
* between surfaces is the verdict and the set of exceptions, and those come
|
|
2029
|
-
* from `readCase`. Both kernels return this same text for the same exception.
|
|
2030
|
-
*/
|
|
2031
1545
|
const because = (reasons: readonly string[] | undefined): string =>
|
|
2032
1546
|
reasons !== undefined && reasons.length > 0 ? `: ${reasons.join("; ")}` : "";
|
|
2033
1547
|
|
|
1548
|
+
/**
|
|
1549
|
+
* The house wording for one exception. Optional: the verdict and the set of
|
|
1550
|
+
* exceptions are what must not vary between surfaces.
|
|
1551
|
+
*/
|
|
2034
1552
|
export function renderException(exception: CaseException): string {
|
|
2035
1553
|
const where = `${exception.packRef}/${exception.eventId}`;
|
|
2036
1554
|
switch (exception.kind) {
|
|
@@ -2053,7 +1571,7 @@ export function renderException(exception: CaseException): string {
|
|
|
2053
1571
|
case COUNTERPART_ABSENT_FROM_COUNTERPARTY_RECORDS:
|
|
2054
1572
|
return (
|
|
2055
1573
|
`${where} has no counterpart among the records this verifier read, ` +
|
|
2056
|
-
"and a pack signed by the key it names
|
|
1574
|
+
"and a pack signed by the key it names is in this case and holds " +
|
|
2057
1575
|
"no counterpart this verifier could read"
|
|
2058
1576
|
);
|
|
2059
1577
|
case COUNTERPART_ABSENT_SIGNER_UNKNOWN:
|
|
@@ -2061,7 +1579,7 @@ export function renderException(exception: CaseException): string {
|
|
|
2061
1579
|
`${where} has no counterpart among the records this verifier read, ` +
|
|
2062
1580
|
"and a pack in this case could not be tied to a signing key, so " +
|
|
2063
1581
|
"whether a pack signed by the key it names is in this case is " +
|
|
2064
|
-
"
|
|
1582
|
+
"unknown"
|
|
2065
1583
|
);
|
|
2066
1584
|
case COUNTERPART_AMBIGUOUS:
|
|
2067
1585
|
return (
|
|
@@ -2069,65 +1587,77 @@ export function renderException(exception: CaseException): string {
|
|
|
2069
1587
|
"reading is the worst of them and no one match was chosen"
|
|
2070
1588
|
);
|
|
2071
1589
|
case PAIR_FAILED:
|
|
2072
|
-
return `${where}
|
|
1590
|
+
return `${where} failed on ${(exception.failedChecks ?? []).join(", ")}`;
|
|
2073
1591
|
case RECORD_MALFORMED:
|
|
2074
1592
|
return `${where} is not a well formed hand-off record`;
|
|
2075
1593
|
case COUNTERPART_MALFORMED:
|
|
2076
1594
|
return (
|
|
2077
1595
|
`${where} is well formed and a candidate counterpart of it is not, ` +
|
|
2078
|
-
"so what this record pairs with is
|
|
1596
|
+
"so what this record pairs with is unknown"
|
|
1597
|
+
);
|
|
1598
|
+
case RECORD_NONCONFORMING:
|
|
1599
|
+
return (
|
|
1600
|
+
`${where} is signed and intact and does not follow its vocabulary, ` +
|
|
1601
|
+
"so nothing is proven altered and it cannot be paired"
|
|
1602
|
+
);
|
|
1603
|
+
case COUNTERPART_NONCONFORMING:
|
|
1604
|
+
return (
|
|
1605
|
+
`${where} conforms and a candidate counterpart of it does not, ` +
|
|
1606
|
+
"so what this record pairs with is unknown"
|
|
2079
1607
|
);
|
|
2080
1608
|
case RECORD_UNCLAIMED:
|
|
2081
1609
|
return (
|
|
2082
1610
|
`${where} is a record its own pack declares and claims in no ` +
|
|
2083
1611
|
"hand-off pair, so whether the holder meant to hand it over is " +
|
|
2084
|
-
"
|
|
1612
|
+
"unknown"
|
|
2085
1613
|
);
|
|
2086
1614
|
case RECORD_BYTES_WITHHELD:
|
|
2087
|
-
return `${where} travels with no record bytes, so what that record
|
|
1615
|
+
return `${where} travels with no record bytes, so what that record is, is unknown`;
|
|
2088
1616
|
case RECORD_BYTES_UNREADABLE:
|
|
2089
1617
|
return `${where} travels with record bytes this verifier cannot read`;
|
|
2090
1618
|
case PACK_DECLARATION_DEVIATES:
|
|
2091
|
-
//
|
|
2092
|
-
// against the content the reader was handed, and content beside a pack is
|
|
2093
|
-
// unsigned. Whoever assembles a case can swap it, so the sentence names
|
|
2094
|
-
// what was compared rather than only the pack.
|
|
1619
|
+
// Names what was compared: the side content is unsigned and swappable.
|
|
2095
1620
|
return (
|
|
2096
1621
|
`${exception.packRef}, read together with any content handed in ` +
|
|
2097
|
-
`beside it,
|
|
1622
|
+
`beside it, deviates from its own declaration, in ` +
|
|
2098
1623
|
`${exception.count} way(s)`
|
|
2099
1624
|
);
|
|
2100
1625
|
case PACK_DECLARATION_CONTENT_DEPENDENT:
|
|
2101
|
-
//
|
|
2102
|
-
//
|
|
2103
|
-
// assembled the case chose them.
|
|
1626
|
+
// Names what could not be established and the content, never the holder:
|
|
1627
|
+
// the bytes are unsigned and the case assembler chose them.
|
|
2104
1628
|
return (
|
|
2105
1629
|
`${exception.packRef} agrees with its own declaration everywhere its ` +
|
|
2106
1630
|
`signature reaches, and in ${exception.count} way(s) it does not line ` +
|
|
2107
1631
|
"up with the unsigned content handed in beside it, which anyone could " +
|
|
2108
|
-
"have supplied, so whether it holds what it declares is
|
|
1632
|
+
"have supplied, so whether it holds what it declares is unknown"
|
|
2109
1633
|
);
|
|
2110
1634
|
case PACK_DECLARATION_UNREADABLE:
|
|
2111
1635
|
return (
|
|
2112
1636
|
`${exception.packRef} says what it holds in a declaration this ` +
|
|
2113
|
-
"verifier cannot read, so whether it is complete is
|
|
1637
|
+
"verifier cannot read, so whether it is complete is unknown"
|
|
2114
1638
|
);
|
|
2115
1639
|
case PACK_UNDECLARED:
|
|
2116
1640
|
return (
|
|
2117
1641
|
`${exception.packRef} does not say what it holds, so whether it is ` +
|
|
2118
|
-
"complete is
|
|
1642
|
+
"complete is unknown"
|
|
2119
1643
|
);
|
|
2120
1644
|
case PACK_BODY_TYPE_UNKNOWN:
|
|
2121
1645
|
return (
|
|
2122
1646
|
`${exception.packRef} is a pack body this verifier does not read, so ` +
|
|
2123
|
-
"what it holds is
|
|
1647
|
+
"what it holds is unknown"
|
|
2124
1648
|
);
|
|
2125
1649
|
case PACK_HOLDS_NO_RECORDS:
|
|
2126
1650
|
return `${exception.packRef} holds no records this verifier can read`;
|
|
1651
|
+
case PACK_UNREADABLE:
|
|
1652
|
+
return (
|
|
1653
|
+
`${exception.packRef} cannot be read: its bundle.json ` +
|
|
1654
|
+
`${(exception.reasons ?? []).join("; ")}, so who signed it is unknown and none of its ` +
|
|
1655
|
+
"records are read"
|
|
1656
|
+
);
|
|
2127
1657
|
case PACK_KEY_UNREADABLE:
|
|
2128
1658
|
return (
|
|
2129
1659
|
`${exception.packRef} names a signing key this verifier cannot ` +
|
|
2130
|
-
"read, so who signed it is
|
|
1660
|
+
"read, so who signed it is unknown and none of its records are read"
|
|
2131
1661
|
);
|
|
2132
1662
|
case PACK_DOES_NOT_VERIFY:
|
|
2133
1663
|
return (
|
|
@@ -2136,18 +1666,17 @@ export function renderException(exception: CaseException): string {
|
|
|
2136
1666
|
"names is not counted as its signer"
|
|
2137
1667
|
);
|
|
2138
1668
|
case PACK_GRADE_NOT_VERIFIED: {
|
|
2139
|
-
let head = `${exception.packRef} grades ${
|
|
1669
|
+
let head = `${exception.packRef} grades ${exception.status} as a pack`;
|
|
2140
1670
|
if (exception.causes !== undefined && exception.causes.length > 0) {
|
|
2141
1671
|
head += ` (${exception.causes.join(", ")})`;
|
|
2142
1672
|
}
|
|
2143
|
-
if (exception.status === "malformed") head += ", so none of its records are read";
|
|
2144
1673
|
return head + because(exception.reasons);
|
|
2145
1674
|
}
|
|
2146
1675
|
case PACK_BUNDLE_CONTENT_DEPENDENT:
|
|
2147
1676
|
return (
|
|
2148
|
-
`${exception.packRef} reads
|
|
1677
|
+
`${exception.packRef} reads failed as a bundle only together with ` +
|
|
2149
1678
|
"the unsigned content handed in beside it, which anyone could have " +
|
|
2150
|
-
"supplied, so whether that failure is the pack's is
|
|
1679
|
+
"supplied, so whether that failure is the pack's is unknown" +
|
|
2151
1680
|
because(exception.reasons)
|
|
2152
1681
|
);
|
|
2153
1682
|
case RECORD_ENVELOPE_DOES_NOT_VERIFY:
|
|
@@ -2172,14 +1701,14 @@ export function renderException(exception: CaseException): string {
|
|
|
2172
1701
|
return (
|
|
2173
1702
|
`${where} commits to content that the unsigned content handed in ` +
|
|
2174
1703
|
"beside its pack does not match, and anyone could have supplied " +
|
|
2175
|
-
"that content, so what it says about the record is
|
|
1704
|
+
"that content, so what it says about the record is unknown; the " +
|
|
2176
1705
|
"record is still read as a hand-off record" +
|
|
2177
1706
|
because(exception.reasons)
|
|
2178
1707
|
);
|
|
2179
1708
|
case RECORD_PROFILE_QUARANTINED:
|
|
2180
1709
|
return (
|
|
2181
1710
|
`${where} is signed under a profile this case reader does not read ` +
|
|
2182
|
-
"while its admission is undecided, so what it says is
|
|
1711
|
+
"while its admission is undecided, so what it says is unknown"
|
|
2183
1712
|
);
|
|
2184
1713
|
case CASE_HOLDS_NO_PACKS:
|
|
2185
1714
|
return "this case holds no packs";
|
|
@@ -2188,12 +1717,13 @@ export function renderException(exception: CaseException): string {
|
|
|
2188
1717
|
}
|
|
2189
1718
|
}
|
|
2190
1719
|
|
|
2191
|
-
/** The house wording for one
|
|
1720
|
+
/** The house wording for one note. */
|
|
2192
1721
|
export function renderCaseNote(note: CaseNote): string {
|
|
2193
1722
|
switch (note.kind) {
|
|
2194
1723
|
case CONTENT_WITHHELD_BY_DECLARATION:
|
|
2195
1724
|
return (
|
|
2196
|
-
`${note.packRef} withholds the content of ${note.count}
|
|
1725
|
+
`${note.packRef} withholds the content of ${note.count} ` +
|
|
1726
|
+
`${note.count === 1 ? "record" : "records"} by ` +
|
|
2197
1727
|
"its own signed declaration: their signatures and commitments were " +
|
|
2198
1728
|
"checked, their content was not"
|
|
2199
1729
|
);
|
|
@@ -2210,18 +1740,11 @@ export function renderCaseNote(note: CaseNote): string {
|
|
|
2210
1740
|
}
|
|
2211
1741
|
|
|
2212
1742
|
/**
|
|
2213
|
-
* The scope sentence
|
|
2214
|
-
*
|
|
2215
|
-
*
|
|
2216
|
-
*
|
|
2217
|
-
*
|
|
2218
|
-
* exceptions; and that the reading was made offline. Where any pack holds
|
|
2219
|
-
* records withheld by its declaration, their total; where any pack is noted
|
|
2220
|
-
* `pack_undeclared`, how many. Those two lines are part of what the headline
|
|
2221
|
-
* MEANS, and a surface MUST print them with it.
|
|
2222
|
-
*
|
|
2223
|
-
* What was not checked on the public record is no longer a clause here. It is
|
|
2224
|
-
* the witness line (`renderWitnessLine`), printed with every headline.
|
|
1743
|
+
* The scope sentence printed under the headline (evidence-bundle §5.5.6): packs,
|
|
1744
|
+
* distinct signing keys of packs whose wrapper held, hand-off records,
|
|
1745
|
+
* exceptions, and that it was read offline, plus withheld and undeclared
|
|
1746
|
+
* counts where non-zero. Those counts are part of what the headline means, and
|
|
1747
|
+
* a surface must print them with it.
|
|
2225
1748
|
*/
|
|
2226
1749
|
export function renderCaseSummary(reading: CaseReading): string {
|
|
2227
1750
|
const keys = new Set(reading.packs.filter((p) => p.grade !== null).map((p) => p.signingKey));
|
|
@@ -2242,77 +1765,51 @@ export function renderCaseSummary(reading: CaseReading): string {
|
|
|
2242
1765
|
return lines.join(" ");
|
|
2243
1766
|
}
|
|
2244
1767
|
|
|
2245
|
-
// ---- The headline and the witness line (evidence-bundle 5.5.6 v1.20) -------
|
|
2246
|
-
|
|
2247
1768
|
/**
|
|
2248
|
-
* The one display title of each of the
|
|
2249
|
-
* title, never the word, and never a title of its own
|
|
2250
|
-
* is Will's to change through the glossary (DL-71); what each MEANS is pinned
|
|
2251
|
-
* in the spec.
|
|
1769
|
+
* The one display title of each of the three section 3 verdicts. A surface prints the
|
|
1770
|
+
* title, never the word, and never a title of its own.
|
|
2252
1771
|
*/
|
|
2253
|
-
export const VERDICT_DISPLAY_TITLES: Readonly<Record<string, string>> = {
|
|
2254
|
-
verified: "Verified",
|
|
2255
|
-
partial: "Partial",
|
|
2256
|
-
pending: "Pending",
|
|
2257
|
-
incomplete: "Incomplete",
|
|
2258
|
-
failed: "Failed",
|
|
2259
|
-
malformed: "Not readable",
|
|
2260
|
-
not_found: "Not found",
|
|
2261
|
-
mixed: "Mixed",
|
|
2262
|
-
};
|
|
1772
|
+
export const VERDICT_DISPLAY_TITLES: Readonly<Record<string, string>> = { ...VERDICT_TITLES };
|
|
2263
1773
|
|
|
2264
1774
|
/**
|
|
2265
|
-
* What a reader may rely on, for each
|
|
2266
|
-
*
|
|
2267
|
-
*
|
|
2268
|
-
* witness states. `verified` is the base sentence, printed alone where the
|
|
2269
|
-
* drop holds no hand-off record; a caller reads `caseReliance` for
|
|
2270
|
-
* `verified`, never this mapping directly.
|
|
1775
|
+
* What a reader may rely on, for each verdict a case reading reaches (section 5.5.6), the
|
|
1776
|
+
* same for every drop, keyed by the verdict and, for `failed`, its reason kind. Call
|
|
1777
|
+
* `caseReliance` rather than reading `verified` here directly.
|
|
2271
1778
|
*/
|
|
2272
|
-
export const CASE_RELIANCE: Readonly<Record<CaseVerdict, string>> = {
|
|
1779
|
+
export const CASE_RELIANCE: Readonly<Record<CaseVerdict | "failed_nonconforming", string>> = {
|
|
2273
1780
|
verified:
|
|
2274
1781
|
"Every pack in this drop is genuine, unaltered, and matches its own " +
|
|
2275
1782
|
"signed declaration. Every record in it was signed by its holder's " +
|
|
2276
1783
|
"key. This was read offline, on this device. It does not say this " +
|
|
2277
1784
|
"drop holds everything that was recorded.",
|
|
2278
|
-
partial:
|
|
2279
|
-
"Nothing here was proven wrong, and a check could not be completed: a " +
|
|
2280
|
-
"pack names no witness, or a pack in a hand-off reading does not say " +
|
|
2281
|
-
"what it holds. The exceptions say which.",
|
|
2282
1785
|
incomplete:
|
|
2283
1786
|
"Nothing here was proven wrong, and part of it could not be checked: " +
|
|
2284
1787
|
"the other side of a hand-off is not in this drop, bytes were not " +
|
|
2285
|
-
"handed over
|
|
2286
|
-
"
|
|
1788
|
+
"handed over or could not be read, a pack names no witness, or a " +
|
|
1789
|
+
"claim could not be read. Do not rely on the parts that are not " +
|
|
1790
|
+
"proven. Each exception says what is missing and, where a key is " +
|
|
1791
|
+
"known, whose pack would supply it.",
|
|
2287
1792
|
failed:
|
|
2288
1793
|
"A check ran and did not pass. Do not rely on this drop until the " +
|
|
2289
1794
|
"named failure is explained. The failure is named on the pack or " +
|
|
2290
1795
|
"record it is about, and every other exception is listed beside it.",
|
|
2291
|
-
|
|
2292
|
-
"
|
|
2293
|
-
"
|
|
2294
|
-
|
|
2295
|
-
"
|
|
2296
|
-
"and some could not be checked. Read each pack's row and each " +
|
|
2297
|
-
"exception before relying on any of it.",
|
|
1796
|
+
failed_nonconforming:
|
|
1797
|
+
"A record in this drop is signed and unaltered and does not follow " +
|
|
1798
|
+
"the vocabulary it names: its producer wrote something the " +
|
|
1799
|
+
"specification does not allow. Nothing was proven altered. The " +
|
|
1800
|
+
"exceptions name the record and the rule it breaks.",
|
|
2298
1801
|
};
|
|
2299
1802
|
|
|
2300
1803
|
/**
|
|
2301
|
-
*
|
|
2302
|
-
*
|
|
2303
|
-
* and never where `handoffRecords` is zero: most drops hold one agent's
|
|
2304
|
-
* pack and no hand-off to say anything about.
|
|
1804
|
+
* Added to a `verified` reliance line after its first sentence, only where the
|
|
1805
|
+
* drop holds a hand-off record.
|
|
2305
1806
|
*/
|
|
2306
1807
|
export const CASE_RELIANCE_HANDOFF_CLAUSE =
|
|
2307
1808
|
"Every hand-off with both sides in this drop is intact.";
|
|
2308
1809
|
|
|
2309
1810
|
/**
|
|
2310
|
-
*
|
|
2311
|
-
*
|
|
2312
|
-
* marker (`environmentMarker`): `dev` where every envelope is signed dev,
|
|
2313
|
-
* `mixed` where only some are. Inserted after the line's first sentence, for
|
|
2314
|
-
* every verdict word. A drop with no `dev` envelope has no marker and gains
|
|
2315
|
-
* nothing.
|
|
1811
|
+
* Added after the reliance line's first sentence, for every verdict, where the
|
|
1812
|
+
* drop holds `environment: "dev"` envelopes: `dev` if all are, `mixed` if some.
|
|
2316
1813
|
*/
|
|
2317
1814
|
export const CASE_RELIANCE_SANDBOX: Readonly<Record<EnvironmentMarker, string>> = {
|
|
2318
1815
|
dev:
|
|
@@ -2324,24 +1821,16 @@ export const CASE_RELIANCE_SANDBOX: Readonly<Record<EnvironmentMarker, string>>
|
|
|
2324
1821
|
};
|
|
2325
1822
|
|
|
2326
1823
|
/**
|
|
2327
|
-
* What a reader may rely on for one case reading (5.5.6
|
|
2328
|
-
*
|
|
2329
|
-
*
|
|
2330
|
-
*
|
|
2331
|
-
* `verified`, the hand-off clause is inserted after the first sentence
|
|
2332
|
-
* where `handoffRecords` is not zero, and left out where it is zero: the
|
|
2333
|
-
* base sentence never claims a hand-off the drop does not hold.
|
|
2334
|
-
*
|
|
2335
|
-
* Where `environmentMarker` is `dev` or `mixed`, the `CASE_RELIANCE_SANDBOX`
|
|
2336
|
-
* sentence for it is inserted after the first sentence, for every word, so
|
|
2337
|
-
* it directly follows the claim it qualifies (and precedes the hand-off
|
|
2338
|
-
* clause). Where it is null or absent, the text is exactly what it was
|
|
2339
|
-
* before A48.
|
|
1824
|
+
* What a reader may rely on for one case reading (§5.5.6). The sandbox
|
|
1825
|
+
* sentence, if any, follows the first sentence and precedes the hand-off
|
|
1826
|
+
* clause, which a `verified` reading gains only with `handoffRecords > 0`, so
|
|
1827
|
+
* the line never claims a hand-off the drop does not hold.
|
|
2340
1828
|
*/
|
|
2341
1829
|
export function caseReliance(
|
|
2342
1830
|
verdict: CaseVerdict,
|
|
2343
1831
|
handoffRecords: number,
|
|
2344
1832
|
environmentMarker: EnvironmentMarker | null = null,
|
|
1833
|
+
reasonKind: ReasonKind | null = null,
|
|
2345
1834
|
): string {
|
|
2346
1835
|
let first: string;
|
|
2347
1836
|
let rest: string;
|
|
@@ -2352,13 +1841,14 @@ export function caseReliance(
|
|
|
2352
1841
|
"in it was signed by its holder's key. This was read offline, on this " +
|
|
2353
1842
|
"device. It does not say this drop holds everything that was recorded.";
|
|
2354
1843
|
} else {
|
|
2355
|
-
const
|
|
1844
|
+
const key =
|
|
1845
|
+
verdict === "failed" && reasonKind === "nonconforming" ? "failed_nonconforming" : verdict;
|
|
1846
|
+
const base = CASE_RELIANCE[key];
|
|
2356
1847
|
const cut = base.indexOf(". ");
|
|
2357
1848
|
first = base.slice(0, cut + 1);
|
|
2358
1849
|
rest = base.slice(cut + 2);
|
|
2359
1850
|
}
|
|
2360
|
-
//
|
|
2361
|
-
// forbids but an untyped caller can pass, reads as no marker.
|
|
1851
|
+
// An untyped caller can pass any value; only `dev` and `mixed` add a sentence.
|
|
2362
1852
|
const sandbox =
|
|
2363
1853
|
environmentMarker === "dev" || environmentMarker === "mixed"
|
|
2364
1854
|
? CASE_RELIANCE_SANDBOX[environmentMarker]
|
|
@@ -2367,7 +1857,7 @@ export function caseReliance(
|
|
|
2367
1857
|
return `${first} ${sandbox} ${rest}`;
|
|
2368
1858
|
}
|
|
2369
1859
|
|
|
2370
|
-
/** The three witness states
|
|
1860
|
+
/** The three witness states and how each is printed (§5.5.6). No others. */
|
|
2371
1861
|
export const WITNESS_STATES = {
|
|
2372
1862
|
confirmed: "Confirmed on the public record",
|
|
2373
1863
|
not_checked: "Not checked",
|
|
@@ -2376,41 +1866,37 @@ export const WITNESS_STATES = {
|
|
|
2376
1866
|
export type WitnessState = keyof typeof WITNESS_STATES;
|
|
2377
1867
|
|
|
2378
1868
|
/**
|
|
2379
|
-
* The
|
|
2380
|
-
*
|
|
2381
|
-
*
|
|
2382
|
-
* settle, in a fixed order; `services` names each service that answered, as
|
|
2383
|
-
* the URL the lookup used: named, never trusted (6).
|
|
1869
|
+
* The result of a public-record lookup, reported beside the headline, never
|
|
1870
|
+
* in it, and never moving it. `reasons` says why `not_checked` did not settle;
|
|
1871
|
+
* `services` names each gateway that answered: named, never trusted.
|
|
2384
1872
|
*/
|
|
2385
1873
|
export interface WitnessLine {
|
|
2386
1874
|
state: WitnessState;
|
|
2387
1875
|
reasons: string[];
|
|
2388
1876
|
services: string[];
|
|
2389
1877
|
/**
|
|
2390
|
-
*
|
|
2391
|
-
*
|
|
2392
|
-
*
|
|
1878
|
+
* Cached witness times no lookup settled, as `[blockHeight, blockTime]`,
|
|
1879
|
+
* distinct and in time order (§5.6.6). Printed as not checked, never as a
|
|
1880
|
+
* time a reader may rely on.
|
|
2393
1881
|
*/
|
|
2394
1882
|
cached?: [number, number][];
|
|
2395
1883
|
/**
|
|
2396
|
-
* Witness times the public record returned
|
|
2397
|
-
*
|
|
2398
|
-
* service, never the cache's value. Absent reads as none.
|
|
1884
|
+
* Witness times the public record returned that matched the cache, distinct
|
|
1885
|
+
* and in time order. Always the gateway's answer, never the cache's value.
|
|
2399
1886
|
*/
|
|
2400
1887
|
recorded?: [number, number][];
|
|
2401
1888
|
}
|
|
2402
1889
|
|
|
2403
|
-
/** The line
|
|
1890
|
+
/** The line where no lookup ran: every reading this module produces on its own. */
|
|
2404
1891
|
export const WITNESS_NOT_LOOKED_UP: Readonly<WitnessLine> = Object.freeze({
|
|
2405
1892
|
state: "not_checked",
|
|
2406
1893
|
reasons: ["no lookup was run"],
|
|
2407
1894
|
services: [],
|
|
2408
1895
|
});
|
|
2409
1896
|
|
|
2410
|
-
/**
|
|
2411
|
-
/** How a witness time a pack caches is printed where nothing checked it (5.5.6, 5.6.6, v1.25, A30). */
|
|
1897
|
+
/** A cached witness time no lookup checked (§5.6.6). */
|
|
2412
1898
|
export const WITNESS_TIME_CACHED = "the pack dates the witness, not checked";
|
|
2413
|
-
/**
|
|
1899
|
+
/** A witness time the public record returned that matched the cache. */
|
|
2414
1900
|
export const WITNESS_TIME_RECORDED = "the public record dates the witness";
|
|
2415
1901
|
|
|
2416
1902
|
function witnessTimes(times: readonly [number, number][]): string {
|