diff --git a/CHANGELOG.md b/CHANGELOG.md index 54364a2..572d767 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,24 @@ All notable changes to this project will be documented in this file starting from version **v4.0.0**. This project adheres to [Semantic Versioning](http://semver.org/). +## 9.0.0 - 2022-12-21 + + **Breaking changes: See [Migration from v8 to v9](https://github.com/auth0/node-jsonwebtoken/wiki/Migration-Notes:-v8-to-v9)** + +### Breaking changes + +- Removed support for Node versions 11 and below. +- The verify() function no longer accepts unsigned tokens by default. ([834503079514b72264fd13023a3b8d648afd6a16]https://github.com/auth0/node-jsonwebtoken/commit/834503079514b72264fd13023a3b8d648afd6a16) +- RSA key size must be 2048 bits or greater. ([ecdf6cc6073ea13a7e71df5fad043550f08d0fa6]https://github.com/auth0/node-jsonwebtoken/commit/ecdf6cc6073ea13a7e71df5fad043550f08d0fa6) +- Key types must be valid for the signing / verification algorithm + +### Security fixes + +- security: fixes `Arbitrary File Write via verify function` - CVE-2022-23529 +- security: fixes `Insecure default algorithm in jwt.verify() could lead to signature validation bypass` - CVE-2022-23540 +- security: fixes `Insecure implementation of key retrieval function could lead to Forgeable Public/Private Tokens from RSA to HMAC` - CVE-2022-23541 +- security: fixes `Unrestricted key type could lead to legacy keys usage` - CVE-2022-23539 + ## 8.5.1 - 2019-03-18 ### Bug fix diff --git a/README.md b/README.md index 0510907..4e20dd9 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ $ npm install jsonwebtoken # Migration notes +* [From v8 to v9](https://github.com/auth0/node-jsonwebtoken/wiki/Migration-Notes:-v8-to-v9) * [From v7 to v8](https://github.com/auth0/node-jsonwebtoken/wiki/Migration-Notes:-v7-to-v8) # Usage @@ -52,6 +53,7 @@ When signing with RSA algorithms the minimum modulus length is 2048 except when * `keyid` * `mutatePayload`: if true, the sign function will modify the payload object directly. This is useful if you need a raw reference to the payload after claims have been applied to it but before it has been encoded into a token. * `allowInsecureKeySizes`: if true allows private keys with a modulus below 2048 to be used for RSA +* `allowInvalidAsymmetricKeyTypes`: if true, allows asymmetric keys which do not match the specified algorithm. This option is intended only for backwards compatability and should be avoided. @@ -158,7 +160,7 @@ As mentioned in [this comment](https://github.com/auth0/node-jsonwebtoken/issues > Eg: `1000`, `"2 days"`, `"10h"`, `"7d"`. A numeric value is interpreted as a seconds count. If you use a string be sure you provide the time units (days, hours, etc), otherwise milliseconds unit is used by default (`"120"` is equal to `"120ms"`). * `clockTimestamp`: the time in seconds that should be used as the current time for all necessary comparisons. * `nonce`: if you want to check `nonce` claim, provide a string value here. It is used on Open ID for the ID Tokens. ([Open ID implementation notes](https://openid.net/specs/openid-connect-core-1_0.html#NonceNotes)) - +* `allowInvalidAsymmetricKeyTypes`: if true, allows asymmetric keys which do not match the specified algorithm. This option is intended only for backwards compatability and should be avoided. ```js // verify a token symmetric - synchronous diff --git a/lib/asymmetricKeyDetailsSupported.js b/lib/asymmetricKeyDetailsSupported.js new file mode 100644 index 0000000..a6ede56 --- /dev/null +++ b/lib/asymmetricKeyDetailsSupported.js @@ -0,0 +1,3 @@ +const semver = require('semver'); + +module.exports = semver.satisfies(process.version, '>=15.7.0'); diff --git a/lib/rsaPssKeyDetailsSupported.js b/lib/rsaPssKeyDetailsSupported.js new file mode 100644 index 0000000..7fcf368 --- /dev/null +++ b/lib/rsaPssKeyDetailsSupported.js @@ -0,0 +1,3 @@ +const semver = require('semver'); + +module.exports = semver.satisfies(process.version, '>=16.9.0'); diff --git a/lib/validateAsymmetricKey.js b/lib/validateAsymmetricKey.js new file mode 100644 index 0000000..c10340b --- /dev/null +++ b/lib/validateAsymmetricKey.js @@ -0,0 +1,66 @@ +const ASYMMETRIC_KEY_DETAILS_SUPPORTED = require('./asymmetricKeyDetailsSupported'); +const RSA_PSS_KEY_DETAILS_SUPPORTED = require('./rsaPssKeyDetailsSupported'); + +const allowedAlgorithmsForKeys = { + 'ec': ['ES256', 'ES384', 'ES512'], + 'rsa': ['RS256', 'PS256', 'RS384', 'PS384', 'RS512', 'PS512'], + 'rsa-pss': ['PS256', 'PS384', 'PS512'] +}; + +const allowedCurves = { + ES256: 'prime256v1', + ES384: 'secp384r1', + ES512: 'secp521r1', +}; + +module.exports = function(algorithm, key) { + if (!algorithm || !key) return; + + const keyType = key.asymmetricKeyType; + if (!keyType) return; + + const allowedAlgorithms = allowedAlgorithmsForKeys[keyType]; + + if (!allowedAlgorithms) { + throw new Error(`Unknown key type "${keyType}".`); + } + + if (!allowedAlgorithms.includes(algorithm)) { + throw new Error(`"alg" parameter for "${keyType}" key type must be one of: ${allowedAlgorithms.join(', ')}.`) + } + + /* + * Ignore the next block from test coverage because it gets executed + * conditionally depending on the Node version. Not ignoring it would + * prevent us from reaching the target % of coverage for versions of + * Node under 15.7.0. + */ + /* istanbul ignore next */ + if (ASYMMETRIC_KEY_DETAILS_SUPPORTED) { + switch (keyType) { + case 'ec': + const keyCurve = key.asymmetricKeyDetails.namedCurve; + const allowedCurve = allowedCurves[algorithm]; + + if (keyCurve !== allowedCurve) { + throw new Error(`"alg" parameter "${algorithm}" requires curve "${allowedCurve}".`); + } + break; + + case 'rsa-pss': + if (RSA_PSS_KEY_DETAILS_SUPPORTED) { + const length = parseInt(algorithm.slice(-3), 10); + const { hashAlgorithm, mgf1HashAlgorithm, saltLength } = key.asymmetricKeyDetails; + + if (hashAlgorithm !== `sha${length}` || mgf1HashAlgorithm !== hashAlgorithm) { + throw new Error(`Invalid key for this operation, its RSA-PSS parameters do not meet the requirements of "alg" ${algorithm}.`); + } + + if (saltLength !== undefined && saltLength > length >> 3) { + throw new Error(`Invalid key for this operation, its RSA-PSS parameter saltLength does not meet the requirements of "alg" ${algorithm}.`) + } + } + break; + } + } +} diff --git a/package.json b/package.json index 8e4345c..4f1e4e9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonwebtoken", - "version": "8.5.1", + "version": "9.0.0", "description": "JSON Web Token implementation (symmetric and asymmetric)", "main": "index.js", "nyc": { diff --git a/sign.js b/sign.js index 3da5119..1aeeabc 100644 --- a/sign.js +++ b/sign.js @@ -1,5 +1,6 @@ const timespan = require('./lib/timespan'); const PS_SUPPORTED = require('./lib/psSupported'); +const validateAsymmetricKey = require('./lib/validateAsymmetricKey'); const jws = require('jws'); const {includes, isBoolean, isInteger, isNumber, isPlainObject, isString, once} = require('lodash') const { KeyObject, createSecretKey, createPrivateKey } = require('crypto') @@ -22,7 +23,8 @@ const sign_options_schema = { noTimestamp: { isValid: isBoolean, message: '"noTimestamp" must be a boolean' }, keyid: { isValid: isString, message: '"keyid" must be a string' }, mutatePayload: { isValid: isBoolean, message: '"mutatePayload" must be a boolean' }, - allowInsecureKeySizes: { isValid: isBoolean, message: '"allowInsecureKeySizes" must be a boolean'} + allowInsecureKeySizes: { isValid: isBoolean, message: '"allowInsecureKeySizes" must be a boolean'}, + allowInvalidAsymmetricKeyTypes: { isValid: isBoolean, message: '"allowInvalidAsymmetricKeyTypes" must be a boolean'} }; const registered_claims_schema = { @@ -166,6 +168,14 @@ module.exports = function (payload, secretOrPrivateKey, options, callback) { return failure(error); } + if (!options.allowInvalidAsymmetricKeyTypes) { + try { + validateAsymmetricKey(header.alg, secretOrPrivateKey); + } catch (error) { + return failure(error); + } + } + const timestamp = payload.iat || Math.floor(Date.now() / 1000); if (options.noTimestamp) { diff --git a/test/dsa-private.pem b/test/dsa-private.pem new file mode 100644 index 0000000..e73003a --- /dev/null +++ b/test/dsa-private.pem @@ -0,0 +1,36 @@ +-----BEGIN DSA PRIVATE KEY----- +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 +-----END DSA PRIVATE KEY----- diff --git a/test/dsa-public.pem b/test/dsa-public.pem new file mode 100644 index 0000000..659d96b --- /dev/null +++ b/test/dsa-public.pem @@ -0,0 +1,36 @@ +-----BEGIN PUBLIC KEY----- +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 +y0elWU3mxe6Rn6R7WHM8or7sX3tWDq6OntJlKg== +-----END PUBLIC KEY----- diff --git a/test/jwt.asymmetric_signing.tests.js b/test/jwt.asymmetric_signing.tests.js index c56eea3..a8472d5 100644 --- a/test/jwt.asymmetric_signing.tests.js +++ b/test/jwt.asymmetric_signing.tests.js @@ -1,17 +1,17 @@ -var jwt = require('../index'); -var PS_SUPPORTED = require('../lib/psSupported'); -var fs = require('fs'); -var path = require('path'); +const jwt = require('../index'); +const PS_SUPPORTED = require('../lib/psSupported'); +const fs = require('fs'); +const path = require('path'); -var expect = require('chai').expect; -var assert = require('chai').assert; -var ms = require('ms'); +const expect = require('chai').expect; +const assert = require('chai').assert; +const ms = require('ms'); function loadKey(filename) { return fs.readFileSync(path.join(__dirname, filename)); } -var algorithms = { +const algorithms = { RS256: { pub_key: loadKey('pub.pem'), priv_key: loadKey('priv.pem'), @@ -35,18 +35,17 @@ if (PS_SUPPORTED) { } -describe('Asymmetric Algorithms', function(){ - +describe('Asymmetric Algorithms', function() { Object.keys(algorithms).forEach(function (algorithm) { describe(algorithm, function () { - var pub = algorithms[algorithm].pub_key; - var priv = algorithms[algorithm].priv_key; + const pub = algorithms[algorithm].pub_key; + const priv = algorithms[algorithm].priv_key; // "invalid" means it is not the public key for the loaded "priv" key - var invalid_pub = algorithms[algorithm].invalid_pub_key; + const invalid_pub = algorithms[algorithm].invalid_pub_key; describe('when signing a token', function () { - var token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm }); + const token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm }); it('should be syntactically valid', function () { expect(token).to.be.a('string'); @@ -73,13 +72,13 @@ describe('Asymmetric Algorithms', function(){ context('synchronous', function () { it('should validate with public key', function () { - var decoded = jwt.verify(token, pub); + const decoded = jwt.verify(token, pub); assert.ok(decoded.foo); assert.equal('bar', decoded.foo); }); it('should throw with invalid public key', function () { - var jwtVerify = jwt.verify.bind(null, token, invalid_pub) + const jwtVerify = jwt.verify.bind(null, token, invalid_pub) assert.throw(jwtVerify, 'invalid signature'); }); }); @@ -87,9 +86,8 @@ describe('Asymmetric Algorithms', function(){ }); describe('when signing a token with expiration', function () { - var token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm, expiresIn: '10m' }); - it('should be valid expiration', function (done) { + const token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm, expiresIn: '10m' }); jwt.verify(token, pub, function (err, decoded) { assert.isNotNull(decoded); assert.isNull(err); @@ -99,8 +97,7 @@ describe('Asymmetric Algorithms', function(){ it('should be invalid', function (done) { // expired token - token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm, expiresIn: -1 * ms('10m') }); - + const token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm, expiresIn: -1 * ms('10m') }); jwt.verify(token, pub, function (err, decoded) { assert.isUndefined(decoded); assert.isNotNull(err); @@ -113,7 +110,7 @@ describe('Asymmetric Algorithms', function(){ it('should NOT be invalid', function (done) { // expired token - token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm, expiresIn: -1 * ms('10m') }); + const token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm, expiresIn: -1 * ms('10m') }); jwt.verify(token, pub, { ignoreExpiration: true }, function (err, decoded) { assert.ok(decoded.foo); @@ -135,7 +132,7 @@ describe('Asymmetric Algorithms', function(){ }); describe('when decoding a jwt token with additional parts', function () { - var token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm }); + const token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm }); it('should throw', function (done) { jwt.verify(token + '.foo', pub, function (err, decoded) { @@ -148,7 +145,7 @@ describe('Asymmetric Algorithms', function(){ describe('when decoding a invalid jwt token', function () { it('should return null', function (done) { - var payload = jwt.decode('whatever.token'); + const payload = jwt.decode('whatever.token'); assert.isNull(payload); done(); }); @@ -156,16 +153,16 @@ describe('Asymmetric Algorithms', function(){ describe('when decoding a valid jwt token', function () { it('should return the payload', function (done) { - var obj = { foo: 'bar' }; - var token = jwt.sign(obj, priv, { algorithm: algorithm }); - var payload = jwt.decode(token); + const obj = { foo: 'bar' }; + const token = jwt.sign(obj, priv, { algorithm: algorithm }); + const payload = jwt.decode(token); assert.equal(payload.foo, obj.foo); done(); }); it('should return the header and payload and signature if complete option is set', function (done) { - var obj = { foo: 'bar' }; - var token = jwt.sign(obj, priv, { algorithm: algorithm }); - var decoded = jwt.decode(token, { complete: true }); + const obj = { foo: 'bar' }; + const token = jwt.sign(obj, priv, { algorithm: algorithm }); + const decoded = jwt.decode(token, { complete: true }); assert.equal(decoded.payload.foo, obj.foo); assert.deepEqual(decoded.header, { typ: 'JWT', alg: algorithm }); assert.ok(typeof decoded.signature == 'string'); @@ -174,4 +171,38 @@ describe('Asymmetric Algorithms', function(){ }); }); }); + + describe('when signing a token with an unsupported private key type', function () { + it('should throw an error', function() { + const obj = { foo: 'bar' }; + const key = loadKey('dsa-private.pem'); + const algorithm = 'RS256'; + + expect(function() { + jwt.sign(obj, key, { algorithm }); + }).to.throw('Unknown key type "dsa".'); + }); + }); + + describe('when signing a token with an incorrect private key type', function () { + it('should throw a validation error if key validation is enabled', function() { + const obj = { foo: 'bar' }; + const key = loadKey('rsa-private.pem'); + const algorithm = 'ES256'; + + expect(function() { + jwt.sign(obj, key, { algorithm }); + }).to.throw(/"alg" parameter for "rsa" key type must be one of:/); + }); + + it('should throw an unknown error if key validation is disabled', function() { + const obj = { foo: 'bar' }; + const key = loadKey('rsa-private.pem'); + const algorithm = 'ES256'; + + expect(function() { + jwt.sign(obj, key, { algorithm, allowInvalidAsymmetricKeyTypes: true }); + }).to.not.throw(/"alg" parameter for "rsa" key type must be one of:/); + }); + }); }); diff --git a/test/prime256v1-private.pem b/test/prime256v1-private.pem new file mode 100644 index 0000000..3173665 --- /dev/null +++ b/test/prime256v1-private.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIMP1Xt/ic2jAHJva2Pll866d1jYL+dk3VdLytEU1+LFmoAoGCCqGSM49 +AwEHoUQDQgAEvIywoA1H1a2XpPPTqsRxSk6YnNRVsu4E+wTvb7uV6Yttvko9zWar +jmtM3LHDXk/nHn+Pva0KD+lby8gb2daHGg== +-----END EC PRIVATE KEY----- diff --git a/test/rsa-pss-invalid-salt-length-private.pem b/test/rsa-pss-invalid-salt-length-private.pem new file mode 100644 index 0000000..cbafa66 --- /dev/null +++ b/test/rsa-pss-invalid-salt-length-private.pem @@ -0,0 +1,29 @@ +-----BEGIN PRIVATE KEY----- +MIIE8gIBADBCBgkqhkiG9w0BAQowNaAPMA0GCWCGSAFlAwQCAQUAoRwwGgYJKoZI +hvcNAQEIMA0GCWCGSAFlAwQCAQUAogQCAgQABIIEpzCCBKMCAQACggEBAJy3FuDR +1qKXsC8o+0xDJbuJCnysT71EFDGQY2/b3cZmxW3rzDYLyE65t2Go1jeK5Kxs+kwS +1VxfefD8DifeDZN66wjRse4iWLcxmQB5FfishXOdozciimgXNvXJNS8X//feSofl +vDQaTUI0NJnw1qQ2CB0pgGInwajsRKpWnDOhfk3NA/cmGlmfhTtDSTxq0ReytUie +TjY7gy+S9YYm4bAgBcMeoup0GEPzYccK4+1yCmWzQZGFcrY1cuB9bL+vT7ajQFhe +WVKlp6z35GyBF2zI7gJSkHpUHaWV5+Z9aTr6+YP6U7xuCRvXQ/l6BEOUjt4Es2YG +3frgxeVbOs1gAakCAwEAAQKCAQAMvFxhnOwCfq1Ux9HUWsigOvzdMOuyB+xUMtXB +625Uh1mYG0eXRNHcg/9BMoVmMiVvVdPphsZMIX45dWJ5HvSffafIKbJ6FdR73s3+ +WdjNQsf9o1v2SRpSZ0CSLO3ji+HDdQ89iBAJc/G/ZZq4v/fRlIqIRC0ozO5SGhFi +fnNnRqH78d2KeJMX/g9jBZM8rJQCi+pb0keHmFmLJ5gZa4HokE8rWQJQY46PVYUH +W2BwEJToMl3MPC7D95soWVuFt3KHnIWhuma/tnCmd2AUvcMrdWq0CwStH3vuX4LB +vJug0toWkobt1tzZgzzCASb2EpzJj8UNxP1CzTQWsvl8OephAoGBAMVnmZeLHoh2 +kxn/+rXetZ4Msjgu19MHNQAtlMvqzwZLan0K/BhnHprJLy4SDOuQYIs+PYJuXdT7 +Yv2mp9kwTPz8glP9LAto4MDeDfCu0cyXmZb2VQcT/lqVyrwfx3Psqxm/Yxg62YKr +aQE8WqgZGUdOvU9dYU+7EmPlYpdGpPVlAoGBAMs7ks+12oE6kci3WApdnt0kk5+f +8fbQ0lp2vR3tEw8DURa5FnHWA4o46XvcMcuXwZBrpxANPNAxJJjMBs1hSkc8h4hd +4vjtRNYJpj+uBdDIRmdqTzbpWv+hv8Xpiol5EVgnMVs2UZWDjoxQ+mYa1R8tAUfj +ojzV2KBMWGCoHgj1AoGALki6JGQEBq72kpQILnhHUQVdC/s/s0TvUlldl+o4HBu2 +nhbjQL182YHuQ/kLenfhiwRO27QQ4A0JCrv2gt/mTTLPQ+4KU6qFd/MYhaQXoMay +xkh/aydu7cJNRIqW80E8ZM8Q5u91bEPQXO/PubYYzTVTAba9SDpud2mjEiEIMFkC +gYEAxINEQEgtkkuZ76UpIkzIcjkN7YlxJCFjZUnvL+KvTRL986TgyQ4RujOxwKx4 +Ec8ZwZX2opTKOt1p771IzorGkf87ZmayM9TpfLUz5dtVkD43pYOsOQKHlStIDgz2 +gltoo/6xwOrTFGlzCsa6eMR1U4Hm/SZlF8IHh2iLBFtLP4kCgYBqTi1XeWeVQVSA +y9Wolv9kMoRh/Xh6F2D8bTTybGshDVO+P4YLM4lLxh5UDZAd/VOkdf3ZIcUGv022 +lxrYbLbIEGckMCpkdHeZH/1/iuJUeiCrXeyNlQsXBrmJKr/0lENniJHGpiSEyvY5 +D8Oafyjd7ZjUmyBFvS4heQEC6Pjo3Q== +-----END PRIVATE KEY----- diff --git a/test/rsa-pss-private.pem b/test/rsa-pss-private.pem new file mode 100644 index 0000000..52b1c08 --- /dev/null +++ b/test/rsa-pss-private.pem @@ -0,0 +1,29 @@ +-----BEGIN PRIVATE KEY----- +MIIE8QIBADBBBgkqhkiG9w0BAQowNKAPMA0GCWCGSAFlAwQCAQUAoRwwGgYJKoZI +hvcNAQEIMA0GCWCGSAFlAwQCAQUAogMCASAEggSnMIIEowIBAAKCAQEA00tEqqyF +VnyvcVA2ewVoSicCMdQXmWyYM82sBWX0wcnn0WUuZp1zjux4xTvQ71Lhx95OJCQZ +7r7b2192Im5ca37wNRbI6DhyXNdNVFXLFYlNAvgP+V0gIwlr6NgopdJqHCjYVv/g +GOoesRZaDdtV1A3O9CXdJ34x2HZh7nhwYK5hqZDhUW4rd+5GzIIzwCJfwgTQpkIc +18UeMMEoKJ6A0ixdpf43HqJ5fAB5nsbYFhyHpfiX1UO2EFJtSdbKEIbRmqcbNjG1 +tu1tjt6u8LI2coetLh/IYMbMfkyQz+eAUHLQCUb2R8BqLOL3hRqEsVTBo93UJlOs +VWC1fKaq+HOEWQIDAQABAoIBAAet23PagPQTjwAZcAlzjlvs5AMHQsj5gznqwSmR +ut3/e7SGrrOIXbv1iIQejZQ3w8CS/0MH/ttIRiRIaWTh9EDsjvKsU9FAxUNDiJTG +k3LCbTFCQ7kGiJWiu4XDCWMmwmLTRzLjlMjtr/+JS5eSVPcNKMGDI3D9K0xDLSxQ +u0DVigYgWOCWlejHCEU4yi6vBO0HlumWjVPelWb9GmihBDwCLUJtG0JA6H6rw+KS +i6SNXcMGVKfjEghChRp+HaMvLvMgU44Ptnj8jhlfBctXInBY1is1FfDSWxXdVbUM +1HdKXfV4A50GXSvJLiWP9ZZsaZ7NiBJK8IiJBXD72EFOzwECgYEA3RjnTJn9emzG +84eIHZQujWWt4Tk/wjeLJYOYtAZpF7R3/fYLVypX9Bsw1IbwZodq/jChTjMaUkYt +//FgUjF/t0uakEg1i+THPZvktNB8Q1E9NwHerB8HF/AD/jMALD+ejdLQ11Z4VScw +zyNmSvD9I84/sgpms5YVKSH9sqww2RkCgYEA9KYws3sTfRLc1hlsS25V6+Zg3ZCk +iGcp+zrxGC1gb2/PpRvEDBucZO21KbSRuQDavWIOZYl4fGu7s8wo2oF8RxOsHQsM +LJyjklruvtjnvuoft/bGAv2zLQkNaj+f7IgK6965gIxcLYL66UPCZZkTfL5CoJis +V0v2hBh1ES5bLUECgYEAuONeaLxNL9dO989akAGefDePFExfePYhshk91S2XLG+J ++CGMkjOioUsrpk3BMrwDSNU5zr8FP8/YH7OlrJYgCxN6CTWZMYb65hY7RskhYNnK +qvkxUBYSRH49mJDlkBsTZ93nLmvs7Kh9NHqRzBGCXjLXKPdxsrPKtj7qfENqBeEC +gYAC9dPXCCE3PTgw2wPlccNWZGY9qBdlkyH96TurmDj3gDnZ/JkFsHvW+M1dYNL2 +kx0Sd5JHBj/P+Zm+1jSUWEbBsWo+u7h8/bQ4/CKxanx7YefaWQESXjGB1P81jumH +einvqrVB6fDfmBsjIW/DvPNwafjyaoaDU+b6uDUKbS4rQQKBgCe0pvDl5lO8FM81 +NP7GoCIu1gKBS+us1sgYE65ZFmVXJ6b5DckvobXSjM60G2N5w2xaXEXJsnwMApf1 +SClQUsgNWcSXRwL+w0pIdyFKS25BSfwUNQ9n7QLJcYgmflbARTfB3He/10vbFzTp +G6ZAiKUp9bKFPzviII40AEPL2hPX +-----END PRIVATE KEY----- diff --git a/test/schema.tests.js b/test/schema.tests.js index 0a648f1..ebd553f 100644 --- a/test/schema.tests.js +++ b/test/schema.tests.js @@ -6,31 +6,31 @@ var PS_SUPPORTED = require('../lib/psSupported'); describe('schema', function() { describe('sign options', function() { - var cert_rsa_priv = fs.readFileSync(__dirname + '/rsa-private.pem'); var cert_ecdsa_priv = fs.readFileSync(__dirname + '/ecdsa-private.pem'); + var cert_secp384r1_priv = fs.readFileSync(__dirname + '/secp384r1-private.pem'); + var cert_secp521r1_priv = fs.readFileSync(__dirname + '/secp521r1-private.pem'); - function sign(options, secret) { - var isEcdsa = options.algorithm && options.algorithm.indexOf('ES') === 0; - jwt.sign({foo: 123}, secret || (isEcdsa ? cert_ecdsa_priv : cert_rsa_priv), options); + function sign(options, secretOrPrivateKey) { + jwt.sign({foo: 123}, secretOrPrivateKey, options); } it('should validate algorithm', function () { expect(function () { - sign({ algorithm: 'foo' }); + sign({ algorithm: 'foo' }, cert_rsa_priv); }).to.throw(/"algorithm" must be a valid string enum value/); - sign({ algorithm: 'none' }); - sign({algorithm: 'RS256'}); - sign({algorithm: 'RS384'}); - sign({algorithm: 'RS512'}); + sign({ algorithm: 'none' }, null); + sign({algorithm: 'RS256'}, cert_rsa_priv); + sign({algorithm: 'RS384'}, cert_rsa_priv); + sign({algorithm: 'RS512'}, cert_rsa_priv); if (PS_SUPPORTED) { - sign({algorithm: 'PS256'}); - sign({algorithm: 'PS384'}); - sign({algorithm: 'PS512'}); + sign({algorithm: 'PS256'}, cert_rsa_priv); + sign({algorithm: 'PS384'}, cert_rsa_priv); + sign({algorithm: 'PS512'}, cert_rsa_priv); } - sign({algorithm: 'ES256'}); - sign({algorithm: 'ES384'}); - sign({algorithm: 'ES512'}); + sign({algorithm: 'ES256'}, cert_ecdsa_priv); + sign({algorithm: 'ES384'}, cert_secp384r1_priv); + sign({algorithm: 'ES512'}, cert_secp521r1_priv); sign({algorithm: 'HS256'}, 'superSecret'); sign({algorithm: 'HS384'}, 'superSecret'); sign({algorithm: 'HS512'}, 'superSecret'); diff --git a/test/secp384r1-private.pem b/test/secp384r1-private.pem new file mode 100644 index 0000000..82336b6 --- /dev/null +++ b/test/secp384r1-private.pem @@ -0,0 +1,6 @@ +-----BEGIN EC PRIVATE KEY----- +MIGkAgEBBDCez58vZHVp+ArI7/fe835GAtRzE0AtrxGgQAY1U/uk2SQOaSw1ph61 +3Unr0ygS172gBwYFK4EEACKhZANiAARtwlnIqYqZxfiWR+/EM35nKHuLpOjUHiX1 +kEpSS03C9XlrBLNwLQfgjpYx9Qvqh26XAzTe74DYjcc748R+zZD2YAd3lV+OcdRE +U+DWm4j5E6dlOXzvmw/3qxUcg3rRgR4= +-----END EC PRIVATE KEY----- diff --git a/test/secp521r1-private.pem b/test/secp521r1-private.pem new file mode 100644 index 0000000..397a3df --- /dev/null +++ b/test/secp521r1-private.pem @@ -0,0 +1,7 @@ +-----BEGIN EC PRIVATE KEY----- +MIHcAgEBBEIBlWXKBKKCgTgf7+NS09TMv7/NO3RtMBn9xTe+46oNNNK405lrZ9mz +WYtlsYvkdsc2Cx3v5V8JegaCOM+XtAZ0MNKgBwYFK4EEACOhgYkDgYYABAFNzaM7 +Zb9ug0p5KaZb5mjHrIshoVJSHaOXGtcjLVUakYVk0v9VsE+FKqyuLYcORUuAZdxl +ITAlC5e5JZ0o8NEKbAE+8oOrePrItR3IFBtWO15p7qiRa2dBB8oQklFrmQaJYn4K +fDV0hYpfu6ahpRNu2akR7aMXL/vXrptCH/n64q9KjA== +-----END EC PRIVATE KEY----- diff --git a/test/validateAsymmetricKey.tests.js b/test/validateAsymmetricKey.tests.js new file mode 100644 index 0000000..e0194b8 --- /dev/null +++ b/test/validateAsymmetricKey.tests.js @@ -0,0 +1,142 @@ +const validateAsymmetricKey = require('../lib/validateAsymmetricKey'); +const PS_SUPPORTED = require('../lib/psSupported'); +const ASYMMETRIC_KEY_DETAILS_SUPPORTED = require('../lib/asymmetricKeyDetailsSupported'); +const RSA_PSS_KEY_DETAILS_SUPPORTED = require('../lib/rsaPssKeyDetailsSupported'); +const fs = require('fs'); +const path = require('path'); +const { createPrivateKey } = require('crypto'); +const expect = require('chai').expect; + +function loadKey(filename) { + return createPrivateKey( + fs.readFileSync(path.join(__dirname, filename)) + ); +} + +const algorithmParams = { + RS256: { + invalidPrivateKey: loadKey('secp384r1-private.pem') + }, + ES256: { + invalidPrivateKey: loadKey('priv.pem') + } +}; + +if (PS_SUPPORTED) { + algorithmParams.PS256 = { + invalidPrivateKey: loadKey('secp384r1-private.pem') + }; +} + +describe('Asymmetric key validation', function() { + Object.keys(algorithmParams).forEach(function(algorithm) { + describe(algorithm, function() { + const keys = algorithmParams[algorithm]; + + describe('when validating a key with an invalid private key type', function () { + it('should throw an error', function () { + const expectedErrorMessage = /"alg" parameter for "[\w\d-]+" key type must be one of:/; + + expect(function() { + validateAsymmetricKey(algorithm, keys.invalidPrivateKey); + }).to.throw(expectedErrorMessage); + }); + }); + }); + }); + + describe('when the function has missing parameters', function() { + it('should pass the validation if no key has been provided', function() { + const algorithm = 'ES256'; + validateAsymmetricKey(algorithm); + }); + + it('should pass the validation if no algorithm has been provided', function() { + const key = loadKey('dsa-private.pem'); + validateAsymmetricKey(null, key); + }); + }); + + describe('when validating a key with an unsupported type', function () { + it('should throw an error', function() { + const algorithm = 'RS256'; + const key = loadKey('dsa-private.pem'); + const expectedErrorMessage = 'Unknown key type "dsa".'; + + expect(function() { + validateAsymmetricKey(algorithm, key); + }).to.throw(expectedErrorMessage); + }); + }); + + describe('Elliptic curve algorithms', function () { + const curvesAlgorithms = [ + { algorithm: 'ES256', curve: 'prime256v1' }, + { algorithm: 'ES384', curve: 'secp384r1' }, + { algorithm: 'ES512', curve: 'secp521r1' }, + ]; + + const curvesKeys = [ + { curve: 'prime256v1', key: loadKey('prime256v1-private.pem') }, + { curve: 'secp384r1', key: loadKey('secp384r1-private.pem') }, + { curve: 'secp521r1', key: loadKey('secp521r1-private.pem') } + ]; + + describe('when validating keys generated using Elliptic Curves', function () { + curvesAlgorithms.forEach(function(curveAlgorithm) { + curvesKeys + .forEach((curveKeys) => { + if (curveKeys.curve !== curveAlgorithm.curve) { + if (ASYMMETRIC_KEY_DETAILS_SUPPORTED) { + it(`should throw an error when validating an ${curveAlgorithm.algorithm} token for key with curve ${curveKeys.curve}`, function() { + expect(() => { + validateAsymmetricKey(curveAlgorithm.algorithm, curveKeys.key); + }).to.throw(`"alg" parameter "${curveAlgorithm.algorithm}" requires curve "${curveAlgorithm.curve}".`); + }); + } else { + it(`should pass the validation for incorrect keys if the Node version does not support checking the key's curve name`, function() { + expect(() => { + validateAsymmetricKey(curveAlgorithm.algorithm, curveKeys.key); + }).not.to.throw(); + }); + } + } else { + it(`should accept an ${curveAlgorithm.algorithm} token for key with curve ${curveKeys.curve}`, function() { + expect(() => { + validateAsymmetricKey(curveAlgorithm.algorithm, curveKeys.key); + }).not.to.throw(); + }); + } + }); + }); + }); + }); + + if (RSA_PSS_KEY_DETAILS_SUPPORTED) { + describe('RSA-PSS algorithms', function () { + const key = loadKey('rsa-pss-private.pem'); + + it(`it should throw an error when validating a key with wrong RSA-RSS parameters`, function () { + const algorithm = 'PS512'; + expect(function() { + validateAsymmetricKey(algorithm, key); + }).to.throw('Invalid key for this operation, its RSA-PSS parameters do not meet the requirements of "alg" PS512') + }); + + it(`it should throw an error when validating a key with invalid salt length`, function () { + const algorithm = 'PS256'; + const shortSaltKey = loadKey('rsa-pss-invalid-salt-length-private.pem'); + expect(function() { + validateAsymmetricKey(algorithm, shortSaltKey); + }).to.throw('Invalid key for this operation, its RSA-PSS parameter saltLength does not meet the requirements of "alg" PS256.') + }); + + it(`it should pass the validation when the key matches all the requirements for the algorithm`, function () { + expect(function() { + const algorithm = 'PS256'; + validateAsymmetricKey(algorithm, key); + }).not.to.throw() + }); + }); + } +}); diff --git a/test/verify.tests.js b/test/verify.tests.js index 9ef24e4..8850075 100644 --- a/test/verify.tests.js +++ b/test/verify.tests.js @@ -1,22 +1,22 @@ -var jwt = require('../index'); -var jws = require('jws'); -var fs = require('fs'); -var path = require('path'); -var sinon = require('sinon'); -var JsonWebTokenError = require('../lib/JsonWebTokenError'); +const jwt = require('../index'); +const jws = require('jws'); +const fs = require('fs'); +const path = require('path'); +const sinon = require('sinon'); +const JsonWebTokenError = require('../lib/JsonWebTokenError'); -var assert = require('chai').assert; -var expect = require('chai').expect; +const assert = require('chai').assert; +const expect = require('chai').expect; describe('verify', function() { - var pub = fs.readFileSync(path.join(__dirname, 'pub.pem')); - var priv = fs.readFileSync(path.join(__dirname, 'priv.pem')); + const pub = fs.readFileSync(path.join(__dirname, 'pub.pem')); + const priv = fs.readFileSync(path.join(__dirname, 'priv.pem')); it('should first assume JSON claim set', function (done) { - var header = { alg: 'RS256' }; - var payload = { iat: Math.floor(Date.now() / 1000 ) }; + const header = { alg: 'RS256' }; + const payload = { iat: Math.floor(Date.now() / 1000 ) }; - var signed = jws.sign({ + const signed = jws.sign({ header: header, payload: payload, secret: priv, @@ -31,10 +31,10 @@ describe('verify', function() { }); it('should not be able to verify unsigned token', function () { - var header = { alg: 'none' }; - var payload = { iat: Math.floor(Date.now() / 1000 ) }; + const header = { alg: 'none' }; + const payload = { iat: Math.floor(Date.now() / 1000 ) }; - var signed = jws.sign({ + const signed = jws.sign({ header: header, payload: payload, secret: 'secret', @@ -47,10 +47,10 @@ describe('verify', function() { }); it('should not be able to verify unsigned token', function () { - var header = { alg: 'none' }; - var payload = { iat: Math.floor(Date.now() / 1000 ) }; + const header = { alg: 'none' }; + const payload = { iat: Math.floor(Date.now() / 1000 ) }; - var signed = jws.sign({ + const signed = jws.sign({ header: header, payload: payload, secret: 'secret', @@ -63,10 +63,10 @@ describe('verify', function() { }); it('should be able to verify unsigned token when none is specified', function (done) { - var header = { alg: 'none' }; - var payload = { iat: Math.floor(Date.now() / 1000 ) }; + const header = { alg: 'none' }; + const payload = { iat: Math.floor(Date.now() / 1000 ) }; - var signed = jws.sign({ + const signed = jws.sign({ header: header, payload: payload, secret: 'secret', @@ -99,11 +99,11 @@ describe('verify', function() { }); describe('secret or token as callback', function () { - var token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MzcwMTg1ODIsImV4cCI6MTQzNzAxODU5Mn0.3aR3vocmgRpG05rsI9MpR6z2T_BGtMQaPq2YR6QaroU'; - var key = 'key'; + const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MzcwMTg1ODIsImV4cCI6MTQzNzAxODU5Mn0.3aR3vocmgRpG05rsI9MpR6z2T_BGtMQaPq2YR6QaroU'; + const key = 'key'; - var payload = { foo: 'bar', iat: 1437018582, exp: 1437018592 }; - var options = {algorithms: ['HS256'], ignoreExpiration: true}; + const payload = { foo: 'bar', iat: 1437018582, exp: 1437018592 }; + const options = {algorithms: ['HS256'], ignoreExpiration: true}; it('without callback', function (done) { jwt.verify(token, key, options, function (err, p) { @@ -114,7 +114,7 @@ describe('verify', function() { }); it('simple callback', function (done) { - var keyFunc = function(header, callback) { + const keyFunc = function(header, callback) { assert.deepEqual(header, { alg: 'HS256', typ: 'JWT' }); callback(undefined, key); @@ -128,7 +128,7 @@ describe('verify', function() { }); it('should error if called synchronously', function (done) { - var keyFunc = function(header, callback) { + const keyFunc = function(header, callback) { callback(undefined, key); }; @@ -140,7 +140,7 @@ describe('verify', function() { }); it('simple error', function (done) { - var keyFunc = function(header, callback) { + const keyFunc = function(header, callback) { callback(new Error('key not found')); }; @@ -153,7 +153,7 @@ describe('verify', function() { }); it('delayed callback', function (done) { - var keyFunc = function(header, callback) { + const keyFunc = function(header, callback) { setTimeout(function() { callback(undefined, key); }, 25); @@ -167,7 +167,7 @@ describe('verify', function() { }); it('delayed error', function (done) { - var keyFunc = function(header, callback) { + const keyFunc = function(header, callback) { setTimeout(function() { callback(new Error('key not found')); }, 25); @@ -184,17 +184,17 @@ describe('verify', function() { describe('expiration', function () { // { foo: 'bar', iat: 1437018582, exp: 1437018592 } - var token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MzcwMTg1ODIsImV4cCI6MTQzNzAxODU5Mn0.3aR3vocmgRpG05rsI9MpR6z2T_BGtMQaPq2YR6QaroU'; - var key = 'key'; + const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MzcwMTg1ODIsImV4cCI6MTQzNzAxODU5Mn0.3aR3vocmgRpG05rsI9MpR6z2T_BGtMQaPq2YR6QaroU'; + const key = 'key'; - var clock; + let clock; afterEach(function () { try { clock.restore(); } catch (e) {} }); it('should error on expired token', function (done) { clock = sinon.useFakeTimers(1437018650000); // iat + 58s, exp + 48s - var options = {algorithms: ['HS256']}; + const options = {algorithms: ['HS256']}; jwt.verify(token, key, options, function (err, p) { assert.equal(err.name, 'TokenExpiredError'); @@ -208,7 +208,7 @@ describe('verify', function() { it('should not error on expired token within clockTolerance interval', function (done) { clock = sinon.useFakeTimers(1437018594000); // iat + 12s, exp + 2s - var options = {algorithms: ['HS256'], clockTolerance: 5 } + const options = {algorithms: ['HS256'], clockTolerance: 5 } jwt.verify(token, key, options, function (err, p) { assert.isNull(err); @@ -218,16 +218,16 @@ describe('verify', function() { }); describe('option: clockTimestamp', function () { - var clockTimestamp = 1000000000; + const clockTimestamp = 1000000000; it('should verify unexpired token relative to user-provided clockTimestamp', function (done) { - var token = jwt.sign({foo: 'bar', iat: clockTimestamp, exp: clockTimestamp + 1}, key); + const token = jwt.sign({foo: 'bar', iat: clockTimestamp, exp: clockTimestamp + 1}, key); jwt.verify(token, key, {clockTimestamp: clockTimestamp}, function (err) { assert.isNull(err); done(); }); }); it('should error on expired token relative to user-provided clockTimestamp', function (done) { - var token = jwt.sign({foo: 'bar', iat: clockTimestamp, exp: clockTimestamp + 1}, key); + const token = jwt.sign({foo: 'bar', iat: clockTimestamp, exp: clockTimestamp + 1}, key); jwt.verify(token, key, {clockTimestamp: clockTimestamp + 1}, function (err, p) { assert.equal(err.name, 'TokenExpiredError'); assert.equal(err.message, 'jwt expired'); @@ -238,7 +238,7 @@ describe('verify', function() { }); }); it('should verify clockTimestamp is a number', function (done) { - var token = jwt.sign({foo: 'bar', iat: clockTimestamp, exp: clockTimestamp + 1}, key); + const token = jwt.sign({foo: 'bar', iat: clockTimestamp, exp: clockTimestamp + 1}, key); jwt.verify(token, key, {clockTimestamp: 'notANumber'}, function (err, p) { assert.equal(err.name, 'JsonWebTokenError'); assert.equal(err.message,'clockTimestamp must be a number'); @@ -250,10 +250,10 @@ describe('verify', function() { describe('option: maxAge and clockTimestamp', function () { // { foo: 'bar', iat: 1437018582, exp: 1437018800 } exp = iat + 218s - var token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MzcwMTg1ODIsImV4cCI6MTQzNzAxODgwMH0.AVOsNC7TiT-XVSpCpkwB1240izzCIJ33Lp07gjnXVpA'; + const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MzcwMTg1ODIsImV4cCI6MTQzNzAxODgwMH0.AVOsNC7TiT-XVSpCpkwB1240izzCIJ33Lp07gjnXVpA'; it('cannot be more permissive than expiration', function (done) { - var clockTimestamp = 1437018900; // iat + 318s (exp: iat + 218s) - var options = {algorithms: ['HS256'], clockTimestamp: clockTimestamp, maxAge: '1000y'}; + const clockTimestamp = 1437018900; // iat + 318s (exp: iat + 218s) + const options = {algorithms: ['HS256'], clockTimestamp: clockTimestamp, maxAge: '1000y'}; jwt.verify(token, key, options, function (err, p) { // maxAge not exceded, but still expired @@ -267,4 +267,35 @@ describe('verify', function() { }); }); }); + + describe('when verifying a token with an unsupported public key type', function () { + it('should throw an error', function() { + const token = 'eyJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE2Njk5OTAwMDN9.YdjFWJtPg_9nccMnTfQyesWQ0UX-GsWrfCGit_HqjeIkNjoV6dkAJ8AtbnVEhA4oxwqSXx6ilMOfHEjmMlPtyyyVKkWKQHcIWYnqPbNSEv8a7Men8KhJTIWb4sf5YbhgSCpNvU_VIZjLO1Z0PzzgmEikp0vYbxZFAbCAlZCvUlcIc-kdjIRCnDJe0BBrYRxNLEJtYsf7D1yFIFIqw8-VP87yZdExA4eHsTaE84SgnL24ZK5h5UooDx-IRNd_rrMyio8kNy63grVxCWOtkXZ26iZk6v-HMsnBqxvUwR6-8wfaWrcpADkyUO1q3SNsoTdwtflbvfwgjo3uve0IvIzHMw'; + const key = fs.readFileSync(path.join(__dirname, 'dsa-public.pem')); + + expect(function() { + jwt.verify(token, key); + }).to.throw('Unknown key type "dsa".'); + }); + }); + + describe('when verifying a token with an incorrect public key type', function () { + it('should throw a validation error if key validation is enabled', function() { + const token = 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJwYXkiOiJsb2FkIiwiaWF0IjoxNjcwMjMwNDE2fQ.7TYP8SB_9Tw1fNIfuG60b4tvoLPpDAVBQpV1oepnuKwjUz8GOw4fRLzclo0Q2YAXisJ3zIYMEFsHpYrflfoZJQ'; + const key = fs.readFileSync(path.join(__dirname, 'rsa-public.pem')); + + expect(function() { + jwt.verify(token, key, { algorithms: ['ES256'] }); + }).to.throw('"alg" parameter for "rsa" key type must be one of: RS256, PS256, RS384, PS384, RS512, PS512.'); + }); + + it('should throw an unknown error if key validation is disabled', function() { + const token = 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJwYXkiOiJsb2FkIiwiaWF0IjoxNjcwMjMwNDE2fQ.7TYP8SB_9Tw1fNIfuG60b4tvoLPpDAVBQpV1oepnuKwjUz8GOw4fRLzclo0Q2YAXisJ3zIYMEFsHpYrflfoZJQ'; + const key = fs.readFileSync(path.join(__dirname, 'rsa-public.pem')); + + expect(function() { + jwt.verify(token, key, { algorithms: ['ES256'], allowInvalidAsymmetricKeyTypes: true }); + }).to.not.throw('"alg" parameter for "rsa" key type must be one of: RS256, PS256, RS384, PS384, RS512, PS512.'); + }); + }); }); diff --git a/verify.js b/verify.js index 0b649db..cdbfdc4 100644 --- a/verify.js +++ b/verify.js @@ -3,6 +3,7 @@ const NotBeforeError = require('./lib/NotBeforeError'); const TokenExpiredError = require('./lib/TokenExpiredError'); const decode = require('./decode'); const timespan = require('./lib/timespan'); +const validateAsymmetricKey = require('./lib/validateAsymmetricKey'); const PS_SUPPORTED = require('./lib/psSupported'); const jws = require('jws'); const {KeyObject, createSecretKey, createPublicKey} = require("crypto"); @@ -49,6 +50,10 @@ module.exports = function (jwtString, secretOrPublicKey, options, callback) { return done(new JsonWebTokenError('nonce must be a non-empty string')); } + if (options.allowInvalidAsymmetricKeyTypes !== undefined && typeof options.allowInvalidAsymmetricKeyTypes !== 'boolean') { + return done(new JsonWebTokenError('allowInvalidAsymmetricKeyTypes must be a boolean')); + } + const clockTimestamp = options.clockTimestamp || Math.floor(Date.now() / 1000); if (!jwtString){ @@ -146,6 +151,14 @@ module.exports = function (jwtString, secretOrPublicKey, options, callback) { return done(new JsonWebTokenError((`secretOrPublicKey must be an asymmetric key when using ${header.alg}`))) } + if (!options.allowInvalidAsymmetricKeyTypes) { + try { + validateAsymmetricKey(header.alg, secretOrPublicKey); + } catch (e) { + return done(e); + } + } + let valid; try {