diff --git a/src/index.ts b/src/index.ts index bfd8366a5..ec28b8772 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'; diff --git a/src/lib/crypto_wrappers/PrivateKey.spec.ts b/src/lib/crypto_wrappers/PrivateKey.spec.ts index 2b6f47dd6..0e4c55ff8 100644 --- a/src/lib/crypto_wrappers/PrivateKey.spec.ts +++ b/src/lib/crypto_wrappers/PrivateKey.spec.ts @@ -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); }); diff --git a/src/lib/crypto_wrappers/PrivateKey.ts b/src/lib/crypto_wrappers/PrivateKey.ts index 173523ac5..744fe6af2 100644 --- a/src/lib/crypto_wrappers/PrivateKey.ts +++ b/src/lib/crypto_wrappers/PrivateKey.ts @@ -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); } } diff --git a/src/lib/crypto_wrappers/cms/signedData.spec.ts b/src/lib/crypto_wrappers/cms/signedData.spec.ts index 21da9b6d2..1bd7b559f 100644 --- a/src/lib/crypto_wrappers/cms/signedData.spec.ts +++ b/src/lib/crypto_wrappers/cms/signedData.spec.ts @@ -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'; @@ -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', () => { diff --git a/src/lib/crypto_wrappers/keys.spec.ts b/src/lib/crypto_wrappers/keys.spec.ts index d0de31e90..df7825d83 100644 --- a/src/lib/crypto_wrappers/keys.spec.ts +++ b/src/lib/crypto_wrappers/keys.spec.ts @@ -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 () => { @@ -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(); @@ -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); + }); }); }); @@ -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); diff --git a/src/lib/crypto_wrappers/keys.ts b/src/lib/crypto_wrappers/keys.ts index de1cbdd0b..f8ca4b62f 100644 --- a/src/lib/crypto_wrappers/keys.ts +++ b/src/lib/crypto_wrappers/keys.ts @@ -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(); @@ -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); } /** @@ -64,10 +64,8 @@ export async function generateECDHKeyPair( } export async function getRSAPublicKeyFromPrivate(privateKey: CryptoKey): Promise { - 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 @@ -80,7 +78,10 @@ export async function getRSAPublicKeyFromPrivate(privateKey: CryptoKey): Promise * @param publicKey */ export async function derSerializePublicKey(publicKey: CryptoKey): Promise { - 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); } @@ -168,7 +169,7 @@ export async function derDeserializeECDHPrivateKey( * @param publicKey */ export async function getPublicKeyDigest(publicKey: CryptoKey): Promise { - const publicKeyDer = await cryptoEngine.exportKey('spki', publicKey); + const publicKeyDer = await derSerializePublicKey(publicKey); return cryptoEngine.digest({ name: 'SHA-256' }, publicKeyDer); } diff --git a/src/lib/crypto_wrappers/rsaSigning.spec.ts b/src/lib/crypto_wrappers/rsaSigning.spec.ts index 3dd991ece..f7c43f2d8 100644 --- a/src/lib/crypto_wrappers/rsaSigning.spec.ts +++ b/src/lib/crypto_wrappers/rsaSigning.spec.ts @@ -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'); @@ -31,15 +30,14 @@ describe('sign', () => { test('The plaintext should be signed with PrivateKey if requested', async () => { const mockSignature = arrayBufferFrom('signature'); - const mockProvider: Partial = { - 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); }); }); diff --git a/src/lib/crypto_wrappers/webcrypto/_test_utils.ts b/src/lib/crypto_wrappers/webcrypto/_test_utils.ts index e74e1d48e..ad23f3e25 100644 --- a/src/lib/crypto_wrappers/webcrypto/_test_utils.ts +++ b/src/lib/crypto_wrappers/webcrypto/_test_utils.ts @@ -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(); } diff --git a/src/lib/crypto_wrappers/webcrypto/engine.spec.ts b/src/lib/crypto_wrappers/webcrypto/engine.spec.ts index 9e450b5c6..c1044a869 100644 --- a/src/lib/crypto_wrappers/webcrypto/engine.spec.ts +++ b/src/lib/crypto_wrappers/webcrypto/engine.spec.ts @@ -1,11 +1,11 @@ import { SubtleCrypto } from 'webcrypto-core'; -import { PrivateKey } from '../PrivateKey'; -import { MockAesKwProvider } from './_test_utils'; +import { RsaPssPrivateKey } from '../PrivateKey'; +import { MockRsaPssProvider } from './_test_utils'; import { getEngineForPrivateKey } from './engine'; describe('getEngine', () => { - const PROVIDER = new MockAesKwProvider(); + const PROVIDER = new MockRsaPssProvider(); test('undefined should be returned if CryptoKey is used', () => { const engine = getEngineForPrivateKey(null as unknown as CryptoKey); @@ -14,7 +14,7 @@ describe('getEngine', () => { }); test('Nameless engine should be returned if PrivateKey is used', () => { - const key = new PrivateKey(PROVIDER); + const key = new RsaPssPrivateKey('SHA-256', PROVIDER); const engine = getEngineForPrivateKey(key); @@ -22,7 +22,7 @@ describe('getEngine', () => { }); test('Engine crypto should use provider from private key', () => { - const key = new PrivateKey(PROVIDER); + const key = new RsaPssPrivateKey('SHA-256', PROVIDER); const engine = getEngineForPrivateKey(key); @@ -31,8 +31,8 @@ describe('getEngine', () => { test('Same engine should be returned if multiple keys share provider', () => { // This is to check engines are being cached - const key1 = new PrivateKey(PROVIDER); - const key2 = new PrivateKey(PROVIDER); + const key1 = new RsaPssPrivateKey('SHA-256', PROVIDER); + const key2 = new RsaPssPrivateKey('SHA-256', PROVIDER); const engine1 = getEngineForPrivateKey(key1); const engine2 = getEngineForPrivateKey(key2); @@ -41,8 +41,8 @@ describe('getEngine', () => { }); test('Different engines should be returned if keys use different providers', () => { - const key1 = new PrivateKey(PROVIDER); - const key2 = new PrivateKey(new MockAesKwProvider()); + const key1 = new RsaPssPrivateKey('SHA-256', PROVIDER); + const key2 = new RsaPssPrivateKey('SHA-256', new MockRsaPssProvider()); const engine1 = getEngineForPrivateKey(key1); const engine2 = getEngineForPrivateKey(key2); diff --git a/src/lib/crypto_wrappers/x509/Certificate.spec.ts b/src/lib/crypto_wrappers/x509/Certificate.spec.ts index 0569e7d65..475ab9986 100644 --- a/src/lib/crypto_wrappers/x509/Certificate.spec.ts +++ b/src/lib/crypto_wrappers/x509/Certificate.spec.ts @@ -12,8 +12,8 @@ import { generateRSAKeyPair, getPrivateAddressFromIdentityKey, } from '../keys'; -import { PrivateKey } from '../PrivateKey'; -import { MockAesKwProvider } from '../webcrypto/_test_utils'; +import { RsaPssPrivateKey } from '../PrivateKey'; +import { MockRsaPssProvider } from '../webcrypto/_test_utils'; import { getEngineForPrivateKey } from '../webcrypto/engine'; import Certificate from './Certificate'; import CertificateError from './CertificateError'; @@ -111,9 +111,7 @@ describe('issue()', () => { }); test('should use crypto engine in private key if set', async () => { - const privateKey = new PrivateKey(new MockAesKwProvider()); - // tslint:disable-next-line:no-object-mutation - privateKey.algorithm = subjectKeyPair.privateKey.algorithm; + const privateKey = new RsaPssPrivateKey('SHA-256', new MockRsaPssProvider()); jest.spyOn(pkijs.Certificate.prototype, 'sign'); await expect( @@ -122,7 +120,7 @@ describe('issue()', () => { issuerPrivateKey: privateKey, subjectPublicKey: subjectKeyPair.publicKey, }), - ).toReject(); + ).toResolve(); const engine = getEngineForPrivateKey(privateKey); expect(engine).toBeInstanceOf(pkijs.CryptoEngine);