Skip to content

Commit

Permalink
fix(PrivateKey): Support public key export from KMS-backed providers (#…
Browse files Browse the repository at this point in the history
…485)

Fixes #484.
  • Loading branch information
gnarea authored Jul 18, 2022
1 parent b0eb394 commit 36458ce
Show file tree
Hide file tree
Showing 10 changed files with 182 additions and 74 deletions.
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export {
getPrivateAddressFromIdentityKey,
RSAKeyGenOptions,
} from './lib/crypto_wrappers/keys';
export { PrivateKey } from './lib/crypto_wrappers/PrivateKey';
export { PrivateKey, RsaPssPrivateKey } from './lib/crypto_wrappers/PrivateKey';
export { ECDHCurveName } from './lib/crypto_wrappers/algorithms';
export { IdentityKeyPair } from './lib/IdentityKeyPair';

Expand Down
50 changes: 45 additions & 5 deletions src/lib/crypto_wrappers/PrivateKey.spec.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,58 @@
import { PrivateKey } from './PrivateKey';
import { HashingAlgorithm } from './algorithms';
import { PrivateKey, RsaPssPrivateKey } from './PrivateKey';
import { MockAesKwProvider } from './webcrypto/_test_utils';
import { AwalaAesKwProvider } from './webcrypto/AwalaAesKwProvider';

describe('constructor', () => {
const PROVIDER = new AwalaAesKwProvider(new MockAesKwProvider());
const PROVIDER = new AwalaAesKwProvider(new MockAesKwProvider());

describe('PrivateKey', () => {
const ALGORITHM: KeyAlgorithm = { name: 'RSA-PSS' };

test('Key type should be private', () => {
const key = new PrivateKey(PROVIDER);
const key = new PrivateKey(ALGORITHM, PROVIDER);

expect(key.type).toEqual('private');
});

test('Key should be extractable', () => {
const key = new PrivateKey(ALGORITHM, PROVIDER);

expect(key.extractable).toBeTrue();
});

test('Algorithm should be honoured', () => {
const key = new PrivateKey(ALGORITHM, PROVIDER);

expect(key.algorithm).toEqual(ALGORITHM);
});

test('Provider should be honoured', () => {
const key = new PrivateKey(ALGORITHM, PROVIDER);

expect(key.provider).toEqual(PROVIDER);
});
});

describe('RsaPssPrivateKey', () => {
const HASHING_ALGORITHM: HashingAlgorithm = 'SHA-384';

test('Key usages should only allow signing', () => {
const key = new RsaPssPrivateKey(HASHING_ALGORITHM, PROVIDER);

expect(key.usages).toEqual(['sign']);
});

test('Hashing algorithm should be added to key algorithm', () => {
const key = new RsaPssPrivateKey(HASHING_ALGORITHM, PROVIDER);

expect(key.algorithm).toEqual({
hash: { name: HASHING_ALGORITHM },
name: 'RSA-PSS',
});
});

test('Provider should be honoured', () => {
const key = new PrivateKey(PROVIDER);
const key = new RsaPssPrivateKey(HASHING_ALGORITHM, PROVIDER);

expect(key.provider).toEqual(PROVIDER);
});
Expand Down
24 changes: 21 additions & 3 deletions src/lib/crypto_wrappers/PrivateKey.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,27 @@
import { CryptoKey, ProviderCrypto } from 'webcrypto-core';
// tslint:disable:max-classes-per-file

import { CryptoKey, KeyAlgorithm, KeyUsages, ProviderCrypto } from 'webcrypto-core';

import { HashingAlgorithm } from './algorithms';

export class PrivateKey extends CryptoKey {
constructor(public readonly provider: ProviderCrypto) {
public override readonly extractable = true; // The **public** key is extractable as SPKI

public override readonly type = 'private' as KeyType;

constructor(
public override readonly algorithm: KeyAlgorithm,
public readonly provider: ProviderCrypto,
) {
super();
}
}

export class RsaPssPrivateKey extends PrivateKey {
public override readonly usages = ['sign'] as KeyUsages;

this.type = 'private';
constructor(hashingAlgorithm: HashingAlgorithm, provider: ProviderCrypto) {
const algorithm = { name: 'RSA-PSS', hash: { name: hashingAlgorithm } };
super(algorithm, provider);
}
}
17 changes: 6 additions & 11 deletions src/lib/crypto_wrappers/cms/signedData.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@ import {
import { CMS_OIDS } from '../../oids';
import { HashingAlgorithm } from '../algorithms';
import { generateRSAKeyPair } from '../keys';
import { PrivateKey } from '../PrivateKey';
import { MockAesKwProvider } from '../webcrypto/_test_utils';
import { getEngineForPrivateKey } from '../webcrypto/engine';
import { RsaPssPrivateKey } from '../PrivateKey';
import { MockRsaPssProvider } from '../webcrypto/_test_utils';
import Certificate from '../x509/Certificate';
import { deserializeContentInfo, serializeContentInfo } from './_test_utils';
import CMSError from './CMSError';
Expand Down Expand Up @@ -47,16 +46,12 @@ describe('sign', () => {
});

test('Crypto in private key should be used if set', async () => {
const privateKey = new PrivateKey(new MockAesKwProvider());
const engine = getEngineForPrivateKey(privateKey);
const signSpy = jest.spyOn(engine!.crypto.subtle, 'sign');
privateKey.algorithm = keyPair.privateKey.algorithm;
privateKey.usages = keyPair.privateKey.usages;
privateKey.extractable = keyPair.privateKey.extractable;
const provider = new MockRsaPssProvider();
const privateKey = new RsaPssPrivateKey('SHA-256', provider);

await expect(SignedData.sign(plaintext, privateKey, certificate)).toReject();
await expect(SignedData.sign(plaintext, privateKey, certificate)).toResolve();

expect(signSpy).toBeCalled();
expect(provider.onSign).toBeCalled();
});

describe('SignerInfo', () => {
Expand Down
86 changes: 67 additions & 19 deletions src/lib/crypto_wrappers/keys.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
getPublicKeyDigestHex,
getRSAPublicKeyFromPrivate,
} from './keys';
import { RsaPssPrivateKey } from './PrivateKey';
import { MockRsaPssProvider } from './webcrypto/_test_utils';

describe('generateRsaKeyPair', () => {
test('Keys should be RSA-PSS', async () => {
Expand Down Expand Up @@ -140,6 +142,21 @@ describe('getRSAPublicKeyFromPrivate', () => {
);
});

test('Public key should be taken from provider if custom one is used', async () => {
const keyPair = await generateRSAKeyPair();
const mockRsaPssProvider = new MockRsaPssProvider();
mockRsaPssProvider.onExportKey.mockResolvedValue(
await derSerializePublicKey(keyPair.publicKey),
);
const privateKey = new RsaPssPrivateKey('SHA-256', mockRsaPssProvider);

const publicKey = await getRSAPublicKeyFromPrivate(privateKey);

await expect(derSerializePublicKey(publicKey)).resolves.toEqual(
await derSerializePublicKey(keyPair.publicKey),
);
});

test('Public key should honour algorithm parameters', async () => {
const keyPair = await generateRSAKeyPair();

Expand Down Expand Up @@ -174,22 +191,38 @@ describe('Key serializers', () => {
mockExportKey.mockRestore();
});

test('derSerializePublicKey should convert public key to buffer', async () => {
const publicKeyDer = await derSerializePublicKey(stubKeyPair.publicKey);
describe('derSerializePublicKey', () => {
test('Public key should be converted to buffer', async () => {
const publicKeyDer = await derSerializePublicKey(stubKeyPair.publicKey);

expect(publicKeyDer).toEqual(Buffer.from(stubExportedKeyDer));

expect(mockExportKey).toBeCalledTimes(1);
expect(mockExportKey).toBeCalledWith('spki', stubKeyPair.publicKey);
});

test('Public key should be extracted first if input is PrivateKey', async () => {
const provider = new MockRsaPssProvider();
provider.onExportKey.mockResolvedValue(stubExportedKeyDer);
const privateKey = new RsaPssPrivateKey('SHA-256', provider);

expect(publicKeyDer).toEqual(Buffer.from(stubExportedKeyDer));
await expect(derSerializePublicKey(privateKey)).resolves.toEqual(
Buffer.from(stubExportedKeyDer),
);

expect(mockExportKey).toBeCalledTimes(1);
expect(mockExportKey).toBeCalledWith('spki', stubKeyPair.publicKey);
expect(mockExportKey).not.toBeCalled();
});
});

test('derSerializePrivateKey should convert private key to buffer', async () => {
const privateKeyDer = await derSerializePrivateKey(stubKeyPair.privateKey);
describe('derSerializePrivateKey', () => {
test('derSerializePrivateKey should convert private key to buffer', async () => {
const privateKeyDer = await derSerializePrivateKey(stubKeyPair.privateKey);

expect(privateKeyDer).toEqual(Buffer.from(stubExportedKeyDer));
expect(privateKeyDer).toEqual(Buffer.from(stubExportedKeyDer));

expect(mockExportKey).toBeCalledTimes(1);
expect(mockExportKey).toBeCalledWith('pkcs8', stubKeyPair.privateKey);
expect(mockExportKey).toBeCalledTimes(1);
expect(mockExportKey).toBeCalledWith('pkcs8', stubKeyPair.privateKey);
});
});
});

Expand Down Expand Up @@ -358,19 +391,34 @@ describe('Key deserializers', () => {
});
});

test('getPublicKeyDigest should return the SHA-256 digest of the public key', async () => {
const keyPair = await generateRSAKeyPair();
describe('getPublicKeyDigest', () => {
test('SHA-256 digest should be returned in hex', async () => {
const keyPair = await generateRSAKeyPair();

const digest = await getPublicKeyDigest(keyPair.publicKey);
const digest = await getPublicKeyDigest(keyPair.publicKey);

expect(Buffer.from(digest)).toEqual(
createHash('sha256')
.update(await derSerializePublicKey(keyPair.publicKey))
.digest(),
);
expect(Buffer.from(digest)).toEqual(
createHash('sha256')
.update(await derSerializePublicKey(keyPair.publicKey))
.digest(),
);
});

test('Public key should be extracted first if input is private key', async () => {
const mockPublicKeySerialized = arrayBufferFrom('the public key');
const provider = new MockRsaPssProvider();
provider.onExportKey.mockResolvedValue(mockPublicKeySerialized);
const privateKey = new RsaPssPrivateKey('SHA-256', provider);

const digest = await getPublicKeyDigest(privateKey);

expect(Buffer.from(digest)).toEqual(
createHash('sha256').update(Buffer.from(mockPublicKeySerialized)).digest(),
);
});
});

test('getPublicKeyDigest should return the SHA-256 hex digest of the public key', async () => {
test('getPublicKeyDigestHex should return the SHA-256 hex digest of the public key', async () => {
const keyPair = await generateRSAKeyPair();

const digestHex = await getPublicKeyDigestHex(keyPair.publicKey);
Expand Down
17 changes: 9 additions & 8 deletions src/lib/crypto_wrappers/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getAlgorithmParameters } from 'pkijs';

import { getPkijsCrypto } from './_utils';
import { ECDHCurveName, HashingAlgorithm, RSAModulus } from './algorithms';
import { PrivateKey } from './PrivateKey';

const cryptoEngine = getPkijsCrypto();

Expand Down Expand Up @@ -45,8 +46,7 @@ export async function generateRSAKeyPair(
// tslint:disable-next-line:no-object-mutation
rsaAlgorithm.modulusLength = modulus;

const keyPair = await cryptoEngine.generateKey(rsaAlgorithm, true, algorithm.usages);
return keyPair;
return cryptoEngine.generateKey(rsaAlgorithm, true, algorithm.usages);
}

/**
Expand All @@ -64,10 +64,8 @@ export async function generateECDHKeyPair(
}

export async function getRSAPublicKeyFromPrivate(privateKey: CryptoKey): Promise<CryptoKey> {
const publicKeyDer = await cryptoEngine.exportKey('spki', privateKey);
const hashingAlgoName = (privateKey.algorithm as any).hash.name;
const opts = { hash: { name: hashingAlgoName }, name: privateKey.algorithm.name };
return cryptoEngine.importKey('spki', publicKeyDer, opts, true, ['verify']);
const publicKeyDer = bufferToArray(await derSerializePublicKey(privateKey));
return cryptoEngine.importKey('spki', publicKeyDer, privateKey.algorithm, true, ['verify']);
}

//endregion
Expand All @@ -80,7 +78,10 @@ export async function getRSAPublicKeyFromPrivate(privateKey: CryptoKey): Promise
* @param publicKey
*/
export async function derSerializePublicKey(publicKey: CryptoKey): Promise<Buffer> {
const publicKeyDer = await cryptoEngine.exportKey('spki', publicKey);
const publicKeyDer =
publicKey instanceof PrivateKey
? ((await publicKey.provider.exportKey('spki', publicKey)) as ArrayBuffer)
: await cryptoEngine.exportKey('spki', publicKey);
return Buffer.from(publicKeyDer);
}

Expand Down Expand Up @@ -168,7 +169,7 @@ export async function derDeserializeECDHPrivateKey(
* @param publicKey
*/
export async function getPublicKeyDigest(publicKey: CryptoKey): Promise<ArrayBuffer> {
const publicKeyDer = await cryptoEngine.exportKey('spki', publicKey);
const publicKeyDer = await derSerializePublicKey(publicKey);
return cryptoEngine.digest({ name: 'SHA-256' }, publicKeyDer);
}

Expand Down
14 changes: 6 additions & 8 deletions src/lib/crypto_wrappers/rsaSigning.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { ProviderCrypto } from 'webcrypto-core';

import { arrayBufferFrom } from '../_test_utils';
import * as utils from './_utils';
import { generateRSAKeyPair } from './keys';
import { PrivateKey } from './PrivateKey';
import { RsaPssPrivateKey } from './PrivateKey';
import { sign, verify } from './rsaSigning';
import { MockRsaPssProvider } from './webcrypto/_test_utils';

const plaintext = arrayBufferFrom('the plaintext');

Expand All @@ -31,15 +30,14 @@ describe('sign', () => {

test('The plaintext should be signed with PrivateKey if requested', async () => {
const mockSignature = arrayBufferFrom('signature');
const mockProvider: Partial<ProviderCrypto> = {
sign: jest.fn().mockReturnValue(mockSignature),
};
const privateKey = new PrivateKey(mockProvider as any);
const mockProvider = new MockRsaPssProvider();
mockProvider.onSign.mockResolvedValue(mockSignature);
const privateKey = new RsaPssPrivateKey('SHA-256', mockProvider);

const signature = await sign(plaintext, privateKey);

expect(signature).toBe(mockSignature);
expect(mockProvider.sign).toBeCalledWith(RSA_PSS_PARAMS, privateKey, plaintext);
expect(mockProvider.onSign).toBeCalledWith(RSA_PSS_PARAMS, privateKey, plaintext);
});
});

Expand Down
18 changes: 14 additions & 4 deletions src/lib/crypto_wrappers/webcrypto/_test_utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
import { AesKwProvider } from 'webcrypto-core';
/* tslint:disable:max-classes-per-file */

import { AesKwProvider, RsaPssProvider } from 'webcrypto-core';

export class MockAesKwProvider extends AesKwProvider {
public readonly onGenerateKey = jest.fn();
public readonly onExportKey = jest.fn();
public readonly onImportKey = jest.fn();
public override readonly onGenerateKey = jest.fn();
public override readonly onExportKey = jest.fn();
public override readonly onImportKey = jest.fn();
}

export class MockRsaPssProvider extends RsaPssProvider {
public override readonly onGenerateKey = jest.fn();
public override readonly onSign = jest.fn();
public override readonly onVerify = jest.fn();
public override readonly onExportKey = jest.fn();
public override readonly onImportKey = jest.fn();
}
Loading

0 comments on commit 36458ce

Please sign in to comment.