attenu-guard 0.13.1 → 0.13.2
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/CHANGELOG.md +10 -0
- package/dist/cjs/audit.d.ts +15 -1
- package/dist/cjs/audit.d.ts.map +1 -1
- package/dist/cjs/audit.js +26 -3
- package/dist/cjs/audit.js.map +1 -1
- package/dist/cjs/ceilings.d.ts +7 -0
- package/dist/cjs/ceilings.d.ts.map +1 -1
- package/dist/cjs/ceilings.js +1 -0
- package/dist/cjs/ceilings.js.map +1 -1
- package/dist/cjs/cli.d.ts.map +1 -1
- package/dist/cjs/cli.js +11 -4
- package/dist/cjs/cli.js.map +1 -1
- package/dist/cjs/evidence.d.ts +35 -9
- package/dist/cjs/evidence.d.ts.map +1 -1
- package/dist/cjs/evidence.js +332 -38
- package/dist/cjs/evidence.js.map +1 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +3 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/audit.d.ts +15 -1
- package/dist/esm/audit.d.ts.map +1 -1
- package/dist/esm/audit.js +26 -4
- package/dist/esm/audit.js.map +1 -1
- package/dist/esm/ceilings.d.ts +7 -0
- package/dist/esm/ceilings.d.ts.map +1 -1
- package/dist/esm/ceilings.js +1 -1
- package/dist/esm/ceilings.js.map +1 -1
- package/dist/esm/cli.d.ts.map +1 -1
- package/dist/esm/cli.js +12 -5
- package/dist/esm/cli.js.map +1 -1
- package/dist/esm/evidence.d.ts +35 -9
- package/dist/esm/evidence.d.ts.map +1 -1
- package/dist/esm/evidence.js +334 -40
- package/dist/esm/evidence.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +1 -1
package/dist/cjs/evidence.js
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
* `attenu_guard.evidence`.
|
|
33
33
|
*/
|
|
34
34
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
35
|
-
exports.ENVELOPE_FAILURES = exports.PROCESS_ASSERTED = exports.WITNESS_SIGNED = exports.ENVELOPE_ALG = exports.ENVELOPE_RESULTS = exports.ENVELOPE_SUBJECT_MEMBERS = exports.ENVELOPE_MEMBERS = exports.ENVELOPE_TYP = exports.ENVELOPE_VERSION = exports.SUPPORTED_BUNDLE_VERSIONS = exports.EvidenceLeakError = exports.LEDGER_FIELDS = void 0;
|
|
35
|
+
exports.ENVELOPE_FAILURES = exports.PROCESS_ASSERTED = exports.WITNESS_SIGNED = exports.ENVELOPE_ALG = exports.ENVELOPE_RESULTS = exports.ENVELOPE_SUBJECT_MEMBERS = exports.ENVELOPE_MEMBERS = exports.ENVELOPE_TYP = exports.ENVELOPE_VERSION = exports.SUPPORTED_BUNDLE_VERSIONS = exports.EvidenceLeakError = exports.LEDGER_EVENTS_V1 = exports.LEDGER_EVENTS = exports.LEDGER_FIELDS = void 0;
|
|
36
36
|
exports.redactionReport = redactionReport;
|
|
37
37
|
exports.anchorFor = anchorFor;
|
|
38
38
|
exports.exportBundle = exportBundle;
|
|
@@ -117,6 +117,26 @@ exports.LEDGER_FIELDS = new Set([
|
|
|
117
117
|
"receipt",
|
|
118
118
|
"pending_at_kill",
|
|
119
119
|
]);
|
|
120
|
+
/**
|
|
121
|
+
* The COMPLETE set of ledger event names, closed: the `event` enum of
|
|
122
|
+
* `schema/agent-audit.schema.json` (in the Python distribution). Every check in `verifyBundle`
|
|
123
|
+
* reads an entry by its event, so an entry whose event none of them reads was skipped by all of
|
|
124
|
+
* them: a `done` renamed `frobnicate` verified, and an `allow` renamed left containment without a
|
|
125
|
+
* word. `verifyBundle` reports such an entry as `unknown_ledger_event`. The Python implementation's
|
|
126
|
+
* `evidence.LEDGER_EVENTS`, name for name.
|
|
127
|
+
*/
|
|
128
|
+
exports.LEDGER_EVENTS = new Set([
|
|
129
|
+
"root",
|
|
130
|
+
"spawn",
|
|
131
|
+
"spawn_denied",
|
|
132
|
+
"allow",
|
|
133
|
+
"deny",
|
|
134
|
+
"kill",
|
|
135
|
+
"done",
|
|
136
|
+
"outcome",
|
|
137
|
+
]);
|
|
138
|
+
/** `outcome` is v2-only (0.9.0, execution binding): a schemaVersion 1 chain has the other seven. */
|
|
139
|
+
exports.LEDGER_EVENTS_V1 = new Set([...exports.LEDGER_EVENTS].filter((event) => event !== "outcome"));
|
|
120
140
|
/**
|
|
121
141
|
* Thrown by `exportBundle({strict: true})` when a bundle would carry a field or
|
|
122
142
|
* context key outside the allow-list — potential customer data the custody
|
|
@@ -164,27 +184,60 @@ function redactTask(t) {
|
|
|
164
184
|
function redactionReport(entries, contextAllowlist) {
|
|
165
185
|
const allow = contextAllowlist === undefined || contextAllowlist === null ? null : new Set(contextAllowlist);
|
|
166
186
|
const violations = [];
|
|
167
|
-
|
|
187
|
+
// What it cannot read is a violation, never a throw, and never read by projection (a string's
|
|
188
|
+
// characters as fields). The Python implementation's same rule and shapes.
|
|
189
|
+
if (!Array.isArray(entries)) {
|
|
190
|
+
return { ok: false, violations: [{ event_index: null, event: null, entries: (0, ceilings_js_1.jsonKind)(entries) }] };
|
|
191
|
+
}
|
|
192
|
+
for (let i = 0; i < entries.length; i++) {
|
|
193
|
+
const e = entries[i];
|
|
194
|
+
if (!(0, audit_js_1.isLedgerObject)(e)) {
|
|
195
|
+
violations.push({ event_index: i, event: null, entry: (0, ceilings_js_1.jsonKind)(e) });
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
168
198
|
for (const f of Object.keys(e)) {
|
|
169
199
|
if (!exports.LEDGER_FIELDS.has(f)) {
|
|
170
|
-
violations.push({ event_index: i, event: (
|
|
200
|
+
violations.push({ event_index: i, event: orNull(e["event"]), field: f });
|
|
171
201
|
}
|
|
172
202
|
}
|
|
173
203
|
if (allow !== null) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
204
|
+
// A context Python counts as false (null, "", 0, false, [] or {}) is no context, as
|
|
205
|
+
// `verifyBundle` reads it; any other context that is not an object is a violation.
|
|
206
|
+
const raw = e["context"];
|
|
207
|
+
if (pyFalsy((0, canonical_js_1.toPlain)(raw)))
|
|
208
|
+
continue;
|
|
209
|
+
if (!(0, audit_js_1.isLedgerObject)(raw)) {
|
|
210
|
+
violations.push({ event_index: i, event: orNull(e["event"]), context: (0, ceilings_js_1.jsonKind)(raw) });
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
for (const k of Object.keys(raw)) {
|
|
214
|
+
if (!allow.has(k)) {
|
|
215
|
+
violations.push({ event_index: i, event: orNull(e["event"]), context_key: k });
|
|
180
216
|
}
|
|
181
217
|
}
|
|
182
218
|
}
|
|
183
|
-
}
|
|
219
|
+
}
|
|
184
220
|
return { ok: violations.length === 0, violations };
|
|
185
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* `entries` as the functions that SIGN them take them: an array of JSON objects. These produce
|
|
224
|
+
* evidence rather than read it, so anything else is refused by name, with a TypeError, before
|
|
225
|
+
* anything is signed over it. Before, a TypeError in the runtime's own words, or a signature over
|
|
226
|
+
* whatever was read. The Python implementation's `export_bundle` takes an `AuditLog`, so it has no
|
|
227
|
+
* such input to refuse.
|
|
228
|
+
*/
|
|
229
|
+
function signable(entries) {
|
|
230
|
+
if (!Array.isArray(entries))
|
|
231
|
+
throw new TypeError(`entries is ${(0, ceilings_js_1.jsonKind)(entries)}, not an array`);
|
|
232
|
+
for (let i = 0; i < entries.length; i++) {
|
|
233
|
+
if (!(0, audit_js_1.isLedgerObject)(entries[i]))
|
|
234
|
+
throw new TypeError(`entries[${i}] is ${(0, ceilings_js_1.jsonKind)(entries[i])}, not an object`);
|
|
235
|
+
}
|
|
236
|
+
return entries;
|
|
237
|
+
}
|
|
186
238
|
/** A signed commitment to the head of `entries` — mirrors `AuditLog.anchor`. */
|
|
187
|
-
function anchorFor(
|
|
239
|
+
function anchorFor(given, signer, ts = 0) {
|
|
240
|
+
const entries = signable(given);
|
|
188
241
|
let seq;
|
|
189
242
|
let head;
|
|
190
243
|
if (entries.length === 0) {
|
|
@@ -222,7 +275,7 @@ function exportBundle(auditLog, signer, options = {}) {
|
|
|
222
275
|
throw new Error("sign envelopes over the redacted ledger: export with redact_task=True first, then " +
|
|
223
276
|
"sign_envelope over the exported entries");
|
|
224
277
|
}
|
|
225
|
-
const source = auditLog instanceof audit_js_1.AuditLog ? auditLog.entries : auditLog;
|
|
278
|
+
const source = signable(auditLog instanceof audit_js_1.AuditLog ? auditLog.entries : auditLog);
|
|
226
279
|
const entries = source.map((e) => ({ ...e }));
|
|
227
280
|
if (options.redactTask) {
|
|
228
281
|
for (const e of entries) {
|
|
@@ -371,6 +424,45 @@ function monotonicityDetail(child, parent) {
|
|
|
371
424
|
// it exists so that such a dimension cannot fail SILENTLY.
|
|
372
425
|
return "child not narrower than parent";
|
|
373
426
|
}
|
|
427
|
+
/**
|
|
428
|
+
* Why `value`, a root's `authority` or a spawn's `granted`, cannot be read as an authority by the
|
|
429
|
+
* type of one of its members, or null. The object itself is an object; `scopes` and `constraints`,
|
|
430
|
+
* when present, arrays, and a scope a string; `ttl`, when present and not null, a finite number.
|
|
431
|
+
* `Authority.fromWire` reports the rest, as before: a member it does not read, a scope outside the
|
|
432
|
+
* grammar, a constraint of the wrong shape.
|
|
433
|
+
*
|
|
434
|
+
* `fromWire` read a `ttl` that was not a number as unbounded, so a root `ttl: "x"` verified, and a
|
|
435
|
+
* null `scopes` or `constraints` as empty; the Python implementation raised on the first and on
|
|
436
|
+
* the null ones. The Python implementation's `_authority_wire_error`, message for message.
|
|
437
|
+
*/
|
|
438
|
+
function authorityWireError(value, member) {
|
|
439
|
+
if (!(0, audit_js_1.isLedgerObject)(value))
|
|
440
|
+
return `${member} is ${(0, ceilings_js_1.jsonKind)(value)}, not an object`;
|
|
441
|
+
if (Object.keys(value).some((k) => k !== "scopes" && k !== "constraints" && k !== "ttl")) {
|
|
442
|
+
return null; // fromWire names the members it does not read
|
|
443
|
+
}
|
|
444
|
+
if (Object.hasOwn(value, "scopes")) {
|
|
445
|
+
const scopes = value["scopes"];
|
|
446
|
+
if (!Array.isArray(scopes))
|
|
447
|
+
return `scopes is ${(0, ceilings_js_1.jsonKind)(scopes)}, not an array`;
|
|
448
|
+
for (const scope of scopes) {
|
|
449
|
+
if (typeof scope !== "string")
|
|
450
|
+
return `a scope is ${(0, ceilings_js_1.jsonKind)(scope)}, not a string`;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (Object.hasOwn(value, "constraints") && !Array.isArray(value["constraints"])) {
|
|
454
|
+
return `constraints is ${(0, ceilings_js_1.jsonKind)(value["constraints"])}, not an array`;
|
|
455
|
+
}
|
|
456
|
+
const ttl = value["ttl"];
|
|
457
|
+
if (ttl !== undefined && ttl !== null) {
|
|
458
|
+
const plain = (0, canonical_js_1.toPlain)(ttl);
|
|
459
|
+
if (typeof plain !== "number")
|
|
460
|
+
return `ttl is ${(0, ceilings_js_1.jsonKind)(ttl)}, not a number`;
|
|
461
|
+
if (!Number.isFinite(plain))
|
|
462
|
+
return "ttl is not a finite number";
|
|
463
|
+
}
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
374
466
|
/**
|
|
375
467
|
* `node -> Authority` and `node -> parent`, reconstructed from `root` and `spawn` events in ledger
|
|
376
468
|
* order. No engine state.
|
|
@@ -409,6 +501,9 @@ function nodeAuthorities(entries) {
|
|
|
409
501
|
definedBy.set(node, e);
|
|
410
502
|
if (ev === "root") {
|
|
411
503
|
try {
|
|
504
|
+
const wrong = authorityWireError(e["authority"] ?? null, "authority");
|
|
505
|
+
if (wrong !== null)
|
|
506
|
+
throw new Error(wrong);
|
|
412
507
|
auth.set(node, authority_js_1.Authority.fromWire(e["authority"] ?? null));
|
|
413
508
|
}
|
|
414
509
|
catch (exc) {
|
|
@@ -418,6 +513,9 @@ function nodeAuthorities(entries) {
|
|
|
418
513
|
else {
|
|
419
514
|
parent.set(node, orNull(e["parent"]));
|
|
420
515
|
try {
|
|
516
|
+
const wrong = authorityWireError(e["granted"] ?? null, "granted");
|
|
517
|
+
if (wrong !== null)
|
|
518
|
+
throw new Error(wrong);
|
|
421
519
|
auth.set(node, authority_js_1.Authority.fromWire(e["granted"] ?? null));
|
|
422
520
|
}
|
|
423
521
|
catch (exc) {
|
|
@@ -447,7 +545,10 @@ function noteRevoked(entry, revokedAt) {
|
|
|
447
545
|
* renders. Derived from the ledger alone.
|
|
448
546
|
*/
|
|
449
547
|
function delegationGraph(bundle) {
|
|
450
|
-
|
|
548
|
+
// A view, not a verdict: it shows what it can read and throws on nothing. A bundle, or entries,
|
|
549
|
+
// it cannot read is an empty graph, and an entry that is not a JSON object names no node;
|
|
550
|
+
// `verifyBundle` is what reports either.
|
|
551
|
+
const entries = readableEntries(bundle);
|
|
451
552
|
const { auth, parent } = nodeAuthorities(entries);
|
|
452
553
|
const meta = {};
|
|
453
554
|
for (const e of entries) {
|
|
@@ -477,9 +578,14 @@ function delegationGraph(bundle) {
|
|
|
477
578
|
}
|
|
478
579
|
else if (ev === "deny" && Object.hasOwn(meta, n)) {
|
|
479
580
|
meta[n].denies += 1;
|
|
480
|
-
// A deny without a disposition is named by its reason
|
|
481
|
-
|
|
482
|
-
|
|
581
|
+
// A deny without a disposition is named by its reason, read as Python reads
|
|
582
|
+
// `disposition or reason`: an empty disposition (null, "", 0, false, [] or {}) names none.
|
|
583
|
+
// A key is a string; a value of another kind is filed by its printed form.
|
|
584
|
+
const disposition = (0, canonical_js_1.toPlain)(e["disposition"]);
|
|
585
|
+
const reason = (0, canonical_js_1.toPlain)(e["reason"]);
|
|
586
|
+
const picked = !pyFalsy(disposition) ? e["disposition"] : !pyFalsy(reason) ? e["reason"] : "unstated";
|
|
587
|
+
const d = typeof (0, canonical_js_1.toPlain)(picked) === "string" ? (0, canonical_js_1.toPlain)(picked) : (0, display_js_1.shown)(picked);
|
|
588
|
+
setOwn(meta[n].denials_by_disposition, d, (Object.hasOwn(meta[n].denials_by_disposition, d) ? meta[n].denials_by_disposition[d] : 0) + 1);
|
|
483
589
|
}
|
|
484
590
|
else if (ev === "done" && Object.hasOwn(meta, n)) {
|
|
485
591
|
meta[n].complete = true;
|
|
@@ -498,7 +604,7 @@ function delegationGraph(bundle) {
|
|
|
498
604
|
if (typeof p === "string" && p !== "")
|
|
499
605
|
edges.push({ parent: p, child });
|
|
500
606
|
}
|
|
501
|
-
return { chain_id: orNull(bundle
|
|
607
|
+
return { chain_id: (0, audit_js_1.isLedgerObject)(bundle) ? orNull(bundle["chain_id"]) : null, nodes: meta, edges };
|
|
502
608
|
}
|
|
503
609
|
/**
|
|
504
610
|
* Every refusal on the ledger, grouped by (node, tool, scope, disposition, requested) — the rows
|
|
@@ -515,12 +621,16 @@ function delegationGraph(bundle) {
|
|
|
515
621
|
* larger event of the two.
|
|
516
622
|
*/
|
|
517
623
|
function denials(bundle) {
|
|
518
|
-
|
|
624
|
+
// A view, not a verdict, like `delegationGraph`: a bundle, or entries, it cannot read has no
|
|
625
|
+
// rows, an entry that is not a JSON object is no refusal, and it throws on nothing.
|
|
626
|
+
const entries = readableEntries(bundle);
|
|
519
627
|
const agentOf = new Map();
|
|
520
628
|
for (const e of entries) {
|
|
521
629
|
const ev = (0, canonical_js_1.toPlain)(e["event"]);
|
|
522
|
-
|
|
523
|
-
|
|
630
|
+
const node = (0, canonical_js_1.toPlain)(e["node"]);
|
|
631
|
+
// A node id is a string; anything else names no node.
|
|
632
|
+
if ((ev === "root" || ev === "spawn") && typeof node === "string") {
|
|
633
|
+
agentOf.set(node, orNull(e["agent"]));
|
|
524
634
|
}
|
|
525
635
|
}
|
|
526
636
|
const rows = new Map();
|
|
@@ -532,19 +642,22 @@ function denials(bundle) {
|
|
|
532
642
|
// what was refused), so it is folded onto the node that asked.
|
|
533
643
|
const node = ev === "spawn_denied" ? orNull(e["parent"]) : orNull(e["node"]);
|
|
534
644
|
const requested = ev === "spawn_denied" ? orNull(e["agent"]) : null;
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
645
|
+
// Grouped by the escaped JSON text of the five values, as the Python implementation groups
|
|
646
|
+
// them, which tells `true` from 1 and 1.0 from 1 as written.
|
|
647
|
+
const asWritten = (v) => (v === undefined ? null : v);
|
|
648
|
+
const key = (0, display_js_1.escaped)([
|
|
649
|
+
asWritten(ev === "spawn_denied" ? e["parent"] : e["node"]),
|
|
650
|
+
asWritten(e["tool"]),
|
|
651
|
+
asWritten(e["scope"]),
|
|
652
|
+
asWritten(e["disposition"]),
|
|
653
|
+
ev === "spawn_denied" ? asWritten(e["agent"]) : null,
|
|
541
654
|
]);
|
|
542
|
-
const seq = (
|
|
655
|
+
const seq = orNull(e["seq"]);
|
|
543
656
|
const existing = rows.get(key);
|
|
544
657
|
if (existing === undefined) {
|
|
545
658
|
rows.set(key, {
|
|
546
659
|
node,
|
|
547
|
-
agent: agentOf.get(node) ?? null,
|
|
660
|
+
agent: typeof node === "string" ? (agentOf.get(node) ?? null) : null,
|
|
548
661
|
tool: orNull(e["tool"]),
|
|
549
662
|
scope: orNull(e["scope"]),
|
|
550
663
|
disposition: orNull(e["disposition"]),
|
|
@@ -561,7 +674,9 @@ function denials(bundle) {
|
|
|
561
674
|
existing.last_seq = seq;
|
|
562
675
|
}
|
|
563
676
|
}
|
|
564
|
-
|
|
677
|
+
// In the order each row first occurs. Sorting by `first_seq` said the same on a well-formed
|
|
678
|
+
// ledger, and ordered a seq that is not a number by NaN.
|
|
679
|
+
return Array.from(rows.values());
|
|
565
680
|
}
|
|
566
681
|
// `pyRepr` and `pyStr` (display.ts) are Python's `repr` and `str` for the values these failure
|
|
567
682
|
// messages carry. Both implementations report the same failure bytes, so the distinction between
|
|
@@ -648,6 +763,10 @@ exports.ENVELOPE_FAILURES = [
|
|
|
648
763
|
* implementation, not a second one for envelopes.
|
|
649
764
|
*/
|
|
650
765
|
function envelopeSigningInput(envelope) {
|
|
766
|
+
// An envelope that is not a JSON object has no members to sign. The Python implementation raises
|
|
767
|
+
// ValueError with the same words.
|
|
768
|
+
if (!(0, audit_js_1.isLedgerObject)(envelope))
|
|
769
|
+
throw new Error(`an envelope is ${(0, ceilings_js_1.jsonKind)(envelope)}, not an object`);
|
|
651
770
|
const body = {};
|
|
652
771
|
for (const [k, v] of Object.entries(envelope))
|
|
653
772
|
if (k !== "sig")
|
|
@@ -661,7 +780,12 @@ function envelopeSigningInput(envelope) {
|
|
|
661
780
|
* envelope finds the entry it covers through `subjectIndex`.
|
|
662
781
|
*/
|
|
663
782
|
function stateKey(e, index) {
|
|
664
|
-
|
|
783
|
+
// An entry that is not a JSON object has no seq member, so it is filed by its index; a seq that
|
|
784
|
+
// is a list or an object by its escaped JSON text, as the Python implementation files it.
|
|
785
|
+
if (!(0, audit_js_1.isLedgerObject)(e) || !("seq" in e))
|
|
786
|
+
return String(index);
|
|
787
|
+
const seq = orNull((0, canonical_js_1.intOr)(e["seq"]));
|
|
788
|
+
return seq !== null && typeof seq === "object" ? (0, display_js_1.escaped)(seq) : String(seq);
|
|
665
789
|
}
|
|
666
790
|
/**
|
|
667
791
|
* Each entry's hash RECOMPUTED from the bundle, by index, never read off the entry.
|
|
@@ -808,14 +932,20 @@ function compareVersions(a, b) {
|
|
|
808
932
|
* verifier will check it against. Throws when `seq` names no entry, or names one whose `event` v1
|
|
809
933
|
* defines no subject for.
|
|
810
934
|
*/
|
|
811
|
-
function envelopeSubject(
|
|
935
|
+
function envelopeSubject(given, seq) {
|
|
936
|
+
// `entries` that are not an array are refused, and an entry that is not a JSON object is read as
|
|
937
|
+
// one with no members, as the verifier reads it. The Python implementation's ValueError, worded
|
|
938
|
+
// the same.
|
|
939
|
+
if (!Array.isArray(given))
|
|
940
|
+
throw new Error(`entries is ${(0, ceilings_js_1.jsonKind)(given)}, not an array`);
|
|
941
|
+
const entries = Array.from(given, (e) => ((0, audit_js_1.isLedgerObject)(e) ? e : {}));
|
|
812
942
|
const n = (0, canonical_js_1.integral)(seq);
|
|
813
943
|
const at = n === null ? undefined : subjectIndex(entries).get(n);
|
|
814
944
|
if (n === null || at === undefined)
|
|
815
945
|
throw new Error(`no entry at seq ${seq}`);
|
|
816
946
|
const entry = entries[at];
|
|
817
947
|
const event = (0, canonical_js_1.toPlain)(entry["event"]);
|
|
818
|
-
if (!exports.ENVELOPE_SUBJECT_MEMBERS.has(event)) {
|
|
948
|
+
if (typeof event !== "string" || !exports.ENVELOPE_SUBJECT_MEMBERS.has(event)) {
|
|
819
949
|
throw new Error(`envelope v${exports.ENVELOPE_VERSION} defines no subject for event '${event}'`);
|
|
820
950
|
}
|
|
821
951
|
const subject = {
|
|
@@ -1391,7 +1521,30 @@ function scoreEnvelope(envelope, index, entries, subjectAt, recomputed, trust, r
|
|
|
1391
1521
|
* one the witness signed is in it.
|
|
1392
1522
|
*/
|
|
1393
1523
|
function verifyEnvelopes(bundle, options = {}) {
|
|
1394
|
-
|
|
1524
|
+
// A bundle it cannot read, or whose `entries` or `envelopes` it cannot (the anchor is not read
|
|
1525
|
+
// here), is reported, `ok` false and status "not checked", one `invalid_bundle` failure per
|
|
1526
|
+
// member, as `verifyBundle` reports it. An entry that is not a JSON object is read as one with
|
|
1527
|
+
// no members, filed by its index. The Python implementation's same report.
|
|
1528
|
+
const unreadable = unreadableBundle(bundle, ["entries", "envelopes"]);
|
|
1529
|
+
if (unreadable.length > 0) {
|
|
1530
|
+
const log = new FailureLog();
|
|
1531
|
+
for (const detail of unreadable)
|
|
1532
|
+
log.add("invalid_bundle", detail);
|
|
1533
|
+
return {
|
|
1534
|
+
ok: false,
|
|
1535
|
+
status: "not checked",
|
|
1536
|
+
count: 0,
|
|
1537
|
+
witness_signed: [],
|
|
1538
|
+
states: {},
|
|
1539
|
+
results: {},
|
|
1540
|
+
witnesses: {},
|
|
1541
|
+
lines: {},
|
|
1542
|
+
failures: log.messages,
|
|
1543
|
+
failure_details: log.details,
|
|
1544
|
+
failure_entries: unreadable.map(() => null),
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
1547
|
+
const entries = readableEntries(bundle);
|
|
1395
1548
|
const [summary, fail] = scoreEnvelopes(entries, bundle.envelopes ?? [], trustedWitnesses(options.witnessKeys ?? null, verificationTime(options.now)), options.envelopeBytes ?? null);
|
|
1396
1549
|
return { ok: fail.length === 0, ...summary, failure_details: fail.details, failure_entries: fail.entryIndices(entries) };
|
|
1397
1550
|
}
|
|
@@ -1942,9 +2095,16 @@ function executionBinding(entries, bundleV) {
|
|
|
1942
2095
|
* re-hashed ledger fails against the signed anchor, not here, and that failure is chain-level.
|
|
1943
2096
|
*/
|
|
1944
2097
|
function integrityBreak(entries) {
|
|
2098
|
+
// `entries` that are not an array are no chain, and no entry is where it breaks.
|
|
2099
|
+
if (!Array.isArray(entries))
|
|
2100
|
+
return null;
|
|
1945
2101
|
let prev = audit_js_1.GENESIS;
|
|
1946
2102
|
for (let i = 0; i < entries.length; i++) {
|
|
1947
2103
|
const e = entries[i];
|
|
2104
|
+
// An entry that is not a JSON object carries no member, so the chain breaks at it, as
|
|
2105
|
+
// `AuditLog.verify` reads it.
|
|
2106
|
+
if (!(0, audit_js_1.isLedgerObject)(e))
|
|
2107
|
+
return i;
|
|
1948
2108
|
const payload = {};
|
|
1949
2109
|
for (const [k, v] of Object.entries(e)) {
|
|
1950
2110
|
if (k !== "hash")
|
|
@@ -1967,8 +2127,118 @@ function integrityBreak(entries) {
|
|
|
1967
2127
|
}
|
|
1968
2128
|
return null;
|
|
1969
2129
|
}
|
|
2130
|
+
/** What each bundle member this module reads has to be, when it is present and not null. */
|
|
2131
|
+
const BUNDLE_MEMBERS = {
|
|
2132
|
+
entries: ["an array", (v) => Array.isArray(v)],
|
|
2133
|
+
anchor: ["an object", (v) => (0, audit_js_1.isLedgerObject)(v)],
|
|
2134
|
+
envelopes: ["an array", (v) => Array.isArray(v)],
|
|
2135
|
+
};
|
|
2136
|
+
/**
|
|
2137
|
+
* The `invalid_bundle` messages for a bundle this module cannot read: one when the bundle is not a
|
|
2138
|
+
* JSON object, or one for each member in `members` that is present, not null, and not the type the
|
|
2139
|
+
* format gives it. Empty when there is nothing of the kind. Reading such a member anyway reads it
|
|
2140
|
+
* by projection (a string's characters as entries) or throws. The Python implementation's
|
|
2141
|
+
* `_unreadable_bundle`.
|
|
2142
|
+
*/
|
|
2143
|
+
function unreadableBundle(bundle, members = ["entries", "anchor", "envelopes"]) {
|
|
2144
|
+
if (!(0, audit_js_1.isLedgerObject)(bundle))
|
|
2145
|
+
return [`invalid_bundle: the bundle is ${(0, ceilings_js_1.jsonKind)(bundle)}, not an object`];
|
|
2146
|
+
const out = [];
|
|
2147
|
+
for (const member of members) {
|
|
2148
|
+
const [want, ok] = BUNDLE_MEMBERS[member];
|
|
2149
|
+
const value = bundle[member];
|
|
2150
|
+
if (value !== undefined && value !== null && !ok(value)) {
|
|
2151
|
+
out.push(`invalid_bundle: ${member} is ${(0, ceilings_js_1.jsonKind)(value)}, not ${want}`);
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
return out;
|
|
2155
|
+
}
|
|
2156
|
+
/**
|
|
2157
|
+
* A bundle's entries as the readers that never fail read them: none when the bundle or its
|
|
2158
|
+
* `entries` cannot be read (`unreadableBundle`), and an entry that is not a JSON object read as one
|
|
2159
|
+
* with no members, which names no node, no event and no seq. The Python implementation's
|
|
2160
|
+
* `_readable_entries`.
|
|
2161
|
+
*/
|
|
2162
|
+
function readableEntries(bundle) {
|
|
2163
|
+
if (!(0, audit_js_1.isLedgerObject)(bundle))
|
|
2164
|
+
return [];
|
|
2165
|
+
const entries = bundle["entries"];
|
|
2166
|
+
if (!Array.isArray(entries))
|
|
2167
|
+
return [];
|
|
2168
|
+
return Array.from(entries, (e) => ((0, audit_js_1.isLedgerObject)(e) ? e : {}));
|
|
2169
|
+
}
|
|
2170
|
+
/**
|
|
2171
|
+
* The report on a bundle this verifier cannot read at all (`unreadableBundle`): one
|
|
2172
|
+
* `invalid_bundle` failure per member it cannot read, each about no single entry; every check
|
|
2173
|
+
* false, and every status `"not checked"`, since none ran. The report has every member a full
|
|
2174
|
+
* verification's has, so a reader of it needs no second shape. The Python implementation's
|
|
2175
|
+
* `_invalid_bundle_report`, member for member.
|
|
2176
|
+
*/
|
|
2177
|
+
function invalidBundleReport(bundle, details, verifiedAgainst) {
|
|
2178
|
+
const log = new FailureLog();
|
|
2179
|
+
for (const detail of details)
|
|
2180
|
+
log.add("invalid_bundle", detail);
|
|
2181
|
+
return {
|
|
2182
|
+
ok: false,
|
|
2183
|
+
checks: {
|
|
2184
|
+
integrity: false,
|
|
2185
|
+
monotonicity: false,
|
|
2186
|
+
containment: false,
|
|
2187
|
+
anchor: "not checked",
|
|
2188
|
+
version: false,
|
|
2189
|
+
ledger_fields: false,
|
|
2190
|
+
chain_id: false,
|
|
2191
|
+
root: false,
|
|
2192
|
+
expected_anchor: "not checked",
|
|
2193
|
+
envelopes: "not checked",
|
|
2194
|
+
},
|
|
2195
|
+
failures: log.messages,
|
|
2196
|
+
failure_details: log.details,
|
|
2197
|
+
failure_entries: details.map(() => null),
|
|
2198
|
+
nodes: 0,
|
|
2199
|
+
actions_checked: 0,
|
|
2200
|
+
ungated: 0,
|
|
2201
|
+
chain_id: (0, audit_js_1.isLedgerObject)(bundle) ? orNull(bundle["chain_id"]) : null,
|
|
2202
|
+
execution_binding: { status: "not applicable" },
|
|
2203
|
+
envelopes: {
|
|
2204
|
+
status: "not checked",
|
|
2205
|
+
count: 0,
|
|
2206
|
+
witness_signed: [],
|
|
2207
|
+
states: {},
|
|
2208
|
+
results: {},
|
|
2209
|
+
witnesses: {},
|
|
2210
|
+
lines: {},
|
|
2211
|
+
failures: [],
|
|
2212
|
+
},
|
|
2213
|
+
verified_against: verifiedAgainst,
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
1970
2216
|
function verifyBundle(bundle, signer = null, options = {}) {
|
|
1971
|
-
const
|
|
2217
|
+
const verifiedAgainst = (options.expectedAnchor ?? null) !== null || (options.expectedHead ?? null) !== null
|
|
2218
|
+
? "expected_anchor"
|
|
2219
|
+
: "bundle_anchor";
|
|
2220
|
+
// A bundle is a JSON object, its `entries` and `envelopes` arrays and its `anchor` an object.
|
|
2221
|
+
// Anything else threw out of this function (a TypeError), whose 0.8.0 note says it never throws,
|
|
2222
|
+
// or was read by projection (a string anchor's characters as its members). Such a bundle is
|
|
2223
|
+
// reported alone, as `invalid_bundle`, once per member it cannot read. An absent or null member
|
|
2224
|
+
// is absent. The Python implementation's same rule, message for message.
|
|
2225
|
+
const given = bundle;
|
|
2226
|
+
const unreadable = unreadableBundle(given);
|
|
2227
|
+
if (unreadable.length > 0 || !(0, audit_js_1.isLedgerObject)(given))
|
|
2228
|
+
return invalidBundleReport(given, unreadable, verifiedAgainst);
|
|
2229
|
+
const rawEntries = (given["entries"] ?? []);
|
|
2230
|
+
// An entry that is not a JSON object is reported once, as `invalid_ledger_entry` in (0b2), and
|
|
2231
|
+
// read everywhere else as an entry with no members: the checks that read an entry's fields skip
|
|
2232
|
+
// it, and the hash chain breaks at it. Each stand-in is a new object, so `failure_entries` finds
|
|
2233
|
+
// its index by identity, as it finds every other entry's. `Array.from` visits a hole as well.
|
|
2234
|
+
const unread = new Set();
|
|
2235
|
+
const entries = Array.from(rawEntries, (e) => {
|
|
2236
|
+
if ((0, audit_js_1.isLedgerObject)(e))
|
|
2237
|
+
return e;
|
|
2238
|
+
const standIn = {};
|
|
2239
|
+
unread.add(standIn);
|
|
2240
|
+
return standIn;
|
|
2241
|
+
});
|
|
1972
2242
|
const anchor = (bundle.anchor ?? {});
|
|
1973
2243
|
const anchorPresent = Object.keys(anchor).length > 0;
|
|
1974
2244
|
const checks = {
|
|
@@ -2012,7 +2282,7 @@ function verifyBundle(bundle, signer = null, options = {}) {
|
|
|
2012
2282
|
versionOk = false;
|
|
2013
2283
|
log.add("root_version_mismatch", `root_version_mismatch: root v=${reprIntOr(rootEntry["v"])} != bundle v=${reprIntOr(rawV)}`, { seq: orNull(rootEntry["seq"]), node: orNull(rootEntry["node"]), entry: rootEntry });
|
|
2014
2284
|
}
|
|
2015
|
-
const mixedEntries = entries.filter((e) => !sameNumber(e["v"], rawV));
|
|
2285
|
+
const mixedEntries = entries.filter((e) => !unread.has(e) && !sameNumber(e["v"], rawV));
|
|
2016
2286
|
// An entry without `v` is Python's None in the list. Numbers come first in numeric order, then
|
|
2017
2287
|
// everything else by its repr, compared by code point as Python compares strings, never by locale.
|
|
2018
2288
|
const mixed = pySetOf(mixedEntries.map((e) => ((0, canonical_js_1.intOr)(e["v"]) ?? null))).sort(compareVersions);
|
|
@@ -2038,8 +2308,32 @@ function verifyBundle(bundle, signer = null, options = {}) {
|
|
|
2038
2308
|
// is a property of the bundle, not an error in the call.
|
|
2039
2309
|
//
|
|
2040
2310
|
// Kept in step with the Python port (`evidence._verify_bundle`, same check, same reason string).
|
|
2311
|
+
//
|
|
2312
|
+
// The same pass reads every entry's shape and event, and reports each defect an entry has. An
|
|
2313
|
+
// entry that is not a JSON object is `invalid_ledger_entry`, chain-level since it has no seq or
|
|
2314
|
+
// node, its index in the message. An event that is absent, not a string, or not one its chain's
|
|
2315
|
+
// version defines (`outcome` is v2-only) is `unknown_ledger_event`: every check below reads an
|
|
2316
|
+
// entry by its event, so such an entry was read by none of them.
|
|
2041
2317
|
let unknownOk = true;
|
|
2042
|
-
|
|
2318
|
+
const events = bundleV === 1 ? exports.LEDGER_EVENTS_V1 : exports.LEDGER_EVENTS;
|
|
2319
|
+
for (let index = 0; index < entries.length; index++) {
|
|
2320
|
+
const e = entries[index];
|
|
2321
|
+
if (unread.has(e)) {
|
|
2322
|
+
unknownOk = false;
|
|
2323
|
+
log.add("invalid_ledger_entry", `invalid_ledger_entry: entries[${index}] is ${(0, ceilings_js_1.jsonKind)(rawEntries[index])}, not an object`, { entry: e });
|
|
2324
|
+
continue;
|
|
2325
|
+
}
|
|
2326
|
+
const ev = (0, canonical_js_1.toPlain)(e["event"]);
|
|
2327
|
+
if (typeof ev !== "string" || !events.has(ev)) {
|
|
2328
|
+
unknownOk = false;
|
|
2329
|
+
const why = typeof ev !== "string"
|
|
2330
|
+
? ", which is not a string"
|
|
2331
|
+
: exports.LEDGER_EVENTS.has(ev)
|
|
2332
|
+
? ", a v2-only event on a schema_version=1 chain"
|
|
2333
|
+
: "";
|
|
2334
|
+
log.add("unknown_ledger_event", `unknown_ledger_event: entry carries an event this verifier does not evaluate and will not ` +
|
|
2335
|
+
`ignore: ${(0, display_js_1.shown)(e["event"])}${why}`, { seq: orNull(e["seq"]), node: orNull(e["node"]), entry: e });
|
|
2336
|
+
}
|
|
2043
2337
|
const extra = Object.keys(e)
|
|
2044
2338
|
.filter((f) => !exports.LEDGER_FIELDS.has(f))
|
|
2045
2339
|
.sort();
|
|
@@ -2096,7 +2390,7 @@ function verifyBundle(bundle, signer = null, options = {}) {
|
|
|
2096
2390
|
// must all name the SAME chain. Without this a correctly-signed, internally-consistent bundle
|
|
2097
2391
|
// for a DIFFERENT chain could be handed to a verifier who believes it is checking this one.
|
|
2098
2392
|
const bundleChainId = orNull(bundle.chain_id);
|
|
2099
|
-
const foreign = entries.find((e) => orNull(e["chain_id"]) !== bundleChainId);
|
|
2393
|
+
const foreign = entries.find((e) => !unread.has(e) && orNull(e["chain_id"]) !== bundleChainId);
|
|
2100
2394
|
const entriesOk = foreign === undefined;
|
|
2101
2395
|
if (foreign !== undefined) {
|
|
2102
2396
|
log.add("chain_id_mismatch", `chain_id_mismatch: an entry does not carry chain_id=${(0, display_js_1.pyRepr)(bundleChainId)}`, {
|
|
@@ -2308,7 +2602,7 @@ function verifyBundle(bundle, signer = null, options = {}) {
|
|
|
2308
2602
|
chain_id: orNull(bundle.chain_id),
|
|
2309
2603
|
execution_binding: eb,
|
|
2310
2604
|
envelopes: envelopeSummary,
|
|
2311
|
-
verified_against:
|
|
2605
|
+
verified_against: verifiedAgainst,
|
|
2312
2606
|
};
|
|
2313
2607
|
}
|
|
2314
2608
|
/** Read a bundle from JSON text, keeping every number's original literal. */
|