@bcts/envelope 1.0.0-alpha.5

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.
Files changed (44) hide show
  1. package/LICENSE +48 -0
  2. package/README.md +23 -0
  3. package/dist/index.cjs +2646 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +978 -0
  6. package/dist/index.d.cts.map +1 -0
  7. package/dist/index.d.mts +978 -0
  8. package/dist/index.d.mts.map +1 -0
  9. package/dist/index.iife.js +2644 -0
  10. package/dist/index.iife.js.map +1 -0
  11. package/dist/index.mjs +2552 -0
  12. package/dist/index.mjs.map +1 -0
  13. package/package.json +85 -0
  14. package/src/base/assertion.ts +179 -0
  15. package/src/base/assertions.ts +304 -0
  16. package/src/base/cbor.ts +122 -0
  17. package/src/base/digest.ts +204 -0
  18. package/src/base/elide.ts +526 -0
  19. package/src/base/envelope-decodable.ts +229 -0
  20. package/src/base/envelope-encodable.ts +71 -0
  21. package/src/base/envelope.ts +790 -0
  22. package/src/base/error.ts +421 -0
  23. package/src/base/index.ts +56 -0
  24. package/src/base/leaf.ts +226 -0
  25. package/src/base/queries.ts +374 -0
  26. package/src/base/walk.ts +241 -0
  27. package/src/base/wrap.ts +72 -0
  28. package/src/extension/attachment.ts +369 -0
  29. package/src/extension/compress.ts +293 -0
  30. package/src/extension/encrypt.ts +379 -0
  31. package/src/extension/expression.ts +404 -0
  32. package/src/extension/index.ts +72 -0
  33. package/src/extension/proof.ts +276 -0
  34. package/src/extension/recipient.ts +557 -0
  35. package/src/extension/salt.ts +223 -0
  36. package/src/extension/signature.ts +463 -0
  37. package/src/extension/types.ts +222 -0
  38. package/src/format/diagnostic.ts +116 -0
  39. package/src/format/hex.ts +25 -0
  40. package/src/format/index.ts +13 -0
  41. package/src/format/tree.ts +168 -0
  42. package/src/index.ts +32 -0
  43. package/src/utils/index.ts +8 -0
  44. package/src/utils/string.ts +48 -0
@@ -0,0 +1,223 @@
1
+ import { Envelope } from "../base/envelope";
2
+ import { EnvelopeError } from "../base/error";
3
+ import {
4
+ SecureRandomNumberGenerator,
5
+ rngRandomData,
6
+ rngNextInClosedRangeI32,
7
+ type RandomNumberGenerator,
8
+ } from "@bcts/rand";
9
+
10
+ /// Extension for adding salt to envelopes to prevent correlation.
11
+ ///
12
+ /// This module provides functionality for decorrelating envelopes by adding
13
+ /// random salt. Salt is added as an assertion with the predicate 'salt' and
14
+ /// a random value. When an envelope is elided, this salt ensures that the
15
+ /// digest of the elided envelope cannot be correlated with other elided
16
+ /// envelopes containing the same information.
17
+ ///
18
+ /// Decorrelation is an important privacy feature that prevents third parties
19
+ /// from determining whether two elided envelopes originally contained the same
20
+ /// information by comparing their digests.
21
+ ///
22
+ /// Based on bc-envelope-rust/src/extension/salt.rs and bc-components-rust/src/salt.rs
23
+ ///
24
+ /// @example
25
+ /// ```typescript
26
+ /// // Create a simple envelope
27
+ /// const envelope = Envelope.new("Hello");
28
+ ///
29
+ /// // Create a decorrelated version by adding salt
30
+ /// const salted = envelope.addSalt();
31
+ ///
32
+ /// // The salted envelope has a different digest than the original
33
+ /// console.log(envelope.digest().equals(salted.digest())); // false
34
+ /// ```
35
+
36
+ /// The standard predicate for salt assertions
37
+ export const SALT = "salt";
38
+
39
+ /// Minimum salt size in bytes (64 bits)
40
+ const MIN_SALT_SIZE = 8;
41
+
42
+ /// Creates a new SecureRandomNumberGenerator instance
43
+ function createSecureRng(): RandomNumberGenerator {
44
+ return new SecureRandomNumberGenerator();
45
+ }
46
+
47
+ /// Generates random bytes using the rand package
48
+ function generateRandomBytes(length: number, rng?: RandomNumberGenerator): Uint8Array {
49
+ const actualRng = rng ?? createSecureRng();
50
+ return rngRandomData(actualRng, length);
51
+ }
52
+
53
+ /// Calculates salt size proportional to envelope size
54
+ /// This matches the Rust implementation in bc-components-rust/src/salt.rs
55
+ function calculateProportionalSaltSize(envelopeSize: number, rng?: RandomNumberGenerator): number {
56
+ const actualRng = rng ?? createSecureRng();
57
+ const count = envelopeSize;
58
+ const minSize = Math.max(8, Math.ceil(count * 0.05));
59
+ const maxSize = Math.max(minSize + 8, Math.ceil(count * 0.25));
60
+ return rngNextInClosedRangeI32(actualRng, minSize, maxSize);
61
+ }
62
+
63
+ declare module "../base/envelope" {
64
+ interface Envelope {
65
+ /// Adds a proportionally-sized salt assertion to decorrelate the envelope.
66
+ ///
67
+ /// This method adds random salt bytes as an assertion to the envelope. The
68
+ /// size of the salt is proportional to the size of the envelope being
69
+ /// salted:
70
+ /// - For small envelopes: 8-16 bytes
71
+ /// - For larger envelopes: 5-25% of the envelope's size
72
+ ///
73
+ /// Salt is added as an assertion with the predicate 'salt' and an object
74
+ /// containing random bytes. This changes the digest of the envelope while
75
+ /// preserving its semantic content, making it impossible to correlate with
76
+ /// other envelopes containing the same information.
77
+ ///
78
+ /// @returns A new envelope with the salt assertion added
79
+ ///
80
+ /// @example
81
+ /// ```typescript
82
+ /// // Create an envelope with personally identifiable information
83
+ /// const alice = Envelope.new("Alice")
84
+ /// .addAssertion("email", "alice@example.com")
85
+ /// .addAssertion("ssn", "123-45-6789");
86
+ ///
87
+ /// // Create a second envelope with the same information
88
+ /// const alice2 = Envelope.new("Alice")
89
+ /// .addAssertion("email", "alice@example.com")
90
+ /// .addAssertion("ssn", "123-45-6789");
91
+ ///
92
+ /// // The envelopes have the same digest
93
+ /// console.log(alice.digest().equals(alice2.digest())); // true
94
+ ///
95
+ /// // Add salt to both envelopes
96
+ /// const aliceSalted = alice.addSalt();
97
+ /// const alice2Salted = alice2.addSalt();
98
+ ///
99
+ /// // Now the envelopes have different digests, preventing correlation
100
+ /// console.log(aliceSalted.digest().equals(alice2Salted.digest())); // false
101
+ /// ```
102
+ addSalt(): Envelope;
103
+
104
+ /// Adds salt of a specific byte length to the envelope.
105
+ ///
106
+ /// This method adds salt of a specified number of bytes to decorrelate the
107
+ /// envelope. It requires that the byte count be at least 8 bytes (64 bits)
108
+ /// to ensure sufficient entropy for effective decorrelation.
109
+ ///
110
+ /// @param count - The exact number of salt bytes to add
111
+ /// @returns A new envelope with salt added
112
+ /// @throws {EnvelopeError} If the byte count is less than 8
113
+ ///
114
+ /// @example
115
+ /// ```typescript
116
+ /// const envelope = Envelope.new("Hello");
117
+ ///
118
+ /// // Add exactly 16 bytes of salt
119
+ /// const salted = envelope.addSaltWithLength(16);
120
+ ///
121
+ /// // Trying to add less than 8 bytes will throw an error
122
+ /// try {
123
+ /// envelope.addSaltWithLength(7);
124
+ /// } catch (e) {
125
+ /// console.log("Error: salt must be at least 8 bytes");
126
+ /// }
127
+ /// ```
128
+ addSaltWithLength(count: number): Envelope;
129
+
130
+ /// Adds the given salt bytes as an assertion to the envelope.
131
+ ///
132
+ /// This method attaches specific salt bytes as an assertion to the
133
+ /// envelope, using 'salt' as the predicate. This is useful when you need
134
+ /// to control the specific salt content being added.
135
+ ///
136
+ /// @param saltBytes - A Uint8Array containing salt bytes
137
+ /// @returns A new envelope with the salt assertion added
138
+ ///
139
+ /// @example
140
+ /// ```typescript
141
+ /// // Create salt with specific bytes
142
+ /// const salt = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
143
+ ///
144
+ /// // Add this specific salt to an envelope
145
+ /// const envelope = Envelope.new("Hello");
146
+ /// const salted = envelope.addSaltBytes(salt);
147
+ /// ```
148
+ addSaltBytes(saltBytes: Uint8Array): Envelope;
149
+
150
+ /// Adds salt with a byte length randomly chosen from the given range.
151
+ ///
152
+ /// This method adds salt with a length randomly selected from the specified
153
+ /// range to decorrelate the envelope. This provides additional
154
+ /// decorrelation by varying the size of the salt itself.
155
+ ///
156
+ /// @param min - Minimum number of salt bytes (must be at least 8)
157
+ /// @param max - Maximum number of salt bytes
158
+ /// @returns A new envelope with salt added
159
+ /// @throws {EnvelopeError} If min is less than 8 or max is less than min
160
+ ///
161
+ /// @example
162
+ /// ```typescript
163
+ /// const envelope = Envelope.new("Hello");
164
+ ///
165
+ /// // Add salt with a length randomly chosen between 16 and 32 bytes
166
+ /// const salted = envelope.addSaltInRange(16, 32);
167
+ /// ```
168
+ addSaltInRange(min: number, max: number): Envelope;
169
+ }
170
+ }
171
+
172
+ /// Implementation of addSalt()
173
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
174
+ if (Envelope?.prototype) {
175
+ Envelope.prototype.addSalt = function (this: Envelope): Envelope {
176
+ const rng = createSecureRng();
177
+ const envelopeSize = this.cborBytes().length;
178
+ const saltSize = calculateProportionalSaltSize(envelopeSize, rng);
179
+ const saltBytes = generateRandomBytes(saltSize, rng);
180
+ return this.addAssertion(SALT, saltBytes);
181
+ };
182
+
183
+ /// Implementation of addSaltWithLength()
184
+ Envelope.prototype.addSaltWithLength = function (this: Envelope, count: number): Envelope {
185
+ if (count < MIN_SALT_SIZE) {
186
+ throw EnvelopeError.general(`Salt must be at least ${MIN_SALT_SIZE} bytes, got ${count}`);
187
+ }
188
+ const saltBytes = generateRandomBytes(count);
189
+ return this.addAssertion(SALT, saltBytes);
190
+ };
191
+
192
+ /// Implementation of addSaltBytes()
193
+ Envelope.prototype.addSaltBytes = function (this: Envelope, saltBytes: Uint8Array): Envelope {
194
+ if (saltBytes.length < MIN_SALT_SIZE) {
195
+ throw EnvelopeError.general(
196
+ `Salt must be at least ${MIN_SALT_SIZE} bytes, got ${saltBytes.length}`,
197
+ );
198
+ }
199
+ return this.addAssertion(SALT, saltBytes);
200
+ };
201
+
202
+ /// Implementation of addSaltInRange()
203
+ Envelope.prototype.addSaltInRange = function (
204
+ this: Envelope,
205
+ min: number,
206
+ max: number,
207
+ ): Envelope {
208
+ if (min < MIN_SALT_SIZE) {
209
+ throw EnvelopeError.general(
210
+ `Minimum salt size must be at least ${MIN_SALT_SIZE} bytes, got ${min}`,
211
+ );
212
+ }
213
+ if (max < min) {
214
+ throw EnvelopeError.general(
215
+ `Maximum salt size must be at least minimum, got min=${min} max=${max}`,
216
+ );
217
+ }
218
+ const rng = createSecureRng();
219
+ const saltSize = rngNextInClosedRangeI32(rng, min, max);
220
+ const saltBytes = generateRandomBytes(saltSize, rng);
221
+ return this.addAssertion(SALT, saltBytes);
222
+ };
223
+ }
@@ -0,0 +1,463 @@
1
+ /**
2
+ * Signature Extension for Gordian Envelope
3
+ *
4
+ * Provides functionality for digitally signing Envelopes and verifying signatures,
5
+ * with optional metadata support.
6
+ *
7
+ * The signature extension allows:
8
+ * - Signing envelope subjects to validate their authenticity
9
+ * - Adding metadata to signatures (e.g., signer identity, date, purpose)
10
+ * - Verification of signatures, both with and without metadata
11
+ * - Support for multiple signatures on a single envelope
12
+ */
13
+
14
+ import { Envelope } from "../base/envelope";
15
+ import { EnvelopeError } from "../base/error";
16
+ import {
17
+ ecdsaSign,
18
+ ecdsaVerify,
19
+ ecdsaPublicKeyFromPrivateKey,
20
+ ECDSA_PRIVATE_KEY_SIZE,
21
+ ECDSA_PUBLIC_KEY_SIZE,
22
+ ECDSA_UNCOMPRESSED_PUBLIC_KEY_SIZE,
23
+ } from "@bcts/crypto";
24
+ import { SecureRandomNumberGenerator, rngRandomData } from "@bcts/rand";
25
+
26
+ /**
27
+ * Known value for the 'signed' predicate.
28
+ * This is the standard predicate used for signature assertions.
29
+ */
30
+ export const SIGNED = "signed";
31
+
32
+ /**
33
+ * Known value for the 'verifiedBy' predicate.
34
+ * Used to indicate verification status.
35
+ */
36
+ export const VERIFIED_BY = "verifiedBy";
37
+
38
+ /**
39
+ * Known value for the 'note' predicate.
40
+ * Used for adding notes/comments to signatures.
41
+ */
42
+ export const NOTE = "note";
43
+
44
+ /**
45
+ * Represents a cryptographic signature.
46
+ */
47
+ export class Signature {
48
+ readonly #data: Uint8Array;
49
+
50
+ constructor(data: Uint8Array) {
51
+ this.#data = data;
52
+ }
53
+
54
+ /**
55
+ * Returns the raw signature bytes.
56
+ */
57
+ data(): Uint8Array {
58
+ return this.#data;
59
+ }
60
+
61
+ /**
62
+ * Returns the hex-encoded signature.
63
+ */
64
+ hex(): string {
65
+ return Array.from(this.#data)
66
+ .map((b) => b.toString(16).padStart(2, "0"))
67
+ .join("");
68
+ }
69
+
70
+ /**
71
+ * Creates a Signature from hex string.
72
+ */
73
+ static fromHex(hex: string): Signature {
74
+ const bytes = new Uint8Array(hex.length / 2);
75
+ for (let i = 0; i < hex.length; i += 2) {
76
+ bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
77
+ }
78
+ return new Signature(bytes);
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Interface for types that can sign data.
84
+ */
85
+ export interface Signer {
86
+ /**
87
+ * Signs the provided data and returns a Signature.
88
+ */
89
+ sign(data: Uint8Array): Signature;
90
+ }
91
+
92
+ /**
93
+ * Interface for types that can verify signatures.
94
+ */
95
+ export interface Verifier {
96
+ /**
97
+ * Verifies a signature against the provided data.
98
+ * Returns true if the signature is valid.
99
+ */
100
+ verify(data: Uint8Array, signature: Signature): boolean;
101
+ }
102
+
103
+ /**
104
+ * ECDSA signing key using secp256k1 curve.
105
+ * Uses @bcts/crypto functions.
106
+ */
107
+ export class SigningPrivateKey implements Signer {
108
+ readonly #privateKey: Uint8Array;
109
+
110
+ constructor(privateKey: Uint8Array) {
111
+ if (privateKey.length !== ECDSA_PRIVATE_KEY_SIZE) {
112
+ throw new Error(`Private key must be ${ECDSA_PRIVATE_KEY_SIZE} bytes`);
113
+ }
114
+ this.#privateKey = privateKey;
115
+ }
116
+
117
+ /**
118
+ * Generates a new random private key.
119
+ */
120
+ static generate(): SigningPrivateKey {
121
+ const rng = new SecureRandomNumberGenerator();
122
+ const privateKey = rngRandomData(rng, ECDSA_PRIVATE_KEY_SIZE);
123
+ return new SigningPrivateKey(privateKey);
124
+ }
125
+
126
+ /**
127
+ * Creates a private key from hex string.
128
+ */
129
+ static fromHex(hex: string): SigningPrivateKey {
130
+ const bytes = new Uint8Array(hex.length / 2);
131
+ for (let i = 0; i < hex.length; i += 2) {
132
+ bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
133
+ }
134
+ return new SigningPrivateKey(bytes);
135
+ }
136
+
137
+ /**
138
+ * Returns the corresponding public key.
139
+ */
140
+ publicKey(): SigningPublicKey {
141
+ const publicKey = ecdsaPublicKeyFromPrivateKey(this.#privateKey);
142
+ return new SigningPublicKey(publicKey);
143
+ }
144
+
145
+ /**
146
+ * Signs data and returns a Signature.
147
+ */
148
+ sign(data: Uint8Array): Signature {
149
+ const signatureBytes = ecdsaSign(this.#privateKey, data);
150
+ return new Signature(signatureBytes);
151
+ }
152
+
153
+ /**
154
+ * Returns the raw private key bytes.
155
+ */
156
+ data(): Uint8Array {
157
+ return this.#privateKey;
158
+ }
159
+ }
160
+
161
+ /**
162
+ * ECDSA public key for signature verification using secp256k1 curve.
163
+ * Uses @bcts/crypto functions.
164
+ */
165
+ export class SigningPublicKey implements Verifier {
166
+ readonly #publicKey: Uint8Array;
167
+
168
+ constructor(publicKey: Uint8Array) {
169
+ if (
170
+ publicKey.length !== ECDSA_PUBLIC_KEY_SIZE &&
171
+ publicKey.length !== ECDSA_UNCOMPRESSED_PUBLIC_KEY_SIZE
172
+ ) {
173
+ throw new Error(
174
+ `Public key must be ${ECDSA_PUBLIC_KEY_SIZE} bytes (compressed) or ${ECDSA_UNCOMPRESSED_PUBLIC_KEY_SIZE} bytes (uncompressed)`,
175
+ );
176
+ }
177
+ this.#publicKey = publicKey;
178
+ }
179
+
180
+ /**
181
+ * Creates a public key from hex string.
182
+ */
183
+ static fromHex(hex: string): SigningPublicKey {
184
+ const bytes = new Uint8Array(hex.length / 2);
185
+ for (let i = 0; i < hex.length; i += 2) {
186
+ bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
187
+ }
188
+ return new SigningPublicKey(bytes);
189
+ }
190
+
191
+ /**
192
+ * Verifies a signature against the provided data.
193
+ */
194
+ verify(data: Uint8Array, signature: Signature): boolean {
195
+ try {
196
+ return ecdsaVerify(this.#publicKey, signature.data(), data);
197
+ } catch {
198
+ return false;
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Returns the raw public key bytes.
204
+ */
205
+ data(): Uint8Array {
206
+ return this.#publicKey;
207
+ }
208
+
209
+ /**
210
+ * Returns the hex-encoded public key.
211
+ */
212
+ hex(): string {
213
+ return Array.from(this.#publicKey)
214
+ .map((b) => b.toString(16).padStart(2, "0"))
215
+ .join("");
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Metadata that can be attached to a signature.
221
+ */
222
+ export class SignatureMetadata {
223
+ readonly #assertions: [string, unknown][] = [];
224
+
225
+ /**
226
+ * Adds an assertion to the metadata.
227
+ */
228
+ withAssertion(predicate: string, object: unknown): SignatureMetadata {
229
+ const metadata = new SignatureMetadata();
230
+ metadata.#assertions.push(...this.#assertions);
231
+ metadata.#assertions.push([predicate, object]);
232
+ return metadata;
233
+ }
234
+
235
+ /**
236
+ * Returns all assertions in the metadata.
237
+ */
238
+ assertions(): [string, unknown][] {
239
+ return this.#assertions;
240
+ }
241
+
242
+ /**
243
+ * Returns true if this metadata has any assertions.
244
+ */
245
+ hasAssertions(): boolean {
246
+ return this.#assertions.length > 0;
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Support for signing envelopes and verifying signatures.
252
+ */
253
+ declare module "../base/envelope" {
254
+ interface Envelope {
255
+ /**
256
+ * Creates a signature for the envelope's subject and returns a new
257
+ * envelope with a 'signed': Signature assertion.
258
+ *
259
+ * @param signer - The signing key
260
+ * @returns The signed envelope
261
+ *
262
+ * @example
263
+ * ```typescript
264
+ * const privateKey = SigningPrivateKey.generate();
265
+ * const envelope = Envelope.new("Hello, world!");
266
+ * const signed = envelope.addSignature(privateKey);
267
+ * ```
268
+ */
269
+ addSignature(signer: Signer): Envelope;
270
+
271
+ /**
272
+ * Creates a signature for the envelope's subject with optional metadata.
273
+ *
274
+ * @param signer - The signing key
275
+ * @param metadata - Optional metadata to attach to the signature
276
+ * @returns The signed envelope
277
+ */
278
+ addSignatureWithMetadata(signer: Signer, metadata?: SignatureMetadata): Envelope;
279
+
280
+ /**
281
+ * Creates multiple signatures for the envelope's subject.
282
+ *
283
+ * @param signers - Array of signing keys
284
+ * @returns The envelope with multiple signatures
285
+ */
286
+ addSignatures(signers: Signer[]): Envelope;
287
+
288
+ /**
289
+ * Returns whether this envelope has a valid signature from the given verifier.
290
+ *
291
+ * @param verifier - The public key to verify against
292
+ * @returns True if a valid signature from this verifier exists
293
+ */
294
+ hasSignatureFrom(verifier: Verifier): boolean;
295
+
296
+ /**
297
+ * Verifies that this envelope has a valid signature from the given verifier
298
+ * and returns the envelope.
299
+ *
300
+ * @param verifier - The public key to verify against
301
+ * @returns The verified envelope
302
+ * @throws {EnvelopeError} If no valid signature is found
303
+ */
304
+ verifySignatureFrom(verifier: Verifier): Envelope;
305
+
306
+ /**
307
+ * Returns all signature assertions in this envelope.
308
+ *
309
+ * @returns Array of signature envelopes
310
+ */
311
+ signatures(): Envelope[];
312
+ }
313
+ }
314
+
315
+ // Implementation
316
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
317
+ if (Envelope?.prototype) {
318
+ Envelope.prototype.addSignature = function (this: Envelope, signer: Signer): Envelope {
319
+ return this.addSignatureWithMetadata(signer, undefined);
320
+ };
321
+
322
+ Envelope.prototype.addSignatureWithMetadata = function (
323
+ this: Envelope,
324
+ signer: Signer,
325
+ metadata?: SignatureMetadata,
326
+ ): Envelope {
327
+ const digest = this.subject().digest();
328
+ const signature = signer.sign(digest.data());
329
+ let signatureEnvelope = Envelope.new(signature.data());
330
+
331
+ if (metadata?.hasAssertions() === true) {
332
+ // Add metadata assertions to the signature
333
+ for (const [predicate, object] of metadata.assertions()) {
334
+ signatureEnvelope = signatureEnvelope.addAssertion(
335
+ predicate,
336
+ object as string | number | boolean,
337
+ );
338
+ }
339
+
340
+ // Wrap the signature with metadata
341
+ signatureEnvelope = signatureEnvelope.wrap();
342
+
343
+ // Sign the wrapped envelope
344
+ const outerSignature = signer.sign(signatureEnvelope.digest().data());
345
+ signatureEnvelope = signatureEnvelope.addAssertion(SIGNED, outerSignature.data());
346
+ }
347
+
348
+ return this.addAssertion(SIGNED, signatureEnvelope);
349
+ };
350
+
351
+ Envelope.prototype.addSignatures = function (this: Envelope, signers: Signer[]): Envelope {
352
+ return signers.reduce((envelope, signer) => envelope.addSignature(signer), this);
353
+ };
354
+
355
+ Envelope.prototype.hasSignatureFrom = function (this: Envelope, verifier: Verifier): boolean {
356
+ const subjectDigest = this.subject().digest();
357
+ const signatures = this.signatures();
358
+
359
+ for (const sigEnvelope of signatures) {
360
+ const c = sigEnvelope.case();
361
+
362
+ if (c.type === "leaf") {
363
+ // Simple signature - verify directly
364
+ try {
365
+ const sigData = sigEnvelope.asByteString();
366
+ if (sigData !== undefined) {
367
+ const signature = new Signature(sigData);
368
+ if (verifier.verify(subjectDigest.data(), signature)) {
369
+ return true;
370
+ }
371
+ }
372
+ } catch {
373
+ continue;
374
+ }
375
+ } else if (c.type === "node") {
376
+ // Signature with metadata - it's a node with 'signed' assertion
377
+ // The structure is: { wrapped_signature [signed: outer_signature] }
378
+ // Check if this node has a 'signed' assertion
379
+ const outerSigs = sigEnvelope.assertions().filter((a) => {
380
+ const aC = a.case();
381
+ if (aC.type === "assertion") {
382
+ const pred = aC.assertion.predicate();
383
+ try {
384
+ return pred.asText() === SIGNED;
385
+ } catch {
386
+ return false;
387
+ }
388
+ }
389
+ return false;
390
+ });
391
+
392
+ for (const outerSig of outerSigs) {
393
+ const outerSigCase = outerSig.case();
394
+ if (outerSigCase.type === "assertion") {
395
+ const outerSigObj = outerSigCase.assertion.object();
396
+ try {
397
+ const outerSigData = outerSigObj.asByteString();
398
+ if (outerSigData !== undefined) {
399
+ const outerSignature = new Signature(outerSigData);
400
+
401
+ // The subject of this node should be a wrapped envelope
402
+ const nodeSubject = c.subject;
403
+ const nodeSubjectCase = nodeSubject.case();
404
+
405
+ // Verify outer signature against the wrapped envelope
406
+ if (
407
+ nodeSubjectCase.type === "wrapped" &&
408
+ verifier.verify(nodeSubject.digest().data(), outerSignature)
409
+ ) {
410
+ // Now verify inner signature
411
+ const wrapped = nodeSubjectCase.envelope;
412
+ const innerSig = wrapped.subject();
413
+ const innerSigData = innerSig.asByteString();
414
+ if (innerSigData !== undefined) {
415
+ const innerSignature = new Signature(innerSigData);
416
+ if (verifier.verify(subjectDigest.data(), innerSignature)) {
417
+ return true;
418
+ }
419
+ }
420
+ }
421
+ }
422
+ } catch {
423
+ continue;
424
+ }
425
+ }
426
+ }
427
+ }
428
+ }
429
+
430
+ return false;
431
+ };
432
+
433
+ Envelope.prototype.verifySignatureFrom = function (this: Envelope, verifier: Verifier): Envelope {
434
+ if (this.hasSignatureFrom(verifier)) {
435
+ return this;
436
+ }
437
+ throw EnvelopeError.general("No valid signature found from the given verifier");
438
+ };
439
+
440
+ Envelope.prototype.signatures = function (this: Envelope): Envelope[] {
441
+ const assertions = this.assertions();
442
+ return assertions
443
+ .filter((a) => {
444
+ const c = a.case();
445
+ if (c.type === "assertion") {
446
+ const pred = c.assertion.predicate();
447
+ try {
448
+ return pred.asText() === SIGNED;
449
+ } catch {
450
+ return false;
451
+ }
452
+ }
453
+ return false;
454
+ })
455
+ .map((a) => {
456
+ const c = a.case();
457
+ if (c.type === "assertion") {
458
+ return c.assertion.object();
459
+ }
460
+ throw EnvelopeError.general("Invalid signature assertion");
461
+ });
462
+ };
463
+ }