Skip to content

Commit c2c97ea

Browse files
authored
feat: Implement key destruction (#28)
Fixes #9.
1 parent bfb2564 commit c2c97ea

File tree

5 files changed

+167
-20
lines changed

5 files changed

+167
-20
lines changed

src/lib/KmsRsaPssProvider.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { RsaPssProvider } from 'webcrypto-core';
22

33
export abstract class KmsRsaPssProvider extends RsaPssProvider {
4+
public abstract destroyKey(key: CryptoKey): Promise<void>;
5+
46
public abstract close(): Promise<void>;
57
}

src/lib/aws/AwsKmsRsaPssProvider.spec.ts

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import {
55
GetPublicKeyCommandOutput,
66
KeyUsageType,
77
KMSClient,
8+
ScheduleKeyDeletionCommand,
9+
ScheduleKeyDeletionCommandOutput,
810
SignCommand,
911
SignCommandOutput,
1012
} from '@aws-sdk/client-kms';
@@ -302,13 +304,13 @@ describe('AwsKmsRsaPssProvider', () => {
302304
});
303305
});
304306

305-
test('Non-KMS key should be refused', async () => {
307+
test('Non-AWS key should be refused', async () => {
306308
const provider = new AwsKmsRsaPssProvider(makeAwsClient());
307309
const invalidKey = new CryptoKey();
308310

309311
await expect(provider.onExportKey('spki', invalidKey)).rejects.toThrowWithMessage(
310312
KmsError,
311-
'Key is not managed by AWS KMS',
313+
`Only AWS KMS keys are supported (got ${invalidKey.constructor.name})`,
312314
);
313315
});
314316

@@ -423,7 +425,7 @@ describe('AwsKmsRsaPssProvider', () => {
423425

424426
await expect(provider.onSign(ALGORITHM, invalidKey, PLAINTEXT)).rejects.toThrowWithMessage(
425427
KmsError,
426-
'Key is not managed by AWS KMS',
428+
`Only AWS KMS keys are supported (got ${invalidKey.constructor.name})`,
427429
);
428430
});
429431

@@ -461,6 +463,59 @@ describe('AwsKmsRsaPssProvider', () => {
461463
});
462464
});
463465

466+
describe('destroyKey', () => {
467+
test('Non-AWS KMS key should be refused', async () => {
468+
const client = makeAwsClient();
469+
const provider = new AwsKmsRsaPssProvider(client);
470+
const invalidKey = new CryptoKey();
471+
472+
await expect(provider.destroyKey(invalidKey)).rejects.toThrowWithMessage(
473+
KmsError,
474+
`Only AWS KMS keys are supported (got ${invalidKey.constructor.name})`,
475+
);
476+
expect(client.send).not.toHaveBeenCalled();
477+
});
478+
479+
test('Specified key should be destroyed', async () => {
480+
const client = makeAwsClient();
481+
const provider = new AwsKmsRsaPssProvider(client);
482+
483+
await provider.destroyKey(PRIVATE_KEY);
484+
485+
expect(client.send).toHaveBeenCalledWith(
486+
expect.any(ScheduleKeyDeletionCommand),
487+
expect.anything(),
488+
);
489+
expect(client.send).toHaveBeenCalledWith(
490+
expect.objectContaining({
491+
input: expect.objectContaining({ KeyId: PRIVATE_KEY.arn }),
492+
}),
493+
expect.anything(),
494+
);
495+
});
496+
497+
test('Call should time out after 3 seconds', async () => {
498+
const client = makeAwsClient();
499+
const provider = new AwsKmsRsaPssProvider(client);
500+
501+
await provider.destroyKey(PRIVATE_KEY);
502+
503+
expect(client.send).toHaveBeenCalledWith(
504+
expect.anything(),
505+
expect.objectContaining({ requestTimeout: 3_000 }),
506+
);
507+
});
508+
509+
function makeAwsClient(): KMSClient {
510+
const client = new KMSClient({});
511+
const response: ScheduleKeyDeletionCommandOutput = {
512+
$metadata: {},
513+
};
514+
jest.spyOn<KMSClient, any>(client, 'send').mockResolvedValue(response);
515+
return client;
516+
}
517+
});
518+
464519
describe('close', () => {
465520
test('Client should be destroyed', async () => {
466521
const client = new KMSClient({});

src/lib/aws/AwsKmsRsaPssProvider.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
GetPublicKeyCommand,
44
KeyUsageType,
55
KMSClient,
6+
ScheduleKeyDeletionCommand,
67
SignCommand,
78
} from '@aws-sdk/client-kms';
89
import { CryptoKey } from 'webcrypto-core';
@@ -57,9 +58,7 @@ export class AwsKmsRsaPssProvider extends KmsRsaPssProvider {
5758
}
5859

5960
async onExportKey(format: KeyFormat, key: CryptoKey): Promise<ArrayBuffer | JsonWebKey> {
60-
if (!(key instanceof AwsKmsRsaPssPrivateKey)) {
61-
throw new KmsError('Key is not managed by AWS KMS');
62-
}
61+
requireAwsKmsKey(key);
6362

6463
let keySerialised: ArrayBuffer;
6564
if (format === 'raw') {
@@ -92,9 +91,7 @@ export class AwsKmsRsaPssProvider extends KmsRsaPssProvider {
9291
}
9392

9493
async onSign(_algorithm: RsaPssParams, key: CryptoKey, data: ArrayBuffer): Promise<ArrayBuffer> {
95-
if (!(key instanceof AwsKmsRsaPssPrivateKey)) {
96-
throw new KmsError('Key is not managed by AWS KMS');
97-
}
94+
requireAwsKmsKey(key);
9895

9996
const hashingAlgorithm = (key.algorithm as RsaHashedKeyAlgorithm).hash.name;
10097
const digest = await hash(data, hashingAlgorithm as HashingAlgorithm);
@@ -114,6 +111,12 @@ export class AwsKmsRsaPssProvider extends KmsRsaPssProvider {
114111
throw new KmsError('Signature verification is unsupported');
115112
}
116113

114+
async destroyKey(key: CryptoKey): Promise<void> {
115+
requireAwsKmsKey(key);
116+
const command = new ScheduleKeyDeletionCommand({ KeyId: key.arn });
117+
await this.client.send(command, REQUEST_OPTIONS);
118+
}
119+
117120
async close(): Promise<void> {
118121
this.client.destroy();
119122
}
@@ -124,3 +127,9 @@ export class AwsKmsRsaPssProvider extends KmsRsaPssProvider {
124127
return bufferToArrayBuffer(response.PublicKey!);
125128
}
126129
}
130+
131+
function requireAwsKmsKey(key: CryptoKey): asserts key is AwsKmsRsaPssPrivateKey {
132+
if (!(key instanceof AwsKmsRsaPssPrivateKey)) {
133+
throw new KmsError(`Only AWS KMS keys are supported (got ${key.constructor.name})`);
134+
}
135+
}

src/lib/gcp/GcpKmsRsaPssProvider.spec.ts

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,7 @@ describe('onExportKey', () => {
509509
}
510510

511511
class MockGCPError extends Error {
512+
// noinspection JSMismatchedCollectionQueryUpdate
512513
public readonly statusDetails: readonly any[];
513514

514515
constructor(message: string, violationType: string) {
@@ -519,13 +520,13 @@ describe('onExportKey', () => {
519520
}
520521
});
521522

522-
test('Non-KMS key should be refused', async () => {
523+
test('Non-GCP key should be refused', async () => {
523524
const provider = new GcpKmsRsaPssProvider(null as any, KMS_CONFIG);
524525
const invalidKey = new CryptoKey();
525526

526527
await expect(provider.onExportKey('spki', invalidKey)).rejects.toThrowWithMessage(
527528
KmsError,
528-
'Key is not managed by GCP KMS',
529+
`Only GCP KMS keys are supported (got ${invalidKey.constructor.name})`,
529530
);
530531
});
531532
});
@@ -579,14 +580,14 @@ describe('onImportKey', () => {
579580
describe('onSign', () => {
580581
const ALGORITHM = RSA_PSS_SIGN_ALGORITHM;
581582

582-
test('Non-KMS key should be refused', async () => {
583+
test('Non-GCP key should be refused', async () => {
583584
const kmsClient = makeKmsClient();
584585
const provider = new GcpKmsRsaPssProvider(kmsClient, KMS_CONFIG);
585586
const invalidKey = CryptoKey.create({ name: 'RSA-PSS' }, 'private', true, ['sign']);
586587

587588
await expect(provider.sign(ALGORITHM, invalidKey, PLAINTEXT)).rejects.toThrowWithMessage(
588589
KmsError,
589-
`Cannot sign with key of unsupported type (${invalidKey.constructor.name})`,
590+
`Only GCP KMS keys are supported (got ${invalidKey.constructor.name})`,
590591
);
591592

592593
expect(kmsClient.asymmetricSign).not.toHaveBeenCalled();
@@ -771,6 +772,75 @@ describe('onVerify', () => {
771772
});
772773
});
773774

775+
describe('destroyKey', () => {
776+
test('Non-GCP KMS key should be refused', async () => {
777+
const invalidKey = CryptoKey.create({ name: 'RSA-PSS' }, 'private', true, ['sign']);
778+
779+
const provider = new GcpKmsRsaPssProvider(makeKmsClient(), KMS_CONFIG);
780+
781+
await expect(provider.destroyKey(invalidKey)).rejects.toThrowWithMessage(
782+
KmsError,
783+
`Only GCP KMS keys are supported (got ${invalidKey.constructor.name})`,
784+
);
785+
});
786+
787+
test('Specified key should be destroyed', async () => {
788+
const kmsClient = makeKmsClient();
789+
const provider = new GcpKmsRsaPssProvider(kmsClient, KMS_CONFIG);
790+
791+
await provider.destroyKey(PRIVATE_KEY);
792+
793+
expect(kmsClient.destroyCryptoKeyVersion).toHaveBeenCalledWith(
794+
expect.objectContaining({ name: PRIVATE_KEY.kmsKeyVersionPath }),
795+
expect.anything(),
796+
);
797+
});
798+
799+
test('Request should time out after 3 seconds', async () => {
800+
const kmsClient = makeKmsClient();
801+
const provider = new GcpKmsRsaPssProvider(kmsClient, KMS_CONFIG);
802+
803+
await provider.destroyKey(PRIVATE_KEY);
804+
805+
expect(kmsClient.destroyCryptoKeyVersion).toHaveBeenCalledWith(
806+
expect.anything(),
807+
expect.objectContaining({ timeout: 3_000 }),
808+
);
809+
});
810+
811+
test('Request should be retried', async () => {
812+
const kmsClient = makeKmsClient();
813+
const provider = new GcpKmsRsaPssProvider(kmsClient, KMS_CONFIG);
814+
815+
await provider.destroyKey(PRIVATE_KEY);
816+
817+
expect(kmsClient.destroyCryptoKeyVersion).toHaveBeenCalledWith(
818+
expect.anything(),
819+
expect.objectContaining({ maxRetries: 10 }),
820+
);
821+
});
822+
823+
test('API call errors should be wrapped', async () => {
824+
const callError = new Error('Bruno. There. I said it.');
825+
const client = makeKmsClient();
826+
getMockInstance(client.destroyCryptoKeyVersion).mockRejectedValue(callError);
827+
const provider = new GcpKmsRsaPssProvider(client, KMS_CONFIG);
828+
829+
const error = await catchPromiseRejection(provider.destroyKey(PRIVATE_KEY), KmsError);
830+
831+
expect(error.message).toBe('Key destruction failed');
832+
expect(error.cause).toBe(callError);
833+
});
834+
835+
function makeKmsClient(): KeyManagementServiceClient {
836+
const kmsClient = new KeyManagementServiceClient();
837+
jest
838+
.spyOn(kmsClient, 'destroyCryptoKeyVersion')
839+
.mockImplementation(async () => [undefined, undefined, undefined]);
840+
return kmsClient;
841+
}
842+
});
843+
774844
describe('close', () => {
775845
test('Client should be closed', async () => {
776846
const client = new KeyManagementServiceClient();

src/lib/gcp/GcpKmsRsaPssProvider.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,7 @@ export class GcpKmsRsaPssProvider extends KmsRsaPssProvider {
8484
}
8585

8686
public async onExportKey(format: KeyFormat, key: CryptoKey): Promise<ArrayBuffer> {
87-
if (!(key instanceof GcpKmsRsaPssPrivateKey)) {
88-
throw new KmsError('Key is not managed by GCP KMS');
89-
}
87+
requireGcpKmsKey(key);
9088

9189
let keySerialised: ArrayBuffer;
9290
if (format === 'spki') {
@@ -105,9 +103,7 @@ export class GcpKmsRsaPssProvider extends KmsRsaPssProvider {
105103
key: CryptoKey,
106104
data: ArrayBuffer,
107105
): Promise<ArrayBuffer> {
108-
if (!(key instanceof GcpKmsRsaPssPrivateKey)) {
109-
throw new KmsError(`Cannot sign with key of unsupported type (${key.constructor.name})`);
110-
}
106+
requireGcpKmsKey(key);
111107

112108
if (!SUPPORTED_SALT_LENGTHS.includes(algorithm.saltLength)) {
113109
throw new KmsError(`Unsupported salt length of ${algorithm.saltLength} octets`);
@@ -120,7 +116,16 @@ export class GcpKmsRsaPssProvider extends KmsRsaPssProvider {
120116
throw new KmsError('Signature verification is unsupported');
121117
}
122118

123-
async close(): Promise<void> {
119+
public async destroyKey(key: CryptoKey): Promise<void> {
120+
requireGcpKmsKey(key);
121+
122+
await wrapGCPCallError(
123+
this.client.destroyCryptoKeyVersion({ name: key.kmsKeyVersionPath }, REQUEST_OPTIONS),
124+
'Key destruction failed',
125+
);
126+
}
127+
128+
public async close(): Promise<void> {
124129
await this.client.close();
125130
}
126131

@@ -195,6 +200,12 @@ export class GcpKmsRsaPssProvider extends KmsRsaPssProvider {
195200
}
196201
}
197202

203+
function requireGcpKmsKey(key: CryptoKey): asserts key is GcpKmsRsaPssPrivateKey {
204+
if (!(key instanceof GcpKmsRsaPssPrivateKey)) {
205+
throw new KmsError(`Only GCP KMS keys are supported (got ${key.constructor.name})`);
206+
}
207+
}
208+
198209
function getKmsAlgorithm(algorithm: RsaHashedKeyGenParams): string {
199210
const hash = (algorithm.hash as KeyAlgorithm).name === 'SHA-256' ? 'SHA256' : 'SHA512';
200211
return `RSA_SIGN_PSS_${algorithm.modulusLength}_${hash}`;

0 commit comments

Comments
 (0)