@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/dist/case.js
CHANGED
|
@@ -1,684 +1,293 @@
|
|
|
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
|
import { DECLARATION_VERSION, DECLARATION_VERSIONS, checkDeclaration, declarationClaims, packRecords, } from "./declaration.js";
|
|
64
|
+
import { readAgentRecords } from "./agent-record.js";
|
|
245
65
|
import { readChainLinks } from "./chain.js";
|
|
246
|
-
import { NOT_A_HANDOFF_FINDING, normalizedSigningKey, readHandoffs, recordIsMalformed, } from "./handoff.js";
|
|
66
|
+
import { NOT_A_HANDOFF_FINDING, normalizedSigningKey, readHandoffs, recordIsMalformed, recordIsNonconforming, } from "./handoff.js";
|
|
247
67
|
import { coSignaturesPresent, resolveTraceEvents, verifyEvidenceBundle } from "./evidence.js";
|
|
248
68
|
import { accountDisclosure, completenessNotes, gradePack, } from "./grading.js";
|
|
249
69
|
import { renderWitnessTime } from "./witness-cache.js";
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
|
|
253
|
-
* `evidence-bundle.md` 3 or `PACK_VERDICTS`, and anchor#198 says in terms that
|
|
254
|
-
* no new verdict word is introduced anywhere.
|
|
255
|
-
*/
|
|
256
|
-
export const CASE_VERDICTS = [
|
|
257
|
-
"verified",
|
|
258
|
-
"failed",
|
|
259
|
-
"partial",
|
|
260
|
-
"mixed",
|
|
261
|
-
"incomplete",
|
|
262
|
-
"malformed",
|
|
263
|
-
];
|
|
264
|
-
/**
|
|
265
|
-
* anchor#198 says in terms that no new verdict word is introduced anywhere, and
|
|
266
|
-
* this line is what ENFORCES it rather than asserting it in prose.
|
|
267
|
-
*
|
|
268
|
-
* `tsc` refuses to compile if any member of `CASE_VERDICTS` is not already a
|
|
269
|
-
* `PackVerdict`, so a seventh word added here fails the BUILD rather than
|
|
270
|
-
* reaching a test that might not cover it. The Python half asserts the same
|
|
271
|
-
* containment at run time against `grading.PACK_VERDICTS`, which is that
|
|
272
|
-
* language's equivalent gate.
|
|
273
|
-
*/
|
|
274
|
-
const _caseVerdictsAreSharedVocabulary = [...CASE_VERDICTS];
|
|
275
|
-
void _caseVerdictsAreSharedVocabulary;
|
|
70
|
+
import { KIND_CLAIMS, VERDICTS, VERDICT_TITLES, bundleProblems, claimsOf, problem, restOnSigned, verdictOf as claimsVerdictOf, withItems, } from "./claims.js";
|
|
71
|
+
/** The three verdicts (evidence-bundle.md section 3). A case reads its worst pack. */
|
|
72
|
+
export const CASE_VERDICTS = VERDICTS;
|
|
276
73
|
/**
|
|
277
|
-
* The
|
|
278
|
-
* (
|
|
279
|
-
*
|
|
280
|
-
* `body_type` selects a code path and is written by the party assembling the
|
|
281
|
-
* pack. Without this set, a pack whose records live under a key this reader
|
|
282
|
-
* does not know yields zero records, contributes nothing, and lets the rest of
|
|
283
|
-
* the case read clean over it: the exact defect class this module is written
|
|
284
|
-
* against. An unknown value is `PACK_BODY_TYPE_UNKNOWN`.
|
|
74
|
+
* The pack bodies that carry `events[]` and may carry a `declaration`
|
|
75
|
+
* (evidence-bundle.md §5.4.1). An accept-set: `body_type` is party-written, so
|
|
76
|
+
* an unknown value is `PACK_BODY_TYPE_UNKNOWN`, never zero records read clean.
|
|
285
77
|
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
* named `PACK_BODY_TYPE_UNKNOWN`, `incomplete`. Reading inside it changes which
|
|
290
|
-
* packs this reader grades, so it is a decision of its own. A reader who needs
|
|
291
|
-
* the records hands in the source bundle as a pack. The export's wrapper key is
|
|
292
|
-
* still read, so wrapping a pack in one does not change which counterpart kind
|
|
293
|
-
* another record gets.
|
|
78
|
+
* `sanning.evidence.export/v2` is not unwrapped: it is named
|
|
79
|
+
* `PACK_BODY_TYPE_UNKNOWN`, and a reader who needs its records hands in the
|
|
80
|
+
* source bundle. Its wrapper key is still read.
|
|
294
81
|
*/
|
|
295
82
|
export const CASE_BODY_TYPES = [
|
|
296
83
|
"sanning.anchor.trace/v3",
|
|
297
84
|
"sanning.stamp.evidence/v2",
|
|
298
85
|
];
|
|
299
86
|
/**
|
|
300
|
-
* The
|
|
301
|
-
* accept-set
|
|
302
|
-
*
|
|
303
|
-
* from that file, and a test here pins this one equal to this kernel's own
|
|
304
|
-
* `ACCEPTED_SPEC_MAJORS`, which is what `verifyEvidenceBundle` reads under:
|
|
305
|
-
* the Python kernel narrows its verifier to this set for the case reader, and
|
|
306
|
-
* this kernel's verifier already reads exactly it. If this kernel ever admits
|
|
307
|
-
* a quarantined profile, that test fails, and the case reader has to narrow
|
|
308
|
-
* its verifier the way the Python one does before it can pass.
|
|
87
|
+
* The profiles records are read under: the accept-set (envelope-spec.md §5)
|
|
88
|
+
* minus quarantined profiles in `cross-kernel/accept-set.json`. A test pins
|
|
89
|
+
* this equal to `ACCEPTED_SPEC_MAJORS`, which `verifyEvidenceBundle` reads under.
|
|
309
90
|
*/
|
|
310
|
-
export const CASE_RECORD_PROFILES = ["sanning.
|
|
311
|
-
/**
|
|
91
|
+
export const CASE_RECORD_PROFILES = ["sanning.events/v2"];
|
|
92
|
+
/** Accepted profiles under quarantine. */
|
|
312
93
|
export const CASE_QUARANTINED_PROFILES = ["sanning.mlflow/v1"];
|
|
313
|
-
// ---- The exceptions ---------------------------------------------------------
|
|
314
|
-
//
|
|
315
|
-
// Each of these is a NAME for something a reader would otherwise have to infer
|
|
316
|
-
// from an absence. None of them is a verdict word.
|
|
317
94
|
/**
|
|
318
|
-
* A conformant hand-off record with no counterpart
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
* That is ALL it says. It does not say the counterparty's account is absent,
|
|
326
|
-
* and it does not say nothing was tampered with: a counterparty that signs its
|
|
327
|
-
* own pack under a second key, or hands over an export an issuer signed,
|
|
328
|
-
* reaches this reading too, and nothing offline binds a pack's key to the keys
|
|
329
|
-
* of the records inside it (proof#143). The rendered text states the fact and
|
|
330
|
-
* stops.
|
|
331
|
-
*
|
|
332
|
-
* It says nothing about RECORDS signed by that key either. A party may hold records
|
|
333
|
-
* its counterparty signed, and those are still graded as candidates, but they
|
|
334
|
-
* are not the counterparty's account of itself, so they do not move this
|
|
335
|
-
* record to `COUNTERPART_ABSENT_FROM_COUNTERPARTY_RECORDS`.
|
|
95
|
+
* A conformant hand-off record with no counterpart, where every pack verified
|
|
96
|
+
* under a readable key and none is signed by the key the record names, or every
|
|
97
|
+
* candidate was accounted for by another record (then `count` is set and no
|
|
98
|
+
* pack is named). An exception, never a failure. It says only that no pack is
|
|
99
|
+
* signed by the named key; records that key signed and others hold are not that
|
|
100
|
+
* party's own account and do not change it.
|
|
336
101
|
*/
|
|
337
102
|
export const COUNTERPART_ABSENT = "counterpart_absent";
|
|
338
103
|
/**
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
* nothing at all.
|
|
357
|
-
*
|
|
358
|
-
* What it does NOT prove, stated so no surface claims it: that B handed the
|
|
359
|
-
* pack over for THIS case. A pack B signed for some other purpose and a third
|
|
360
|
-
* party replayed reads the same, because the only values that could say
|
|
361
|
-
* otherwise (the declaration's `case_id` and `period`) are written by B and
|
|
362
|
-
* would let B leave this line by writing them differently. The cost the other
|
|
363
|
-
* way: a pack B assembles under any key other than the one its records name, a
|
|
364
|
-
* per-organization hand-over key (anchor#196) included, reads
|
|
365
|
-
* `COUNTERPART_ABSENT`, because nothing in the case binds that key to B. That
|
|
366
|
-
* reading is still true: no pack signed by the named key is here. So a
|
|
367
|
-
* counterparty can AVOID this line by choosing the key it signs under; what it
|
|
368
|
-
* cannot do is make the missing counterpart read clean.
|
|
369
|
-
*
|
|
370
|
-
* It is deliberately NOT also conditioned on a record signed by B being in the
|
|
371
|
-
* pile. B's pack can hold none this reader reads (a body it does not unwrap,
|
|
372
|
-
* bytes B withheld, an empty `events[]`), and each of those is a choice B makes
|
|
373
|
-
* and is named on its own. A second conjunct on them hands B the innocent line
|
|
374
|
-
* for making one.
|
|
375
|
-
*
|
|
376
|
-
* It is a DISTINCT kind from `COUNTERPART_ABSENT` and not a louder spelling of
|
|
377
|
-
* it. Collapsing the two would be the defect class again: "the counterparty
|
|
378
|
-
* never joined" and "the counterparty joined and its own account does not
|
|
379
|
-
* contain this hand-off" are the honest reading and the damning one, and one
|
|
380
|
-
* name for both lets the second read as the first.
|
|
381
|
-
*
|
|
382
|
-
* It is still an EXCEPTION and not a failure, which criterion 3 requires and
|
|
383
|
-
* which is also the honest call: the counterparty's pack is a selection over
|
|
384
|
-
* its own interval, and a hand-off outside that interval is legitimately not
|
|
385
|
-
* in it. What this names is that the question can now be ASKED of the other
|
|
386
|
-
* party's own evidence, which is what no single pack could ever support.
|
|
104
|
+
* A conformant hand-off record with no counterpart, where a verified pack
|
|
105
|
+
* signed by the key the record names is in the case and holds no counterpart.
|
|
106
|
+
* The closure this module exists for (`declaration-unclaimed-record-01`).
|
|
107
|
+
*
|
|
108
|
+
* "Its own account" is read from the key that signed the pack, never from
|
|
109
|
+
* record keys: a wrapper signed by B is provenance an accuser cannot forge, but
|
|
110
|
+
* records signed by B that A holds are not B's account.
|
|
111
|
+
*
|
|
112
|
+
* Not proven: that B handed the pack over for this case. A pack B signs under
|
|
113
|
+
* any other key reads `COUNTERPART_ABSENT`, which is still true; B can avoid
|
|
114
|
+
* this line by choice of key, but cannot make the missing counterpart read
|
|
115
|
+
* clean. Not conditioned on B's records being readable, since each way B could
|
|
116
|
+
* make them unreadable is its own named choice.
|
|
117
|
+
*
|
|
118
|
+
* A distinct kind from `COUNTERPART_ABSENT`: one name for both would let the
|
|
119
|
+
* damning reading pass as the honest one. Still `incomplete`, not a failure: a
|
|
120
|
+
* hand-off outside the interval B's pack covers is legitimately absent.
|
|
387
121
|
*/
|
|
388
122
|
export const COUNTERPART_ABSENT_FROM_COUNTERPARTY_RECORDS = "counterpart_absent_from_counterparty_records";
|
|
389
123
|
/**
|
|
390
|
-
* A conformant hand-off record with no counterpart, no
|
|
391
|
-
*
|
|
392
|
-
*
|
|
393
|
-
*
|
|
394
|
-
* Whether a pack signed by the named key is in the case is UNKNOWN, so neither
|
|
395
|
-
* of the other two kinds is true.
|
|
396
|
-
*
|
|
397
|
-
* The gate on the previous version of this reader showed why it exists. A
|
|
398
|
-
* counterparty that signed its own pack under `" " + key` got
|
|
399
|
-
* `PACK_KEY_UNREADABLE` for the pack and the innocent `COUNTERPART_ABSENT` for
|
|
400
|
-
* the record, and the output contradicted itself: whose pack it was is
|
|
401
|
-
* UNKNOWN, and then no pack is signed by the named key. The second sentence
|
|
402
|
-
* was not established.
|
|
403
|
-
*
|
|
404
|
-
* `incomplete`, like the other two one-sided kinds: "I could not tell".
|
|
124
|
+
* A conformant hand-off record with no counterpart, no verified pack signed by
|
|
125
|
+
* the key it names, and at least one pack whose signer is unknown
|
|
126
|
+
* (`PACK_KEY_UNREADABLE` or `PACK_DOES_NOT_VERIFY`). Whether the named key
|
|
127
|
+
* signed a pack is unknown, so neither other kind is true. `incomplete`.
|
|
405
128
|
*/
|
|
406
129
|
export const COUNTERPART_ABSENT_SIGNER_UNKNOWN = "counterpart_absent_signer_unknown";
|
|
407
130
|
/**
|
|
408
|
-
* More than one candidate counterpart
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
* record. `count` is how many of the candidates are live, which is the number
|
|
412
|
-
* the ambiguity is about.
|
|
131
|
+
* More than one live candidate counterpart (§4.1). The reading is the worst of
|
|
132
|
+
* them and the ambiguity is named, so an honest decoy cannot hide a tampered
|
|
133
|
+
* record. `count` is the number of live candidates.
|
|
413
134
|
*/
|
|
414
135
|
export const COUNTERPART_AMBIGUOUS = "counterpart_ambiguous";
|
|
136
|
+
/** A formed pair that failed checks, listed in §4.1's fixed order. */
|
|
137
|
+
export const PAIR_FAILED = "pair_failed";
|
|
138
|
+
/** This record itself is malformed under §4.1 (`recordIsMalformed`). */
|
|
139
|
+
export const RECORD_MALFORMED = "record_malformed";
|
|
415
140
|
/**
|
|
416
|
-
* A
|
|
417
|
-
*
|
|
141
|
+
* A candidate counterpart is malformed and this record is not. §4.1 reports
|
|
142
|
+
* the worst candidate, so without this kind one junk record signed by the
|
|
143
|
+
* counterparty's key would blame the honest record and drop its other failures.
|
|
144
|
+
* `incomplete`: the defect is the candidate's, which carries its own
|
|
145
|
+
* `RECORD_MALFORMED`.
|
|
418
146
|
*/
|
|
419
|
-
export const
|
|
147
|
+
export const COUNTERPART_MALFORMED = "counterpart_malformed";
|
|
420
148
|
/**
|
|
421
|
-
*
|
|
422
|
-
*
|
|
423
|
-
*
|
|
149
|
+
* This record is one of the two hand-off types, signed and intact, and does
|
|
150
|
+
* not follow its vocabulary (`recordIsNonconforming`). A producer's error,
|
|
151
|
+
* never a proven alteration.
|
|
424
152
|
*/
|
|
425
|
-
export const
|
|
153
|
+
export const RECORD_NONCONFORMING = "record_nonconforming";
|
|
426
154
|
/**
|
|
427
|
-
* A
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
* outranks `failed`, so one junk record signed by the counterparty's key makes
|
|
431
|
-
* the reading of a perfectly well formed record `malformed` too. Filed under
|
|
432
|
-
* `RECORD_MALFORMED`, that read as "this record is not a well formed hand-off
|
|
433
|
-
* record" about the HONEST record, and the branch it took skipped the
|
|
434
|
-
* counterpart, ambiguity and `failedChecks` reporting entirely: one junk record
|
|
435
|
-
* both blamed the wrong party and stripped a named `message_hash` failure out
|
|
436
|
-
* of the case. That is proof#133 ruling 6, which 4.1 closed by keeping the
|
|
437
|
-
* UNION of `failedChecks`, reappearing one layer out because this layer
|
|
438
|
-
* discarded that union.
|
|
439
|
-
*
|
|
440
|
-
* A record's own conformance and its candidate's are different facts and do
|
|
441
|
-
* not share a kind. This one is `incomplete` and not `malformed`: the proven
|
|
442
|
-
* defect belongs to the candidate, which carries its own `RECORD_MALFORMED`
|
|
443
|
-
* line, and what is true of THIS record is that its pairing could not be
|
|
444
|
-
* determined. Contributing `malformed` here would attribute the candidate's
|
|
445
|
-
* defect to the record a party being accused did not write.
|
|
155
|
+
* A candidate counterpart is nonconforming and this record is not.
|
|
156
|
+
* `incomplete` for the reason `COUNTERPART_MALFORMED` is: the defect is the
|
|
157
|
+
* candidate's, which carries its own `RECORD_NONCONFORMING`.
|
|
446
158
|
*/
|
|
447
|
-
export const
|
|
159
|
+
export const COUNTERPART_NONCONFORMING = "counterpart_nonconforming";
|
|
448
160
|
/**
|
|
449
|
-
* A record
|
|
450
|
-
*
|
|
451
|
-
*
|
|
452
|
-
*
|
|
453
|
-
*
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
*
|
|
457
|
-
* with an `ff` ligature in `event_type` and a Cyrillic `е` in
|
|
458
|
-
* `counterparty_public_key`, left 4.1's scope, took the `not_a_handoff_record`
|
|
459
|
-
* branch, and bought a case reading of `verified` with zero exceptions where
|
|
460
|
-
* the conformant spelling read `mixed` with four. proof#136 ratified that a
|
|
461
|
-
* respelling buys a DIFFERENT LINE rather than no line; at this layer it
|
|
462
|
-
* bought no line at all, and a strictly better outcome than honesty.
|
|
463
|
-
*
|
|
464
|
-
* It is not an accusation. `handoffs[]` MAY be empty and an unclaimed record
|
|
465
|
-
* may be entirely innocent. What it is, is the end of the silence: the reader
|
|
466
|
-
* cannot tell whether an unclaimed record is part of the hand-over the holder
|
|
467
|
-
* meant to give or one it is keeping out of its own account, and that is "I
|
|
468
|
-
* could not tell", which is why it carries `incomplete` and not `verified`.
|
|
469
|
-
*
|
|
470
|
-
* THE COST, stated rather than left to be found: a pack that declares records
|
|
471
|
-
* which are not hand-off sides at all, an ordinary log line among them, is
|
|
472
|
-
* named once per such record, so a case holding one cannot read `verified`.
|
|
473
|
-
* That is the price of a rule with no branch the holder selects, and the only
|
|
474
|
-
* narrower rule available keys on a value the holder writes. A surface that
|
|
475
|
-
* wants the two apart reads `CaseRecordReading.findings`, where
|
|
476
|
-
* `not_a_handoff_record` still says which is which.
|
|
161
|
+
* A record its pack declares and its pack's readable declaration claims in no
|
|
162
|
+
* `handoffs[]` entry (§5.4.2 rules 2 and 7 cannot reach it). Turns on what the
|
|
163
|
+
* pack said, never on whether §4.1 governs the record, so respelling
|
|
164
|
+
* `event_type` cannot move a record out of this check.
|
|
165
|
+
*
|
|
166
|
+
* Not an accusation: `handoffs[]` may be empty and the record innocent, so it
|
|
167
|
+
* is `incomplete`. The cost is that a declared non-hand-off record (a log line)
|
|
168
|
+
* is named too; `CaseRecordReading.findings` tells the two apart.
|
|
477
169
|
*/
|
|
478
170
|
export const RECORD_UNCLAIMED = "record_unclaimed_by_its_pack";
|
|
479
171
|
/**
|
|
480
|
-
* The pack
|
|
481
|
-
*
|
|
482
|
-
*
|
|
483
|
-
* `event_bytes` is OPTIONAL (5.1, 5.3) and withholding it is minimum
|
|
484
|
-
* disclosure, which the corpus calls a customer's right and not a defect
|
|
485
|
-
* (proof#137, defect 3), so this is not a deviation and does not fail. It is
|
|
486
|
-
* still SAID, because a withheld record is exactly where a missing counterpart
|
|
487
|
-
* would hide, and a reader that dropped it silently would read a trimmed pack
|
|
488
|
-
* as an honest one.
|
|
172
|
+
* The pack withheld `event_bytes` for a record. Withholding is minimal
|
|
173
|
+
* disclosure (§5.1, §5.3), not a deviation, but it is said: a withheld record is
|
|
174
|
+
* where a missing counterpart would hide.
|
|
489
175
|
*/
|
|
490
176
|
export const RECORD_BYTES_WITHHELD = "record_bytes_withheld";
|
|
491
177
|
/**
|
|
492
|
-
* The pack
|
|
493
|
-
*
|
|
494
|
-
* `packRecords`). Bytes the holder handed over that do not decode are a defect
|
|
495
|
-
* in what was handed over, not an absence, so this one is `malformed`.
|
|
178
|
+
* The pack supplied `event_bytes` that do not read as `JCS(event record)`.
|
|
179
|
+
* A defect in what was handed over, so `malformed`.
|
|
496
180
|
*/
|
|
497
181
|
export const RECORD_BYTES_UNREADABLE = "record_bytes_unreadable";
|
|
498
|
-
/**
|
|
499
|
-
* The pack deviates from its own declaration (5.4.2). A proven mismatch, so it
|
|
500
|
-
* fails: a case cannot be clean over a pack that is proven to be trimmed.
|
|
501
|
-
*/
|
|
182
|
+
/** The pack deviates from its own declaration (§5.4.2). A proven mismatch: `failed`. */
|
|
502
183
|
export const PACK_DECLARATION_DEVIATES = "pack_declaration_deviates";
|
|
503
184
|
/**
|
|
504
|
-
* The pack and the
|
|
505
|
-
*
|
|
506
|
-
*
|
|
507
|
-
* declares `content_withheld` for which bytes reached this reader from the
|
|
508
|
-
* side-content map alone.
|
|
185
|
+
* The pack and the unsigned content beside it do not line up (§5.4.2 rules 3
|
|
186
|
+
* and 4): a record declared `disclosed` with no bytes, or declared
|
|
187
|
+
* `content_withheld` with bytes from the side-content map alone.
|
|
509
188
|
*
|
|
510
|
-
* `incomplete
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
* `PACK_DECLARATION_DEVIATES` an accuser could put an intact pack into a
|
|
514
|
-
* `failed` case WITH NO KEY AT ALL: supply bytes for a record its holder
|
|
515
|
-
* declared withheld, or drop the bytes of one it declared disclosed. The pack
|
|
516
|
-
* was intact, its wrapper verified, and its holder had done nothing. This is
|
|
517
|
-
* the mirror of proof#143, where a counterparty makes ITSELF look better by
|
|
518
|
-
* choosing what it hands over.
|
|
519
|
-
*
|
|
520
|
-
* It is emitted BESIDE `PACK_DECLARATION_DEVIATES` where a pack does both,
|
|
521
|
-
* because the two say different things about different records.
|
|
189
|
+
* `incomplete`, never `failed`: whoever assembles a case fills in that map, so
|
|
190
|
+
* otherwise an accuser could fail an intact pack with no key. Emitted beside
|
|
191
|
+
* `PACK_DECLARATION_DEVIATES` when a pack does both.
|
|
522
192
|
*/
|
|
523
193
|
export const PACK_DECLARATION_CONTENT_DEPENDENT = "pack_declaration_content_dependent";
|
|
524
194
|
/**
|
|
525
|
-
* The pack
|
|
526
|
-
*
|
|
527
|
-
* does not satisfy 5.4.1. `declaration_version` is chosen by the same party
|
|
528
|
-
* that assembles the pack and has no accept-set of its own, so this is UNKNOWN
|
|
529
|
-
* and never clean.
|
|
195
|
+
* The pack's `declaration` cannot be read (§5.4.3). Its version is chosen by
|
|
196
|
+
* the pack's assembler with no accept-set, so this is unknown, never clean.
|
|
530
197
|
*/
|
|
531
198
|
export const PACK_DECLARATION_UNREADABLE = "pack_declaration_unreadable";
|
|
532
199
|
/**
|
|
533
|
-
* The pack carries no `declaration`
|
|
534
|
-
*
|
|
535
|
-
* `verified`, because there is nothing to check the pack against, and never
|
|
536
|
-
* `failed`, because nothing is wrong.
|
|
200
|
+
* The pack carries no `declaration`. `partial` (§5.4.3): nothing to check
|
|
201
|
+
* against, and nothing wrong.
|
|
537
202
|
*/
|
|
538
203
|
export const PACK_UNDECLARED = "pack_undeclared";
|
|
539
|
-
/**
|
|
540
|
-
* The pack's `body_type` is not one this reader knows to carry `events[]`
|
|
541
|
-
* (`CASE_BODY_TYPES`).
|
|
542
|
-
*/
|
|
204
|
+
/** The pack's `body_type` is not in `CASE_BODY_TYPES`. */
|
|
543
205
|
export const PACK_BODY_TYPE_UNKNOWN = "pack_body_type_unknown";
|
|
544
|
-
/**
|
|
545
|
-
* The pack carries a body this reader knows and no records in it. A pack that
|
|
546
|
-
* contributes nothing is named rather than passed over.
|
|
547
|
-
*/
|
|
206
|
+
/** The pack carries a known body with no records. */
|
|
548
207
|
export const PACK_HOLDS_NO_RECORDS = "pack_holds_no_records";
|
|
549
208
|
/**
|
|
550
|
-
* The
|
|
551
|
-
*
|
|
552
|
-
*
|
|
553
|
-
*
|
|
554
|
-
* `
|
|
555
|
-
*
|
|
556
|
-
* Found by constructing it, while enumerating this module's exits: the wrapper
|
|
557
|
-
* key is the ONE input that separates the two counterpart kinds, and a key this
|
|
558
|
-
* reader could not read used to drop out of that set in silence. The Python
|
|
559
|
-
* `verify_evidence_bundle` accepts a wrapper `public_key` with ASCII
|
|
560
|
-
* whitespace inside it or around it (`bytes.fromhex` skips it; this kernel's
|
|
561
|
-
* verifier rejects it), so a counterparty that signed its own pack under
|
|
562
|
-
* `" " + key` handed a Python reader a pack that verified, contributed no key,
|
|
563
|
-
* and moved the subject from `COUNTERPART_ABSENT_FROM_COUNTERPARTY_RECORDS` to
|
|
564
|
-
* the innocent `COUNTERPART_ABSENT`. The same repair 4.1 made for a RECORD
|
|
565
|
-
* whose envelope key it cannot read (proof#133, fourth hunt): a key a reader
|
|
566
|
-
* holds and cannot read is named, never treated as absent. It is `incomplete`
|
|
567
|
-
* rather than `malformed` because whether the spelling verifies is the envelope
|
|
568
|
-
* layer's call (proof#135), and this reader only knows it cannot read it.
|
|
209
|
+
* The pack's wrapper key cannot be read (`normalizedSigningKey` returns
|
|
210
|
+
* nothing), so its signer is unknown. None of its records are read, and a
|
|
211
|
+
* one-sided record whose named key signed no verified pack reads
|
|
212
|
+
* `COUNTERPART_ABSENT_SIGNER_UNKNOWN`. A key a reader holds and cannot read is
|
|
213
|
+
* named, never treated as absent. `incomplete`: whether the spelling verifies
|
|
214
|
+
* is the envelope layer's call.
|
|
569
215
|
*/
|
|
570
216
|
export const PACK_KEY_UNREADABLE = "pack_key_unreadable";
|
|
571
217
|
/**
|
|
572
|
-
* The pack
|
|
573
|
-
*
|
|
574
|
-
*
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
*
|
|
579
|
-
*
|
|
580
|
-
*
|
|
581
|
-
*
|
|
582
|
-
* an unknown body or withheld bytes produce, and those are named on their own.
|
|
583
|
-
*
|
|
584
|
-
* `failed`: a pack in the case is proven broken.
|
|
218
|
+
* The pack's `bundle.json` cannot be read as JSON: it does not parse, nests too deep, or
|
|
219
|
+
* names one member twice. The pack is not verified and none of its records are read, and
|
|
220
|
+
* every other pack in the case still is. `incomplete`: the signature could not be checked.
|
|
221
|
+
*/
|
|
222
|
+
export const PACK_UNREADABLE = "pack_unreadable";
|
|
223
|
+
/**
|
|
224
|
+
* `verifyEvidenceBundle`, run offline here, returned `failed` or `malformed`.
|
|
225
|
+
* None of its records are read (§4.1 requires verified envelopes), and its
|
|
226
|
+
* wrapper key is never counted as a pack signer: an unverified wrapper claiming
|
|
227
|
+
* the counterparty's key is what an accuser would forge. `failed`.
|
|
585
228
|
*/
|
|
586
229
|
export const PACK_DOES_NOT_VERIFY = "pack_does_not_verify";
|
|
587
230
|
/**
|
|
588
|
-
* The pack's wrapper
|
|
589
|
-
* (evidence-bundle 5.5.1
|
|
590
|
-
*
|
|
591
|
-
*
|
|
592
|
-
*
|
|
593
|
-
*
|
|
594
|
-
*
|
|
595
|
-
* otherwise the full grade. A body the verifier could not read is `malformed`,
|
|
596
|
-
* a body that is not one of the two event-carrying bodies takes the signed
|
|
597
|
-
* run's status, and a verifier that graded a different number of records than
|
|
598
|
-
* the pack holds takes the signed status, or `malformed` where that status is
|
|
231
|
+
* The pack's wrapper holds and its case grade is not `verified`
|
|
232
|
+
* (evidence-bundle §5.5.1). The case grade is the §5.4.5 pack grade taken
|
|
233
|
+
* twice: signed (side map empty) and full (with it). The signed grade decides
|
|
234
|
+
* where it is `failed` or `malformed`; a failure only the full grade reaches is
|
|
235
|
+
* `incomplete`, since it rests on unsigned bytes; otherwise the full grade.
|
|
236
|
+
* An unreadable body is `malformed`; a non-event body takes the signed status;
|
|
237
|
+
* a record-count mismatch takes the signed status, or `malformed` where that is
|
|
599
238
|
* `verified`.
|
|
600
239
|
*
|
|
601
|
-
*
|
|
602
|
-
*
|
|
603
|
-
* `
|
|
604
|
-
* CONTRIBUTES the grade, and `incomplete` for `pending`, `not_found` or
|
|
605
|
-
* `mixed`, so the case verdict never reads above its weakest pack.
|
|
240
|
+
* Carries `status`, `causes` (the deciding grade's cause kinds) and `reasons`
|
|
241
|
+
* (from the signed run). Contributes the grade, or `incomplete` for `pending`,
|
|
242
|
+
* `not_found` or `mixed`, so the case never reads above its weakest pack.
|
|
606
243
|
*/
|
|
607
244
|
export const PACK_GRADE_NOT_VERIFIED = "pack_grade_not_verified";
|
|
608
245
|
/**
|
|
609
|
-
* A record
|
|
610
|
-
*
|
|
611
|
-
*
|
|
612
|
-
* is proven not to verify. As a reason to drop the whole pack it let the
|
|
613
|
-
* counterparty add one junk event to its own pack, re-seal it, and strip a
|
|
614
|
-
* proven `message_hash` failure out of the case (gate 3 on proof#140).
|
|
246
|
+
* A record whose own envelope does not verify, in a pack whose wrapper holds.
|
|
247
|
+
* Not read by §4.1. `failed`. Scoped to the record so one junk event cannot
|
|
248
|
+
* drop the whole pack and a proven failure with it.
|
|
615
249
|
*/
|
|
616
250
|
export const RECORD_ENVELOPE_DOES_NOT_VERIFY = "record_envelope_does_not_verify";
|
|
617
251
|
/**
|
|
618
|
-
* A record whose envelope verifies
|
|
619
|
-
*
|
|
620
|
-
*
|
|
621
|
-
*
|
|
622
|
-
* remove its own tampered receipt from pairing by breaking that receipt's
|
|
623
|
-
* inclusion proof would be choosing its own reading.
|
|
252
|
+
* A record whose envelope verifies but which fails another check in its pack
|
|
253
|
+
* (inclusion proof, checkpoint or stamp join, a retired field). `failed`, and
|
|
254
|
+
* the record stays in the pile, so breaking its own proof cannot remove a
|
|
255
|
+
* tampered receipt from pairing.
|
|
624
256
|
*/
|
|
625
257
|
export const RECORD_DOES_NOT_VERIFY_IN_ITS_PACK = "record_does_not_verify_in_its_pack";
|
|
626
258
|
/**
|
|
627
|
-
* A record whose
|
|
628
|
-
*
|
|
629
|
-
*
|
|
630
|
-
* side map EMPTY: the content is inside the signed body. The same mismatch from
|
|
631
|
-
* content handed in beside the pack is `RECORD_CONTENT_DEPENDENT` (proof#159).
|
|
632
|
-
* `failed`, and the record STAYS in the pile: content beside a pack is unsigned and often
|
|
633
|
-
* supplied by whoever assembled the case, and gate 3 on proof#140 showed that
|
|
634
|
-
* party erasing an honest verified pair by swapping it.
|
|
259
|
+
* A record whose disclosed content does not hash to its `content_hash` (or a
|
|
260
|
+
* hand-off record's `message_hash`) with the side map empty: the content is in
|
|
261
|
+
* the signed body. `failed`, and the record stays in the pile.
|
|
635
262
|
*/
|
|
636
263
|
export const RECORD_CONTENT_MISMATCH = "record_content_mismatch";
|
|
637
264
|
/**
|
|
638
|
-
*
|
|
639
|
-
*
|
|
640
|
-
* bundle
|
|
641
|
-
*
|
|
642
|
-
*
|
|
643
|
-
* that map alone is `RECORD_CONTENT_DEPENDENT`. Both are `incomplete`: the
|
|
644
|
-
* reader established that the bytes it was handed are not the bytes the record
|
|
645
|
-
* commits to, and nothing about the party that signed the record, because
|
|
646
|
-
* whoever assembled the case chose those bytes with no key. Up to v1.15 both
|
|
647
|
-
* readings carried `failed`, and one flipped nibble of accuser-supplied content
|
|
648
|
-
* put a `failed` against an intact pack; under A17's headline rule that would
|
|
649
|
-
* have become the case's headline.
|
|
650
|
-
*
|
|
651
|
-
* The test that separates the two is the one proof#159 states for the inverse:
|
|
652
|
-
* every deviation a signature covers still reads `failed` with the side map
|
|
653
|
-
* EMPTY. So the pack is verified twice, with the map and without it, and what
|
|
654
|
-
* fails without it is `PACK_GRADE_NOT_VERIFIED`, `RECORD_CONTENT_MISMATCH`
|
|
655
|
-
* (content inside the signed body) or `RECORD_DOES_NOT_VERIFY_IN_ITS_PACK`,
|
|
656
|
-
* each still `failed`. Emptying the map is the most a party that signed
|
|
657
|
-
* nothing can do to starve the check, and it cannot remove any of those.
|
|
265
|
+
* A pack whose bundle verdict is `failed` only with the unsigned side content,
|
|
266
|
+
* and a record whose content mismatch comes from that content alone
|
|
267
|
+
* (evidence-bundle §5.5.4). Both `incomplete`: whoever assembled the case chose
|
|
268
|
+
* those bytes with no key. Every deviation a signature covers still fails with
|
|
269
|
+
* the side map empty, so starving the check cannot hide one.
|
|
658
270
|
*/
|
|
659
271
|
export const PACK_BUNDLE_CONTENT_DEPENDENT = "pack_bundle_content_dependent";
|
|
660
272
|
export const RECORD_CONTENT_DEPENDENT = "record_content_dependent";
|
|
661
273
|
/**
|
|
662
|
-
* A record
|
|
663
|
-
*
|
|
664
|
-
* `incomplete`: whether it verifies is what the kernels disagree about.
|
|
274
|
+
* A record under a quarantined profile (`CASE_QUARANTINED_PROFILES`). Not read.
|
|
275
|
+
* `incomplete`.
|
|
665
276
|
*/
|
|
666
277
|
export const RECORD_PROFILE_QUARANTINED = "record_profile_quarantined";
|
|
667
278
|
/** The reader was handed no packs at all. */
|
|
668
279
|
export const CASE_HOLDS_NO_PACKS = "case_holds_no_packs";
|
|
669
280
|
/**
|
|
670
|
-
*
|
|
671
|
-
*
|
|
672
|
-
*
|
|
673
|
-
*
|
|
674
|
-
* of that name.
|
|
281
|
+
* Notes (§5.5.3): findings with no verdict contribution, printed in the scope
|
|
282
|
+
* sentence so a clean verdict never hides them. `pack_undeclared` is a note
|
|
283
|
+
* only where the reading holds no hand-off record; otherwise it is the
|
|
284
|
+
* exception.
|
|
675
285
|
*/
|
|
676
286
|
export const CONTENT_WITHHELD_BY_DECLARATION = "content_withheld_by_declaration";
|
|
677
287
|
/**
|
|
678
|
-
* A pack's wrapper carries
|
|
679
|
-
*
|
|
680
|
-
*
|
|
681
|
-
* rather than presenting it as evidence, and it contributes no verdict.
|
|
288
|
+
* A pack's wrapper carries `co_signatures` (evidence-bundle section 2). It verifies
|
|
289
|
+
* nothing and sits outside the signed scope, so it is named, not presented as
|
|
290
|
+
* evidence.
|
|
682
291
|
*/
|
|
683
292
|
export const CO_SIGNATURES_PRESENT = "co_signatures_present";
|
|
684
293
|
export const CASE_NOTE_KINDS = [
|
|
@@ -686,18 +295,17 @@ export const CASE_NOTE_KINDS = [
|
|
|
686
295
|
CONTENT_WITHHELD_BY_DECLARATION,
|
|
687
296
|
CO_SIGNATURES_PRESENT,
|
|
688
297
|
];
|
|
689
|
-
/**
|
|
690
|
-
* Every exception this reader can report, as a closed set. A surface may render
|
|
691
|
-
* them and a test may assert over them; nothing outside this array is emitted.
|
|
692
|
-
*/
|
|
298
|
+
/** Every exception this reader can report. Nothing outside this set is emitted. */
|
|
693
299
|
export const CASE_EXCEPTION_KINDS = [
|
|
694
300
|
COUNTERPART_ABSENT,
|
|
695
301
|
COUNTERPART_ABSENT_FROM_COUNTERPARTY_RECORDS,
|
|
696
302
|
COUNTERPART_ABSENT_SIGNER_UNKNOWN,
|
|
697
303
|
COUNTERPART_AMBIGUOUS,
|
|
698
304
|
COUNTERPART_MALFORMED,
|
|
305
|
+
COUNTERPART_NONCONFORMING,
|
|
699
306
|
PAIR_FAILED,
|
|
700
307
|
RECORD_MALFORMED,
|
|
308
|
+
RECORD_NONCONFORMING,
|
|
701
309
|
RECORD_UNCLAIMED,
|
|
702
310
|
RECORD_BYTES_WITHHELD,
|
|
703
311
|
RECORD_BYTES_UNREADABLE,
|
|
@@ -707,6 +315,7 @@ export const CASE_EXCEPTION_KINDS = [
|
|
|
707
315
|
PACK_UNDECLARED,
|
|
708
316
|
PACK_BODY_TYPE_UNKNOWN,
|
|
709
317
|
PACK_HOLDS_NO_RECORDS,
|
|
318
|
+
PACK_UNREADABLE,
|
|
710
319
|
PACK_KEY_UNREADABLE,
|
|
711
320
|
PACK_DOES_NOT_VERIFY,
|
|
712
321
|
PACK_GRADE_NOT_VERIFIED,
|
|
@@ -719,141 +328,57 @@ export const CASE_EXCEPTION_KINDS = [
|
|
|
719
328
|
CASE_HOLDS_NO_PACKS,
|
|
720
329
|
];
|
|
721
330
|
/**
|
|
722
|
-
* The status
|
|
723
|
-
*
|
|
724
|
-
*
|
|
725
|
-
* `
|
|
726
|
-
*
|
|
727
|
-
*
|
|
728
|
-
* - `
|
|
729
|
-
*
|
|
730
|
-
*
|
|
731
|
-
*
|
|
732
|
-
*
|
|
733
|
-
* - `read_flagged`: the record went into the pile, and at least one
|
|
734
|
-
* exception carries its `packIndex` and `recordIndex`, or its pack's
|
|
735
|
-
* declaration check names its `eventId` by a deviation or a content
|
|
736
|
-
* finding (`declarationFindings` is not empty; under an `eventId` the
|
|
737
|
-
* pack carries twice, every record with that id). An exception about the
|
|
738
|
-
* pack alone flags no record.
|
|
739
|
-
* - `not_read`: the record never went into the pile. `reason` is the kind
|
|
740
|
-
* of the exception that says why, on the record
|
|
741
|
-
* (`record_profile_quarantined`, `record_envelope_does_not_verify`,
|
|
742
|
-
* `record_bytes_withheld`, `record_bytes_unreadable`) or on its pack
|
|
743
|
-
* (`pack_key_unreadable`, `pack_does_not_verify`,
|
|
744
|
-
* `pack_body_type_unknown`, `pack_grade_not_verified` where the body
|
|
745
|
-
* could not be read or the verifier graded a different number of records).
|
|
746
|
-
* An export's wrapped records carry the reason of the exception their pack
|
|
747
|
-
* actually got: `pack_body_type_unknown` where its wrapper holds, and
|
|
748
|
-
* otherwise `pack_key_unreadable`, `pack_does_not_verify` or
|
|
749
|
-
* `pack_grade_not_verified`.
|
|
750
|
-
*
|
|
751
|
-
* Before A43 the reading listed only the records it read, so a surface had to
|
|
752
|
-
* work out which records were missing, and why, by a rule of its own.
|
|
331
|
+
* The status each record of each pack gets (evidence-bundle §5.5.4), one per
|
|
332
|
+
* element of `body.events` (or, for an export, its source bundle's events):
|
|
333
|
+
*
|
|
334
|
+
* - `read_clean`: in the pile, named by no exception and no declaration
|
|
335
|
+
* finding. Says nothing about its pack; a surface must show the pack grade
|
|
336
|
+
* beside it.
|
|
337
|
+
* - `read_flagged`: in the pile, and named by an exception or a declaration
|
|
338
|
+
* finding (every record sharing a duplicated `eventId`). A pack-only
|
|
339
|
+
* exception flags no record.
|
|
340
|
+
* - `not_read`: never in the pile. `reason` is the record or pack exception
|
|
341
|
+
* that says why; an export's wrapped records take their pack's exception.
|
|
753
342
|
*/
|
|
754
343
|
export const CASE_RECORD_STATUSES = ["read_clean", "read_flagged", "not_read"];
|
|
755
344
|
/**
|
|
756
|
-
* What each exception kind contributes to the case verdict
|
|
757
|
-
*
|
|
758
|
-
*
|
|
759
|
-
*
|
|
760
|
-
*
|
|
761
|
-
* Read the `incomplete` rows as "I could not tell", which is the distinction
|
|
762
|
-
* this whole module turns on. None of them is `verified`: an exception never
|
|
763
|
-
* contributes a clean verdict, which is what makes the invariant in `verdictOf`
|
|
764
|
-
* hold.
|
|
765
|
-
*
|
|
766
|
-
* `PACK_GRADE_NOT_VERIFIED` is the one row that is not a constant: it
|
|
767
|
-
* contributes the pack's case grade, which is never `verified` (the exception
|
|
768
|
-
* is emitted only when it is not). `null` marks that row, and `readCase` fills
|
|
769
|
-
* it from `status`.
|
|
770
|
-
*/
|
|
771
|
-
const EXCEPTION_VERDICT = {
|
|
772
|
-
[COUNTERPART_ABSENT]: "incomplete",
|
|
773
|
-
[COUNTERPART_ABSENT_FROM_COUNTERPARTY_RECORDS]: "incomplete",
|
|
774
|
-
[COUNTERPART_ABSENT_SIGNER_UNKNOWN]: "incomplete",
|
|
775
|
-
[COUNTERPART_AMBIGUOUS]: "incomplete",
|
|
776
|
-
[COUNTERPART_MALFORMED]: "incomplete",
|
|
777
|
-
[PAIR_FAILED]: "failed",
|
|
778
|
-
[RECORD_MALFORMED]: "malformed",
|
|
779
|
-
[RECORD_UNCLAIMED]: "incomplete",
|
|
780
|
-
[RECORD_BYTES_WITHHELD]: "incomplete",
|
|
781
|
-
[RECORD_BYTES_UNREADABLE]: "malformed",
|
|
782
|
-
[PACK_DECLARATION_DEVIATES]: "failed",
|
|
783
|
-
[PACK_DECLARATION_CONTENT_DEPENDENT]: "incomplete",
|
|
784
|
-
[PACK_DECLARATION_UNREADABLE]: "incomplete",
|
|
785
|
-
[PACK_UNDECLARED]: "partial",
|
|
786
|
-
[PACK_BODY_TYPE_UNKNOWN]: "incomplete",
|
|
787
|
-
[PACK_HOLDS_NO_RECORDS]: "incomplete",
|
|
788
|
-
[PACK_KEY_UNREADABLE]: "incomplete",
|
|
789
|
-
[PACK_DOES_NOT_VERIFY]: "failed",
|
|
790
|
-
[PACK_GRADE_NOT_VERIFIED]: null,
|
|
791
|
-
[PACK_BUNDLE_CONTENT_DEPENDENT]: "incomplete",
|
|
792
|
-
[RECORD_ENVELOPE_DOES_NOT_VERIFY]: "failed",
|
|
793
|
-
[RECORD_DOES_NOT_VERIFY_IN_ITS_PACK]: "failed",
|
|
794
|
-
[RECORD_CONTENT_MISMATCH]: "failed",
|
|
795
|
-
[RECORD_CONTENT_DEPENDENT]: "incomplete",
|
|
796
|
-
[RECORD_PROFILE_QUARANTINED]: "incomplete",
|
|
797
|
-
[CASE_HOLDS_NO_PACKS]: "partial",
|
|
798
|
-
};
|
|
799
|
-
/**
|
|
800
|
-
* The case grades `PACK_GRADE_NOT_VERIFIED` contributes as themselves. Every
|
|
801
|
-
* other grade a pack can reach (`pending`, `not_found`, `mixed`, which a pack
|
|
802
|
-
* grade passes through and a case verdict never issues) is `incomplete`.
|
|
345
|
+
* What each exception kind contributes to the case verdict, read off the claim it maps to
|
|
346
|
+
* (`KIND_CLAIMS` in `claims.ts`): a disproof is `failed`, anything not proven
|
|
347
|
+
* `incomplete`. A kind with no mapping throws rather than reading as a quiet `verified`.
|
|
348
|
+
* `null` marks `PACK_GRADE_NOT_VERIFIED`, which contributes the pack's case grade from
|
|
349
|
+
* `status`.
|
|
803
350
|
*/
|
|
804
|
-
const
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
351
|
+
const EXCEPTION_VERDICT = Object.fromEntries(CASE_EXCEPTION_KINDS.map((kind) => {
|
|
352
|
+
if (kind === PACK_GRADE_NOT_VERIFIED)
|
|
353
|
+
return [kind, null];
|
|
354
|
+
const mapping = KIND_CLAIMS[kind];
|
|
355
|
+
if (mapping === undefined)
|
|
356
|
+
throw new Error(`KeyError: '${kind}'`);
|
|
357
|
+
return [kind, mapping[1] === "disproven" ? "failed" : "incomplete"];
|
|
358
|
+
}));
|
|
811
359
|
const bodyTypeOf = (bundle) => typeof bundle === "object" && bundle !== null
|
|
812
360
|
? bundle["body_type"]
|
|
813
361
|
: undefined;
|
|
814
362
|
/**
|
|
815
|
-
* The key that
|
|
816
|
-
*
|
|
817
|
-
*
|
|
818
|
-
* The wrapper signature covers `body_hash` and `body` carries the
|
|
819
|
-
* `declaration` (`evidence-bundle.md` 5.1, 5.4.1), so this key is the party
|
|
820
|
-
* whose signed account of itself this pack is. Read from the ENVELOPE of the
|
|
821
|
-
* pack and never from `issuer`: `issuer` is a label and DL-60 keeps it out of
|
|
822
|
-
* every reading, while `public_key` is the value the signature binds.
|
|
823
|
-
*
|
|
824
|
-
* It means something only for a pack whose wrapper HOLDS, which is why
|
|
825
|
-
* `readCase` counts it only after its own `verifyEvidenceBundle` call over the
|
|
826
|
-
* pack returned `signatureOk` and `bodyHashOk`.
|
|
363
|
+
* The key that signed the pack wrapper, and so the pack's declaration
|
|
364
|
+
* (evidence-bundle §5.1, §5.4.1). Read from `public_key`, never `issuer`, which
|
|
365
|
+
* is a label. Meaningful only once the wrapper is verified to hold.
|
|
827
366
|
*/
|
|
828
367
|
const holderKeyOf = (bundle) => typeof bundle === "object" && bundle !== null
|
|
829
368
|
? bundle["public_key"]
|
|
830
369
|
: undefined;
|
|
831
370
|
/**
|
|
832
|
-
* Provenance
|
|
833
|
-
*
|
|
834
|
-
*
|
|
835
|
-
*/
|
|
836
|
-
const wrapperHolds = (result) => result.signatureOk && result.bodyHashOk;
|
|
837
|
-
/**
|
|
838
|
-
* Why the pack reads what it reads, as the verifier said it: its bundle errors,
|
|
839
|
-
* then each stamp's, then each checkpoint's, then each event's behind the
|
|
840
|
-
* event's own id.
|
|
841
|
-
*
|
|
842
|
-
* The event errors are here as well as on the records' own exceptions on
|
|
843
|
-
* purpose. A pack whose only defect is one record reads `failed` with no
|
|
844
|
-
* bundle-level error at all, and an exception that named the verdict and no
|
|
845
|
-
* reason would send a reader looking for a defect in the pack as a whole.
|
|
371
|
+
* Provenance only: the wrapper signature verified and `body_hash` matches the
|
|
372
|
+
* body (evidence-bundle §5.5.1). Under a version this verifier does not know, the
|
|
373
|
+
* signature alone ties the pack to its key, and the body is not read.
|
|
846
374
|
*/
|
|
375
|
+
const wrapperHolds = (result) => result.signatureOk && (result.bodyHashOk || result.versionUnknown === true);
|
|
847
376
|
/**
|
|
848
|
-
*
|
|
849
|
-
*
|
|
850
|
-
* a signature covers.
|
|
377
|
+
* Reasons present only with the side map: what the content-dependent kinds are
|
|
378
|
+
* about, and nothing a signature covers.
|
|
851
379
|
*/
|
|
852
380
|
const onlyIn = (withMap, withoutMap) => withMap.filter((reason) => !withoutMap.includes(reason));
|
|
853
|
-
/**
|
|
854
|
-
* Whether a side-content map holds any key. Python's `if pack.content` is
|
|
855
|
-
* the twin: `None` and an empty map both mean the two verify runs are one run.
|
|
856
|
-
*/
|
|
381
|
+
/** `null`, `undefined` and an empty map all mean the two verify runs are one. */
|
|
857
382
|
function sideMapHasKeys(content) {
|
|
858
383
|
if (content === null || content === undefined)
|
|
859
384
|
return false;
|
|
@@ -861,6 +386,11 @@ function sideMapHasKeys(content) {
|
|
|
861
386
|
return content.size > 0;
|
|
862
387
|
return Object.keys(content).length > 0;
|
|
863
388
|
}
|
|
389
|
+
/**
|
|
390
|
+
* The verifier's reasons for a pack: bundle, then stamps, checkpoints and
|
|
391
|
+
* events (prefixed with the event id). Event errors are included because a pack
|
|
392
|
+
* whose only defect is one record has no bundle-level error.
|
|
393
|
+
*/
|
|
864
394
|
const bundleReasons = (result) => [
|
|
865
395
|
...result.errors,
|
|
866
396
|
...(result.stamps ?? []).flatMap((stamp) => stamp.errors),
|
|
@@ -868,9 +398,8 @@ const bundleReasons = (result) => [
|
|
|
868
398
|
...result.events.flatMap((ev) => ev.errors.map((e) => `${ev.eventId}: ${e}`)),
|
|
869
399
|
];
|
|
870
400
|
/**
|
|
871
|
-
* The `spec_version` of
|
|
872
|
-
*
|
|
873
|
-
* `index` is the same record in both.
|
|
401
|
+
* The `spec_version` of `body.events[index]`'s envelope. `packRecords` and the
|
|
402
|
+
* verifier walk events in the same order.
|
|
874
403
|
*/
|
|
875
404
|
function recordSpecVersion(bundle, index) {
|
|
876
405
|
const at = (v, k) => typeof v === "object" && v !== null ? v[k] : undefined;
|
|
@@ -878,8 +407,8 @@ function recordSpecVersion(bundle, index) {
|
|
|
878
407
|
return Array.isArray(events) ? at(at(events[index], "envelope"), "spec_version") : undefined;
|
|
879
408
|
}
|
|
880
409
|
/**
|
|
881
|
-
* `specVersion` is
|
|
882
|
-
*
|
|
410
|
+
* Whether `specVersion` is quarantined, on the token boundary
|
|
411
|
+
* `specVersionSupported` uses.
|
|
883
412
|
*/
|
|
884
413
|
function underQuarantine(specVersion) {
|
|
885
414
|
if (typeof specVersion !== "string")
|
|
@@ -894,96 +423,235 @@ function underQuarantine(specVersion) {
|
|
|
894
423
|
});
|
|
895
424
|
}
|
|
896
425
|
/**
|
|
897
|
-
* The case verdict, from the contributions alone
|
|
898
|
-
*
|
|
899
|
-
*
|
|
900
|
-
* A17, proof#158): if any contribution is `failed`, the case is `failed`,
|
|
901
|
-
* whatever else contributes, and every other exception stays named beside it.
|
|
902
|
-
* Otherwise one outcome is that outcome, and more than one is `mixed`
|
|
903
|
-
* (anchor#198, criterion 4), so `mixed` never holds a `failed`. `malformed`
|
|
904
|
-
* does NOT keep the headline: it says a record cannot be read, not that a
|
|
905
|
-
* check ran and did not pass. `mixed` is in the 3 vocabulary and coins no word.
|
|
426
|
+
* The case verdict, from the contributions alone: `failed` where any contribution is,
|
|
427
|
+
* else `incomplete` where any is, else `verified`. A case reads its worst pack, and every
|
|
428
|
+
* exception is still named beside it.
|
|
906
429
|
*
|
|
907
|
-
*
|
|
908
|
-
*
|
|
909
|
-
* non-`verified` contribution (`EXCEPTION_VERDICT`), so this falls out of the
|
|
910
|
-
* table rather than being enforced by a second rule that could drift from it.
|
|
911
|
-
* The assertion is here anyway, because that is the property every one of the
|
|
912
|
-
* seventeen findings on this program violated.
|
|
430
|
+
* Invariant: `verified` only when `exceptions` is empty. It follows from
|
|
431
|
+
* `EXCEPTION_VERDICT` and is asserted anyway.
|
|
913
432
|
*/
|
|
914
433
|
function verdictOf(exceptions, contributions, holdsPacks = true) {
|
|
915
|
-
//
|
|
916
|
-
//
|
|
917
|
-
// nothing in it was an exception. Every exception contributes, so no
|
|
918
|
-
// contribution means no exception. A reading with no packs always carries
|
|
919
|
-
// `case_holds_no_packs`, and is defended here anyway: an empty aggregate over
|
|
920
|
-
// nothing reading as `verified` is the defect class itself.
|
|
434
|
+
// No contribution means every pack graded `verified` and no exception. A
|
|
435
|
+
// reading with no packs is `incomplete`, never `verified` over nothing.
|
|
921
436
|
if (contributions.length === 0) {
|
|
922
437
|
if (exceptions.length > 0) {
|
|
923
438
|
throw new Error(`exceptions with no contribution: ${exceptions.map((e) => e.kind).join(", ")}`);
|
|
924
439
|
}
|
|
925
|
-
return holdsPacks ? "verified" : "
|
|
440
|
+
return holdsPacks ? "verified" : "incomplete";
|
|
926
441
|
}
|
|
927
442
|
const outcomes = new Set(contributions.map(([, v]) => v));
|
|
928
443
|
const verdict = outcomes.has("failed")
|
|
929
444
|
? "failed"
|
|
930
|
-
: outcomes.
|
|
931
|
-
?
|
|
932
|
-
: "
|
|
445
|
+
: outcomes.has("incomplete")
|
|
446
|
+
? "incomplete"
|
|
447
|
+
: "verified";
|
|
933
448
|
if (verdict === "verified" && exceptions.length > 0) {
|
|
934
449
|
throw new Error(`verified over a non-empty exception list: ${exceptions.map((e) => e.kind).join(", ")}`);
|
|
935
450
|
}
|
|
936
451
|
return verdict;
|
|
937
452
|
}
|
|
453
|
+
function at(pack, record = null) {
|
|
454
|
+
return [[pack, record]];
|
|
455
|
+
}
|
|
456
|
+
// The envelope faults a record's pack already names with their own claim, so the case does
|
|
457
|
+
// not also call the envelope one whose signature fails.
|
|
458
|
+
const ENVELOPE_FAULTS_OF_THE_PACK = [
|
|
459
|
+
"payload_unbound",
|
|
460
|
+
"not_an_envelope",
|
|
461
|
+
"key_unreadable",
|
|
462
|
+
"spec_version_unknown",
|
|
463
|
+
"nonconforming",
|
|
464
|
+
];
|
|
465
|
+
/** A pack's own problems (read at position 0) at its position in the case. */
|
|
466
|
+
function moved(problems, position) {
|
|
467
|
+
return problems.map((p) => withItems(p, p.items.map(([, ...rest]) => [position, ...rest])));
|
|
468
|
+
}
|
|
938
469
|
/**
|
|
939
|
-
*
|
|
940
|
-
*
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
470
|
+
* Every exception, mapped to its claim and state, in exception order. A pack grade
|
|
471
|
+
* carries its own problems; a pack grade with none (a defence) reads as its status.
|
|
472
|
+
*/
|
|
473
|
+
export function caseProblems(exceptions) {
|
|
474
|
+
const out = [];
|
|
475
|
+
for (const e of exceptions) {
|
|
476
|
+
let items;
|
|
477
|
+
if (e.recordIndex !== undefined)
|
|
478
|
+
items = at(e.packIndex ?? null, e.recordIndex);
|
|
479
|
+
else if (e.packIndex !== undefined)
|
|
480
|
+
items = at(e.packIndex);
|
|
481
|
+
else
|
|
482
|
+
items = at(null);
|
|
483
|
+
if (e.kind === PACK_GRADE_NOT_VERIFIED) {
|
|
484
|
+
if (e.problems !== undefined && e.problems.length > 0)
|
|
485
|
+
out.push(...e.problems);
|
|
486
|
+
else if (e.status === "failed")
|
|
487
|
+
out.push(problem("bundle_does_not_verify", items));
|
|
488
|
+
else
|
|
489
|
+
out.push(problem("bundle_not_checked", items));
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
if (e.kind === PAIR_FAILED && e.failedChecks !== undefined && e.failedChecks.length > 0) {
|
|
493
|
+
// One problem per check that failed, so each is said in its own words.
|
|
494
|
+
for (const c of e.failedChecks)
|
|
495
|
+
out.push(problem(e.kind, items, { rule: c }));
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
out.push(problem(e.kind, items, {
|
|
499
|
+
key: e.kind.startsWith("counterpart_absent") ? (e.counterpartyKey ?? null) : null,
|
|
500
|
+
}));
|
|
501
|
+
}
|
|
502
|
+
return out;
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* The reading, with its problems and the verdict they give, which must be the verdict
|
|
506
|
+
* the contributions give: two derivations of one answer, held to agree.
|
|
507
|
+
*/
|
|
508
|
+
function withProblems(reading) {
|
|
509
|
+
const problems = caseProblems(reading.exceptions);
|
|
510
|
+
const outcome = claimsVerdictOf(claimsOf(problems));
|
|
511
|
+
if (outcome.status !== reading.verdict) {
|
|
512
|
+
throw new Error(`two derivations of one verdict disagree: ${outcome.status} and ${reading.verdict}`);
|
|
513
|
+
}
|
|
514
|
+
return {
|
|
515
|
+
...reading,
|
|
516
|
+
...(outcome.reason_kind === undefined ? {} : { reasonKind: outcome.reason_kind }),
|
|
517
|
+
problems,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* The pack's grade, read as `verify --logs` reads it: no disproof rests on bytes nobody
|
|
522
|
+
* signed.
|
|
523
|
+
*/
|
|
524
|
+
async function gradeInCase(pack, verified, signed, position) {
|
|
525
|
+
const bodyType = bodyTypeOf(pack.bundle);
|
|
526
|
+
const records = packRecords(pack.bundle, pack.content);
|
|
527
|
+
// Per-record gradings are joined to `records` by index. A pack whose
|
|
528
|
+
// `body_hash` does not cover its body reads `failed` with no events, so a
|
|
529
|
+
// count mismatch is graded, never indexed into.
|
|
530
|
+
const ungraded = verified.events.length !== records.length || signed.events.length !== records.length;
|
|
531
|
+
const knownBody = CASE_BODY_TYPES.includes(bodyType);
|
|
532
|
+
// The pack's grade propagates: the case never reads above its weakest pack.
|
|
533
|
+
let check = null;
|
|
534
|
+
let full = null;
|
|
535
|
+
let causes = [];
|
|
536
|
+
let grade;
|
|
537
|
+
let packProblems = bundleProblems(signed, position);
|
|
538
|
+
if (signed.unreadable === true) {
|
|
539
|
+
// A body that cannot be read under a wrapper whose time breaks its format fails.
|
|
540
|
+
grade = signed.status === "failed" ? "failed" : "incomplete";
|
|
541
|
+
}
|
|
542
|
+
else if (!knownBody) {
|
|
543
|
+
grade = signed.status;
|
|
544
|
+
}
|
|
545
|
+
else if (ungraded) {
|
|
546
|
+
grade = signed.status === "verified" ? "incomplete" : signed.status;
|
|
547
|
+
if (signed.status === "verified")
|
|
548
|
+
packProblems = [problem("bundle_not_checked", at(position))];
|
|
549
|
+
}
|
|
550
|
+
else {
|
|
551
|
+
// The declaration's chain rule reads the same signed chain links for both
|
|
552
|
+
// checks (evidence-bundle §5.4.2).
|
|
553
|
+
const chainLinks = await readChainLinks(pack.bundle);
|
|
554
|
+
check = checkDeclaration(pack.bundle, pack.content, chainLinks);
|
|
555
|
+
const completeness = completenessNotes(pack.bundle);
|
|
556
|
+
full = gradePack(verified, accountDisclosure(pack.bundle, pack.content ?? {}), completeness, check);
|
|
557
|
+
const bareGrade = gradePack(signed, accountDisclosure(pack.bundle, {}), completeness, checkDeclaration(pack.bundle, null, chainLinks));
|
|
558
|
+
// No disproof here rests on unsigned bytes: each disproof the pack read with the
|
|
559
|
+
// bytes beside it makes and the signed reading does not is not proven, as `verify`
|
|
560
|
+
// reads it, and everything else is the reading with those bytes, so a pack's logs/
|
|
561
|
+
// count in `case` as they do in `verify --logs`.
|
|
562
|
+
packProblems = moved(restOnSigned(full.problems, bareGrade.problems), position);
|
|
563
|
+
grade = claimsVerdictOf(claimsOf(packProblems)).status;
|
|
564
|
+
causes = full.causes.map((c) => c.kind);
|
|
565
|
+
}
|
|
566
|
+
return { grade, problems: packProblems, causes, check, full, records, ungraded, knownBody };
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Each pack that reads as a pack, verified twice: with the side map, and with
|
|
570
|
+
* it empty. What fails only in the first rests on bytes nobody
|
|
571
|
+
* signed. One run where there is no map. Offline, no gateways.
|
|
572
|
+
*/
|
|
573
|
+
async function verifyPacks(packs) {
|
|
574
|
+
const runs = new Map();
|
|
575
|
+
for (const [position, pack] of packs.entries()) {
|
|
576
|
+
if (typeof pack.unreadable === "string")
|
|
577
|
+
continue;
|
|
578
|
+
const verified = await verifyEvidenceBundle(pack.bundle, {
|
|
579
|
+
content: (pack.content ?? undefined),
|
|
580
|
+
});
|
|
581
|
+
const signed = sideMapHasKeys(pack.content)
|
|
582
|
+
? await verifyEvidenceBundle(pack.bundle, {})
|
|
583
|
+
: verified;
|
|
584
|
+
runs.set(position, [verified, signed]);
|
|
585
|
+
}
|
|
586
|
+
return runs;
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Read the agent records of every pack as one pile and put each reading on
|
|
590
|
+
* both runs of its pack, in place of the reading over its own pack alone
|
|
591
|
+
* (agent-events.md section 10 step 7): pairing, chains and closing lists span packs.
|
|
592
|
+
* A record takes part where its pack's wrapper holds, its pack's records are
|
|
593
|
+
* all graded, and its envelope verified with its record bytes bound to it.
|
|
594
|
+
*/
|
|
595
|
+
async function readAgentRecordsAcross(packs, runs) {
|
|
596
|
+
const items = [];
|
|
597
|
+
const where = [];
|
|
598
|
+
for (const [position, [verified, signed]] of runs) {
|
|
599
|
+
if (!wrapperHolds(verified))
|
|
600
|
+
continue;
|
|
601
|
+
const records = packRecords(packs[position].bundle, packs[position].content);
|
|
602
|
+
if (verified.events.length !== records.length || signed.events.length !== records.length)
|
|
603
|
+
continue;
|
|
604
|
+
for (const [index, record] of records.entries()) {
|
|
605
|
+
const graded = verified.events[index];
|
|
606
|
+
graded.agent = null;
|
|
607
|
+
signed.events[index].agent = null;
|
|
608
|
+
if (graded.envelopeOk && graded.payloadBindingOk === true && record.record !== null) {
|
|
609
|
+
items.push({ event_id: record.eventId, public_key: record.publicKey, record: record.record });
|
|
610
|
+
where.push([position, index]);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
const readings = await readAgentRecords(items);
|
|
615
|
+
for (const [i, [position, index]] of where.entries()) {
|
|
616
|
+
const [verified, signed] = runs.get(position);
|
|
617
|
+
verified.events[index].agent = readings[i];
|
|
618
|
+
signed.events[index].agent = readings[i];
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Reads a case: packs in, one verdict and its exceptions out.
|
|
623
|
+
*
|
|
624
|
+
* 1. Each pack is read into records (`packRecords`); an unknown body or no
|
|
625
|
+
* records is named.
|
|
626
|
+
* 2. Each pack is graded against its own declaration (`checkDeclaration`).
|
|
627
|
+
* 3. Every record goes into one pile, unpartitioned, read by `readHandoffs`.
|
|
628
|
+
* 4. Each reading becomes an exception or a clean contribution. The
|
|
629
|
+
* counterpart kinds are told apart by whether a pack signed by the
|
|
630
|
+
* counterparty's key is in the case, read from every wrapper before any exit.
|
|
631
|
+
*
|
|
632
|
+
* Before step 1 the agent records of every pack are read as one pile
|
|
633
|
+
* (agent-events.md section 10), and each pack is graded with those readings
|
|
634
|
+
* (evidence-bundle.md section 5.4.5, section 5.5.1).
|
|
964
635
|
*/
|
|
965
636
|
export async function readCase(packs) {
|
|
966
|
-
// A `null` slot is held for each pack
|
|
967
|
-
//
|
|
637
|
+
// A `null` slot is held for each undeclared pack until the pile says whether
|
|
638
|
+
// the reading holds a hand-off record.
|
|
968
639
|
const exceptions = [];
|
|
969
640
|
const contributions = [];
|
|
970
641
|
const note = (kind, fields = {}) => {
|
|
971
642
|
exceptions.push({ kind, ...fields });
|
|
972
|
-
// `null` is PACK_GRADE_NOT_VERIFIED: the pack's case grade, never
|
|
973
|
-
// verified
|
|
974
|
-
// case verdict never issues (5.5.1 step 1 v1.20).
|
|
643
|
+
// `null` is PACK_GRADE_NOT_VERIFIED: contribute the pack's case grade, never
|
|
644
|
+
// verified (section 5.5.1 step 1).
|
|
975
645
|
let contribution = EXCEPTION_VERDICT[kind];
|
|
976
|
-
if (contribution === null)
|
|
977
|
-
|
|
978
|
-
contribution = GRADE_CONTRIBUTES.has(status) ? status : "incomplete";
|
|
979
|
-
}
|
|
646
|
+
if (contribution === null)
|
|
647
|
+
contribution = fields.status;
|
|
980
648
|
contributions.push([fields.eventId ?? fields.packRef ?? "case", contribution]);
|
|
981
649
|
};
|
|
982
650
|
if (packs.length === 0) {
|
|
983
651
|
note(CASE_HOLDS_NO_PACKS);
|
|
984
652
|
const named = exceptions;
|
|
985
653
|
const contributed = contributions;
|
|
986
|
-
return {
|
|
654
|
+
return withProblems({
|
|
987
655
|
verdict: verdictOf(named, contributed, false),
|
|
988
656
|
exceptions: named,
|
|
989
657
|
records: [],
|
|
@@ -992,85 +660,81 @@ export async function readCase(packs) {
|
|
|
992
660
|
onChainChecked: false,
|
|
993
661
|
notes: [],
|
|
994
662
|
packs: [],
|
|
995
|
-
};
|
|
663
|
+
});
|
|
996
664
|
}
|
|
997
|
-
//
|
|
998
|
-
// Whether every pack's witness was re-fetched on chain, ALL over the packs
|
|
999
|
-
// this reader verified. It passes no gateways, so this stays false and the
|
|
1000
|
-
// reading says so (see `CaseReading.onChainChecked`).
|
|
665
|
+
// Steps 1 and 2, pack by pack. No gateways, so `witnessChecked` stays false.
|
|
1001
666
|
const witnessChecked = [];
|
|
1002
667
|
const held = [];
|
|
1003
|
-
//
|
|
1004
|
-
//
|
|
1005
|
-
// human to read; a join rebuilt by splitting one is a join on a value a
|
|
1006
|
-
// holder can put a separator in.
|
|
668
|
+
// Pack and `event_id` per pile member, kept beside the pile rather than
|
|
669
|
+
// parsed from a reference string a holder could put a separator in.
|
|
1007
670
|
const provenance = [];
|
|
1008
|
-
// Every record of every pack, in
|
|
1009
|
-
//
|
|
1010
|
-
// kind of the exception that says why. Nothing is listed only where the pack
|
|
1011
|
-
// body carries no `events` array, and that pack's own exception says why.
|
|
671
|
+
// Every record of every pack, in order: its pile position, or the exception
|
|
672
|
+
// kind that kept it out.
|
|
1012
673
|
const slots = [];
|
|
1013
674
|
const unread = (position, packRef, listed, reason) => {
|
|
1014
675
|
listed.forEach((record, recordIndex) => slots.push({ position, packRef, recordIndex, eventId: record.eventId, heldAt: null, reason }));
|
|
1015
676
|
};
|
|
1016
|
-
//
|
|
1017
|
-
//
|
|
1018
|
-
// `event_id` alone, so under an id the pack carries twice every record with
|
|
1019
|
-
// that id is named: failing closed, rather than guessing which of the two the
|
|
1020
|
-
// check meant.
|
|
677
|
+
// Declaration-check findings per pack position, then `eventId`. A duplicated
|
|
678
|
+
// id names every record with it: fail closed rather than guess.
|
|
1021
679
|
const declarationFindings = new Map();
|
|
1022
|
-
// Keyed by
|
|
1023
|
-
//
|
|
1024
|
-
// the same one: keyed by ref, the second pack's claims silently replaced the
|
|
1025
|
-
// first's and records from one pack were graded against another pack's
|
|
1026
|
-
// declaration. Found by a constructed archive rather than by reading, which
|
|
1027
|
-
// is the only way this class of defect is ever found.
|
|
680
|
+
// Keyed by pack position, never `ref`: two packs can carry one ref, and a
|
|
681
|
+
// record must never be graded against another pack's declaration.
|
|
1028
682
|
const claimsByPack = new Map();
|
|
1029
|
-
// Every key that
|
|
1030
|
-
//
|
|
1031
|
-
//
|
|
1032
|
-
//
|
|
1033
|
-
// case.
|
|
1034
|
-
//
|
|
1035
|
-
// The record-key set was wrong twice over, and both times in the direction
|
|
1036
|
-
// that matters. It let the ACCUSER manufacture the damning line: one pack,
|
|
1037
|
-
// plus one unrelated record the counterparty signed at some other time, read
|
|
1038
|
-
// "the counterparty's OWN records ARE here" when the counterparty had handed
|
|
1039
|
-
// over nothing (F4). And it let the COUNTERPARTY choose the innocent line: it
|
|
1040
|
-
// was filled inside the record loop, which the `body_type` and no-records
|
|
1041
|
-
// exits `continue` past, so a counterparty that wrapped its own pack in a body
|
|
1042
|
-
// this reader does not read took its keys out of the set (F3), the third
|
|
1043
|
-
// instance of a route already closed once for withheld `event_bytes`.
|
|
1044
|
-
//
|
|
1045
|
-
// A wrapper key closes both. The accuser cannot forge one without the
|
|
1046
|
-
// counterparty's private key, and it is read HERE, first, before any exit
|
|
1047
|
-
// below, from the pack envelope and never from the body, so no body type, no
|
|
1048
|
-
// disclosure choice and no record content decides whether it is read.
|
|
683
|
+
// Every key that signed a pack wrapper (and so its declaration): the one
|
|
684
|
+
// input that separates the counterpart kinds. Wrapper keys, not record keys:
|
|
685
|
+
// an accuser cannot forge one, and it is read before any exit, from the
|
|
686
|
+
// envelope, so no body type or disclosure choice decides whether it counts.
|
|
1049
687
|
const holderKeys = new Set();
|
|
1050
|
-
// One reading per pack, in the order the reader was given them (5.5.4 v1.20).
|
|
1051
688
|
const packReadings = [];
|
|
1052
|
-
// The NOTEs (5.5.3 v1.20), with the pack position they are about.
|
|
1053
689
|
const caseNotes = [];
|
|
1054
|
-
//
|
|
1055
|
-
// by the pack's POSITION for the reason `claimsByPack` is.
|
|
690
|
+
// Keyed by pack position, as `claimsByPack` is.
|
|
1056
691
|
const rolesByPack = new Map();
|
|
1057
|
-
//
|
|
692
|
+
// Undeclared packs' held slots, settled after the pile is read.
|
|
1058
693
|
const undeclaredSlots = [];
|
|
1059
|
-
//
|
|
1060
|
-
//
|
|
1061
|
-
// case is signed by the named key" is not established, so the innocent
|
|
1062
|
-
// one-sided kind is not available (COUNTERPART_ABSENT_SIGNER_UNKNOWN).
|
|
694
|
+
// Packs not tied to a key (unreadable key, or wrapper does not hold). While
|
|
695
|
+
// non-zero, the innocent `COUNTERPART_ABSENT` is not established.
|
|
1063
696
|
let unattributed = 0;
|
|
697
|
+
const runs = await verifyPacks(packs);
|
|
698
|
+
await readAgentRecordsAcross(packs, runs);
|
|
699
|
+
// The agent reading of each record that went into the pile, by pack and record position.
|
|
700
|
+
const agentReadings = new Map();
|
|
1064
701
|
for (let position = 0; position < packs.length; position += 1) {
|
|
1065
702
|
const pack = packs[position];
|
|
703
|
+
if (typeof pack.unreadable === "string") {
|
|
704
|
+
note(PACK_UNREADABLE, { packRef: pack.ref, packIndex: position, reasons: [pack.unreadable] });
|
|
705
|
+
unattributed += 1;
|
|
706
|
+
packReadings.push({
|
|
707
|
+
index: position,
|
|
708
|
+
ref: pack.ref,
|
|
709
|
+
signingKey: null,
|
|
710
|
+
grade: null,
|
|
711
|
+
recordsWithheld: 0,
|
|
712
|
+
declarationVersion: null,
|
|
713
|
+
});
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
1066
716
|
const holder = normalizedSigningKey(holderKeyOf(pack.bundle));
|
|
1067
717
|
if (holder === null) {
|
|
1068
|
-
//
|
|
1069
|
-
//
|
|
1070
|
-
// BEFORE verification, because the two bundle verifiers disagree on some
|
|
1071
|
-
// of these spellings (proof#135) and the reading must not.
|
|
718
|
+
// Signer unknown, not unsigned. Checked before verification so verifier
|
|
719
|
+
// differences over key spelling cannot change the reading.
|
|
1072
720
|
note(PACK_KEY_UNREADABLE, { packRef: pack.ref, packIndex: position });
|
|
1073
721
|
unattributed += 1;
|
|
722
|
+
// `signed` is not proven on that claim only: a pack that can be read is still graded,
|
|
723
|
+
// so a forged record inside it, or a `body_hash` that does not hold, fails it. Its
|
|
724
|
+
// records do not join the pile, since no key ties them to a party.
|
|
725
|
+
const [keyless, keylessSigned] = runs.get(position);
|
|
726
|
+
const keylessGrade = await gradeInCase(pack, keyless, keylessSigned, position);
|
|
727
|
+
const inside = keylessGrade.problems.filter((p) => p.kind !== PACK_KEY_UNREADABLE);
|
|
728
|
+
if (inside.length > 0 && keylessSigned.unreadable !== true) {
|
|
729
|
+
note(PACK_GRADE_NOT_VERIFIED, {
|
|
730
|
+
packRef: pack.ref,
|
|
731
|
+
packIndex: position,
|
|
732
|
+
status: claimsVerdictOf(claimsOf(inside)).status,
|
|
733
|
+
causes: keylessGrade.causes,
|
|
734
|
+
reasons: bundleReasons(keylessSigned),
|
|
735
|
+
problems: inside,
|
|
736
|
+
});
|
|
737
|
+
}
|
|
1074
738
|
unread(position, pack.ref, listedRecords(pack.bundle, pack.content), PACK_KEY_UNREADABLE);
|
|
1075
739
|
packReadings.push({
|
|
1076
740
|
index: position,
|
|
@@ -1082,23 +746,9 @@ export async function readCase(packs) {
|
|
|
1082
746
|
});
|
|
1083
747
|
continue;
|
|
1084
748
|
}
|
|
1085
|
-
|
|
1086
|
-
// caller: every key this module compares is an envelope `public_key`, and
|
|
1087
|
-
// "unforgeable" is a property of a VERIFIED envelope. Offline, no
|
|
1088
|
-
// gateways: this reader makes no network call. This kernel's verifier
|
|
1089
|
-
// reads envelopes under exactly CASE_RECORD_PROFILES (pinned by a test).
|
|
1090
|
-
const verified = await verifyEvidenceBundle(pack.bundle, {
|
|
1091
|
-
content: (pack.content ?? undefined),
|
|
1092
|
-
});
|
|
749
|
+
const [verified, signed] = runs.get(position);
|
|
1093
750
|
witnessChecked.push(verified.onChainChecked);
|
|
1094
|
-
//
|
|
1095
|
-
// what a signature covers, and what fails only above rests on bytes nobody
|
|
1096
|
-
// signed. Skipped where there is no map, because the two runs are then the
|
|
1097
|
-
// same run.
|
|
1098
|
-
const signed = sideMapHasKeys(pack.content)
|
|
1099
|
-
? await verifyEvidenceBundle(pack.bundle, {})
|
|
1100
|
-
: verified;
|
|
1101
|
-
// WHOSE PACK IS IT. The wrapper, and only the wrapper.
|
|
751
|
+
// Whose pack it is: the wrapper, and only the wrapper.
|
|
1102
752
|
if (!wrapperHolds(verified)) {
|
|
1103
753
|
note(PACK_DOES_NOT_VERIFY, { packRef: pack.ref, packIndex: position });
|
|
1104
754
|
unattributed += 1;
|
|
@@ -1115,74 +765,19 @@ export async function readCase(packs) {
|
|
|
1115
765
|
}
|
|
1116
766
|
holderKeys.add(holder);
|
|
1117
767
|
if (coSignaturesPresent(pack.bundle)) {
|
|
1118
|
-
// A
|
|
1119
|
-
// co_signatures verifies nothing and sits outside its signed scope, so
|
|
1120
|
-
// the case reading names it rather than moving anything on its account.
|
|
768
|
+
// A note, no contribution: `co_signatures` verifies nothing.
|
|
1121
769
|
caseNotes.push({
|
|
1122
770
|
position,
|
|
1123
771
|
note: { kind: CO_SIGNATURES_PRESENT, packRef: pack.ref, packIndex: position },
|
|
1124
772
|
});
|
|
1125
773
|
}
|
|
1126
|
-
const
|
|
1127
|
-
const records =
|
|
1128
|
-
|
|
1129
|
-
// verifier that graded a different number of records than the pack holds
|
|
1130
|
-
// leaves this loop with no grading to read. That is not hypothetical: a
|
|
1131
|
-
// pack whose `body_hash` does not cover its body reads `failed` with
|
|
1132
|
-
// `events` EMPTY while `packRecords` reads every record in it, and only
|
|
1133
|
-
// `wrapperHolds` above stands between that pack and this loop. A reader
|
|
1134
|
-
// must never crash on a pack a party can build, so the gap is graded rather
|
|
1135
|
-
// than indexed into, and the pack's records are dropped exactly as a
|
|
1136
|
-
// `malformed` pack's are (gate 4 on proof#140, finding 3).
|
|
1137
|
-
const ungraded = verified.events.length !== records.length || signed.events.length !== records.length;
|
|
1138
|
-
const knownBody = CASE_BODY_TYPES.includes(bodyType);
|
|
1139
|
-
// WHAT IS WRONG INSIDE IT, from here on. First the pack's own GRADE, which
|
|
1140
|
-
// propagates: the case never reads above its weakest pack (5.5.1 step 1
|
|
1141
|
-
// v1.20, A23; up to v1.19 it was the bundle status).
|
|
1142
|
-
let check = null;
|
|
1143
|
-
let full = null;
|
|
1144
|
-
let causes = [];
|
|
1145
|
-
let grade;
|
|
1146
|
-
if (signed.status === "malformed") {
|
|
1147
|
-
grade = "malformed";
|
|
1148
|
-
}
|
|
1149
|
-
else if (!knownBody) {
|
|
1150
|
-
grade = signed.status;
|
|
1151
|
-
}
|
|
1152
|
-
else if (ungraded) {
|
|
1153
|
-
grade = signed.status === "verified" ? "malformed" : signed.status;
|
|
1154
|
-
}
|
|
1155
|
-
else {
|
|
1156
|
-
// Rule 11 reads the chain links of the same pack, and the reader reads
|
|
1157
|
-
// only the signed body, so the bare check below takes the same reading
|
|
1158
|
-
// (evidence-bundle.md 5.4.2 v1.36, A45).
|
|
1159
|
-
const chainLinks = await readChainLinks(pack.bundle);
|
|
1160
|
-
check = checkDeclaration(pack.bundle, pack.content, chainLinks);
|
|
1161
|
-
const completeness = completenessNotes(pack.bundle);
|
|
1162
|
-
full = gradePack(verified, accountDisclosure(pack.bundle, pack.content ?? {}), completeness, check);
|
|
1163
|
-
const bareGrade = gradePack(signed, accountDisclosure(pack.bundle, {}), completeness, checkDeclaration(pack.bundle, null, chainLinks));
|
|
1164
|
-
// NO `failed` HERE RESTS ON BYTES NOBODY SIGNED. The signed grade decides
|
|
1165
|
-
// where it is `failed` or `malformed`; a failure only the side map
|
|
1166
|
-
// produced is `incomplete`; otherwise the full grade.
|
|
1167
|
-
if (bareGrade.verdict === "failed" || bareGrade.verdict === "malformed") {
|
|
1168
|
-
grade = bareGrade.verdict;
|
|
1169
|
-
causes = bareGrade.causes.map((c) => c.kind);
|
|
1170
|
-
}
|
|
1171
|
-
else if (full.verdict === "failed" || full.verdict === "malformed") {
|
|
1172
|
-
grade = "incomplete";
|
|
1173
|
-
causes = full.causes.map((c) => c.kind);
|
|
1174
|
-
}
|
|
1175
|
-
else {
|
|
1176
|
-
grade = full.verdict;
|
|
1177
|
-
causes = full.causes.map((c) => c.kind);
|
|
1178
|
-
}
|
|
1179
|
-
}
|
|
774
|
+
const graded = await gradeInCase(pack, verified, signed, position);
|
|
775
|
+
const { grade, check, full, causes, records, ungraded, knownBody } = graded;
|
|
776
|
+
const packProblems = graded.problems;
|
|
1180
777
|
if (grade !== "verified") {
|
|
1181
778
|
const reasons = bundleReasons(signed);
|
|
1182
|
-
if (knownBody && ungraded && signed.
|
|
1183
|
-
//
|
|
1184
|
-
// about the bundle, and this pack's records are dropped for a reason
|
|
1185
|
-
// of this reader's.
|
|
779
|
+
if (knownBody && ungraded && signed.unreadable !== true) {
|
|
780
|
+
// The verifier's reasons are about the bundle; this one is ours.
|
|
1186
781
|
reasons.push(`this verifier graded ${verified.events.length} of the ${records.length} ` +
|
|
1187
782
|
"record(s) the pack holds, so none of them is read");
|
|
1188
783
|
}
|
|
@@ -1192,6 +787,7 @@ export async function readCase(packs) {
|
|
|
1192
787
|
status: grade,
|
|
1193
788
|
causes,
|
|
1194
789
|
reasons,
|
|
790
|
+
problems: packProblems,
|
|
1195
791
|
});
|
|
1196
792
|
}
|
|
1197
793
|
if (verified.status === "failed" && signed.status !== "failed") {
|
|
@@ -1211,9 +807,8 @@ export async function readCase(packs) {
|
|
|
1211
807
|
declarationVersion: knownBody ? declarationVersionOf(pack.bundle) : null,
|
|
1212
808
|
});
|
|
1213
809
|
if (recordsWithheld > 0) {
|
|
1214
|
-
// A
|
|
1215
|
-
//
|
|
1216
|
-
// them, so a clean verdict never hides them.
|
|
810
|
+
// A note, no contribution: the holder signed that this content does not
|
|
811
|
+
// travel, and the scope sentence counts it.
|
|
1217
812
|
caseNotes.push({
|
|
1218
813
|
position,
|
|
1219
814
|
note: {
|
|
@@ -1224,28 +819,20 @@ export async function readCase(packs) {
|
|
|
1224
819
|
},
|
|
1225
820
|
});
|
|
1226
821
|
}
|
|
1227
|
-
// The
|
|
1228
|
-
|
|
1229
|
-
if (signed.status === "malformed") {
|
|
822
|
+
// The verifier graded no record; named above.
|
|
823
|
+
if (signed.unreadable === true) {
|
|
1230
824
|
unread(position, pack.ref, listedRecords(pack.bundle, pack.content), PACK_GRADE_NOT_VERIFIED);
|
|
1231
825
|
continue;
|
|
1232
826
|
}
|
|
1233
827
|
if (!knownBody) {
|
|
1234
|
-
//
|
|
1235
|
-
//
|
|
1236
|
-
// which packs this reader grades, so it is a decision of its own and not
|
|
1237
|
-
// a repair (evidence-bundle.md 5.5.1 step 1 says so too). What this exit
|
|
1238
|
-
// costs is that pack's RECORDS, and it is named every time. It no longer
|
|
1239
|
-
// costs the pack's KEY: that was read above.
|
|
828
|
+
// Not unwrapped (evidence-bundle §5.5.1). Costs the pack's records, never
|
|
829
|
+
// its key, which was read above.
|
|
1240
830
|
note(PACK_BODY_TYPE_UNKNOWN, { packRef: pack.ref, packIndex: position });
|
|
1241
|
-
// Listed,
|
|
1242
|
-
// are listed from its inline source bundle, so a reading never says "0
|
|
1243
|
-
// not read" of a pack that holds records (A43).
|
|
831
|
+
// Listed, not read, so a pack holding records never reads "0 not read".
|
|
1244
832
|
unread(position, pack.ref, listedRecords(pack.bundle, pack.content), PACK_BODY_TYPE_UNKNOWN);
|
|
1245
833
|
continue;
|
|
1246
834
|
}
|
|
1247
|
-
// Graded above
|
|
1248
|
-
// none of its records are read.
|
|
835
|
+
// Graded above; none of its records are read.
|
|
1249
836
|
if (ungraded) {
|
|
1250
837
|
unread(position, pack.ref, records, PACK_GRADE_NOT_VERIFIED);
|
|
1251
838
|
continue;
|
|
@@ -1267,10 +854,8 @@ export async function readCase(packs) {
|
|
|
1267
854
|
}
|
|
1268
855
|
declarationFindings.set(position, namedByCheck);
|
|
1269
856
|
if (checked.contentFindings.length > 0) {
|
|
1270
|
-
//
|
|
1271
|
-
//
|
|
1272
|
-
// one is `incomplete` because whoever assembled the case chose the bytes
|
|
1273
|
-
// it turns on (proof#147).
|
|
857
|
+
// Beside the deviation exception, never instead: they concern different
|
|
858
|
+
// records. `incomplete` because the case assembler chose the bytes.
|
|
1274
859
|
note(PACK_DECLARATION_CONTENT_DEPENDENT, {
|
|
1275
860
|
packRef: pack.ref,
|
|
1276
861
|
packIndex: position,
|
|
@@ -1278,38 +863,31 @@ export async function readCase(packs) {
|
|
|
1278
863
|
});
|
|
1279
864
|
}
|
|
1280
865
|
if (checked.verdict === "failed") {
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
866
|
+
// A declaration that does not parse deviates from nothing: the pack grade names it as
|
|
867
|
+
// a declaration that breaks its own format.
|
|
868
|
+
if (checked.deviations.length === 0 ||
|
|
869
|
+
checked.deviations.some((d) => d.kind !== "declaration_malformed")) {
|
|
870
|
+
note(PACK_DECLARATION_DEVIATES, {
|
|
871
|
+
packRef: pack.ref,
|
|
872
|
+
packIndex: position,
|
|
873
|
+
count: checked.deviations.length,
|
|
874
|
+
});
|
|
875
|
+
}
|
|
1286
876
|
}
|
|
1287
877
|
else if (checked.contentFindings.length > 0) {
|
|
1288
|
-
//
|
|
1289
|
-
// claim": the claim was read, and part of it rests on bytes nobody
|
|
1290
|
-
// signed.
|
|
878
|
+
// Named above: the claim was read, and part of it rests on unsigned bytes.
|
|
1291
879
|
}
|
|
1292
880
|
else if (checked.verdict === "verified") {
|
|
1293
|
-
// A pack
|
|
1294
|
-
// a `verified`
|
|
1295
|
-
// readings, and criterion 4's "mixed" is about mixed HAND-OFF outcomes: a
|
|
1296
|
-
// pack-level clean contribution would put `verified` into the set on
|
|
1297
|
-
// every case, so one broken pair beside two honest declarations would
|
|
1298
|
-
// read `mixed` and the failure would be the quieter half of a two-word
|
|
1299
|
-
// answer. A pack-level PROBLEM still contributes, through its exception.
|
|
881
|
+
// A clean pack contributes nothing, so the case verdict reflects hand-off
|
|
882
|
+
// outcomes: a `verified` per pack would turn one broken pair into `mixed`.
|
|
1300
883
|
}
|
|
1301
884
|
else if (checked.present) {
|
|
1302
|
-
//
|
|
1303
|
-
// cannot read", which `gradePack` already carries to `incomplete`. The
|
|
1304
|
-
// same posture here, for the same reason: the field has no accept-set and
|
|
1305
|
-
// the holder writes it.
|
|
885
|
+
// A claim this reader cannot read (§5.4.3); the holder writes the field.
|
|
1306
886
|
note(PACK_DECLARATION_UNREADABLE, { packRef: pack.ref, packIndex: position });
|
|
1307
887
|
}
|
|
1308
888
|
else {
|
|
1309
|
-
//
|
|
1310
|
-
//
|
|
1311
|
-
// NOTE where it holds none. That is not known until the pile is read, so
|
|
1312
|
-
// the slot is held HERE, in pack order, and settled below.
|
|
889
|
+
// `partial` if the reading holds a hand-off record, else a note (§5.5.1).
|
|
890
|
+
// Not known until the pile is read, so hold the slot.
|
|
1313
891
|
undeclaredSlots.push({
|
|
1314
892
|
exceptionAt: exceptions.length,
|
|
1315
893
|
contributionAt: contributions.length,
|
|
@@ -1319,25 +897,27 @@ export async function readCase(packs) {
|
|
|
1319
897
|
exceptions.push(null);
|
|
1320
898
|
contributions.push(null);
|
|
1321
899
|
}
|
|
1322
|
-
// Each record on its
|
|
1323
|
-
// `body.events` in order and emit one entry per element, so the index is
|
|
1324
|
-
// the join, exactly as `held`, `provenance` and `readings` below.
|
|
900
|
+
// Each record on its own envelope, joined by index.
|
|
1325
901
|
for (let index = 0; index < records.length; index += 1) {
|
|
1326
902
|
const record = records[index];
|
|
1327
903
|
const graded = verified.events[index];
|
|
1328
904
|
const bare = signed.events[index];
|
|
1329
|
-
//
|
|
1330
|
-
// unique in the reading whatever the reader called the pack and whatever
|
|
1331
|
-
// the holder wrote as `event_id` (A43).
|
|
905
|
+
// Pack and record by position: unique whatever `ref` or `event_id` say.
|
|
1332
906
|
const at = {
|
|
1333
907
|
packRef: pack.ref,
|
|
1334
908
|
packIndex: position,
|
|
1335
909
|
eventId: record.eventId,
|
|
1336
910
|
recordIndex: index,
|
|
1337
911
|
};
|
|
1338
|
-
// Not read: listed with the kind of the exception that says why.
|
|
912
|
+
// Not read: listed with the kind of the exception that says why. Signed bytes that
|
|
913
|
+
// bind and are not JSON, and an envelope that cannot be read, names a version this
|
|
914
|
+
// verifier does not know or breaks its format under a signature that holds, are their
|
|
915
|
+
// pack's own problems, so no exception is noted here.
|
|
1339
916
|
const skip = (kind, fields = {}) => {
|
|
1340
|
-
|
|
917
|
+
if (!(kind === RECORD_BYTES_UNREADABLE && bare.recordNotJson === true) &&
|
|
918
|
+
!(kind === RECORD_ENVELOPE_DOES_NOT_VERIFY && ENVELOPE_FAULTS_OF_THE_PACK.includes(graded.envelopeFault ?? ""))) {
|
|
919
|
+
note(kind, { ...at, ...fields });
|
|
920
|
+
}
|
|
1341
921
|
slots.push({ position, packRef: pack.ref, recordIndex: index, eventId: record.eventId, heldAt: null, reason: kind });
|
|
1342
922
|
};
|
|
1343
923
|
if (underQuarantine(recordSpecVersion(pack.bundle, index))) {
|
|
@@ -1356,9 +936,8 @@ export async function readCase(packs) {
|
|
|
1356
936
|
skip(RECORD_BYTES_UNREADABLE);
|
|
1357
937
|
continue;
|
|
1358
938
|
}
|
|
1359
|
-
// Named, and the record stays in the pile
|
|
1360
|
-
//
|
|
1361
|
-
// with the side map is `incomplete` (proof#159).
|
|
939
|
+
// Named, and the record stays in the pile. A mismatch in the signed body
|
|
940
|
+
// is `failed`; one only with the side map is `incomplete`.
|
|
1362
941
|
if (bare.contentOk === false) {
|
|
1363
942
|
note(RECORD_CONTENT_MISMATCH, {
|
|
1364
943
|
...at,
|
|
@@ -1371,10 +950,12 @@ export async function readCase(packs) {
|
|
|
1371
950
|
reasons: onlyIn(graded.errors, bare.errors),
|
|
1372
951
|
});
|
|
1373
952
|
}
|
|
1374
|
-
// Every other check on the
|
|
1375
|
-
// add a failure here nor
|
|
1376
|
-
|
|
1377
|
-
|
|
953
|
+
// Every other check on the signed reading, so side content can neither
|
|
954
|
+
// add a failure here nor remove one. A stamp the pack does not hold, and signed
|
|
955
|
+
// bytes that are not JSON, are their pack's own problems (not proven, and a
|
|
956
|
+
// producer's error), never a record that does not match its pack.
|
|
957
|
+
if (!(bare.stampAbsent === true || bare.recordNotJson === true) &&
|
|
958
|
+
(!(bare.inclusionOk && bare.checkpointBound) || (!bare.ok && bare.contentOk !== false))) {
|
|
1378
959
|
note(RECORD_DOES_NOT_VERIFY_IN_ITS_PACK, {
|
|
1379
960
|
...at,
|
|
1380
961
|
reasons: [...bare.errors],
|
|
@@ -1388,41 +969,30 @@ export async function readCase(packs) {
|
|
|
1388
969
|
heldAt: held.length,
|
|
1389
970
|
reason: null,
|
|
1390
971
|
});
|
|
972
|
+
const agent = graded.agent;
|
|
973
|
+
agentReadings.set(`${position}:${index}`, agent);
|
|
1391
974
|
held.push({
|
|
1392
975
|
publicKey: record.publicKey,
|
|
1393
976
|
record: record.record,
|
|
1394
977
|
ref: `${pack.ref}/${record.eventId}`,
|
|
978
|
+
agentNonconforming: agent !== null && agent.verdict === "failed",
|
|
1395
979
|
});
|
|
1396
980
|
provenance.push({ position, packRef: pack.ref, eventId: record.eventId, recordIndex: index });
|
|
1397
981
|
}
|
|
1398
982
|
}
|
|
1399
|
-
// Step 3
|
|
1400
|
-
// the line DL-60 constrains: there is nothing here that reads a tenant, an
|
|
1401
|
-
// issuer or an organization, so a pair across two organizations' packs is
|
|
1402
|
-
// read exactly as a pair inside one.
|
|
983
|
+
// Step 3: one pile, no partition by tenant, issuer or organization.
|
|
1403
984
|
const readings = readHandoffs(held);
|
|
1404
985
|
// Step 4.
|
|
1405
986
|
let handoffRecords = 0;
|
|
1406
987
|
for (let index = 0; index < readings.length; index += 1) {
|
|
1407
988
|
const reading = readings[index];
|
|
1408
989
|
const { position, packRef, eventId, recordIndex } = provenance[index];
|
|
1409
|
-
// The record this reading is about, by position (A43).
|
|
1410
990
|
const at = { packRef, packIndex: position, eventId, recordIndex };
|
|
1411
|
-
//
|
|
1412
|
-
//
|
|
1413
|
-
//
|
|
1414
|
-
//
|
|
1415
|
-
//
|
|
1416
|
-
//
|
|
1417
|
-
// ABOVE the two `continue`s below, deliberately. This check asks what the
|
|
1418
|
-
// PACK said about the record, which is independent of whether 4.1 governs
|
|
1419
|
-
// the record and of whether the record is well formed. Behind the
|
|
1420
|
-
// `not_a_handoff_record` exit it was a check the audited party switched off
|
|
1421
|
-
// by respelling two fields, and behind the `malformed` exit it was one a
|
|
1422
|
-
// party switched off by breaking its own record.
|
|
1423
|
-
//
|
|
1424
|
-
// v1's ALONE (5.5.1 v1.20, A21). A v2 pack's claim check is rule 9, which
|
|
1425
|
-
// the declaration check already ran on the signed body.
|
|
991
|
+
// Read from `declarationClaims`, never a second parse. Only a readable
|
|
992
|
+
// declaration makes a record unclaimed; otherwise the pack's own exception
|
|
993
|
+
// covers it. Runs above both `continue`s below, so neither respelling a
|
|
994
|
+
// record out of §4.1 nor breaking it switches the check off. v1 only: a v2
|
|
995
|
+
// pack's claim check already ran inside the declaration check.
|
|
1426
996
|
const claims = claimsByPack.get(position);
|
|
1427
997
|
if (claims !== undefined &&
|
|
1428
998
|
claims.readable &&
|
|
@@ -1431,50 +1001,41 @@ export async function readCase(packs) {
|
|
|
1431
1001
|
note(RECORD_UNCLAIMED, at);
|
|
1432
1002
|
}
|
|
1433
1003
|
if (reading.findings.includes(NOT_A_HANDOFF_FINDING)) {
|
|
1434
|
-
//
|
|
1435
|
-
// is not a hand-off exception, and it is still in `records` and still
|
|
1436
|
-
// subject to the claim check above.
|
|
1004
|
+
// Not a hand-off exception; still listed and still claim-checked above.
|
|
1437
1005
|
continue;
|
|
1438
1006
|
}
|
|
1439
1007
|
handoffRecords += 1;
|
|
1440
|
-
if (reading.
|
|
1441
|
-
//
|
|
1442
|
-
//
|
|
1443
|
-
//
|
|
1444
|
-
// CANDIDATE, so it is a different kind, it does not `continue`, and the
|
|
1445
|
-
// counterpart, ambiguity and `failedChecks` reporting below all keep
|
|
1446
|
-
// running over it.
|
|
1008
|
+
if (reading.findings.includes("malformed_record")) {
|
|
1009
|
+
// `recordIsMalformed` answers about this record. A malformed aggregate
|
|
1010
|
+
// over a well-formed record is a malformed candidate, and reporting
|
|
1011
|
+
// continues for it.
|
|
1447
1012
|
if (recordIsMalformed(held[index])) {
|
|
1448
1013
|
note(RECORD_MALFORMED, at);
|
|
1449
1014
|
continue;
|
|
1450
1015
|
}
|
|
1451
1016
|
note(COUNTERPART_MALFORMED, at);
|
|
1452
1017
|
}
|
|
1018
|
+
else if (reading.findings.includes("nonconforming_record")) {
|
|
1019
|
+
// The same question for the other defect: this record's own, or a candidate's.
|
|
1020
|
+
if (recordIsNonconforming(held[index])) {
|
|
1021
|
+
note(RECORD_NONCONFORMING, at);
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
note(COUNTERPART_NONCONFORMING, at);
|
|
1025
|
+
}
|
|
1453
1026
|
if (reading.findings.includes(COUNTERPART_ABSENT)) {
|
|
1454
|
-
// `held`, `provenance` and `readings`
|
|
1455
|
-
//
|
|
1456
|
-
// Deliberately not a lookup by reference string: a reference is for a
|
|
1457
|
-
// person to read, and a join rebuilt by splitting one is a join on a
|
|
1458
|
-
// value a holder can put a separator inside.
|
|
1027
|
+
// `held`, `provenance` and `readings` share one order, never filtered, so
|
|
1028
|
+
// the index is the join.
|
|
1459
1029
|
const subject = held[index];
|
|
1460
1030
|
const event = subject.record["event"];
|
|
1461
1031
|
const named = normalizedSigningKey(typeof event === "object" && event !== null
|
|
1462
1032
|
? event["counterparty_public_key"]
|
|
1463
1033
|
: undefined);
|
|
1464
|
-
//
|
|
1465
|
-
//
|
|
1466
|
-
//
|
|
1467
|
-
//
|
|
1468
|
-
//
|
|
1469
|
-
//
|
|
1470
|
-
// EXCEPT where 4.1 set every candidate aside (v1.17, A20). There the
|
|
1471
|
-
// absence is a finding about the candidates the reader DID read, each
|
|
1472
|
-
// paired `verified` with another record the subject's own signer made,
|
|
1473
|
-
// and not a finding about any pack. 4.1 and evidence-bundle A16 name the
|
|
1474
|
-
// neutral kind for it, and the damning one would put the audited party's
|
|
1475
|
-
// own unfaithful record on the counterparty's pack as a statement
|
|
1476
|
-
// against it. `count` is how many candidates were set aside, which is
|
|
1477
|
-
// also what tells the rendered sentence which absence this is.
|
|
1034
|
+
// The damning kind needs a verified pack signed by the named key; with
|
|
1035
|
+
// any unattributed pack, the reading says it could not tell. Where every
|
|
1036
|
+
// candidate was set aside (each paired with another record by the
|
|
1037
|
+
// subject's own signer), the absence is about those candidates, not any
|
|
1038
|
+
// pack, so the neutral kind carries `count`.
|
|
1478
1039
|
const graded = reading.candidateReadings;
|
|
1479
1040
|
const setAside = graded.length > 0 && graded.every((c) => c.accountedFor !== null)
|
|
1480
1041
|
? graded.length
|
|
@@ -1497,13 +1058,9 @@ export async function readCase(packs) {
|
|
|
1497
1058
|
count: reading.candidateReadings.filter((c) => c.accountedFor === null).length,
|
|
1498
1059
|
});
|
|
1499
1060
|
}
|
|
1500
|
-
//
|
|
1501
|
-
//
|
|
1502
|
-
//
|
|
1503
|
-
// verdict instead threw that union away: `malformed` outranks `failed`, so
|
|
1504
|
-
// one junk candidate took the branch below and a proven `message_hash`
|
|
1505
|
-
// failure disappeared from the case. A named failed check is a named failed
|
|
1506
|
-
// check whatever the aggregate word is.
|
|
1061
|
+
// On `failedChecks`, not the verdict word: `malformed` outranks `failed`,
|
|
1062
|
+
// and §4.1 keeps the union of failed checks so a junk candidate cannot hide
|
|
1063
|
+
// a proven failure.
|
|
1507
1064
|
if (reading.failedChecks.length > 0) {
|
|
1508
1065
|
note(PAIR_FAILED, {
|
|
1509
1066
|
...at,
|
|
@@ -1514,11 +1071,8 @@ export async function readCase(packs) {
|
|
|
1514
1071
|
contributions.push([`${packRef}/${eventId}`, "verified"]);
|
|
1515
1072
|
}
|
|
1516
1073
|
}
|
|
1517
|
-
//
|
|
1518
|
-
//
|
|
1519
|
-
// contributing `partial`, in the slot it was given in pack order where the
|
|
1520
|
-
// reading holds a hand-off record; a NOTE with no contribution where it holds
|
|
1521
|
-
// none.
|
|
1074
|
+
// Undeclared packs: the `pack_undeclared` exception, contributing `incomplete`,
|
|
1075
|
+
// where the reading holds a hand-off record; otherwise a note (§5.5.3).
|
|
1522
1076
|
for (const slot of undeclaredSlots) {
|
|
1523
1077
|
if (handoffRecords > 0) {
|
|
1524
1078
|
exceptions[slot.exceptionAt] = {
|
|
@@ -1537,17 +1091,13 @@ export async function readCase(packs) {
|
|
|
1537
1091
|
}
|
|
1538
1092
|
const named = exceptions.filter((e) => e !== null);
|
|
1539
1093
|
const contributed = contributions.filter((c) => c !== null);
|
|
1540
|
-
// Pack order, stable.
|
|
1541
|
-
// declaration withholds nothing by one.
|
|
1094
|
+
// Pack order, stable.
|
|
1542
1095
|
const notes = caseNotes
|
|
1543
1096
|
.map((n, i) => ({ ...n, i }))
|
|
1544
1097
|
.sort((a, b) => a.position - b.position || a.i - b.i)
|
|
1545
1098
|
.map((n) => n.note);
|
|
1546
|
-
//
|
|
1547
|
-
//
|
|
1548
|
-
// record position, or its pack's declaration check names it (by deviation
|
|
1549
|
-
// or by content finding), and `read_clean` where neither does; every other
|
|
1550
|
-
// record is `not_read`, with the kind of the exception that says why.
|
|
1099
|
+
// Read records are `read_flagged` when an exception or declaration finding
|
|
1100
|
+
// names them, else `read_clean`; the rest are `not_read`.
|
|
1551
1101
|
const flagged = new Set(named
|
|
1552
1102
|
.filter((e) => e.packIndex !== undefined && e.recordIndex !== undefined)
|
|
1553
1103
|
.map((e) => `${e.packIndex}:${e.recordIndex}`));
|
|
@@ -1570,6 +1120,7 @@ export async function readCase(packs) {
|
|
|
1570
1120
|
failedChecks: [],
|
|
1571
1121
|
counterparts: [],
|
|
1572
1122
|
role: null,
|
|
1123
|
+
agent: null,
|
|
1573
1124
|
};
|
|
1574
1125
|
}
|
|
1575
1126
|
const reading = readings[slot.heldAt];
|
|
@@ -1585,9 +1136,10 @@ export async function readCase(packs) {
|
|
|
1585
1136
|
failedChecks: [...reading.failedChecks],
|
|
1586
1137
|
counterparts: [...reading.candidates],
|
|
1587
1138
|
role: rolesByPack.get(slot.position)?.get(slot.eventId) ?? null,
|
|
1139
|
+
agent: agentReadings.get(`${slot.position}:${slot.recordIndex}`) ?? null,
|
|
1588
1140
|
};
|
|
1589
1141
|
});
|
|
1590
|
-
return {
|
|
1142
|
+
return withProblems({
|
|
1591
1143
|
verdict: verdictOf(named, contributed),
|
|
1592
1144
|
exceptions: named,
|
|
1593
1145
|
records,
|
|
@@ -1596,14 +1148,11 @@ export async function readCase(packs) {
|
|
|
1596
1148
|
onChainChecked: witnessChecked.length > 0 && witnessChecked.every((c) => c),
|
|
1597
1149
|
notes,
|
|
1598
1150
|
packs: packReadings,
|
|
1599
|
-
};
|
|
1151
|
+
});
|
|
1600
1152
|
}
|
|
1601
1153
|
/**
|
|
1602
|
-
* The records a pack
|
|
1603
|
-
*
|
|
1604
|
-
* `sanning.evidence.export/v2` to its inline source bundle, and otherwise
|
|
1605
|
-
* `body.events` as `packRecords` reads it. Listing is not reading: every record
|
|
1606
|
-
* listed here is `not_read`.
|
|
1154
|
+
* The records a pack lists when this reader does not read them one by one,
|
|
1155
|
+
* unwrapping an export to its source bundle. Every one is `not_read`.
|
|
1607
1156
|
*/
|
|
1608
1157
|
function listedRecords(bundle, content) {
|
|
1609
1158
|
const events = resolveTraceEvents(bundle);
|
|
@@ -1611,10 +1160,7 @@ function listedRecords(bundle, content) {
|
|
|
1611
1160
|
return packRecords(bundle, content);
|
|
1612
1161
|
return packRecords({ body: { events } }, content);
|
|
1613
1162
|
}
|
|
1614
|
-
/**
|
|
1615
|
-
* `body.declaration.declaration_version` where it is one this reader knows,
|
|
1616
|
-
* and `null` otherwise.
|
|
1617
|
-
*/
|
|
1163
|
+
/** The pack's known `declaration_version`, or `null`. */
|
|
1618
1164
|
function declarationVersionOf(bundle) {
|
|
1619
1165
|
const at = (v, k) => typeof v === "object" && v !== null && !Array.isArray(v)
|
|
1620
1166
|
? v[k]
|
|
@@ -1628,15 +1174,11 @@ function declarationVersionOf(bundle) {
|
|
|
1628
1174
|
? version
|
|
1629
1175
|
: null;
|
|
1630
1176
|
}
|
|
1631
|
-
|
|
1177
|
+
const because = (reasons) => reasons !== undefined && reasons.length > 0 ? `: ${reasons.join("; ")}` : "";
|
|
1632
1178
|
/**
|
|
1633
|
-
* The house wording for one exception.
|
|
1634
|
-
*
|
|
1635
|
-
* Offered, not imposed, exactly as `renderDeviation` is: what must not vary
|
|
1636
|
-
* between surfaces is the verdict and the set of exceptions, and those come
|
|
1637
|
-
* from `readCase`. Both kernels return this same text for the same exception.
|
|
1179
|
+
* The house wording for one exception. Optional: the verdict and the set of
|
|
1180
|
+
* exceptions are what must not vary between surfaces.
|
|
1638
1181
|
*/
|
|
1639
|
-
const because = (reasons) => reasons !== undefined && reasons.length > 0 ? `: ${reasons.join("; ")}` : "";
|
|
1640
1182
|
export function renderException(exception) {
|
|
1641
1183
|
const where = `${exception.packRef}/${exception.eventId}`;
|
|
1642
1184
|
switch (exception.kind) {
|
|
@@ -1653,78 +1195,82 @@ export function renderException(exception) {
|
|
|
1653
1195
|
"and no pack in this case is signed by the key it names");
|
|
1654
1196
|
case COUNTERPART_ABSENT_FROM_COUNTERPARTY_RECORDS:
|
|
1655
1197
|
return (`${where} has no counterpart among the records this verifier read, ` +
|
|
1656
|
-
"and a pack signed by the key it names
|
|
1198
|
+
"and a pack signed by the key it names is in this case and holds " +
|
|
1657
1199
|
"no counterpart this verifier could read");
|
|
1658
1200
|
case COUNTERPART_ABSENT_SIGNER_UNKNOWN:
|
|
1659
1201
|
return (`${where} has no counterpart among the records this verifier read, ` +
|
|
1660
1202
|
"and a pack in this case could not be tied to a signing key, so " +
|
|
1661
1203
|
"whether a pack signed by the key it names is in this case is " +
|
|
1662
|
-
"
|
|
1204
|
+
"unknown");
|
|
1663
1205
|
case COUNTERPART_AMBIGUOUS:
|
|
1664
1206
|
return (`${where} has ${exception.count} candidate counterparts, so the ` +
|
|
1665
1207
|
"reading is the worst of them and no one match was chosen");
|
|
1666
1208
|
case PAIR_FAILED:
|
|
1667
|
-
return `${where}
|
|
1209
|
+
return `${where} failed on ${(exception.failedChecks ?? []).join(", ")}`;
|
|
1668
1210
|
case RECORD_MALFORMED:
|
|
1669
1211
|
return `${where} is not a well formed hand-off record`;
|
|
1670
1212
|
case COUNTERPART_MALFORMED:
|
|
1671
1213
|
return (`${where} is well formed and a candidate counterpart of it is not, ` +
|
|
1672
|
-
"so what this record pairs with is
|
|
1214
|
+
"so what this record pairs with is unknown");
|
|
1215
|
+
case RECORD_NONCONFORMING:
|
|
1216
|
+
return (`${where} is signed and intact and does not follow its vocabulary, ` +
|
|
1217
|
+
"so nothing is proven altered and it cannot be paired");
|
|
1218
|
+
case COUNTERPART_NONCONFORMING:
|
|
1219
|
+
return (`${where} conforms and a candidate counterpart of it does not, ` +
|
|
1220
|
+
"so what this record pairs with is unknown");
|
|
1673
1221
|
case RECORD_UNCLAIMED:
|
|
1674
1222
|
return (`${where} is a record its own pack declares and claims in no ` +
|
|
1675
1223
|
"hand-off pair, so whether the holder meant to hand it over is " +
|
|
1676
|
-
"
|
|
1224
|
+
"unknown");
|
|
1677
1225
|
case RECORD_BYTES_WITHHELD:
|
|
1678
|
-
return `${where} travels with no record bytes, so what that record
|
|
1226
|
+
return `${where} travels with no record bytes, so what that record is, is unknown`;
|
|
1679
1227
|
case RECORD_BYTES_UNREADABLE:
|
|
1680
1228
|
return `${where} travels with record bytes this verifier cannot read`;
|
|
1681
1229
|
case PACK_DECLARATION_DEVIATES:
|
|
1682
|
-
//
|
|
1683
|
-
// against the content the reader was handed, and content beside a pack is
|
|
1684
|
-
// unsigned. Whoever assembles a case can swap it, so the sentence names
|
|
1685
|
-
// what was compared rather than only the pack.
|
|
1230
|
+
// Names what was compared: the side content is unsigned and swappable.
|
|
1686
1231
|
return (`${exception.packRef}, read together with any content handed in ` +
|
|
1687
|
-
`beside it,
|
|
1232
|
+
`beside it, deviates from its own declaration, in ` +
|
|
1688
1233
|
`${exception.count} way(s)`);
|
|
1689
1234
|
case PACK_DECLARATION_CONTENT_DEPENDENT:
|
|
1690
|
-
//
|
|
1691
|
-
//
|
|
1692
|
-
// assembled the case chose them.
|
|
1235
|
+
// Names what could not be established and the content, never the holder:
|
|
1236
|
+
// the bytes are unsigned and the case assembler chose them.
|
|
1693
1237
|
return (`${exception.packRef} agrees with its own declaration everywhere its ` +
|
|
1694
1238
|
`signature reaches, and in ${exception.count} way(s) it does not line ` +
|
|
1695
1239
|
"up with the unsigned content handed in beside it, which anyone could " +
|
|
1696
|
-
"have supplied, so whether it holds what it declares is
|
|
1240
|
+
"have supplied, so whether it holds what it declares is unknown");
|
|
1697
1241
|
case PACK_DECLARATION_UNREADABLE:
|
|
1698
1242
|
return (`${exception.packRef} says what it holds in a declaration this ` +
|
|
1699
|
-
"verifier cannot read, so whether it is complete is
|
|
1243
|
+
"verifier cannot read, so whether it is complete is unknown");
|
|
1700
1244
|
case PACK_UNDECLARED:
|
|
1701
1245
|
return (`${exception.packRef} does not say what it holds, so whether it is ` +
|
|
1702
|
-
"complete is
|
|
1246
|
+
"complete is unknown");
|
|
1703
1247
|
case PACK_BODY_TYPE_UNKNOWN:
|
|
1704
1248
|
return (`${exception.packRef} is a pack body this verifier does not read, so ` +
|
|
1705
|
-
"what it holds is
|
|
1249
|
+
"what it holds is unknown");
|
|
1706
1250
|
case PACK_HOLDS_NO_RECORDS:
|
|
1707
1251
|
return `${exception.packRef} holds no records this verifier can read`;
|
|
1252
|
+
case PACK_UNREADABLE:
|
|
1253
|
+
return (`${exception.packRef} cannot be read: its bundle.json ` +
|
|
1254
|
+
`${(exception.reasons ?? []).join("; ")}, so who signed it is unknown and none of its ` +
|
|
1255
|
+
"records are read");
|
|
1708
1256
|
case PACK_KEY_UNREADABLE:
|
|
1709
1257
|
return (`${exception.packRef} names a signing key this verifier cannot ` +
|
|
1710
|
-
"read, so who signed it is
|
|
1258
|
+
"read, so who signed it is unknown and none of its records are read");
|
|
1711
1259
|
case PACK_DOES_NOT_VERIFY:
|
|
1712
1260
|
return (`${exception.packRef} carries a wrapper signature over its body that ` +
|
|
1713
1261
|
"does not verify, so none of its records are read and the key it " +
|
|
1714
1262
|
"names is not counted as its signer");
|
|
1715
1263
|
case PACK_GRADE_NOT_VERIFIED: {
|
|
1716
|
-
let head = `${exception.packRef} grades ${
|
|
1264
|
+
let head = `${exception.packRef} grades ${exception.status} as a pack`;
|
|
1717
1265
|
if (exception.causes !== undefined && exception.causes.length > 0) {
|
|
1718
1266
|
head += ` (${exception.causes.join(", ")})`;
|
|
1719
1267
|
}
|
|
1720
|
-
if (exception.status === "malformed")
|
|
1721
|
-
head += ", so none of its records are read";
|
|
1722
1268
|
return head + because(exception.reasons);
|
|
1723
1269
|
}
|
|
1724
1270
|
case PACK_BUNDLE_CONTENT_DEPENDENT:
|
|
1725
|
-
return (`${exception.packRef} reads
|
|
1271
|
+
return (`${exception.packRef} reads failed as a bundle only together with ` +
|
|
1726
1272
|
"the unsigned content handed in beside it, which anyone could have " +
|
|
1727
|
-
"supplied, so whether that failure is the pack's is
|
|
1273
|
+
"supplied, so whether that failure is the pack's is unknown" +
|
|
1728
1274
|
because(exception.reasons));
|
|
1729
1275
|
case RECORD_ENVELOPE_DOES_NOT_VERIFY:
|
|
1730
1276
|
return (`${where} is in a pack whose wrapper verifies, and its own envelope ` +
|
|
@@ -1741,23 +1287,24 @@ export function renderException(exception) {
|
|
|
1741
1287
|
case RECORD_CONTENT_DEPENDENT:
|
|
1742
1288
|
return (`${where} commits to content that the unsigned content handed in ` +
|
|
1743
1289
|
"beside its pack does not match, and anyone could have supplied " +
|
|
1744
|
-
"that content, so what it says about the record is
|
|
1290
|
+
"that content, so what it says about the record is unknown; the " +
|
|
1745
1291
|
"record is still read as a hand-off record" +
|
|
1746
1292
|
because(exception.reasons));
|
|
1747
1293
|
case RECORD_PROFILE_QUARANTINED:
|
|
1748
1294
|
return (`${where} is signed under a profile this case reader does not read ` +
|
|
1749
|
-
"while its admission is undecided, so what it says is
|
|
1295
|
+
"while its admission is undecided, so what it says is unknown");
|
|
1750
1296
|
case CASE_HOLDS_NO_PACKS:
|
|
1751
1297
|
return "this case holds no packs";
|
|
1752
1298
|
default:
|
|
1753
1299
|
return exception.kind;
|
|
1754
1300
|
}
|
|
1755
1301
|
}
|
|
1756
|
-
/** The house wording for one
|
|
1302
|
+
/** The house wording for one note. */
|
|
1757
1303
|
export function renderCaseNote(note) {
|
|
1758
1304
|
switch (note.kind) {
|
|
1759
1305
|
case CONTENT_WITHHELD_BY_DECLARATION:
|
|
1760
|
-
return (`${note.packRef} withholds the content of ${note.count}
|
|
1306
|
+
return (`${note.packRef} withholds the content of ${note.count} ` +
|
|
1307
|
+
`${note.count === 1 ? "record" : "records"} by ` +
|
|
1761
1308
|
"its own signed declaration: their signatures and commitments were " +
|
|
1762
1309
|
"checked, their content was not");
|
|
1763
1310
|
case PACK_UNDECLARED:
|
|
@@ -1770,18 +1317,11 @@ export function renderCaseNote(note) {
|
|
|
1770
1317
|
}
|
|
1771
1318
|
}
|
|
1772
1319
|
/**
|
|
1773
|
-
* The scope sentence
|
|
1774
|
-
*
|
|
1775
|
-
*
|
|
1776
|
-
*
|
|
1777
|
-
*
|
|
1778
|
-
* exceptions; and that the reading was made offline. Where any pack holds
|
|
1779
|
-
* records withheld by its declaration, their total; where any pack is noted
|
|
1780
|
-
* `pack_undeclared`, how many. Those two lines are part of what the headline
|
|
1781
|
-
* MEANS, and a surface MUST print them with it.
|
|
1782
|
-
*
|
|
1783
|
-
* What was not checked on the public record is no longer a clause here. It is
|
|
1784
|
-
* the witness line (`renderWitnessLine`), printed with every headline.
|
|
1320
|
+
* The scope sentence printed under the headline (evidence-bundle §5.5.6): packs,
|
|
1321
|
+
* distinct signing keys of packs whose wrapper held, hand-off records,
|
|
1322
|
+
* exceptions, and that it was read offline, plus withheld and undeclared
|
|
1323
|
+
* counts where non-zero. Those counts are part of what the headline means, and
|
|
1324
|
+
* a surface must print them with it.
|
|
1785
1325
|
*/
|
|
1786
1326
|
export function renderCaseSummary(reading) {
|
|
1787
1327
|
const keys = new Set(reading.packs.filter((p) => p.grade !== null).map((p) => p.signingKey));
|
|
@@ -1800,66 +1340,43 @@ export function renderCaseSummary(reading) {
|
|
|
1800
1340
|
lines.push(`${undeclared} pack(s) do not say what they hold.`);
|
|
1801
1341
|
return lines.join(" ");
|
|
1802
1342
|
}
|
|
1803
|
-
// ---- The headline and the witness line (evidence-bundle 5.5.6 v1.20) -------
|
|
1804
1343
|
/**
|
|
1805
|
-
* The one display title of each of the
|
|
1806
|
-
* title, never the word, and never a title of its own
|
|
1807
|
-
* is Will's to change through the glossary (DL-71); what each MEANS is pinned
|
|
1808
|
-
* in the spec.
|
|
1344
|
+
* The one display title of each of the three section 3 verdicts. A surface prints the
|
|
1345
|
+
* title, never the word, and never a title of its own.
|
|
1809
1346
|
*/
|
|
1810
|
-
export const VERDICT_DISPLAY_TITLES = {
|
|
1811
|
-
verified: "Verified",
|
|
1812
|
-
partial: "Partial",
|
|
1813
|
-
pending: "Pending",
|
|
1814
|
-
incomplete: "Incomplete",
|
|
1815
|
-
failed: "Failed",
|
|
1816
|
-
malformed: "Not readable",
|
|
1817
|
-
not_found: "Not found",
|
|
1818
|
-
mixed: "Mixed",
|
|
1819
|
-
};
|
|
1347
|
+
export const VERDICT_DISPLAY_TITLES = { ...VERDICT_TITLES };
|
|
1820
1348
|
/**
|
|
1821
|
-
* What a reader may rely on, for each
|
|
1822
|
-
*
|
|
1823
|
-
*
|
|
1824
|
-
* witness states. `verified` is the base sentence, printed alone where the
|
|
1825
|
-
* drop holds no hand-off record; a caller reads `caseReliance` for
|
|
1826
|
-
* `verified`, never this mapping directly.
|
|
1349
|
+
* What a reader may rely on, for each verdict a case reading reaches (section 5.5.6), the
|
|
1350
|
+
* same for every drop, keyed by the verdict and, for `failed`, its reason kind. Call
|
|
1351
|
+
* `caseReliance` rather than reading `verified` here directly.
|
|
1827
1352
|
*/
|
|
1828
1353
|
export const CASE_RELIANCE = {
|
|
1829
1354
|
verified: "Every pack in this drop is genuine, unaltered, and matches its own " +
|
|
1830
1355
|
"signed declaration. Every record in it was signed by its holder's " +
|
|
1831
1356
|
"key. This was read offline, on this device. It does not say this " +
|
|
1832
1357
|
"drop holds everything that was recorded.",
|
|
1833
|
-
partial: "Nothing here was proven wrong, and a check could not be completed: a " +
|
|
1834
|
-
"pack names no witness, or a pack in a hand-off reading does not say " +
|
|
1835
|
-
"what it holds. The exceptions say which.",
|
|
1836
1358
|
incomplete: "Nothing here was proven wrong, and part of it could not be checked: " +
|
|
1837
1359
|
"the other side of a hand-off is not in this drop, bytes were not " +
|
|
1838
|
-
"handed over
|
|
1839
|
-
"
|
|
1360
|
+
"handed over or could not be read, a pack names no witness, or a " +
|
|
1361
|
+
"claim could not be read. Do not rely on the parts that are not " +
|
|
1362
|
+
"proven. Each exception says what is missing and, where a key is " +
|
|
1363
|
+
"known, whose pack would supply it.",
|
|
1840
1364
|
failed: "A check ran and did not pass. Do not rely on this drop until the " +
|
|
1841
1365
|
"named failure is explained. The failure is named on the pack or " +
|
|
1842
1366
|
"record it is about, and every other exception is listed beside it.",
|
|
1843
|
-
|
|
1844
|
-
"
|
|
1845
|
-
|
|
1846
|
-
"
|
|
1847
|
-
"exception before relying on any of it.",
|
|
1367
|
+
failed_nonconforming: "A record in this drop is signed and unaltered and does not follow " +
|
|
1368
|
+
"the vocabulary it names: its producer wrote something the " +
|
|
1369
|
+
"specification does not allow. Nothing was proven altered. The " +
|
|
1370
|
+
"exceptions name the record and the rule it breaks.",
|
|
1848
1371
|
};
|
|
1849
1372
|
/**
|
|
1850
|
-
*
|
|
1851
|
-
*
|
|
1852
|
-
* and never where `handoffRecords` is zero: most drops hold one agent's
|
|
1853
|
-
* pack and no hand-off to say anything about.
|
|
1373
|
+
* Added to a `verified` reliance line after its first sentence, only where the
|
|
1374
|
+
* drop holds a hand-off record.
|
|
1854
1375
|
*/
|
|
1855
1376
|
export const CASE_RELIANCE_HANDOFF_CLAUSE = "Every hand-off with both sides in this drop is intact.";
|
|
1856
1377
|
/**
|
|
1857
|
-
*
|
|
1858
|
-
*
|
|
1859
|
-
* marker (`environmentMarker`): `dev` where every envelope is signed dev,
|
|
1860
|
-
* `mixed` where only some are. Inserted after the line's first sentence, for
|
|
1861
|
-
* every verdict word. A drop with no `dev` envelope has no marker and gains
|
|
1862
|
-
* nothing.
|
|
1378
|
+
* Added after the reliance line's first sentence, for every verdict, where the
|
|
1379
|
+
* drop holds `environment: "dev"` envelopes: `dev` if all are, `mixed` if some.
|
|
1863
1380
|
*/
|
|
1864
1381
|
export const CASE_RELIANCE_SANDBOX = {
|
|
1865
1382
|
dev: "The records in this drop are sandbox records, signed environment " +
|
|
@@ -1868,21 +1385,12 @@ export const CASE_RELIANCE_SANDBOX = {
|
|
|
1868
1385
|
"dev: those are not production evidence.",
|
|
1869
1386
|
};
|
|
1870
1387
|
/**
|
|
1871
|
-
* What a reader may rely on for one case reading (5.5.6
|
|
1872
|
-
*
|
|
1873
|
-
*
|
|
1874
|
-
*
|
|
1875
|
-
* `verified`, the hand-off clause is inserted after the first sentence
|
|
1876
|
-
* where `handoffRecords` is not zero, and left out where it is zero: the
|
|
1877
|
-
* base sentence never claims a hand-off the drop does not hold.
|
|
1878
|
-
*
|
|
1879
|
-
* Where `environmentMarker` is `dev` or `mixed`, the `CASE_RELIANCE_SANDBOX`
|
|
1880
|
-
* sentence for it is inserted after the first sentence, for every word, so
|
|
1881
|
-
* it directly follows the claim it qualifies (and precedes the hand-off
|
|
1882
|
-
* clause). Where it is null or absent, the text is exactly what it was
|
|
1883
|
-
* before A48.
|
|
1388
|
+
* What a reader may rely on for one case reading (§5.5.6). The sandbox
|
|
1389
|
+
* sentence, if any, follows the first sentence and precedes the hand-off
|
|
1390
|
+
* clause, which a `verified` reading gains only with `handoffRecords > 0`, so
|
|
1391
|
+
* the line never claims a hand-off the drop does not hold.
|
|
1884
1392
|
*/
|
|
1885
|
-
export function caseReliance(verdict, handoffRecords, environmentMarker = null) {
|
|
1393
|
+
export function caseReliance(verdict, handoffRecords, environmentMarker = null, reasonKind = null) {
|
|
1886
1394
|
let first;
|
|
1887
1395
|
let rest;
|
|
1888
1396
|
if (verdict === "verified" && handoffRecords > 0) {
|
|
@@ -1893,13 +1401,13 @@ export function caseReliance(verdict, handoffRecords, environmentMarker = null)
|
|
|
1893
1401
|
"device. It does not say this drop holds everything that was recorded.";
|
|
1894
1402
|
}
|
|
1895
1403
|
else {
|
|
1896
|
-
const
|
|
1404
|
+
const key = verdict === "failed" && reasonKind === "nonconforming" ? "failed_nonconforming" : verdict;
|
|
1405
|
+
const base = CASE_RELIANCE[key];
|
|
1897
1406
|
const cut = base.indexOf(". ");
|
|
1898
1407
|
first = base.slice(0, cut + 1);
|
|
1899
1408
|
rest = base.slice(cut + 2);
|
|
1900
1409
|
}
|
|
1901
|
-
//
|
|
1902
|
-
// forbids but an untyped caller can pass, reads as no marker.
|
|
1410
|
+
// An untyped caller can pass any value; only `dev` and `mixed` add a sentence.
|
|
1903
1411
|
const sandbox = environmentMarker === "dev" || environmentMarker === "mixed"
|
|
1904
1412
|
? CASE_RELIANCE_SANDBOX[environmentMarker]
|
|
1905
1413
|
: null;
|
|
@@ -1907,22 +1415,21 @@ export function caseReliance(verdict, handoffRecords, environmentMarker = null)
|
|
|
1907
1415
|
return `${first} ${rest}`;
|
|
1908
1416
|
return `${first} ${sandbox} ${rest}`;
|
|
1909
1417
|
}
|
|
1910
|
-
/** The three witness states
|
|
1418
|
+
/** The three witness states and how each is printed (§5.5.6). No others. */
|
|
1911
1419
|
export const WITNESS_STATES = {
|
|
1912
1420
|
confirmed: "Confirmed on the public record",
|
|
1913
1421
|
not_checked: "Not checked",
|
|
1914
1422
|
differs: "The public record holds different bytes",
|
|
1915
1423
|
};
|
|
1916
|
-
/** The line
|
|
1424
|
+
/** The line where no lookup ran: every reading this module produces on its own. */
|
|
1917
1425
|
export const WITNESS_NOT_LOOKED_UP = Object.freeze({
|
|
1918
1426
|
state: "not_checked",
|
|
1919
1427
|
reasons: ["no lookup was run"],
|
|
1920
1428
|
services: [],
|
|
1921
1429
|
});
|
|
1922
|
-
/**
|
|
1923
|
-
/** How a witness time a pack caches is printed where nothing checked it (5.5.6, 5.6.6, v1.25, A30). */
|
|
1430
|
+
/** A cached witness time no lookup checked (§5.6.6). */
|
|
1924
1431
|
export const WITNESS_TIME_CACHED = "the pack dates the witness, not checked";
|
|
1925
|
-
/**
|
|
1432
|
+
/** A witness time the public record returned that matched the cache. */
|
|
1926
1433
|
export const WITNESS_TIME_RECORDED = "the public record dates the witness";
|
|
1927
1434
|
function witnessTimes(times) {
|
|
1928
1435
|
return times.map(([h, t]) => renderWitnessTime(h, t)).join(", ");
|