@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.
- package/LICENSE +48 -0
- package/README.md +23 -0
- package/dist/index.cjs +2646 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +978 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +978 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.iife.js +2644 -0
- package/dist/index.iife.js.map +1 -0
- package/dist/index.mjs +2552 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +85 -0
- package/src/base/assertion.ts +179 -0
- package/src/base/assertions.ts +304 -0
- package/src/base/cbor.ts +122 -0
- package/src/base/digest.ts +204 -0
- package/src/base/elide.ts +526 -0
- package/src/base/envelope-decodable.ts +229 -0
- package/src/base/envelope-encodable.ts +71 -0
- package/src/base/envelope.ts +790 -0
- package/src/base/error.ts +421 -0
- package/src/base/index.ts +56 -0
- package/src/base/leaf.ts +226 -0
- package/src/base/queries.ts +374 -0
- package/src/base/walk.ts +241 -0
- package/src/base/wrap.ts +72 -0
- package/src/extension/attachment.ts +369 -0
- package/src/extension/compress.ts +293 -0
- package/src/extension/encrypt.ts +379 -0
- package/src/extension/expression.ts +404 -0
- package/src/extension/index.ts +72 -0
- package/src/extension/proof.ts +276 -0
- package/src/extension/recipient.ts +557 -0
- package/src/extension/salt.ts +223 -0
- package/src/extension/signature.ts +463 -0
- package/src/extension/types.ts +222 -0
- package/src/format/diagnostic.ts +116 -0
- package/src/format/hex.ts +25 -0
- package/src/format/index.ts +13 -0
- package/src/format/tree.ts +168 -0
- package/src/index.ts +32 -0
- package/src/utils/index.ts +8 -0
- 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
|
+
}
|