attenu-guard 0.7.0 → 0.8.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/CHANGELOG.md +24 -0
- package/README.md +40 -0
- package/dist/cjs/cli.d.ts +4 -1
- package/dist/cjs/cli.d.ts.map +1 -1
- package/dist/cjs/cli.js +37 -3
- package/dist/cjs/cli.js.map +1 -1
- package/dist/cjs/evidence.d.ts +161 -1
- package/dist/cjs/evidence.d.ts.map +1 -1
- package/dist/cjs/evidence.js +605 -5
- package/dist/cjs/evidence.js.map +1 -1
- package/dist/cjs/index.d.ts +2 -2
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +15 -2
- 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/cli.d.ts +4 -1
- package/dist/esm/cli.d.ts.map +1 -1
- package/dist/esm/cli.js +37 -3
- package/dist/esm/cli.js.map +1 -1
- package/dist/esm/evidence.d.ts +161 -1
- package/dist/esm/evidence.d.ts.map +1 -1
- package/dist/esm/evidence.js +601 -5
- package/dist/esm/evidence.js.map +1 -1
- package/dist/esm/index.d.ts +2 -2
- 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,12 +32,16 @@
|
|
|
32
32
|
* `attenu_guard.evidence`.
|
|
33
33
|
*/
|
|
34
34
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
35
|
-
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_FIELDS = void 0;
|
|
36
36
|
exports.redactionReport = redactionReport;
|
|
37
37
|
exports.anchorFor = anchorFor;
|
|
38
38
|
exports.exportBundle = exportBundle;
|
|
39
39
|
exports.delegationGraph = delegationGraph;
|
|
40
40
|
exports.denials = denials;
|
|
41
|
+
exports.envelopeSigningInput = envelopeSigningInput;
|
|
42
|
+
exports.envelopeSubject = envelopeSubject;
|
|
43
|
+
exports.signEnvelope = signEnvelope;
|
|
44
|
+
exports.verifyEnvelopes = verifyEnvelopes;
|
|
41
45
|
exports.verifyBundle = verifyBundle;
|
|
42
46
|
exports.parseBundle = parseBundle;
|
|
43
47
|
const canonical_js_1 = require("./canonical.js");
|
|
@@ -47,6 +51,7 @@ const authority_js_1 = require("./authority.js");
|
|
|
47
51
|
const ceilings_js_1 = require("./ceilings.js");
|
|
48
52
|
const reasons_js_1 = require("./reasons.js");
|
|
49
53
|
const params_js_1 = require("./params.js");
|
|
54
|
+
const wire_js_1 = require("./wire.js");
|
|
50
55
|
/**
|
|
51
56
|
* The COMPLETE set of top-level ledger field names the library emits. Custody
|
|
52
57
|
* guarantee: an exported bundle may carry ONLY these — an unknown field is
|
|
@@ -189,8 +194,18 @@ function anchorFor(entries, signer, ts = 0) {
|
|
|
189
194
|
* only ever removes. With `strict`, the bundle is checked against
|
|
190
195
|
* `LEDGER_FIELDS` (and `contextAllowlist` if given) and an `EvidenceLeakError`
|
|
191
196
|
* is thrown on any field outside it.
|
|
197
|
+
*
|
|
198
|
+
* ORDER MATTERS with `redactTask`: redaction rewrites every entry hash, so envelopes signed
|
|
199
|
+
* over the unredacted ledger no longer bind to the entries that ship and would all fail
|
|
200
|
+
* `envelope_subject_mismatch`. Giving both throws `Error` rather than exporting a bundle that
|
|
201
|
+
* cannot verify. Export the redacted bundle first, then `signEnvelope` over ITS entries, then
|
|
202
|
+
* export again with those envelopes.
|
|
192
203
|
*/
|
|
193
204
|
function exportBundle(auditLog, signer, options = {}) {
|
|
205
|
+
if (options.redactTask && (options.envelopes?.length ?? 0) > 0) {
|
|
206
|
+
throw new Error("sign envelopes over the redacted ledger: export with redact_task=True first, then " +
|
|
207
|
+
"sign_envelope over the exported entries");
|
|
208
|
+
}
|
|
194
209
|
const source = auditLog instanceof audit_js_1.AuditLog ? auditLog.entries : auditLog;
|
|
195
210
|
const entries = source.map((e) => ({ ...e }));
|
|
196
211
|
if (options.redactTask) {
|
|
@@ -217,7 +232,7 @@ function exportBundle(auditLog, signer, options = {}) {
|
|
|
217
232
|
}
|
|
218
233
|
const anchor = anchorFor(entries, signer, options.ts ?? 0);
|
|
219
234
|
anchor.verified = audit_js_1.AuditLog.verifyAnchor(entries, anchor, signer)[0];
|
|
220
|
-
|
|
235
|
+
const bundle = {
|
|
221
236
|
v: bundleVersion(entries),
|
|
222
237
|
c14n: "JCS",
|
|
223
238
|
chain_id: (0, audit_js_1.chainIdOf)(entries),
|
|
@@ -226,6 +241,10 @@ function exportBundle(auditLog, signer, options = {}) {
|
|
|
226
241
|
redaction: report,
|
|
227
242
|
note: "offline-verifiable: attenu_guard.evidence.verify_bundle(bundle, signer)",
|
|
228
243
|
};
|
|
244
|
+
const envelopes = options.envelopes ?? null;
|
|
245
|
+
if (envelopes !== null && envelopes.length > 0)
|
|
246
|
+
bundle.envelopes = [...envelopes];
|
|
247
|
+
return bundle;
|
|
229
248
|
}
|
|
230
249
|
/**
|
|
231
250
|
* A ledger field, or `null` when it is absent. Python's `dict.get` yields `None`
|
|
@@ -455,13 +474,580 @@ function denials(bundle) {
|
|
|
455
474
|
function pyRepr(value) {
|
|
456
475
|
if (value === null)
|
|
457
476
|
return "None";
|
|
477
|
+
if (typeof value === "boolean")
|
|
478
|
+
return value ? "True" : "False";
|
|
458
479
|
if (typeof value === "number")
|
|
459
480
|
return String(value);
|
|
460
481
|
if (typeof value === "string")
|
|
461
482
|
return `'${value}'`;
|
|
483
|
+
// Containers reach here only on hostile input — a subject member that is a list or an object
|
|
484
|
+
// where the contract wants a scalar. Rendered the way Python's repr renders them, because the
|
|
485
|
+
// two implementations report the same failure strings and a JSON spelling would not match.
|
|
486
|
+
if (Array.isArray(value))
|
|
487
|
+
return `[${value.map((v) => pyRepr(v)).join(", ")}]`;
|
|
488
|
+
if (typeof value === "object") {
|
|
489
|
+
const body = Object.entries(value)
|
|
490
|
+
.map(([k, v]) => `${pyRepr(k)}: ${pyRepr(v)}`)
|
|
491
|
+
.join(", ");
|
|
492
|
+
return `{${body}}`;
|
|
493
|
+
}
|
|
462
494
|
return JSON.stringify(value);
|
|
463
495
|
}
|
|
464
496
|
// =============================================================================================
|
|
497
|
+
// Observer envelopes (envelope v1) — the TypeScript half of `attenu_guard.evidence`'s.
|
|
498
|
+
//
|
|
499
|
+
// One question a reader of a bundle cannot answer today: was this delegation event signed by
|
|
500
|
+
// something OUTSIDE the process that wrote it? An envelope is a witness's signature over the
|
|
501
|
+
// IDENTITY of one committed ledger entry — never over its contents, which the entry's own hash
|
|
502
|
+
// already covers. Envelopes travel beside the ledger in a top-level `envelopes` array; no entry
|
|
503
|
+
// changes, so a bundle without them stays valid exactly as it is today.
|
|
504
|
+
//
|
|
505
|
+
// An envelope is never REQUIRED. An absent one is the status quo and changes nothing. A present
|
|
506
|
+
// one has to verify: a broken envelope lands in the same failure list as the chain-level checks
|
|
507
|
+
// and the bundle rejects. Byte-compatible with the Python implementation, and scored against the
|
|
508
|
+
// same `envelope_vectors_v1.json`.
|
|
509
|
+
// =============================================================================================
|
|
510
|
+
/**
|
|
511
|
+
* The only envelope version this build knows. The version commits the exact signed member set of
|
|
512
|
+
* the WHOLE envelope, the subject included, so a member added anywhere is a new version and the
|
|
513
|
+
* digest cannot widen silently.
|
|
514
|
+
*/
|
|
515
|
+
exports.ENVELOPE_VERSION = 1;
|
|
516
|
+
/** The only `typ` at v1. A different one is a different contract, not a different envelope. */
|
|
517
|
+
exports.ENVELOPE_TYP = "delegation-event-observation";
|
|
518
|
+
/** The envelope's own member set at v1. */
|
|
519
|
+
exports.ENVELOPE_MEMBERS = new Set([
|
|
520
|
+
"v",
|
|
521
|
+
"typ",
|
|
522
|
+
"subject",
|
|
523
|
+
"observed",
|
|
524
|
+
"witness",
|
|
525
|
+
"sig",
|
|
526
|
+
]);
|
|
527
|
+
/**
|
|
528
|
+
* The subject member set, keyed by `event`. v1 defines a subject for `spawn` and `allow` and for
|
|
529
|
+
* no other event. `entry_hash` is the BINDING member — the only evidence of WHICH entry the
|
|
530
|
+
* witness signed — and the rest are locators, whose job is to find the entry without hashing
|
|
531
|
+
* every entry.
|
|
532
|
+
*/
|
|
533
|
+
exports.ENVELOPE_SUBJECT_MEMBERS = new Map([
|
|
534
|
+
["spawn", new Set(["chain_id", "node", "seq", "entry_hash", "event"])],
|
|
535
|
+
["allow", new Set(["chain_id", "node", "seq", "entry_hash", "event", "call_id"])],
|
|
536
|
+
]);
|
|
537
|
+
const ENVELOPE_OBSERVED_MEMBERS = new Set(["result", "at", "method"]);
|
|
538
|
+
const ENVELOPE_WITNESS_MEMBERS = new Set(["kid", "alg"]);
|
|
539
|
+
/**
|
|
540
|
+
* `observed.result`'s closed vocabulary. `not_matched` requires evidence that CONTRADICTS the
|
|
541
|
+
* event; `indeterminate` is the residual state, and covers thin or absent evidence. No verifier
|
|
542
|
+
* decision turns on the result: it is reported next to the state, never instead of it.
|
|
543
|
+
*/
|
|
544
|
+
exports.ENVELOPE_RESULTS = ["matched", "not_matched", "indeterminate"];
|
|
545
|
+
/** The JOSE identifier for Ed25519, and the only `witness.alg` v1 defines. */
|
|
546
|
+
exports.ENVELOPE_ALG = "EdDSA";
|
|
547
|
+
/**
|
|
548
|
+
* A verifying envelope's state. It says where the signature came from and NOTHING about
|
|
549
|
+
* authority — the witness is whoever holds the key `witness.kid` names, which nothing in the
|
|
550
|
+
* envelope makes the delegation parent.
|
|
551
|
+
*/
|
|
552
|
+
exports.WITNESS_SIGNED = "witness-signed";
|
|
553
|
+
/**
|
|
554
|
+
* No envelope, or one that does not verify. It covers two facts a bundle does not separate — a
|
|
555
|
+
* hop nobody undertook to cover, and a hop a witness undertook to cover and never did — and v1
|
|
556
|
+
* takes the weaker reading of the two.
|
|
557
|
+
*/
|
|
558
|
+
exports.PROCESS_ASSERTED = "process-asserted";
|
|
559
|
+
/** The seven named envelope failures, in the order this build checks them. */
|
|
560
|
+
exports.ENVELOPE_FAILURES = [
|
|
561
|
+
"envelope_unknown_version",
|
|
562
|
+
"envelope_unknown_member",
|
|
563
|
+
"envelope_subject_mismatch",
|
|
564
|
+
"envelope_duplicate_subject",
|
|
565
|
+
"envelope_non_canonical",
|
|
566
|
+
"envelope_unknown_witness",
|
|
567
|
+
"envelope_bad_signature",
|
|
568
|
+
];
|
|
569
|
+
/**
|
|
570
|
+
* The bytes a witness signs: `JCS(envelope minus its "sig" member)`.
|
|
571
|
+
*
|
|
572
|
+
* The same RFC 8785 canonicalization the ledger has signed with since 0.7.0 — one
|
|
573
|
+
* implementation, not a second one for envelopes.
|
|
574
|
+
*/
|
|
575
|
+
function envelopeSigningInput(envelope) {
|
|
576
|
+
const body = {};
|
|
577
|
+
for (const [k, v] of Object.entries(envelope))
|
|
578
|
+
if (k !== "sig")
|
|
579
|
+
body[k] = v;
|
|
580
|
+
return (0, canonical_js_1.canonicalBytes)(body);
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* seq -> the entry's hash RECOMPUTED from the bundle, never read off the entry.
|
|
584
|
+
*
|
|
585
|
+
* `entry_hash` in a subject is checked against this. The walk mirrors `AuditLog.verify`, so an
|
|
586
|
+
* entry whose stored `hash` was replaced does not get to supply the value it is compared against.
|
|
587
|
+
*/
|
|
588
|
+
function recomputedHashes(entries) {
|
|
589
|
+
const out = new Map();
|
|
590
|
+
let prev = audit_js_1.GENESIS;
|
|
591
|
+
entries.forEach((e, i) => {
|
|
592
|
+
const payload = {};
|
|
593
|
+
for (const [k, v] of Object.entries(e))
|
|
594
|
+
if (k !== "hash")
|
|
595
|
+
payload[k] = v;
|
|
596
|
+
let computed;
|
|
597
|
+
try {
|
|
598
|
+
computed = (0, audit_js_1.hashEntry)(prev, payload);
|
|
599
|
+
}
|
|
600
|
+
catch {
|
|
601
|
+
// An unhashable payload has no recomputable hash; that IS the break, at this entry.
|
|
602
|
+
computed = null;
|
|
603
|
+
}
|
|
604
|
+
out.set(orNull(e["seq"]) ?? i, computed);
|
|
605
|
+
prev = computed ?? audit_js_1.GENESIS;
|
|
606
|
+
});
|
|
607
|
+
return out;
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* The v1 subject for the entry at `seq`, recomputed from the ledger.
|
|
611
|
+
*
|
|
612
|
+
* Throws when `seq` names no entry, or names one whose `event` v1 defines no subject for.
|
|
613
|
+
*/
|
|
614
|
+
function envelopeSubject(entries, seq) {
|
|
615
|
+
const entry = entries.find((e) => (0, canonical_js_1.toPlain)(e["seq"]) === seq);
|
|
616
|
+
if (entry === undefined)
|
|
617
|
+
throw new Error(`no entry at seq ${seq}`);
|
|
618
|
+
const event = (0, canonical_js_1.toPlain)(entry["event"]);
|
|
619
|
+
if (!exports.ENVELOPE_SUBJECT_MEMBERS.has(event)) {
|
|
620
|
+
throw new Error(`envelope v${exports.ENVELOPE_VERSION} defines no subject for event '${event}'`);
|
|
621
|
+
}
|
|
622
|
+
const subject = {
|
|
623
|
+
chain_id: orNull(entry["chain_id"]),
|
|
624
|
+
node: orNull(entry["node"]),
|
|
625
|
+
seq,
|
|
626
|
+
entry_hash: recomputedHashes(entries).get(seq) ?? null,
|
|
627
|
+
event,
|
|
628
|
+
};
|
|
629
|
+
if (event === "allow")
|
|
630
|
+
subject["call_id"] = orNull(entry["call_id"]);
|
|
631
|
+
return subject;
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* An observer envelope over the entry at `seq`, signed with the 32-byte Ed25519 `seed`.
|
|
635
|
+
*
|
|
636
|
+
* `entries` is the ledger the subject is recomputed from — a witness signs the identity of an
|
|
637
|
+
* entry that already exists, never a claim it composes itself. That makes the ledger it is
|
|
638
|
+
* signed over part of the signature: sign over the entries AS THEY WILL SHIP. With
|
|
639
|
+
* `exportBundle({redactTask: true})` those are the redacted entries, so export first and sign
|
|
640
|
+
* over the exported bundle's `entries` — `exportBundle` refuses to redact and carry envelopes in
|
|
641
|
+
* one call for exactly this reason.
|
|
642
|
+
*/
|
|
643
|
+
function signEnvelope(entries, seq, seed, kid, observed) {
|
|
644
|
+
const result = observed.result ?? "matched";
|
|
645
|
+
if (!exports.ENVELOPE_RESULTS.includes(result)) {
|
|
646
|
+
throw new Error(`observed.result must be one of [${exports.ENVELOPE_RESULTS.join(", ")}], got '${result}'`);
|
|
647
|
+
}
|
|
648
|
+
const body = {
|
|
649
|
+
v: exports.ENVELOPE_VERSION,
|
|
650
|
+
typ: exports.ENVELOPE_TYP,
|
|
651
|
+
subject: envelopeSubject(entries, seq),
|
|
652
|
+
observed: { result, at: observed.at, method: observed.method },
|
|
653
|
+
witness: { kid, alg: exports.ENVELOPE_ALG },
|
|
654
|
+
};
|
|
655
|
+
const sig = wire_js_1.Ed25519Signer.fromPrivateBytes(seed, kid).sign(envelopeSigningInput(body));
|
|
656
|
+
return { ...body, sig: sig.toString("hex") };
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* The 32-byte Ed25519 public key for `kid`, or an `Error` naming it.
|
|
660
|
+
*
|
|
661
|
+
* A trust set is CALLER CONFIGURATION, not bundle content, so a malformed row is a mistake in the
|
|
662
|
+
* deployment and failing loudly is the only way it does not become a silent downgrade: coercing a
|
|
663
|
+
* number would fabricate zero bytes, and every envelope from that witness would then fail on its
|
|
664
|
+
* SIGNATURE, reading as a witness who signed badly rather than as a trust set never configured.
|
|
665
|
+
*/
|
|
666
|
+
function witnessPublicKey(kid, value) {
|
|
667
|
+
if (typeof value === "string") {
|
|
668
|
+
if (value.length !== 64) {
|
|
669
|
+
throw new Error(`witness key '${kid}': public_key_hex must be 64 hex characters (a 32-byte Ed25519 key), ` +
|
|
670
|
+
`got ${value.length}`);
|
|
671
|
+
}
|
|
672
|
+
if (!/^[0-9a-fA-F]{64}$/.test(value)) {
|
|
673
|
+
throw new Error(`witness key '${kid}': public_key_hex is not hexadecimal`);
|
|
674
|
+
}
|
|
675
|
+
return Buffer.from(value, "hex");
|
|
676
|
+
}
|
|
677
|
+
if (value instanceof Uint8Array) {
|
|
678
|
+
if (value.length !== 32) {
|
|
679
|
+
throw new Error(`witness key '${kid}': an Ed25519 public key is 32 bytes, got ${value.length}`);
|
|
680
|
+
}
|
|
681
|
+
return Buffer.from(value);
|
|
682
|
+
}
|
|
683
|
+
throw new Error(`witness key '${kid}': expected 64 hex characters or 32 bytes`);
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* kid -> `[alg, raw public key]`, from the vector file's own `witness_keys` shape or from a plain
|
|
687
|
+
* `{kid: publicKeyBytes}` record.
|
|
688
|
+
*
|
|
689
|
+
* `null`/absent means no trust anchor is configured, which is an EMPTY set, not an absent check:
|
|
690
|
+
* an envelope naming a kid nobody trusts is `envelope_unknown_witness`, and that is the honest
|
|
691
|
+
* answer whether the trust set is empty or merely does not contain it.
|
|
692
|
+
*
|
|
693
|
+
* Every row is validated here and a bad one throws, naming its kid. This is the one envelope
|
|
694
|
+
* input that is NOT attacker-supplied — the deployment chose these keys — so a mistake in them is
|
|
695
|
+
* reported to the caller rather than folded into a finding about the bundle. v1 defines Ed25519
|
|
696
|
+
* and no other algorithm, so a row declaring anything else is refused too.
|
|
697
|
+
*/
|
|
698
|
+
function trustedWitnesses(witnessKeys) {
|
|
699
|
+
const trusted = new Map();
|
|
700
|
+
if (witnessKeys === null || witnessKeys === undefined)
|
|
701
|
+
return trusted;
|
|
702
|
+
const rows = Array.isArray(witnessKeys)
|
|
703
|
+
? witnessKeys.map((k) => [isRecordLike(k) ? k["kid"] : undefined, k])
|
|
704
|
+
: Object.entries(witnessKeys);
|
|
705
|
+
for (const [kid, value] of rows) {
|
|
706
|
+
if (typeof kid !== "string")
|
|
707
|
+
throw new Error("witness key kid must be a string");
|
|
708
|
+
let key = value;
|
|
709
|
+
if (isRecordLike(value)) {
|
|
710
|
+
const alg = value["alg"];
|
|
711
|
+
if (alg !== exports.ENVELOPE_ALG) {
|
|
712
|
+
throw new Error(`witness key '${kid}': alg must be '${exports.ENVELOPE_ALG}', got ${pyRepr(alg)}`);
|
|
713
|
+
}
|
|
714
|
+
key = value["public_key_hex"];
|
|
715
|
+
}
|
|
716
|
+
trusted.set(kid, [exports.ENVELOPE_ALG, witnessPublicKey(kid, key)]);
|
|
717
|
+
}
|
|
718
|
+
return trusted;
|
|
719
|
+
}
|
|
720
|
+
/** A plain object (not an array, not null, not a Buffer). */
|
|
721
|
+
function isRecordLike(v) {
|
|
722
|
+
return v !== null && typeof v === "object" && !Array.isArray(v) && !(v instanceof Uint8Array);
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* One `envelopeBytes` element as bytes, or null when it is not bytes at all.
|
|
726
|
+
*
|
|
727
|
+
* Coercing a number would fabricate that many ZERO bytes, turning a caller's mistake into a
|
|
728
|
+
* canonicality finding about the bundle; hex that does not parse is the same mistake in a
|
|
729
|
+
* different shape. Neither is coerced.
|
|
730
|
+
*/
|
|
731
|
+
function receivedBytes(raw) {
|
|
732
|
+
if (typeof raw === "string") {
|
|
733
|
+
if (raw.length % 2 !== 0 || !/^[0-9a-fA-F]*$/.test(raw))
|
|
734
|
+
return null;
|
|
735
|
+
return Buffer.from(raw, "hex");
|
|
736
|
+
}
|
|
737
|
+
if (raw instanceof Uint8Array)
|
|
738
|
+
return Buffer.from(raw);
|
|
739
|
+
return null;
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* A subject `seq` this build will look an entry up by: a JSON integer, and never a boolean.
|
|
743
|
+
*
|
|
744
|
+
* The type check comes first and every use of `seq` is behind it — in Python an unguarded lookup
|
|
745
|
+
* raises on a list or an object and finds the entry at seq 1 for `true`, and the two
|
|
746
|
+
* implementations report the same failure for the same bundle.
|
|
747
|
+
*/
|
|
748
|
+
function isSeq(value) {
|
|
749
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* The report line: the state and the result together, in the same form for all three results. A
|
|
753
|
+
* process-asserted entry gets no result.
|
|
754
|
+
*/
|
|
755
|
+
function envelopeLine(state, result) {
|
|
756
|
+
return state === exports.WITNESS_SIGNED ? `${state} (${String(result)})` : state;
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Score every envelope in the bundle and derive the per-entry state.
|
|
760
|
+
*
|
|
761
|
+
* Two rules bind where a failure may land: an envelope failure lands only on the hop that
|
|
762
|
+
* envelope covers, never on a hop coverage skipped; and no chain-level integrity failure is ever
|
|
763
|
+
* raised because an envelope failed — that one comes from a real anchor mismatch and from
|
|
764
|
+
* nothing else.
|
|
765
|
+
*
|
|
766
|
+
* One entry, at most one envelope. A second envelope naming a `subject.seq` an earlier one in
|
|
767
|
+
* this array already named is `envelope_duplicate_subject`, and the entry falls back to
|
|
768
|
+
* `process-asserted`: two observations of one event contradict each other by construction —
|
|
769
|
+
* whoever appends the second decides what the first said, and an entry whose coverage is
|
|
770
|
+
* disputed must not read as clean.
|
|
771
|
+
*/
|
|
772
|
+
function scoreEnvelopes(entries, envelopes, trusted, rawBytes) {
|
|
773
|
+
const fail = new FailureLog();
|
|
774
|
+
const states = {};
|
|
775
|
+
const results = {};
|
|
776
|
+
entries.forEach((e, i) => {
|
|
777
|
+
states[String(orNull(e["seq"]) ?? i)] = exports.PROCESS_ASSERTED;
|
|
778
|
+
});
|
|
779
|
+
// The hash walk is what an envelope's binding member is checked against; a bundle carrying
|
|
780
|
+
// none does not pay for it. Every entry is process-asserted in that case, which is the status
|
|
781
|
+
// quo and exactly what this reports.
|
|
782
|
+
const bySeq = new Map();
|
|
783
|
+
let recomputed = new Map();
|
|
784
|
+
if (envelopes.length > 0) {
|
|
785
|
+
entries.forEach((e, i) => bySeq.set(orNull(e["seq"]) ?? i, e));
|
|
786
|
+
recomputed = recomputedHashes(entries);
|
|
787
|
+
}
|
|
788
|
+
// seq -> how many envelopes in this array named it, valid or not. `scoreEnvelope` counts an
|
|
789
|
+
// envelope in as soon as its subject names an entry this bundle has.
|
|
790
|
+
const claims = new Map();
|
|
791
|
+
envelopes.forEach((envelope, index) => {
|
|
792
|
+
const raw = rawBytes !== null && index < rawBytes.length ? rawBytes[index] ?? null : null;
|
|
793
|
+
const covered = scoreEnvelope(envelope, index, bySeq, recomputed, trusted, raw, fail, claims);
|
|
794
|
+
if (covered === null)
|
|
795
|
+
return;
|
|
796
|
+
states[String(covered.seq)] = exports.WITNESS_SIGNED;
|
|
797
|
+
results[String(covered.seq)] = covered.result;
|
|
798
|
+
});
|
|
799
|
+
// The first envelope's result stands in `results` — it is what that witness said, and the
|
|
800
|
+
// duplicate does not erase it — but the STATE falls back, so a contradicted entry never
|
|
801
|
+
// reports witness-signed and the bundle rejects.
|
|
802
|
+
for (const [seq, count] of claims) {
|
|
803
|
+
if (count > 1)
|
|
804
|
+
states[seq] = exports.PROCESS_ASSERTED;
|
|
805
|
+
}
|
|
806
|
+
const lines = {};
|
|
807
|
+
for (const [seq, state] of Object.entries(states)) {
|
|
808
|
+
lines[seq] = envelopeLine(state, results[seq] ?? null);
|
|
809
|
+
}
|
|
810
|
+
const summary = {
|
|
811
|
+
status: fail.length === 0 ? "verified" : "FAILED",
|
|
812
|
+
count: envelopes.length,
|
|
813
|
+
witness_signed: Object.entries(states)
|
|
814
|
+
.filter(([, state]) => state === exports.WITNESS_SIGNED)
|
|
815
|
+
.map(([seq]) => Number(seq))
|
|
816
|
+
.sort((a, b) => a - b),
|
|
817
|
+
states,
|
|
818
|
+
results,
|
|
819
|
+
lines,
|
|
820
|
+
failures: [...fail.messages],
|
|
821
|
+
};
|
|
822
|
+
return [summary, fail];
|
|
823
|
+
}
|
|
824
|
+
/** Python `repr` for a member set, so both implementations print the same failure strings. */
|
|
825
|
+
function reprList(values) {
|
|
826
|
+
return `[${values.map((v) => `'${v}'`).join(", ")}]`;
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* One envelope, checked in the order the seven named failures are defined in.
|
|
830
|
+
*
|
|
831
|
+
* Returns the covered entry for an envelope that verified, and `null` for one that did not.
|
|
832
|
+
* Every failure is positioned on the entry the envelope COVERS, found by `subject.seq` — the
|
|
833
|
+
* locators are checked against that entry, not used to find it.
|
|
834
|
+
*
|
|
835
|
+
* `claims` is the caller's seq -> count of the envelopes that have named each entry so far, and
|
|
836
|
+
* this function updates it. An envelope claims its entry as soon as `subject.seq` finds one,
|
|
837
|
+
* BEFORE the rest of the subject is checked, so a second envelope over an entry an earlier one
|
|
838
|
+
* already named is `envelope_duplicate_subject` whether either of them is otherwise sound: the
|
|
839
|
+
* point of the check is that no one can decide what an earlier witness said by appending after
|
|
840
|
+
* it.
|
|
841
|
+
*/
|
|
842
|
+
function scoreEnvelope(envelope, index, bySeq, recomputed, trusted, raw, fail, claims) {
|
|
843
|
+
const isRecord = (v) => v !== null && typeof v === "object" && !Array.isArray(v) && !(v instanceof canonical_js_1.RawNumber);
|
|
844
|
+
const subject = isRecord(envelope) ? envelope["subject"] : undefined;
|
|
845
|
+
function position() {
|
|
846
|
+
// Every failure is positioned by `subject.seq`, and `subject` is attacker-supplied, so the
|
|
847
|
+
// lookup is guarded: a seq that is not an integer positions nothing, which is honest — it
|
|
848
|
+
// names no entry — and it is never used as a key.
|
|
849
|
+
const s = isRecord(subject) ? (0, canonical_js_1.toPlain)(subject["seq"]) : null;
|
|
850
|
+
if (!isSeq(s))
|
|
851
|
+
return [null, null];
|
|
852
|
+
const entry = bySeq.get(s);
|
|
853
|
+
if (entry === undefined)
|
|
854
|
+
return [s, null];
|
|
855
|
+
return [orNull(entry["seq"]), orNull(entry["node"])];
|
|
856
|
+
}
|
|
857
|
+
function report(reason, detail) {
|
|
858
|
+
const [seq, node] = position();
|
|
859
|
+
fail.add(reason, `${reason}: ${detail}`, { seq, node });
|
|
860
|
+
return null;
|
|
861
|
+
}
|
|
862
|
+
if (!isRecord(envelope)) {
|
|
863
|
+
fail.add("envelope_unknown_version", `envelope_unknown_version: envelope #${index} is not a JSON object`);
|
|
864
|
+
return null;
|
|
865
|
+
}
|
|
866
|
+
// (1) version — a `v` or `typ` this build does not know is a DIFFERENT CONTRACT, and nothing
|
|
867
|
+
// further about it can be read safely.
|
|
868
|
+
const v = (0, canonical_js_1.toPlain)(envelope["v"]);
|
|
869
|
+
const typ = (0, canonical_js_1.toPlain)(envelope["typ"]);
|
|
870
|
+
if (v !== exports.ENVELOPE_VERSION || typ !== exports.ENVELOPE_TYP) {
|
|
871
|
+
return report("envelope_unknown_version", `envelope v=${pyRepr(v)} typ=${pyRepr(typ)}, this build knows v=${exports.ENVELOPE_VERSION} ` +
|
|
872
|
+
`typ='${exports.ENVELOPE_TYP}'`);
|
|
873
|
+
}
|
|
874
|
+
// (2) member sets — the version commits the exact signed member set of the whole envelope, so
|
|
875
|
+
// a member added ANYWHERE is a new version that did not declare itself.
|
|
876
|
+
const levels = [
|
|
877
|
+
["envelope", envelope, exports.ENVELOPE_MEMBERS],
|
|
878
|
+
["observed", envelope["observed"], ENVELOPE_OBSERVED_MEMBERS],
|
|
879
|
+
["witness", envelope["witness"], ENVELOPE_WITNESS_MEMBERS],
|
|
880
|
+
];
|
|
881
|
+
for (const [label, value, expected] of levels) {
|
|
882
|
+
const members = isRecord(value) ? (0, canonical_js_1.sortedStrings)(Object.keys(value)) : null;
|
|
883
|
+
if (members === null || members.length !== expected.size || members.some((m) => !expected.has(m))) {
|
|
884
|
+
// "not an object" rather than the type's name: the two languages spell their type names
|
|
885
|
+
// differently and both implementations report the same failure strings for the same bundle.
|
|
886
|
+
const got = members === null ? "not an object" : reprList(members);
|
|
887
|
+
return report("envelope_unknown_member", `${label} member set is ${got}, expected ${reprList((0, canonical_js_1.sortedStrings)(expected))}`);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
// (3) subject — the event decides the member set; a member ADDED to it is unknown_member, one
|
|
891
|
+
// MISSING is subject_mismatch (a subject that does not say what it covers).
|
|
892
|
+
if (!isRecord(subject)) {
|
|
893
|
+
// No type name, for the same reason as the member-set message above: the two languages spell
|
|
894
|
+
// their type names differently and report the same strings for the same bundle.
|
|
895
|
+
return report("envelope_subject_mismatch", "subject is not a JSON object");
|
|
896
|
+
}
|
|
897
|
+
const event = (0, canonical_js_1.toPlain)(subject["event"]);
|
|
898
|
+
if (typeof event !== "string") {
|
|
899
|
+
// `event` selects the subject member set, so it is a lookup key as well; in Python an
|
|
900
|
+
// unhashable one raises. Found by the hostile-value suite, not by review.
|
|
901
|
+
return report("envelope_subject_mismatch", "subject event is not a string");
|
|
902
|
+
}
|
|
903
|
+
const expectedMembers = exports.ENVELOPE_SUBJECT_MEMBERS.get(event);
|
|
904
|
+
if (expectedMembers === undefined) {
|
|
905
|
+
return report("envelope_subject_mismatch", `subject event=${pyRepr(event)}; envelope v${exports.ENVELOPE_VERSION} defines a subject for ` +
|
|
906
|
+
`${reprList((0, canonical_js_1.sortedStrings)(exports.ENVELOPE_SUBJECT_MEMBERS.keys()))} and no other event`);
|
|
907
|
+
}
|
|
908
|
+
const present = (0, canonical_js_1.sortedStrings)(Object.keys(subject));
|
|
909
|
+
const added = present.filter((m) => !expectedMembers.has(m));
|
|
910
|
+
if (added.length > 0) {
|
|
911
|
+
return report("envelope_unknown_member", `subject member set is ${reprList(present)}, expected ` +
|
|
912
|
+
`${reprList((0, canonical_js_1.sortedStrings)(expectedMembers))} for a ${event} subject`);
|
|
913
|
+
}
|
|
914
|
+
const missing = (0, canonical_js_1.sortedStrings)(expectedMembers).filter((m) => !(m in subject));
|
|
915
|
+
if (missing.length > 0) {
|
|
916
|
+
return report("envelope_subject_mismatch", `subject is missing ${reprList(missing)}, which a ${event} subject requires`);
|
|
917
|
+
}
|
|
918
|
+
// (3a) the binding member. `seq` is the lookup key, so there is nothing to compare it against;
|
|
919
|
+
// the entry it finds supplies the hash the subject is checked against. It is also the one
|
|
920
|
+
// subject member used as a KEY, so its type is checked before it is used as one.
|
|
921
|
+
const subjectSeq = (0, canonical_js_1.toPlain)(subject["seq"]);
|
|
922
|
+
if (!isSeq(subjectSeq)) {
|
|
923
|
+
return report("envelope_subject_mismatch", "subject seq is not an integer");
|
|
924
|
+
}
|
|
925
|
+
const entry = bySeq.get(subjectSeq);
|
|
926
|
+
if (entry === undefined) {
|
|
927
|
+
return report("envelope_subject_mismatch", `no entry at seq ${pyRepr(subjectSeq)} in this bundle`);
|
|
928
|
+
}
|
|
929
|
+
const seq = orNull(entry["seq"]);
|
|
930
|
+
// (3a') one entry, at most one envelope. Counted here, before anything else about this
|
|
931
|
+
// envelope is judged, so the rule cannot be sidestepped by making the second envelope
|
|
932
|
+
// defective in some other way as well.
|
|
933
|
+
const claimKey = String(seq);
|
|
934
|
+
const already = claims.get(claimKey) ?? 0;
|
|
935
|
+
claims.set(claimKey, already + 1);
|
|
936
|
+
if (already > 0) {
|
|
937
|
+
return report("envelope_duplicate_subject", `seq ${claimKey} is already covered by an earlier envelope in this bundle; two ` +
|
|
938
|
+
"observations of one event contradict each other by construction, so this entry is not " +
|
|
939
|
+
"witness-signed");
|
|
940
|
+
}
|
|
941
|
+
const computed = recomputed.get(seq) ?? null;
|
|
942
|
+
const claimed = (0, canonical_js_1.toPlain)(subject["entry_hash"]);
|
|
943
|
+
if (claimed !== computed) {
|
|
944
|
+
return report("envelope_subject_mismatch", `subject entry_hash ${pyRepr(claimed)} != the hash recomputed for seq ${String(seq)} from ` +
|
|
945
|
+
`this bundle (${pyRepr(computed)})`);
|
|
946
|
+
}
|
|
947
|
+
// (3b) the locators, checked against the SAME entry `seq` found. A matching locator attests
|
|
948
|
+
// nothing on its own; a disagreeing one is the same failure at the same position.
|
|
949
|
+
const locators = [
|
|
950
|
+
["chain_id", orNull(entry["chain_id"])],
|
|
951
|
+
["node", orNull(entry["node"])],
|
|
952
|
+
["event", orNull(entry["event"])],
|
|
953
|
+
];
|
|
954
|
+
if (event === "allow")
|
|
955
|
+
locators.push(["call_id", orNull(entry["call_id"])]);
|
|
956
|
+
for (const [member, actual] of locators) {
|
|
957
|
+
const stated = (0, canonical_js_1.toPlain)(subject[member]);
|
|
958
|
+
if (stated !== actual) {
|
|
959
|
+
return report("envelope_subject_mismatch", `subject ${member}=${pyRepr(stated)} != ${pyRepr(actual)} on the entry at seq ${String(seq)}`);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
// (4) canonicality — an invariant SEPARATE from the signature: the received bytes must equal
|
|
963
|
+
// JCS of what they parse to. It can only be raised where the bytes as received are supplied,
|
|
964
|
+
// because formatting and escaping do not survive a parse.
|
|
965
|
+
let nonCanonical = false;
|
|
966
|
+
if (raw !== null) {
|
|
967
|
+
const received = receivedBytes(raw);
|
|
968
|
+
if (received === null) {
|
|
969
|
+
return report("envelope_non_canonical", "envelope_bytes entry is not hex or bytes");
|
|
970
|
+
}
|
|
971
|
+
let recanonicalized;
|
|
972
|
+
try {
|
|
973
|
+
recanonicalized = (0, canonical_js_1.canonicalBytes)(envelope);
|
|
974
|
+
}
|
|
975
|
+
catch (err) {
|
|
976
|
+
// A value JCS cannot represent at all — a non-finite number, an integer outside the
|
|
977
|
+
// binary64 safe range, a lone surrogate. There is no canonical form to compare the
|
|
978
|
+
// received bytes with and none to verify a signature over, so this is the end of it.
|
|
979
|
+
return report("envelope_non_canonical", `the envelope cannot be canonicalized: ${String(err)}`);
|
|
980
|
+
}
|
|
981
|
+
if (!recanonicalized.equals(received)) {
|
|
982
|
+
nonCanonical = true;
|
|
983
|
+
report("envelope_non_canonical", "the bytes as received are not JCS of what they parse to " +
|
|
984
|
+
`(${received.length} received, ${recanonicalized.length} canonical)`);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
// (5) the witness key. A signature that verifies under some OTHER trusted key is not
|
|
988
|
+
// witness-signed: the kid names the key, and that is the key it has to verify under.
|
|
989
|
+
const witness = envelope["witness"];
|
|
990
|
+
const kid = (0, canonical_js_1.toPlain)(witness["kid"]);
|
|
991
|
+
const alg = (0, canonical_js_1.toPlain)(witness["alg"]);
|
|
992
|
+
if (typeof kid !== "string") {
|
|
993
|
+
// `kid` names a key, so it is a lookup key here too, and in Python an unhashable one raises.
|
|
994
|
+
return report("envelope_unknown_witness", "witness kid is not a string");
|
|
995
|
+
}
|
|
996
|
+
if (alg !== exports.ENVELOPE_ALG) {
|
|
997
|
+
// v1 defines Ed25519 and nothing else. Without this, `"alg": "none"` on both sides — in the
|
|
998
|
+
// envelope and in a trust-set row — agreed with each other and read as witness-signed.
|
|
999
|
+
return report("envelope_unknown_witness", `witness alg=${pyRepr(alg)} is not '${exports.ENVELOPE_ALG}'; envelope v${exports.ENVELOPE_VERSION} ` +
|
|
1000
|
+
"defines Ed25519 and no other algorithm");
|
|
1001
|
+
}
|
|
1002
|
+
const known = trusted.get(kid);
|
|
1003
|
+
if (known === undefined) {
|
|
1004
|
+
return report("envelope_unknown_witness", `witness kid=${pyRepr(kid)} alg=${pyRepr(alg)} is not in the trusted witness keys ` +
|
|
1005
|
+
`(${reprList((0, canonical_js_1.sortedStrings)(trusted.keys()))})`);
|
|
1006
|
+
}
|
|
1007
|
+
// (6) the signature, over JCS(envelope minus "sig").
|
|
1008
|
+
const sigHex = (0, canonical_js_1.toPlain)(envelope["sig"]);
|
|
1009
|
+
if (typeof sigHex !== "string") {
|
|
1010
|
+
// A `sig` that is not a string is not a signature. In Python it reached `bytes.fromhex` and
|
|
1011
|
+
// raised a TypeError the surrounding `except ValueError` does not catch.
|
|
1012
|
+
return report("envelope_bad_signature", "sig is not a hex string");
|
|
1013
|
+
}
|
|
1014
|
+
const signature = /^[0-9a-fA-F]*$/.test(sigHex) && sigHex.length % 2 === 0
|
|
1015
|
+
? Buffer.from(sigHex, "hex")
|
|
1016
|
+
: Buffer.alloc(0);
|
|
1017
|
+
let signingInput;
|
|
1018
|
+
try {
|
|
1019
|
+
signingInput = envelopeSigningInput(envelope);
|
|
1020
|
+
}
|
|
1021
|
+
catch (err) {
|
|
1022
|
+
// Reached only when no `envelopeBytes` were supplied, so step (4) did not run: the envelope
|
|
1023
|
+
// holds a value JCS cannot represent and there is nothing to verify OVER.
|
|
1024
|
+
return report("envelope_non_canonical", `the envelope cannot be canonicalized: ${String(err)}`);
|
|
1025
|
+
}
|
|
1026
|
+
let verified = false;
|
|
1027
|
+
try {
|
|
1028
|
+
verified = new wire_js_1.Ed25519Verifier(known[1], kid).verify(signingInput, signature);
|
|
1029
|
+
}
|
|
1030
|
+
catch {
|
|
1031
|
+
verified = false;
|
|
1032
|
+
}
|
|
1033
|
+
if (!verified) {
|
|
1034
|
+
return report("envelope_bad_signature", `the signature does not verify under the key kid=${pyRepr(kid)} names`);
|
|
1035
|
+
}
|
|
1036
|
+
if (nonCanonical)
|
|
1037
|
+
return null;
|
|
1038
|
+
return { seq, node: orNull(entry["node"]), result: (0, canonical_js_1.toPlain)(envelope["observed"]["result"]) };
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Score a bundle's observer envelopes on their own, without the ledger checks.
|
|
1042
|
+
*
|
|
1043
|
+
* Returns `{ok, ...summary, failure_details}`. `states` maps every entry's seq to
|
|
1044
|
+
* `witness-signed` or `process-asserted`; `lines` is the report line for each.
|
|
1045
|
+
*/
|
|
1046
|
+
function verifyEnvelopes(bundle, options = {}) {
|
|
1047
|
+
const [summary, fail] = scoreEnvelopes(bundle.entries ?? [], bundle.envelopes ?? [], trustedWitnesses(options.witnessKeys ?? null), options.envelopeBytes ?? null);
|
|
1048
|
+
return { ok: fail.length === 0, ...summary, failure_details: fail.details };
|
|
1049
|
+
}
|
|
1050
|
+
// =============================================================================================
|
|
465
1051
|
// Execution binding (0.9.0): offline checks over callId/allow/outcome, from the ledger alone —
|
|
466
1052
|
// docs/execution-binding spec section 5. schemaVersion=2 chains only; a v1 bundle's
|
|
467
1053
|
// executionBinding is `{status: "not applicable"}`.
|
|
@@ -971,6 +1557,7 @@ function verifyBundle(bundle, signer = null, options = {}) {
|
|
|
971
1557
|
chain_id: false,
|
|
972
1558
|
root: false,
|
|
973
1559
|
expected_anchor: "not checked",
|
|
1560
|
+
envelopes: "not present",
|
|
974
1561
|
};
|
|
975
1562
|
const log = new FailureLog();
|
|
976
1563
|
// (0) version: the bundle must declare a schema version this build understands, and — when
|
|
@@ -1125,9 +1712,21 @@ function verifyBundle(bundle, signer = null, options = {}) {
|
|
|
1125
1712
|
: [{ status: "not applicable" }, new FailureLog()];
|
|
1126
1713
|
if (eb.failures !== undefined && eb.failures.length > 0)
|
|
1127
1714
|
log.extend(ebFailures);
|
|
1128
|
-
//
|
|
1129
|
-
//
|
|
1130
|
-
//
|
|
1715
|
+
// (4) observer envelopes. Never required — an absent envelope is the status quo and changes
|
|
1716
|
+
// nothing — but a PRESENT one has to verify, and a broken one lands in this same list. The
|
|
1717
|
+
// per-entry state is reported either way, so a reader sees which hops were covered before
|
|
1718
|
+
// reading which one failed.
|
|
1719
|
+
const [envelopeSummary, envelopeFailures] = scoreEnvelopes(entries, bundle.envelopes ?? [], trustedWitnesses(options.witnessKeys ?? null), options.envelopeBytes ?? null);
|
|
1720
|
+
if (bundle.envelopes === undefined) {
|
|
1721
|
+
envelopeSummary.status = "not present";
|
|
1722
|
+
}
|
|
1723
|
+
else {
|
|
1724
|
+
checks.envelopes = envelopeSummary.status;
|
|
1725
|
+
log.extend(envelopeFailures);
|
|
1726
|
+
}
|
|
1727
|
+
// "anchor", "expected_anchor" and "envelopes" are excluded here — each carries a status
|
|
1728
|
+
// string, not a plain pass/fail boolean, and a failed check on any of them already lands its
|
|
1729
|
+
// own entry in `failures`, which the `ok` computation still gates on.
|
|
1131
1730
|
const ok = checks.integrity &&
|
|
1132
1731
|
checks.monotonicity &&
|
|
1133
1732
|
checks.containment &&
|
|
@@ -1144,6 +1743,7 @@ function verifyBundle(bundle, signer = null, options = {}) {
|
|
|
1144
1743
|
actions_checked: actions,
|
|
1145
1744
|
chain_id: orNull(bundle.chain_id),
|
|
1146
1745
|
execution_binding: eb,
|
|
1746
|
+
envelopes: envelopeSummary,
|
|
1147
1747
|
verified_against: expectedAnchor !== null || expectedHead !== null ? "expected_anchor" : "bundle_anchor",
|
|
1148
1748
|
};
|
|
1149
1749
|
}
|