@sanning/proof 0.11.1 → 0.13.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 +43 -8
- package/dist/agent-proof.d.ts +2 -2
- package/dist/agent-proof.js +4 -4
- package/dist/case-file.d.ts +126 -0
- package/dist/case-file.js +349 -0
- package/dist/case.d.ts +199 -43
- package/dist/case.js +522 -146
- package/dist/cli.d.ts +132 -1
- package/dist/cli.js +1140 -96
- package/dist/crypto.d.ts +1 -1
- package/dist/crypto.js +5 -1
- package/dist/declaration.d.ts +58 -52
- package/dist/declaration.js +256 -33
- package/dist/evidence.d.ts +2 -0
- package/dist/evidence.js +134 -19
- package/dist/exit-codes.d.ts +17 -0
- package/dist/exit-codes.js +50 -0
- package/dist/grading.d.ts +28 -13
- package/dist/grading.js +90 -31
- package/dist/handoff.d.ts +16 -1
- package/dist/handoff.js +91 -16
- package/dist/index.d.ts +10 -7
- package/dist/index.js +15 -7
- package/dist/message-parts.d.ts +11 -9
- package/dist/message-parts.js +11 -9
- package/dist/types.d.ts +1 -0
- package/dist/verifier.d.ts +9 -2
- package/dist/verifier.js +28 -12
- package/dist/zip-reader.d.ts +50 -0
- package/dist/zip-reader.js +220 -0
- package/package.json +2 -2
- package/src/agent-proof.ts +4 -4
- package/src/case-file.ts +419 -0
- package/src/case.ts +651 -156
- package/src/cli.ts +1325 -100
- package/src/crypto.ts +5 -1
- package/src/declaration.ts +248 -52
- package/src/evidence.ts +142 -21
- package/src/exit-codes.ts +54 -0
- package/src/grading.ts +109 -52
- package/src/handoff.ts +108 -17
- package/src/index.ts +60 -3
- package/src/message-parts.ts +11 -9
- package/src/types.ts +5 -0
- package/src/verifier.ts +35 -13
- package/src/zip-reader.ts +229 -0
- package/vectors/README.md +56 -3
- package/vectors/case-file-index.json +668 -0
- package/vectors/case-split.json +1102 -0
- package/vectors/pack-grading.json +68 -4
- package/vectors/stamp-on-chain.json +3 -3
package/src/crypto.ts
CHANGED
|
@@ -80,9 +80,13 @@ export async function sha256Hex(bytes: Uint8Array): Promise<string> {
|
|
|
80
80
|
// appear in the envelope; message is raw bytes. Never throws — a malformed
|
|
81
81
|
// signature or key is "not verified," not an exception, so a hostile envelope
|
|
82
82
|
// can't crash the checker.
|
|
83
|
+
// Arguments in the Python twin's order, `ed25519_verify(message,
|
|
84
|
+
// signature_hex, public_key_hex)` (P-03, 0.13.0). The message came second
|
|
85
|
+
// until 0.13.0; a caller that still passes the signature first gets a compile
|
|
86
|
+
// error, because a string is not a Uint8Array.
|
|
83
87
|
export async function ed25519Verify(
|
|
84
|
-
signatureHex: string,
|
|
85
88
|
message: Uint8Array,
|
|
89
|
+
signatureHex: string,
|
|
86
90
|
publicKeyHex: string,
|
|
87
91
|
): Promise<boolean> {
|
|
88
92
|
try {
|
package/src/declaration.ts
CHANGED
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
// last-write-wins in both languages, so a second record under a declared id
|
|
120
120
|
// was graded by neither rule 2 nor rule 7 and was never reported as surplus.
|
|
121
121
|
// FIRST in bundle order is now the record every other rule grades, and every
|
|
122
|
-
// record after it under the same id is a `
|
|
122
|
+
// record after it under the same id is a `duplicate_event_id` deviation.
|
|
123
123
|
//
|
|
124
124
|
// MINIMUM DISCLOSURE IS NOT A DEVIATION. A claimed side whose `event_bytes`
|
|
125
125
|
// the pack withholds was graded "not a conformant hand-off record", so an
|
|
@@ -133,36 +133,75 @@
|
|
|
133
133
|
// `src/sanning_proof/canonicalize.py` is the twin (proof#140 gate 5).
|
|
134
134
|
import { hexToBytes } from "./crypto.js";
|
|
135
135
|
import { jcsParse } from "./verifier.js";
|
|
136
|
-
import { isConformantHandoffRecord, type HandoffRecord } from "./handoff.js";
|
|
136
|
+
import { isConformantHandoffRecord, isHandoffNamespace, type HandoffRecord } from "./handoff.js";
|
|
137
137
|
import { hasSideContent, inBodyContentOffered, type SideContentMap } from "./side-content.js";
|
|
138
138
|
|
|
139
|
-
/** The declaration schema
|
|
139
|
+
/** The first declaration schema, which every earlier reader knows. */
|
|
140
140
|
export const DECLARATION_VERSION = "sanning.declaration/v1";
|
|
141
141
|
|
|
142
|
+
/**
|
|
143
|
+
* v1 plus a signed `records[].role` on every record (evidence-bundle.md
|
|
144
|
+
* 5.4.1 v1.20, A21). A reader that knows only v1 reads a v2 declaration as one
|
|
145
|
+
* it cannot read, which is the safe direction.
|
|
146
|
+
*/
|
|
147
|
+
export const DECLARATION_VERSION_V2 = "sanning.declaration/v2";
|
|
148
|
+
|
|
149
|
+
/** Every declaration schema this reader knows, in the order they were added. */
|
|
150
|
+
export const DECLARATION_VERSIONS = [DECLARATION_VERSION, DECLARATION_VERSION_V2] as const;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* What a v2 declaration may sign a record IS, compared exactly: `handoff`, a
|
|
154
|
+
* record 4.1 governs that the pack claims as a side in `handoffs[]`, or
|
|
155
|
+
* `step`, any other record.
|
|
156
|
+
*/
|
|
157
|
+
export const DECLARATION_ROLES = ["handoff", "step"] as const;
|
|
158
|
+
export type DeclarationRole = (typeof DECLARATION_ROLES)[number];
|
|
159
|
+
|
|
142
160
|
/** The three states a declaration check can reach, from the §3 vocabulary. */
|
|
143
|
-
export
|
|
161
|
+
export const DECLARATION_VERDICTS = ["verified", "failed", "partial"] as const;
|
|
162
|
+
export type DeclarationVerdict = (typeof DECLARATION_VERDICTS)[number];
|
|
144
163
|
|
|
145
|
-
export
|
|
164
|
+
export const DECLARATION_DEVIATION_KINDS = [
|
|
146
165
|
/** The declaration is written to this version and is not readable as one. */
|
|
147
|
-
|
|
166
|
+
"declaration_malformed",
|
|
148
167
|
/** A record the declaration expects is not in the pack. */
|
|
149
|
-
|
|
168
|
+
"declared_absent",
|
|
150
169
|
/** A record in the pack the declaration never expected. */
|
|
151
|
-
|
|
170
|
+
"undeclared_present",
|
|
152
171
|
/**
|
|
153
172
|
* A record declared `content_withheld` whose bytes the pack supplies INSIDE
|
|
154
173
|
* its own signed body, in `events[].content`.
|
|
155
174
|
*/
|
|
156
|
-
|
|
175
|
+
"withheld_but_disclosed",
|
|
157
176
|
/** The pack carries one `event_id` on two or more records. */
|
|
158
|
-
|
|
177
|
+
"duplicate_event_id",
|
|
159
178
|
/** A claimed hand-off names a record the declaration does not declare. */
|
|
160
|
-
|
|
179
|
+
"pair_record_undeclared",
|
|
161
180
|
/**
|
|
162
181
|
* A claimed hand-off names a record that is not a conformant hand-off
|
|
163
182
|
* record of the side it is claimed on (envelope-spec.md 4.1).
|
|
164
183
|
*/
|
|
165
|
-
|
|
184
|
+
"pair_record_not_a_handoff",
|
|
185
|
+
/**
|
|
186
|
+
* Rule 9 (v1.20, A21), `sanning.declaration/v2` only. A record declared
|
|
187
|
+
* `handoff` that no `handoffs[]` entry names as a side.
|
|
188
|
+
*/
|
|
189
|
+
"role_handoff_unclaimed",
|
|
190
|
+
/** A record a `handoffs[]` entry names as a side, declared `step`. */
|
|
191
|
+
"role_step_claimed",
|
|
192
|
+
/**
|
|
193
|
+
* A record declared `step` that 4.1's scope test puts in scope: its `event`
|
|
194
|
+
* carries `counterparty_public_key`, or its folded `event_type` has a
|
|
195
|
+
* `handoff` segment. The holder signed that a hand-off record is not one.
|
|
196
|
+
*/
|
|
197
|
+
"role_step_governed",
|
|
198
|
+
/**
|
|
199
|
+
* A record declared `step` whose supplied `event_bytes` this reader cannot
|
|
200
|
+
* read under rule 7's parser rule.
|
|
201
|
+
*/
|
|
202
|
+
"role_step_unreadable",
|
|
203
|
+
] as const;
|
|
204
|
+
export type DeclarationDeviationKind = (typeof DECLARATION_DEVIATION_KINDS)[number];
|
|
166
205
|
|
|
167
206
|
/**
|
|
168
207
|
* Every way a pack and the UNSIGNED content handed in beside it fail to line
|
|
@@ -190,7 +229,7 @@ export type DeclarationDeviationKind =
|
|
|
190
229
|
* legal and grades `verified`, which is strictly better than the `incomplete`
|
|
191
230
|
* these carry.
|
|
192
231
|
*/
|
|
193
|
-
export
|
|
232
|
+
export const DECLARATION_CONTENT_KINDS = [
|
|
194
233
|
/**
|
|
195
234
|
* A record declared `disclosed` whose bytes reached this reader from
|
|
196
235
|
* neither channel. ALWAYS content-dependent: 5.4.1 lets those bytes travel
|
|
@@ -198,14 +237,16 @@ export type DeclarationContentKind =
|
|
|
198
237
|
* fact about what the READER was handed and never a fact about what the
|
|
199
238
|
* holder supplied. No signature covers the absence of a file nobody signed.
|
|
200
239
|
*/
|
|
201
|
-
|
|
240
|
+
"content_dependent_withholding",
|
|
202
241
|
/**
|
|
203
242
|
* A record declared `content_withheld` whose bytes reached this reader ONLY
|
|
204
243
|
* from the unsigned side-content map. The pack's own body supplies none, so
|
|
205
244
|
* the holder committed to no such disclosure. The in-body half of the same
|
|
206
|
-
* rule is the `
|
|
245
|
+
* rule is the `withheld_but_disclosed` DEVIATION and still fails.
|
|
207
246
|
*/
|
|
208
|
-
|
|
247
|
+
"content_dependent_disclosure",
|
|
248
|
+
] as const;
|
|
249
|
+
export type DeclarationContentKind = (typeof DECLARATION_CONTENT_KINDS)[number];
|
|
209
250
|
|
|
210
251
|
/**
|
|
211
252
|
* One way the pack and the unsigned content beside it do not line up. Named
|
|
@@ -218,18 +259,27 @@ export interface DeclarationContentFinding {
|
|
|
218
259
|
}
|
|
219
260
|
|
|
220
261
|
/** Everything this check reports that is NOT a deviation, as a closed set. */
|
|
221
|
-
export
|
|
262
|
+
export const DECLARATION_NOTE_KINDS = [
|
|
222
263
|
/**
|
|
223
264
|
* Every record declared `disclosed` supplied no bytes, so nothing was
|
|
224
265
|
* selected: the bytes did not travel with the pack.
|
|
225
266
|
*/
|
|
226
|
-
|
|
267
|
+
"disclosure_did_not_travel",
|
|
227
268
|
/**
|
|
228
269
|
* A claimed hand-off side whose `event_bytes` the pack withholds. The reader
|
|
229
270
|
* cannot see what the record is either way, so it is neither a conformant
|
|
230
271
|
* hand-off record nor a non-conformant one.
|
|
231
272
|
*/
|
|
232
|
-
|
|
273
|
+
"pair_record_withheld",
|
|
274
|
+
/**
|
|
275
|
+
* A record a v2 declaration signs `step` whose `event_bytes` the pack
|
|
276
|
+
* withholds (rule 9, v1.20). Its role cannot be checked in either direction,
|
|
277
|
+
* so it is a note; the pack grade still holds the pack at `incomplete`,
|
|
278
|
+
* because its payload binding is undetermined.
|
|
279
|
+
*/
|
|
280
|
+
"role_step_withheld",
|
|
281
|
+
] as const;
|
|
282
|
+
export type DeclarationNoteKind = (typeof DECLARATION_NOTE_KINDS)[number];
|
|
233
283
|
|
|
234
284
|
/**
|
|
235
285
|
* One named non-deviation finding, as data.
|
|
@@ -244,9 +294,9 @@ export interface DeclarationNote {
|
|
|
244
294
|
count?: number;
|
|
245
295
|
/** The record this is about, where it is about one. */
|
|
246
296
|
eventId?: string;
|
|
247
|
-
/** Which side of a claimed hand-off named it, for `
|
|
297
|
+
/** Which side of a claimed hand-off named it, for `pair_record_withheld`. */
|
|
248
298
|
side?: "sent" | "received";
|
|
249
|
-
/** Which claimed hand-off, by position, for `
|
|
299
|
+
/** Which claimed hand-off, by position, for `pair_record_withheld`. */
|
|
250
300
|
pairIndex?: number;
|
|
251
301
|
}
|
|
252
302
|
|
|
@@ -259,8 +309,14 @@ export interface DeclarationDeviation {
|
|
|
259
309
|
side?: "sent" | "received";
|
|
260
310
|
/** Which claimed hand-off, by position, for the two `pair-record-` deviations. */
|
|
261
311
|
pairIndex?: number;
|
|
262
|
-
/** What is wrong with the declaration itself, for `
|
|
312
|
+
/** What is wrong with the declaration itself, for `declaration_malformed`. */
|
|
263
313
|
reason?: string;
|
|
314
|
+
/**
|
|
315
|
+
* The `declaration_version` the object claimed, for `declaration_malformed`,
|
|
316
|
+
* so the rendered sentence names the version the holder wrote to rather than
|
|
317
|
+
* the first one this reader learned.
|
|
318
|
+
*/
|
|
319
|
+
version?: string;
|
|
264
320
|
}
|
|
265
321
|
|
|
266
322
|
export interface DeclarationCheck {
|
|
@@ -286,6 +342,19 @@ export interface DeclarationCheck {
|
|
|
286
342
|
declared: string[];
|
|
287
343
|
/** Of those, the ones declared withheld on purpose. */
|
|
288
344
|
declaredWithheld: string[];
|
|
345
|
+
/**
|
|
346
|
+
* The `declaration_version` this reader read the object as, one of
|
|
347
|
+
* `DECLARATION_VERSIONS`, or `null` where the body carries no declaration or
|
|
348
|
+
* one whose version this reader does not know. Set for a malformed object
|
|
349
|
+
* too: the version was read, the rest was not. Optional so a check a caller
|
|
350
|
+
* builds by hand, as rule version 5 adapters do, still type-checks.
|
|
351
|
+
*/
|
|
352
|
+
version?: string | null;
|
|
353
|
+
/**
|
|
354
|
+
* Every record a v2 declaration signs a role for, in declaration order.
|
|
355
|
+
* Empty for v1, and for anything this reader could not read.
|
|
356
|
+
*/
|
|
357
|
+
roles?: { eventId: string; role: DeclarationRole }[];
|
|
289
358
|
}
|
|
290
359
|
|
|
291
360
|
// ---- Reading the pack --------------------------------------------------------
|
|
@@ -476,6 +545,7 @@ const isId = (v: unknown): v is string => typeof v === "string" && v.length > 0;
|
|
|
476
545
|
interface ReadDeclaration {
|
|
477
546
|
records: { eventId: string; withheld: boolean }[];
|
|
478
547
|
pairs: { sent: string | null; received: string | null }[];
|
|
548
|
+
roles: { eventId: string; role: DeclarationRole }[];
|
|
479
549
|
}
|
|
480
550
|
|
|
481
551
|
/**
|
|
@@ -488,8 +558,14 @@ interface ReadDeclaration {
|
|
|
488
558
|
*/
|
|
489
559
|
function readDeclaration(
|
|
490
560
|
declaration: Record<string, unknown>,
|
|
561
|
+
version: string = DECLARATION_VERSION,
|
|
491
562
|
): { ok: true; read: ReadDeclaration } | { ok: false; reasons: string[] } {
|
|
492
563
|
const reasons: string[] = [];
|
|
564
|
+
// Under v2 every `records[]` entry must also carry `role`, exactly `handoff`
|
|
565
|
+
// or `step` (5.4.1 v1.20, A21); under v1 the member is not read at all, so a
|
|
566
|
+
// v1 declaration carrying `role` reads exactly as the same object without it.
|
|
567
|
+
const roles: { eventId: string; role: DeclarationRole }[] = [];
|
|
568
|
+
const v2 = version === DECLARATION_VERSION_V2;
|
|
493
569
|
const note = (r: string) => reasons.push(r);
|
|
494
570
|
|
|
495
571
|
if (!isId(declaration["case_id"])) note("case_id is not a non-empty string");
|
|
@@ -526,12 +602,20 @@ function readDeclaration(
|
|
|
526
602
|
note(`records[${i}] (${eventId}) has no disclosure of "disclosed" or "content_withheld"`);
|
|
527
603
|
continue;
|
|
528
604
|
}
|
|
605
|
+
// Compared exactly: `Handoff`, `hand-off` and `step ` are other values,
|
|
606
|
+
// and a non-string never equals one.
|
|
607
|
+
const role = r["role"];
|
|
608
|
+
if (v2 && !(role === "handoff" || role === "step")) {
|
|
609
|
+
note(`records[${i}] (${eventId}) has no role of "handoff" or "step"`);
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
529
612
|
if (seen.has(eventId)) {
|
|
530
613
|
note(`records[${i}] declares ${eventId} a second time`);
|
|
531
614
|
continue;
|
|
532
615
|
}
|
|
533
616
|
seen.add(eventId);
|
|
534
617
|
records.push({ eventId, withheld: disclosure === "content_withheld" });
|
|
618
|
+
if (v2) roles.push({ eventId, role: role as DeclarationRole });
|
|
535
619
|
}
|
|
536
620
|
}
|
|
537
621
|
|
|
@@ -559,7 +643,7 @@ function readDeclaration(
|
|
|
559
643
|
}
|
|
560
644
|
}
|
|
561
645
|
|
|
562
|
-
return reasons.length > 0 ? { ok: false, reasons } : { ok: true, read: { records, pairs } };
|
|
646
|
+
return reasons.length > 0 ? { ok: false, reasons } : { ok: true, read: { records, pairs, roles } };
|
|
563
647
|
}
|
|
564
648
|
|
|
565
649
|
// ---- The check ---------------------------------------------------------------
|
|
@@ -572,8 +656,48 @@ const noDeclaration = (): DeclarationCheck => ({
|
|
|
572
656
|
present: false,
|
|
573
657
|
declared: [],
|
|
574
658
|
declaredWithheld: [],
|
|
659
|
+
version: null,
|
|
660
|
+
roles: [],
|
|
575
661
|
});
|
|
576
662
|
|
|
663
|
+
/**
|
|
664
|
+
* The `declaration_version` of an object this reader knows, or `null`: a
|
|
665
|
+
* declaration that is not an object, an absent or `null` version and a version
|
|
666
|
+
* outside `DECLARATION_VERSIONS` are each a claim this reader cannot read.
|
|
667
|
+
*/
|
|
668
|
+
function knownVersion(declaration: unknown): string | null {
|
|
669
|
+
if (!isObject(declaration)) return null;
|
|
670
|
+
const version = declaration["declaration_version"];
|
|
671
|
+
return version === DECLARATION_VERSION || version === DECLARATION_VERSION_V2 ? version : null;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Rule 9 over one record the pack holds: the deviation or note it earns, or
|
|
676
|
+
* `null` where the signed role agrees with the pack and the record. The first
|
|
677
|
+
* of the four deviations that applies, in the order 5.4.2 lists them. Whether
|
|
678
|
+
* 4.1 governs the record is `isHandoffNamespace`, CALLED, never restated.
|
|
679
|
+
*/
|
|
680
|
+
function roleReading(
|
|
681
|
+
p: { eventId: string; publicKey: string; record: HandoffRecord | null; bytesSupplied: boolean },
|
|
682
|
+
role: DeclarationRole,
|
|
683
|
+
claimed: boolean,
|
|
684
|
+
):
|
|
685
|
+
| "role_handoff_unclaimed"
|
|
686
|
+
| "role_step_claimed"
|
|
687
|
+
| "role_step_governed"
|
|
688
|
+
| "role_step_unreadable"
|
|
689
|
+
| "role_step_withheld"
|
|
690
|
+
| null {
|
|
691
|
+
if (role === "handoff") return claimed ? null : "role_handoff_unclaimed";
|
|
692
|
+
if (claimed) return "role_step_claimed";
|
|
693
|
+
if (!p.bytesSupplied) return "role_step_withheld";
|
|
694
|
+
if (p.record === null) return "role_step_unreadable";
|
|
695
|
+
if (isHandoffNamespace({ publicKey: p.publicKey, record: p.record, ref: p.eventId })) {
|
|
696
|
+
return "role_step_governed";
|
|
697
|
+
}
|
|
698
|
+
return null;
|
|
699
|
+
}
|
|
700
|
+
|
|
577
701
|
/**
|
|
578
702
|
* Check a pack against the declaration inside its body.
|
|
579
703
|
*
|
|
@@ -612,24 +736,31 @@ export function checkDeclaration(
|
|
|
612
736
|
// a deleted one, a null one, and a `declaration` that is a string, an array
|
|
613
737
|
// or null. Each of those hides the evidence of a trim inside a signed body,
|
|
614
738
|
// so none of them may reach a clean pack verdict.
|
|
615
|
-
|
|
739
|
+
const version = knownVersion(declaration);
|
|
740
|
+
if (version === null) {
|
|
616
741
|
return { ...noDeclaration(), present: true };
|
|
617
742
|
}
|
|
618
743
|
|
|
619
|
-
const read = readDeclaration(declaration);
|
|
744
|
+
const read = readDeclaration(declaration as Record<string, unknown>, version);
|
|
620
745
|
if (!read.ok) {
|
|
621
746
|
return {
|
|
622
747
|
verdict: "failed",
|
|
623
|
-
deviations: read.reasons.map((reason) => ({
|
|
748
|
+
deviations: read.reasons.map((reason) => ({
|
|
749
|
+
kind: "declaration_malformed",
|
|
750
|
+
reason,
|
|
751
|
+
version,
|
|
752
|
+
})),
|
|
624
753
|
contentFindings: [],
|
|
625
754
|
notes: [],
|
|
626
755
|
present: true,
|
|
627
756
|
declared: [],
|
|
628
757
|
declaredWithheld: [],
|
|
758
|
+
version,
|
|
759
|
+
roles: [],
|
|
629
760
|
};
|
|
630
761
|
}
|
|
631
762
|
|
|
632
|
-
const { records, pairs } = read.read;
|
|
763
|
+
const { records, pairs, roles } = read.read;
|
|
633
764
|
const declared = records.map((r) => r.eventId);
|
|
634
765
|
const declaredSet = new Set(declared);
|
|
635
766
|
const pack = packRecords(bundle, content);
|
|
@@ -650,7 +781,7 @@ export function checkDeclaration(
|
|
|
650
781
|
// Declared and absent. The trimmed hand-over, which is the whole reason a
|
|
651
782
|
// declaration exists: nothing else in the pack can see it.
|
|
652
783
|
for (const r of records) {
|
|
653
|
-
if (!byId.has(r.eventId)) deviations.push({ kind: "
|
|
784
|
+
if (!byId.has(r.eventId)) deviations.push({ kind: "declared_absent", eventId: r.eventId });
|
|
654
785
|
}
|
|
655
786
|
|
|
656
787
|
// Present and undeclared, and the pack carrying one id twice.
|
|
@@ -668,12 +799,12 @@ export function checkDeclaration(
|
|
|
668
799
|
const seenInPack = new Set<string>();
|
|
669
800
|
for (const p of pack) {
|
|
670
801
|
if (p.nameable && seenInPack.has(p.eventId)) {
|
|
671
|
-
deviations.push({ kind: "
|
|
802
|
+
deviations.push({ kind: "duplicate_event_id", eventId: p.eventId });
|
|
672
803
|
continue;
|
|
673
804
|
}
|
|
674
805
|
if (p.nameable) seenInPack.add(p.eventId);
|
|
675
806
|
if (!p.nameable || !declaredSet.has(p.eventId)) {
|
|
676
|
-
deviations.push({ kind: "
|
|
807
|
+
deviations.push({ kind: "undeclared_present", eventId: p.eventId });
|
|
677
808
|
}
|
|
678
809
|
}
|
|
679
810
|
|
|
@@ -688,7 +819,7 @@ export function checkDeclaration(
|
|
|
688
819
|
.filter((p): p is NonNullable<typeof p> => p !== undefined);
|
|
689
820
|
const nothingTravelled = disclosedHeld.length > 0 && !disclosedHeld.some((p) => p.disclosed);
|
|
690
821
|
const notes: DeclarationNote[] = nothingTravelled
|
|
691
|
-
? [{ kind: "
|
|
822
|
+
? [{ kind: "disclosure_did_not_travel", count: disclosedHeld.length }]
|
|
692
823
|
: [];
|
|
693
824
|
|
|
694
825
|
// The two disclosure rules, in declaration order, over the records the pack
|
|
@@ -713,12 +844,12 @@ export function checkDeclaration(
|
|
|
713
844
|
// the pack, so no pack can prove it supplied them and no reader can
|
|
714
845
|
// prove it did not: an accuser holding a twenty-file drop reaches this
|
|
715
846
|
// reading by deleting one file.
|
|
716
|
-
contentFindings.push({ kind: "
|
|
847
|
+
contentFindings.push({ kind: "content_dependent_withholding", eventId: r.eventId });
|
|
717
848
|
} else if (r.withheld && p.disclosed) {
|
|
718
849
|
if (p.disclosedInBody) {
|
|
719
|
-
deviations.push({ kind: "
|
|
850
|
+
deviations.push({ kind: "withheld_but_disclosed", eventId: r.eventId });
|
|
720
851
|
} else {
|
|
721
|
-
contentFindings.push({ kind: "
|
|
852
|
+
contentFindings.push({ kind: "content_dependent_disclosure", eventId: r.eventId });
|
|
722
853
|
}
|
|
723
854
|
}
|
|
724
855
|
}
|
|
@@ -748,20 +879,42 @@ export function checkDeclaration(
|
|
|
748
879
|
const id = pair[side];
|
|
749
880
|
if (id === null) continue;
|
|
750
881
|
if (!declaredSet.has(id)) {
|
|
751
|
-
deviations.push({ kind: "
|
|
882
|
+
deviations.push({ kind: "pair_record_undeclared", eventId: id, side, pairIndex: i });
|
|
752
883
|
continue;
|
|
753
884
|
}
|
|
754
885
|
const p = byId.get(id);
|
|
755
886
|
if (p === undefined) continue;
|
|
756
887
|
const reading = sideReading(p, side);
|
|
757
888
|
if (reading === "withheld") {
|
|
758
|
-
notes.push({ kind: "
|
|
889
|
+
notes.push({ kind: "pair_record_withheld", eventId: id, side, pairIndex: i });
|
|
759
890
|
} else if (reading === "not-a-handoff") {
|
|
760
|
-
deviations.push({ kind: "
|
|
891
|
+
deviations.push({ kind: "pair_record_not_a_handoff", eventId: id, side, pairIndex: i });
|
|
761
892
|
}
|
|
762
893
|
}
|
|
763
894
|
}
|
|
764
895
|
|
|
896
|
+
// Rule 9 (5.4.2 v1.20, A21), `sanning.declaration/v2` only: a record's
|
|
897
|
+
// signed role against the pack and against the record. Its deviations follow
|
|
898
|
+
// every deviation rules 1 to 8 report, and its note follows every other
|
|
899
|
+
// note, both in `records[]` order. It reads the signed body alone and NEVER
|
|
900
|
+
// the side-content map, so no party that signed nothing can create, move or
|
|
901
|
+
// remove one of its findings.
|
|
902
|
+
if (roles.length > 0) {
|
|
903
|
+
const claimed = new Set<string>();
|
|
904
|
+
for (const pair of pairs) {
|
|
905
|
+
if (pair.sent !== null) claimed.add(pair.sent);
|
|
906
|
+
if (pair.received !== null) claimed.add(pair.received);
|
|
907
|
+
}
|
|
908
|
+
for (const { eventId, role } of roles) {
|
|
909
|
+
const p = byId.get(eventId);
|
|
910
|
+
// Declared and absent: rule 1 named it, and one defect is one finding.
|
|
911
|
+
if (p === undefined) continue;
|
|
912
|
+
const kind = roleReading(p, role, claimed.has(eventId));
|
|
913
|
+
if (kind === "role_step_withheld") notes.push({ kind, eventId });
|
|
914
|
+
else if (kind !== null) deviations.push({ kind, eventId });
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
765
918
|
// A proven deviation wins: it is a mismatch between what the pack claimed
|
|
766
919
|
// and what its own signed body carries, and a failing check always wins.
|
|
767
920
|
// With no deviation and a content-dependent finding, the pack lines up with
|
|
@@ -779,6 +932,8 @@ export function checkDeclaration(
|
|
|
779
932
|
present: true,
|
|
780
933
|
declared,
|
|
781
934
|
declaredWithheld: records.filter((r) => r.withheld).map((r) => r.eventId),
|
|
935
|
+
version,
|
|
936
|
+
roles,
|
|
782
937
|
};
|
|
783
938
|
}
|
|
784
939
|
|
|
@@ -827,7 +982,7 @@ export interface DeclarationClaims {
|
|
|
827
982
|
*/
|
|
828
983
|
present: boolean;
|
|
829
984
|
/**
|
|
830
|
-
* Whether this reader could read the claim as `
|
|
985
|
+
* Whether this reader could read the claim as one of `DECLARATION_VERSIONS` AND the
|
|
831
986
|
* object satisfies 5.4.1. `false` covers an absent declaration, an unknown
|
|
832
987
|
* version and a malformed object, and a case reader must not read any of the
|
|
833
988
|
* three as "the holder claimed nothing to check".
|
|
@@ -841,6 +996,14 @@ export interface DeclarationClaims {
|
|
|
841
996
|
* is legal and is exactly the shape a case reader has to name.
|
|
842
997
|
*/
|
|
843
998
|
claimedSides: string[];
|
|
999
|
+
/**
|
|
1000
|
+
* The version the claim was read as, where `readable`; else `null`. The case
|
|
1001
|
+
* reader's claim check is v1's alone (5.5.1 v1.20): a v2 pack's claim check
|
|
1002
|
+
* is rule 9, which the declaration check already ran.
|
|
1003
|
+
*/
|
|
1004
|
+
version: string | null;
|
|
1005
|
+
/** Every record a READABLE v2 declaration signs a role for. Empty for v1. */
|
|
1006
|
+
roles: { eventId: string; role: DeclarationRole }[];
|
|
844
1007
|
}
|
|
845
1008
|
|
|
846
1009
|
/**
|
|
@@ -857,14 +1020,17 @@ export function declarationClaims(bundle: unknown): DeclarationClaims {
|
|
|
857
1020
|
readable: false,
|
|
858
1021
|
caseId: null,
|
|
859
1022
|
claimedSides: [],
|
|
1023
|
+
version: null,
|
|
1024
|
+
roles: [],
|
|
860
1025
|
};
|
|
861
1026
|
const body = bodyOf(bundle);
|
|
862
1027
|
if (!isObject(body) || !("declaration" in body)) return none;
|
|
863
1028
|
const declaration = (body as Record<string, unknown>)["declaration"];
|
|
864
|
-
|
|
1029
|
+
const version = knownVersion(declaration);
|
|
1030
|
+
if (version === null || !isObject(declaration)) {
|
|
865
1031
|
return { ...none, present: true };
|
|
866
1032
|
}
|
|
867
|
-
const read = readDeclaration(declaration);
|
|
1033
|
+
const read = readDeclaration(declaration, version);
|
|
868
1034
|
if (!read.ok) return { ...none, present: true };
|
|
869
1035
|
|
|
870
1036
|
const claimedSides: string[] = [];
|
|
@@ -879,6 +1045,8 @@ export function declarationClaims(bundle: unknown): DeclarationClaims {
|
|
|
879
1045
|
readable: true,
|
|
880
1046
|
caseId: typeof caseId === "string" ? caseId : null,
|
|
881
1047
|
claimedSides,
|
|
1048
|
+
version,
|
|
1049
|
+
roles: read.read.roles,
|
|
882
1050
|
};
|
|
883
1051
|
}
|
|
884
1052
|
|
|
@@ -936,17 +1104,22 @@ export function renderDeclarationSummary(check: DeclarationCheck): string {
|
|
|
936
1104
|
*/
|
|
937
1105
|
export function renderNote(note: DeclarationNote): string {
|
|
938
1106
|
switch (note.kind) {
|
|
939
|
-
case "
|
|
1107
|
+
case "disclosure_did_not_travel":
|
|
940
1108
|
return (
|
|
941
1109
|
`this pack declares ${note.count} record(s) disclosed and supplies no bytes for ` +
|
|
942
1110
|
"any of them: the bytes did not travel with the pack, so their content is UNKNOWN"
|
|
943
1111
|
);
|
|
944
|
-
case "
|
|
1112
|
+
case "pair_record_withheld":
|
|
945
1113
|
return (
|
|
946
1114
|
`the claimed hand-off at position ${note.pairIndex} names ${note.eventId} on its ` +
|
|
947
1115
|
`${note.side} side, whose record bytes this pack withholds, so what that record ` +
|
|
948
1116
|
"IS is UNKNOWN"
|
|
949
1117
|
);
|
|
1118
|
+
case "role_step_withheld":
|
|
1119
|
+
return (
|
|
1120
|
+
`${note.eventId} is signed as a step and this pack withholds its ` +
|
|
1121
|
+
"record bytes, so whether it is one is UNKNOWN"
|
|
1122
|
+
);
|
|
950
1123
|
}
|
|
951
1124
|
}
|
|
952
1125
|
|
|
@@ -960,13 +1133,13 @@ export function renderNote(note: DeclarationNote): string {
|
|
|
960
1133
|
*/
|
|
961
1134
|
export function renderContentFinding(finding: DeclarationContentFinding): string {
|
|
962
1135
|
switch (finding.kind) {
|
|
963
|
-
case "
|
|
1136
|
+
case "content_dependent_withholding":
|
|
964
1137
|
return (
|
|
965
1138
|
`${finding.eventId} is declared disclosed and no bytes for it reached this ` +
|
|
966
1139
|
"verifier, from the pack or from beside it, so whether the holder supplied " +
|
|
967
1140
|
"them is UNKNOWN"
|
|
968
1141
|
);
|
|
969
|
-
case "
|
|
1142
|
+
case "content_dependent_disclosure":
|
|
970
1143
|
return (
|
|
971
1144
|
`${finding.eventId} is declared withheld and bytes for it were handed in ` +
|
|
972
1145
|
"beside this pack, which the pack does not carry and nobody signed, so " +
|
|
@@ -977,29 +1150,52 @@ export function renderContentFinding(finding: DeclarationContentFinding): string
|
|
|
977
1150
|
|
|
978
1151
|
export function renderDeviation(deviation: DeclarationDeviation): string {
|
|
979
1152
|
switch (deviation.kind) {
|
|
980
|
-
case "
|
|
981
|
-
return
|
|
982
|
-
|
|
1153
|
+
case "declaration_malformed":
|
|
1154
|
+
return (
|
|
1155
|
+
"the declaration is not readable as " +
|
|
1156
|
+
`${deviation.version || DECLARATION_VERSION}: ${deviation.reason}`
|
|
1157
|
+
);
|
|
1158
|
+
case "declared_absent":
|
|
983
1159
|
return `${deviation.eventId} is declared and is NOT in this pack`;
|
|
984
|
-
case "
|
|
1160
|
+
case "undeclared_present":
|
|
985
1161
|
return `${deviation.eventId} is in this pack and is NOT declared`;
|
|
986
|
-
case "
|
|
1162
|
+
case "withheld_but_disclosed":
|
|
987
1163
|
return (
|
|
988
1164
|
`${deviation.eventId} is declared withheld and this pack carries its bytes ` +
|
|
989
1165
|
"in its own signed body"
|
|
990
1166
|
);
|
|
991
|
-
case "
|
|
1167
|
+
case "duplicate_event_id":
|
|
992
1168
|
return `this pack carries ${deviation.eventId} more than once`;
|
|
993
|
-
case "
|
|
1169
|
+
case "pair_record_undeclared":
|
|
994
1170
|
return (
|
|
995
1171
|
`the claimed hand-off at position ${deviation.pairIndex} names ` +
|
|
996
1172
|
`${deviation.eventId} on its ${deviation.side} side, which the declaration does not declare`
|
|
997
1173
|
);
|
|
998
|
-
case "
|
|
1174
|
+
case "pair_record_not_a_handoff":
|
|
999
1175
|
return (
|
|
1000
1176
|
`the claimed hand-off at position ${deviation.pairIndex} names ` +
|
|
1001
1177
|
`${deviation.eventId} on its ${deviation.side} side, which is not a conformant ` +
|
|
1002
1178
|
`${SIDE_EVENT_TYPES[deviation.side ?? "sent"]} record`
|
|
1003
1179
|
);
|
|
1180
|
+
case "role_handoff_unclaimed":
|
|
1181
|
+
return (
|
|
1182
|
+
`${deviation.eventId} is signed as a hand-off record and no claimed ` +
|
|
1183
|
+
"hand-off names it"
|
|
1184
|
+
);
|
|
1185
|
+
case "role_step_claimed":
|
|
1186
|
+
return (
|
|
1187
|
+
`${deviation.eventId} is signed as a step and a claimed hand-off ` +
|
|
1188
|
+
"names it as a side"
|
|
1189
|
+
);
|
|
1190
|
+
case "role_step_governed":
|
|
1191
|
+
return (
|
|
1192
|
+
`${deviation.eventId} is signed as a step and is a record the ` +
|
|
1193
|
+
"hand-off rules govern"
|
|
1194
|
+
);
|
|
1195
|
+
case "role_step_unreadable":
|
|
1196
|
+
return (
|
|
1197
|
+
`${deviation.eventId} is signed as a step and this pack carries ` +
|
|
1198
|
+
"record bytes for it that this verifier cannot read"
|
|
1199
|
+
);
|
|
1004
1200
|
}
|
|
1005
1201
|
}
|