generated from TBD54566975/tbd-project-template
-
Notifications
You must be signed in to change notification settings - Fork 57
/
key-manager.ts
458 lines (415 loc) · 17.5 KB
/
key-manager.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
import type {
Jwk,
CryptoApi,
KeyIdentifier,
KmsSignParams,
KmsDigestParams,
KmsVerifyParams,
KmsGetKeyUriParams,
KmsGenerateKeyParams,
KmsGetPublicKeyParams,
} from '@web5/crypto';
import { computeJwkThumbprint, KEY_URI_PREFIX_JWK, Sha2Algorithm } from '@web5/crypto';
import {
KeySpec,
KMSClient,
GetPublicKeyCommand,
} from '@aws-sdk/client-kms';
import { EcdsaAlgorithm } from './ecdsa.js';
import { convertSpkiToPublicKey, getKeySpec } from './utils.js';
/**
* `supportedAlgorithms` is an object mapping algorithm names to their respective implementations
* and specifications. Each key in the object is a string representing the algorithm name, and the
* value is an object that provides the implementation class, the key specification (if applicable),
* and an array of names associated with the algorithm. This structure allows for easy retrieval
* and instantiation of algorithm implementations based on the algorithm name or key specification.
* It facilitates the support of multiple algorithms within the `AwsKeyManager` class.
*/
const supportedAlgorithms = {
'ES256K': {
implementation : EcdsaAlgorithm,
keySpec : KeySpec.ECC_SECG_P256K1,
names : ['ES256K', 'secp256k1']
},
'SHA-256': {
implementation : Sha2Algorithm,
keySpec : undefined,
names : ['SHA-256']
}
} satisfies {
[key: string]: {
implementation : InstanceType<any>;
keySpec : KeySpec | undefined;
names : string[];
}
};
/** Helper type for `supportedAlgorithms`. */
type SupportedAlgorithm = keyof typeof supportedAlgorithms;
/** Helper type for `supportedAlgorithms` implementations. */
type AlgorithmConstructor = typeof supportedAlgorithms[SupportedAlgorithm]['implementation'];
/**
* The `AwsKeyManagerParams` interface specifies the parameters for initializing an instance of
* `AwsKeyManager`, which is an implementation of the `CryptoApi` interface tailored for AWS KMS.
*
* This interface allows the optional inclusion of a `KMSClient` instance, which is used for
* interacting with AWS KMS. If not provided, a default `KMSClient` instance will be created and
* used.
*/
export type AwsKeyManagerParams = {
/**
* An optional property to specify a custom `KMSClient` instance. If not provided, the
* `AwsKeyManager` class will instantiate a default `KMSClient`. This client is used for all
* interactions with AWS Key Management Service (KMS), such as generating keys and signing data.
*
* @param kmsClient - A `KMSClient` instance from the AWS SDK.
*/
kmsClient?: KMSClient;
};
/**
* The `AwsKeyManagerDigestParams` interface defines the algorithm-specific parameters that should
* be passed into the {@link AwsKeyManager.digest | `AwsKeyManager.digest()`} method.
*/
export interface AwsKeyManagerDigestParams extends KmsDigestParams {
/**
* A string defining the name of hash function to use. The value must be one of the following:
* - `"SHA-256"`: Generates a 256-bit digest.
*/
algorithm: 'SHA-256';
}
/**
* The `AwsKeyManagerGenerateKeyParams` interface defines the algorithm-specific parameters that
* should be passed into the {@link AwsKeyManager.generateKey | `AwsKeyManager.generateKey()`}
* method when generating a key in AWS KMS.
*/
export interface AwsKeyManagerGenerateKeyParams extends KmsGenerateKeyParams {
/**
* A string defining the type of key to generate. The value must be one of the following:
* - `"ES256K"`: ECDSA using the secp256k1 curve and SHA-256.
*/
algorithm: 'ES256K';
}
export class AwsKeyManager implements CryptoApi<AwsKeyManagerGenerateKeyParams> {
/**
* A private map that stores instances of cryptographic algorithm implementations. Each key in
* this map is an `AlgorithmConstructor`, and its corresponding value is an instance of a class
* that implements a specific cryptographic algorithm. This map is used to cache and reuse
* instances for performance optimization, ensuring that each algorithm is instantiated only once.
*/
private _algorithmInstances: Map<AlgorithmConstructor, any> = new Map();
/**
* A private instance of `KMSClient` from the AWS SDK. This client is used for all interactions
* with AWS Key Management Service (KMS), such as generating keys, signing data, and retrieving
* public keys. If a custom `KMSClient` is not provided in the constructor, a default instance is
* created and used.
*/
private _kmsClient: KMSClient;
constructor(params?: AwsKeyManagerParams) {
this._kmsClient = params?.kmsClient ?? new KMSClient();
}
/**
* Generates a hash digest of the provided data.
*
* @remarks
* A digest is the output of the hash function. It's a fixed-size string of bytes
* that uniquely represents the data input into the hash function. The digest is often used for
* data integrity checks, as any alteration in the input data results in a significantly
* different digest.
*
* It takes the algorithm identifier of the hash function and data to digest as input and returns
* the digest of the data.
*
* @example
* ```ts
* const keyManager = new AwsKeyManager();
* const data = new Uint8Array([...]);
* const digest = await keyManager.digest({ algorithm: 'SHA-256', data });
* ```
*
* @param params - The parameters for the digest operation.
* @param params.algorithm - The name of hash function to use.
* @param params.data - The data to digest.
*
* @returns A Promise which will be fulfilled with the hash digest.
*/
public async digest({ algorithm, data }:
AwsKeyManagerDigestParams
): Promise<Uint8Array> {
// Get the hash function implementation based on the specified `algorithm` parameter.
const hasher = this.getAlgorithm({ algorithm });
// Compute the hash.
const hash = await hasher.digest({ algorithm, data });
return hash;
}
/**
* Generates a new cryptographic key in AWS KMS with the specified algorithm and returns a unique
* key URI which can be used to reference the key in subsequent operations.
*
* @remarks
* This method initiates the creation of a customer-managed key in AWS KMS, using the specified
* algorithm parameters. The generated key is an AWS KMS key, identified by an AWS-assigned
* {@link https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#key-id-key-id | key ID}
* (UUID V4 format) and a
* {@link https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#key-id-key-ARN | key ARN}
* (Amazon Resource Name). The method returns a key URI that uniquely
* identifies the key and can be used in subsequent cryptographic operations.
*
* @example
* ```ts
* const keyManager = new AwsKeyManager();
* const keyUri = await keyManager.generateKey({ algorithm: 'ES256K' });
* console.log(keyUri); // Outputs the key URI
* ```
*
* @param params - The parameters for key generation.
* @param params.algorithm - The algorithm to use for key generation, defined in `SupportedAlgorithm`.
*
* @returns A Promise that resolves to the key URI, a unique identifier for the generated key.
*/
public async generateKey({ algorithm }:
AwsKeyManagerGenerateKeyParams
): Promise<KeyIdentifier> {
// Get the key generator based on the specified `algorithm` parameter.
const keyGenerator = this.getAlgorithm({ algorithm });
// Generate a new customer managed key with AWS KMS and get the key URI.
const keyUri = await keyGenerator.generateKey({ algorithm });
return keyUri;
}
/**
* Computes the Key URI for a given public JWK (JSON Web Key).
*
* @remarks
* This method generates a {@link https://datatracker.ietf.org/doc/html/rfc3986 | URI}
* (Uniform Resource Identifier) for the given JWK, which uniquely identifies the key across all
* `CryptoApi` implementations. The key URI is constructed by appending the
* {@link https://datatracker.ietf.org/doc/html/rfc7638 | JWK thumbprint} to the prefix
* `urn:jwk:`. The JWK thumbprint is deterministically computed from the JWK and is consistent
* regardless of property order or optional property inclusion in the JWK. This ensures that the
* same key material represented as a JWK will always yield the same thumbprint, and therefore,
* the same key URI.
*
* @example
* ```ts
* const keyManager = new AwsKeyManager();
* const publicKey = { ... }; // Public key in JWK format
* const keyUri = await keyManager.getKeyUri({ key: publicKey });
* ```
*
* @param params - The parameters for getting the key URI.
* @param params.key - The JWK for which to compute the key URI.
*
* @returns A Promise that resolves to the key URI as a string.
*/
public async getKeyUri({ key }:
KmsGetKeyUriParams
): Promise<KeyIdentifier> {
// Compute the JWK thumbprint.
const jwkThumbprint = await computeJwkThumbprint({ jwk: key });
// Construct the key URI by appending the JWK thumbprint to the key URI prefix.
const keyUri = `${KEY_URI_PREFIX_JWK}${jwkThumbprint}`;
return keyUri;
}
/**
* Retrieves the public key associated with a previously generated private key, identified by
* the provided key URI.
*
* @example
* ```ts
* const keyManager = new AwsKeyManager();
* const keyUri = await keyManager.generateKey({ algorithm: 'ES256K' });
* const publicKey = await keyManager.getPublicKey({ keyUri });
* ```
*
* @param params - The parameters for retrieving the public key.
* @param params.keyUri - The key URI of the private key to retrieve the public key for.
*
* @returns A Promise that resolves to the public key in JWK format.
*/
public async getPublicKey({ keyUri }:
KmsGetPublicKeyParams
): Promise<Jwk> {
/** If the key URI is a JWK URI, prepend the AWS-required "alias/" prefix and replace the URN
* namespace separator with hyphens to accomodate AWS KMS key alias character restrictions. */
const awsKeyId = keyUri.replace('urn:jwk:', 'alias/urn-jwk-');
// Send the request to retrieve the public key to AWS KMS.
const response = await this._kmsClient.send(
new GetPublicKeyCommand({
KeyId: awsKeyId
})
);
if (!response.PublicKey) {
throw new Error('Error occurred during public key retrieval: Public key was not returned');
}
// Convert the public key from SPKI (DER-encoded X.509) to JWK format.
const publicKey = convertSpkiToPublicKey({ spki: response.PublicKey });
// Set the algorithm property based on the key specification.
publicKey.alg = this.getAlgorithmName({ keySpec: response.KeySpec });
// Compute the JWK thumbprint and set as the key ID.
publicKey.kid = await computeJwkThumbprint({ jwk: publicKey });
return publicKey;
}
/**
* Signs the provided data using the private key identified by the provided key URI.
*
* @remarks
* This method uses the signature algorithm determined by the AWS KMS `KeySpec` of the private key
* identified by the provided key URI to sign the provided data. The signature can later be
* verified by parties with access to the corresponding public key, ensuring that the data has not
* been tampered with and was indeed signed by the holder of the private key.
*
* @example
* ```ts
* const keyManager = new AwsKeyManager();
* const data = new TextEncoder().encode('Message to sign');
* const signature = await keyManager.sign({
* keyUri: 'urn:jwk:...',
* data
* });
* ```
*
* @param params - The parameters for the signing operation.
* @param params.keyUri - The key URI of the private key to use for signing.
* @param params.data - The data to sign.
*
* @returns A Promise resolving to the digital signature as a `Uint8Array`.
*/
public async sign({ keyUri, data }:
KmsSignParams
): Promise<Uint8Array> {
// If the keyUri is a JWK URI, prepend the AWS-required "alias/" prefix and replace the URN
// namespace separator with hyphens to accomodate AWS KMS key alias character restrictions.
keyUri = keyUri.replace('urn:jwk:', 'alias/urn-jwk-');
// Retrieve the key specification for the key from AWS KMS.
const keySpec = await getKeySpec({ keyUri, kmsClient: this._kmsClient });
// Get the algorithm name based on the key specification.
const algorithm = this.getAlgorithmName({ keySpec });
// Get the signature algorithm based on the algorithm name.
const signer = this.getAlgorithm({ algorithm });
// Sign the data.
const signature = await signer.sign({ algorithm, keyUri, data });
return signature;
}
/**
* Verifies a digital signature associated the provided data using the provided key.
*
* @remarks
* This method uses the signature algorithm determined by the `alg` and/or `crv` properties of the
* provided key to check the validity of a digital signature against the original data. It
* confirms whether the signature was created by the holder of the corresponding private key and
* that the data has not been tampered with.
*
* @example
* ```ts
* const keyManager = new AwsKeyManager();
* const publicKey = { ... }; // Public key in JWK format corresponding to the private key that signed the data
* const data = new TextEncoder().encode('Message to sign'); // Data that was signed
* const signature = new Uint8Array([...]); // Signature to verify
* const isValid = await ecdsa.verify({
* key: publicKey,
* signature,
* data
* });
* ```
*
* @param params - The parameters for the verification operation.
* @param params.key - The key to use for verification.
* @param params.signature - The signature to verify.
* @param params.data - The data to verify.
*
* @returns A Promise resolving to a boolean indicating whether the signature is valid.
*/
public async verify({ key, signature, data }:
KmsVerifyParams
): Promise<boolean> {
// Get the algorithm name based on the JWK's properties.
const algorithm = this.getAlgorithmName({ key });
// Get the signature algorithm based on the algorithm name.
const signer = this.getAlgorithm({ algorithm });
// Verify the signature.
const isSignatureValid = signer.verify({ key, signature, data });
return isSignatureValid;
}
/**
* Retrieves an algorithm implementation instance based on the provided algorithm name.
*
* @remarks
* This method checks if the requested algorithm is supported and returns a cached instance
* if available. If an instance does not exist, it creates and caches a new one. This approach
* optimizes performance by reusing algorithm instances across cryptographic operations.
*
* @example
* ```ts
* const signer = this.getAlgorithm({ algorithm: 'ES256K' });
* ```
*
* @param params - The parameters for retrieving the algorithm implementation.
* @param params.algorithm - The name of the algorithm to retrieve.
*
* @returns An instance of the requested algorithm implementation.
*
* @throws Error if the requested algorithm is not supported.
*/
private getAlgorithm({ algorithm }: {
algorithm: SupportedAlgorithm;
}) {
// Check if algorithm is supported.
const AlgorithmImplementation = supportedAlgorithms[algorithm]?.['implementation'];
if (!AlgorithmImplementation) {
throw new Error(`Algorithm not supported: ${algorithm}`);
}
// Check if instance already exists for the `AlgorithmImplementation`.
if (!this._algorithmInstances.has(AlgorithmImplementation)) {
// If not, create a new instance and store it in the cache
this._algorithmInstances.set(AlgorithmImplementation, new AlgorithmImplementation({
keyManager : this,
kmsClient : this._kmsClient
}));
}
// Return the cached instance
return this._algorithmInstances.get(AlgorithmImplementation);
}
/**
* Determines the name of the algorithm based on the key's properties or key specification.
*
* @remarks
* This method facilitates the identification of the correct algorithm for cryptographic
* operations based on the `alg` or `crv` properties of a {@link Jwk | JWK} or a given AWS
* key specification.
*
* @example
* ```ts
* // Using a JWK.
* const publicKey = { ... }; // Public key in JWK format
* const algorithm = this.getAlgorithmName({ key: publicKey });
*
* // Using a key specification.
* const keySpec = KeySpec.ECC_SECG_P256K1;
* const algorithm = this.getAlgorithmName({ keySpec });
* ```
*
* @param params - The parameters for determining the algorithm name.
* @param params.keySpec - The AWS key specification.
* @param params.key - A JWK containing the `alg` or `crv` properties.
*
* @returns The name of the algorithm associated with the key.
*
* @throws Error if the algorithm cannot be determined from the provided input.
*/
private getAlgorithmName({ key, keySpec }: {
key?: { alg?: string, crv?: string };
keySpec?: KeySpec;
}): SupportedAlgorithm {
const algProperty = key?.alg;
const crvProperty = key?.crv;
for (const algName in supportedAlgorithms) {
const algorithmInfo = supportedAlgorithms[algName as SupportedAlgorithm];
if (keySpec && algorithmInfo.keySpec === keySpec) {
return algName as SupportedAlgorithm;
} else if (algProperty && algorithmInfo.names.includes(algProperty)) {
return algName as SupportedAlgorithm;
} else if (crvProperty && algorithmInfo.names.includes(crvProperty)) {
return algName as SupportedAlgorithm;
}
}
throw new Error(`Unable to determine algorithm based on provided input: keySpec=${keySpec}, alg=${algProperty}, crv=${crvProperty}`);
}
}