@sanning/proof 0.17.0 → 0.19.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 +2 -2
- package/dist/case-file.js +2 -1
- package/dist/case.js +11 -4
- package/dist/chain.d.ts +28 -0
- package/dist/chain.js +61 -4
- package/dist/cli.js +226 -33
- package/dist/crypto.d.ts +17 -0
- package/dist/crypto.js +116 -16
- package/dist/declaration.d.ts +43 -4
- package/dist/declaration.js +198 -14
- package/dist/environment.d.ts +42 -0
- package/dist/environment.js +136 -0
- package/dist/evidence.d.ts +6 -0
- package/dist/evidence.js +32 -1
- package/dist/exit-codes.d.ts +18 -0
- package/dist/exit-codes.js +25 -0
- package/dist/index.d.ts +9 -5
- package/dist/index.js +21 -5
- package/dist/run-adapters.d.ts +10 -0
- package/dist/run-adapters.js +35 -0
- package/dist/run.d.ts +209 -0
- package/dist/run.js +1405 -0
- package/dist/verifier.d.ts +43 -0
- package/dist/verifier.js +113 -6
- package/dist/witness-lookup.d.ts +13 -1
- package/dist/witness-lookup.js +12 -2
- package/package.json +2 -2
- package/src/case-file.ts +2 -1
- package/src/case.ts +11 -4
- package/src/chain.ts +81 -4
- package/src/cli.ts +259 -28
- package/src/crypto.ts +125 -16
- package/src/declaration.ts +242 -12
- package/src/environment.ts +145 -0
- package/src/evidence.ts +29 -1
- package/src/exit-codes.ts +28 -0
- package/src/index.ts +31 -1
- package/src/run-adapters.ts +44 -0
- package/src/run.ts +1598 -0
- package/src/verifier.ts +102 -5
- package/src/witness-lookup.ts +18 -1
- package/vectors/README.md +37 -0
- package/vectors/sandbox-evidence.json +3161 -0
package/dist/declaration.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ChainLink } from "./chain.js";
|
|
1
2
|
import { type HandoffRecord } from "./handoff.js";
|
|
2
3
|
import { type SideContentMap } from "./side-content.js";
|
|
3
4
|
/** The first declaration schema, which every earlier reader knows. */
|
|
@@ -8,8 +9,15 @@ export declare const DECLARATION_VERSION = "sanning.declaration/v1";
|
|
|
8
9
|
* it cannot read, which is the safe direction.
|
|
9
10
|
*/
|
|
10
11
|
export declare const DECLARATION_VERSION_V2 = "sanning.declaration/v2";
|
|
12
|
+
/**
|
|
13
|
+
* v2 plus a signed `chains[]`: the `chain_key`s the holder claims are complete
|
|
14
|
+
* and in order (evidence-bundle.md 5.4.1 v1.36, A45). A reader that knows only
|
|
15
|
+
* v1 and v2 reads a v3 declaration as one it cannot read, which is the safe
|
|
16
|
+
* direction.
|
|
17
|
+
*/
|
|
18
|
+
export declare const DECLARATION_VERSION_V3 = "sanning.declaration/v3";
|
|
11
19
|
/** Every declaration schema this reader knows, in the order they were added. */
|
|
12
|
-
export declare const DECLARATION_VERSIONS: readonly ["sanning.declaration/v1", "sanning.declaration/v2"];
|
|
20
|
+
export declare const DECLARATION_VERSIONS: readonly ["sanning.declaration/v1", "sanning.declaration/v2", "sanning.declaration/v3"];
|
|
13
21
|
/**
|
|
14
22
|
* What a v2 declaration may sign a record IS, compared exactly: `handoff`, a
|
|
15
23
|
* record 4.1 governs that the pack claims as a side in `handoffs[]`, or
|
|
@@ -20,7 +28,7 @@ export type DeclarationRole = (typeof DECLARATION_ROLES)[number];
|
|
|
20
28
|
/** The three states a declaration check can reach, from the §3 vocabulary. */
|
|
21
29
|
export declare const DECLARATION_VERDICTS: readonly ["verified", "failed", "partial"];
|
|
22
30
|
export type DeclarationVerdict = (typeof DECLARATION_VERDICTS)[number];
|
|
23
|
-
export declare const DECLARATION_DEVIATION_KINDS: readonly ["declaration_malformed", "declared_absent", "undeclared_present", "withheld_but_disclosed", "duplicate_event_id", "pair_record_undeclared", "pair_record_not_a_handoff", "role_handoff_unclaimed", "role_step_claimed", "role_step_governed", "role_step_unreadable"];
|
|
31
|
+
export declare const DECLARATION_DEVIATION_KINDS: readonly ["declaration_malformed", "declared_absent", "undeclared_present", "withheld_but_disclosed", "duplicate_event_id", "pair_record_undeclared", "pair_record_not_a_handoff", "role_handoff_unclaimed", "role_step_claimed", "role_step_governed", "role_step_unreadable", "chain_second_root", "chain_fork", "chain_link_dangling", "chain_claimed_absent"];
|
|
24
32
|
export type DeclarationDeviationKind = (typeof DECLARATION_DEVIATION_KINDS)[number];
|
|
25
33
|
/**
|
|
26
34
|
* Every way a pack and the UNSIGNED content handed in beside it fail to line
|
|
@@ -60,7 +68,7 @@ export interface DeclarationContentFinding {
|
|
|
60
68
|
eventId?: string;
|
|
61
69
|
}
|
|
62
70
|
/** Everything this check reports that is NOT a deviation, as a closed set. */
|
|
63
|
-
export declare const DECLARATION_NOTE_KINDS: readonly ["disclosure_did_not_travel", "pair_record_withheld", "role_step_withheld", "other_declaration_held", "other_declaration_withheld"];
|
|
71
|
+
export declare const DECLARATION_NOTE_KINDS: readonly ["disclosure_did_not_travel", "pair_record_withheld", "role_step_withheld", "other_declaration_held", "other_declaration_withheld", "chain_claim_withheld"];
|
|
64
72
|
export type DeclarationNoteKind = (typeof DECLARATION_NOTE_KINDS)[number];
|
|
65
73
|
/**
|
|
66
74
|
* One named non-deviation finding, as data.
|
|
@@ -79,6 +87,8 @@ export interface DeclarationNote {
|
|
|
79
87
|
side?: "sent" | "received";
|
|
80
88
|
/** Which claimed hand-off, by position, for `pair_record_withheld`. */
|
|
81
89
|
pairIndex?: number;
|
|
90
|
+
/** The claimed chain, for `chain_claim_withheld`. */
|
|
91
|
+
chainKey?: string;
|
|
82
92
|
}
|
|
83
93
|
/** One deviation, as data. A surface renders its own prose or calls `renderDeviation`. */
|
|
84
94
|
export interface DeclarationDeviation {
|
|
@@ -97,6 +107,8 @@ export interface DeclarationDeviation {
|
|
|
97
107
|
* the first one this reader learned.
|
|
98
108
|
*/
|
|
99
109
|
version?: string;
|
|
110
|
+
/** The claimed chain, for the four rule 11 `chain_` deviations. */
|
|
111
|
+
chainKey?: string;
|
|
100
112
|
}
|
|
101
113
|
export interface DeclarationCheck {
|
|
102
114
|
verdict: DeclarationVerdict;
|
|
@@ -137,6 +149,12 @@ export interface DeclarationCheck {
|
|
|
137
149
|
eventId: string;
|
|
138
150
|
role: DeclarationRole;
|
|
139
151
|
}[];
|
|
152
|
+
/**
|
|
153
|
+
* Every `chain_key` a v3 declaration claims is complete and in order, in
|
|
154
|
+
* declaration order. Empty for v1 and v2, and for anything this reader could
|
|
155
|
+
* not read.
|
|
156
|
+
*/
|
|
157
|
+
chains?: string[];
|
|
140
158
|
}
|
|
141
159
|
/**
|
|
142
160
|
* One of the pack's own records: the id, and whether the pack supplied its
|
|
@@ -172,6 +190,19 @@ export interface PackRecord {
|
|
|
172
190
|
* two different readings and never one.
|
|
173
191
|
*/
|
|
174
192
|
bytesSupplied: boolean;
|
|
193
|
+
/**
|
|
194
|
+
* Whether this record's payload binding is UNDETERMINED: no `event_bytes`
|
|
195
|
+
* string and no inline envelope `payload`, the two things the envelope
|
|
196
|
+
* verifier binds `payload_hash` against. Rule 11 excuses a dangling link
|
|
197
|
+
* only for such a record's producing key.
|
|
198
|
+
*/
|
|
199
|
+
bindingUndetermined: boolean;
|
|
200
|
+
/**
|
|
201
|
+
* The envelope's signed `payload_hash` as written, or `""`. For a record
|
|
202
|
+
* whose binding is undetermined it is the identity a successor's pointer
|
|
203
|
+
* names: rule 11 excuses a dangling link only where the pointer equals it.
|
|
204
|
+
*/
|
|
205
|
+
payloadHash: string;
|
|
175
206
|
}
|
|
176
207
|
/**
|
|
177
208
|
* The pack's own records, in bundle order. Exported so the CASE layer
|
|
@@ -189,11 +220,19 @@ export declare function packRecords(bundle: unknown, content: SideContentMap | n
|
|
|
189
220
|
* and a pack that discloses them beside itself passes the map a consumer
|
|
190
221
|
* already built.
|
|
191
222
|
*
|
|
223
|
+
* `chains` is `readChains` over the SAME bundle. Only a v3 declaration that
|
|
224
|
+
* claims a chain reads it (rule 11). It is an argument rather than computed
|
|
225
|
+
* here because `readChains` is asynchronous and this check is not, and Python's
|
|
226
|
+
* `check_declaration` takes it the same way so the two kernels share one
|
|
227
|
+
* contract. A v3 declaration that claims a chain, checked without it, reads as
|
|
228
|
+
* a reader that does not know v3 reads it: `partial`, nothing named, and the
|
|
229
|
+
* pack `incomplete`. Leaving it out can never read `verified`.
|
|
230
|
+
*
|
|
192
231
|
* The verdict is this check's own. It does not restate the bundle verdict and
|
|
193
232
|
* it does not read it: a declaration can match perfectly over a pack whose
|
|
194
233
|
* signatures are forged, and saying so is `gradePack`'s job, which takes both.
|
|
195
234
|
*/
|
|
196
|
-
export declare function checkDeclaration(bundle: unknown, content?: SideContentMap | null): DeclarationCheck;
|
|
235
|
+
export declare function checkDeclaration(bundle: unknown, content?: SideContentMap | null, links?: readonly ChainLink[] | null): DeclarationCheck;
|
|
197
236
|
/**
|
|
198
237
|
* The house wording for one deviation.
|
|
199
238
|
*
|
package/dist/declaration.js
CHANGED
|
@@ -127,9 +127,6 @@
|
|
|
127
127
|
// a deviation, and the cheapest escape from that was to stop claiming pairs
|
|
128
128
|
// at all. A withheld side now gets its OWN reading, distinct from both
|
|
129
129
|
// conformant and non-conformant, and is a NAMED NOTE.
|
|
130
|
-
// The kernel's one reader for bytes a producer signed: strict UTF-8, no leading
|
|
131
|
-
// byte-order mark, no non-finite literal. `jcs_loads` in
|
|
132
|
-
// `src/sanning_proof/canonicalize.py` is the twin (proof#140 gate 5).
|
|
133
130
|
import { hexToBytes, utf8 } from "./crypto.js";
|
|
134
131
|
import { jcs, jcsParse } from "./verifier.js";
|
|
135
132
|
import { isConformantHandoffRecord, isHandoffNamespace } from "./handoff.js";
|
|
@@ -142,8 +139,21 @@ export const DECLARATION_VERSION = "sanning.declaration/v1";
|
|
|
142
139
|
* it cannot read, which is the safe direction.
|
|
143
140
|
*/
|
|
144
141
|
export const DECLARATION_VERSION_V2 = "sanning.declaration/v2";
|
|
142
|
+
/**
|
|
143
|
+
* v2 plus a signed `chains[]`: the `chain_key`s the holder claims are complete
|
|
144
|
+
* and in order (evidence-bundle.md 5.4.1 v1.36, A45). A reader that knows only
|
|
145
|
+
* v1 and v2 reads a v3 declaration as one it cannot read, which is the safe
|
|
146
|
+
* direction.
|
|
147
|
+
*/
|
|
148
|
+
export const DECLARATION_VERSION_V3 = "sanning.declaration/v3";
|
|
145
149
|
/** Every declaration schema this reader knows, in the order they were added. */
|
|
146
|
-
export const DECLARATION_VERSIONS = [
|
|
150
|
+
export const DECLARATION_VERSIONS = [
|
|
151
|
+
DECLARATION_VERSION,
|
|
152
|
+
DECLARATION_VERSION_V2,
|
|
153
|
+
DECLARATION_VERSION_V3,
|
|
154
|
+
];
|
|
155
|
+
/** The versions whose every `records[]` entry carries a signed `role`. */
|
|
156
|
+
const ROLE_VERSIONS = [DECLARATION_VERSION_V2, DECLARATION_VERSION_V3];
|
|
147
157
|
/**
|
|
148
158
|
* What a v2 declaration may sign a record IS, compared exactly: `handoff`, a
|
|
149
159
|
* record 4.1 governs that the pack claims as a side in `handoffs[]`, or
|
|
@@ -191,6 +201,18 @@ export const DECLARATION_DEVIATION_KINDS = [
|
|
|
191
201
|
* read under rule 7's parser rule.
|
|
192
202
|
*/
|
|
193
203
|
"role_step_unreadable",
|
|
204
|
+
/**
|
|
205
|
+
* Rule 11 (v1.36, A45), `sanning.declaration/v3` only. The holder signed
|
|
206
|
+
* that a chain is complete and in order, and a second record under that key,
|
|
207
|
+
* under any producing key, begins it.
|
|
208
|
+
*/
|
|
209
|
+
"chain_second_root",
|
|
210
|
+
/** A record of a claimed chain is the second successor of one predecessor. */
|
|
211
|
+
"chain_fork",
|
|
212
|
+
/** A record of a claimed chain points at a predecessor its chain does not present in the pack. */
|
|
213
|
+
"chain_link_dangling",
|
|
214
|
+
/** A claimed chain of which the pack presents no record at all. */
|
|
215
|
+
"chain_claimed_absent",
|
|
194
216
|
];
|
|
195
217
|
/**
|
|
196
218
|
* Every way a pack and the UNSIGNED content handed in beside it fail to line
|
|
@@ -272,6 +294,14 @@ export const DECLARATION_NOTE_KINDS = [
|
|
|
272
294
|
* declaration other than this pack's own is UNKNOWN.
|
|
273
295
|
*/
|
|
274
296
|
"other_declaration_withheld",
|
|
297
|
+
/**
|
|
298
|
+
* Rule 11 (v1.36, A45). A claimed chain has a dangling link, and a record
|
|
299
|
+
* signed by the same producing key has an undetermined payload binding. That
|
|
300
|
+
* record takes no part in a chain, so whether the missing link is it is
|
|
301
|
+
* UNKNOWN. Moves nothing: the undetermined binding holds the pack at
|
|
302
|
+
* `incomplete`.
|
|
303
|
+
*/
|
|
304
|
+
"chain_claim_withheld",
|
|
275
305
|
];
|
|
276
306
|
/**
|
|
277
307
|
* The record committed inside one `events[]` entry, or `null`.
|
|
@@ -367,6 +397,8 @@ export function packRecords(bundle, content) {
|
|
|
367
397
|
publicKey: typeof key === "string" ? key : "",
|
|
368
398
|
record: committedRecord(rawBytes),
|
|
369
399
|
bytesSupplied: typeof rawBytes === "string" && rawBytes.length > 0,
|
|
400
|
+
bindingUndetermined: typeof rawBytes !== "string" && !(isObject(ev?.envelope) && "payload" in ev.envelope),
|
|
401
|
+
payloadHash: typeof ev?.envelope?.payload_hash === "string" ? ev.envelope.payload_hash : "",
|
|
370
402
|
};
|
|
371
403
|
});
|
|
372
404
|
}
|
|
@@ -393,7 +425,7 @@ function readDeclaration(declaration, version = DECLARATION_VERSION) {
|
|
|
393
425
|
// or `step` (5.4.1 v1.20, A21); under v1 the member is not read at all, so a
|
|
394
426
|
// v1 declaration carrying `role` reads exactly as the same object without it.
|
|
395
427
|
const roles = [];
|
|
396
|
-
const v2 = version
|
|
428
|
+
const v2 = ROLE_VERSIONS.includes(version);
|
|
397
429
|
const note = (r) => reasons.push(r);
|
|
398
430
|
if (!isId(declaration["case_id"]))
|
|
399
431
|
note("case_id is not a non-empty string");
|
|
@@ -472,7 +504,31 @@ function readDeclaration(declaration, version = DECLARATION_VERSION) {
|
|
|
472
504
|
pairs.push({ sent: sent, received: received });
|
|
473
505
|
}
|
|
474
506
|
}
|
|
475
|
-
|
|
507
|
+
// v3 is v2 plus a required `chains[]` (5.4.1 v1.36, A45); v1 and v2 do not
|
|
508
|
+
// read the member.
|
|
509
|
+
const chains = [];
|
|
510
|
+
if (version === DECLARATION_VERSION_V3) {
|
|
511
|
+
const rawChains = declaration["chains"];
|
|
512
|
+
if (!Array.isArray(rawChains))
|
|
513
|
+
note("chains is not an array");
|
|
514
|
+
else {
|
|
515
|
+
for (const [i, c] of rawChains.entries()) {
|
|
516
|
+
if (!isObject(c) || !isId(c["chain_key"])) {
|
|
517
|
+
note(`chains[${i}] carries no chain_key`);
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
const chainKey = c["chain_key"];
|
|
521
|
+
if (chains.includes(chainKey)) {
|
|
522
|
+
note(`chains[${i}] claims ${chainKey} a second time`);
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
chains.push(chainKey);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return reasons.length > 0
|
|
530
|
+
? { ok: false, reasons }
|
|
531
|
+
: { ok: true, read: { records, pairs, roles, chains } };
|
|
476
532
|
}
|
|
477
533
|
// ---- The check ---------------------------------------------------------------
|
|
478
534
|
const noDeclaration = () => ({
|
|
@@ -485,6 +541,7 @@ const noDeclaration = () => ({
|
|
|
485
541
|
declaredWithheld: [],
|
|
486
542
|
version: null,
|
|
487
543
|
roles: [],
|
|
544
|
+
chains: [],
|
|
488
545
|
});
|
|
489
546
|
/**
|
|
490
547
|
* The `declaration_version` of an object this reader knows, or `null`: a
|
|
@@ -495,7 +552,9 @@ function knownVersion(declaration) {
|
|
|
495
552
|
if (!isObject(declaration))
|
|
496
553
|
return null;
|
|
497
554
|
const version = declaration["declaration_version"];
|
|
498
|
-
return
|
|
555
|
+
return typeof version === "string" && DECLARATION_VERSIONS.includes(version)
|
|
556
|
+
? version
|
|
557
|
+
: null;
|
|
499
558
|
}
|
|
500
559
|
/**
|
|
501
560
|
* Rule 9 over one record the pack holds: the deviation or note it earns, or
|
|
@@ -548,9 +607,10 @@ function isDeclarationShape(obj) {
|
|
|
548
607
|
if (!isObject(obj))
|
|
549
608
|
return false;
|
|
550
609
|
const version = obj["declaration_version"];
|
|
551
|
-
if (version !==
|
|
610
|
+
if (typeof version !== "string" || !DECLARATION_VERSIONS.includes(version))
|
|
552
611
|
return false;
|
|
553
|
-
|
|
612
|
+
const v3 = version === DECLARATION_VERSION_V3;
|
|
613
|
+
if (!hasExactKeys(obj, v3 ? [...DECLARATION_KEYS, "chains"] : DECLARATION_KEYS))
|
|
554
614
|
return false;
|
|
555
615
|
if (!isId(obj["case_id"]))
|
|
556
616
|
return false;
|
|
@@ -562,7 +622,9 @@ function isDeclarationShape(obj) {
|
|
|
562
622
|
return false;
|
|
563
623
|
if (typeof period["start"] !== "string" || typeof period["end"] !== "string")
|
|
564
624
|
return false;
|
|
565
|
-
const recordKeys = version
|
|
625
|
+
const recordKeys = ROLE_VERSIONS.includes(version)
|
|
626
|
+
? ["event_id", "disclosure", "role"]
|
|
627
|
+
: ["event_id", "disclosure"];
|
|
566
628
|
const records = obj["records"];
|
|
567
629
|
if (!Array.isArray(records))
|
|
568
630
|
return false;
|
|
@@ -573,7 +635,13 @@ function isDeclarationShape(obj) {
|
|
|
573
635
|
const handoffs = obj["handoffs"];
|
|
574
636
|
if (!Array.isArray(handoffs))
|
|
575
637
|
return false;
|
|
576
|
-
|
|
638
|
+
if (!handoffs.every((h) => hasExactKeys(h, ["sent_event_id", "received_event_id"])))
|
|
639
|
+
return false;
|
|
640
|
+
if (!v3)
|
|
641
|
+
return true;
|
|
642
|
+
const chains = obj["chains"];
|
|
643
|
+
return (Array.isArray(chains) &&
|
|
644
|
+
chains.every((c) => hasExactKeys(c, ["chain_key"]) && typeof c["chain_key"] === "string"));
|
|
577
645
|
}
|
|
578
646
|
/**
|
|
579
647
|
* The content bytes disclosed for one record, or `null` where none are usable:
|
|
@@ -674,11 +742,19 @@ function otherDeclarationNotes(bundle, pack, declaration, content) {
|
|
|
674
742
|
* and a pack that discloses them beside itself passes the map a consumer
|
|
675
743
|
* already built.
|
|
676
744
|
*
|
|
745
|
+
* `chains` is `readChains` over the SAME bundle. Only a v3 declaration that
|
|
746
|
+
* claims a chain reads it (rule 11). It is an argument rather than computed
|
|
747
|
+
* here because `readChains` is asynchronous and this check is not, and Python's
|
|
748
|
+
* `check_declaration` takes it the same way so the two kernels share one
|
|
749
|
+
* contract. A v3 declaration that claims a chain, checked without it, reads as
|
|
750
|
+
* a reader that does not know v3 reads it: `partial`, nothing named, and the
|
|
751
|
+
* pack `incomplete`. Leaving it out can never read `verified`.
|
|
752
|
+
*
|
|
677
753
|
* The verdict is this check's own. It does not restate the bundle verdict and
|
|
678
754
|
* it does not read it: a declaration can match perfectly over a pack whose
|
|
679
755
|
* signatures are forged, and saying so is `gradePack`'s job, which takes both.
|
|
680
756
|
*/
|
|
681
|
-
export function checkDeclaration(bundle, content) {
|
|
757
|
+
export function checkDeclaration(bundle, content, links) {
|
|
682
758
|
// `body.declaration` as WRITTEN, not as hoped. The key being absent and the
|
|
683
759
|
// key holding a string, an array or `null` are graded differently and must
|
|
684
760
|
// not collapse into one another: a body with no `declaration` key claimed
|
|
@@ -721,9 +797,10 @@ export function checkDeclaration(bundle, content) {
|
|
|
721
797
|
declaredWithheld: [],
|
|
722
798
|
version,
|
|
723
799
|
roles: [],
|
|
800
|
+
chains: [],
|
|
724
801
|
};
|
|
725
802
|
}
|
|
726
|
-
const { records, pairs, roles } = read.read;
|
|
803
|
+
const { records, pairs, roles, chains: claimedChains } = read.read;
|
|
727
804
|
const declared = records.map((r) => r.eventId);
|
|
728
805
|
const declaredSet = new Set(declared);
|
|
729
806
|
const pack = packRecords(bundle, content);
|
|
@@ -885,6 +962,18 @@ export function checkDeclaration(bundle, content) {
|
|
|
885
962
|
deviations.push({ kind, eventId });
|
|
886
963
|
}
|
|
887
964
|
}
|
|
965
|
+
// Rule 11 (5.4.2 v1.36, A45), `sanning.declaration/v3` only: a chain the
|
|
966
|
+
// holder signed as complete and in order, against the chain reading of the
|
|
967
|
+
// same pack. Its deviations follow rule 9's, and its notes follow rule 9's
|
|
968
|
+
// and precede rule 10's, both in `chains[]` order.
|
|
969
|
+
// A caller that passed no links leaves the claim UNREAD: rules 1 to 10 still
|
|
970
|
+
// run, and the verdict below is never `verified`.
|
|
971
|
+
const unread = claimedChains.length > 0 && (links === undefined || links === null);
|
|
972
|
+
if (claimedChains.length > 0 && links !== undefined && links !== null) {
|
|
973
|
+
const found = chainClaimFindings(claimedChains, links, pack);
|
|
974
|
+
deviations.push(...found.deviations);
|
|
975
|
+
notes.push(...found.notes);
|
|
976
|
+
}
|
|
888
977
|
// Rule 10 (5.4.2 v1.29, A36): content handed in for a record signed
|
|
889
978
|
// `declaration` that is a declaration other than this pack's own. Notes only, in bundle order, after every other note, and
|
|
890
979
|
// never a deviation: the pack holds what its own declaration says whatever
|
|
@@ -896,7 +985,11 @@ export function checkDeclaration(bundle, content) {
|
|
|
896
985
|
// its claim everywhere a signature reaches and does not line up somewhere
|
|
897
986
|
// only unsigned bytes could settle, which is UNDETERMINED: `partial` is
|
|
898
987
|
// 3's word for that, and it adds none.
|
|
899
|
-
const verdict = deviations.length > 0
|
|
988
|
+
const verdict = deviations.length > 0
|
|
989
|
+
? "failed"
|
|
990
|
+
: contentFindings.length > 0 || unread
|
|
991
|
+
? "partial"
|
|
992
|
+
: "verified";
|
|
900
993
|
return {
|
|
901
994
|
verdict,
|
|
902
995
|
deviations,
|
|
@@ -907,8 +1000,73 @@ export function checkDeclaration(bundle, content) {
|
|
|
907
1000
|
declaredWithheld: records.filter((r) => r.withheld).map((r) => r.eventId),
|
|
908
1001
|
version,
|
|
909
1002
|
roles,
|
|
1003
|
+
chains: claimedChains,
|
|
910
1004
|
};
|
|
911
1005
|
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Rule 11 over every claimed `chain_key`, in `chains[]` order. The twin of
|
|
1008
|
+
* Python's `_chain_claim_findings`, which states the rule in full.
|
|
1009
|
+
*
|
|
1010
|
+
* A claim covers every record whose OWN signed linkage names the key
|
|
1011
|
+
* (`source === "record"`), under every producing key; an adapter's metadata
|
|
1012
|
+
* linkage is never read for a claim. Under a claim, in bundle order: every
|
|
1013
|
+
* record after the first that begins the chain is a `chain_second_root`; a
|
|
1014
|
+
* record whose predecessor already has an earlier successor in its chain is a
|
|
1015
|
+
* `chain_fork`; a record whose pointer resolves to no record of its own chain
|
|
1016
|
+
* (one producing key, one `chain_key`) is a `chain_link_dangling`, with no
|
|
1017
|
+
* window allowance; a claimed key with no such record is
|
|
1018
|
+
* `chain_claimed_absent`. Links are read from the record each envelope binds,
|
|
1019
|
+
* `event_bytes` or an inline `payload`. A dangling link is the note
|
|
1020
|
+
* `chain_claim_withheld` instead, once per key, only where its pointer equals
|
|
1021
|
+
* the signed `payload_hash` of a same-key record whose binding is
|
|
1022
|
+
* undetermined: exactly the missing link. Nothing else is excused.
|
|
1023
|
+
*/
|
|
1024
|
+
function chainClaimFindings(claimed, links, pack) {
|
|
1025
|
+
const deviations = [];
|
|
1026
|
+
const notes = [];
|
|
1027
|
+
const withheldLinks = new Set(pack
|
|
1028
|
+
.filter((p) => p.bindingUndetermined && p.publicKey !== "" && p.payloadHash !== "")
|
|
1029
|
+
.map((p) => JSON.stringify([p.publicKey, p.payloadHash])));
|
|
1030
|
+
for (const chainKey of claimed) {
|
|
1031
|
+
const members = links
|
|
1032
|
+
.filter((l) => l.source === "record" && l.chainKey === chainKey)
|
|
1033
|
+
.sort((a, b) => a.order - b.order);
|
|
1034
|
+
if (members.length === 0) {
|
|
1035
|
+
deviations.push({ kind: "chain_claimed_absent", chainKey });
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
const idsByKey = new Map();
|
|
1039
|
+
for (const m of members) {
|
|
1040
|
+
if (!idsByKey.has(m.publicKey))
|
|
1041
|
+
idsByKey.set(m.publicKey, new Set());
|
|
1042
|
+
idsByKey.get(m.publicKey).add(m.selfId);
|
|
1043
|
+
}
|
|
1044
|
+
let roots = 0;
|
|
1045
|
+
const successors = new Set();
|
|
1046
|
+
let excused = false;
|
|
1047
|
+
for (const m of members) {
|
|
1048
|
+
if (m.prev === null) {
|
|
1049
|
+
roots++;
|
|
1050
|
+
if (roots > 1)
|
|
1051
|
+
deviations.push({ kind: "chain_second_root", eventId: m.eventId, chainKey });
|
|
1052
|
+
continue;
|
|
1053
|
+
}
|
|
1054
|
+
const edge = JSON.stringify([m.publicKey, m.prev]);
|
|
1055
|
+
if (successors.has(edge))
|
|
1056
|
+
deviations.push({ kind: "chain_fork", eventId: m.eventId, chainKey });
|
|
1057
|
+
successors.add(edge);
|
|
1058
|
+
if (!idsByKey.get(m.publicKey).has(m.prev)) {
|
|
1059
|
+
if (withheldLinks.has(JSON.stringify([m.publicKey, m.prev])))
|
|
1060
|
+
excused = true;
|
|
1061
|
+
else
|
|
1062
|
+
deviations.push({ kind: "chain_link_dangling", eventId: m.eventId, chainKey });
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
if (excused)
|
|
1066
|
+
notes.push({ kind: "chain_claim_withheld", chainKey });
|
|
1067
|
+
}
|
|
1068
|
+
return { deviations, notes };
|
|
1069
|
+
}
|
|
912
1070
|
/**
|
|
913
1071
|
* Read the claim a pack's declaration makes, without grading the pack.
|
|
914
1072
|
*
|
|
@@ -986,6 +1144,13 @@ export function renderDeclarationSummary(check) {
|
|
|
986
1144
|
`and in ${check.contentFindings.length} way(s) it does not line up with the ` +
|
|
987
1145
|
"unsigned content handed in beside it, which anyone could have supplied");
|
|
988
1146
|
}
|
|
1147
|
+
if (check.verdict === "partial" && (check.chains ?? []).length > 0) {
|
|
1148
|
+
// Read before the branch below: the declaration WAS read, and its chain
|
|
1149
|
+
// claim was not checked because the caller gave no reading.
|
|
1150
|
+
return ("this pack holds what it declares, and says its chains are complete " +
|
|
1151
|
+
"and in order, which this verifier did not check, so whether they " +
|
|
1152
|
+
"are is UNKNOWN");
|
|
1153
|
+
}
|
|
989
1154
|
if (check.verdict === "partial") {
|
|
990
1155
|
return ("this pack says what it holds in a declaration this verifier cannot read, " +
|
|
991
1156
|
"so whether it is complete is UNKNOWN");
|
|
@@ -1017,6 +1182,11 @@ export function renderNote(note) {
|
|
|
1017
1182
|
return (`${note.eventId} is signed as a declaration and no content for it ` +
|
|
1018
1183
|
"reached this verifier, so whether it is a declaration other than " +
|
|
1019
1184
|
"this pack's own is UNKNOWN");
|
|
1185
|
+
case "chain_claim_withheld":
|
|
1186
|
+
return (`this pack declares chain ${note.chainKey} complete and in order, ` +
|
|
1187
|
+
"and a link in it points at a record its producer signed and this " +
|
|
1188
|
+
"pack withholds: the pointer equals that record's signed " +
|
|
1189
|
+
"payload_hash, and the record's bytes are not here to read");
|
|
1020
1190
|
}
|
|
1021
1191
|
}
|
|
1022
1192
|
/**
|
|
@@ -1072,5 +1242,19 @@ export function renderDeviation(deviation) {
|
|
|
1072
1242
|
case "role_step_unreadable":
|
|
1073
1243
|
return (`${deviation.eventId} is signed as a step and this pack carries ` +
|
|
1074
1244
|
"record bytes for it that this verifier cannot read");
|
|
1245
|
+
case "chain_second_root":
|
|
1246
|
+
return (`${deviation.eventId} begins chain ${deviation.chainKey} a second ` +
|
|
1247
|
+
"time, and the declaration says that chain is complete and in order");
|
|
1248
|
+
case "chain_fork":
|
|
1249
|
+
return (`${deviation.eventId} in chain ${deviation.chainKey} points at a ` +
|
|
1250
|
+
"record another record of that chain already points at, and the " +
|
|
1251
|
+
"declaration says that chain is complete and in order");
|
|
1252
|
+
case "chain_link_dangling":
|
|
1253
|
+
return (`${deviation.eventId} in chain ${deviation.chainKey} points at a ` +
|
|
1254
|
+
"record of that chain this pack does not present, and the " +
|
|
1255
|
+
"declaration says that chain is complete and in order");
|
|
1256
|
+
case "chain_claimed_absent":
|
|
1257
|
+
return (`the declaration says chain ${deviation.chainKey} is complete and ` +
|
|
1258
|
+
"in order, and this pack presents no record of it");
|
|
1075
1259
|
}
|
|
1076
1260
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type EvidenceBundleResult } from "./evidence.js";
|
|
2
|
+
/** `"dev"` when every envelope is signed `dev`, `"mixed"` when only some are. */
|
|
3
|
+
export type EnvironmentMarker = "dev" | "mixed";
|
|
4
|
+
/** The headline `verify` prints for sandbox evidence in place of `VERIFIED`. */
|
|
5
|
+
export declare const SANDBOX_HEADLINE = "SANDBOX";
|
|
6
|
+
/** The title `case` prints for sandbox evidence in place of `Verified` (case titles are title case). */
|
|
7
|
+
export declare const SANDBOX_TITLE = "Sandbox";
|
|
8
|
+
/** The one line printed with it, and with any other headline, wherever the marker is present. */
|
|
9
|
+
export declare const SANDBOX_LINE = "Sandbox evidence (environment dev): not production evidence";
|
|
10
|
+
/**
|
|
11
|
+
* The signed envelopes a pack holds: every checkpoint and event envelope of an
|
|
12
|
+
* anchor-trace body, every event envelope of a stamp-evidence body, the same
|
|
13
|
+
* inside an export's inline source bundle, and an agent proof's checkpoint
|
|
14
|
+
* envelope. A stamp-trace body holds none. A value that is not an object is
|
|
15
|
+
* not an envelope and is not counted.
|
|
16
|
+
*/
|
|
17
|
+
export declare function packEnvelopes(bundle: unknown): Record<string, unknown>[];
|
|
18
|
+
/**
|
|
19
|
+
* The environment marker over one or more packs: `"dev"` where every envelope
|
|
20
|
+
* they hold is signed `environment: "dev"`, `"mixed"` where some are and some
|
|
21
|
+
* are not (`production`, or no `environment` at all), and `null` where none
|
|
22
|
+
* is, which is every production pack and every pack that holds no envelope.
|
|
23
|
+
*/
|
|
24
|
+
export declare function environmentMarker(bundles: readonly unknown[]): EnvironmentMarker | null;
|
|
25
|
+
/**
|
|
26
|
+
* Whether a body's only unsettled witnesses are simulated: the body has events
|
|
27
|
+
* and witnesses (checkpoints, stamps or both), every witness is named, at
|
|
28
|
+
* least one is simulated, and every other one a gateway confirmed. Then the
|
|
29
|
+
* one thing that keeps the bundle from `verified` is a simulated witness
|
|
30
|
+
* nobody can look up: no mismatch, no unreachable gateway, no witness of
|
|
31
|
+
* another kind, no checkpoint or stamp witnessed by nobody.
|
|
32
|
+
*/
|
|
33
|
+
export declare function onlySimulatedUnsettled(result: EvidenceBundleResult): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Whether a run with gateways counted only simulated witnesses, so nothing
|
|
36
|
+
* was looked up on a chain: gateways were passed, the run is not counted as
|
|
37
|
+
* checked on a chain, and it holds a simulated witness. A run that stopped
|
|
38
|
+
* before its witnesses were read (a bad wrapper signature or `body_hash`, an
|
|
39
|
+
* unknown `body_type`, an export whose source is not inline) holds none, so it
|
|
40
|
+
* is not one, and prints what it printed before simulated witnesses existed.
|
|
41
|
+
*/
|
|
42
|
+
export declare function simulatedOnlyRun(result: EvidenceBundleResult, gateways: readonly string[]): boolean;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// The environment marker: is this sandbox evidence?
|
|
2
|
+
//
|
|
3
|
+
// `environment` is an OPTIONAL skeleton field inside every envelope's signed
|
|
4
|
+
// scope, `"dev"` or `"production"` (envelope-spec §2, A1 and A9). The spec
|
|
5
|
+
// says a verifier MAY surface the value as `environment_marker` in its result
|
|
6
|
+
// and a profile MAY refuse to count dev-marked envelopes toward production
|
|
7
|
+
// evidence. Both commands now do the first, and print a pack or case that
|
|
8
|
+
// holds a `dev` envelope as sandbox evidence rather than as `VERIFIED`.
|
|
9
|
+
//
|
|
10
|
+
// The marker is read from the signed envelopes the pack holds, never from a
|
|
11
|
+
// wrapper field, a pack name or anything else the holder could write without a
|
|
12
|
+
// key. It never moves the verdict word: a sandbox pack that verifies is still
|
|
13
|
+
// `verified`, and the commands map that to the headline `SANDBOX` and exit 5
|
|
14
|
+
// (`sandboxExit` in `./exit-codes.ts`). The Python twin is
|
|
15
|
+
// `src/sanning_proof/environment.py`.
|
|
16
|
+
import { isAgentProofSpec } from "./agent-proof.js";
|
|
17
|
+
import { ANCHOR_TRACE_BODY_TYPE, EXPORT_BODY_TYPE, SIMULATED_WITNESS_KIND, STAMP_EVIDENCE_BODY_TYPE, } from "./evidence.js";
|
|
18
|
+
/** The headline `verify` prints for sandbox evidence in place of `VERIFIED`. */
|
|
19
|
+
export const SANDBOX_HEADLINE = "SANDBOX";
|
|
20
|
+
/** The title `case` prints for sandbox evidence in place of `Verified` (case titles are title case). */
|
|
21
|
+
export const SANDBOX_TITLE = "Sandbox";
|
|
22
|
+
/** The one line printed with it, and with any other headline, wherever the marker is present. */
|
|
23
|
+
export const SANDBOX_LINE = "Sandbox evidence (environment dev): not production evidence";
|
|
24
|
+
// The same bound `resolveTraceEvents` puts on unwrapping export wrappers.
|
|
25
|
+
const MAX_EXPORT_UNWRAP = 4;
|
|
26
|
+
function isObject(v) {
|
|
27
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
28
|
+
}
|
|
29
|
+
function own(o, key) {
|
|
30
|
+
return Object.prototype.hasOwnProperty.call(o, key) ? o[key] : undefined;
|
|
31
|
+
}
|
|
32
|
+
function entryEnvelopes(list, out) {
|
|
33
|
+
if (!Array.isArray(list))
|
|
34
|
+
return;
|
|
35
|
+
for (const entry of list) {
|
|
36
|
+
if (!isObject(entry))
|
|
37
|
+
continue;
|
|
38
|
+
const env = own(entry, "envelope");
|
|
39
|
+
if (isObject(env))
|
|
40
|
+
out.push(env);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The signed envelopes a pack holds: every checkpoint and event envelope of an
|
|
45
|
+
* anchor-trace body, every event envelope of a stamp-evidence body, the same
|
|
46
|
+
* inside an export's inline source bundle, and an agent proof's checkpoint
|
|
47
|
+
* envelope. A stamp-trace body holds none. A value that is not an object is
|
|
48
|
+
* not an envelope and is not counted.
|
|
49
|
+
*/
|
|
50
|
+
export function packEnvelopes(bundle) {
|
|
51
|
+
const out = [];
|
|
52
|
+
if (isObject(bundle) && isAgentProofSpec(own(bundle, "spec_version"))) {
|
|
53
|
+
const env = own(bundle, "checkpoint_envelope");
|
|
54
|
+
if (isObject(env))
|
|
55
|
+
out.push(env);
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
let cur = bundle;
|
|
59
|
+
for (let depth = 0; depth < MAX_EXPORT_UNWRAP; depth++) {
|
|
60
|
+
if (!isObject(cur))
|
|
61
|
+
return out;
|
|
62
|
+
const bodyType = own(cur, "body_type");
|
|
63
|
+
const body = own(cur, "body");
|
|
64
|
+
if (!isObject(body))
|
|
65
|
+
return out;
|
|
66
|
+
if (bodyType === ANCHOR_TRACE_BODY_TYPE) {
|
|
67
|
+
entryEnvelopes(own(body, "checkpoints"), out);
|
|
68
|
+
entryEnvelopes(own(body, "events"), out);
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
if (bodyType === STAMP_EVIDENCE_BODY_TYPE) {
|
|
72
|
+
entryEnvelopes(own(body, "events"), out);
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
if (bodyType !== EXPORT_BODY_TYPE)
|
|
76
|
+
return out;
|
|
77
|
+
cur = own(body, "source_bundle");
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The environment marker over one or more packs: `"dev"` where every envelope
|
|
83
|
+
* they hold is signed `environment: "dev"`, `"mixed"` where some are and some
|
|
84
|
+
* are not (`production`, or no `environment` at all), and `null` where none
|
|
85
|
+
* is, which is every production pack and every pack that holds no envelope.
|
|
86
|
+
*/
|
|
87
|
+
export function environmentMarker(bundles) {
|
|
88
|
+
let total = 0;
|
|
89
|
+
let dev = 0;
|
|
90
|
+
for (const bundle of bundles) {
|
|
91
|
+
for (const env of packEnvelopes(bundle)) {
|
|
92
|
+
total++;
|
|
93
|
+
if (own(env, "environment") === "dev")
|
|
94
|
+
dev++;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (dev === 0)
|
|
98
|
+
return null;
|
|
99
|
+
return dev === total ? "dev" : "mixed";
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Whether a body's only unsettled witnesses are simulated: the body has events
|
|
103
|
+
* and witnesses (checkpoints, stamps or both), every witness is named, at
|
|
104
|
+
* least one is simulated, and every other one a gateway confirmed. Then the
|
|
105
|
+
* one thing that keeps the bundle from `verified` is a simulated witness
|
|
106
|
+
* nobody can look up: no mismatch, no unreachable gateway, no witness of
|
|
107
|
+
* another kind, no checkpoint or stamp witnessed by nobody.
|
|
108
|
+
*/
|
|
109
|
+
export function onlySimulatedUnsettled(result) {
|
|
110
|
+
const witnesses = [...result.checkpoints, ...(result.stamps ?? [])];
|
|
111
|
+
if (witnesses.length === 0 || result.events.length === 0)
|
|
112
|
+
return false;
|
|
113
|
+
let simulated = 0;
|
|
114
|
+
for (const w of witnesses) {
|
|
115
|
+
if (w.anchorRef === null)
|
|
116
|
+
return false;
|
|
117
|
+
if (w.anchorRef.kind === SIMULATED_WITNESS_KIND)
|
|
118
|
+
simulated++;
|
|
119
|
+
else if (w.onChain?.rollup !== "confirm")
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
return simulated > 0;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Whether a run with gateways counted only simulated witnesses, so nothing
|
|
126
|
+
* was looked up on a chain: gateways were passed, the run is not counted as
|
|
127
|
+
* checked on a chain, and it holds a simulated witness. A run that stopped
|
|
128
|
+
* before its witnesses were read (a bad wrapper signature or `body_hash`, an
|
|
129
|
+
* unknown `body_type`, an export whose source is not inline) holds none, so it
|
|
130
|
+
* is not one, and prints what it printed before simulated witnesses existed.
|
|
131
|
+
*/
|
|
132
|
+
export function simulatedOnlyRun(result, gateways) {
|
|
133
|
+
if (gateways.length === 0 || result.onChainChecked)
|
|
134
|
+
return false;
|
|
135
|
+
return [...result.checkpoints, ...(result.stamps ?? [])].some((w) => w.anchorRef?.kind === SIMULATED_WITNESS_KIND);
|
|
136
|
+
}
|
package/dist/evidence.d.ts
CHANGED
|
@@ -464,6 +464,12 @@ export declare function coSignaturesPresent(b: unknown): boolean;
|
|
|
464
464
|
/** evidence-bundle §2 (A31). Byte-identical to the Python kernel's. */
|
|
465
465
|
export declare const GENERATED_AT_MALFORMED = "malformed generated_at: not an RFC 3339 UTC date-time with Z naming a real date and time (evidence-bundle \u00A72)";
|
|
466
466
|
export declare function verifyEvidenceBundle(bundle: unknown, options?: VerifyEvidenceOptions): Promise<EvidenceBundleResult>;
|
|
467
|
+
/**
|
|
468
|
+
* The `anchor_ref.kind` a sandbox plane writes for a witness it simulated
|
|
469
|
+
* (chain id `sanning-sandbox`). Never looked up and never counted as checked
|
|
470
|
+
* on a chain.
|
|
471
|
+
*/
|
|
472
|
+
export declare const SIMULATED_WITNESS_KIND = "simulated";
|
|
467
473
|
export type DisclosureStatus = "verified" | "not_disclosed" | "undetermined" | "failed";
|
|
468
474
|
export interface DisclosedEventContent {
|
|
469
475
|
eventId: string;
|