From a9f6f7135005d6231d6f42d95c02414139a89d17 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sun, 26 Jan 2020 19:31:55 +0100 Subject: [PATCH 1/9] feat: keystore filtering by JWK Key thumbprint --- docs/README.md | 2 ++ lib/jwks/keystore.js | 6 +++++- test/jwks/keystore.test.js | 9 +++++++++ types/index.d.ts | 1 + 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index 7838ecc72b..a45bae495b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -680,6 +680,7 @@ specified by the parameters are first. - `crv`: `` Key Curve to filter for. (for EC and OKP keys) - `alg`: `` Key supported algorithm to filter for. - `kid`: `` Key ID to filter for. + - `thumbprint`: `` JWK Key thumbprint to filter for. - `use`: `` Filter keys with the specified use defined. Keys missing "use" parameter will be matched but rank lower then ones with an exact match. - `key_ops`: `string[]` Filter keys with specified key_ops defined (if key_ops is defined on the @@ -701,6 +702,7 @@ parameters is returned. - `crv`: `` Key Curve to filter for. (for EC and OKP keys) - `alg`: `` Key supported algorithm to filter for. - `kid`: `` Key ID to filter for. + - `thumbprint`: `` JWK Key thumbprint to filter for. - `use`: `` Filter keys with the specified use defined. Keys missing "use" parameter will be matched but rank lower then ones with an exact match. - `key_ops`: `string[]` Filter keys with specified key_ops defined (if key_ops is defined on the diff --git a/lib/jwks/keystore.js b/lib/jwks/keystore.js index 39110a82ad..dc24c4caae 100644 --- a/lib/jwks/keystore.js +++ b/lib/jwks/keystore.js @@ -51,7 +51,7 @@ class KeyStore { i(this).keys = new Set(keys) } - all ({ alg, kid, use, kty, key_ops: ops, x5t, 'x5t#S256': x5t256, crv } = {}) { + all ({ alg, kid, thumbprint, use, kty, key_ops: ops, x5t, 'x5t#S256': x5t256, crv } = {}) { if (ops !== undefined && (!Array.isArray(ops) || !ops.length || ops.some(x => typeof x !== 'string'))) { throw new TypeError('`key_ops` must be a non-empty array of strings') } @@ -65,6 +65,10 @@ class KeyStore { candidate = false } + if (candidate && thumbprint !== undefined && key.thumbprint !== thumbprint) { + candidate = false + } + if (candidate && x5t !== undefined && key.x5t !== x5t) { candidate = false } diff --git a/test/jwks/keystore.test.js b/test/jwks/keystore.test.js index 888e62d89b..625e69f9a1 100644 --- a/test/jwks/keystore.test.js +++ b/test/jwks/keystore.test.js @@ -169,6 +169,15 @@ test('.all() and .get() kid filter', t => { t.is(ks.get({ kid: 'foobar' }), k) }) +test('.all() and .get() thumbprint filter', t => { + const k = generateSync('RSA') + const ks = new KeyStore(k) + t.deepEqual(ks.all({ thumbprint: 'baz' }), []) + t.deepEqual(ks.all({ thumbprint: k.thumbprint }), [k]) + t.is(ks.get({ thumbprint: 'baz' }), undefined) + t.is(ks.get({ thumbprint: k.thumbprint }), k) +}) + test('.all() and .get() x5t filter and sort', t => { const k = asKey(withX5C) const ks = new KeyStore(k) diff --git a/types/index.d.ts b/types/index.d.ts index df330cfe07..ba5639ba15 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -193,6 +193,7 @@ export namespace JWKS { x5t?: string; 'x5t#S256'?: string; crv?: string; + thumbprint?: string; } class KeyStore { From 21e0ea5357fad77f27ec60eafc2e97ea2aadbc3c Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Mon, 27 Jan 2020 09:00:34 +0100 Subject: [PATCH 2/9] docs: update secp256k1 draft version link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ce97129195..549f27aae0 100644 --- a/README.md +++ b/README.md @@ -399,7 +399,7 @@ in terms of performance and API (not having well defined errors). [spec-jws]: https://tools.ietf.org/html/rfc7515 [spec-jwt]: https://tools.ietf.org/html/rfc7519 [spec-okp]: https://tools.ietf.org/html/rfc8037 -[draft-secp256k1]: https://tools.ietf.org/html/draft-ietf-cose-webauthn-algorithms-03 +[draft-secp256k1]: https://tools.ietf.org/html/draft-ietf-cose-webauthn-algorithms-04 [draft-ietf-oauth-access-token-jwt]: https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt [draft-jarm]: https://openid.net/specs/openid-financial-api-jarm.html [spec-thumbprint]: https://tools.ietf.org/html/rfc7638 From ed1f78023e70f8e3fe1555b12885ab1ad0886979 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Mon, 27 Jan 2020 10:34:21 +0100 Subject: [PATCH 3/9] refactor: cleanup, code diet --- lib/jwa/aes_cbc_hmac_sha2.js | 5 ---- lib/jwa/aes_gcm.js | 5 ---- lib/jwa/aes_gcm_kw.js | 5 ---- lib/jwa/aes_kw.js | 5 ---- lib/jwa/ecdh/dir.js | 5 ---- lib/jwa/ecdh/kw.js | 5 ---- lib/jwa/ecdsa.js | 21 ++----------- lib/jwa/eddsa.js | 4 --- lib/jwa/hmac.js | 5 ---- lib/jwa/none.js | 13 ++------ lib/jwa/pbes2.js | 4 --- lib/jwa/rsaes.js | 5 ---- lib/jwa/rsassa.js | 35 +++++----------------- lib/jwa/rsassa_pss.js | 57 +++++++++--------------------------- 14 files changed, 27 insertions(+), 147 deletions(-) diff --git a/lib/jwa/aes_cbc_hmac_sha2.js b/lib/jwa/aes_cbc_hmac_sha2.js index 4a4e9c2ca4..29a2fbeac1 100644 --- a/lib/jwa/aes_cbc_hmac_sha2.js +++ b/lib/jwa/aes_cbc_hmac_sha2.js @@ -1,4 +1,3 @@ -const { strict: assert } = require('assert') const { createCipheriv, createDecipheriv, getCiphers } = require('crypto') const uint64be = require('../help/uint64be') @@ -61,10 +60,6 @@ const decrypt = (size, sign, { [KEYOBJECT]: keyObject }, ciphertext, { iv, tag = module.exports = (JWA, JWK) => { ['A128CBC-HS256', 'A192CBC-HS384', 'A256CBC-HS512'].forEach((jwaAlg) => { const size = parseInt(jwaAlg.substr(1, 3), 10) - - assert(!JWA.encrypt.has(jwaAlg), `encrypt alg ${jwaAlg} already registered`) - assert(!JWA.decrypt.has(jwaAlg), `decrypt alg ${jwaAlg} already registered`) - const sign = JWA.sign.get(`HS${size * 2}`) if (getCiphers().includes(`aes-${size}-cbc`)) { JWA.encrypt.set(jwaAlg, encrypt.bind(undefined, size, sign)) diff --git a/lib/jwa/aes_gcm.js b/lib/jwa/aes_gcm.js index 0fa4f5dd55..1acc5ca95f 100644 --- a/lib/jwa/aes_gcm.js +++ b/lib/jwa/aes_gcm.js @@ -1,4 +1,3 @@ -const { strict: assert } = require('assert') const { createCipheriv, createDecipheriv, getCiphers } = require('crypto') const { KEYOBJECT } = require('../help/consts') @@ -47,10 +46,6 @@ const decrypt = (size, { [KEYOBJECT]: keyObject }, ciphertext, { iv, tag = Buffe module.exports = (JWA, JWK) => { ['A128GCM', 'A192GCM', 'A256GCM'].forEach((jwaAlg) => { const size = parseInt(jwaAlg.substr(1, 3), 10) - - assert(!JWA.encrypt.has(jwaAlg), `encrypt alg ${jwaAlg} already registered`) - assert(!JWA.decrypt.has(jwaAlg), `decrypt alg ${jwaAlg} already registered`) - if (getCiphers().includes(`aes-${size}-gcm`)) { JWA.encrypt.set(jwaAlg, encrypt.bind(undefined, size)) JWA.decrypt.set(jwaAlg, decrypt.bind(undefined, size)) diff --git a/lib/jwa/aes_gcm_kw.js b/lib/jwa/aes_gcm_kw.js index 6375ea8e61..3a57bb9ca4 100644 --- a/lib/jwa/aes_gcm_kw.js +++ b/lib/jwa/aes_gcm_kw.js @@ -1,13 +1,8 @@ -const { strict: assert } = require('assert') - const generateIV = require('../help/generate_iv') const base64url = require('../help/base64url') module.exports = (JWA, JWK) => { ['A128GCMKW', 'A192GCMKW', 'A256GCMKW'].forEach((jwaAlg) => { - assert(!JWA.keyManagementEncrypt.has(jwaAlg), `keyManagementEncrypt alg ${jwaAlg} already registered`) - assert(!JWA.keyManagementDecrypt.has(jwaAlg), `keyManagementDecrypt alg ${jwaAlg} already registered`) - const encAlg = jwaAlg.substr(0, 7) const size = parseInt(jwaAlg.substr(1, 3), 10) const encrypt = JWA.encrypt.get(encAlg) diff --git a/lib/jwa/aes_kw.js b/lib/jwa/aes_kw.js index 180d225bda..145f0a275e 100644 --- a/lib/jwa/aes_kw.js +++ b/lib/jwa/aes_kw.js @@ -1,4 +1,3 @@ -const { strict: assert } = require('assert') const { createCipheriv, createDecipheriv, getCiphers } = require('crypto') const uint64be = require('../help/uint64be') @@ -91,10 +90,6 @@ const unwrapKey = (size, { [KEYOBJECT]: keyObject }, payload) => { module.exports = (JWA, JWK) => { ['A128KW', 'A192KW', 'A256KW'].forEach((jwaAlg) => { const size = parseInt(jwaAlg.substr(1, 3), 10) - - assert(!JWA.keyManagementEncrypt.has(jwaAlg), `keyManagementEncrypt alg ${jwaAlg} already registered`) - assert(!JWA.keyManagementDecrypt.has(jwaAlg), `keyManagementDecrypt alg ${jwaAlg} already registered`) - if (getCiphers().includes(`aes${size}`)) { JWA.keyManagementEncrypt.set(jwaAlg, wrapKey.bind(undefined, size)) JWA.keyManagementDecrypt.set(jwaAlg, unwrapKey.bind(undefined, size)) diff --git a/lib/jwa/ecdh/dir.js b/lib/jwa/ecdh/dir.js index 7e6023f198..571b9932a0 100644 --- a/lib/jwa/ecdh/dir.js +++ b/lib/jwa/ecdh/dir.js @@ -1,5 +1,3 @@ -const { strict: assert } = require('assert') - const { KEYLENGTHS } = require('../../registry') const { generateSync } = require('../../jwk/generate') const { name: secp256k1 } = require('../../jwk/key/secp256k1_crv') @@ -23,9 +21,6 @@ const unwrapKey = (key, payload, header) => { } module.exports = (JWA, JWK) => { - assert(!JWA.keyManagementEncrypt.has('ECDH-ES'), 'keyManagementEncrypt alg ECDH-ES already registered') - assert(!JWA.keyManagementDecrypt.has('ECDH-ES'), 'keyManagementDecrypt alg ECDH-ES already registered') - JWA.keyManagementEncrypt.set('ECDH-ES', wrapKey) JWA.keyManagementDecrypt.set('ECDH-ES', unwrapKey) JWK.EC.deriveKey['ECDH-ES'] = key => (key.use === 'enc' || key.use === undefined) && key.crv !== secp256k1 diff --git a/lib/jwa/ecdh/kw.js b/lib/jwa/ecdh/kw.js index 4dfcbd172d..0741410a76 100644 --- a/lib/jwa/ecdh/kw.js +++ b/lib/jwa/ecdh/kw.js @@ -1,5 +1,3 @@ -const { strict: assert } = require('assert') - const { KEYOBJECT } = require('../../help/consts') const { generateSync } = require('../../jwk/generate') const { name: secp256k1 } = require('../../jwk/key/secp256k1_crv') @@ -28,9 +26,6 @@ const unwrapKey = (unwrap, derive, key, payload, header) => { module.exports = (JWA, JWK) => { ['ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW'].forEach((jwaAlg) => { - assert(!JWA.keyManagementEncrypt.has(jwaAlg), `keyManagementEncrypt alg ${jwaAlg} already registered`) - assert(!JWA.keyManagementDecrypt.has(jwaAlg), `keyManagementDecrypt alg ${jwaAlg} already registered`) - const kw = jwaAlg.substr(-6) const kwWrap = JWA.keyManagementEncrypt.get(kw) const kwUnwrap = JWA.keyManagementDecrypt.get(kw) diff --git a/lib/jwa/ecdsa.js b/lib/jwa/ecdsa.js index 91e56240d9..db9645dfa8 100644 --- a/lib/jwa/ecdsa.js +++ b/lib/jwa/ecdsa.js @@ -1,4 +1,3 @@ -const { strict: assert } = require('assert') const { sign: signOneShot, verify: verifyOneShot, createSign, createVerify, getCurves } = require('crypto') const { derToJose, joseToDer } = require('../help/ecdsa_signatures') @@ -10,21 +9,17 @@ const { name: secp256k1 } = require('../jwk/key/secp256k1_crv') let sign, verify -if (dsaEncodingSupported) { // >= 13.2.0 +if (dsaEncodingSupported) { sign = (jwaAlg, nodeAlg, { [KEYOBJECT]: keyObject }, payload) => { return signOneShot(nodeAlg, payload, { key: asInput(keyObject, false), dsaEncoding: 'ieee-p1363' }) } -} else if (signOneShot) { // >= 12.0.0 - sign = (jwaAlg, nodeAlg, { [KEYOBJECT]: keyObject }, payload) => { - return derToJose(signOneShot(nodeAlg, payload, asInput(keyObject, false)), jwaAlg) - } } else { sign = (jwaAlg, nodeAlg, { [KEYOBJECT]: keyObject }, payload) => { return derToJose(createSign(nodeAlg).update(payload).sign(asInput(keyObject, false)), jwaAlg) } } -if (dsaEncodingSupported) { // >= 13.2.0 +if (dsaEncodingSupported) { verify = (jwaAlg, nodeAlg, { [KEYOBJECT]: keyObject }, payload, signature) => { try { return verifyOneShot(nodeAlg, payload, { key: asInput(keyObject, true), dsaEncoding: 'ieee-p1363' }, signature) @@ -32,14 +27,6 @@ if (dsaEncodingSupported) { // >= 13.2.0 return false } } -} else if (verifyOneShot) { // >= 12.0.0 - verify = (jwaAlg, nodeAlg, { [KEYOBJECT]: keyObject }, payload, signature) => { - try { - return verifyOneShot(nodeAlg, payload, asInput(keyObject, true), joseToDer(signature, jwaAlg)) - } catch (err) { - return false - } - } } else { verify = (jwaAlg, nodeAlg, { [KEYOBJECT]: keyObject }, payload, signature) => { try { @@ -84,10 +71,6 @@ module.exports = (JWA, JWK) => { algs.forEach((jwaAlg) => { const nodeAlg = resolveNodeAlg(jwaAlg) - - assert(!JWA.sign.has(jwaAlg), `sign alg ${jwaAlg} already registered`) - assert(!JWA.verify.has(jwaAlg), `verify alg ${jwaAlg} already registered`) - JWA.sign.set(jwaAlg, sign.bind(undefined, jwaAlg, nodeAlg)) JWA.verify.set(jwaAlg, verify.bind(undefined, jwaAlg, nodeAlg)) JWK.EC.sign[jwaAlg] = key => key.private && JWK.EC.verify[jwaAlg](key) diff --git a/lib/jwa/eddsa.js b/lib/jwa/eddsa.js index 05012d43c6..d6736d0566 100644 --- a/lib/jwa/eddsa.js +++ b/lib/jwa/eddsa.js @@ -1,4 +1,3 @@ -const { strict: assert } = require('assert') const { sign: signOneShot, verify: verifyOneShot } = require('crypto') const { KEYOBJECT } = require('../help/consts') @@ -13,9 +12,6 @@ const verify = ({ [KEYOBJECT]: keyObject }, payload, signature) => { } module.exports = (JWA, JWK) => { - assert(!JWA.sign.has('EdDSA'), 'sign alg EdDSA already registered') - assert(!JWA.verify.has('EdDSA'), 'verify alg EdDSA already registered') - if (edDSASupported) { JWA.sign.set('EdDSA', sign) JWA.verify.set('EdDSA', verify) diff --git a/lib/jwa/hmac.js b/lib/jwa/hmac.js index c6ef260929..c80f785e70 100644 --- a/lib/jwa/hmac.js +++ b/lib/jwa/hmac.js @@ -1,4 +1,3 @@ -const { strict: assert } = require('assert') const { createHmac } = require('crypto') const { KEYOBJECT } = require('../help/consts') @@ -24,10 +23,6 @@ const verify = (jwaAlg, hmacAlg, { [KEYOBJECT]: keyObject }, payload, signature) module.exports = (JWA, JWK) => { ['HS256', 'HS384', 'HS512'].forEach((jwaAlg) => { const hmacAlg = resolveNodeAlg(jwaAlg) - - assert(!JWA.sign.has(jwaAlg), `sign alg ${jwaAlg} already registered`) - assert(!JWA.verify.has(jwaAlg), `verify alg ${jwaAlg} already registered`) - JWA.sign.set(jwaAlg, sign.bind(undefined, jwaAlg, hmacAlg)) JWA.verify.set(jwaAlg, verify.bind(undefined, jwaAlg, hmacAlg)) JWK.oct.sign[jwaAlg] = JWK.oct.verify[jwaAlg] = key => key.use === 'sig' || key.use === undefined diff --git a/lib/jwa/none.js b/lib/jwa/none.js index e9de26a837..d2935bb6e0 100644 --- a/lib/jwa/none.js +++ b/lib/jwa/none.js @@ -1,14 +1,7 @@ -const { strict: assert } = require('assert') - -const sign = (key, payload) => Buffer.from('') +const sign = () => Buffer.from('') const verify = (key, payload, signature) => !signature.length module.exports = (JWA, JWK) => { - const jwaAlg = 'none' - - assert(!JWA.sign.has(jwaAlg), `sign alg ${jwaAlg} already registered`) - assert(!JWA.verify.has(jwaAlg), `verify alg ${jwaAlg} already registered`) - - JWA.sign.set(jwaAlg, sign) - JWA.verify.set(jwaAlg, verify) + JWA.sign.set('none', sign) + JWA.verify.set('none', verify) } diff --git a/lib/jwa/pbes2.js b/lib/jwa/pbes2.js index b0c7d40dfa..9468627167 100644 --- a/lib/jwa/pbes2.js +++ b/lib/jwa/pbes2.js @@ -1,4 +1,3 @@ -const { strict: assert } = require('assert') const { pbkdf2Sync: pbkdf2, randomBytes } = require('crypto') const { KEYOBJECT } = require('../help/consts') @@ -42,9 +41,6 @@ const unwrapKey = (keylen, sha, concat, unwrap, { [KEYOBJECT]: keyObject }, payl module.exports = (JWA, JWK) => { ['PBES2-HS256+A128KW', 'PBES2-HS384+A192KW', 'PBES2-HS512+A256KW'].forEach((jwaAlg) => { - assert(!JWA.keyManagementEncrypt.has(jwaAlg), `keyManagementEncrypt alg ${jwaAlg} already registered`) - assert(!JWA.keyManagementDecrypt.has(jwaAlg), `keyManagementDecrypt alg ${jwaAlg} already registered`) - const kw = jwaAlg.substr(-6) const kwWrap = JWA.keyManagementEncrypt.get(kw) const kwUnwrap = JWA.keyManagementDecrypt.get(kw) diff --git a/lib/jwa/rsaes.js b/lib/jwa/rsaes.js index 5ed9a56b25..a7277123f6 100644 --- a/lib/jwa/rsaes.js +++ b/lib/jwa/rsaes.js @@ -1,4 +1,3 @@ -const { strict: assert } = require('assert') const { publicEncrypt, privateDecrypt, constants } = require('crypto') const { oaepHashSupported } = require('../help/runtime_support') @@ -52,10 +51,6 @@ module.exports = (JWA, JWK) => { algs.forEach((jwaAlg) => { const padding = resolvePadding(jwaAlg) const oaepHash = resolveOaepHash(jwaAlg) - - assert(!JWA.keyManagementEncrypt.has(jwaAlg), `keyManagementEncrypt alg ${jwaAlg} already registered`) - assert(!JWA.keyManagementDecrypt.has(jwaAlg), `keyManagementDecrypt alg ${jwaAlg} already registered`) - JWA.keyManagementEncrypt.set(jwaAlg, wrapKey.bind(undefined, padding, oaepHash)) JWA.keyManagementDecrypt.set(jwaAlg, unwrapKey.bind(undefined, padding, oaepHash)) JWK.RSA.wrapKey[jwaAlg] = key => (key.use === 'enc' || key.use === undefined) && key.length >= LENGTHS[jwaAlg] diff --git a/lib/jwa/rsassa.js b/lib/jwa/rsassa.js index b033a6ce52..51c6f45687 100644 --- a/lib/jwa/rsassa.js +++ b/lib/jwa/rsassa.js @@ -1,33 +1,18 @@ -const { strict: assert } = require('assert') -const { sign: signOneShot, verify: verifyOneShot, createSign, createVerify } = require('crypto') +const { createSign, createVerify } = require('crypto') const { KEYOBJECT } = require('../help/consts') const resolveNodeAlg = require('../help/node_alg') const { asInput } = require('../help/key_object') -let sign, verify - -if (signOneShot) { - sign = (nodeAlg, { [KEYOBJECT]: keyObject }, payload) => { - return signOneShot(nodeAlg, payload, keyObject) - } -} else { - sign = (nodeAlg, { [KEYOBJECT]: keyObject }, payload) => { - return createSign(nodeAlg).update(payload).sign(asInput(keyObject, false)) - } +const sign = (nodeAlg, { [KEYOBJECT]: keyObject }, payload) => { + return createSign(nodeAlg).update(payload).sign(asInput(keyObject, false)) } -if (verifyOneShot) { - verify = (nodeAlg, { [KEYOBJECT]: keyObject }, payload, signature) => { - return verifyOneShot(nodeAlg, payload, keyObject, signature) - } -} else { - verify = (nodeAlg, { [KEYOBJECT]: keyObject }, payload, signature) => { - try { - return createVerify(nodeAlg).update(payload).verify(asInput(keyObject, true), signature) - } catch (err) { - return false - } +const verify = (nodeAlg, { [KEYOBJECT]: keyObject }, payload, signature) => { + try { + return createVerify(nodeAlg).update(payload).verify(asInput(keyObject, true), signature) + } catch (err) { + return false } } @@ -40,10 +25,6 @@ const LENGTHS = { module.exports = (JWA, JWK) => { ['RS256', 'RS384', 'RS512'].forEach((jwaAlg) => { const nodeAlg = resolveNodeAlg(jwaAlg) - - assert(!JWA.sign.has(jwaAlg), `sign alg ${jwaAlg} already registered`) - assert(!JWA.verify.has(jwaAlg), `verify alg ${jwaAlg} already registered`) - JWA.sign.set(jwaAlg, sign.bind(undefined, nodeAlg)) JWA.verify.set(jwaAlg, verify.bind(undefined, nodeAlg)) JWK.RSA.sign[jwaAlg] = key => key.private && JWK.RSA.verify[jwaAlg](key) diff --git a/lib/jwa/rsassa_pss.js b/lib/jwa/rsassa_pss.js index 44095cc8ee..bdb828904f 100644 --- a/lib/jwa/rsassa_pss.js +++ b/lib/jwa/rsassa_pss.js @@ -1,7 +1,4 @@ -const { strict: assert } = require('assert') const { - sign: signOneShot, - verify: verifyOneShot, createSign, createVerify, constants @@ -11,44 +8,22 @@ const { KEYOBJECT } = require('../help/consts') const resolveNodeAlg = require('../help/node_alg') const { asInput } = require('../help/key_object') -let sign, verify - -if (signOneShot) { - sign = (nodeAlg, { [KEYOBJECT]: keyObject }, payload) => { - return signOneShot(nodeAlg, payload, { - key: asInput(keyObject, false), - padding: constants.RSA_PKCS1_PSS_PADDING, - saltLength: constants.RSA_PSS_SALTLEN_DIGEST - }) - } -} else { - sign = (nodeAlg, { [KEYOBJECT]: keyObject }, payload) => { - const key = asInput(keyObject, false) - return createSign(nodeAlg).update(payload).sign({ - key, - padding: constants.RSA_PKCS1_PSS_PADDING, - saltLength: constants.RSA_PSS_SALTLEN_DIGEST - }) - } +const sign = (nodeAlg, { [KEYOBJECT]: keyObject }, payload) => { + const key = asInput(keyObject, false) + return createSign(nodeAlg).update(payload).sign({ + key, + padding: constants.RSA_PKCS1_PSS_PADDING, + saltLength: constants.RSA_PSS_SALTLEN_DIGEST + }) } -if (verifyOneShot) { - verify = (nodeAlg, { [KEYOBJECT]: keyObject }, payload, signature) => { - return verifyOneShot(nodeAlg, payload, { - key: asInput(keyObject, false), - padding: constants.RSA_PKCS1_PSS_PADDING, - saltLength: constants.RSA_PSS_SALTLEN_DIGEST - }, signature) - } -} else { - verify = (nodeAlg, { [KEYOBJECT]: keyObject }, payload, signature) => { - const key = asInput(keyObject, true) - return createVerify(nodeAlg).update(payload).verify({ - key, - padding: constants.RSA_PKCS1_PSS_PADDING, - saltLength: constants.RSA_PSS_SALTLEN_DIGEST - }, signature) - } +const verify = (nodeAlg, { [KEYOBJECT]: keyObject }, payload, signature) => { + const key = asInput(keyObject, true) + return createVerify(nodeAlg).update(payload).verify({ + key, + padding: constants.RSA_PKCS1_PSS_PADDING, + saltLength: constants.RSA_PSS_SALTLEN_DIGEST + }, signature) } const LENGTHS = { @@ -60,10 +35,6 @@ const LENGTHS = { module.exports = (JWA, JWK) => { ['PS256', 'PS384', 'PS512'].forEach((jwaAlg) => { const nodeAlg = resolveNodeAlg(jwaAlg) - - assert(!JWA.sign.has(jwaAlg), `sign alg ${jwaAlg} already registered`) - assert(!JWA.verify.has(jwaAlg), `verify alg ${jwaAlg} already registered`) - JWA.sign.set(jwaAlg, sign.bind(undefined, nodeAlg)) JWA.verify.set(jwaAlg, verify.bind(undefined, nodeAlg)) JWK.RSA.sign[jwaAlg] = key => key.private && JWK.RSA.verify[jwaAlg](key) From 93068a63c8bda0511e54e8928590abf4fd368405 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 28 Jan 2020 16:41:03 +0100 Subject: [PATCH 4/9] refactor: improve performance when decoding base64url values When creating a Buffer from a string, this encoding will also correctly accept "URL and Filename Safe Alphabet". --- lib/help/base64url.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/help/base64url.js b/lib/help/base64url.js index c50759978c..29467146cd 100644 --- a/lib/help/base64url.js +++ b/lib/help/base64url.js @@ -6,10 +6,6 @@ const fromBase64 = (base64) => { return base64.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_') } -const toBase64 = (base64url) => { - return base64url.replace(/-/g, '+').replace(/_/g, '/') -} - const encode = (input, encoding = 'utf8') => { return fromBase64(Buffer.from(input, encoding).toString('base64')) } @@ -23,7 +19,7 @@ const decodeToBuffer = (input) => { throw new JOSEInvalidEncoding('input is not a valid base64url encoded string') } - return Buffer.from(toBase64(input), 'base64') + return Buffer.from(input, 'base64') } const decode = (input, encoding = 'utf8') => { From d8c50ade6dce1e5aca765b5503c359ffa9cc4ead Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 28 Jan 2020 18:20:34 +0100 Subject: [PATCH 5/9] chore: package.json tags --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index 1881c917d2..f77d90734b 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "compact", "decode", "decrypt", + "detached", "ec", "ecdsa", "eddsa", @@ -30,9 +31,11 @@ "logout_token", "oct", "okp", + "payload", "rsa", "secp256k1", "sign", + "signature", "validate", "verify" ], From 955171701fd1f79bab1354a76190b7c3e28de05c Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 29 Jan 2020 16:45:54 +0100 Subject: [PATCH 6/9] docs: update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 549f27aae0..9e89a14809 100644 --- a/README.md +++ b/README.md @@ -329,7 +329,7 @@ Legend: 1 Not supported in Electron due to Electron's use of BoringSSL 2 Unsecured JWS is [supported][documentation-none] for the JWS and JWT sign and verify operations but it is an entirely opt-in behaviour, downgrade attacks are prevented by the required -use of a special `JWK.Key` instance that cannot be instantiated through the key import API +use of a special `JWK.Key`-like object that cannot be instantiated through the key import API 3 RSA-OAEP-256 is only supported when Node.js >= 12.9.0 runtime is detected ## FAQ From 470b4c73154e1fcf8b92726d521940e5e11c9d94 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 29 Jan 2020 20:26:15 +0100 Subject: [PATCH 7/9] perf: base64url decode, JWT.verify, JWK.Key instance re-use I'm done trying to educate other JOSE producers about interoperability so i'm going to be accepting their non-conform base64url so that users of this module don't suffer performance loss. --- lib/jwa/index.js | 17 +++++++++++++++ lib/jwe/decrypt.js | 2 -- lib/jws/index.js | 2 +- lib/jws/verify.js | 41 +++++++++++++++++++++++-------------- lib/jwt/verify.js | 24 +++++++++++----------- test/help/base64url.test.js | 6 ------ test/jwa/sanity.test.js | 3 ++- test/jwk/import.test.js | 20 ------------------ test/jws/sanity.test.js | 9 -------- test/jwt/decode.test.js | 6 ------ test/jwt/verify.test.js | 36 ++++++++++++-------------------- 11 files changed, 71 insertions(+), 95 deletions(-) diff --git a/lib/jwa/index.js b/lib/jwa/index.js index 4afd402ccd..a3b61bb7d5 100644 --- a/lib/jwa/index.js +++ b/lib/jwa/index.js @@ -25,7 +25,22 @@ require('./pbes2')(JWA, JWK) require('./ecdh/dir')(JWA, JWK) require('./ecdh/kw')(JWA, JWK) +const map = new WeakMap() + +const i = (ctx) => { + if (!map.has(ctx)) { + map.set(ctx, {}) + } + return map.get(ctx) +} + const check = (key, op, alg) => { + const cache = i(key) + + if (cache[`${op}${alg}`]) { + return true + } + let label let keyOp if (op === 'keyManagementEncrypt') { @@ -43,6 +58,8 @@ const check = (key, op, alg) => { } else { throw new JOSENotSupported(`unsupported ${label || op} alg: ${alg}`) } + + cache[`${op}${alg}`] = true } module.exports = { diff --git a/lib/jwe/decrypt.js b/lib/jwe/decrypt.js index b1adb0abe6..318bc0e577 100644 --- a/lib/jwe/decrypt.js +++ b/lib/jwe/decrypt.js @@ -55,8 +55,6 @@ const jweDecrypt = (skipValidateHeaders, serialization, jwe, key, { crit = [], c if (!serialization) { serialization = resolveSerialization(jwe) - } else if (serialization !== resolveSerialization(jwe)) { - throw new errors.JWEInvalid() } let alg, ciphertext, enc, encryptedKey, iv, opts, prot, tag, unprotected, cek, aad, header diff --git a/lib/jws/index.js b/lib/jws/index.js index ba1fe7474b..3e87554364 100644 --- a/lib/jws/index.js +++ b/lib/jws/index.js @@ -1,5 +1,5 @@ const Sign = require('./sign') -const verify = require('./verify') +const { verify } = require('./verify') const single = (serialization, payload, key, protectedHeader, unprotectedHeader) => { const jws = new Sign(payload) diff --git a/lib/jws/verify.js b/lib/jws/verify.js index 7000e902ea..53a1fe0ec7 100644 --- a/lib/jws/verify.js +++ b/lib/jws/verify.js @@ -9,7 +9,7 @@ const { check, verify } = require('../jwa') const { detect: resolveSerialization } = require('./serializers') validateCrit = validateCrit.bind(undefined, errors.JWSInvalid) -const SINGLE_RECIPIENT = new Set(['compact', 'flattened']) +const SINGLE_RECIPIENT = new Set(['compact', 'flattened', 'preparsed']) /* * @public @@ -29,8 +29,6 @@ const jwsVerify = (skipDisjointCheck, serialization, jws, key, { crit = [], comp if (!serialization) { serialization = resolveSerialization(jws) - } else if (serialization !== resolveSerialization(jws)) { - throw new errors.JWSInvalid() } let prot // protected header @@ -47,26 +45,35 @@ const jwsVerify = (skipDisjointCheck, serialization, jws, key, { crit = [], comp jws = { ...root, ...signatures[0] } } + let decoded + if (SINGLE_RECIPIENT.has(serialization)) { - if (serialization === 'compact') { // compact serialization format - ([prot, payload, signature] = jws.split('.')) - } else { // flattened serialization format - ({ protected: prot, payload, signature, header } = jws) + let parsedProt = {} + + switch (serialization) { + case 'compact': // compact serialization format + ([prot, payload, signature] = jws.split('.')) + break + case 'flattened': // flattened serialization format + ({ protected: prot, payload, signature, header } = jws) + break + case 'preparsed': { // from the JWT module + ({ decoded } = jws); + ([prot, payload, signature] = jws.token.split('.')) + break + } } if (!header) { skipDisjointCheck = true } - let parsedProt = {} - if (prot) { + if (decoded) { + parsedProt = decoded.header + } else if (prot) { try { parsedProt = base64url.JSON.decode(prot) } catch (err) { - if (err instanceof errors.JOSEError) { - throw err - } - throw new errors.JWSInvalid('could not parse JWS protected header') } } else { @@ -125,13 +132,14 @@ const jwsVerify = (skipDisjointCheck, serialization, jws, key, { crit = [], comp Buffer.from('.'), Buffer.isBuffer(payload) ? payload : Buffer.from(payload) ]) + if (!verify(alg, key, toBeVerified, base64url.decodeToBuffer(signature))) { throw new errors.JWSVerificationFailed() } if (!combinedHeader.crit || !combinedHeader.crit.includes('b64') || combinedHeader.b64) { if (parse) { - payload = base64url.JSON.decode.try(payload, encoding) + payload = decoded ? decoded.payload : base64url.JSON.decode.try(payload, encoding) } else { payload = base64url.decodeToBuffer(payload) } @@ -168,4 +176,7 @@ const jwsVerify = (skipDisjointCheck, serialization, jws, key, { crit = [], comp throw multi } -module.exports = jwsVerify.bind(undefined, false, undefined) +module.exports = { + bare: jwsVerify, + verify: jwsVerify.bind(undefined, false, undefined) +} diff --git a/lib/jwt/verify.js b/lib/jwt/verify.js index 364c0a31c1..b0780e3b3e 100644 --- a/lib/jwt/verify.js +++ b/lib/jwt/verify.js @@ -2,7 +2,7 @@ const isObject = require('../help/is_object') const epoch = require('../help/epoch') const secs = require('../help/secs') const getKey = require('../help/get_key') -const JWS = require('../jws') +const { bare: verify } = require('../jws/verify') const { KeyStore } = require('../jwks') const { JWTClaimInvalid, JWTExpired } = require('../errors') @@ -225,9 +225,17 @@ module.exports = (token, key, options = {}) => { jti, maxAuthAge, maxTokenAge, nonce, now, profile, subject } = options = validateOptions(options) - const unix = epoch(now) - const decoded = decode(token, { complete: true }) + key = getKey(key, true) + + if (complete) { + ({ key } = verify(true, 'preparsed', { decoded, token }, key, { crit, algorithms, complete: true })) + decoded.key = key + } else { + verify(true, 'preparsed', { decoded, token }, key, { crit, algorithms }) + } + + const unix = epoch(now) validateTypes(decoded, profile, options) if (issuer && decoded.payload.iss !== issuer) { @@ -292,13 +300,5 @@ module.exports = (token, key, options = {}) => { throw new JWTClaimInvalid('invalid JWT typ header value for the used validation profile', 'typ', 'check_failed') } - key = getKey(key, true) - - if (complete && key instanceof KeyStore) { - ({ key } = JWS.verify(token, key, { crit, algorithms, complete: true })) - } else { - JWS.verify(token, key, { crit, algorithms }) - } - - return complete ? { ...decoded, key } : decoded.payload + return complete ? decoded : decoded.payload } diff --git a/test/help/base64url.test.js b/test/help/base64url.test.js index d0e59d2ed4..267add6a10 100644 --- a/test/help/base64url.test.js +++ b/test/help/base64url.test.js @@ -41,9 +41,3 @@ test('.JSON.decode.try (valid json)', t => { test('.JSON.decode.try (invalid json)', t => { t.is(base64url.JSON.decode.try('Zm9v'), 'foo') }) - -test('decode input with invalid encoding throws', t => { - t.throws(() => { - base64url.decode(testStr) - }, { instanceOf: errors.JOSEInvalidEncoding, code: 'ERR_JOSE_INVALID_ENCODING', message: 'input is not a valid base64url encoded string' }) -}) diff --git a/test/jwa/sanity.test.js b/test/jwa/sanity.test.js index cd6cef7829..49b3be7d65 100644 --- a/test/jwa/sanity.test.js +++ b/test/jwa/sanity.test.js @@ -2,6 +2,7 @@ const test = require('ava') const { errors } = require('../..') const JWA = require('../../lib/jwa') +const JWK = require('../../lib/jwk') ;['sign', 'verify', 'keyManagementEncrypt', 'keyManagementDecrypt', 'encrypt', 'decrypt'].forEach((op) => { let label @@ -10,7 +11,7 @@ const JWA = require('../../lib/jwa') } test(`JWA.${op} will not accept an "unimplemented" algorithm`, t => { t.throws(() => { - JWA[op]('foo') + JWA[op]('foo', JWK.generateSync('oct')) }, { instanceOf: errors.JOSENotSupported, code: 'ERR_JOSE_NOT_SUPPORTED', message: `unsupported ${label || op} alg: foo` }) }) }) diff --git a/test/jwk/import.test.js b/test/jwk/import.test.js index 634b3e4bc3..e2f4cb16f7 100644 --- a/test/jwk/import.test.js +++ b/test/jwk/import.test.js @@ -145,26 +145,6 @@ test('fails to import JWK RSA with oth', async t => { }, { instanceOf: errors.JOSENotSupported, code: 'ERR_JOSE_NOT_SUPPORTED', message: 'Private RSA keys with more than two primes are not supported' }) }) -test('invalid encoded jwk import', async t => { - const jwk = (await generate('oct')).toJWK(true) - - jwk.k = base64url.decodeToBuffer(jwk.k).toString('base64') - - t.throws(() => { - asKey(jwk) - }, { instanceOf: errors.JOSEInvalidEncoding, code: 'ERR_JOSE_INVALID_ENCODING', message: 'input is not a valid base64url encoded string' }) -}) - -test('invalid encoded oct jwk import', async t => { - const jwk = (await generate('EC')).toJWK(true) - - jwk.d = base64url.decodeToBuffer(jwk.d).toString('base64') - - t.throws(() => { - asKey(jwk) - }, { instanceOf: errors.JOSEInvalidEncoding, code: 'ERR_JOSE_INVALID_ENCODING', message: 'input is not a valid base64url encoded string' }) -}) - const cert = `-----BEGIN CERTIFICATE----- MIIC4DCCAcgCCQDO8JBSH914NDANBgkqhkiG9w0BAQsFADAyMQswCQYDVQQGEwJD WjEPMA0GA1UEBwwGUHJhZ3VlMRIwEAYDVQQDDAlwa210bHN0d28wHhcNMTkwNjE4 diff --git a/test/jws/sanity.test.js b/test/jws/sanity.test.js index eefe53d078..e1e14bbcc3 100644 --- a/test/jws/sanity.test.js +++ b/test/jws/sanity.test.js @@ -273,15 +273,6 @@ test('JWS verify algorithms whitelist (multi-recipient)', t => { }) }) -test('invalid tokens', t => { - t.throws(() => { - JWS.verify( - 'eyJ0eXAiOiJKV1QiLCJraWQiOiIyZTFkYjRmMC1mYmY5LTQxZjYtOGMxYi1hMzczYjgwZmNhYTEiLCJhbGciOiJFUzI1NiIsImlzcyI6Imh0dHBzOi8vaWRlbnRpdHktc3RhZ2luZy5kZWxpdmVyb28uY29tLyIsImNsaWVudCI6ImIyM2I0ZjM1YzIyMTI5NDQxZjMwZDMyYmI5ZmM4ZWYyIiwic2lnbmVyIjoiYXJuOmF3czplbGFzdGljbG9hZGJhbGFuY2luZzpldS13ZXN0LTE6NTE3OTAyNjYzOTE1OmxvYWRiYWxhbmNlci9hcHAvcGF5bWVudHMtZGFzaGJvYXJkLXdlYi80YzA4ZGI2NDMyMDIyOWEyIiwiZXhwIjoxNTYyNjkxNTg1fQ==.eyJlbWFpbCI6ImpvYW8udmllaXJhQGRlbGl2ZXJvby5jby51ayIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJmYW1pbHlfbmFtZSI6Ikd1ZXJyYSBWaWVpcmEiLCJnaXZlbl9uYW1lIjoiSm9hbyIsIm5hbWUiOiJKb2FvIEd1ZXJyYSBWaWVpcmEiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tLy1sTUpXTXV3R1dpYy9BQUFBQUFBQUFBSS9BQUFBQUFBQUFCVS9lNGtkTDg5UjlqZy9zOTYtYy9waG90by5qcGciLCJzdWIiOiIxMWE1YmFmMGRjNzcwNWRmMzk1ZTMzYWFkZjU2MDk4OCIsImV4cCI6MTU2MjY5MTU4NSwiaXNzIjoiaHR0cHM6Ly9pZGVudGl0eS1zdGFnaW5nLmRlbGl2ZXJvby5jb20vIn0=.DSHLJXLOfLJ-ZYcX0Vlii6Ak_jcDSkKOvNRj_rvtAyY9uYXtwo798ZrR35fgut-LuCdx0aKz2SgK0KJqw5q6dA==', - generateSync('EC') - ) - }, { instanceOf: errors.JOSEInvalidEncoding, code: 'ERR_JOSE_INVALID_ENCODING', message: 'input is not a valid base64url encoded string' }) -}) - test('"enc" key is not usable for signing', t => { const k = generateSync('oct', 256, { use: 'enc' }) t.throws(() => { diff --git a/test/jwt/decode.test.js b/test/jwt/decode.test.js index 3c549df6e0..49251203ea 100644 --- a/test/jwt/decode.test.js +++ b/test/jwt/decode.test.js @@ -47,9 +47,3 @@ test('returns the payload', t => { foo: 'bar' }) }) - -test('invalid tokens', t => { - t.throws(() => { - JWT.decode('eyJ0eXAiOiJKV1QiLCJraWQiOiIyZTFkYjRmMC1mYmY5LTQxZjYtOGMxYi1hMzczYjgwZmNhYTEiLCJhbGciOiJFUzI1NiIsImlzcyI6Imh0dHBzOi8vaWRlbnRpdHktc3RhZ2luZy5kZWxpdmVyb28uY29tLyIsImNsaWVudCI6ImIyM2I0ZjM1YzIyMTI5NDQxZjMwZDMyYmI5ZmM4ZWYyIiwic2lnbmVyIjoiYXJuOmF3czplbGFzdGljbG9hZGJhbGFuY2luZzpldS13ZXN0LTE6NTE3OTAyNjYzOTE1OmxvYWRiYWxhbmNlci9hcHAvcGF5bWVudHMtZGFzaGJvYXJkLXdlYi80YzA4ZGI2NDMyMDIyOWEyIiwiZXhwIjoxNTYyNjkxNTg1fQ==.eyJlbWFpbCI6ImpvYW8udmllaXJhQGRlbGl2ZXJvby5jby51ayIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJmYW1pbHlfbmFtZSI6Ikd1ZXJyYSBWaWVpcmEiLCJnaXZlbl9uYW1lIjoiSm9hbyIsIm5hbWUiOiJKb2FvIEd1ZXJyYSBWaWVpcmEiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tLy1sTUpXTXV3R1dpYy9BQUFBQUFBQUFBSS9BQUFBQUFBQUFCVS9lNGtkTDg5UjlqZy9zOTYtYy9waG90by5qcGciLCJzdWIiOiIxMWE1YmFmMGRjNzcwNWRmMzk1ZTMzYWFkZjU2MDk4OCIsImV4cCI6MTU2MjY5MTU4NSwiaXNzIjoiaHR0cHM6Ly9pZGVudGl0eS1zdGFnaW5nLmRlbGl2ZXJvby5jb20vIn0=.DSHLJXLOfLJ-ZYcX0Vlii6Ak_jcDSkKOvNRj_rvtAyY9uYXtwo798ZrR35fgut-LuCdx0aKz2SgK0KJqw5q6dA==') - }, { instanceOf: errors.JOSEInvalidEncoding, code: 'ERR_JOSE_INVALID_ENCODING', message: 'input is not a valid base64url encoded string' }) -}) diff --git a/test/jwt/verify.test.js b/test/jwt/verify.test.js index 8a8f228620..844c51a4dd 100644 --- a/test/jwt/verify.test.js +++ b/test/jwt/verify.test.js @@ -1,7 +1,6 @@ const test = require('ava') -const { JWT, JWK, JWKS, errors } = require('../..') -const base64url = require('../../lib/help/base64url') +const { JWS, JWT, JWK, JWKS, errors } = require('../..') const key = JWK.generateSync('oct') const token = JWT.sign({}, key, { iat: false }) @@ -107,7 +106,7 @@ test('options.ignoreIat & options.maxTokenAge may not be used together', t => { test(`"${claim} must be a timestamp when provided"`, t => { ;['', 'foo', true, null, [], {}].forEach((val) => { const err = t.throws(() => { - const invalid = `eyJhbGciOiJub25lIn0.${base64url.JSON.encode({ [claim]: val })}.` + const invalid = JWS.sign({ [claim]: val }, key) JWT.verify(invalid, key) }, { instanceOf: errors.JWTClaimInvalid, message: `"${claim}" claim must be a unix timestamp` }) @@ -121,7 +120,7 @@ test('options.ignoreIat & options.maxTokenAge may not be used together', t => { test(`"${claim} must be a string when provided"`, t => { ;['', 0, 1, true, null, [], {}].forEach((val) => { const err = t.throws(() => { - const invalid = `eyJhbGciOiJub25lIn0.${base64url.JSON.encode({ [claim]: val })}.` + const invalid = JWS.sign({ [claim]: val }, key) JWT.verify(invalid, key) }, { instanceOf: errors.JWTClaimInvalid, message: `"${claim}" claim must be a string` }) @@ -136,14 +135,14 @@ test('options.ignoreIat & options.maxTokenAge may not be used together', t => { ;['', 0, 1, true, null, [], {}].forEach((val) => { let err err = t.throws(() => { - const invalid = `eyJhbGciOiJub25lIn0.${base64url.JSON.encode({ [claim]: val })}.` + const invalid = JWS.sign({ [claim]: val }, key) JWT.verify(invalid, key) }, { instanceOf: errors.JWTClaimInvalid, message: `"${claim}" claim must be a string or array of strings` }) t.is(err.claim, claim) t.is(err.reason, 'invalid') err = t.throws(() => { - const invalid = `eyJhbGciOiJub25lIn0.${base64url.JSON.encode({ [claim]: [val] })}.` + const invalid = JWS.sign({ [claim]: [val] }, key) JWT.verify(invalid, key) }, { instanceOf: errors.JWTClaimInvalid, message: `"${claim}" claim must be a string or array of strings` }) t.is(err.claim, claim) @@ -161,14 +160,14 @@ Object.entries({ test(`option.${option} validation fails`, t => { let err err = t.throws(() => { - const invalid = `eyJhbGciOiJub25lIn0.${base64url.JSON.encode({ [claim]: 'foo' })}.` + const invalid = JWS.sign({ [claim]: 'foo' }, key) JWT.verify(invalid, key, { [option]: 'bar' }) }, { instanceOf: errors.JWTClaimInvalid, message: `unexpected "${claim}" claim value` }) t.is(err.claim, claim) t.is(err.reason, 'check_failed') err = t.throws(() => { - const invalid = `eyJhbGciOiJub25lIn0.${base64url.JSON.encode({ [claim]: undefined })}.` + const invalid = JWS.sign({ [claim]: undefined }, key) JWT.verify(invalid, key, { [option]: 'bar' }) }, { instanceOf: errors.JWTClaimInvalid, message: `"${claim}" claim is missing` }) t.is(err.claim, claim) @@ -185,14 +184,14 @@ Object.entries({ test('option.audience validation fails', t => { let err err = t.throws(() => { - const invalid = `eyJhbGciOiJub25lIn0.${base64url.JSON.encode({ aud: 'foo' })}.` + const invalid = JWS.sign({ aud: 'foo' }, key) JWT.verify(invalid, key, { audience: 'bar' }) }, { instanceOf: errors.JWTClaimInvalid, message: 'unexpected "aud" claim value' }) t.is(err.claim, 'aud') t.is(err.reason, 'check_failed') err = t.throws(() => { - const invalid = `eyJhbGciOiJub25lIn0.${base64url.JSON.encode({ aud: ['foo'] })}.` + const invalid = JWS.sign({ aud: ['foo'] }, key) JWT.verify(invalid, key, { audience: 'bar' }) }, { instanceOf: errors.JWTClaimInvalid, message: 'unexpected "aud" claim value' }) t.is(err.claim, 'aud') @@ -218,7 +217,7 @@ test('option.audience validation success', t => { test('option.maxAuthAge requires iat to be in the payload', t => { const err = t.throws(() => { - const invalid = `eyJhbGciOiJub25lIn0.${base64url.JSON.encode({})}.` + const invalid = JWS.sign({}, key) JWT.verify(invalid, key, { maxAuthAge: '30s' }) }, { instanceOf: errors.JWTClaimInvalid, message: '"auth_time" claim is missing' }) t.is(err.claim, 'auth_time') @@ -230,7 +229,7 @@ const now = new Date(epoch * 1000) test('option.maxAuthAge checks auth_time', t => { const err = t.throws(() => { - const invalid = `eyJhbGciOiJub25lIn0.${base64url.JSON.encode({ auth_time: epoch - 31 })}.` + const invalid = JWS.sign({ auth_time: epoch - 31 }, key) JWT.verify(invalid, key, { maxAuthAge: '30s', now }) }, { instanceOf: errors.JWTClaimInvalid, message: '"auth_time" claim timestamp check failed (too much time has elapsed since the last End-User authentication)' }) t.is(err.claim, 'auth_time') @@ -245,7 +244,7 @@ test('option.maxAuthAge checks auth_time (with tolerance)', t => { test('option.maxTokenAge requires iat to be in the payload', t => { const err = t.throws(() => { - const invalid = `eyJhbGciOiJub25lIn0.${base64url.JSON.encode({})}.` + const invalid = JWS.sign({}, key) JWT.verify(invalid, key, { maxTokenAge: '30s' }) }, { instanceOf: errors.JWTClaimInvalid, message: '"iat" claim is missing' }) t.is(err.claim, 'iat') @@ -254,7 +253,7 @@ test('option.maxTokenAge requires iat to be in the payload', t => { test('option.maxTokenAge checks iat elapsed time', t => { const err = t.throws(() => { - const invalid = `eyJhbGciOiJub25lIn0.${base64url.JSON.encode({ iat: epoch - 31 })}.` + const invalid = JWS.sign({ iat: epoch - 31 }, key) JWT.verify(invalid, key, { maxTokenAge: '30s', now }) }, { instanceOf: errors.JWTExpired, code: 'ERR_JWT_EXPIRED', message: '"iat" claim timestamp check failed (too far in the past)' }) t.true(err instanceof errors.JWTClaimInvalid) @@ -828,12 +827,3 @@ test('must be a supported value', t => { t.is(err.reason, 'check_failed') }) } - -test('invalid tokens', t => { - t.throws(() => { - JWT.verify( - 'eyJ0eXAiOiJKV1QiLCJraWQiOiIyZTFkYjRmMC1mYmY5LTQxZjYtOGMxYi1hMzczYjgwZmNhYTEiLCJhbGciOiJFUzI1NiIsImlzcyI6Imh0dHBzOi8vaWRlbnRpdHktc3RhZ2luZy5kZWxpdmVyb28uY29tLyIsImNsaWVudCI6ImIyM2I0ZjM1YzIyMTI5NDQxZjMwZDMyYmI5ZmM4ZWYyIiwic2lnbmVyIjoiYXJuOmF3czplbGFzdGljbG9hZGJhbGFuY2luZzpldS13ZXN0LTE6NTE3OTAyNjYzOTE1OmxvYWRiYWxhbmNlci9hcHAvcGF5bWVudHMtZGFzaGJvYXJkLXdlYi80YzA4ZGI2NDMyMDIyOWEyIiwiZXhwIjoxNTYyNjkxNTg1fQ==.eyJlbWFpbCI6ImpvYW8udmllaXJhQGRlbGl2ZXJvby5jby51ayIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJmYW1pbHlfbmFtZSI6Ikd1ZXJyYSBWaWVpcmEiLCJnaXZlbl9uYW1lIjoiSm9hbyIsIm5hbWUiOiJKb2FvIEd1ZXJyYSBWaWVpcmEiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tLy1sTUpXTXV3R1dpYy9BQUFBQUFBQUFBSS9BQUFBQUFBQUFCVS9lNGtkTDg5UjlqZy9zOTYtYy9waG90by5qcGciLCJzdWIiOiIxMWE1YmFmMGRjNzcwNWRmMzk1ZTMzYWFkZjU2MDk4OCIsImV4cCI6MTU2MjY5MTU4NSwiaXNzIjoiaHR0cHM6Ly9pZGVudGl0eS1zdGFnaW5nLmRlbGl2ZXJvby5jb20vIn0=.DSHLJXLOfLJ-ZYcX0Vlii6Ak_jcDSkKOvNRj_rvtAyY9uYXtwo798ZrR35fgut-LuCdx0aKz2SgK0KJqw5q6dA==', - key - ) - }, { instanceOf: errors.JOSEInvalidEncoding, code: 'ERR_JOSE_INVALID_ENCODING', message: 'input is not a valid base64url encoded string' }) -}) From 2fb1d8ed8571cefa8c7f61ad9dc250218bc334f2 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 29 Jan 2020 20:38:03 +0100 Subject: [PATCH 8/9] style: remove unused requires --- lib/jwt/verify.js | 1 - test/help/base64url.test.js | 1 - test/jwk/import.test.js | 1 - 3 files changed, 3 deletions(-) diff --git a/lib/jwt/verify.js b/lib/jwt/verify.js index b0780e3b3e..dd8af88f4e 100644 --- a/lib/jwt/verify.js +++ b/lib/jwt/verify.js @@ -3,7 +3,6 @@ const epoch = require('../help/epoch') const secs = require('../help/secs') const getKey = require('../help/get_key') const { bare: verify } = require('../jws/verify') -const { KeyStore } = require('../jwks') const { JWTClaimInvalid, JWTExpired } = require('../errors') const { isString, isNotString } = require('./shared_validations') diff --git a/test/help/base64url.test.js b/test/help/base64url.test.js index 267add6a10..9a561f0597 100644 --- a/test/help/base64url.test.js +++ b/test/help/base64url.test.js @@ -1,6 +1,5 @@ const test = require('ava') -const errors = require('../../lib/errors') const base64url = require('../../lib/help/base64url') const testStr = 'fmkIOj+kafqtjMl+iC32a+9YGz0cKj/JT9Jt31uXR1la7FSXkjoBzg/F+huYm0udbM5z5qGlmPBNZASsixJLcA==' diff --git a/test/jwk/import.test.js b/test/jwk/import.test.js index e2f4cb16f7..a9aed164a4 100644 --- a/test/jwk/import.test.js +++ b/test/jwk/import.test.js @@ -6,7 +6,6 @@ const { edDSASupported, keyObjectSupported } = require('../../lib/help/runtime_s const { createSecretKey } = require('../../lib/help/key_object') const { generateKeyPairSync } = require('../macros/generate') const fixtures = require('../fixtures') -const base64url = require('../../lib/help/base64url') test('imports PrivateKeyObject and then its Key instance', t => { const k = asKey(generateKeyPairSync('ec', { namedCurve: 'P-256' }).privateKey) From 10f8336c45162bf6f2b9717dcbaa20f13ac6edfa Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 29 Jan 2020 21:15:28 +0100 Subject: [PATCH 9/9] chore(release): 1.22.0 --- CHANGELOG.md | 14 ++++++++++++++ package.json | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 549e5d1f90..a337044fad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +# [1.22.0](https://github.com/panva/jose/compare/v1.21.1...v1.22.0) (2020-01-29) + + +### Features + +* keystore filtering by JWK Key thumbprint ([a9f6f71](https://github.com/panva/jose/commit/a9f6f7135005d6231d6f42d95c02414139a89d17)) + + +### Performance Improvements + +* base64url decode, JWT.verify, JWK.Key instance re-use ([470b4c7](https://github.com/panva/jose/commit/470b4c73154e1fcf8b92726d521940e5e11c9d94)) + + + ## [1.21.1](https://github.com/panva/jose/compare/v1.21.0...v1.21.1) (2020-01-25) diff --git a/package.json b/package.json index f77d90734b..59d55e6b8e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jose", - "version": "1.21.1", + "version": "1.22.0", "description": "JSON Web Almost Everything - JWA, JWS, JWE, JWK, JWT, JWKS for Node.js with minimal dependencies", "keywords": [ "access token",