Skip to content

Commit

Permalink
feat(node): support rsa-pss keys in Node.js >= 16.9.0 for sign/verify
Browse files Browse the repository at this point in the history
  • Loading branch information
panva committed Sep 6, 2021
1 parent be3638c commit 0b112cf
Show file tree
Hide file tree
Showing 3 changed files with 214 additions and 6 deletions.
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
48 changes: 45 additions & 3 deletions src/runtime/node/node_key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down Expand Up @@ -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')
}
Expand Down
166 changes: 166 additions & 0 deletions test/jws/rsa-pss.test.mjs
Original file line number Diff line number Diff line change
@@ -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();
});
},
);

0 comments on commit 0b112cf

Please sign in to comment.