From 0b112cf63ed2a859806531853c37486485740f9c Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sun, 29 Aug 2021 16:29:37 +0200 Subject: [PATCH] feat(node): support rsa-pss keys in Node.js >= 16.9.0 for sign/verify --- package.json | 6 +- src/runtime/node/node_key.ts | 48 +++++++++- test/jws/rsa-pss.test.mjs | 166 +++++++++++++++++++++++++++++++++++ 3 files changed, 214 insertions(+), 6 deletions(-) create mode 100644 test/jws/rsa-pss.test.mjs diff --git a/package.json b/package.json index 0f3e9ec7ef..f583e032c7 100644 --- a/package.json +++ b/package.json @@ -365,11 +365,11 @@ "prettier": "npx prettier --loglevel silent --write ./test ./src ./tools ./test-browser ./test-deno" }, "devDependencies": { - "@types/node": "^16.7.6", + "@types/node": "^16.7.10", "ava": "^3.15.0", "bowser": "^2.11.0", "c8": "^7.8.0", - "esbuild": "^0.12.24", + "esbuild": "^0.12.25", "glob": "^7.1.7", "karma": "^6.3.4", "karma-browserstack-launcher": "1.6.0", @@ -378,7 +378,7 @@ "nock": "^13.1.3", "npm-run-all": "^4.1.5", "prettier": "^2.3.2", - "qunit": "^2.16.0", + "qunit": "^2.17.0", "tar": "^6.1.11", "timekeeper": "^2.2.0", "typedoc": "^0.21.9", diff --git a/src/runtime/node/node_key.ts b/src/runtime/node/node_key.ts index d26d14f669..6464620ee8 100644 --- a/src/runtime/node/node_key.ts +++ b/src/runtime/node/node_key.ts @@ -5,6 +5,13 @@ import getNamedCurve from './get_named_curve.js' import { JOSENotSupported } from '../../util/errors.js' import checkModulusLength from './check_modulus_length.js' +const [major, minor] = process.version + .substr(1) + .split('.') + .map((str) => parseInt(str, 10)) + +const rsaPssParams = major >= 17 || (major === 16 && minor >= 9) + const ecCurveAlgMap = new Map([ ['ES256', 'P-256'], ['ES256K', 'secp256k1'], @@ -33,9 +40,44 @@ export default function keyForCrypto(alg: string, key: KeyObject): KeyObject | S return key - case 'PS256': - case 'PS384': - case 'PS512': + case rsaPssParams && 'PS256': + case rsaPssParams && 'PS384': + case rsaPssParams && 'PS512': + if (key.asymmetricKeyType === 'rsa-pss') { + // @ts-expect-error + const { hashAlgorithm, mgf1HashAlgorithm, saltLength } = key.asymmetricKeyDetails + + const length = parseInt(alg.substr(-3), 10) + + if ( + hashAlgorithm !== undefined && + (hashAlgorithm !== `sha${length}` || mgf1HashAlgorithm !== hashAlgorithm) + ) { + throw new TypeError( + `Invalid key for this operation, its RSA-PSS parameters do not meet the requirements of "alg" ${alg}`, + ) + } + if (saltLength !== undefined && saltLength > length >> 3) { + throw new TypeError( + `Invalid key for this operation, its RSA-PSS parameter saltLength does not meet the requirements of "alg" ${alg}`, + ) + } + } else if (key.asymmetricKeyType !== 'rsa') { + throw new TypeError( + 'Invalid key for this operation, its asymmetricKeyType must be rsa or rsa-pss', + ) + } + checkModulusLength(key, alg) + + return { + key, + padding: constants.RSA_PKCS1_PSS_PADDING, + saltLength: constants.RSA_PSS_SALTLEN_DIGEST, + } + + case !rsaPssParams && 'PS256': + case !rsaPssParams && 'PS384': + case !rsaPssParams && 'PS512': if (key.asymmetricKeyType !== 'rsa') { throw new TypeError('Invalid key for this operation, its asymmetricKeyType must be rsa') } diff --git a/test/jws/rsa-pss.test.mjs b/test/jws/rsa-pss.test.mjs new file mode 100644 index 0000000000..804a5890d6 --- /dev/null +++ b/test/jws/rsa-pss.test.mjs @@ -0,0 +1,166 @@ +import test from 'ava'; +import * as crypto from 'crypto'; +import { promisify } from 'util'; + +const generateKeyPair = promisify(crypto.generateKeyPair); + +const [major, minor] = process.version + .substr(1) + .split('.') + .map((str) => parseInt(str, 10)); + +const rsaPssParams = major >= 17 || (major === 16 && minor >= 9); +const electron = 'electron' in process.versions + +Promise.all([import('jose/jws/flattened/sign'), import('jose/jws/flattened/verify')]).then( + ([{ default: FlattenedSign }, { default: flattenedVerify }]) => { + if (rsaPssParams) { + for (const length of [256, 384, 512]) { + test(`valid RSASSA-PSS-Params PS${length}`, async (t) => { + for (const options of [ + { modulusLength: 2048 }, + { + modulusLength: 2048, + hashAlgorithm: `sha${length}`, + hash: `sha${length}`, + mgf1HashAlgorithm: `sha${length}`, + mgf1Hash: `sha${length}`, + saltLength: 0, + }, + { + modulusLength: 2048, + hashAlgorithm: `sha${length}`, + hash: `sha${length}`, + mgf1HashAlgorithm: `sha${length}`, + mgf1Hash: `sha${length}`, + saltLength: length >> 3, + }, + ]) { + const { privateKey, publicKey } = await generateKeyPair('rsa-pss', options); + const jws = await new FlattenedSign(new Uint8Array(0)) + .setProtectedHeader({ alg: `PS${length}` }) + .sign(privateKey); + await flattenedVerify(jws, publicKey); + } + t.pass(); + }); + + test(`invalid saltLength for PS${length}`, async (t) => { + const { privateKey, publicKey } = await generateKeyPair('rsa-pss', { + modulusLength: 2048, + hashAlgorithm: `sha${length}`, + hash: `sha${length}`, + mgf1HashAlgorithm: `sha${length}`, + mgf1Hash: `sha${length}`, + saltLength: (length >> 3) + 1, + }); + await t.throwsAsync( + new FlattenedSign(new Uint8Array(0)) + .setProtectedHeader({ alg: `PS${length}` }) + .sign(privateKey), + { + message: `Invalid key for this operation, its RSA-PSS parameter saltLength does not meet the requirements of "alg" PS${length}`, + instanceOf: TypeError, + }, + ); + await t.throwsAsync( + flattenedVerify( + { header: { alg: `PS${length}` }, payload: '', signature: '' }, + publicKey, + ), + { + message: `Invalid key for this operation, its RSA-PSS parameter saltLength does not meet the requirements of "alg" PS${length}`, + instanceOf: TypeError, + }, + ); + }); + + test(`invalid hashAlgorithm for PS${length}`, async (t) => { + const { privateKey, publicKey } = await generateKeyPair('rsa-pss', { + modulusLength: 2048, + hashAlgorithm: 'sha1', + hash: 'sha1', + mgf1HashAlgorithm: `sha${length}`, + mgf1Hash: `sha${length}`, + saltLength: length >> 3, + }); + await t.throwsAsync( + new FlattenedSign(new Uint8Array(0)) + .setProtectedHeader({ alg: `PS${length}` }) + .sign(privateKey), + { + message: `Invalid key for this operation, its RSA-PSS parameters do not meet the requirements of "alg" PS${length}`, + instanceOf: TypeError, + }, + ); + await t.throwsAsync( + flattenedVerify( + { header: { alg: `PS${length}` }, payload: '', signature: '' }, + publicKey, + ), + { + message: `Invalid key for this operation, its RSA-PSS parameters do not meet the requirements of "alg" PS${length}`, + instanceOf: TypeError, + }, + ); + }); + + test(`invalid mgf1HashAlgorithm for PS${length}`, async (t) => { + const { privateKey, publicKey } = await generateKeyPair('rsa-pss', { + modulusLength: 2048, + hashAlgorithm: `sha${length}`, + hash: `sha${length}`, + mgf1HashAlgorithm: 'sha1', + mgf1Hash: 'sha1', + saltLength: length >> 3, + }); + await t.throwsAsync( + new FlattenedSign(new Uint8Array(0)) + .setProtectedHeader({ alg: `PS${length}` }) + .sign(privateKey), + { + message: `Invalid key for this operation, its RSA-PSS parameters do not meet the requirements of "alg" PS${length}`, + instanceOf: TypeError, + }, + ); + await t.throwsAsync( + flattenedVerify( + { header: { alg: `PS${length}` }, payload: '', signature: '' }, + publicKey, + ), + { + message: `Invalid key for this operation, its RSA-PSS parameters do not meet the requirements of "alg" PS${length}`, + instanceOf: TypeError, + }, + ); + }); + } + } else if (!electron) { + test('does not support rsa-pss', async (t) => { + const { privateKey, publicKey } = await generateKeyPair('rsa-pss', { modulusLength: 2048 }); + await t.throwsAsync( + new FlattenedSign(new Uint8Array(0)) + .setProtectedHeader({ alg: 'PS256' }) + .sign(privateKey), + { + message: 'Invalid key for this operation, its asymmetricKeyType must be rsa', + instanceOf: TypeError, + }, + ); + await t.throwsAsync( + flattenedVerify({ header: { alg: 'PS256' }, payload: '', signature: '' }, publicKey), + { + message: 'Invalid key for this operation, its asymmetricKeyType must be rsa', + instanceOf: TypeError, + }, + ); + }); + } + }, + (err) => { + test('failed to import', (t) => { + console.error(err); + t.fail(); + }); + }, +);