From 7c1cab196edc409ec6cc4741bdf7e06c5aaf5dab Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Mon, 4 May 2020 22:37:11 +0200 Subject: [PATCH] feat: add opt-in objects to verify using embedded JWS Header public keys --- docs/README.md | 24 +++++ lib/help/key_object.js | 2 +- lib/help/key_utils.js | 2 +- lib/jwk/index.js | 6 +- lib/jwk/key/base.js | 2 +- lib/jwk/key/embedded.jwk.js | 27 ++++++ lib/jwk/key/embedded.x5c.js | 27 ++++++ lib/jwk/key/none.js | 3 +- lib/jwks/keystore.js | 6 +- lib/jws/verify.js | 22 +++++ test/jwk/embedded.test.js | 189 ++++++++++++++++++++++++++++++++++++ test/jwk/none.test.js | 20 ++++ types/index.d.ts | 41 +++++--- 13 files changed, 351 insertions(+), 20 deletions(-) create mode 100644 lib/jwk/key/embedded.jwk.js create mode 100644 lib/jwk/key/embedded.x5c.js create mode 100644 test/jwk/embedded.test.js create mode 100644 test/jwk/none.test.js diff --git a/docs/README.md b/docs/README.md index caed72e846..1ac2971a37 100644 --- a/docs/README.md +++ b/docs/README.md @@ -52,6 +52,8 @@ If you or your business use `jose`, please consider becoming a [sponsor][support - [JWK.generateSync(kty[, crvOrSize[, options[, private]]])](#jwkgeneratesynckty-crvorsize-options-private) - [JWK.isKey(object)](#jwkiskeyobject) - [JWK.None](#jwknone) +- [JWK.EmbeddedJWK](#jwkembeddedjwk) +- [JWK.EmbeddedX5C](#jwkembeddedx5c) All sign and encrypt operations require `` or `JWK.asKey()` compatible input. @@ -623,6 +625,28 @@ JWS.verify(unsecuredJWS, None) --- +#### `JWK.EmbeddedJWK` + +`JWK.EmbeddedJWK` is a special key object that can be used with the JWS/JWT verify operations +whenever you want to opt-in to verify signatures with a public key embedded in the JWS Header `jwk` +parameter. It is recommended to combine this with the verify `algorithms` option to whitelist +JWS algorithms to accept as well as the `complete` option set to `true` if you need to work with the +instantiated `JWK.Key` from the token. + +--- + +#### `JWK.EmbeddedX5C` + +`JWK.EmbeddedX5C` is a special key object that can be used with the JWS/JWT verify operations +whenever you want to opt-in to verify signatures with a public key embedded in the first JWS Header +`x5c` parameter. It is recommended to combine this with the verify `algorithms` option to whitelist +JWS algorithms to accept as well as the `complete` option set to `true` if you need to work with the +instantiated `JWK.Key` from the token. ⚠️ the x5c members are all validated to be certificates but +their chain or trust is not validated. Unfortunately Node.js does not have any good tools to do that +reliably. + +--- + ## JWKS (JSON Web Key Set) diff --git a/lib/help/key_object.js b/lib/help/key_object.js index b612301285..1fcaae07f2 100644 --- a/lib/help/key_object.js +++ b/lib/help/key_object.js @@ -30,7 +30,7 @@ if (keyObjectSupported) { } const pemToDer = pem => Buffer.from(pem.replace(/(?:-----(?:BEGIN|END)(?: (?:RSA|EC))? (?:PRIVATE|PUBLIC) KEY-----|\s)/g, ''), 'base64') - const derToPem = (der, label) => `-----BEGIN ${label}-----${EOL}${der.toString('base64').match(/.{1,64}/g).join(EOL)}${EOL}-----END ${label}-----` + const derToPem = (der, label) => `-----BEGIN ${label}-----${EOL}${(der.toString('base64').match(/.{1,64}/g) || []).join(EOL)}${EOL}-----END ${label}-----` const unsupported = (input) => { const label = typeof input === 'string' ? input : `OID ${input.join('.')}` throw new errors.JOSENotSupported(`${label} is not supported in your Node.js runtime version`) diff --git a/lib/help/key_utils.js b/lib/help/key_utils.js index 350da73382..887855ea00 100644 --- a/lib/help/key_utils.js +++ b/lib/help/key_utils.js @@ -10,7 +10,7 @@ const asn1 = require('./asn1') const computePrimes = require('./rsa_primes') const { OKP_CURVES, EC_CURVES } = require('../registry') -const formatPem = (base64pem, descriptor) => `-----BEGIN ${descriptor} KEY-----${EOL}${base64pem.match(/.{1,64}/g).join(EOL)}${EOL}-----END ${descriptor} KEY-----` +const formatPem = (base64pem, descriptor) => `-----BEGIN ${descriptor} KEY-----${EOL}${(base64pem.match(/.{1,64}/g) || []).join(EOL)}${EOL}-----END ${descriptor} KEY-----` const okpToJWK = { private (crv, keyObject) { diff --git a/lib/jwk/index.js b/lib/jwk/index.js index e42b738332..0c95c4f043 100644 --- a/lib/jwk/index.js +++ b/lib/jwk/index.js @@ -1,5 +1,7 @@ const Key = require('./key/base') const None = require('./key/none') +const EmbeddedJWK = require('./key/embedded.jwk') +const EmbeddedX5C = require('./key/embedded.x5c') const importKey = require('./import') const generate = require('./generate') @@ -7,7 +9,9 @@ module.exports = { ...generate, asKey: importKey, isKey: input => input instanceof Key, - None + None, + EmbeddedJWK, + EmbeddedX5C } /* deprecated */ diff --git a/lib/jwk/key/base.js b/lib/jwk/key/base.js index 45b2f6c295..f7e9b8f8bb 100644 --- a/lib/jwk/key/base.js +++ b/lib/jwk/key/base.js @@ -61,7 +61,7 @@ class Key { let publicKey try { publicKey = createPublicKey({ - key: `-----BEGIN CERTIFICATE-----${EOL}${cert.match(/.{1,64}/g).join(EOL)}${EOL}-----END CERTIFICATE-----`, format: 'pem' + key: `-----BEGIN CERTIFICATE-----${EOL}${(cert.match(/.{1,64}/g) || []).join(EOL)}${EOL}-----END CERTIFICATE-----`, format: 'pem' }) } catch (err) { throw new errors.JWKInvalid(`\`x5c\` member at index ${i} is not a valid base64-encoded DER PKIX certificate`) diff --git a/lib/jwk/key/embedded.jwk.js b/lib/jwk/key/embedded.jwk.js new file mode 100644 index 0000000000..dd7f59ff55 --- /dev/null +++ b/lib/jwk/key/embedded.jwk.js @@ -0,0 +1,27 @@ +const { inspect } = require('util') + +const Key = require('./base') + +class EmbeddedJWK extends Key { + constructor () { + super({ type: 'embedded' }) + Object.defineProperties(this, { + kid: { value: undefined }, + kty: { value: undefined }, + thumbprint: { value: undefined }, + toJWK: { value: undefined }, + toPEM: { value: undefined } + }) + } + + /* c8 ignore next 3 */ + [inspect.custom] () { + return 'Embedded.JWK {}' + } + + algorithms () { + return new Set() + } +} + +module.exports = new EmbeddedJWK() diff --git a/lib/jwk/key/embedded.x5c.js b/lib/jwk/key/embedded.x5c.js new file mode 100644 index 0000000000..48cfb106fe --- /dev/null +++ b/lib/jwk/key/embedded.x5c.js @@ -0,0 +1,27 @@ +const { inspect } = require('util') + +const Key = require('./base') + +class EmbeddedX5C extends Key { + constructor () { + super({ type: 'embedded' }) + Object.defineProperties(this, { + kid: { value: undefined }, + kty: { value: undefined }, + thumbprint: { value: undefined }, + toJWK: { value: undefined }, + toPEM: { value: undefined } + }) + } + + /* c8 ignore next 3 */ + [inspect.custom] () { + return 'Embedded.X5C {}' + } + + algorithms () { + return new Set() + } +} + +module.exports = new EmbeddedX5C() diff --git a/lib/jwk/key/none.js b/lib/jwk/key/none.js index 257b4e3c2f..eb53316bd0 100644 --- a/lib/jwk/key/none.js +++ b/lib/jwk/key/none.js @@ -7,6 +7,7 @@ class NoneKey extends Key { super({ type: 'unsecured' }, { alg: 'none' }) Object.defineProperties(this, { kid: { value: undefined }, + kty: { value: undefined }, thumbprint: { value: undefined }, toJWK: { value: undefined }, toPEM: { value: undefined } @@ -30,4 +31,4 @@ class NoneKey extends Key { } } -module.exports = new NoneKey({ type: 'unsecured' }, { alg: 'none' }) +module.exports = new NoneKey() diff --git a/lib/jwks/keystore.js b/lib/jwks/keystore.js index e1ed0e8e53..15a6fbb11e 100644 --- a/lib/jwks/keystore.js +++ b/lib/jwks/keystore.js @@ -3,7 +3,7 @@ const { deprecate, inspect } = require('util') const isObject = require('../help/is_object') const { generate, generateSync } = require('../jwk/generate') const { USES_MAPPING } = require('../help/consts') -const { None, isKey, asKey: importKey } = require('../jwk') +const { isKey, asKey: importKey } = require('../jwk') const keyscore = (key, { alg, use, ops }) => { let score = 0 @@ -35,7 +35,7 @@ class KeyStore { return acc }, []) } - if (keys.some(k => !isKey(k) || k === None)) { + if (keys.some(k => !isKey(k) || !k.kty)) { throw new TypeError('all keys must be instances of a key instantiated by JWK.asKey') } @@ -107,7 +107,7 @@ class KeyStore { } add (key) { - if (!isKey(key) || key === None) { + if (!isKey(key) || !key.kty) { throw new TypeError('key must be an instance of a key instantiated by JWK.asKey') } diff --git a/lib/jws/verify.js b/lib/jws/verify.js index 53a1fe0ec7..4caee7949b 100644 --- a/lib/jws/verify.js +++ b/lib/jws/verify.js @@ -1,10 +1,14 @@ +const { EOL } = require('os') + const base64url = require('../help/base64url') const isDisjoint = require('../help/is_disjoint') +const isObject = require('../help/is_object') let validateCrit = require('../help/validate_crit') const getKey = require('../help/get_key') const { KeyStore } = require('../jwks') const errors = require('../errors') const { check, verify } = require('../jwa') +const JWK = require('../jwk') const { detect: resolveSerialization } = require('./serializers') @@ -125,6 +129,24 @@ const jwsVerify = (skipDisjointCheck, serialization, jws, key, { crit = [], comp } } + if (key === JWK.EmbeddedJWK) { + if (!isObject(combinedHeader.jwk)) { + throw new errors.JWSInvalid('JWS Header Parameter "jwk" must be a JSON object') + } + key = JWK.asKey(combinedHeader.jwk) + if (key.type !== 'public') { + throw new errors.JWSInvalid('JWS Header Parameter "jwk" must be a public key') + } + } else if (key === JWK.EmbeddedX5C) { + if (!Array.isArray(combinedHeader.x5c) || !combinedHeader.x5c.length || combinedHeader.x5c.some(c => typeof c !== 'string' || !c)) { + throw new errors.JWSInvalid('JWS Header Parameter "x5c" must be a JSON array of certificate value strings') + } + key = JWK.asKey( + `-----BEGIN CERTIFICATE-----${EOL}${(combinedHeader.x5c[0].match(/.{1,64}/g) || []).join(EOL)}${EOL}-----END CERTIFICATE-----`, + { x5c: combinedHeader.x5c } + ) + } + check(key, 'verify', alg) const toBeVerified = Buffer.concat([ diff --git a/test/jwk/embedded.test.js b/test/jwk/embedded.test.js new file mode 100644 index 0000000000..7fc1283c00 --- /dev/null +++ b/test/jwk/embedded.test.js @@ -0,0 +1,189 @@ +const test = require('ava') + +const { errors, JWK, JWS, JWT, JWKS } = require('../..') +const { keyObjectSupported } = require('../../lib/help/runtime_support') + +test('JWK.EmbeddedJWK', t => { + const k = JWK.EmbeddedJWK + t.truthy(k) + t.true(JWK.isKey(k)) + t.is(k.kty, undefined) + for (const prop of ['kid', 'kty', 'thumbprint', 'toJWK', 'toPEM']) { + k[prop] = 'foo' + t.is(k[prop], undefined) + } + t.deepEqual([...k.algorithms()], []) + k.type = 'foo' + t.is(k.type, 'embedded') + t.throws(() => new JWKS.KeyStore(k), { instanceOf: TypeError }) + const ks = new JWKS.KeyStore() + t.throws(() => ks.add(k), { instanceOf: TypeError }) +}) + +test('JWK.EmbeddedJWK JWS.verify pass', async t => { + const key = await JWK.generate('EC', 'P-256') + const { kid, ...jwk } = key.toJWK() + const jws = JWS.sign('foo', key, { jwk }) + t.notThrows(() => JWS.verify(jws, JWK.EmbeddedJWK)) + t.throws( + () => JWS.verify(jws, JWK.EmbeddedJWK, { algorithms: ['EdDSA'] }), + { instanceOf: errors.JOSEAlgNotWhitelisted, code: 'ERR_JOSE_ALG_NOT_WHITELISTED', message: 'alg not whitelisted' } + ) + const { key: embedded } = JWS.verify(jws, JWK.EmbeddedJWK, { complete: true }) + t.false(key === embedded) + t.deepEqual(key.toJWK(), embedded.toJWK()) +}) + +test('JWK.EmbeddedJWK JWT.verify pass', async t => { + const key = await JWK.generate('EC', 'P-256') + const { kid, ...jwk } = key.toJWK() + const jws = JWT.sign({}, key, { header: { jwk } }) + t.notThrows(() => JWT.verify(jws, JWK.EmbeddedJWK)) + t.throws( + () => JWT.verify(jws, JWK.EmbeddedJWK, { algorithms: ['EdDSA'] }), + { instanceOf: errors.JOSEAlgNotWhitelisted, code: 'ERR_JOSE_ALG_NOT_WHITELISTED', message: 'alg not whitelisted' } + ) + const { key: embedded } = JWT.verify(jws, JWK.EmbeddedJWK, { complete: true }) + t.false(key === embedded) + t.deepEqual(key.toJWK(), embedded.toJWK()) +}) + +test('JWK.EmbeddedJWK key must be a public key', async t => { + const key = await JWK.generate('EC', 'P-256') + const { kid, ...jwk } = key.toJWK(true) + { + const jws = JWS.sign('foo', key, { jwk }) + t.throws( + () => JWS.verify(jws, JWK.EmbeddedJWK), + { instanceOf: errors.JWSInvalid, code: 'ERR_JWS_INVALID', message: 'JWS Header Parameter "jwk" must be a public key' } + ) + } + { + const jws = JWS.sign('foo', key, { jwk: { kty: 'oct', k: 'foo' } }) + t.throws( + () => JWS.verify(jws, JWK.EmbeddedJWK), + { instanceOf: errors.JWSInvalid, code: 'ERR_JWS_INVALID', message: 'JWS Header Parameter "jwk" must be a public key' } + ) + } + { + const jws = JWS.sign('foo', key, { jwk: { kty: 'oct' } }) + t.throws( + () => JWS.verify(jws, JWK.EmbeddedJWK), + { instanceOf: errors.JWSInvalid, code: 'ERR_JWS_INVALID', message: 'JWS Header Parameter "jwk" must be a public key' } + ) + } + { + const jws = JWS.sign('foo', key, { jwk: { kty: 'foo' } }) + t.throws( + () => JWS.verify(jws, JWK.EmbeddedJWK), + { instanceOf: errors.JOSENotSupported, code: 'ERR_JOSE_NOT_SUPPORTED', message: 'unsupported key type: foo' } + ) + } + { + const invalidEc = key.toJWK() + delete invalidEc.y + const jws = JWS.sign('foo', key, { jwk: invalidEc }) + t.throws( + () => JWS.verify(jws, JWK.EmbeddedJWK), + { instanceOf: errors.JWKImportFailed, code: 'ERR_JWK_IMPORT_FAILED', message: 'key import failed' } + ) + } +}) + +test('JWK.EmbeddedJWK key invalid inputs', async t => { + const key = await JWK.generate('EC', 'P-256') + for (const jwk of [undefined, '', null, false, true, 1, 3.14, 'pi']) { + const jws = JWS.sign('foo', key, { jwk }) + t.throws( + () => JWS.verify(jws, JWK.EmbeddedJWK), + { instanceOf: errors.JWSInvalid, code: 'ERR_JWS_INVALID', message: 'JWS Header Parameter "jwk" must be a JSON object' } + ) + } +}) + +if (keyObjectSupported) { + test('JWK.EmbeddedX5C', t => { + const k = JWK.EmbeddedX5C + t.truthy(k) + t.true(JWK.isKey(k)) + t.is(k.kty, undefined) + for (const prop of ['kid', 'kty', 'thumbprint', 'toJWK', 'toPEM']) { + k[prop] = 'foo' + t.is(k[prop], undefined) + } + t.deepEqual([...k.algorithms()], []) + k.type = 'foo' + t.is(k.type, 'embedded') + t.throws(() => new JWKS.KeyStore(k), { instanceOf: TypeError }) + const ks = new JWKS.KeyStore() + t.throws(() => ks.add(k), { instanceOf: TypeError }) + }) + + const rsa = { + e: 'AQAB', + n: 'u4kSwZDMd93b1fvd6CXUfHa-rF0DBd03tCCpWN31giKCskP09c7VigwkyHu34X__1rA7CNMaSrXQn4ChkhulSxzQyojBc3t06AjyKe_Nzpd72zaGFjaLfN-C2U5QmmaXn_2dOiQTH3aTaHDA5I8zd7ZEwrln9G6DD9KtbAcal-RWN_XT-dD-hHUSH4X4iHIvVC1El6lOtu9yjpmQtAvU3mpvxKK6AUGEA9wCWmIEcpfosOCpgHiwVeuPwJwAmuHRFA-h5N4wWw1KQuW66ocgeTzwKZ33DuMWeLap3AEeDVErInAwPPjzLSj3i3DvtveGlGZQH10wAZMAQrcUhHS06Q', + d: 'e_PUztXbH5snc58fBBMFCCMgUiLEHbsi108DP7atUA9pXVRnc5T7NVxjb5O-bTDCM--VhXaqmRjlRJerszvMnAH2yvdrDd5a3gcTsL5MxLEBb1nxdHsm5SmCfgkyY2tN6rShmE1BynkAY3arOCaieQyjFCWh3UCyJeI1OALWA_AJP1kOrmrM9Cpd3FYNkKN163_D7Nv5g2PuMMO1T6Zx8xdgC1C6OxXfmVpvNtOI4pnkODRcQhTspHJLzGTtp2yYPRHCsBhF7i3XsbQ4D8a8t_vZ3yLt-3ZkJnHnRQeCeyBlALUcnFFo43CA96ohOk6NhjYNbi8uJbZyxWVXllwV5Q', + p: '5kZFOcFl6jrZ7XQWDoHipOqHyACKRCdk1V-JBi8_iOZYpDXiTWCAdl_XAqMLI8vrcceOi2TLbdFBASVQVWwvOPUkpZ5BvNI6Zpzvv7PRjQlhBBogC06zwCcMi0f1RVZrvtt1_URwMgSuc7OaJFEIcIllVaXKa2KhmBBlQnDVSXc', + q: '0Hx9Ccd1iaESKbfv6Wsx1emCaKcIBpxYe41jNWbmtGNixaZHyCr0_FFAOwzbfe_W_kuX6Tk5rmDDivG92Rm0QcRas70CvXP8m64R3Z3qV7mKY8QZpwooPFoOaZfjl0--w_HEdtf-epm43kCIXtqgaIj4aUFEEdEe4AVnTyZpjJ8', + dp: '1zYhiKLpXwn1lukBnDlj2wGeORvYHW473PdWlsMdvBKcEYySngJszTUxO7Opu6DfwQziegCP52jEOg_njo53a-Igh_DqO1C3aCOQJjgmxotXcoAAJtE9SX61SI7N-imUtWFiWnvV58lcSaI3k21wV8zxOiSik84wfHAGUxwlGm0', + dq: 'Q9_DdWOSSHQ_zYUsffmAB_w1kIyQeFZ-F_s3yTLu-NtCVMaFqA0UJPDu0EqnSqDChZdmpW8T8ElgX-PDwuIzZRXf0ZQ_SB5yptxMxLGckWK-QyycjV0pLDzFZGsmlSRJHtGe_HHlT1Sscu7fdsIGZwHwnZO57XL_cj9QGtyOkFE', + qi: 'qhNTCBRZi6zC-2nyVuleB5DAfzza_HSqa_FvSZpzbxv_cIgI52FIB2Vn6u6c8M-n0PEVpCOwVOD2VuRqWhidfOJsFbGLyGtEg9ZRE-bQoOPvRIeUqOt5jTe3bqboG84vNcmw5m0zbCw8upUmu2LK0NIFDxrjognJEwIlMoAgALE', + kty: 'RSA' + } + const x5c = ['MIIDmjCCAoKgAwIBAgIJfEZch1k3018JMA0GCSqGSIb3DQEBBQUAMGkxFDASBgNVBAMTC2V4YW1wbGUub3JnMQswCQYDVQQGEwJVUzERMA8GA1UECBMIVmlyZ2luaWExEzARBgNVBAcTCkJsYWNrc2J1cmcxDTALBgNVBAoTBFRlc3QxDTALBgNVBAsTBFRlc3QwHhcNMjAwNTA1MTI0MTQ2WhcNMjEwNTA1MTI0MTQ2WjBpMRQwEgYDVQQDEwtleGFtcGxlLm9yZzELMAkGA1UEBhMCVVMxETAPBgNVBAgTCFZpcmdpbmlhMRMwEQYDVQQHEwpCbGFja3NidXJnMQ0wCwYDVQQKEwRUZXN0MQ0wCwYDVQQLEwRUZXN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu4kSwZDMd93b1fvd6CXUfHa+rF0DBd03tCCpWN31giKCskP09c7VigwkyHu34X//1rA7CNMaSrXQn4ChkhulSxzQyojBc3t06AjyKe/Nzpd72zaGFjaLfN+C2U5QmmaXn/2dOiQTH3aTaHDA5I8zd7ZEwrln9G6DD9KtbAcal+RWN/XT+dD+hHUSH4X4iHIvVC1El6lOtu9yjpmQtAvU3mpvxKK6AUGEA9wCWmIEcpfosOCpgHiwVeuPwJwAmuHRFA+h5N4wWw1KQuW66ocgeTzwKZ33DuMWeLap3AEeDVErInAwPPjzLSj3i3DvtveGlGZQH10wAZMAQrcUhHS06QIDAQABo0UwQzAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIC9DAmBgNVHREEHzAdhhtodHRwOi8vZXhhbXBsZS5vcmcvd2ViaWQjbWUwDQYJKoZIhvcNAQEFBQADggEBAA3oxXuMEXdi/5ndPuoJYe1eK3KHIFRajZOrMxSz65ErlkiIt9K0weoYSywufuIdP71kc+S/x/NUpXzaZ1XvUDK3IvwKxf/dnLgtDJRABCWHBQ+81YvtxJVzDSq9grVdGby7IRzuDaayvEo0YFc5xh0r8Jc/Dlaz4ZUhXLxpKZeyT47aPyk6Ys+d/2vEFDhOwyipqKI+xtfrdPaBi9FXk/QA+Th16DKx9Uxau+sXbAVSG3YZtUUkadi1sP/oKqQcJ6VTSGt+8Yg+DJwY8ZnX+QgVyEtqpTwfJNjt3G4qOtAr8LpNTN+r2BG4XYN3bck9SVfjDky5dXJo3NY2pT9AO58='] + + test('JWK.EmbeddedX5C JWS.verify pass', async t => { + const key = JWK.asKey({ ...rsa, x5c }) + const jws = JWS.sign('foo', key, { x5c }) + t.notThrows(() => JWS.verify(jws, JWK.EmbeddedX5C)) + t.throws( + () => JWS.verify(jws, JWK.EmbeddedX5C, { algorithms: ['EdDSA'] }), + { instanceOf: errors.JOSEAlgNotWhitelisted, code: 'ERR_JOSE_ALG_NOT_WHITELISTED', message: 'alg not whitelisted' } + ) + const { key: embedded } = JWS.verify(jws, JWK.EmbeddedX5C, { complete: true }) + t.false(key === embedded) + t.deepEqual(key.toJWK(), embedded.toJWK()) + }) + + test('JWK.EmbeddedX5C JWT.verify pass', async t => { + const key = JWK.asKey({ ...rsa, x5c }) + const jws = JWT.sign({}, key, { header: { x5c } }) + t.notThrows(() => JWT.verify(jws, JWK.EmbeddedX5C)) + t.throws( + () => JWT.verify(jws, JWK.EmbeddedX5C, { algorithms: ['EdDSA'] }), + { instanceOf: errors.JOSEAlgNotWhitelisted, code: 'ERR_JOSE_ALG_NOT_WHITELISTED', message: 'alg not whitelisted' } + ) + const { key: embedded } = JWT.verify(jws, JWK.EmbeddedX5C, { complete: true }) + t.false(key === embedded) + t.deepEqual(key.toJWK(), embedded.toJWK()) + }) + + test('JWK.EmbeddedJWK key must be a properly formatted cert value', async t => { + const key = JWK.asKey({ ...rsa, x5c }) + const jws = JWS.sign('foo', key, { x5c: [x5c[0].slice(0, 16)] }) + t.throws( + () => JWS.verify(jws, JWK.EmbeddedX5C), + { instanceOf: errors.JWKImportFailed, code: 'ERR_JWK_IMPORT_FAILED', message: 'key import failed' } + ) + }) + + test('JWK.EmbeddedX5C key invalid inputs', async t => { + const key = JWK.asKey({ ...rsa, x5c }) + for (const x5c of [undefined, '', null, false, true, 1, 3.14, []]) { + { + const jws = JWS.sign('foo', key, { x5c }) + t.throws( + () => JWS.verify(jws, JWK.EmbeddedX5C), + { instanceOf: errors.JWSInvalid, code: 'ERR_JWS_INVALID', message: 'JWS Header Parameter "x5c" must be a JSON array of certificate value strings' } + ) + } + { + const jws = JWS.sign('foo', key, { x5c: [x5c] }) + t.throws( + () => JWS.verify(jws, JWK.EmbeddedX5C), + { instanceOf: errors.JWSInvalid, code: 'ERR_JWS_INVALID', message: 'JWS Header Parameter "x5c" must be a JSON array of certificate value strings' } + ) + } + } + }) +} diff --git a/test/jwk/none.test.js b/test/jwk/none.test.js new file mode 100644 index 0000000000..c8c287f658 --- /dev/null +++ b/test/jwk/none.test.js @@ -0,0 +1,20 @@ +const test = require('ava') + +const { JWK, JWKS } = require('../..') + +test('JWK.None', t => { + const k = JWK.None + t.truthy(k) + t.true(JWK.isKey(k)) + t.is(k.kty, undefined) + for (const prop of ['kid', 'kty', 'thumbprint', 'toJWK', 'toPEM']) { + k[prop] = 'foo' + t.is(k[prop], undefined) + } + t.deepEqual([...k.algorithms()], ['none']) + k.type = 'foo' + t.is(k.type, 'unsecured') + t.throws(() => new JWKS.KeyStore(k), { instanceOf: TypeError }) + const ks = new JWKS.KeyStore() + t.throws(() => ks.add(k), { instanceOf: TypeError }) +}) diff --git a/types/index.d.ts b/types/index.d.ts index 222400b053..ba0948b59f 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -27,6 +27,9 @@ export type KeyInput = PrivateKeyInput | PublicKeyInput | string | Buffer; export type ProduceKeyInput = JWK.Key | KeyObject | KeyInput | JWKOctKey | JWKRSAKey | JWKECKey | JWKOKPKey; export type ConsumeKeyInput = ProduceKeyInput | JWKS.KeyStore; export type NoneKey = JWK.NoneKey; +export type EmbeddedJWK = JWK.EmbeddedJWK; +export type EmbeddedX5C = JWK.EmbeddedX5C; +export type EmbeddedVerifyKeys = EmbeddedJWK | EmbeddedX5C; export type ProduceKeyInputWithNone = ProduceKeyInput | NoneKey; export type ConsumeKeyInputWithNone = ConsumeKeyInput | NoneKey; @@ -216,6 +219,20 @@ export namespace JWK { const None: NoneKey; + interface EmbeddedJWK { + readonly type: 'embedded'; + algorithms(operation?: keyOperation): Set; + } + + const EmbeddedJWK: EmbeddedJWK; + + interface EmbeddedX5C { + readonly type: 'embedded'; + algorithms(operation?: keyOperation): Set; + } + + const EmbeddedX5C: EmbeddedX5C; + function isKey(object: any): boolean; function asKey(key: KeyObject | KeyInput, parameters?: KeyParameters): RSAKey | ECKey | OKPKey | OctKey; @@ -340,10 +357,10 @@ export namespace JWS { header?: object; } - function verify(jws: string | FlattenedJWS | GeneralJWS, key: ConsumeKeyInputWithNone, options?: VerifyOptions): string | object; - function verify(jws: string | FlattenedJWS | GeneralJWS, key: ConsumeKeyInputWithNone, options?: VerifyOptions): Buffer; - function verify(jws: string | FlattenedJWS | GeneralJWS, key: ConsumeKeyInput, options?: VerifyOptions): completeVerification; - function verify(jws: string | FlattenedJWS | GeneralJWS, key: ConsumeKeyInput, options?: VerifyOptions): completeVerification; + function verify(jws: string | FlattenedJWS | GeneralJWS, key: ConsumeKeyInputWithNone | EmbeddedVerifyKeys, options?: VerifyOptions): string | object; + function verify(jws: string | FlattenedJWS | GeneralJWS, key: ConsumeKeyInputWithNone | EmbeddedVerifyKeys, options?: VerifyOptions): Buffer; + function verify(jws: string | FlattenedJWS | GeneralJWS, key: ConsumeKeyInput | EmbeddedVerifyKeys, options?: VerifyOptions): completeVerification; + function verify(jws: string | FlattenedJWS | GeneralJWS, key: ConsumeKeyInput | EmbeddedVerifyKeys, options?: VerifyOptions): completeVerification; function verify(jws: string | FlattenedJWS | GeneralJWS, key: NoneKey, options?: VerifyOptions): completeVerification; function verify(jws: string | FlattenedJWS | GeneralJWS, key: NoneKey, options?: VerifyOptions): completeVerification; } @@ -440,8 +457,8 @@ export namespace JWT { profile?: JWTProfiles; } - function verify(jwt: string, key: ConsumeKeyInputWithNone, options?: VerifyOptions): object; - function verify(jwt: string, key: ConsumeKeyInput, options?: VerifyOptions): completeResult; + function verify(jwt: string, key: ConsumeKeyInputWithNone | EmbeddedVerifyKeys, options?: VerifyOptions): object; + function verify(jwt: string, key: ConsumeKeyInput | EmbeddedVerifyKeys, options?: VerifyOptions): completeResult; function verify(jwt: string, key: NoneKey, options?: VerifyOptions): completeResult; interface SignOptions { @@ -468,20 +485,20 @@ export namespace JWT { } namespace IdToken { - function verify(jwt: string, key: ConsumeKeyInputWithNone, options: VerifyOptions & VerifyProfileOptions<'id_token'>): object; - function verify(jwt: string, key: ConsumeKeyInput, options: VerifyOptions & VerifyProfileOptions<'id_token'>): completeResult; + function verify(jwt: string, key: ConsumeKeyInputWithNone | EmbeddedVerifyKeys, options: VerifyOptions & VerifyProfileOptions<'id_token'>): object; + function verify(jwt: string, key: ConsumeKeyInput | EmbeddedVerifyKeys, options: VerifyOptions & VerifyProfileOptions<'id_token'>): completeResult; function verify(jwt: string, key: NoneKey, options: VerifyOptions & VerifyProfileOptions<'id_token'>): completeResult; } namespace LogoutToken { - function verify(jwt: string, key: ConsumeKeyInputWithNone, options: VerifyOptions & VerifyProfileOptions<'logout_token'>): object; - function verify(jwt: string, key: ConsumeKeyInput, options: VerifyOptions & VerifyProfileOptions<'logout_token'>): completeResult; + function verify(jwt: string, key: ConsumeKeyInputWithNone | EmbeddedVerifyKeys, options: VerifyOptions & VerifyProfileOptions<'logout_token'>): object; + function verify(jwt: string, key: ConsumeKeyInput | EmbeddedVerifyKeys, options: VerifyOptions & VerifyProfileOptions<'logout_token'>): completeResult; function verify(jwt: string, key: NoneKey, options: VerifyOptions & VerifyProfileOptions<'logout_token'>): completeResult; } namespace AccessToken { - function verify(jwt: string, key: ConsumeKeyInputWithNone, options: VerifyOptions & VerifyProfileOptions<'at+JWT'>): object; - function verify(jwt: string, key: ConsumeKeyInput, options: VerifyOptions & VerifyProfileOptions<'at+JWT'>): completeResult; + function verify(jwt: string, key: ConsumeKeyInputWithNone | EmbeddedVerifyKeys, options: VerifyOptions & VerifyProfileOptions<'at+JWT'>): object; + function verify(jwt: string, key: ConsumeKeyInput | EmbeddedVerifyKeys, options: VerifyOptions & VerifyProfileOptions<'at+JWT'>): completeResult; function verify(jwt: string, key: NoneKey, options: VerifyOptions & VerifyProfileOptions<'at+JWT'>): completeResult; } }