@sanning/proof 0.15.0 → 0.16.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/dist/declaration.d.ts +1 -1
- package/dist/declaration.js +182 -3
- package/package.json +2 -2
- package/src/declaration.ts +186 -3
package/dist/declaration.d.ts
CHANGED
|
@@ -60,7 +60,7 @@ export interface DeclarationContentFinding {
|
|
|
60
60
|
eventId?: string;
|
|
61
61
|
}
|
|
62
62
|
/** 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"];
|
|
63
|
+
export declare const DECLARATION_NOTE_KINDS: readonly ["disclosure_did_not_travel", "pair_record_withheld", "role_step_withheld", "other_declaration_held", "other_declaration_withheld"];
|
|
64
64
|
export type DeclarationNoteKind = (typeof DECLARATION_NOTE_KINDS)[number];
|
|
65
65
|
/**
|
|
66
66
|
* One named non-deviation finding, as data.
|
package/dist/declaration.js
CHANGED
|
@@ -130,10 +130,10 @@
|
|
|
130
130
|
// The kernel's one reader for bytes a producer signed: strict UTF-8, no leading
|
|
131
131
|
// byte-order mark, no non-finite literal. `jcs_loads` in
|
|
132
132
|
// `src/sanning_proof/canonicalize.py` is the twin (proof#140 gate 5).
|
|
133
|
-
import { hexToBytes } from "./crypto.js";
|
|
134
|
-
import { jcsParse } from "./verifier.js";
|
|
133
|
+
import { hexToBytes, utf8 } from "./crypto.js";
|
|
134
|
+
import { jcs, jcsParse } from "./verifier.js";
|
|
135
135
|
import { isConformantHandoffRecord, isHandoffNamespace } from "./handoff.js";
|
|
136
|
-
import { hasSideContent, inBodyContentOffered } from "./side-content.js";
|
|
136
|
+
import { hasSideContent, inBodyContentOffered, sideContentEntry, } from "./side-content.js";
|
|
137
137
|
/** The first declaration schema, which every earlier reader knows. */
|
|
138
138
|
export const DECLARATION_VERSION = "sanning.declaration/v1";
|
|
139
139
|
/**
|
|
@@ -255,6 +255,23 @@ export const DECLARATION_NOTE_KINDS = [
|
|
|
255
255
|
* because its payload binding is undetermined.
|
|
256
256
|
*/
|
|
257
257
|
"role_step_withheld",
|
|
258
|
+
/**
|
|
259
|
+
* Rule 10 (v1.29, A36). A record whose signed `event_type` is `declaration`
|
|
260
|
+
* and whose content HANDED IN to this reader is exactly `JCS(object)` of a
|
|
261
|
+
* declaration object of a version this reader knows, carrying exactly that
|
|
262
|
+
* version's keys, and not this pack's own. The note names the content handed
|
|
263
|
+
* in, never the record: whether those bytes bind to the record's
|
|
264
|
+
* `content_hash` is the bundle verifier's check, and an accuser can hand in
|
|
265
|
+
* declaration-shaped bytes for an honest record. What the pack holds is what
|
|
266
|
+
* the declaration in its body says, so this moves no verdict.
|
|
267
|
+
*/
|
|
268
|
+
"other_declaration_held",
|
|
269
|
+
/**
|
|
270
|
+
* Rule 10. A record whose signed `event_type` is `declaration` and whose
|
|
271
|
+
* content did not reach this reader from either channel, so whether it is a
|
|
272
|
+
* declaration other than this pack's own is UNKNOWN.
|
|
273
|
+
*/
|
|
274
|
+
"other_declaration_withheld",
|
|
258
275
|
];
|
|
259
276
|
/**
|
|
260
277
|
* The record committed inside one `events[]` entry, or `null`.
|
|
@@ -500,6 +517,155 @@ function roleReading(p, role, claimed) {
|
|
|
500
517
|
}
|
|
501
518
|
return null;
|
|
502
519
|
}
|
|
520
|
+
/**
|
|
521
|
+
* The `event_type` a declaration record is signed under: a reserved
|
|
522
|
+
* single-segment name, and the one `@sanning/anchor`'s `bundle` anchors a
|
|
523
|
+
* declaration as.
|
|
524
|
+
*/
|
|
525
|
+
const DECLARATION_EVENT_TYPE = "declaration";
|
|
526
|
+
/**
|
|
527
|
+
* The keys a declaration object carries, under every version this reader
|
|
528
|
+
* knows: exactly these, no more and no fewer (5.4.1).
|
|
529
|
+
*/
|
|
530
|
+
const DECLARATION_KEYS = ["declaration_version", "case_id", "agents", "period", "records", "handoffs"];
|
|
531
|
+
function hasExactKeys(value, keys) {
|
|
532
|
+
if (!isObject(value))
|
|
533
|
+
return false;
|
|
534
|
+
const have = Object.keys(value);
|
|
535
|
+
return have.length === keys.length && keys.every((k) => Object.prototype.hasOwnProperty.call(value, k));
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Whether `obj` is shaped like a declaration this reader knows: one of
|
|
539
|
+
* `DECLARATION_VERSIONS` compared exactly, exactly the keys that version
|
|
540
|
+
* defines, and the value types this module reads.
|
|
541
|
+
*
|
|
542
|
+
* It is the shape test `@sanning/anchor`'s `isDeclarationShape` makes, and it
|
|
543
|
+
* checks shape only, not the 5.4.1 rules `readDeclaration` grades: a stale
|
|
544
|
+
* declaration is somebody else's claim, and this reader does not grade it, it
|
|
545
|
+
* names it.
|
|
546
|
+
*/
|
|
547
|
+
function isDeclarationShape(obj) {
|
|
548
|
+
if (!isObject(obj))
|
|
549
|
+
return false;
|
|
550
|
+
const version = obj["declaration_version"];
|
|
551
|
+
if (version !== DECLARATION_VERSION && version !== DECLARATION_VERSION_V2)
|
|
552
|
+
return false;
|
|
553
|
+
if (!hasExactKeys(obj, DECLARATION_KEYS))
|
|
554
|
+
return false;
|
|
555
|
+
if (!isId(obj["case_id"]))
|
|
556
|
+
return false;
|
|
557
|
+
const agents = obj["agents"];
|
|
558
|
+
if (!Array.isArray(agents) || !agents.every((a) => typeof a === "string"))
|
|
559
|
+
return false;
|
|
560
|
+
const period = obj["period"];
|
|
561
|
+
if (!hasExactKeys(period, ["start", "end"]))
|
|
562
|
+
return false;
|
|
563
|
+
if (typeof period["start"] !== "string" || typeof period["end"] !== "string")
|
|
564
|
+
return false;
|
|
565
|
+
const recordKeys = version === DECLARATION_VERSION_V2 ? ["event_id", "disclosure", "role"] : ["event_id", "disclosure"];
|
|
566
|
+
const records = obj["records"];
|
|
567
|
+
if (!Array.isArray(records))
|
|
568
|
+
return false;
|
|
569
|
+
for (const r of records) {
|
|
570
|
+
if (!hasExactKeys(r, recordKeys) || typeof r["event_id"] !== "string")
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
const handoffs = obj["handoffs"];
|
|
574
|
+
if (!Array.isArray(handoffs))
|
|
575
|
+
return false;
|
|
576
|
+
return handoffs.every((h) => hasExactKeys(h, ["sent_event_id", "received_event_id"]));
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* The content bytes disclosed for one record, or `null` where none are usable:
|
|
580
|
+
* `events[].content` first, then the side-content map, chosen the way the
|
|
581
|
+
* bundle verifier chooses the bytes it binds.
|
|
582
|
+
*/
|
|
583
|
+
function disclosedBytes(ev, p, content) {
|
|
584
|
+
const inBody = isObject(ev) ? ev["content"] : undefined;
|
|
585
|
+
if (typeof inBody === "string") {
|
|
586
|
+
try {
|
|
587
|
+
return hexToBytes(inBody);
|
|
588
|
+
}
|
|
589
|
+
catch {
|
|
590
|
+
// falls through to the side channel, as the bundle verifier does
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
if (!p.nameable)
|
|
594
|
+
return null;
|
|
595
|
+
const side = sideContentEntry(content, p.eventId).value;
|
|
596
|
+
if (side instanceof Uint8Array)
|
|
597
|
+
return side;
|
|
598
|
+
if (typeof side === "string") {
|
|
599
|
+
try {
|
|
600
|
+
return hexToBytes(side);
|
|
601
|
+
}
|
|
602
|
+
catch {
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
function sameBytes(a, b) {
|
|
609
|
+
if (a.length !== b.length)
|
|
610
|
+
return false;
|
|
611
|
+
for (let i = 0; i < a.length; i++)
|
|
612
|
+
if (a[i] !== b[i])
|
|
613
|
+
return false;
|
|
614
|
+
return true;
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Whether `raw` is exactly `JCS(object)` of a declaration-shaped object.
|
|
618
|
+
* Canonical bytes, not merely parseable ones: a declaration is anchored as
|
|
619
|
+
* `JCS(declaration)`, and nothing an agent writes by accident.
|
|
620
|
+
*/
|
|
621
|
+
function isDeclarationBytes(raw) {
|
|
622
|
+
try {
|
|
623
|
+
const obj = jcsParse(raw);
|
|
624
|
+
return isDeclarationShape(obj) && sameBytes(utf8(jcs(obj)), raw);
|
|
625
|
+
}
|
|
626
|
+
catch {
|
|
627
|
+
return false;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Rule 10: every record that is a declaration other than the pack's own, in
|
|
632
|
+
* bundle order.
|
|
633
|
+
*
|
|
634
|
+
* The record's `event_type` is read from its signed `event_bytes`, so a record
|
|
635
|
+
* whose bytes the pack withholds is not named: the reader cannot see what it
|
|
636
|
+
* was signed as. Its content is read from either channel; a record whose
|
|
637
|
+
* content is byte for byte `JCS` of the pack's own declaration is the pack's
|
|
638
|
+
* own and is never named.
|
|
639
|
+
*/
|
|
640
|
+
function otherDeclarationNotes(bundle, pack, declaration, content) {
|
|
641
|
+
const events = pack.length > 0 ? (bodyOf(bundle)?.events ?? []) : [];
|
|
642
|
+
let own = null;
|
|
643
|
+
try {
|
|
644
|
+
own = utf8(jcs(declaration));
|
|
645
|
+
}
|
|
646
|
+
catch {
|
|
647
|
+
own = null;
|
|
648
|
+
}
|
|
649
|
+
const notes = [];
|
|
650
|
+
pack.forEach((p, i) => {
|
|
651
|
+
if (p.record === null || p.record.event_type !== DECLARATION_EVENT_TYPE)
|
|
652
|
+
return;
|
|
653
|
+
const raw = disclosedBytes(events[i], p, content);
|
|
654
|
+
if (raw === null) {
|
|
655
|
+
// Offered and unusable is not withheld: something arrived, it is not a
|
|
656
|
+
// declaration, and the bundle verifier names what is wrong with it.
|
|
657
|
+
// Nothing arrived is the reading the reader cannot settle.
|
|
658
|
+
if (!p.disclosed)
|
|
659
|
+
notes.push({ kind: "other_declaration_withheld", eventId: p.eventId });
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
if (own !== null && sameBytes(raw, own))
|
|
663
|
+
return;
|
|
664
|
+
if (isDeclarationBytes(raw))
|
|
665
|
+
notes.push({ kind: "other_declaration_held", eventId: p.eventId });
|
|
666
|
+
});
|
|
667
|
+
return notes;
|
|
668
|
+
}
|
|
503
669
|
/**
|
|
504
670
|
* Check a pack against the declaration inside its body.
|
|
505
671
|
*
|
|
@@ -719,6 +885,11 @@ export function checkDeclaration(bundle, content) {
|
|
|
719
885
|
deviations.push({ kind, eventId });
|
|
720
886
|
}
|
|
721
887
|
}
|
|
888
|
+
// Rule 10 (5.4.2 v1.29, A36): content handed in for a record signed
|
|
889
|
+
// `declaration` that is a declaration other than this pack's own. Notes only, in bundle order, after every other note, and
|
|
890
|
+
// never a deviation: the pack holds what its own declaration says whatever
|
|
891
|
+
// one of its records says.
|
|
892
|
+
notes.push(...otherDeclarationNotes(bundle, pack, declaration, content));
|
|
722
893
|
// A proven deviation wins: it is a mismatch between what the pack claimed
|
|
723
894
|
// and what its own signed body carries, and a failing check always wins.
|
|
724
895
|
// With no deviation and a content-dependent finding, the pack lines up with
|
|
@@ -838,6 +1009,14 @@ export function renderNote(note) {
|
|
|
838
1009
|
case "role_step_withheld":
|
|
839
1010
|
return (`${note.eventId} is signed as a step and this pack withholds its ` +
|
|
840
1011
|
"record bytes, so whether it is one is UNKNOWN");
|
|
1012
|
+
case "other_declaration_held":
|
|
1013
|
+
return (`the content handed in for ${note.eventId} is a declaration that is ` +
|
|
1014
|
+
"not this pack's own: it says what some pack holds, and what this " +
|
|
1015
|
+
"pack holds is what the declaration in its body says");
|
|
1016
|
+
case "other_declaration_withheld":
|
|
1017
|
+
return (`${note.eventId} is signed as a declaration and no content for it ` +
|
|
1018
|
+
"reached this verifier, so whether it is a declaration other than " +
|
|
1019
|
+
"this pack's own is UNKNOWN");
|
|
841
1020
|
}
|
|
842
1021
|
}
|
|
843
1022
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanning/proof",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "TypeScript kernel of the Sanning verification stack: RFC 8785 (JCS) canonicalization, SHA-256, and Ed25519 verification for Verifiable Event Envelopes. Full-family verifier (inline + external commitment); accepts sanning.agent/v1 and sanning.events/v2. Conformance-gated byte-for-byte against the test-vectors corpus.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^20.19.41",
|
|
38
38
|
"typescript": "^5.4.0",
|
|
39
|
-
"vitest": "^
|
|
39
|
+
"vitest": "^4.1.11"
|
|
40
40
|
},
|
|
41
41
|
"publishConfig": {
|
|
42
42
|
"access": "public"
|
package/src/declaration.ts
CHANGED
|
@@ -131,10 +131,15 @@
|
|
|
131
131
|
// The kernel's one reader for bytes a producer signed: strict UTF-8, no leading
|
|
132
132
|
// byte-order mark, no non-finite literal. `jcs_loads` in
|
|
133
133
|
// `src/sanning_proof/canonicalize.py` is the twin (proof#140 gate 5).
|
|
134
|
-
import { hexToBytes } from "./crypto.js";
|
|
135
|
-
import { jcsParse } from "./verifier.js";
|
|
134
|
+
import { hexToBytes, utf8 } from "./crypto.js";
|
|
135
|
+
import { jcs, jcsParse } from "./verifier.js";
|
|
136
136
|
import { isConformantHandoffRecord, isHandoffNamespace, type HandoffRecord } from "./handoff.js";
|
|
137
|
-
import {
|
|
137
|
+
import {
|
|
138
|
+
hasSideContent,
|
|
139
|
+
inBodyContentOffered,
|
|
140
|
+
sideContentEntry,
|
|
141
|
+
type SideContentMap,
|
|
142
|
+
} from "./side-content.js";
|
|
138
143
|
|
|
139
144
|
/** The first declaration schema, which every earlier reader knows. */
|
|
140
145
|
export const DECLARATION_VERSION = "sanning.declaration/v1";
|
|
@@ -278,6 +283,23 @@ export const DECLARATION_NOTE_KINDS = [
|
|
|
278
283
|
* because its payload binding is undetermined.
|
|
279
284
|
*/
|
|
280
285
|
"role_step_withheld",
|
|
286
|
+
/**
|
|
287
|
+
* Rule 10 (v1.29, A36). A record whose signed `event_type` is `declaration`
|
|
288
|
+
* and whose content HANDED IN to this reader is exactly `JCS(object)` of a
|
|
289
|
+
* declaration object of a version this reader knows, carrying exactly that
|
|
290
|
+
* version's keys, and not this pack's own. The note names the content handed
|
|
291
|
+
* in, never the record: whether those bytes bind to the record's
|
|
292
|
+
* `content_hash` is the bundle verifier's check, and an accuser can hand in
|
|
293
|
+
* declaration-shaped bytes for an honest record. What the pack holds is what
|
|
294
|
+
* the declaration in its body says, so this moves no verdict.
|
|
295
|
+
*/
|
|
296
|
+
"other_declaration_held",
|
|
297
|
+
/**
|
|
298
|
+
* Rule 10. A record whose signed `event_type` is `declaration` and whose
|
|
299
|
+
* content did not reach this reader from either channel, so whether it is a
|
|
300
|
+
* declaration other than this pack's own is UNKNOWN.
|
|
301
|
+
*/
|
|
302
|
+
"other_declaration_withheld",
|
|
281
303
|
] as const;
|
|
282
304
|
export type DeclarationNoteKind = (typeof DECLARATION_NOTE_KINDS)[number];
|
|
283
305
|
|
|
@@ -698,6 +720,149 @@ function roleReading(
|
|
|
698
720
|
return null;
|
|
699
721
|
}
|
|
700
722
|
|
|
723
|
+
/**
|
|
724
|
+
* The `event_type` a declaration record is signed under: a reserved
|
|
725
|
+
* single-segment name, and the one `@sanning/anchor`'s `bundle` anchors a
|
|
726
|
+
* declaration as.
|
|
727
|
+
*/
|
|
728
|
+
const DECLARATION_EVENT_TYPE = "declaration";
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* The keys a declaration object carries, under every version this reader
|
|
732
|
+
* knows: exactly these, no more and no fewer (5.4.1).
|
|
733
|
+
*/
|
|
734
|
+
const DECLARATION_KEYS = ["declaration_version", "case_id", "agents", "period", "records", "handoffs"];
|
|
735
|
+
|
|
736
|
+
function hasExactKeys(value: unknown, keys: string[]): value is Record<string, unknown> {
|
|
737
|
+
if (!isObject(value)) return false;
|
|
738
|
+
const have = Object.keys(value);
|
|
739
|
+
return have.length === keys.length && keys.every((k) => Object.prototype.hasOwnProperty.call(value, k));
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Whether `obj` is shaped like a declaration this reader knows: one of
|
|
744
|
+
* `DECLARATION_VERSIONS` compared exactly, exactly the keys that version
|
|
745
|
+
* defines, and the value types this module reads.
|
|
746
|
+
*
|
|
747
|
+
* It is the shape test `@sanning/anchor`'s `isDeclarationShape` makes, and it
|
|
748
|
+
* checks shape only, not the 5.4.1 rules `readDeclaration` grades: a stale
|
|
749
|
+
* declaration is somebody else's claim, and this reader does not grade it, it
|
|
750
|
+
* names it.
|
|
751
|
+
*/
|
|
752
|
+
function isDeclarationShape(obj: unknown): boolean {
|
|
753
|
+
if (!isObject(obj)) return false;
|
|
754
|
+
const version = obj["declaration_version"];
|
|
755
|
+
if (version !== DECLARATION_VERSION && version !== DECLARATION_VERSION_V2) return false;
|
|
756
|
+
if (!hasExactKeys(obj, DECLARATION_KEYS)) return false;
|
|
757
|
+
if (!isId(obj["case_id"])) return false;
|
|
758
|
+
const agents = obj["agents"];
|
|
759
|
+
if (!Array.isArray(agents) || !agents.every((a) => typeof a === "string")) return false;
|
|
760
|
+
const period = obj["period"];
|
|
761
|
+
if (!hasExactKeys(period, ["start", "end"])) return false;
|
|
762
|
+
if (typeof period["start"] !== "string" || typeof period["end"] !== "string") return false;
|
|
763
|
+
const recordKeys =
|
|
764
|
+
version === DECLARATION_VERSION_V2 ? ["event_id", "disclosure", "role"] : ["event_id", "disclosure"];
|
|
765
|
+
const records = obj["records"];
|
|
766
|
+
if (!Array.isArray(records)) return false;
|
|
767
|
+
for (const r of records) {
|
|
768
|
+
if (!hasExactKeys(r, recordKeys) || typeof r["event_id"] !== "string") return false;
|
|
769
|
+
}
|
|
770
|
+
const handoffs = obj["handoffs"];
|
|
771
|
+
if (!Array.isArray(handoffs)) return false;
|
|
772
|
+
return handoffs.every((h) => hasExactKeys(h, ["sent_event_id", "received_event_id"]));
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* The content bytes disclosed for one record, or `null` where none are usable:
|
|
777
|
+
* `events[].content` first, then the side-content map, chosen the way the
|
|
778
|
+
* bundle verifier chooses the bytes it binds.
|
|
779
|
+
*/
|
|
780
|
+
function disclosedBytes(
|
|
781
|
+
ev: unknown,
|
|
782
|
+
p: PackRecord,
|
|
783
|
+
content: SideContentMap | null | undefined,
|
|
784
|
+
): Uint8Array | null {
|
|
785
|
+
const inBody = isObject(ev) ? ev["content"] : undefined;
|
|
786
|
+
if (typeof inBody === "string") {
|
|
787
|
+
try {
|
|
788
|
+
return hexToBytes(inBody);
|
|
789
|
+
} catch {
|
|
790
|
+
// falls through to the side channel, as the bundle verifier does
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (!p.nameable) return null;
|
|
794
|
+
const side = sideContentEntry(content, p.eventId).value;
|
|
795
|
+
if (side instanceof Uint8Array) return side;
|
|
796
|
+
if (typeof side === "string") {
|
|
797
|
+
try {
|
|
798
|
+
return hexToBytes(side);
|
|
799
|
+
} catch {
|
|
800
|
+
return null;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
return null;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function sameBytes(a: Uint8Array, b: Uint8Array): boolean {
|
|
807
|
+
if (a.length !== b.length) return false;
|
|
808
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
809
|
+
return true;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Whether `raw` is exactly `JCS(object)` of a declaration-shaped object.
|
|
814
|
+
* Canonical bytes, not merely parseable ones: a declaration is anchored as
|
|
815
|
+
* `JCS(declaration)`, and nothing an agent writes by accident.
|
|
816
|
+
*/
|
|
817
|
+
function isDeclarationBytes(raw: Uint8Array): boolean {
|
|
818
|
+
try {
|
|
819
|
+
const obj = jcsParse(raw);
|
|
820
|
+
return isDeclarationShape(obj) && sameBytes(utf8(jcs(obj)), raw);
|
|
821
|
+
} catch {
|
|
822
|
+
return false;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Rule 10: every record that is a declaration other than the pack's own, in
|
|
828
|
+
* bundle order.
|
|
829
|
+
*
|
|
830
|
+
* The record's `event_type` is read from its signed `event_bytes`, so a record
|
|
831
|
+
* whose bytes the pack withholds is not named: the reader cannot see what it
|
|
832
|
+
* was signed as. Its content is read from either channel; a record whose
|
|
833
|
+
* content is byte for byte `JCS` of the pack's own declaration is the pack's
|
|
834
|
+
* own and is never named.
|
|
835
|
+
*/
|
|
836
|
+
function otherDeclarationNotes(
|
|
837
|
+
bundle: unknown,
|
|
838
|
+
pack: PackRecord[],
|
|
839
|
+
declaration: unknown,
|
|
840
|
+
content: SideContentMap | null | undefined,
|
|
841
|
+
): DeclarationNote[] {
|
|
842
|
+
const events = pack.length > 0 ? (bodyOf(bundle)?.events ?? []) : [];
|
|
843
|
+
let own: Uint8Array | null = null;
|
|
844
|
+
try {
|
|
845
|
+
own = utf8(jcs(declaration));
|
|
846
|
+
} catch {
|
|
847
|
+
own = null;
|
|
848
|
+
}
|
|
849
|
+
const notes: DeclarationNote[] = [];
|
|
850
|
+
pack.forEach((p, i) => {
|
|
851
|
+
if (p.record === null || p.record.event_type !== DECLARATION_EVENT_TYPE) return;
|
|
852
|
+
const raw = disclosedBytes(events[i], p, content);
|
|
853
|
+
if (raw === null) {
|
|
854
|
+
// Offered and unusable is not withheld: something arrived, it is not a
|
|
855
|
+
// declaration, and the bundle verifier names what is wrong with it.
|
|
856
|
+
// Nothing arrived is the reading the reader cannot settle.
|
|
857
|
+
if (!p.disclosed) notes.push({ kind: "other_declaration_withheld", eventId: p.eventId });
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
if (own !== null && sameBytes(raw, own)) return;
|
|
861
|
+
if (isDeclarationBytes(raw)) notes.push({ kind: "other_declaration_held", eventId: p.eventId });
|
|
862
|
+
});
|
|
863
|
+
return notes;
|
|
864
|
+
}
|
|
865
|
+
|
|
701
866
|
/**
|
|
702
867
|
* Check a pack against the declaration inside its body.
|
|
703
868
|
*
|
|
@@ -915,6 +1080,12 @@ export function checkDeclaration(
|
|
|
915
1080
|
}
|
|
916
1081
|
}
|
|
917
1082
|
|
|
1083
|
+
// Rule 10 (5.4.2 v1.29, A36): content handed in for a record signed
|
|
1084
|
+
// `declaration` that is a declaration other than this pack's own. Notes only, in bundle order, after every other note, and
|
|
1085
|
+
// never a deviation: the pack holds what its own declaration says whatever
|
|
1086
|
+
// one of its records says.
|
|
1087
|
+
notes.push(...otherDeclarationNotes(bundle, pack, declaration, content));
|
|
1088
|
+
|
|
918
1089
|
// A proven deviation wins: it is a mismatch between what the pack claimed
|
|
919
1090
|
// and what its own signed body carries, and a failing check always wins.
|
|
920
1091
|
// With no deviation and a content-dependent finding, the pack lines up with
|
|
@@ -1120,6 +1291,18 @@ export function renderNote(note: DeclarationNote): string {
|
|
|
1120
1291
|
`${note.eventId} is signed as a step and this pack withholds its ` +
|
|
1121
1292
|
"record bytes, so whether it is one is UNKNOWN"
|
|
1122
1293
|
);
|
|
1294
|
+
case "other_declaration_held":
|
|
1295
|
+
return (
|
|
1296
|
+
`the content handed in for ${note.eventId} is a declaration that is ` +
|
|
1297
|
+
"not this pack's own: it says what some pack holds, and what this " +
|
|
1298
|
+
"pack holds is what the declaration in its body says"
|
|
1299
|
+
);
|
|
1300
|
+
case "other_declaration_withheld":
|
|
1301
|
+
return (
|
|
1302
|
+
`${note.eventId} is signed as a declaration and no content for it ` +
|
|
1303
|
+
"reached this verifier, so whether it is a declaration other than " +
|
|
1304
|
+
"this pack's own is UNKNOWN"
|
|
1305
|
+
);
|
|
1123
1306
|
}
|
|
1124
1307
|
}
|
|
1125
1308
|
|