From ce69aee4dd01fe468cf15ae84b12d436125d930a Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Mon, 13 May 2019 10:57:03 +0200 Subject: [PATCH 01/22] style: index.d.ts 4 spaces [skip ci] --- lib/index.d.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/index.d.ts b/lib/index.d.ts index 72b4e18f21..aeb5d614a7 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -208,8 +208,8 @@ export namespace JWS { export function sign(payload: string | Buffer | object, key: JWK.Key, protected?: object): string namespace sign { - export function flattened(payload: string | Buffer | object, key: JWK.Key, protected?: object, header?: object): FlattenedJWS - export function general(payload: string | Buffer | object, key: JWK.Key, protected?: object, header?: object): GeneralJWS + export function flattened(payload: string | Buffer | object, key: JWK.Key, protected?: object, header?: object): FlattenedJWS + export function general(payload: string | Buffer | object, key: JWK.Key, protected?: object, header?: object): GeneralJWS } interface VerifyOptions { @@ -263,8 +263,8 @@ export namespace JWE { export function encrypt(payload: string | Buffer, key: JWK.Key, protected?: object): string namespace encrypt { - export function flattened(payload: string | Buffer, key: JWK.Key, protected?: object, header?: object, aad?: string): FlattenedJWE - export function general(payload: string | Buffer, key: JWK.Key, protected?: object, header?: object, aad?: string): GeneralJWE + export function flattened(payload: string | Buffer, key: JWK.Key, protected?: object, header?: object, aad?: string): FlattenedJWE + export function general(payload: string | Buffer, key: JWK.Key, protected?: object, header?: object, aad?: string): GeneralJWE } interface DecryptOptions { From 6b7c92ab6932afacd682d54290b307ff12387750 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 22 May 2019 09:05:54 +0200 Subject: [PATCH 02/22] chore: add sponsor placement [skip ci] --- README.md | 5 +++++ docs/README.md | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/README.md b/README.md index 2f2e880371..9bbc3bb484 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,10 @@ Found a bug? - [report it][bug]. Missing a feature? - If it wasn't already discussed before, [ask for it][suggest-feature]. Found a vulnerability? - Reach out to us via email first, see [security vulnerability disclosure][security-vulnerability]. +## Sponsor + +[auth0-logo][sponsor-auth0] If you want to quickly add secure token-based authentication to Node.js projects, feel free to check Auth0’s free plan at [auth0.com/overview][sponsor-auth0].

+ ## Support [][support-patreon] @@ -320,3 +324,4 @@ in terms of performance and API (not having well defined errors). [support-paypal]: https://www.paypal.me/panva [travis-image]: https://api.travis-ci.com/panva/jose.svg?branch=master [travis-url]: https://travis-ci.com/panva/jose +[sponsor-auth0]: https://auth0.com/overview?utm_source=GHsponsor&utm_medium=GHsponsor&utm_campaign=%40panva%2Fjose&utm_content=auth diff --git a/docs/README.md b/docs/README.md index 90a9216698..dc6b326247 100644 --- a/docs/README.md +++ b/docs/README.md @@ -9,6 +9,10 @@ - [JWE (JSON Web Encryption)](#jwe-json-web-encryption) - [errors](#errors) +## Sponsor + +[auth0-logo][sponsor-auth0] If you want to quickly add secure token-based authentication to Node.js projects, feel free to check Auth0’s free plan at [auth0.com/overview][sponsor-auth0].

+ ## Support [][support-patreon] @@ -1414,3 +1418,4 @@ if (err.code === 'ERR_JWT_MALFORMED') { [support-paypal]: https://www.paypal.me/panva [connect-core]: https://openid.net/specs/openid-connect-core-1_0.html [bug]: https://github.com/panva/jose/issues/new?labels=bug&template=bug-report.md&title=bug%3A+ +[sponsor-auth0]: https://auth0.com/overview?utm_source=GHsponsor&utm_medium=GHsponsor&utm_campaign=%40panva%2Fjose&utm_content=auth From b0ff436daf0e99e2c193401e33cb6b4630e3c8e3 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 23 May 2019 19:20:30 +0200 Subject: [PATCH 03/22] fix: reject rsa keys without all factors and exponents with a specific message --- lib/help/asn1/rsa_private_key.js | 2 +- lib/help/key_utils.js | 16 +++++++++++++++- lib/jwk/import.js | 6 +++++- test/jwk/import.test.js | 23 ++++++++++++++++++++++- 4 files changed, 43 insertions(+), 4 deletions(-) diff --git a/lib/help/asn1/rsa_private_key.js b/lib/help/asn1/rsa_private_key.js index ac3cc2b375..acd4f7575c 100644 --- a/lib/help/asn1/rsa_private_key.js +++ b/lib/help/asn1/rsa_private_key.js @@ -1,6 +1,6 @@ module.exports = function () { this.seq().obj( - this.key('version').int(), + this.key('version').int({ 0: 'two-prime', 1: 'multi' }), this.key('n').int(), this.key('e').int(), this.key('d').int(), diff --git a/lib/help/key_utils.js b/lib/help/key_utils.js index 3494127235..81328c6f8d 100644 --- a/lib/help/key_utils.js +++ b/lib/help/key_utils.js @@ -59,7 +59,11 @@ const keyObjectToJWK = { const RSAPrivateKey = asn1.get('RSAPrivateKey') const { privateKey } = PrivateKeyInfo.decode(der) - const { n, e, d, p, q, dp, dq, qi } = RSAPrivateKey.decode(privateKey) + const { version, n, e, d, p, q, dp, dq, qi } = RSAPrivateKey.decode(privateKey) + + if (version !== 'two-prime') { + throw new errors.JOSENotSupported('Private RSA keys with more than two primes are not supported') + } return { kty: 'RSA', @@ -186,6 +190,16 @@ const jwkToPem = { private (jwk) { const RSAPrivateKey = asn1.get('RSAPrivateKey') + if ('oth' in jwk) { + throw new errors.JOSENotSupported('Private RSA keys with more than two primes are not supported') + } + + if (jwk.p || jwk.q || jwk.dp || jwk.dq || jwk.qi) { + if (!(jwk.p && jwk.q && jwk.dp && jwk.dq && jwk.qi)) { + throw new errors.JWKImportFailed('all other private key parameters must be present when any one of them is present') + } + } + return RSAPrivateKey.encode({ version: 0, n: base64url.decodeToBuffer(jwk.n), diff --git a/lib/jwk/import.js b/lib/jwk/import.js index 7b1583c23e..dc42677e36 100644 --- a/lib/jwk/import.js +++ b/lib/jwk/import.js @@ -52,7 +52,11 @@ const importKey = (key, parameters) => { let pem try { pem = jwkToPem(key) - } catch (err) {} + } catch (err) { + if (err instanceof errors.JOSEError) { + throw err + } + } if (pem && key.d) { privateKey = createPrivateKey(pem) } else if (pem) { diff --git a/test/jwk/import.test.js b/test/jwk/import.test.js index ce01ff6b73..536ff3dab5 100644 --- a/test/jwk/import.test.js +++ b/test/jwk/import.test.js @@ -1,7 +1,7 @@ const test = require('ava') const crypto = require('crypto') -const { JWK: { importKey }, errors } = require('../..') +const { JWK: { importKey, generate }, errors } = require('../..') const fixtures = require('../fixtures') @@ -85,3 +85,24 @@ test('failed to import throws an error', t => { }, { instanceOf: errors.JOSENotSupported, code: 'ERR_JOSE_NOT_SUPPORTED', message: 'only RSA, EC and OKP asymmetric keys are supported' }) }) }) + + +test('fails to import RSA without all optimization parameters', async t => { + const full = (await generate('RSA')).toJWK(true) + for (const param of ['p', 'q', 'dp', 'dq', 'qi']) { + const { [param]: omit, ...jwk } = full + t.throws(() => { + importKey(jwk) + }, { instanceOf: errors.JWKImportFailed, code: 'ERR_JWK_IMPORT_FAILED', message: 'all other private key parameters must be present when any one of them is present' }) + } +}) + +test('fails to import JWK RSA with oth', async t => { + const jwk = (await generate('RSA')).toJWK(true) + t.throws(() => { + importKey({ + ...jwk, + oth: [] + }) + }, { instanceOf: errors.JOSENotSupported, code: 'ERR_JOSE_NOT_SUPPORTED', message: 'Private RSA keys with more than two primes are not supported' }) +}) From 6e3d6fd1113b72f56eefc663ceaaf5f071134876 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 23 May 2019 19:51:45 +0200 Subject: [PATCH 04/22] feat: compute private RSA key p, q, dp, dq, qi when omitted resolves #26 --- lib/help/key_utils.js | 3 + lib/help/rsa_primes.js | 149 ++++++++++++++++++++++++++++++++++++++++ test/jwe/smoke.test.js | 15 +++- test/jwk/import.test.js | 16 ++++- test/jws/smoke.test.js | 10 +++ 5 files changed, 191 insertions(+), 2 deletions(-) create mode 100644 lib/help/rsa_primes.js diff --git a/lib/help/key_utils.js b/lib/help/key_utils.js index 81328c6f8d..3654a64f7b 100644 --- a/lib/help/key_utils.js +++ b/lib/help/key_utils.js @@ -3,6 +3,7 @@ const { createPublicKey } = require('crypto') const base64url = require('./base64url') const errors = require('../errors') const asn1 = require('./asn1') +const computePrimes = require('./rsa_primes') const { OKP_CURVES, EC_CURVES } = require('./consts') const oidHexToCurve = new Map([ @@ -198,6 +199,8 @@ const jwkToPem = { if (!(jwk.p && jwk.q && jwk.dp && jwk.dq && jwk.qi)) { throw new errors.JWKImportFailed('all other private key parameters must be present when any one of them is present') } + } else { + jwk = computePrimes(jwk) } return RSAPrivateKey.encode({ diff --git a/lib/help/rsa_primes.js b/lib/help/rsa_primes.js new file mode 100644 index 0000000000..539640371b --- /dev/null +++ b/lib/help/rsa_primes.js @@ -0,0 +1,149 @@ +/* global BigInt */ + +const { randomBytes } = require('crypto') + +const base64url = require('./base64url') + +const ZERO = BigInt(0) +const ONE = BigInt(1) +const TWO = BigInt(2) + +const toJWKParameter = n => base64url.encodeBuffer(Buffer.from(n.toString(16), 'hex')) +const fromBuffer = buf => BigInt(`0x${buf.toString('hex')}`) +const bitLength = n => n.toString(2).length + +const eGcdX = (a, b) => { + let x = ZERO + let y = ONE + let u = ONE + let v = ZERO + + while (a !== ZERO) { + let q = b / a + let r = b % a + let m = x - (u * q) + let n = y - (v * q) + b = a + a = r + x = u + y = v + u = m + v = n + } + return x +} + +const gcd = (a, b) => { + let shift = ZERO + while (!((a | b) & ONE)) { + a >>= ONE + b >>= ONE + shift++ + } + while (!(a & ONE)) { + a >>= ONE + } + do { + while (!(b & ONE)) { + b >>= ONE + } + if (a > b) { + let x = a + a = b + b = x + } + b -= a + } while (b) + + return a << shift +} + +const modPow = (a, b, n) => { + a = toZn(a, n) + let result = ONE + let x = a + while (b > 0) { + var leastSignificantBit = b % TWO + b = b / TWO + if (leastSignificantBit === ONE) { + result = result * x + result = result % n + } + x = x * x + x = x % n + } + return result +} + +const randBetween = (min, max) => { + const interval = max - min + const bitLen = bitLength(interval) + let rnd + do { + rnd = fromBuffer(randBits(bitLen)) + } while (rnd > interval) + return rnd + min +} + +const randBits = (bitLength) => { + const byteLength = Math.ceil(bitLength / 8) + const rndBytes = randomBytes(byteLength) + // Fill with 0's the extra bits + rndBytes[0] = rndBytes[0] & (2 ** (bitLength % 8) - 1) + return rndBytes +} + +const toZn = (a, n) => { + a = a % n + return (a < 0) ? a + n : a +} + +const odd = (n) => { + let r = n + while (r % TWO === ZERO) { + r = r / TWO + } + return r +} + +const getPrimeFactors = (e, d, n) => { + const r = odd(e * d - ONE) + + let y + do { + let i = modPow(randBetween(TWO, n), r, n) + let o = ZERO + while (i !== ONE) { + o = i + i = (i * i) % n + } + if (o !== (n - ONE)) { + y = o + } + } while (!y) + + const p = gcd(y - ONE, n) + const q = n / p + + return p > q ? { p, q } : { p: q, q: p } +} + +module.exports = (jwk) => { + const e = fromBuffer(base64url.decodeToBuffer(jwk.e)) + const d = fromBuffer(base64url.decodeToBuffer(jwk.d)) + const n = fromBuffer(base64url.decodeToBuffer(jwk.n)) + + const { p, q } = getPrimeFactors(e, d, n) + const dp = d % (p - ONE) + const dq = d % (q - ONE) + const qi = toZn(eGcdX(toZn(q, p), p), p) + + return { + ...jwk, + p: toJWKParameter(p), + q: toJWKParameter(q), + dp: toJWKParameter(dp), + dq: toJWKParameter(dq), + qi: toJWKParameter(qi) + } +} diff --git a/test/jwe/smoke.test.js b/test/jwe/smoke.test.js index a460de7e28..852b919e6b 100644 --- a/test/jwe/smoke.test.js +++ b/test/jwe/smoke.test.js @@ -3,7 +3,7 @@ const test = require('ava') const { randomBytes } = require('crypto') const { encrypt, decrypt } = require('../../lib/jwe') -const { JWK: { importKey }, errors } = require('../..') +const { JWK: { importKey, generateSync }, errors } = require('../..') const PAYLOAD = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c' const ENCS = [ @@ -119,3 +119,16 @@ Object.entries(fixtures.PEM).forEach(([type, { private: key, public: pub }]) => }) }) }) + +{ + const rsa = generateSync('RSA') + const dKey = importKey({ kty: 'RSA', e: rsa.e, n: rsa.n, d: rsa.d }) + const eKey = importKey({ kty: 'RSA', e: rsa.e, n: rsa.n }) + eKey.algorithms('wrapKey').forEach((alg) => { + ENCS.forEach((enc) => { + if (alg === 'ECDH-ES' && ['A192CBC-HS384', 'A256CBC-HS512'].includes(enc)) return + test(`key RSA (min) > alg ${alg} > ${enc}`, success, eKey, dKey, alg, enc) + test(`key RSA (min) > alg ${alg} > ${enc} (negative cases)`, failure, eKey, dKey, alg, enc) + }) + }) +} diff --git a/test/jwk/import.test.js b/test/jwk/import.test.js index 536ff3dab5..ff79d69f3a 100644 --- a/test/jwk/import.test.js +++ b/test/jwk/import.test.js @@ -1,7 +1,7 @@ const test = require('ava') const crypto = require('crypto') -const { JWK: { importKey, generate }, errors } = require('../..') +const { JWS, JWE, JWK: { importKey, generate }, errors } = require('../..') const fixtures = require('../fixtures') @@ -86,6 +86,20 @@ test('failed to import throws an error', t => { }) }) +test('minimal RSA test', async t => { + const key = await generate('RSA') + const { d, e, n } = key.toJWK(true) + const minKey = importKey({ kty: 'RSA', d, e, n }) + key.algorithms('sign').forEach((alg) => { + JWS.verify(JWS.sign({}, key), minKey, { alg }) + JWS.verify(JWS.sign({}, minKey), key, { alg }) + }) + key.algorithms('wrapKey').forEach((alg) => { + JWE.decrypt(JWE.encrypt('foo', key), minKey, { alg }) + JWE.decrypt(JWE.encrypt('foo', minKey), key, { alg }) + }) + t.pass() +}) test('fails to import RSA without all optimization parameters', async t => { const full = (await generate('RSA')).toJWK(true) diff --git a/test/jws/smoke.test.js b/test/jws/smoke.test.js index 4a6ba8717c..c3d2d28280 100644 --- a/test/jws/smoke.test.js +++ b/test/jws/smoke.test.js @@ -75,3 +75,13 @@ sym.algorithms('sign').forEach((alg) => { test(`key ${sym.kty} > alg ${alg}`, success, sym, sym, alg) test(`key ${sym.kty} > alg ${alg} (negative cases)`, failure, sym, sym, alg) }) + +{ + const rsa = generateSync('RSA') + const sKey = importKey({ kty: 'RSA', e: rsa.e, n: rsa.n, d: rsa.d }) + const vKey = importKey({ kty: 'RSA', e: rsa.e, n: rsa.n }) + sKey.algorithms('sign').forEach((alg) => { + test(`key RSA (min) > alg ${alg}`, success, sKey, vKey, alg) + test(`key RSA (min) > alg ${alg} (negative cases)`, failure, sKey, vKey, alg) + }) +} From 2557e50dc01cf5de30eba5f7fe4c9fd54aee0378 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 23 May 2019 20:03:51 +0200 Subject: [PATCH 05/22] test: update test coverage tool to c8 --- lib/jwk/key/base.js | 14 +++++++------- lib/jwks/keystore.js | 2 +- package.json | 36 ++++++++++++++---------------------- 3 files changed, 22 insertions(+), 30 deletions(-) diff --git a/lib/jwk/key/base.js b/lib/jwk/key/base.js index b8e00e8bf8..902959016b 100644 --- a/lib/jwk/key/base.js +++ b/lib/jwk/key/base.js @@ -159,7 +159,7 @@ class Key { }, {})) } - /* istanbul ignore next */ + /* c8 ignore next 8 */ [inspect.custom] () { return `${this.constructor.name} ${inspect(this.toJWK(false), { depth: Infinity, @@ -169,32 +169,32 @@ class Key { })}` } - /* istanbul ignore next */ + /* c8 ignore next 3 */ [THUMBPRINT_MATERIAL] () { throw new Error(`"[THUMBPRINT_MATERIAL]()" is not implemented on ${this.constructor.name}`) } - /* istanbul ignore next */ + /* c8 ignore next 3 */ algorithms () { throw new Error(`"algorithms()" is not implemented on ${this.constructor.name}`) } - /* istanbul ignore next */ + /* c8 ignore next 3 */ static async generate () { throw new Error(`"static async generate()" is not implemented on ${this.name}`) } - /* istanbul ignore next */ + /* c8 ignore next 3 */ static generateSync () { throw new Error(`"static generateSync()" is not implemented on ${this.name}`) } - /* istanbul ignore next */ + /* c8 ignore next 3 */ static get [PUBLIC_MEMBERS] () { throw new Error(`"static get [PUBLIC_MEMBERS]()" is not implemented on ${this.name}`) } - /* istanbul ignore next */ + /* c8 ignore next 3 */ static get [PRIVATE_MEMBERS] () { throw new Error(`"static get [PRIVATE_MEMBERS]()" is not implemented on ${this.name}`) } diff --git a/lib/jwks/keystore.js b/lib/jwks/keystore.js index 147e11ca33..6e76e035da 100644 --- a/lib/jwks/keystore.js +++ b/lib/jwks/keystore.js @@ -130,7 +130,7 @@ class KeyStore { return this.#keys.size } - /* istanbul ignore next */ + /* c8 ignore next 8 */ [inspect.custom] () { return `${this.constructor.name} ${inspect(this.toJWKS(false), { depth: Infinity, diff --git a/package.json b/package.json index 9f118cf76c..e82cd8cf7c 100644 --- a/package.json +++ b/package.json @@ -6,18 +6,18 @@ "compact", "decode", "decrypt", + "eddsa", "encrypt", "flattened", "general", "jose", + "jsonwebtoken", "jwa", "jwe", "jwk", "jwks", "jws", "jwt", - "jsonwebtoken", - "eddsa", "sign", "verify" ], @@ -32,12 +32,22 @@ "main": "lib/index.js", "types": "lib/index.d.ts", "scripts": { - "coverage": "nyc ava", + "coverage": "c8 -r lcov -r text-summary ava", "lint": "standard", "lint-fix": "standard --fix", "test": "ava", "watch": "ava --watch" }, + "husky": { + "hooks": { + "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" + } + }, + "commitlint": { + "extends": [ + "@commitlint/config-conventional" + ] + }, "dependencies": { "asn1.js": "^5.0.1" }, @@ -45,8 +55,8 @@ "@commitlint/cli": "^7.5.2", "@commitlint/config-conventional": "^7.5.0", "ava": "^1.2.1", + "c8": "^5.0.0", "husky": "^2.1.0", - "nyc": "^14.0.0", "standard": "^12.0.1" }, "engines": { @@ -58,23 +68,5 @@ "files": [ "test/**/*.test.js" ] - }, - "commitlint": { - "extends": [ - "@commitlint/config-conventional" - ] - }, - "husky": { - "hooks": { - "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" - } - }, - "nyc": { - "all": true, - "cache": false, - "reporter": [ - "lcov", - "text-summary" - ] } } From b1abdff6371f11aa3b707ae8aeb84d8939488aff Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 23 May 2019 20:09:26 +0200 Subject: [PATCH 06/22] chore(release): 1.1.0 --- CHANGELOG.md | 14 ++++++++++++++ package.json | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20e2d07e49..fb6319c0f3 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.1.0](https://github.com/panva/jose/compare/v1.0.2...v1.1.0) (2019-05-23) + + +### Bug Fixes + +* reject rsa keys without all factors and exponents with a specific message ([b0ff436](https://github.com/panva/jose/commit/b0ff436)) + + +### Features + +* compute private RSA key p, q, dp, dq, qi when omitted ([6e3d6fd](https://github.com/panva/jose/commit/6e3d6fd)), closes [#26](https://github.com/panva/jose/issues/26) + + + ## [1.0.2](https://github.com/panva/jose/compare/v1.0.1...v1.0.2) (2019-05-13) diff --git a/package.json b/package.json index e82cd8cf7c..261727080d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@panva/jose", - "version": "1.0.2", + "version": "1.1.0", "description": "JSON Web Almost Everything - JWA, JWS, JWE, JWK, JWT, JWKS for Node.js with minimal dependencies", "keywords": [ "compact", From 5496a6094547fe974ffa99ddb38e2ac1458f060a Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sat, 25 May 2019 18:54:34 +0200 Subject: [PATCH 07/22] chore: add FUNDING.yml --- .github/FUNDING.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000..c54c475e0a --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +github: panva +patreon: panva +custom: https://www.paypal.me/panva From 2eae2937603ce6387739514bb134d2296c1704a3 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sat, 25 May 2019 22:18:27 +0200 Subject: [PATCH 08/22] feat: instances of JWKS.KeyStore are now iterable (e.g. for ... of) --- lib/jwks/keystore.js | 6 ++++++ test/jwks/keystore.test.js | 10 ++++++++++ 2 files changed, 16 insertions(+) diff --git a/lib/jwks/keystore.js b/lib/jwks/keystore.js index 6e76e035da..c81c2d3021 100644 --- a/lib/jwks/keystore.js +++ b/lib/jwks/keystore.js @@ -139,6 +139,12 @@ class KeyStore { sorted: true })}` } + + * [Symbol.iterator] () { + for (const key of this.#keys) { + yield key + } + } } module.exports = KeyStore diff --git a/test/jwks/keystore.test.js b/test/jwks/keystore.test.js index 7aba9bdeb3..af12836848 100644 --- a/test/jwks/keystore.test.js +++ b/test/jwks/keystore.test.js @@ -199,3 +199,13 @@ test('.fromJWKS() input validation', t => { }, { instanceOf: TypeError, message: 'jwks must be a JSON Web Key Set formatted object' }) }) }) + +test('keystore instance is an iterator', t => { + const ks = new KeyStore() + ks.generateSync('EC') + ks.generateSync('RSA') + for (const key of ks) { + t.truthy(key) + } + t.pass() +}) From 9d46c48fd5d3ac19f8570564bf717c6edca157fb Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sat, 25 May 2019 22:34:28 +0200 Subject: [PATCH 09/22] feat: add support for JWK x5c, x5t and x5t#S256 --- README.md | 6 +-- docs/README.md | 31 ++++++++++++++ lib/help/node_alg.js | 20 +-------- lib/index.d.ts | 50 ++++++++++++++--------- lib/jwk/import.js | 10 ++++- lib/jwk/key/base.js | 69 +++++++++++++++++++++++++++++++- lib/jwk/thumbprint.js | 4 ++ lib/jwks/keystore.js | 23 +++++++++-- test/jwk/x5c_thumbprints.test.js | 65 ++++++++++++++++++++++++++++++ test/jwks/keystore.test.js | 35 +++++++++++++++- 10 files changed, 262 insertions(+), 51 deletions(-) create mode 100644 test/jwk/x5c_thumbprints.test.js diff --git a/README.md b/README.md index 9bbc3bb484..cbdc4ed2eb 100644 --- a/README.md +++ b/README.md @@ -75,16 +75,12 @@ Won't implement: - ✕ JWS embedded key / referenced verification - one can decode the header and pass the (`x5c`, `jwk`) to `JWK.importKey` and validate with that key, similarly the application can handle fetching and then instantiating the referenced `x5u` - or `jku` in its own code. This way you opt-in to these behaviours and for `x5c` specifically - the recipient is responsible for validating the certificate chain is trusted + or `jku` in its own code. This way you opt-in to these behaviours. - ✕ JWS detached content - one can remove/attach the payload after/before the respective operation - ✕ "none" alg support - no crypto, no use -Not Planned / PR | Use-Case | Discussion Welcome: -- ◯ `x5c`, `x5t`, `x5t#S256`, `x5u` etc `JWK.Key` fields -
diff --git a/docs/README.md b/docs/README.md index dc6b326247..66901fb5d4 100644 --- a/docs/README.md +++ b/docs/README.md @@ -33,6 +33,9 @@ I can continue maintaining it and adding new features carefree. You may also don - [key.alg](#keyalg) - [key.use](#keyuse) - [key.kid](#keykid) + - [key.x5c](#keyx5c) + - [key.x5t](#keyx5t) + - [key['x5t#S256']](#keyx5ts256) - [key.key_ops](#keykey_ops) - [key.thumbprint](#keythumbprint) - [key.type](#keytype) @@ -121,6 +124,34 @@ defined in [RFC7638][spec-thumbprint]. --- +#### `key.x5c` + +Returns the key's X.509 Certificate Chain Parameter if set + +- `string[]` + +--- + +#### `key.x5t` + +Returns the key's X.509 Certificate SHA-1 Thumbprint Parameter if set. This +property can be either be set manually by the JWK producer or left to @panva/jose to compute based +on the first certificate in the key's `x5c`. + +- `` + +--- + +#### `key['x5t#S256']` + +Returns the key's X.509 Certificate SHA-256 Thumbprint Parameter if set. This +property can be either be set manually by the JWK producer or left to @panva/jose to compute based +on the first certificate in the key's `x5c`. + +- `` + +--- + #### `key.key_ops` Returns the key's JWK Key Operations Parameter if set. If set the key can only be used for the diff --git a/lib/help/node_alg.js b/lib/help/node_alg.js index f5b95ee267..10c8802d43 100644 --- a/lib/help/node_alg.js +++ b/lib/help/node_alg.js @@ -1,19 +1 @@ -module.exports = (alg) => { - switch (alg) { - case 'RS256': - case 'PS256': - case 'HS256': - case 'ES256': - return 'sha256' - case 'RS384': - case 'PS384': - case 'HS384': - case 'ES384': - return 'sha384' - case 'RS512': - case 'PS512': - case 'HS512': - case 'ES512': - return 'sha512' - } -} +module.exports = alg => `sha${alg.substr(-3)}` diff --git a/lib/index.d.ts b/lib/index.d.ts index aeb5d614a7..fc4e75fbe6 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -4,19 +4,24 @@ import { KeyObject, PrivateKeyInput, PublicKeyInput } from 'crypto' type use = 'sig' | 'enc' type keyOperation = 'sign' | 'verify' | 'encrypt' | 'decrypt' | 'wrapKey' | 'unwrapKey' | 'deriveKey' -interface KeyParameters { +interface BasicParameters { alg?: string use?: use kid?: string key_ops?: keyOperation[] } +interface KeyParameters extends BasicParameters { + x5c?: string[] + x5t?: string + 'x5t#S256'?: string +} type ECCurve = 'P-256' | 'P-384' | 'P-521' type OKPCurve = 'Ed25519' | 'Ed448' | 'X25519' | 'X448' type keyType = 'RSA' | 'EC' | 'OKP' | 'oct' type asymmetricKeyObjectTypes = 'private' | 'public' type keyObjectTypes = asymmetricKeyObjectTypes | 'secret' -interface JWKOctKey extends KeyParameters { +interface JWKOctKey extends BasicParameters { // no x5c kty: 'oct', k?: string } @@ -73,6 +78,9 @@ export namespace JWK { key_ops?: keyOperation[] kid: string thumbprint: string + x5c?: string[] + x5t?: string + 'x5t#S256'?: string toPEM(private?: boolean, encoding?: pemEncodingOptions): string @@ -138,20 +146,22 @@ export namespace JWK { export function importKey(jwk: JWKECKey): ECKey export function importKey(jwk: JWKOKPKey): OKPKey - export function generate(kty: 'EC', crv?: ECCurve, parameters?: KeyParameters, private?: boolean): Promise - export function generate(kty: 'OKP', crv?: OKPCurve, parameters?: KeyParameters, private?: boolean): Promise - export function generate(kty: 'RSA', bitlength?: number, parameters?: KeyParameters, private?: boolean): Promise - export function generate(kty: 'oct', bitlength?: number, parameters?: KeyParameters): Promise + export function generate(kty: 'EC', crv?: ECCurve, parameters?: BasicParameters, private?: boolean): Promise + export function generate(kty: 'OKP', crv?: OKPCurve, parameters?: BasicParameters, private?: boolean): Promise + export function generate(kty: 'RSA', bitlength?: number, parameters?: BasicParameters, private?: boolean): Promise + export function generate(kty: 'oct', bitlength?: number, parameters?: BasicParameters): Promise - export function generateSync(kty: 'EC', crv?: ECCurve, parameters?: KeyParameters, private?: boolean): ECKey - export function generateSync(kty: 'OKP', crv?: OKPCurve, parameters?: KeyParameters, private?: boolean): OKPKey - export function generateSync(kty: 'RSA', bitlength?: number, parameters?: KeyParameters, private?: boolean): RSAKey - export function generateSync(kty: 'oct', bitlength?: number, parameters?: KeyParameters): OctKey + export function generateSync(kty: 'EC', crv?: ECCurve, parameters?: BasicParameters, private?: boolean): ECKey + export function generateSync(kty: 'OKP', crv?: OKPCurve, parameters?: BasicParameters, private?: boolean): OKPKey + export function generateSync(kty: 'RSA', bitlength?: number, parameters?: BasicParameters, private?: boolean): RSAKey + export function generateSync(kty: 'oct', bitlength?: number, parameters?: BasicParameters): OctKey } export namespace JWKS { - interface KeyQuery extends KeyParameters { - kty: keyType + interface KeyQuery extends BasicParameters { + kty?: keyType + x5t?: string + 'x5t#S256'?: string } class KeyStore { @@ -166,15 +176,15 @@ export namespace JWKS { toJWKS(private?: boolean): JSONWebKeySet - generate(kty: 'EC', crv?: ECCurve, parameters?: KeyParameters, private?: boolean): void - generate(kty: 'OKP', crv?: OKPCurve, parameters?: KeyParameters, private?: boolean): void - generate(kty: 'RSA', bitlength?: number, parameters?: KeyParameters, private?: boolean): void - generate(kty: 'oct', bitlength?: number, parameters?: KeyParameters): void + generate(kty: 'EC', crv?: ECCurve, parameters?: BasicParameters, private?: boolean): void + generate(kty: 'OKP', crv?: OKPCurve, parameters?: BasicParameters, private?: boolean): void + generate(kty: 'RSA', bitlength?: number, parameters?: BasicParameters, private?: boolean): void + generate(kty: 'oct', bitlength?: number, parameters?: BasicParameters): void - generateSync(kty: 'EC', crv?: ECCurve, parameters?: KeyParameters, private?: boolean): void - generateSync(kty: 'OKP', crv?: OKPCurve, parameters?: KeyParameters, private?: boolean): void - generateSync(kty: 'RSA', bitlength?: number, parameters?: KeyParameters, private?: boolean): void - generateSync(kty: 'oct', bitlength?: number, parameters?: KeyParameters): void + generateSync(kty: 'EC', crv?: ECCurve, parameters?: BasicParameters, private?: boolean): void + generateSync(kty: 'OKP', crv?: OKPCurve, parameters?: BasicParameters, private?: boolean): void + generateSync(kty: 'RSA', bitlength?: number, parameters?: BasicParameters, private?: boolean): void + generateSync(kty: 'oct', bitlength?: number, parameters?: BasicParameters): void } } diff --git a/lib/jwk/import.js b/lib/jwk/import.js index dc42677e36..61646f8975 100644 --- a/lib/jwk/import.js +++ b/lib/jwk/import.js @@ -13,7 +13,15 @@ const OctKey = require('./key/oct') const importable = new Set(['string', 'buffer', 'object']) const mergedParameters = (target = {}, source = {}) => { - return Object.assign({}, { alg: source.alg, use: source.use, kid: source.kid, key_ops: source.key_ops }, target) + return Object.assign({}, { + alg: source.alg, + key_ops: source.key_ops, + kid: source.kid, + use: source.use, + x5c: source.x5c, + x5t: source.x5t, + 'x5t#S256': source['x5t#S256'] + }, target) } const importKey = (key, parameters) => { diff --git a/lib/jwk/key/base.js b/lib/jwk/key/base.js index 902959016b..e6e6087483 100644 --- a/lib/jwk/key/base.js +++ b/lib/jwk/key/base.js @@ -1,3 +1,4 @@ +const { strict: assert } = require('assert') const { createPublicKey } = require('crypto') const { inspect } = require('util') @@ -11,7 +12,7 @@ const thumbprint = require('../thumbprint') const errors = require('../../errors') class Key { - constructor (keyObject, { alg, use, kid, key_ops: ops } = {}) { + constructor (keyObject, { alg, use, kid, key_ops: ops, x5c, x5t, 'x5t#S256': x5t256 } = {}) { if (use !== undefined) { if (typeof use !== 'string' || !USES.has(use)) { throw new TypeError('`use` must be either "sig" or "enc" string when provided') @@ -31,7 +32,7 @@ class Key { } if (ops !== undefined) { - if (!Array.isArray(ops) || !ops.length || ops.some(x => typeof x !== 'string')) { + if (!Array.isArray(ops) || !ops.length || ops.some(o => typeof o !== 'string')) { throw new TypeError('`key_ops` must be a non-empty array of strings when provided') } ops = Array.from(new Set(ops)).filter(x => OPS.has(x)) @@ -46,6 +47,33 @@ class Key { } } + if (x5c !== undefined) { + if (!Array.isArray(x5c) || !x5c.length || x5c.some(c => typeof c !== 'string')) { + throw new TypeError('`x5c` must be an array of one or more PKIX certificates when provided') + } + + x5c.forEach((cert, i) => { + let publicKey + try { + publicKey = createPublicKey({ + key: `-----BEGIN CERTIFICATE-----\n${cert}\n-----END CERTIFICATE-----`, format: 'pem' + }) + } catch (err) { + throw new errors.JWKInvalid(`\`x5c\` member at index ${i} is not a valid base64-encoded DER PKIX certificate`) + } + if (i === 0) { + try { + assert.deepEqual( + publicKey.export({ type: 'spki', format: 'der' }), + (keyObject.type === 'public' ? keyObject : createPublicKey(keyObject)).export({ type: 'spki', format: 'der' }) + ) + } catch (err) { + throw new errors.JWKInvalid('The key in the first `x5c` certificate MUST match the public key represented by the JWK') + } + } + }) + } + Object.defineProperties(this, { [KEYOBJECT]: { value: isObject(keyObject) ? undefined : keyObject }, type: { value: keyObject.type }, @@ -54,6 +82,7 @@ class Key { secret: { value: keyObject.type === 'secret' }, alg: { value: alg, enumerable: alg !== undefined }, use: { value: use, enumerable: use !== undefined }, + x5c: { value: x5c, enumerable: x5c !== undefined }, key_ops: { enumerable: ops !== undefined, ...(ops ? { get () { return [...ops] } } : { value: undefined }) @@ -68,6 +97,30 @@ class Key { configurable: true }) }, + ...(x5c ? { + x5t: { + enumerable: true, + ...(x5t ? { value: x5t } : { + get () { + Object.defineProperty(this, 'x5t', { value: thumbprint.x5t(this.x5c[0]), configurable: false }) + return this.x5t + }, + configurable: true + }) + } + } : undefined), + ...(x5c ? { + 'x5t#S256': { + enumerable: true, + ...(x5t256 ? { value: x5t256 } : { + get () { + Object.defineProperty(this, 'x5t#S256', { value: thumbprint['x5t#S256'](this.x5c[0]), configurable: false }) + return this['x5t#S256'] + }, + configurable: true + }) + } + } : undefined), thumbprint: { get () { Object.defineProperty(this, 'thumbprint', { value: thumbprint.kid(this[THUMBPRINT_MATERIAL]()), configurable: false }) @@ -131,6 +184,18 @@ class Key { result.use = this.use } + if (this.x5c) { + result.x5c = this.x5c + } + + if (this.x5t) { + result.x5t = this.x5t + } + + if (this['x5t#S256']) { + result['x5t#S256'] = this['x5t#S256'] + } + return result } diff --git a/lib/jwk/thumbprint.js b/lib/jwk/thumbprint.js index 631cfc4ef6..ac23cc55c1 100644 --- a/lib/jwk/thumbprint.js +++ b/lib/jwk/thumbprint.js @@ -2,4 +2,8 @@ const { createHash } = require('crypto') const base64url = require('../help/base64url') +const xt5 = (hash, cert) => base64url.encodeBuffer(createHash(hash).update(Buffer.from(cert, 'base64')).digest()) + module.exports.kid = components => base64url.encodeBuffer(createHash('sha256').update(JSON.stringify(components)).digest()) +module.exports.x5t = xt5.bind(undefined, 'sha1') +module.exports['x5t#S256'] = xt5.bind(undefined, 'sha256') diff --git a/lib/jwks/keystore.js b/lib/jwks/keystore.js index c81c2d3021..9adacc664d 100644 --- a/lib/jwks/keystore.js +++ b/lib/jwks/keystore.js @@ -6,7 +6,7 @@ const Key = require('../jwk/key/base') const importKey = require('../jwk/import') const { USES_MAPPING } = require('../help/consts') -const keyscore = (key, { alg, kid, use, ops }) => { +const keyscore = (key, { alg, kid, use, ops, x5t, x5t256 }) => { let score = 0 if (alg && key.alg) { @@ -17,6 +17,14 @@ const keyscore = (key, { alg, kid, use, ops }) => { score++ } + if (x5t && key.x5t) { + score++ + } + + if (x5t256 && key['x5t#S256']) { + score++ + } + if (use && key.use) { score++ } @@ -52,11 +60,12 @@ class KeyStore { return new KeyStore(...keys) } - all ({ alg, kid, use, kty, key_ops: ops } = {}) { + all ({ alg, kid, use, kty, key_ops: ops, x5t, 'x5t#S256': x5t256 } = {}) { 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') } + const search = { alg, kid, use, ops, x5t, x5t256 } return [...this.#keys] .filter((key) => { let candidate = true @@ -69,6 +78,14 @@ class KeyStore { candidate = false } + if (candidate && x5t !== undefined && key.x5t !== x5t) { + candidate = false + } + + if (candidate && x5t256 !== undefined && key['x5t#S256'] !== x5t256) { + candidate = false + } + if (candidate && kty !== undefined && key.kty !== kty) { candidate = false } @@ -91,7 +108,7 @@ class KeyStore { return candidate }) - .sort((first, second) => keyscore(second, { alg, kid, use, ops }) - keyscore(first, { alg, kid, use, ops })) + .sort((first, second) => keyscore(second, search) - keyscore(first, search)) } get (...args) { diff --git a/test/jwk/x5c_thumbprints.test.js b/test/jwk/x5c_thumbprints.test.js new file mode 100644 index 0000000000..0abc6fe4a8 --- /dev/null +++ b/test/jwk/x5c_thumbprints.test.js @@ -0,0 +1,65 @@ +const test = require('ava') + +const errors = require('../../lib/errors') + +const { JWK: { importKey } } = require('../..') + +const jwk = { + kty: 'RSA', + use: 'sig', + kid: '1b94c', + n: 'vrjOfz9Ccdgx5nQudyhdoR17V-IubWMeOZCwX_jj0hgAsz2J_pqYW08PLbK_PdiVGKPrqzmDIsLI7sA25VEnHU1uCLNwBuUiCO11_-7dYbsr4iJmG0Qu2j8DsVyT1azpJC_NG84Ty5KKthuCaPod7iI7w0LK9orSMhBEwwZDCxTWq4aYWAchc8t-emd9qOvWtVMDC2BXksRngh6X5bUYLy6AyHKvj-nUy1wgzjYQDwHMTplCoLtU-o-8SNnZ1tmRoGE9uJkBLdh5gFENabWnU5m1ZqZPdwS-qo-meMvVfJb6jJVWRpl2SUtCnYG2C32qvbWbjZ_jBPD5eunqsIo1vQ', + e: 'AQAB', + x5c: [ + 'MIIDQjCCAiqgAwIBAgIGATz/FuLiMA0GCSqGSIb3DQEBBQUAMGIxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDTzEPMA0GA1UEBxMGRGVudmVyMRwwGgYDVQQKExNQaW5nIElkZW50aXR5IENvcnAuMRcwFQYDVQQDEw5CcmlhbiBDYW1wYmVsbDAeFw0xMzAyMjEyMzI5MTVaFw0xODA4MTQyMjI5MTVaMGIxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDTzEPMA0GA1UEBxMGRGVudmVyMRwwGgYDVQQKExNQaW5nIElkZW50aXR5IENvcnAuMRcwFQYDVQQDEw5CcmlhbiBDYW1wYmVsbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL64zn8/QnHYMeZ0LncoXaEde1fiLm1jHjmQsF/449IYALM9if6amFtPDy2yvz3YlRij66s5gyLCyO7ANuVRJx1NbgizcAblIgjtdf/u3WG7K+IiZhtELto/A7Fck9Ws6SQvzRvOE8uSirYbgmj6He4iO8NCyvaK0jIQRMMGQwsU1quGmFgHIXPLfnpnfajr1rVTAwtgV5LEZ4Iel+W1GC8ugMhyr4/p1MtcIM42EA8BzE6ZQqC7VPqPvEjZ2dbZkaBhPbiZAS3YeYBRDWm1p1OZtWamT3cEvqqPpnjL1XyW+oyVVkaZdklLQp2Btgt9qr21m42f4wTw+Xrp6rCKNb0CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAh8zGlfSlcI0o3rYDPBB07aXNswb4ECNIKG0CETTUxmXl9KUL+9gGlqCz5iWLOgWsnrcKcY0vXPG9J1r9AqBNTqNgHq2G03X09266X5CpOe1zFo+Owb1zxtp3PehFdfQJ610CDLEaS9V9Rqp17hCyybEpOGVwe8fnk+fbEL2Bo3UPGrpsHzUoaGpDftmWssZkhpBJKVMJyf/RuP2SmmaIzmnw9JiSlYhzo4tpzd5rFXhjRbg4zW9C+2qok+2+qDM1iJ684gPHMIY8aLWrdgQTxkumGmTqgawR+N5MDtdPTEQ0XfIBc2cJEUyMTY5MPvACWpkA6SdS4xSvdXK3IVfOWA==' + ] +} + +test('x5c can be imported and have their X.509 cert thumbprints calculated', t => { + let key + t.notThrows(() => { key = importKey(jwk) }) + t.deepEqual(key.x5c, jwk.x5c) + const asJWK = key.toJWK() + t.deepEqual(asJWK.x5c, jwk.x5c) + ;[key.x5t, asJWK.x5t, key['x5t#S256'], asJWK['x5t#S256']].forEach((prop) => { + t.truthy(prop) + t.is(typeof prop, 'string') + }) +}) + +test('checks that x5c is an array of valid PKIX certificates', t => { + ;[[], {}, false, 1].forEach((value) => { + t.throws(() => { + importKey({ + ...jwk, + x5c: value + }) + }, { instanceOf: TypeError, message: '`x5c` must be an array of one or more PKIX certificates when provided' }) + t.throws(() => { + importKey({ + ...jwk, + x5c: [value] + }) + }, { instanceOf: TypeError, message: '`x5c` must be an array of one or more PKIX certificates when provided' }) + }) +}) + +test('checks that first x5c member must represent the key', t => { + t.throws(() => { + importKey({ + ...jwk, + x5c: [ + 'MIIC/zCCAeegAwIBAgIJYdZUZz2rikftMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNVBAMTEnBhbnZhLmV1LmF1dGgwLmNvbTAeFw0xNzEwMTgxNTExMjBaFw0zMTA2MjcxNTExMjBaMB0xGzAZBgNVBAMTEnBhbnZhLmV1LmF1dGgwLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKROvB+A+ZlFV1AXl75tVegjaCuBE7CiXHNstVZ/F6fKl6OvIRhAW3YKnJEglzVvHw0q46Nw48yBdbbKjdwGo1jbrI15D2+MYPy8xlMfDzEqNWBjOsgnA1nhFFDXD7wITwFRMtlRKVvKMa19QCmMFrpQ2qcloMne/DzSvxlEnVA6DG1SYqHR/gdK5hoRATJkwHXQ5F/nUxD3BOAyyjsU5RsGJAeVVS4Yf532xmziIbda3iV4LMUiHUb1v8Oy2sDncYF+imq/sbHGgE7dyv5R5AsYHGANgvIPMHJ1QTFSQVU0lxPy+EWnLk9abVOZYzD6O5YRdJ29UWVtQ1q5UcyrF18CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUSUcoPFHi/vm7dw1rt/IRmxvLMyowDgYDVR0PAQH/BAQDAgKEMA0GCSqGSIb3DQEBCwUAA4IBAQBcBXXBcbqliVOHkTgxocSYNUajcgIKjgeqG9RKFkbHfPuK/Hn80vQhd6mBKJTIyM7fY7DPh1/PjRsAyDQEwouHWItcM6iJBSdAkPq2DPfCkpUOi7MHrhXSouU1X4IOBvAl94k9Z8oj5k12KWVH8jZn5G03lwkWUgSfkLJ0Dh86+4sF2W4Dz2qZUXZuQbUL5eJcWRpfEZowff+T8xsiRjcIEpgfLz4nWonijtvEWESEa3bYpI9pI5OXLImgVJLGxVaUktsGIexQ6eM1AoxBYE7E+nbN/rwo30XWGbTkYecisySSYuzVn2c0xnC/8ZvW+gJ4SkzRDjlOAbm3R0r5j7b1' + ] + }) + }, { instanceOf: errors.JWKInvalid, code: 'ERR_JWK_INVALID', message: 'The key in the first `x5c` certificate MUST match the public key represented by the JWK' }) + t.throws(() => { + importKey({ + ...jwk, + x5c: [ + jwk.x5c[0], + 'MIIC/zCCAeegAwIBAgIJYdZUZz2rikftMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNVBAMTEnBhbnZhLmV1LmF1dGgwLmNvbTAeFw0xNzEwMTgxNTExMjBaFw0zMTA2MjcxNTExMjBaMB0xGzAZBgNVBAMTEnBhbnZhLmV1LmF1dGgwLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKROvB+A+ZlFV1AXl75tVegjaCuBE7CiXHNstVZ/F6fKl6OvIRhAW3YKnJEglzVvHw0q46Nw48yBdbbKjdwGo1jbrI15D2+MYPy8xlMfDzEqNWBjOsgnA1nhFFDXD7wITwFRMtlRKVvKMa19QCmMFrpQ2qcloMne/DzSvxlEnVA6DG1SYqHR/gdK5hoRATJkwHXQ5F/nUxD3BOAyyjsU5RsGJAeVVS4Yf532xmziIbda3iV4LMUiHUb1v8Oy2sDncYF+imq/sbHGgE7dyv5R5AsYHGANgvIPMHJ1QTFSQVU0lxPy+EWnLk9abVOZYzD6O5YRdJ29UWVtQ1q5UcyrF18CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUSUcoPFHi/vm7dw1rt/IRmxvLMyowDgYDVR0PAQH/BAQDAgKEMA0GCSqGSIb3DQEBCwUAA4IBAQBcBXXBcbqliVOHkTgxocSYNUajcgIKjgeqG9RKFkbHfPuK/Hn80vQhd6mBKJTIyM7fY7DPh1/PjRsAyDQEwouHWItcM6iJBSdAkPq2DPfCkpUOi7MHrhXSouU1X4IOBvAl94k9Z8oj5k12KWVH8jZn5G03lwkWUgSfkLJ0Dh86+4sF2W4Dz2qZUXZuQbUL5eJcWRpfEZowff+T8xsiRjcIEpgfLz4nWonijtvEWESEa3bYpI9pI5OXLImgVJLGxVaUktsGIexQ6eM1AoxBYE7E+nbN/rwo30XWGbTkYecisySSYuzVn2c0xnC/8ZvW+gJ4SkzRDjlOAbm3R0r5j7b1f' + ] + }) + }, { instanceOf: errors.JWKInvalid, code: 'ERR_JWK_INVALID', message: '`x5c` member at index 1 is not a valid base64-encoded DER PKIX certificate' }) +}) diff --git a/test/jwks/keystore.test.js b/test/jwks/keystore.test.js index af12836848..88cd22c480 100644 --- a/test/jwks/keystore.test.js +++ b/test/jwks/keystore.test.js @@ -1,7 +1,16 @@ const test = require('ava') const KeyStore = require('../../lib/jwks/keystore') -const { generateSync } = require('../../lib/jwk') +const { importKey, generateSync } = require('../../lib/jwk') + +const withX5C = { + kty: 'RSA', + n: 'vrjOfz9Ccdgx5nQudyhdoR17V-IubWMeOZCwX_jj0hgAsz2J_pqYW08PLbK_PdiVGKPrqzmDIsLI7sA25VEnHU1uCLNwBuUiCO11_-7dYbsr4iJmG0Qu2j8DsVyT1azpJC_NG84Ty5KKthuCaPod7iI7w0LK9orSMhBEwwZDCxTWq4aYWAchc8t-emd9qOvWtVMDC2BXksRngh6X5bUYLy6AyHKvj-nUy1wgzjYQDwHMTplCoLtU-o-8SNnZ1tmRoGE9uJkBLdh5gFENabWnU5m1ZqZPdwS-qo-meMvVfJb6jJVWRpl2SUtCnYG2C32qvbWbjZ_jBPD5eunqsIo1vQ', + e: 'AQAB', + x5c: [ + 'MIIDQjCCAiqgAwIBAgIGATz/FuLiMA0GCSqGSIb3DQEBBQUAMGIxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDTzEPMA0GA1UEBxMGRGVudmVyMRwwGgYDVQQKExNQaW5nIElkZW50aXR5IENvcnAuMRcwFQYDVQQDEw5CcmlhbiBDYW1wYmVsbDAeFw0xMzAyMjEyMzI5MTVaFw0xODA4MTQyMjI5MTVaMGIxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDTzEPMA0GA1UEBxMGRGVudmVyMRwwGgYDVQQKExNQaW5nIElkZW50aXR5IENvcnAuMRcwFQYDVQQDEw5CcmlhbiBDYW1wYmVsbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL64zn8/QnHYMeZ0LncoXaEde1fiLm1jHjmQsF/449IYALM9if6amFtPDy2yvz3YlRij66s5gyLCyO7ANuVRJx1NbgizcAblIgjtdf/u3WG7K+IiZhtELto/A7Fck9Ws6SQvzRvOE8uSirYbgmj6He4iO8NCyvaK0jIQRMMGQwsU1quGmFgHIXPLfnpnfajr1rVTAwtgV5LEZ4Iel+W1GC8ugMhyr4/p1MtcIM42EA8BzE6ZQqC7VPqPvEjZ2dbZkaBhPbiZAS3YeYBRDWm1p1OZtWamT3cEvqqPpnjL1XyW+oyVVkaZdklLQp2Btgt9qr21m42f4wTw+Xrp6rCKNb0CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAh8zGlfSlcI0o3rYDPBB07aXNswb4ECNIKG0CETTUxmXl9KUL+9gGlqCz5iWLOgWsnrcKcY0vXPG9J1r9AqBNTqNgHq2G03X09266X5CpOe1zFo+Owb1zxtp3PehFdfQJ610CDLEaS9V9Rqp17hCyybEpOGVwe8fnk+fbEL2Bo3UPGrpsHzUoaGpDftmWssZkhpBJKVMJyf/RuP2SmmaIzmnw9JiSlYhzo4tpzd5rFXhjRbg4zW9C+2qok+2+qDM1iJ684gPHMIY8aLWrdgQTxkumGmTqgawR+N5MDtdPTEQ0XfIBc2cJEUyMTY5MPvACWpkA6SdS4xSvdXK3IVfOWA==' + ] +} test('constructor', t => { t.notThrows(() => { @@ -148,6 +157,30 @@ test('.all() and .get() kid filter', t => { t.is(ks.get({ kid: 'foobar' }), k) }) +test('.all() and .get() x5t filter and sort', t => { + const k = importKey(withX5C) + const ks = new KeyStore(k) + t.deepEqual(ks.all({ x5t: 'baz' }), []) + t.deepEqual(ks.all({ x5t: '4pNenEBLv0JpLIdugWxQkOsZcK0' }), [k]) + t.is(ks.get({ x5t: 'baz' }), undefined) + t.is(ks.get({ x5t: '4pNenEBLv0JpLIdugWxQkOsZcK0' }), k) + const k2 = importKey({ ...withX5C, alg: 'RS256' }) + ks.add(k2) + t.is(ks.get({ x5t: '4pNenEBLv0JpLIdugWxQkOsZcK0', alg: 'RS256' }), k2) +}) + +test('.all() and .get() x5t#S256 filter and sort', t => { + const k = importKey(withX5C) + const ks = new KeyStore(k) + t.deepEqual(ks.all({ 'x5t#S256': 'baz' }), []) + t.deepEqual(ks.all({ 'x5t#S256': 'pJm2BBpkB8y7tCqrWM0X37WOmQTO8zQw-VpxVgBb21I' }), [k]) + t.is(ks.get({ 'x5t#S256': 'baz' }), undefined) + t.is(ks.get({ 'x5t#S256': 'pJm2BBpkB8y7tCqrWM0X37WOmQTO8zQw-VpxVgBb21I' }), k) + const k2 = importKey({ ...withX5C, alg: 'RS256' }) + ks.add(k2) + t.is(ks.get({ 'x5t#S256': 'pJm2BBpkB8y7tCqrWM0X37WOmQTO8zQw-VpxVgBb21I', alg: 'RS256' }), k2) +}) + test('.all() and .get() kty filter', t => { const ks = new KeyStore() ks.generateSync('RSA') From 0231841687f5418e963fe82e83548f7a1512ebbf Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sat, 25 May 2019 22:53:03 +0200 Subject: [PATCH 10/22] chore(release): 1.2.0 --- CHANGELOG.md | 10 ++++++++++ package.json | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb6319c0f3..a838f626b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ 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.2.0](https://github.com/panva/jose/compare/v1.1.0...v1.2.0) (2019-05-25) + + +### Features + +* add support for JWK x5c, x5t and x5t#S256 ([9d46c48](https://github.com/panva/jose/commit/9d46c48)), closes [x5t#S256](https://github.com/x5t/issues/S256) +* instances of JWKS.KeyStore are now iterable (e.g. for ... of) ([2eae293](https://github.com/panva/jose/commit/2eae293)) + + + # [1.1.0](https://github.com/panva/jose/compare/v1.0.2...v1.1.0) (2019-05-23) diff --git a/package.json b/package.json index 261727080d..8feda899ce 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@panva/jose", - "version": "1.1.0", + "version": "1.2.0", "description": "JSON Web Almost Everything - JWA, JWS, JWE, JWK, JWT, JWKS for Node.js with minimal dependencies", "keywords": [ "compact", From 9f9542e54f721fcd14960751d0b00810d2c0fddc Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sat, 25 May 2019 23:07:54 +0200 Subject: [PATCH 11/22] docs: fix changelog [skip ci] --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a838f626b3..46328875f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ All notable changes to this project will be documented in this file. See [standa ### Features -* add support for JWK x5c, x5t and x5t#S256 ([9d46c48](https://github.com/panva/jose/commit/9d46c48)), closes [x5t#S256](https://github.com/x5t/issues/S256) +* add support for JWK x5c, x5t and x5t#S256 ([9d46c48](https://github.com/panva/jose/commit/9d46c48)) * instances of JWKS.KeyStore are now iterable (e.g. for ... of) ([2eae293](https://github.com/panva/jose/commit/2eae293)) From d4258beaf33373ae4dbe3ee5c6a54bc523f0a591 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sun, 26 May 2019 15:44:20 +0200 Subject: [PATCH 12/22] docs: update docs/README.md [skip ci] --- docs/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/README.md b/docs/README.md index 66901fb5d4..f6239ba769 100644 --- a/docs/README.md +++ b/docs/README.md @@ -135,8 +135,8 @@ Returns the key's X.509 Certificate Chain Parameter if set #### `key.x5t` Returns the key's X.509 Certificate SHA-1 Thumbprint Parameter if set. This -property can be either be set manually by the JWK producer or left to @panva/jose to compute based -on the first certificate in the key's `x5c`. +property can be either set manually by the JWK producer or left to the library to compute based +on the first certificate in the key's `x5c`, the latter is preferred. - `` @@ -145,8 +145,8 @@ on the first certificate in the key's `x5c`. #### `key['x5t#S256']` Returns the key's X.509 Certificate SHA-256 Thumbprint Parameter if set. This -property can be either be set manually by the JWK producer or left to @panva/jose to compute based -on the first certificate in the key's `x5c`. +property can be either set manually by the JWK producer or left to the library to compute based +on the first certificate in the key's `x5c`, the latter is preferred. - `` From 1432681891f9a64a1cc44c6cd32a47b503f7373b Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Mon, 27 May 2019 09:56:08 +0200 Subject: [PATCH 13/22] docs: update README.md [skip ci] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cbdc4ed2eb..60283f4fca 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ The following specifications are implemented by @panva/jose - JSON Web Key (JWK) - [RFC7517][spec-jwk] - JSON Web Algorithms (JWA) - [RFC7518][spec-jwa] - JSON Web Token (JWT) - [RFC7519][spec-jwt] -- JSON Web Key (JWK) Thumbprint - [RFC7638][spec-thumbprint] +- JSON Web Key Thumbprint - [RFC7638][spec-thumbprint] - JWS Unencoded Payload Option - [RFC7797][spec-b64] - CFRG Elliptic Curve Signatures (EdDSA) - [RFC8037][spec-okp] From 67beaae8157459a47d3c242537ea7e6480c2b4be Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Mon, 27 May 2019 17:22:29 +0200 Subject: [PATCH 14/22] style: enable lint with esnext features --- .c8rc.json | 3 +++ lib/jwks/keystore.js | 2 +- package.json | 7 ++++++- 3 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 .c8rc.json diff --git a/.c8rc.json b/.c8rc.json new file mode 100644 index 0000000000..7cb7d759b6 --- /dev/null +++ b/.c8rc.json @@ -0,0 +1,3 @@ +{ + "reporter": ["lcov", "text-summary"] +} diff --git a/lib/jwks/keystore.js b/lib/jwks/keystore.js index 9adacc664d..8abb875135 100644 --- a/lib/jwks/keystore.js +++ b/lib/jwks/keystore.js @@ -47,7 +47,7 @@ class KeyStore { throw new TypeError('all keys must be an instances of a key instantiated by JWK.importKey') } - this.#keys = new Set(keys); + this.#keys = new Set(keys) } static fromJWKS (jwks) { diff --git a/package.json b/package.json index 8feda899ce..d36cb78637 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "flattened", "general", "jose", + "json web token", "jsonwebtoken", "jwa", "jwe", @@ -32,7 +33,7 @@ "main": "lib/index.js", "types": "lib/index.d.ts", "scripts": { - "coverage": "c8 -r lcov -r text-summary ava", + "coverage": "c8 ava", "lint": "standard", "lint-fix": "standard --fix", "test": "ava", @@ -55,6 +56,7 @@ "@commitlint/cli": "^7.5.2", "@commitlint/config-conventional": "^7.5.0", "ava": "^1.2.1", + "babel-eslint": "^10.0.1", "c8": "^5.0.0", "husky": "^2.1.0", "standard": "^12.0.1" @@ -68,5 +70,8 @@ "files": [ "test/**/*.test.js" ] + }, + "standard": { + "parser": "babel-eslint" } } From bf3d3cec1c9c44640d98d99eed6481a139bad678 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 29 May 2019 18:46:15 +0200 Subject: [PATCH 15/22] chore: update github bug report template [skip ci] --- .github/ISSUE_TEMPLATE/bug-report.md | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index 3d3b708356..ff7039ada8 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -28,5 +28,4 @@ A clear and concise description of what you expected to happen. Add any other context about the problem here. - [ ] the bug is happening on latest @panva/jose too. - - [ ] i have tried DEBUG (see readme.md) and can see the issue is with the provider and not my code. - [ ] i have searched the issues tracker on github for similar issues and couldn't find anything related. From 68591969f6b56017bf1b627c34ba3a58915fa418 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 6 Jun 2019 17:53:52 +0200 Subject: [PATCH 16/22] chore: update devDependencies --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index d36cb78637..81d5d7c8e7 100644 --- a/package.json +++ b/package.json @@ -53,12 +53,12 @@ "asn1.js": "^5.0.1" }, "devDependencies": { - "@commitlint/cli": "^7.5.2", - "@commitlint/config-conventional": "^7.5.0", - "ava": "^1.2.1", + "@commitlint/cli": "^8.0.0", + "@commitlint/config-conventional": "^8.0.0", + "ava": "^2.0.0", "babel-eslint": "^10.0.1", "c8": "^5.0.0", - "husky": "^2.1.0", + "husky": "^2.4.0", "standard": "^12.0.1" }, "engines": { From 83186af455ca95b490c669f45b76a3f02000563b Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sun, 9 Jun 2019 11:23:24 +0200 Subject: [PATCH 17/22] chore: remove github funding yml section [skip ci] --- .github/FUNDING.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index c54c475e0a..c17dee0ead 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,3 +1,3 @@ -github: panva +# github: panva patreon: panva custom: https://www.paypal.me/panva From 80cdd4f4f5e3e724676fa3a2368dabf95dfcb170 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sun, 16 Jun 2019 18:32:58 +0200 Subject: [PATCH 18/22] chore: update dependencies --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 81d5d7c8e7..e17f3c4eec 100644 --- a/package.json +++ b/package.json @@ -50,15 +50,15 @@ ] }, "dependencies": { - "asn1.js": "^5.0.1" + "asn1.js": "^5.1.0" }, "devDependencies": { "@commitlint/cli": "^8.0.0", "@commitlint/config-conventional": "^8.0.0", - "ava": "^2.0.0", + "ava": "^2.1.0", "babel-eslint": "^10.0.1", "c8": "^5.0.0", - "husky": "^2.4.0", + "husky": "^2.4.1", "standard": "^12.0.1" }, "engines": { From 5b53cb01552fcff3d48a97ff465ffbf69b286db7 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 20 Jun 2019 23:32:13 +0200 Subject: [PATCH 19/22] fix: limit calculation of missing RSA private components - this deprecates the use of `JWK.importKey` in favor of `JWK.asKey` - this deprecates the use of `JWKS.KeyStore.fromJWKS` in favor of `JWKS.asKeyStore` Both `JWK.importKey` and `JWKS.KeyStore.fromJWKS` could have resulted in the process getting blocked when large bitsize RSA private keys were missing their components and could also result in an endless calculation loop when the private key's private exponent was outright invalid or tampered with. The new methods still allow to import private RSA keys with these optimization key parameters missing but its disabled by default and one should choose to enable it when working with keys from trusted sources It is recommended not to use @panva/jose versions with this feature in its original on-by-default form - v1.1.0 and v1.2.0 These will --- CHANGELOG.md | 4 +- README.md | 6 +- docs/README.md | 92 ++++++++++++------- lib/help/base64url.js | 12 ++- lib/help/key_utils.js | 12 ++- lib/help/rsa_primes.js | 20 ++++ lib/index.d.ts | 22 +++++ lib/jwe/decrypt.js | 10 +- lib/jwe/encrypt.js | 2 +- lib/jwk/import.js | 14 ++- lib/jwk/index.js | 8 +- lib/jwks/index.js | 2 +- lib/jwks/keystore.js | 35 ++++--- lib/jws/sign.js | 2 +- lib/jws/verify.js | 4 +- lib/jwt/verify.js | 2 +- test/cookbook/4_1.rsa_v15_signature.test.js | 6 +- test/cookbook/4_2.rsa-pss_signature.test.js | 6 +- test/cookbook/4_3.ecdsa_signature.test.js | 6 +- ...4_4.hmac-sha2_integrity_protection.test.js | 6 +- ....protecting_specific_header_fields.test.js | 6 +- .../4_7.protecting_content_only.test.js | 6 +- test/cookbook/4_8.multiple_signatures.test.js | 4 +- ...on_using_rsa_v15_and_aes-hmac-sha2.test.js | 6 +- ...ing_additional_authentication_data.test.js | 6 +- ....protecting_specific_header_fields.test.js | 6 +- .../5_12.protecting_content_only.test.js | 6 +- ....encrypting_to_multiple_recipients.test.js | 4 +- ...yption_using_rsa-oaep_with_aes-gcm.test.js | 6 +- ...aes-keywrap_with-aes-cbc-hmac-sha2.test.js | 6 +- ...dh-es_and_aes-keywrap_with_aes-gcm.test.js | 6 +- ...ing_ecdh-es_with_aes-cbc-hmac-sha2.test.js | 6 +- ..._6.direct_encryption_using_aes-gcm.test.js | 6 +- ...gcm_keywrap_with_aes-cbc-hmac-sha2.test.js | 6 +- ...rap_using_aes-keywrap_with_aes-gcm.test.js | 6 +- test/cookbook/5_9.compressed_content.test.js | 6 +- test/cookbook/jwk.test.js | 18 ++-- .../rfc7797.4_1.hmac-sha2_b64_false.test.js | 6 +- .../rfc7797.4_2.hmac-sha2_b64_false.js | 6 +- test/cookbook/rfc8037.a4.ed25519.test.js | 6 +- test/help/base64url.test.js | 4 - test/help/ecdsa_signatures.test.js | 8 +- test/jwe/sanity.test.js | 4 +- test/jwe/smoke.test.js | 12 +-- test/jwk/general.test.js | 4 +- test/jwk/import.test.js | 53 ++++++----- test/jwk/key_ops.test.js | 34 +++---- test/jwk/oct.test.js | 8 +- test/jwk/x5c_thumbprints.test.js | 12 +-- test/jwks/keystore.test.js | 50 ++++++---- test/jws/b64.test.js | 2 +- test/jws/sanity.test.js | 4 +- test/jws/smoke.test.js | 10 +- 53 files changed, 359 insertions(+), 245 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46328875f5..6d8a1a6d77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ 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.2.0](https://github.com/panva/jose/compare/v1.1.0...v1.2.0) (2019-05-25) +# YANKED [1.2.0](https://github.com/panva/jose/compare/v1.1.0...v1.2.0) (2019-05-25) ### Features @@ -12,7 +12,7 @@ All notable changes to this project will be documented in this file. See [standa -# [1.1.0](https://github.com/panva/jose/compare/v1.0.2...v1.1.0) (2019-05-23) +# YANKED [1.1.0](https://github.com/panva/jose/compare/v1.0.2...v1.1.0) (2019-05-23) ### Bug Fixes diff --git a/README.md b/README.md index 60283f4fca..e2e51e0739 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ Pending Node.js Support 🤞: Won't implement: - ✕ JWS embedded key / referenced verification - - one can decode the header and pass the (`x5c`, `jwk`) to `JWK.importKey` and validate with that + - one can decode the header and pass the (`x5c`, `jwk`) to `JWK.asKey` and validate with that key, similarly the application can handle fetching and then instantiating the referenced `x5u` or `jku` in its own code. This way you opt-in to these behaviours. - ✕ JWS detached content @@ -137,14 +137,14 @@ const { Prepare your Keys and KeyStores. See the [documentation][documentation-jwk] for more. ```js -const key = jose.JWK.importKey(fs.readFileSync('path/to/key/file')) +const key = jose.JWK.asKey(fs.readFileSync('path/to/key/file')) const jwk = { kty: 'EC', kid: 'dl4M_fcI7XoFCsQ22PYrQBkuxZ2pDcbDimcdFmmXM98', crv: 'P-256', x: 'v37avifcL-xgh8cy6IFzcINqqmFLc2JF20XUpn4Y2uQ', y: 'QTwy27XgP7ZMOdGOSopAHB-FU1JMQn3J9GEWGtUXreQ' } -const anotherKey = jose.JWK.importKey(jwk) +const anotherKey = jose.JWK.asKey(jwk) const keystore = new jose.JWK.KeyStore(key, key2) ``` diff --git a/docs/README.md b/docs/README.md index f6239ba769..e3420fc72f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -45,10 +45,10 @@ I can continue maintaining it and adding new features carefree. You may also don - [key.algorithms([operation])](#keyalgorithmsoperation) - [key.toJWK([private])](#keytojwkprivate) - [key.toPEM([private[, encoding]])](#keytopemprivate-encoding) -- JWK.importKey - - [JWK.importKey(key[, options]) asymmetric key import](#jwkimportkeykey-options-asymmetric-key-import) - - [JWK.importKey(secret[, options]) secret key import](#jwkimportkeysecret-options-secret-key-import) - - [JWK.importKey(jwk) JWK-formatted key import](#jwkimportkeyjwk-jwk-formatted-key-import) +- JWK.asKey + - [JWK.asKey(key[, options]) asymmetric key import](#jwkaskeykey-options-asymmetric-key-import) + - [JWK.asKey(secret[, options]) secret key import](#jwkaskeysecret-options-secret-key-import) + - [JWK.asKey(jwk[, options]) JWK-formatted key import](#jwkaskeyjwk-options-jwk-formatted-key-import) - [JWK.generate(kty[, crvOrSize[, options[, private]]]) generating new keys](#jwkgeneratekty-crvorsize-options-private-generating-new-keys) - [JWK.generateSync(kty[, crvOrSize[, options[, private]]])](#jwkgeneratesynckty-crvorsize-options-private) - [JWK.isKey(object)](#jwkiskeyobject) @@ -60,7 +60,7 @@ how to get a `` instances generated or instantiated from existing key m ```js const { JWK } = require('@panva/jose') -// { importKey: [Function: importKey], +// { asKey: [Function: asKey], // generate: [AsyncFunction: generate], // generateSync: [Function: generateSync] } ``` @@ -70,7 +70,7 @@ const { JWK } = require('@panva/jose') #### Class: `` and `` | `` | `` | `` ``, ``, `` and `` represent a key usable for JWS and JWE operations. -The `JWK.importKey()` method is used to retrieve a key representation of an existing key or secret. +The `JWK.asKey()` method is used to retrieve a key representation of an existing key or secret. `JWK.generate()` method is used to generate a new random key. ``, ``, `` and `` inherit methods from `` and in addition @@ -330,7 +330,7 @@ key.toPEM(true, { passphrase: 'super-strong', cipher: 'aes-256-cbc' }) --- -#### `JWK.importKey(key[, options])` asymmetric key import +#### `JWK.asKey(key[, options])` asymmetric key import Imports an asymmetric private or public key. Supports importing JWK formatted keys (private, public, secrets), `pem` and `der` formatted private and public keys, `pem` formatted X.509 certificates. @@ -362,9 +362,9 @@ formats ```js const { readFileSync } = require('fs') -const { JWK: { importKey } } = require('@panva/jose') +const { JWK: { asKey } } = require('@panva/jose') -const key = importKey(readFileSync('path/to/key/file')) +const key = asKey(readFileSync('path/to/key/file')) // ECKey { // kty: 'EC', // public: true, @@ -377,7 +377,7 @@ const key = importKey(readFileSync('path/to/key/file')) --- -#### `JWK.importKey(secret[, options])` secret key import +#### `JWK.asKey(secret[, options])` secret key import Imports a symmetric key. @@ -394,9 +394,9 @@ Imports a symmetric key. Example (Click to expand) ```js -const { JWK: { importKey } } = require('@panva/jose') +const { JWK: { asKey } } = require('@panva/jose') -const key = importKey(Buffer.from('8yHym6h5CG5FylbzrCn8fhxEbp3kOaTsgLaawaaJ')) +const key = asKey(Buffer.from('8yHym6h5CG5FylbzrCn8fhxEbp3kOaTsgLaawaaJ')) // OctKey { // kty: 'oct', // kid: [Getter], @@ -406,7 +406,7 @@ const key = importKey(Buffer.from('8yHym6h5CG5FylbzrCn8fhxEbp3kOaTsgLaawaaJ')) --- -#### `JWK.importKey(jwk)` JWK-formatted key import +#### `JWK.asKey(jwk[, options])` JWK-formatted key import Imports a JWK formatted key. This supports JWK formatted RSA, EC, OKP and oct keys. Asymmetrical keys may be both private and public. @@ -420,18 +420,28 @@ keys may be both private and public. [RFC7638][spec-thumbprint] - `e`, `n` properties as `` for RSA public keys - `e`, `n`, `d`, `p`, `q`, `dp`, `dq`, `qi` properties as `` for RSA private keys + - `e`, `n`, `d` properties as `` for RSA private keys without optimization parametes (only + with `calculateMissingRSAPrimes` option, see below) - `crv`, `x`, `y` properties as `` for EC public keys - `crv`, `x`, `y`, `d` properties as `` for EC private keys - `crv`, `x`, properties as `` for OKP public keys - `crv`, `x`, `d` properties as `` for OKP private keys - `k` properties as `` for secret oct keys +- `options`: `` + - `calculateMissingRSAPrimes`: `` **Default** 'false'. This option is really only in + effect when importing private RSA JWK keys, by default, keys without the optimization private + key parameters (p, q, dp, dq, qi) won't imported because their calculation is heavy and prone + to blocking the process. Setting this option to true will enable these keys to be imported, + albeit at your own risk. Depending on the key size the calculation takes long and it should + only be used for JWK keys from trusted sources. - Returns: `` | `` | `` | `` +
Example (Click to expand) ```js -const { JWK: { importKey } } = require('@panva/jose') +const { JWK: { asKey } } = require('@panva/jose') const jwk = { kty: 'RSA', kid: 'r1LkbBo3925Rb2ZFFrKyU3MVex9T2817Kx0vbi6i_Kc', @@ -440,7 +450,7 @@ const jwk = { n: 'xwQ72P9z9OYshiQ-ntDYaPnnfwG6u9JAdLMZ5o0dmjlcyrvwQRdoFIKPnO65Q8mh6F_LDSxjxa2Yzo_wdjhbPZLjfUJXgCzm54cClXzT5twzo7lzoAfaJlkTsoZc2HFWqmcri0BuzmTFLZx2Q7wYBm0pXHmQKF0V-C1O6NWfd4mfBhbM-I1tHYSpAMgarSm22WDMDx-WWI7TEzy2QhaBVaENW9BKaKkJklocAZCxk18WhR0fckIGiWiSM5FcU1PY2jfGsTmX505Ub7P5Dz75Ygqrutd5tFrcqyPAtPTFDk8X1InxkkUwpP3nFU5o50DGhwQolGYKPGtQ-ZtmbOfcWQ' } -const key = importKey(jwk) +const key = asKey(jwk) // RSAKey { // kty: 'RSA', // public: true, @@ -563,7 +573,7 @@ Returns 'true' if the value is an instance of ``. - [keystore.generate(...)](#keystoregenerate) - [keystore.generateSync(...)](#keystoregeneratesync) - [keystore.toJWKS([private])](#keystoretojwksprivate) - - [JWKS.KeyStore.fromJWKS(jwks)](#jwkskeystorefromjwksjwks) + - [JWKS.asKeyStore(jwks[, options])](#jwksaskeystorejwks-options) ```js @@ -584,7 +594,7 @@ an existing store. Creates a new KeyStore, either empty or populated. -- `keys`: `` Array of key keys instantiated by `JWK.importKey()` +- `keys`: `` Array of key keys instantiated by `JWK.asKey()` - Returns: `` --- @@ -671,29 +681,41 @@ Exports the keystore to a JSON Web Key Set formatted object. --- -#### `JWKS.KeyStore.fromJWKS(jwks)` +#### `JWKS.asKeyStore(jwks[, options])` Creates a new KeyStore from a JSON Web Key Set. - `jwks`: `` JWKS formatted object (`{ keys: [{ kty: '...', ... }, ...] }`) +- `options`: `` + - `calculateMissingRSAPrimes`: `` **Default** 'false'. This option is really only in + effect when the JWKS contains private RSA JWK keys, by default, keys without the optimization + private key parameters (p, q, dp, dq, qi) won't imported because their calculation is heavy and + prone to blocking the process. Setting this option to true will enable these keys to be + imported, albeit at your own risk. Depending on the key size the calculation takes long and it + should only be used for JWKS from trusted sources. - Returns: ``
Example (Click to expand) ```js -const { JWKS: { KeyStore } } = require('@panva/jose') -const jwks = { keys: - [ { kty: 'RSA', - kid: 'gqUcZ2TjhmNrVOd1d27tedkabhOTs9WghMHIyjIBn7Y', - e: 'AQAB', - n: - 'vi1Aui6R0rUL_7pdcFKKMhBF25h4x8WiTZ4w66eNZhwIp48lz-vBuyUUrSR-RwcuvnxlXdjBdSaN-PZkNRDv2bXE3mVtjZgoYyzQlGLJ1wduQaBXIkrQWxc7yzL91MvtP1kWwFHHrQHZRlpiFQQm9gNCy2wXCTbWGT9kjrR1W1bkwhmOKK4rF-hMgaCNDrtEQ6xWknxV8aXW4itouJ0pJv8xplc6J14f_SNq6arVUcAZ26EzJYC2fcvqwsrnKzvW7QxQGQzh-u9Tn82Tl14Omh1KDV8C7Vb_m8XClv_9zOrKBGdaTl1zgINyMEaa_IMophnBgK_kAXvtVvEThQ93GQ', - use: 'enc' } ] } -const ks = KeyStore.fromJWKS(jwks) +const { JWKS: { KeyStore, asKeyStore } } = require('@panva/jose') +const jwks = { + keys: [ + { kty: 'RSA', + kid: 'gqUcZ2TjhmNrVOd1d27tedkabhOTs9WghMHIyjIBn7Y', + e: 'AQAB', + n: + 'vi1Aui6R0rUL_7pdcFKKMhBF25h4x8WiTZ4w66eNZhwIp48lz-vBuyUUrSR-RwcuvnxlXdjBdSaN-PZkNRDv2bXE3mVtjZgoYyzQlGLJ1wduQaBXIkrQWxc7yzL91MvtP1kWwFHHrQHZRlpiFQQm9gNCy2wXCTbWGT9kjrR1W1bkwhmOKK4rF-hMgaCNDrtEQ6xWknxV8aXW4itouJ0pJv8xplc6J14f_SNq6arVUcAZ26EzJYC2fcvqwsrnKzvW7QxQGQzh-u9Tn82Tl14Omh1KDV8C7Vb_m8XClv_9zOrKBGdaTl1zgINyMEaa_IMophnBgK_kAXvtVvEThQ93GQ', + use: 'enc' } + ] +} +const ks = asKeyStore(jwks) // KeyStore {} ks.size // 1 +ks instanceof KeyStore +// true ```
@@ -750,7 +772,7 @@ that will be used to sign with is either provided as part of the 'options.algori ```js const { JWT, JWK } = require('@panva/jose') -const key = JWK.importKey({ +const key = JWK.asKey({ kty: 'oct', k: 'hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg' }) @@ -820,7 +842,7 @@ Verifies the claims and signature of a JSON Web Token. ```js const { JWK, JWT } = require('@panva/jose') -const key = JWK.importKey({ +const key = JWK.asKey({ kty: 'oct', k: 'hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg' }) @@ -914,11 +936,11 @@ signatures of the same payload) using the General JWS JSON Serialization Syntax. ```js const { JWK, JWS } = require('@panva/jose') -const key = JWK.importKey({ +const key = JWK.asKey({ kty: 'oct', k: 'hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg' }) -const key2 = JWK.importKey({ +const key2 = JWK.asKey({ kty: 'oct', k: 'AAPapAv4LbFbiVawEjagUBluYqN5rhna-8nuldDvOx8' }) @@ -997,7 +1019,7 @@ provided `` instance. ```js const { JWK, JWS } = require('@panva/jose') -const key = JWK.importKey({ +const key = JWK.asKey({ kty: 'oct', k: 'hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg' }) @@ -1031,7 +1053,7 @@ inferred from the provided `` instance. ```js const { JWK, JWS } = require('@panva/jose') -const key = JWK.importKey({ +const key = JWK.asKey({ kty: 'oct', k: 'hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg' }) @@ -1073,11 +1095,11 @@ Verifies the provided JWS in either serialization with a given `` or `< ```js const { JWK, JWS, JWKS } = require('@panva/jose') -const key = JWK.importKey({ +const key = JWK.asKey({ kty: 'oct', k: 'hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg' }) -const key2 = JWK.importKey({ +const key2 = JWK.asKey({ kty: 'oct', k: 'AAPapAv4LbFbiVawEjagUBluYqN5rhna-8nuldDvOx8' }) diff --git a/lib/help/base64url.js b/lib/help/base64url.js index df95d14be7..83b07bb801 100644 --- a/lib/help/base64url.js +++ b/lib/help/base64url.js @@ -1,3 +1,5 @@ +const b64uRegExp = /^[a-zA-Z0-9_-]*$/ + const fromBase64 = (base64) => { return base64.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_') } @@ -14,11 +16,17 @@ const encodeBuffer = (buf) => { return fromBase64(buf.toString('base64')) } -const decode = (input, encoding = 'utf8') => { - return Buffer.from(toBase64(input), 'base64').toString(encoding) +const decode = (input) => { + if (!b64uRegExp.test(input)) { + throw new TypeError('input is not a valid base64url encoded string') + } + return Buffer.from(toBase64(input), 'base64').toString('utf8') } const decodeToBuffer = (input) => { + if (!b64uRegExp.test(input)) { + throw new TypeError('input is not a valid base64url encoded string') + } return Buffer.from(toBase64(input), 'base64') } diff --git a/lib/help/key_utils.js b/lib/help/key_utils.js index 3654a64f7b..3814849a10 100644 --- a/lib/help/key_utils.js +++ b/lib/help/key_utils.js @@ -188,7 +188,7 @@ const concatEcPublicKey = (x, y) => ({ const jwkToPem = { RSA: { - private (jwk) { + private (jwk, { calculateMissingRSAPrimes }) { const RSAPrivateKey = asn1.get('RSAPrivateKey') if ('oth' in jwk) { @@ -197,10 +197,12 @@ const jwkToPem = { if (jwk.p || jwk.q || jwk.dp || jwk.dq || jwk.qi) { if (!(jwk.p && jwk.q && jwk.dp && jwk.dq && jwk.qi)) { - throw new errors.JWKImportFailed('all other private key parameters must be present when any one of them is present') + throw new errors.JWKInvalid('all other private key parameters must be present when any one of them is present') } - } else { + } else if (calculateMissingRSAPrimes) { jwk = computePrimes(jwk) + } else if (!calculateMissingRSAPrimes) { + throw new errors.JOSENotSupported('importing private RSA keys without all other private key parameters is not enabled, see documentation and its advisory on how and when its ok to enable it') } return RSAPrivateKey.encode({ @@ -293,7 +295,7 @@ const okpCrvToOid = (crv) => { } } -module.exports.jwkToPem = (jwk) => { +module.exports.jwkToPem = (jwk, { calculateMissingRSAPrimes = false } = {}) => { switch (jwk.kty) { case 'EC': if (!EC_CURVES.has(jwk.crv)) { @@ -312,7 +314,7 @@ module.exports.jwkToPem = (jwk) => { } if (jwk.d) { - return jwkToPem[jwk.kty].private(jwk) + return jwkToPem[jwk.kty].private(jwk, { calculateMissingRSAPrimes }) } return jwkToPem[jwk.kty].public(jwk) diff --git a/lib/help/rsa_primes.js b/lib/help/rsa_primes.js index 539640371b..5d77ec069a 100644 --- a/lib/help/rsa_primes.js +++ b/lib/help/rsa_primes.js @@ -3,6 +3,7 @@ const { randomBytes } = require('crypto') const base64url = require('./base64url') +const errors = require('../errors') const ZERO = BigInt(0) const ONE = BigInt(1) @@ -106,14 +107,29 @@ const odd = (n) => { return r } +// not sold on these values +const maxCountWhileNoY = 30 +const maxCountWhileInot0 = 22 + const getPrimeFactors = (e, d, n) => { const r = odd(e * d - ONE) + let countWhileNoY = 0 let y do { + countWhileNoY++ + if (countWhileNoY === maxCountWhileNoY) { + throw new errors.JWKImportFailed('failed to calculate missing primes') + } + + let countWhileInot0 = 0 let i = modPow(randBetween(TWO, n), r, n) let o = ZERO while (i !== ONE) { + countWhileInot0++ + if (countWhileInot0 === maxCountWhileInot0) { + throw new errors.JWKImportFailed('failed to calculate missing primes') + } o = i i = (i * i) % n } @@ -133,6 +149,10 @@ module.exports = (jwk) => { const d = fromBuffer(base64url.decodeToBuffer(jwk.d)) const n = fromBuffer(base64url.decodeToBuffer(jwk.n)) + if (d >= n) { + throw new errors.JWKInvalid('invalid RSA private exponent') + } + const { p, q } = getPrimeFactors(e, d, n) const dp = d % (p - ONE) const dq = d % (q - ONE) diff --git a/lib/index.d.ts b/lib/index.d.ts index fc4e75fbe6..8d112ae843 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -59,6 +59,10 @@ interface JSONWebKeySet { keys: JSONWebKey[] } +interface ImportOptions { + calculateMissingRSAPrimes?: boolean +} + export namespace JWK { interface pemEncodingOptions { @@ -139,6 +143,17 @@ export namespace JWK { export function isKey(object: any): boolean + export function asKey(keyObject: KeyObject, parameters?: KeyParameters): RSAKey | ECKey | OKPKey | OctKey + export function asKey(key: PrivateKeyInput | PublicKeyInput | string | Buffer, parameters?: KeyParameters): RSAKey | ECKey | OKPKey | OctKey + export function asKey(jwk: JWKOctKey): OctKey + export function asKey(jwk: JWKRSAKey, options?: ImportOptions): RSAKey + export function asKey(jwk: JWKECKey): ECKey + export function asKey(jwk: JWKOKPKey): OKPKey + + + /* + * @deprecated in favor of asKey + */ export function importKey(keyObject: KeyObject, parameters?: KeyParameters): RSAKey | ECKey | OKPKey | OctKey export function importKey(key: PrivateKeyInput | PublicKeyInput | string | Buffer, parameters?: KeyParameters): RSAKey | ECKey | OKPKey | OctKey export function importKey(jwk: JWKOctKey): OctKey @@ -185,7 +200,14 @@ export namespace JWKS { generateSync(kty: 'OKP', crv?: OKPCurve, parameters?: BasicParameters, private?: boolean): void generateSync(kty: 'RSA', bitlength?: number, parameters?: BasicParameters, private?: boolean): void generateSync(kty: 'oct', bitlength?: number, parameters?: BasicParameters): void + + /* + * @deprecated in favor of JWKS.asKeyStore + */ + static fromJWKS(jwks: JSONWebKeySet): KeyStore } + + export function asKeyStore(jwks: JSONWebKeySet, options?: ImportOptions): KeyStore } export namespace JWS { diff --git a/lib/jwe/decrypt.js b/lib/jwe/decrypt.js index f01f6ac400..8490d3236c 100644 --- a/lib/jwe/decrypt.js +++ b/lib/jwe/decrypt.js @@ -2,7 +2,7 @@ const { createSecretKey } = require('crypto') const { inflateRawSync } = require('zlib') const base64url = require('../help/base64url') -const KeyStore = require('../jwks/keystore') +const { KeyStore } = require('../jwks') const Key = require('../jwk/key/base') const errors = require('../errors') const { check, decrypt, keyManagementDecrypt } = require('../jwa') @@ -42,7 +42,7 @@ const combineHeader = (prot = {}, unprotected = {}, header = {}) => { */ const jweDecrypt = (skipValidateHeaders, serialization, jwe, key, { crit = [], complete = false, algorithms } = {}) => { if (!(key instanceof Key) && !(key instanceof KeyStore)) { - throw new TypeError('key must be an instance of a key instantiated by JWK.importKey or a JWKS.KeyStore') + throw new TypeError('key must be an instance of a key instantiated by JWK.asKey or a JWKS.KeyStore') } if (algorithms !== undefined && (!Array.isArray(algorithms) || algorithms.some(s => typeof s !== 'string' || !s))) { @@ -130,13 +130,13 @@ const jweDecrypt = (skipValidateHeaders, serialization, jwe, key, { crit = [], c try { if (alg === 'dir') { - cek = JWK.importKey(key, { alg: enc, use: 'enc' }) + cek = JWK.asKey(key, { alg: enc, use: 'enc' }) } else if (alg === 'ECDH-ES') { const unwrapped = keyManagementDecrypt(alg, key, undefined, opts) - cek = JWK.importKey(createSecretKey(unwrapped), { alg: enc, use: 'enc' }) + cek = JWK.asKey(createSecretKey(unwrapped), { alg: enc, use: 'enc' }) } else { const unwrapped = keyManagementDecrypt(alg, key, base64url.decodeToBuffer(encryptedKey), opts) - cek = JWK.importKey(createSecretKey(unwrapped), { alg: enc, use: 'enc' }) + cek = JWK.asKey(createSecretKey(unwrapped), { alg: enc, use: 'enc' }) } } catch (err) { // To mitigate the attacks described in RFC 3218, the diff --git a/lib/jwe/encrypt.js b/lib/jwe/encrypt.js index 0c9dec7278..b3fa074a0f 100644 --- a/lib/jwe/encrypt.js +++ b/lib/jwe/encrypt.js @@ -56,7 +56,7 @@ class Encrypt { */ recipient (key, header) { if (!(key instanceof Key)) { - throw new TypeError('key must be an instance of a key instantiated by JWK.importKey') + throw new TypeError('key must be an instance of a key instantiated by JWK.asKey') } if (header !== undefined && !isObject(header)) { diff --git a/lib/jwk/import.js b/lib/jwk/import.js index 61646f8975..6bf31393e8 100644 --- a/lib/jwk/import.js +++ b/lib/jwk/import.js @@ -1,4 +1,5 @@ const { createPublicKey, createPrivateKey, createSecretKey, KeyObject } = require('crypto') +const { deprecate } = require('util') const base64url = require('../help/base64url') const isObject = require('../help/is_object') @@ -24,7 +25,7 @@ const mergedParameters = (target = {}, source = {}) => { }, target) } -const importKey = (key, parameters) => { +const asKey = (key, parameters, { calculateMissingRSAPrimes = false } = {}) => { let privateKey, publicKey, secret if (!importable.has(typeof key)) { @@ -57,9 +58,10 @@ const importKey = (key, parameters) => { } parameters = mergedParameters(parameters, key) } else if (typeof key === 'object' && 'kty' in key) { // assume JWK formatted asymmetric key + ({ calculateMissingRSAPrimes = false } = parameters || { calculateMissingRSAPrimes }) let pem try { - pem = jwkToPem(key) + pem = jwkToPem(key, { calculateMissingRSAPrimes }) } catch (err) { if (err instanceof errors.JOSEError) { throw err @@ -70,7 +72,7 @@ const importKey = (key, parameters) => { } else if (pem) { publicKey = createPublicKey(pem) } - parameters = mergedParameters(parameters, key) + parameters = mergedParameters({}, key) } else { // | | passed to crypto.createPrivateKey or crypto.createPublicKey or passed to crypto.createSecretKey try { privateKey = createPrivateKey(key) @@ -110,4 +112,8 @@ const importKey = (key, parameters) => { throw new errors.JWKImportFailed('import failed') } -module.exports = importKey +module.exports = asKey +Object.defineProperty(asKey, 'deprecated', { + value: deprecate((key, parameters) => { return asKey(key, parameters, { calculateMissingRSAPrimes: true }) }, 'JWK.importKey() is deprecated, use JWK.asKey() instead'), + enumerable: false +}) diff --git a/lib/jwk/index.js b/lib/jwk/index.js index e6c57248ad..580908dc78 100644 --- a/lib/jwk/index.js +++ b/lib/jwk/index.js @@ -2,7 +2,13 @@ const Key = require('./key/base') const importKey = require('./import') const { generate, generateSync } = require('./generate') -module.exports.importKey = importKey +module.exports.asKey = importKey module.exports.generate = generate module.exports.generateSync = generateSync module.exports.isKey = input => input instanceof Key + +/* deprecated */ +Object.defineProperty(module.exports, 'importKey', { + value: importKey.deprecated, + enumerable: false +}) diff --git a/lib/jwks/index.js b/lib/jwks/index.js index 5b56614630..285a2798d4 100644 --- a/lib/jwks/index.js +++ b/lib/jwks/index.js @@ -1,3 +1,3 @@ const KeyStore = require('./keystore') -module.exports = { KeyStore } +module.exports = KeyStore diff --git a/lib/jwks/keystore.js b/lib/jwks/keystore.js index 8abb875135..1eb425f533 100644 --- a/lib/jwks/keystore.js +++ b/lib/jwks/keystore.js @@ -1,4 +1,4 @@ -const { inspect } = require('util') +const { deprecate, inspect } = require('util') const isObject = require('../help/is_object') const { generate, generateSync } = require('../jwk/generate') @@ -44,22 +44,12 @@ class KeyStore { keys = keys.flat() } if (keys.some(k => !(k instanceof Key))) { - throw new TypeError('all keys must be an instances of a key instantiated by JWK.importKey') + throw new TypeError('all keys must be an instances of a key instantiated by JWK.asKey') } this.#keys = new Set(keys) } - static fromJWKS (jwks) { - if (!isObject(jwks) || !Array.isArray(jwks.keys) || jwks.keys.some(k => !isObject(k) || !('kty' in k))) { - throw new TypeError('jwks must be a JSON Web Key Set formatted object') - } - - const keys = jwks.keys.map((jwk) => importKey(jwk)) - - return new KeyStore(...keys) - } - all ({ alg, kid, use, kty, key_ops: ops, x5t, 'x5t#S256': x5t256 } = {}) { 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') @@ -117,7 +107,7 @@ class KeyStore { add (key) { if (!(key instanceof Key)) { - throw new TypeError('key must be an instance of a key instantiated by JWK.importKey') + throw new TypeError('key must be an instance of a key instantiated by JWK.asKey') } this.#keys.add(key) @@ -125,7 +115,7 @@ class KeyStore { remove (key) { if (!(key instanceof Key)) { - throw new TypeError('key must be an instance of a key instantiated by JWK.importKey') + throw new TypeError('key must be an instance of a key instantiated by JWK.asKey') } this.#keys.delete(key) @@ -164,4 +154,19 @@ class KeyStore { } } -module.exports = KeyStore +function asKeyStore (jwks, { calculateMissingRSAPrimes = false } = {}) { + if (!isObject(jwks) || !Array.isArray(jwks.keys) || jwks.keys.some(k => !isObject(k) || !('kty' in k))) { + throw new TypeError('jwks must be a JSON Web Key Set formatted object') + } + + const keys = jwks.keys.map((jwk) => importKey(jwk, { calculateMissingRSAPrimes })) + + return new KeyStore(...keys) +} + +Object.defineProperty(KeyStore, 'fromJWKS', { + value: deprecate(jwks => asKeyStore(jwks, { calculateMissingRSAPrimes: true }), 'JWKS.KeyStore.fromJWKS() is deprecated, use JWKS.asKeyStore() instead'), + enumerable: false +}) + +module.exports = { KeyStore, asKeyStore } diff --git a/lib/jws/sign.js b/lib/jws/sign.js index 06fa97f6ff..e75f86c29c 100644 --- a/lib/jws/sign.js +++ b/lib/jws/sign.js @@ -35,7 +35,7 @@ class Sign { */ recipient (key, protectedHeader, unprotectedHeader) { if (!(key instanceof Key)) { - throw new TypeError('key must be an instance of a key instantiated by JWK.importKey') + throw new TypeError('key must be an instance of a key instantiated by JWK.asKey') } if (protectedHeader !== undefined && !isObject(protectedHeader)) { diff --git a/lib/jws/verify.js b/lib/jws/verify.js index 16910f8a52..904fc7d5d3 100644 --- a/lib/jws/verify.js +++ b/lib/jws/verify.js @@ -1,7 +1,7 @@ const base64url = require('../help/base64url') const isDisjoint = require('../help/is_disjoint') let validateCrit = require('../help/validate_crit') -const KeyStore = require('../jwks/keystore') +const { KeyStore } = require('../jwks') const Key = require('../jwk/key/base') const errors = require('../errors') const { check, verify } = require('../jwa') @@ -16,7 +16,7 @@ const SINGLE_RECIPIENT = new Set(['compact', 'flattened']) */ const jwsVerify = (skipDisjointCheck, serialization, jws, key, { crit = [], complete = false, algorithms } = {}) => { if (!(key instanceof Key) && !(key instanceof KeyStore)) { - throw new TypeError('key must be an instance of a key instantiated by JWK.importKey or a JWKS.KeyStore') + throw new TypeError('key must be an instance of a key instantiated by JWK.asKey or a JWKS.KeyStore') } if (algorithms !== undefined && (!Array.isArray(algorithms) || algorithms.some(s => typeof s !== 'string' || !s))) { diff --git a/lib/jwt/verify.js b/lib/jwt/verify.js index 8c61afe9c8..977c05315d 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 JWS = require('../jws') -const KeyStore = require('../jwks/keystore') +const { KeyStore } = require('../jwks') const { JWTClaimInvalid } = require('../errors') const { isStringOptional, isNotString } = require('./shared_validations') diff --git a/test/cookbook/4_1.rsa_v15_signature.test.js b/test/cookbook/4_1.rsa_v15_signature.test.js index 1cdcd60b95..a16a041b12 100644 --- a/test/cookbook/4_1.rsa_v15_signature.test.js +++ b/test/cookbook/4_1.rsa_v15_signature.test.js @@ -2,15 +2,15 @@ const test = require('ava') const recipe = require('./recipes').get('4.1') -const { JWS, JWK: { importKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') +const { JWS, JWK: { asKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') const { input: { payload, key: jwk }, signing: { protected: header } } = recipe -const key = importKey(jwk) +const key = asKey(jwk) const keystoreEmpty = new KeyStore() const keystoreMatchOne = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use }), key) -const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use, kid: key.kid }), key, importKey(key)) +const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use, kid: key.kid }), key, asKey(key)) const keystoreMatchNone = new KeyStore(generateSync(key.kty), generateSync(key.kty)) test(`${recipe.title} - compact sign`, t => { diff --git a/test/cookbook/4_2.rsa-pss_signature.test.js b/test/cookbook/4_2.rsa-pss_signature.test.js index 83630c6aa0..7ee91fb54c 100644 --- a/test/cookbook/4_2.rsa-pss_signature.test.js +++ b/test/cookbook/4_2.rsa-pss_signature.test.js @@ -3,15 +3,15 @@ const test = require('ava') const recipe = require('./recipes').get('4.2') const { sig: verifiers } = require('./verifiers') -const { JWS, JWK: { importKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') +const { JWS, JWK: { asKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') const { input: { payload, key: jwk }, signing: { protected: header } } = recipe -const key = importKey(jwk) +const key = asKey(jwk) const keystoreEmpty = new KeyStore() const keystoreMatchOne = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use }), key) -const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use, kid: key.kid }), key, importKey(key)) +const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use, kid: key.kid }), key, asKey(key)) const keystoreMatchNone = new KeyStore(generateSync(key.kty), generateSync(key.kty)) test(`${recipe.title} - compact sign (random)`, t => { diff --git a/test/cookbook/4_3.ecdsa_signature.test.js b/test/cookbook/4_3.ecdsa_signature.test.js index e09155082a..85c14b0510 100644 --- a/test/cookbook/4_3.ecdsa_signature.test.js +++ b/test/cookbook/4_3.ecdsa_signature.test.js @@ -3,15 +3,15 @@ const test = require('ava') const recipe = require('./recipes').get('4.3') const { sig: verifiers } = require('./verifiers') -const { JWS, JWK: { importKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') +const { JWS, JWK: { asKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') const { input: { payload, key: jwk }, signing: { protected: header } } = recipe -const key = importKey(jwk) +const key = asKey(jwk) const keystoreEmpty = new KeyStore() const keystoreMatchOne = new KeyStore(generateSync(key.kty, key.crv, { alg: key.alg, use: key.use }), key) -const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.crv, { alg: key.alg, use: key.use, kid: key.kid }), key, importKey(key)) +const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.crv, { alg: key.alg, use: key.use, kid: key.kid }), key, asKey(key)) const keystoreMatchNone = new KeyStore(generateSync(key.kty), generateSync(key.kty)) test(`${recipe.title} - compact sign (random)`, t => { diff --git a/test/cookbook/4_4.hmac-sha2_integrity_protection.test.js b/test/cookbook/4_4.hmac-sha2_integrity_protection.test.js index 841c8e1259..6decf67eab 100644 --- a/test/cookbook/4_4.hmac-sha2_integrity_protection.test.js +++ b/test/cookbook/4_4.hmac-sha2_integrity_protection.test.js @@ -2,15 +2,15 @@ const test = require('ava') const recipe = require('./recipes').get('4.4') -const { JWS, JWK: { importKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') +const { JWS, JWK: { asKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') const { input: { payload, key: jwk }, signing: { protected: header } } = recipe -const key = importKey(jwk) +const key = asKey(jwk) const keystoreEmpty = new KeyStore() const keystoreMatchOne = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use }), key) -const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use, kid: key.kid }), key, importKey(key)) +const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use, kid: key.kid }), key, asKey(key)) const keystoreMatchNone = new KeyStore(generateSync(key.kty), generateSync(key.kty)) test(`${recipe.title} - compact sign`, t => { diff --git a/test/cookbook/4_6.protecting_specific_header_fields.test.js b/test/cookbook/4_6.protecting_specific_header_fields.test.js index 9061ae03a6..7ca420fb10 100644 --- a/test/cookbook/4_6.protecting_specific_header_fields.test.js +++ b/test/cookbook/4_6.protecting_specific_header_fields.test.js @@ -2,15 +2,15 @@ const test = require('ava') const recipe = require('./recipes').get('4.6') -const { JWS, JWK: { importKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') +const { JWS, JWK: { asKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') const { input: { payload, key: jwk }, signing: { protected: protec, unprotected } } = recipe -const key = importKey(jwk) +const key = asKey(jwk) const keystoreEmpty = new KeyStore() const keystoreMatchOne = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use }), key) -const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use, kid: key.kid }), key, importKey(key)) +const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use, kid: key.kid }), key, asKey(key)) const keystoreMatchNone = new KeyStore(generateSync(key.kty), generateSync(key.kty)) test(`${recipe.title} - flattened sign`, t => { diff --git a/test/cookbook/4_7.protecting_content_only.test.js b/test/cookbook/4_7.protecting_content_only.test.js index 8d216d669c..34e46a48c4 100644 --- a/test/cookbook/4_7.protecting_content_only.test.js +++ b/test/cookbook/4_7.protecting_content_only.test.js @@ -2,15 +2,15 @@ const test = require('ava') const recipe = require('./recipes').get('4.7') -const { JWS, JWK: { importKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') +const { JWS, JWK: { asKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') const { input: { payload, key: jwk }, signing: { unprotected } } = recipe -const key = importKey(jwk) +const key = asKey(jwk) const keystoreEmpty = new KeyStore() const keystoreMatchOne = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use }), key) -const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use, kid: key.kid }), key, importKey(key)) +const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use, kid: key.kid }), key, asKey(key)) const keystoreMatchNone = new KeyStore(generateSync(key.kty), generateSync(key.kty)) test(`${recipe.title} - flattened sign`, t => { diff --git a/test/cookbook/4_8.multiple_signatures.test.js b/test/cookbook/4_8.multiple_signatures.test.js index 4a1c46129f..cb0cb5a82f 100644 --- a/test/cookbook/4_8.multiple_signatures.test.js +++ b/test/cookbook/4_8.multiple_signatures.test.js @@ -2,11 +2,11 @@ const test = require('ava') const recipe = require('./recipes').get('4.8') -const { JWS, JWK: { importKey }, JWKS: { KeyStore }, errors } = require('../..') +const { JWS, JWK: { asKey }, JWKS: { KeyStore }, errors } = require('../..') const { input: { payload, key: jwks }, signing: recipients } = recipe -const keys = jwks.map((jwk) => importKey(jwk)) +const keys = jwks.map((jwk) => asKey(jwk)) const keystoreEmpty = new KeyStore() const keystore = new KeyStore(...keys) diff --git a/test/cookbook/5_1.key_encryption_using_rsa_v15_and_aes-hmac-sha2.test.js b/test/cookbook/5_1.key_encryption_using_rsa_v15_and_aes-hmac-sha2.test.js index a5f3a02037..b8de073e32 100644 --- a/test/cookbook/5_1.key_encryption_using_rsa_v15_and_aes-hmac-sha2.test.js +++ b/test/cookbook/5_1.key_encryption_using_rsa_v15_and_aes-hmac-sha2.test.js @@ -3,18 +3,18 @@ const test = require('ava') const recipe = require('./recipes').get('5.1') const { enc: verifiers } = require('./verifiers') -const { JWE, JWK: { importKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') +const { JWE, JWK: { asKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') const { input: { plaintext, key: jwk }, encrypting_content: { protected: prot } } = recipe -const key = importKey(jwk) +const key = asKey(jwk) const keystoreEmpty = new KeyStore() const keystoreMatchOne = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use }), key) -const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use, kid: key.kid }), key, importKey(key)) +const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use, kid: key.kid }), key, asKey(key)) const keystoreMatchNone = new KeyStore(generateSync(key.kty), generateSync(key.kty)) test(`${recipe.title} - compact encrypt`, t => { diff --git a/test/cookbook/5_10.including_additional_authentication_data.test.js b/test/cookbook/5_10.including_additional_authentication_data.test.js index 651b43d56d..f566492052 100644 --- a/test/cookbook/5_10.including_additional_authentication_data.test.js +++ b/test/cookbook/5_10.including_additional_authentication_data.test.js @@ -3,18 +3,18 @@ const test = require('ava') const recipe = require('./recipes').get('5.10') const { enc: verifiers } = require('./verifiers') -const { JWE, JWK: { importKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') +const { JWE, JWK: { asKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') const { input: { plaintext, key: jwk, aad }, encrypting_content: { protected: prot } } = recipe -const key = importKey(jwk) +const key = asKey(jwk) const keystoreEmpty = new KeyStore() const keystoreMatchOne = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use }), key) -const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use, kid: key.kid }), key, importKey(key)) +const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use, kid: key.kid }), key, asKey(key)) const keystoreMatchNone = new KeyStore(generateSync(key.kty), generateSync(key.kty)) test(`${recipe.title} - flattened encrypt`, t => { diff --git a/test/cookbook/5_11.protecting_specific_header_fields.test.js b/test/cookbook/5_11.protecting_specific_header_fields.test.js index 3f2e44f615..32ddb81e0d 100644 --- a/test/cookbook/5_11.protecting_specific_header_fields.test.js +++ b/test/cookbook/5_11.protecting_specific_header_fields.test.js @@ -3,18 +3,18 @@ const test = require('ava') const recipe = require('./recipes').get('5.11') const { enc: verifiers } = require('./verifiers') -const { JWE, JWK: { importKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') +const { JWE, JWK: { asKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') const { input: { plaintext, key: jwk }, encrypting_content: { protected: prot, unprotected } } = recipe -const key = importKey(jwk) +const key = asKey(jwk) const keystoreEmpty = new KeyStore() const keystoreMatchOne = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use }), key) -const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use, kid: key.kid }), key, importKey(key)) +const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use, kid: key.kid }), key, asKey(key)) const keystoreMatchNone = new KeyStore(generateSync(key.kty), generateSync(key.kty)) test(`${recipe.title} - flattened encrypt`, t => { diff --git a/test/cookbook/5_12.protecting_content_only.test.js b/test/cookbook/5_12.protecting_content_only.test.js index 874665d076..95410df8f1 100644 --- a/test/cookbook/5_12.protecting_content_only.test.js +++ b/test/cookbook/5_12.protecting_content_only.test.js @@ -3,18 +3,18 @@ const test = require('ava') const recipe = require('./recipes').get('5.12') const { enc: verifiers } = require('./verifiers') -const { JWE, JWK: { importKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') +const { JWE, JWK: { asKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') const { input: { plaintext, key: jwk }, encrypting_content: { unprotected } } = recipe -const key = importKey(jwk) +const key = asKey(jwk) const keystoreEmpty = new KeyStore() const keystoreMatchOne = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use }), key) -const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use, kid: key.kid }), key, importKey(key)) +const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use, kid: key.kid }), key, asKey(key)) const keystoreMatchNone = new KeyStore(generateSync(key.kty), generateSync(key.kty)) test(`${recipe.title} - flattened encrypt`, t => { diff --git a/test/cookbook/5_13.encrypting_to_multiple_recipients.test.js b/test/cookbook/5_13.encrypting_to_multiple_recipients.test.js index 6ed304f688..72bb225a8b 100644 --- a/test/cookbook/5_13.encrypting_to_multiple_recipients.test.js +++ b/test/cookbook/5_13.encrypting_to_multiple_recipients.test.js @@ -3,7 +3,7 @@ const test = require('ava') const recipe = require('./recipes').get('5.13') const { enc: verifiers } = require('./verifiers') -const { JWE, JWK: { importKey }, JWKS: { KeyStore }, errors } = require('../..') +const { JWE, JWK: { asKey }, JWKS: { KeyStore }, errors } = require('../..') const { input: { plaintext, key: jwks }, @@ -11,7 +11,7 @@ const { encrypting_key: recipients } = recipe -const keys = jwks.map((jwk) => importKey(jwk)) +const keys = jwks.map((jwk) => asKey(jwk)) const keystoreEmpty = new KeyStore() const keystore = new KeyStore(...keys) diff --git a/test/cookbook/5_2.key_encryption_using_rsa-oaep_with_aes-gcm.test.js b/test/cookbook/5_2.key_encryption_using_rsa-oaep_with_aes-gcm.test.js index 3e28e0b753..7ba5171e6e 100644 --- a/test/cookbook/5_2.key_encryption_using_rsa-oaep_with_aes-gcm.test.js +++ b/test/cookbook/5_2.key_encryption_using_rsa-oaep_with_aes-gcm.test.js @@ -3,18 +3,18 @@ const test = require('ava') const recipe = require('./recipes').get('5.2') const { enc: verifiers } = require('./verifiers') -const { JWE, JWK: { importKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') +const { JWE, JWK: { asKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') const { input: { plaintext, key: jwk }, encrypting_content: { protected: prot } } = recipe -const key = importKey(jwk) +const key = asKey(jwk) const keystoreEmpty = new KeyStore() const keystoreMatchOne = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use }), key) -const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use, kid: key.kid }), key, importKey(key)) +const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use, kid: key.kid }), key, asKey(key)) const keystoreMatchNone = new KeyStore(generateSync(key.kty), generateSync(key.kty)) test(`${recipe.title} - compact encrypt`, t => { diff --git a/test/cookbook/5_3.key_wrap_using_pbes2-aes-keywrap_with-aes-cbc-hmac-sha2.test.js b/test/cookbook/5_3.key_wrap_using_pbes2-aes-keywrap_with-aes-cbc-hmac-sha2.test.js index fd8b217623..e5603fc20d 100644 --- a/test/cookbook/5_3.key_wrap_using_pbes2-aes-keywrap_with-aes-cbc-hmac-sha2.test.js +++ b/test/cookbook/5_3.key_wrap_using_pbes2-aes-keywrap_with-aes-cbc-hmac-sha2.test.js @@ -3,18 +3,18 @@ const test = require('ava') const recipe = require('./recipes').get('5.3') const { enc: verifiers } = require('./verifiers') -const { JWE, JWK: { importKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') +const { JWE, JWK: { asKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') const { input: { plaintext, pwd }, encrypting_content: { protected: prot } } = recipe -const key = importKey(Buffer.from(pwd)) +const key = asKey(Buffer.from(pwd)) const keystoreEmpty = new KeyStore() const keystoreMatchOne = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use }), key) -const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use, kid: key.kid }), key, importKey(key)) +const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use, kid: key.kid }), key, asKey(key)) const keystoreMatchNone = new KeyStore(generateSync('EC'), generateSync('RSA')) test(`${recipe.title} - compact encrypt`, t => { diff --git a/test/cookbook/5_4.key_agreement_with_key_wrapping_using_ecdh-es_and_aes-keywrap_with_aes-gcm.test.js b/test/cookbook/5_4.key_agreement_with_key_wrapping_using_ecdh-es_and_aes-keywrap_with_aes-gcm.test.js index 68349e436f..3c929c8c2d 100644 --- a/test/cookbook/5_4.key_agreement_with_key_wrapping_using_ecdh-es_and_aes-keywrap_with_aes-gcm.test.js +++ b/test/cookbook/5_4.key_agreement_with_key_wrapping_using_ecdh-es_and_aes-keywrap_with_aes-gcm.test.js @@ -3,18 +3,18 @@ const test = require('ava') const recipe = require('./recipes').get('5.4') const { enc: verifiers } = require('./verifiers') -const { JWE, JWK: { importKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') +const { JWE, JWK: { asKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') const { input: { plaintext, key: jwk }, encrypting_content: { protected: prot } } = recipe -const key = importKey(jwk) +const key = asKey(jwk) const keystoreEmpty = new KeyStore() const keystoreMatchOne = new KeyStore(generateSync(key.kty, key.crv, { alg: key.alg, use: key.use }), key) -const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.crv, { alg: key.alg, use: key.use, kid: key.kid }), key, importKey(key)) +const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.crv, { alg: key.alg, use: key.use, kid: key.kid }), key, asKey(key)) const keystoreMatchNone = new KeyStore(generateSync(key.kty), generateSync(key.kty)) test(`${recipe.title} - compact encrypt`, t => { diff --git a/test/cookbook/5_5.key_agreement_using_ecdh-es_with_aes-cbc-hmac-sha2.test.js b/test/cookbook/5_5.key_agreement_using_ecdh-es_with_aes-cbc-hmac-sha2.test.js index f69d5f92ca..f3900f0421 100644 --- a/test/cookbook/5_5.key_agreement_using_ecdh-es_with_aes-cbc-hmac-sha2.test.js +++ b/test/cookbook/5_5.key_agreement_using_ecdh-es_with_aes-cbc-hmac-sha2.test.js @@ -3,18 +3,18 @@ const test = require('ava') const recipe = require('./recipes').get('5.5') const { enc: verifiers } = require('./verifiers') -const { JWE, JWK: { importKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') +const { JWE, JWK: { asKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') const { input: { plaintext, key: jwk }, encrypting_content: { protected: prot } } = recipe -const key = importKey(jwk) +const key = asKey(jwk) const keystoreEmpty = new KeyStore() const keystoreMatchOne = new KeyStore(generateSync(key.kty, key.crv, { alg: key.alg, use: key.use }), key) -const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.crv, { alg: key.alg, use: key.use, kid: key.kid }), key, importKey(key)) +const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.crv, { alg: key.alg, use: key.use, kid: key.kid }), key, asKey(key)) const keystoreMatchNone = new KeyStore(generateSync(key.kty), generateSync(key.kty)) test(`${recipe.title} - compact encrypt`, t => { diff --git a/test/cookbook/5_6.direct_encryption_using_aes-gcm.test.js b/test/cookbook/5_6.direct_encryption_using_aes-gcm.test.js index c59bd5e120..96d674f780 100644 --- a/test/cookbook/5_6.direct_encryption_using_aes-gcm.test.js +++ b/test/cookbook/5_6.direct_encryption_using_aes-gcm.test.js @@ -3,18 +3,18 @@ const test = require('ava') const recipe = require('./recipes').get('5.6') const { enc: verifiers } = require('./verifiers') -const { JWE, JWK: { importKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') +const { JWE, JWK: { asKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') const { input: { plaintext, key: jwk }, encrypting_content: { protected: prot } } = recipe -const key = importKey(jwk) +const key = asKey(jwk) const keystoreEmpty = new KeyStore() const keystoreMatchOne = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use }), key) -const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use, kid: key.kid }), key, importKey(key)) +const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use, kid: key.kid }), key, asKey(key)) const keystoreMatchNone = new KeyStore(generateSync(key.kty), generateSync(key.kty)) test(`${recipe.title} - compact encrypt`, t => { diff --git a/test/cookbook/5_7.key_wrap_using_aes-gcm_keywrap_with_aes-cbc-hmac-sha2.test.js b/test/cookbook/5_7.key_wrap_using_aes-gcm_keywrap_with_aes-cbc-hmac-sha2.test.js index e7edee31fd..03d031807e 100644 --- a/test/cookbook/5_7.key_wrap_using_aes-gcm_keywrap_with_aes-cbc-hmac-sha2.test.js +++ b/test/cookbook/5_7.key_wrap_using_aes-gcm_keywrap_with_aes-cbc-hmac-sha2.test.js @@ -3,18 +3,18 @@ const test = require('ava') const recipe = require('./recipes').get('5.7') const { enc: verifiers } = require('./verifiers') -const { JWE, JWK: { importKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') +const { JWE, JWK: { asKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') const { input: { plaintext, key: jwk }, encrypting_content: { protected: prot } } = recipe -const key = importKey(jwk) +const key = asKey(jwk) const keystoreEmpty = new KeyStore() const keystoreMatchOne = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use }), key) -const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use, kid: key.kid }), key, importKey(key)) +const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use, kid: key.kid }), key, asKey(key)) const keystoreMatchNone = new KeyStore(generateSync(key.kty), generateSync(key.kty)) test(`${recipe.title} - compact encrypt`, t => { diff --git a/test/cookbook/5_8.key_wrap_using_aes-keywrap_with_aes-gcm.test.js b/test/cookbook/5_8.key_wrap_using_aes-keywrap_with_aes-gcm.test.js index decbf5631b..dfb5ff50ad 100644 --- a/test/cookbook/5_8.key_wrap_using_aes-keywrap_with_aes-gcm.test.js +++ b/test/cookbook/5_8.key_wrap_using_aes-keywrap_with_aes-gcm.test.js @@ -3,18 +3,18 @@ const test = require('ava') const recipe = require('./recipes').get('5.8') const { enc: verifiers } = require('./verifiers') -const { JWE, JWK: { importKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') +const { JWE, JWK: { asKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') const { input: { plaintext, key: jwk }, encrypting_content: { protected: prot } } = recipe -const key = importKey(jwk) +const key = asKey(jwk) const keystoreEmpty = new KeyStore() const keystoreMatchOne = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use }), key) -const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use, kid: key.kid }), key, importKey(key)) +const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use, kid: key.kid }), key, asKey(key)) const keystoreMatchNone = new KeyStore(generateSync(key.kty), generateSync(key.kty)) test(`${recipe.title} - compact encrypt`, t => { diff --git a/test/cookbook/5_9.compressed_content.test.js b/test/cookbook/5_9.compressed_content.test.js index 2629d52ac3..ed685ad97d 100644 --- a/test/cookbook/5_9.compressed_content.test.js +++ b/test/cookbook/5_9.compressed_content.test.js @@ -3,18 +3,18 @@ const test = require('ava') const recipe = require('./recipes').get('5.9') const { enc: verifiers } = require('./verifiers') -const { JWE, JWK: { importKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') +const { JWE, JWK: { asKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') const { input: { plaintext, key: jwk }, encrypting_content: { protected: prot } } = recipe -const key = importKey(jwk) +const key = asKey(jwk) const keystoreEmpty = new KeyStore() const keystoreMatchOne = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use }), key) -const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use, kid: key.kid }), key, importKey(key)) +const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use, kid: key.kid }), key, asKey(key)) const keystoreMatchNone = new KeyStore(generateSync(key.kty), generateSync(key.kty)) test(`${recipe.title} - compact encrypt`, t => { diff --git a/test/cookbook/jwk.test.js b/test/cookbook/jwk.test.js index 23aac3c72b..0f4000e33a 100644 --- a/test/cookbook/jwk.test.js +++ b/test/cookbook/jwk.test.js @@ -2,11 +2,11 @@ const test = require('ava') const recipes = require('./recipes') -const { JWK: { importKey }, JWKS: { KeyStore } } = require('../..') +const { JWK: { asKey }, JWKS: { KeyStore } } = require('../..') test('public EC', t => { const jwk = recipes.get('3.1') - const key = importKey(jwk) + const key = asKey(jwk) t.true(key.toPEM().includes('BEGIN PUBLIC KEY')) t.deepEqual(key.toJWK(), jwk) t.deepEqual(key.toJWK(false), jwk) @@ -26,7 +26,7 @@ test('public EC', t => { test('private EC', t => { const jwk = recipes.get('3.2') - const key = importKey(jwk) + const key = asKey(jwk) t.true(key.toPEM(true, { cipher: 'aes-256-cbc', passphrase: 'top secret' }).includes('BEGIN ENCRYPTED PRIVATE KEY')) t.true(key.toPEM(true, { type: 'sec1' }).includes('BEGIN EC PRIVATE KEY')) t.true(key.toPEM(true, { type: 'sec1', cipher: 'aes-256-cbc', passphrase: 'top secret' }).includes('ENCRYPTED')) @@ -46,7 +46,7 @@ test('private EC', t => { test('public RSA', t => { const jwk = recipes.get('3.3') - const key = importKey(jwk) + const key = asKey(jwk) t.true(key.toPEM().includes('BEGIN PUBLIC KEY')) t.deepEqual(key.toJWK(), jwk) t.deepEqual(key.toJWK(false), jwk) @@ -60,7 +60,7 @@ test('public RSA', t => { test('private RSA', t => { const jwk = recipes.get('3.4') - const key = importKey(jwk) + const key = asKey(jwk) t.true(key.toPEM(true, { type: 'pkcs1' }).includes('BEGIN RSA PRIVATE KEY')) t.true(key.toPEM(true, { cipher: 'aes-256-cbc', passphrase: 'top secret', type: 'pkcs1' }).includes('ENCRYPTED')) t.true(key.toPEM(true, { type: 'pkcs1', cipher: 'aes-256-cbc', passphrase: 'top secret' }).includes('BEGIN RSA PRIVATE KEY')) @@ -75,7 +75,7 @@ test('private RSA', t => { test('oct (1/2)', t => { const jwk = recipes.get('3.5') - const key = importKey(jwk) + const key = asKey(jwk) t.throws(() => { key.toPEM() }, { instanceOf: TypeError, message: 'symmetric keys cannot be exported as PEM' }) @@ -87,7 +87,7 @@ test('oct (1/2)', t => { test('oct (2/2)', t => { const jwk = recipes.get('3.6') - const key = importKey(jwk) + const key = asKey(jwk) t.deepEqual(key.toJWK(true), jwk) const { k, ...pub } = jwk t.deepEqual(key.toJWK(), pub) @@ -101,11 +101,11 @@ test('keystore .toJWKS()', t => { const { d, dp, dq, p, q, qi, ...pubRsa } = rsa const oct = recipes.get('3.5') const { k, ...pubOct } = oct - const ks = new KeyStore(importKey(ec), importKey(rsa), importKey(oct)) + const ks = new KeyStore(asKey(ec), asKey(rsa), asKey(oct)) t.deepEqual(ks.toJWKS(true), { keys: [ec, rsa, oct] }) t.deepEqual(ks.toJWKS(), { keys: [pubEc, pubRsa, pubOct] }) t.deepEqual(ks.toJWKS(false), { keys: [pubEc, pubRsa, pubOct] }) - ks.add(importKey(pubRsa)) + ks.add(asKey(pubRsa)) t.throws(() => { ks.toJWKS(true) }, { instanceOf: TypeError, message: 'public key cannot be exported as private' }) diff --git a/test/cookbook/rfc7797.4_1.hmac-sha2_b64_false.test.js b/test/cookbook/rfc7797.4_1.hmac-sha2_b64_false.test.js index f6c9e72acb..455e132cdb 100644 --- a/test/cookbook/rfc7797.4_1.hmac-sha2_b64_false.test.js +++ b/test/cookbook/rfc7797.4_1.hmac-sha2_b64_false.test.js @@ -2,15 +2,15 @@ const test = require('ava') const recipe = require('./recipes').get('4.1 rfc7797') -const { JWS, JWK: { importKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') +const { JWS, JWK: { asKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') const { input: { payload, key: jwk }, signing: { protected: header } } = recipe -const key = importKey(jwk) +const key = asKey(jwk) const keystoreEmpty = new KeyStore() const keystoreMatchOne = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use }), key) -const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use, kid: key.kid }), key, importKey(key)) +const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use, kid: key.kid }), key, asKey(key)) const keystoreMatchNone = new KeyStore(generateSync('EC'), generateSync('RSA')) test(`${recipe.title} - compact sign`, t => { diff --git a/test/cookbook/rfc7797.4_2.hmac-sha2_b64_false.js b/test/cookbook/rfc7797.4_2.hmac-sha2_b64_false.js index 560a067418..ae6ca3f072 100644 --- a/test/cookbook/rfc7797.4_2.hmac-sha2_b64_false.js +++ b/test/cookbook/rfc7797.4_2.hmac-sha2_b64_false.js @@ -2,15 +2,15 @@ const test = require('ava') const recipe = require('./recipes').get('4.2 rfc7797') -const { JWS, JWK: { importKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') +const { JWS, JWK: { asKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') const { input: { payload, key: jwk }, signing: { protected: header } } = recipe -const key = importKey(jwk) +const key = asKey(jwk) const keystoreEmpty = new KeyStore() const keystoreMatchOne = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use }), key) -const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use, kid: key.kid }), key, importKey(key)) +const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use, kid: key.kid }), key, asKey(key)) const keystoreMatchNone = new KeyStore(generateSync(key.kty), generateSync(key.kty)) test(`${recipe.title} - flattened sign`, t => { diff --git a/test/cookbook/rfc8037.a4.ed25519.test.js b/test/cookbook/rfc8037.a4.ed25519.test.js index 821a0f3ccf..667d7ebaec 100644 --- a/test/cookbook/rfc8037.a4.ed25519.test.js +++ b/test/cookbook/rfc8037.a4.ed25519.test.js @@ -2,11 +2,11 @@ const test = require('ava') const recipe = require('./recipes').get('A.4 rfc8037') -const { JWS, JWK: { importKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') +const { JWS, JWK: { asKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') const { input: { payload, key: jwk }, signing: { protected: header } } = recipe -const key = importKey(jwk) +const key = asKey(jwk) test('OKP JWK Thumbprint Canonicalization', t => { t.is(key.kid, 'kPrK_qmxVWaYVA9wwBF6Iuo3vVzz7TxHCTwXBygrS4k') @@ -14,7 +14,7 @@ test('OKP JWK Thumbprint Canonicalization', t => { const keystoreEmpty = new KeyStore() const keystoreMatchOne = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use }), key) -const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use, kid: key.kid }), key, importKey(key)) +const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use, kid: key.kid }), key, asKey(key)) const keystoreMatchNone = new KeyStore(generateSync('EC'), generateSync('RSA')) test(`${recipe.title} - compact sign`, t => { diff --git a/test/help/base64url.test.js b/test/help/base64url.test.js index e51c6fc6c2..9a561f0597 100644 --- a/test/help/base64url.test.js +++ b/test/help/base64url.test.js @@ -21,10 +21,6 @@ test('.decode with default encoding', t => { t.is(base64url.decode('Zm9v'), 'foo') }) -test('.decode with non-default encoding', t => { - t.is(base64url.decode('Zm9v', 'hex'), '666f6f') -}) - test('.decodeToBuffer', t => { t.deepEqual(base64url.decodeToBuffer('fmkIOj-kafqtjMl-iC32a-9YGz0cKj_JT9Jt31uXR1la7FSXkjoBzg_F-huYm0udbM5z5qGlmPBNZASsixJLcA'), testBuf) }) diff --git a/test/help/ecdsa_signatures.test.js b/test/help/ecdsa_signatures.test.js index bfb2f7bda0..65c2ecedc2 100644 --- a/test/help/ecdsa_signatures.test.js +++ b/test/help/ecdsa_signatures.test.js @@ -152,7 +152,7 @@ test('.joseToDer non buffer or base64 signature', t => { }) test('.joseToDer unknown algorithm', t => { - t.throws(() => joseToDer(decodeToBuffer('Zm9vLmJhci5iYXo='), 'foobar'), { instanceOf: Error, message: /"foobar"/ }) + t.throws(() => joseToDer(decodeToBuffer('Zm9vLmJhci5iYXo'), 'foobar'), { instanceOf: Error, message: /"foobar"/ }) }) test('.joseToDer incorrect signature length (ES256)', t => { @@ -176,7 +176,7 @@ test('ES256 should jose -> der -> jose', t => { }) test('ES256 should der -> jose -> der', t => { - const expected = decodeToBuffer('MEUCIQD0nDQE4uBS6JuklnyACfPQRB/LMEh5Stq6sAfp38k6ewIgHvhX59iuruBiFpVkg3dQKJ3+Wk29lJmXfxp6ciRdj+Q=') + const expected = decodeToBuffer('MEUCIQD0nDQE4uBS6JuklnyACfPQRB_LMEh5Stq6sAfp38k6ewIgHvhX59iuruBiFpVkg3dQKJ3-Wk29lJmXfxp6ciRdj-Q') const jose = derToJose(expected, 'ES256') const actual = joseToDer(jose, 'ES256') @@ -192,7 +192,7 @@ test('ES384 should jose -> der -> jose', t => { }) test('ES384 should der -> jose -> der', t => { - const expected = decodeToBuffer('MGUCMADcY5icKo+sLF0YCh5eVzju55Elt3Dfu4geMMDnUlLNaEO8NiCFzCHeqMx7mW5GMwIxAI6sp8ihHjRJ0sn/WV6mZCxN6/5lEg1QZJ5eiUHYv2kBgmiJ/Yv1pnqqFY3gVDBp/g==') + const expected = decodeToBuffer('MGUCMADcY5icKo-sLF0YCh5eVzju55Elt3Dfu4geMMDnUlLNaEO8NiCFzCHeqMx7mW5GMwIxAI6sp8ihHjRJ0sn_WV6mZCxN6_5lEg1QZJ5eiUHYv2kBgmiJ_Yv1pnqqFY3gVDBp_g') const jose = derToJose(expected, 'ES384') const actual = joseToDer(jose, 'ES384') @@ -208,7 +208,7 @@ test('ES512 should jose -> der -> jose', t => { }) test('ES512 should der -> jose -> der', t => { - const expected = decodeToBuffer('MIGHAkFgiYpVsYxx6XiQp2OXscRW/PrbEcoime/FftP+B7x4QVa+M3KZzXlfP66zKqjo7O3nwK2s8GbTftW8H4HwojzimwJCAYQNsozTpCo5nwIkBgelcfIQ0y/U/60TbNH1+rlKpFDCFs6Q1ro7R1tjtXoAUb9aPIOVyXGiSQX/+fcmmWs1rkJU') + const expected = decodeToBuffer('MIGHAkFgiYpVsYxx6XiQp2OXscRW_PrbEcoime_FftP-B7x4QVa-M3KZzXlfP66zKqjo7O3nwK2s8GbTftW8H4HwojzimwJCAYQNsozTpCo5nwIkBgelcfIQ0y_U_60TbNH1-rlKpFDCFs6Q1ro7R1tjtXoAUb9aPIOVyXGiSQX_-fcmmWs1rkJU') const jose = derToJose(expected, 'ES512') const actual = joseToDer(jose, 'ES512') diff --git a/test/jwe/sanity.test.js b/test/jwe/sanity.test.js index c66c4c3691..5584840c3d 100644 --- a/test/jwe/sanity.test.js +++ b/test/jwe/sanity.test.js @@ -38,7 +38,7 @@ test('verify key or store argument', t => { ;[{}, new Object(), false, null, Infinity, 0, Buffer.from('foo')].forEach((val) => { // eslint-disable-line no-new-object t.throws(() => { JWE.decrypt('....', val) - }, { instanceOf: TypeError, message: 'key must be an instance of a key instantiated by JWK.importKey or a JWKS.KeyStore' }) + }, { instanceOf: TypeError, message: 'key must be an instance of a key instantiated by JWK.asKey or a JWKS.KeyStore' }) }) }) @@ -288,7 +288,7 @@ test('JWE encrypt rejects non keys', t => { ;[[], false, true, undefined, null, Infinity, 0].forEach((val) => { t.throws(() => { JWE.encrypt('foo', val) - }, { instanceOf: TypeError, message: 'key must be an instance of a key instantiated by JWK.importKey' }) + }, { instanceOf: TypeError, message: 'key must be an instance of a key instantiated by JWK.asKey' }) }) }) diff --git a/test/jwe/smoke.test.js b/test/jwe/smoke.test.js index 852b919e6b..b63c14a83e 100644 --- a/test/jwe/smoke.test.js +++ b/test/jwe/smoke.test.js @@ -3,7 +3,7 @@ const test = require('ava') const { randomBytes } = require('crypto') const { encrypt, decrypt } = require('../../lib/jwe') -const { JWK: { importKey, generateSync }, errors } = require('../..') +const { JWK: { asKey, generateSync }, errors } = require('../..') const PAYLOAD = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c' const ENCS = [ @@ -98,8 +98,8 @@ const failure = (t, eKey, dKey, alg, enc) => { } Object.entries(fixtures.PEM).forEach(([type, { private: key, public: pub }]) => { - const eKey = importKey(pub) - const dKey = importKey(key) + const eKey = asKey(pub) + const dKey = asKey(key) ;[...eKey.algorithms('wrapKey'), ...eKey.algorithms('deriveKey')].forEach((alg) => { ENCS.forEach((enc) => { @@ -111,7 +111,7 @@ Object.entries(fixtures.PEM).forEach(([type, { private: key, public: pub }]) => }) ;[16, 24, 32, 48, 64].forEach((len) => { - const sym = importKey(randomBytes(len)) + const sym = asKey(randomBytes(len)) ;[...sym.algorithms('wrapKey'), ...sym.algorithms('deriveKey')].forEach((alg) => { sym.algorithms('encrypt').forEach((enc) => { test(`key ${sym.kty} > alg ${alg} > ${enc}`, success, sym, sym, alg, enc) @@ -122,8 +122,8 @@ Object.entries(fixtures.PEM).forEach(([type, { private: key, public: pub }]) => { const rsa = generateSync('RSA') - const dKey = importKey({ kty: 'RSA', e: rsa.e, n: rsa.n, d: rsa.d }) - const eKey = importKey({ kty: 'RSA', e: rsa.e, n: rsa.n }) + const dKey = asKey({ kty: 'RSA', e: rsa.e, n: rsa.n, d: rsa.d }, { calculateMissingRSAPrimes: true }) + const eKey = asKey({ kty: 'RSA', e: rsa.e, n: rsa.n }) eKey.algorithms('wrapKey').forEach((alg) => { ENCS.forEach((enc) => { if (alg === 'ECDH-ES' && ['A192CBC-HS384', 'A256CBC-HS512'].includes(enc)) return diff --git a/test/jwk/general.test.js b/test/jwk/general.test.js index e3f3c0eca2..a3c75041d0 100644 --- a/test/jwk/general.test.js +++ b/test/jwk/general.test.js @@ -1,6 +1,6 @@ const test = require('ava') -const { JWK: { generateSync, isKey, importKey } } = require('../..') +const { JWK: { generateSync, isKey, asKey } } = require('../..') test('.isKey() only key objects return true', t => { ;[[], false, true, null, Infinity, 0].forEach((val) => { @@ -40,7 +40,7 @@ test('"kid" must be a non-empty string', t => { test('"kid" from JWK is used when available and its different from thumbprint', t => { const { kid: generatedThumbprint, ...jwk } = generateSync('oct').toJWK(true) - const key = importKey({ ...jwk, kid: 'foo' }) + const key = asKey({ ...jwk, kid: 'foo' }) t.is(key.kid, 'foo') t.is(key.thumbprint, generatedThumbprint) }) diff --git a/test/jwk/import.test.js b/test/jwk/import.test.js index ff79d69f3a..d0a8adbf74 100644 --- a/test/jwk/import.test.js +++ b/test/jwk/import.test.js @@ -1,29 +1,29 @@ const test = require('ava') const crypto = require('crypto') -const { JWS, JWE, JWK: { importKey, generate }, errors } = require('../..') +const { JWS, JWE, JWK: { asKey, importKey, generate }, errors } = require('../..') const fixtures = require('../fixtures') test('imports PrivateKeyObject and then its Key instance', t => { - const k = importKey(crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }).privateKey) - t.deepEqual(importKey(k).toJWK(), k.toJWK()) + const k = asKey(crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }).privateKey) + t.deepEqual(asKey(k).toJWK(), k.toJWK()) }) test('imports PublicKeyObject and then its Key instance', t => { - const k = importKey(crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }).publicKey) - t.deepEqual(importKey(k).toJWK(), k.toJWK()) + const k = asKey(crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }).publicKey) + t.deepEqual(asKey(k).toJWK(), k.toJWK()) }) test('imports SecretKeyObject and then its Key instance', t => { - const k = importKey(crypto.createSecretKey(Buffer.from('foo'))) - t.deepEqual(importKey(k).toJWK(), k.toJWK()) + const k = asKey(crypto.createSecretKey(Buffer.from('foo'))) + t.deepEqual(asKey(k).toJWK(), k.toJWK()) }) test('only imports string, object or buffer', t => { ;[Buffer, () => {}, async () => {}, true, Infinity, 1].forEach((val) => { t.throws(() => { - importKey(val) + asKey(val) }, { instanceOf: TypeError, message: 'key argument must be a string, buffer or an object' }) }) }) @@ -31,7 +31,7 @@ test('only imports string, object or buffer', t => { test('parameters must be a plain object', t => { ;[Buffer, () => {}, async () => {}, true, Infinity, 1, [], Buffer.from('foo')].forEach((val) => { t.throws(() => { - importKey('foo', val) + asKey('foo', val) }, { instanceOf: TypeError, message: 'parameters argument must be a plain object when provided' }) }) }) @@ -39,35 +39,35 @@ test('parameters must be a plain object', t => { Object.entries(fixtures.PEM).forEach(([type, { private: priv, public: pub }]) => { test(`fails to import ${type} as invalid string`, t => { t.throws(() => { - importKey(priv.toString('ascii').replace(/\n/g, '')) + asKey(priv.toString('ascii').replace(/\n/g, '')) }, { instanceOf: errors.JWKImportFailed, code: 'ERR_JWK_IMPORT_FAILED' }) }) test(`fails to import ${type} as invalid buffer`, t => { t.throws(() => { - importKey(Buffer.from(priv.toString('ascii').replace(/\n/g, ''))) + asKey(Buffer.from(priv.toString('ascii').replace(/\n/g, ''))) }, { instanceOf: errors.JWKImportFailed, code: 'ERR_JWK_IMPORT_FAILED' }) }) test(`${type} private can be imported as a string`, t => { - const k = importKey(priv.toString('ascii')) + const k = asKey(priv.toString('ascii')) t.true(k.private) }) test(`${type} public can be imported as a string`, t => { - const k = importKey(pub.toString('ascii')) + const k = asKey(pub.toString('ascii')) t.true(k.public) }) test(`${type} private can be imported as a buffer`, t => { - const k = importKey(priv) + const k = asKey(priv) t.true(k.private) }) test(`${type} public can be imported as a buffer`, t => { - const k = importKey(pub) + const k = asKey(pub) t.true(k.public) }) }) test('failed to import throws an error', t => { t.throws(() => { - importKey({ + asKey({ key: fixtures.PEM.RSA.public, format: 'der' }) @@ -81,7 +81,7 @@ test('failed to import throws an error', t => { ].forEach((unsupported, i) => { test(`fails to import unsupported PEM ${i + 1}/4`, t => { t.throws(() => { - importKey(unsupported) + asKey(unsupported) }, { instanceOf: errors.JOSENotSupported, code: 'ERR_JOSE_NOT_SUPPORTED', message: 'only RSA, EC and OKP asymmetric keys are supported' }) }) }) @@ -89,7 +89,8 @@ test('failed to import throws an error', t => { test('minimal RSA test', async t => { const key = await generate('RSA') const { d, e, n } = key.toJWK(true) - const minKey = importKey({ kty: 'RSA', d, e, n }) + const minKey = asKey({ kty: 'RSA', d, e, n }, { calculateMissingRSAPrimes: true }) + importKey({ kty: 'RSA', d, e, n }) // deprecated key.algorithms('sign').forEach((alg) => { JWS.verify(JWS.sign({}, key), minKey, { alg }) JWS.verify(JWS.sign({}, minKey), key, { alg }) @@ -98,7 +99,15 @@ test('minimal RSA test', async t => { JWE.decrypt(JWE.encrypt('foo', key), minKey, { alg }) JWE.decrypt(JWE.encrypt('foo', minKey), key, { alg }) }) - t.pass() + t.throws(() => { + asKey({ kty: 'RSA', d: d.substr(1), e, n }, { calculateMissingRSAPrimes: true }) + }, { instanceOf: errors.JWKImportFailed, code: 'ERR_JWK_IMPORT_FAILED', message: 'failed to calculate missing primes' }) + t.throws(() => { + asKey({ kty: 'RSA', d, e, n }) + }, { instanceOf: errors.JOSENotSupported, code: 'ERR_JOSE_NOT_SUPPORTED', message: 'importing private RSA keys without all other private key parameters is not enabled, see documentation and its advisory on how and when its ok to enable it' }) + t.throws(() => { + asKey({ kty: 'RSA', d: `${d}F`, e, n }, { calculateMissingRSAPrimes: true }) + }, { instanceOf: errors.JWKInvalid, code: 'ERR_JWK_INVALID', message: 'invalid RSA private exponent' }) }) test('fails to import RSA without all optimization parameters', async t => { @@ -106,15 +115,15 @@ test('fails to import RSA without all optimization parameters', async t => { for (const param of ['p', 'q', 'dp', 'dq', 'qi']) { const { [param]: omit, ...jwk } = full t.throws(() => { - importKey(jwk) - }, { instanceOf: errors.JWKImportFailed, code: 'ERR_JWK_IMPORT_FAILED', message: 'all other private key parameters must be present when any one of them is present' }) + asKey(jwk) + }, { instanceOf: errors.JWKInvalid, code: 'ERR_JWK_INVALID', message: 'all other private key parameters must be present when any one of them is present' }) } }) test('fails to import JWK RSA with oth', async t => { const jwk = (await generate('RSA')).toJWK(true) t.throws(() => { - importKey({ + asKey({ ...jwk, oth: [] }) diff --git a/test/jwk/key_ops.test.js b/test/jwk/key_ops.test.js index 94be1ca5aa..5c15935e83 100644 --- a/test/jwk/key_ops.test.js +++ b/test/jwk/key_ops.test.js @@ -2,77 +2,77 @@ const test = require('ava') const crypto = require('crypto') const errors = require('../../lib/errors') -const importKey = require('../../lib/jwk/import') +const asKey = require('../../lib/jwk/import') const { generateSync } = require('../../lib/jwk/generate') -const jwk = importKey('foo').toJWK(true) +const jwk = asKey('foo').toJWK(true) test('key_ops ignores unrecognized values', t => { - importKey({ ...jwk, key_ops: ['sign', 'verify', 'foo'] }) + asKey({ ...jwk, key_ops: ['sign', 'verify', 'foo'] }) t.pass() }) test('key_ops ignores duplicate values', t => { - const k = importKey({ ...jwk, key_ops: ['sign', 'verify', 'sign'] }) + const k = asKey({ ...jwk, key_ops: ['sign', 'verify', 'sign'] }) t.deepEqual(k.key_ops, ['sign', 'verify']) }) test('key_ops can be combined with use if consistent', t => { - importKey({ ...jwk, key_ops: ['sign', 'verify'], use: 'sig' }) + asKey({ ...jwk, key_ops: ['sign', 'verify'], use: 'sig' }) t.pass() }) test('key_ops are part of toJWK', t => { - const k = importKey({ ...jwk, key_ops: ['sign', 'verify'], use: 'sig' }) + const k = asKey({ ...jwk, key_ops: ['sign', 'verify'], use: 'sig' }) t.deepEqual(k.toJWK().key_ops, ['sign', 'verify']) t.deepEqual(k.toJWK(true).key_ops, ['sign', 'verify']) }) test('key_ops must be an array', t => { t.throws(() => { - importKey({ ...jwk, key_ops: 'wrapKey' }) + asKey({ ...jwk, key_ops: 'wrapKey' }) }, { instanceOf: TypeError, message: '`key_ops` must be a non-empty array of strings when provided' }) }) test('key_ops must not be empty', t => { t.throws(() => { - importKey({ ...jwk, key_ops: [] }) + asKey({ ...jwk, key_ops: [] }) }, { instanceOf: TypeError, message: '`key_ops` must be a non-empty array of strings when provided' }) }) test('key_ops must only contain strings', t => { t.throws(() => { - importKey({ ...jwk, key_ops: ['wrapKey', true] }) + asKey({ ...jwk, key_ops: ['wrapKey', true] }) }, { instanceOf: TypeError, message: '`key_ops` must be a non-empty array of strings when provided' }) }) -test('JWK importKey with invalid use / key_ops throws', t => { +test('JWK asKey with invalid use / key_ops throws', t => { t.throws(() => { - importKey({ ...jwk, use: 'sig', key_ops: ['wrapKey'] }) + asKey({ ...jwk, use: 'sig', key_ops: ['wrapKey'] }) }, { instanceOf: errors.JWKInvalid, code: 'ERR_JWK_INVALID' }) }) -test('keyObject importKey with invalid use / key_ops throws 1/2', t => { +test('keyObject asKey with invalid use / key_ops throws 1/2', t => { const { publicKey } = crypto.generateKeyPairSync('ed25519') t.throws(() => { - importKey(publicKey, { use: 'sig', key_ops: ['wrapKey'] }) + asKey(publicKey, { use: 'sig', key_ops: ['wrapKey'] }) }, { instanceOf: errors.JWKInvalid, code: 'ERR_JWK_INVALID' }) }) -test('keyObject importKey with invalid use / key_ops throws 2/2', t => { +test('keyObject asKey with invalid use / key_ops throws 2/2', t => { const { publicKey } = crypto.generateKeyPairSync('ed25519') t.throws(() => { - importKey(publicKey, { use: 'enc', key_ops: ['sign'] }) + asKey(publicKey, { use: 'enc', key_ops: ['sign'] }) }, { instanceOf: errors.JWKInvalid, code: 'ERR_JWK_INVALID' }) }) -test('PEM importKey with invalid use / key_ops throws', t => { +test('PEM asKey with invalid use / key_ops throws', t => { const { publicKey } = crypto.generateKeyPairSync('ed25519') t.throws(() => { - importKey(publicKey.export({ type: 'spki', format: 'pem' }), { use: 'sig', key_ops: ['wrapKey'] }) + asKey(publicKey.export({ type: 'spki', format: 'pem' }), { use: 'sig', key_ops: ['wrapKey'] }) }, { instanceOf: errors.JWKInvalid, code: 'ERR_JWK_INVALID' }) }) diff --git a/test/jwk/oct.test.js b/test/jwk/oct.test.js index 01244fbe0e..430849349f 100644 --- a/test/jwk/oct.test.js +++ b/test/jwk/oct.test.js @@ -4,7 +4,7 @@ const { hasProperty, hasNoProperties } = require('../macros') const errors = require('../../lib/errors') const OctKey = require('../../lib/jwk/key/oct') -const { JWK: { importKey } } = require('../..') +const { JWK: { asKey } } = require('../..') const { generateSync } = require('../../lib/jwk/generate') const keyObject = createSecretKey(Buffer.from('secret')) @@ -113,7 +113,7 @@ test('oct keys may not be generated as public', t => { }) test('they may be imported from', t => { - const key = importKey({ + const key = asKey({ kty: 'oct', kid: '4p9o4_DcKoT6Qg2BI_mSgMP_MsXwFqogKuI26CunKAM' }) @@ -126,7 +126,7 @@ test('they may be imported from', t => { }) test('they may be imported from (no kid)', t => { - const key = importKey({ + const key = asKey({ kty: 'oct' }) @@ -141,7 +141,7 @@ test('they may be imported from (no kid)', t => { test('they may be imported so long as there was no k', t => { t.throws(() => { - importKey({ + asKey({ kty: 'oct', kid: '4p9o4_DcKoT6Qg2BI_mSgMP_MsXwFqogKuI26CunKAM', k: undefined diff --git a/test/jwk/x5c_thumbprints.test.js b/test/jwk/x5c_thumbprints.test.js index 0abc6fe4a8..dd58bb0371 100644 --- a/test/jwk/x5c_thumbprints.test.js +++ b/test/jwk/x5c_thumbprints.test.js @@ -2,7 +2,7 @@ const test = require('ava') const errors = require('../../lib/errors') -const { JWK: { importKey } } = require('../..') +const { JWK: { asKey } } = require('../..') const jwk = { kty: 'RSA', @@ -17,7 +17,7 @@ const jwk = { test('x5c can be imported and have their X.509 cert thumbprints calculated', t => { let key - t.notThrows(() => { key = importKey(jwk) }) + t.notThrows(() => { key = asKey(jwk) }) t.deepEqual(key.x5c, jwk.x5c) const asJWK = key.toJWK() t.deepEqual(asJWK.x5c, jwk.x5c) @@ -30,13 +30,13 @@ test('x5c can be imported and have their X.509 cert thumbprints calculated', t = test('checks that x5c is an array of valid PKIX certificates', t => { ;[[], {}, false, 1].forEach((value) => { t.throws(() => { - importKey({ + asKey({ ...jwk, x5c: value }) }, { instanceOf: TypeError, message: '`x5c` must be an array of one or more PKIX certificates when provided' }) t.throws(() => { - importKey({ + asKey({ ...jwk, x5c: [value] }) @@ -46,7 +46,7 @@ test('checks that x5c is an array of valid PKIX certificates', t => { test('checks that first x5c member must represent the key', t => { t.throws(() => { - importKey({ + asKey({ ...jwk, x5c: [ 'MIIC/zCCAeegAwIBAgIJYdZUZz2rikftMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNVBAMTEnBhbnZhLmV1LmF1dGgwLmNvbTAeFw0xNzEwMTgxNTExMjBaFw0zMTA2MjcxNTExMjBaMB0xGzAZBgNVBAMTEnBhbnZhLmV1LmF1dGgwLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKROvB+A+ZlFV1AXl75tVegjaCuBE7CiXHNstVZ/F6fKl6OvIRhAW3YKnJEglzVvHw0q46Nw48yBdbbKjdwGo1jbrI15D2+MYPy8xlMfDzEqNWBjOsgnA1nhFFDXD7wITwFRMtlRKVvKMa19QCmMFrpQ2qcloMne/DzSvxlEnVA6DG1SYqHR/gdK5hoRATJkwHXQ5F/nUxD3BOAyyjsU5RsGJAeVVS4Yf532xmziIbda3iV4LMUiHUb1v8Oy2sDncYF+imq/sbHGgE7dyv5R5AsYHGANgvIPMHJ1QTFSQVU0lxPy+EWnLk9abVOZYzD6O5YRdJ29UWVtQ1q5UcyrF18CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUSUcoPFHi/vm7dw1rt/IRmxvLMyowDgYDVR0PAQH/BAQDAgKEMA0GCSqGSIb3DQEBCwUAA4IBAQBcBXXBcbqliVOHkTgxocSYNUajcgIKjgeqG9RKFkbHfPuK/Hn80vQhd6mBKJTIyM7fY7DPh1/PjRsAyDQEwouHWItcM6iJBSdAkPq2DPfCkpUOi7MHrhXSouU1X4IOBvAl94k9Z8oj5k12KWVH8jZn5G03lwkWUgSfkLJ0Dh86+4sF2W4Dz2qZUXZuQbUL5eJcWRpfEZowff+T8xsiRjcIEpgfLz4nWonijtvEWESEa3bYpI9pI5OXLImgVJLGxVaUktsGIexQ6eM1AoxBYE7E+nbN/rwo30XWGbTkYecisySSYuzVn2c0xnC/8ZvW+gJ4SkzRDjlOAbm3R0r5j7b1' @@ -54,7 +54,7 @@ test('checks that first x5c member must represent the key', t => { }) }, { instanceOf: errors.JWKInvalid, code: 'ERR_JWK_INVALID', message: 'The key in the first `x5c` certificate MUST match the public key represented by the JWK' }) t.throws(() => { - importKey({ + asKey({ ...jwk, x5c: [ jwk.x5c[0], diff --git a/test/jwks/keystore.test.js b/test/jwks/keystore.test.js index 88cd22c480..c16e8e1eed 100644 --- a/test/jwks/keystore.test.js +++ b/test/jwks/keystore.test.js @@ -1,7 +1,8 @@ const test = require('ava') -const KeyStore = require('../../lib/jwks/keystore') -const { importKey, generateSync } = require('../../lib/jwk') +const { KeyStore, asKeyStore } = require('../../lib/jwks') +const { asKey, generateSync } = require('../../lib/jwk') +const errors = require('../../lib/errors') const withX5C = { kty: 'RSA', @@ -30,10 +31,10 @@ test('constructor', t => { }) }) -test('constructor only accepts Key instances created through JWK.importKey', t => { +test('constructor only accepts Key instances created through JWK.asKey', t => { t.throws(() => { new KeyStore({}) // eslint-disable-line no-new - }, { instanceOf: TypeError, message: 'all keys must be an instances of a key instantiated by JWK.importKey' }) + }, { instanceOf: TypeError, message: 'all keys must be an instances of a key instantiated by JWK.asKey' }) }) test('.generate()', async t => { @@ -56,7 +57,7 @@ test('.add()', t => { t.is(ks.size, 1) t.throws(() => { ks.add({}) - }, { instanceOf: TypeError, message: 'key must be an instance of a key instantiated by JWK.importKey' }) + }, { instanceOf: TypeError, message: 'key must be an instance of a key instantiated by JWK.asKey' }) }) test('.remove()', t => { @@ -67,7 +68,7 @@ test('.remove()', t => { ks.remove(k) t.throws(() => { ks.remove({}) - }, { instanceOf: TypeError, message: 'key must be an instance of a key instantiated by JWK.importKey' }) + }, { instanceOf: TypeError, message: 'key must be an instance of a key instantiated by JWK.asKey' }) }) test('.all() key_ops must be an array', t => { @@ -158,25 +159,25 @@ test('.all() and .get() kid filter', t => { }) test('.all() and .get() x5t filter and sort', t => { - const k = importKey(withX5C) + const k = asKey(withX5C) const ks = new KeyStore(k) t.deepEqual(ks.all({ x5t: 'baz' }), []) t.deepEqual(ks.all({ x5t: '4pNenEBLv0JpLIdugWxQkOsZcK0' }), [k]) t.is(ks.get({ x5t: 'baz' }), undefined) t.is(ks.get({ x5t: '4pNenEBLv0JpLIdugWxQkOsZcK0' }), k) - const k2 = importKey({ ...withX5C, alg: 'RS256' }) + const k2 = asKey({ ...withX5C, alg: 'RS256' }) ks.add(k2) t.is(ks.get({ x5t: '4pNenEBLv0JpLIdugWxQkOsZcK0', alg: 'RS256' }), k2) }) test('.all() and .get() x5t#S256 filter and sort', t => { - const k = importKey(withX5C) + const k = asKey(withX5C) const ks = new KeyStore(k) t.deepEqual(ks.all({ 'x5t#S256': 'baz' }), []) t.deepEqual(ks.all({ 'x5t#S256': 'pJm2BBpkB8y7tCqrWM0X37WOmQTO8zQw-VpxVgBb21I' }), [k]) t.is(ks.get({ 'x5t#S256': 'baz' }), undefined) t.is(ks.get({ 'x5t#S256': 'pJm2BBpkB8y7tCqrWM0X37WOmQTO8zQw-VpxVgBb21I' }), k) - const k2 = importKey({ ...withX5C, alg: 'RS256' }) + const k2 = asKey({ ...withX5C, alg: 'RS256' }) ks.add(k2) t.is(ks.get({ 'x5t#S256': 'pJm2BBpkB8y7tCqrWM0X37WOmQTO8zQw-VpxVgBb21I', alg: 'RS256' }), k2) }) @@ -210,25 +211,26 @@ test('.all() and .get() alg sort', t => { t.is(ks.get({ alg: 'RS256' }), k2) }) -test('.fromJWKS()', t => { +test('.asKeyStore()', t => { const ks = new KeyStore() ks.generateSync('EC') ks.generateSync('RSA') - const ks2 = KeyStore.fromJWKS(ks.toJWKS()) + const ks2 = asKeyStore(ks.toJWKS()) + t.true(ks2 instanceof KeyStore) t.is(ks2.size, 2) }) -test('.fromJWKS() input validation', t => { +test('.asKeyStore() input validation', t => { [Buffer, 1, false, '', 'foo', {}, { foo: 'bar' }].forEach((val) => { t.throws(() => { - KeyStore.fromJWKS(val) + asKeyStore(val) }, { instanceOf: TypeError, message: 'jwks must be a JSON Web Key Set formatted object' }) t.throws(() => { - KeyStore.fromJWKS({ keys: val }) + asKeyStore({ keys: val }) }, { instanceOf: TypeError, message: 'jwks must be a JSON Web Key Set formatted object' }) t.throws(() => { - KeyStore.fromJWKS({ keys: [val] }) + asKeyStore({ keys: [val] }) }, { instanceOf: TypeError, message: 'jwks must be a JSON Web Key Set formatted object' }) }) }) @@ -242,3 +244,19 @@ test('keystore instance is an iterator', t => { } t.pass() }) + +test('minimal RSA test', async t => { + const key = generateSync('RSA') + const { d, e, n } = key.toJWK(true) + asKeyStore({ keys: [{ kty: 'RSA', d, e, n }] }, { calculateMissingRSAPrimes: true }) + KeyStore.fromJWKS({ keys: [{ kty: 'RSA', d, e, n }] }) // deprecated + t.throws(() => { + asKeyStore({ keys: [{ kty: 'RSA', d: d.substr(1), e, n }] }, { calculateMissingRSAPrimes: true }) + }, { instanceOf: errors.JWKImportFailed, code: 'ERR_JWK_IMPORT_FAILED', message: 'failed to calculate missing primes' }) + t.throws(() => { + asKeyStore({ keys: [{ kty: 'RSA', d, e, n }] }) + }, { instanceOf: errors.JOSENotSupported, code: 'ERR_JOSE_NOT_SUPPORTED', message: 'importing private RSA keys without all other private key parameters is not enabled, see documentation and its advisory on how and when its ok to enable it' }) + t.throws(() => { + asKeyStore({ keys: [{ kty: 'RSA', d: `${d}F`, e, n }] }, { calculateMissingRSAPrimes: true }) + }, { instanceOf: errors.JWKInvalid, code: 'ERR_JWK_INVALID', message: 'invalid RSA private exponent' }) +}) diff --git a/test/jws/b64.test.js b/test/jws/b64.test.js index 10a1c4375c..d66d024f7b 100644 --- a/test/jws/b64.test.js +++ b/test/jws/b64.test.js @@ -2,7 +2,7 @@ const test = require('ava') const { JWK, JWS, errors } = require('../..') -const k = JWK.importKey({ +const k = JWK.asKey({ kty: 'oct', k: 'AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow' }) diff --git a/test/jws/sanity.test.js b/test/jws/sanity.test.js index fd74c66631..cdb1e8dc23 100644 --- a/test/jws/sanity.test.js +++ b/test/jws/sanity.test.js @@ -38,7 +38,7 @@ test('verify key or store argument', t => { ;[{}, new Object(), false, null, Infinity, 0, Buffer.from('foo')].forEach((val) => { // eslint-disable-line no-new-object t.throws(() => { JWS.verify('..', val) - }, { instanceOf: TypeError, message: 'key must be an instance of a key instantiated by JWK.importKey or a JWKS.KeyStore' }) + }, { instanceOf: TypeError, message: 'key must be an instance of a key instantiated by JWK.asKey or a JWKS.KeyStore' }) }) }) @@ -73,7 +73,7 @@ test('JWS sign rejects non keys', t => { ;[[], false, true, undefined, null, Infinity, 0].forEach((val) => { t.throws(() => { JWS.sign('foo', val) - }, { instanceOf: TypeError, message: 'key must be an instance of a key instantiated by JWK.importKey' }) + }, { instanceOf: TypeError, message: 'key must be an instance of a key instantiated by JWK.asKey' }) }) }) diff --git a/test/jws/smoke.test.js b/test/jws/smoke.test.js index c3d2d28280..17e014f5c0 100644 --- a/test/jws/smoke.test.js +++ b/test/jws/smoke.test.js @@ -1,7 +1,7 @@ const test = require('ava') const { sign, verify } = require('../../lib/jws') -const { JWK: { importKey, generateSync }, errors } = require('../..') +const { JWK: { asKey, generateSync }, errors } = require('../..') const PAYLOAD = {} @@ -61,8 +61,8 @@ const failure = (t, sKey, vKey, alg) => { } Object.entries(fixtures.PEM).forEach(([type, { private: key, public: pub }]) => { - const sKey = importKey(key) - const vKey = importKey(pub) + const sKey = asKey(key) + const vKey = asKey(pub) sKey.algorithms('sign').forEach((alg) => { test(`key ${type} > alg ${alg}`, success, sKey, vKey, alg) @@ -78,8 +78,8 @@ sym.algorithms('sign').forEach((alg) => { { const rsa = generateSync('RSA') - const sKey = importKey({ kty: 'RSA', e: rsa.e, n: rsa.n, d: rsa.d }) - const vKey = importKey({ kty: 'RSA', e: rsa.e, n: rsa.n }) + const sKey = asKey({ kty: 'RSA', e: rsa.e, n: rsa.n, d: rsa.d }, { calculateMissingRSAPrimes: true }) + const vKey = asKey({ kty: 'RSA', e: rsa.e, n: rsa.n }) sKey.algorithms('sign').forEach((alg) => { test(`key RSA (min) > alg ${alg}`, success, sKey, vKey, alg) test(`key RSA (min) > alg ${alg} (negative cases)`, failure, sKey, vKey, alg) From 48655f8399935a47a8721a72abb993a94149c9ba Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 20 Jun 2019 23:35:34 +0200 Subject: [PATCH 20/22] chore: update dependencies --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index e17f3c4eec..f7fb1e2b41 100644 --- a/package.json +++ b/package.json @@ -50,14 +50,14 @@ ] }, "dependencies": { - "asn1.js": "^5.1.0" + "asn1.js": "^5.1.1" }, "devDependencies": { "@commitlint/cli": "^8.0.0", "@commitlint/config-conventional": "^8.0.0", "ava": "^2.1.0", - "babel-eslint": "^10.0.1", - "c8": "^5.0.0", + "babel-eslint": "^10.0.2", + "c8": "^5.0.1", "husky": "^2.4.1", "standard": "^12.0.1" }, From 2969e559c6c316f881e674963860388e22825acc Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Fri, 21 Jun 2019 15:03:20 +0200 Subject: [PATCH 21/22] chore(release): 1.3.0 --- CHANGELOG.md | 19 ++----------------- package.json | 2 +- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d8a1a6d77..39647956a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,27 +2,12 @@ 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. -# YANKED [1.2.0](https://github.com/panva/jose/compare/v1.1.0...v1.2.0) (2019-05-25) - - -### Features - -* add support for JWK x5c, x5t and x5t#S256 ([9d46c48](https://github.com/panva/jose/commit/9d46c48)) -* instances of JWKS.KeyStore are now iterable (e.g. for ... of) ([2eae293](https://github.com/panva/jose/commit/2eae293)) - - - -# YANKED [1.1.0](https://github.com/panva/jose/compare/v1.0.2...v1.1.0) (2019-05-23) +# [1.3.0](https://github.com/panva/jose/compare/v1.2.0...v1.3.0) (2019-06-21) ### Bug Fixes -* reject rsa keys without all factors and exponents with a specific message ([b0ff436](https://github.com/panva/jose/commit/b0ff436)) - - -### Features - -* compute private RSA key p, q, dp, dq, qi when omitted ([6e3d6fd](https://github.com/panva/jose/commit/6e3d6fd)), closes [#26](https://github.com/panva/jose/issues/26) +* limit calculation of missing RSA private components ([5b53cb0](https://github.com/panva/jose/commit/5b53cb0)) diff --git a/package.json b/package.json index f7fb1e2b41..fcbdf41e8a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@panva/jose", - "version": "1.2.0", + "version": "1.3.0", "description": "JSON Web Almost Everything - JWA, JWS, JWE, JWK, JWT, JWKS for Node.js with minimal dependencies", "keywords": [ "compact", From c51dc28cfd27e890d0a4fcb795381fc96f141439 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Fri, 21 Jun 2019 15:09:38 +0200 Subject: [PATCH 22/22] docs: update changelog.md --- CHANGELOG.md | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39647956a4..590a34045a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,39 @@ 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.3.0](https://github.com/panva/jose/compare/v1.2.0...v1.3.0) (2019-06-21) +# [1.3.0](https://github.com/panva/jose/compare/v1.0.2...v1.3.0) (2019-06-21) +### Features + +* compute private RSA key p, q, dp, dq, qi when omitted ([6e3d6fd](https://github.com/panva/jose/commit/6e3d6fd)), closes [#26](https://github.com/panva/jose/issues/26) +* add support for JWK x5c, x5t and x5t#S256 ([9d46c48](https://github.com/panva/jose/commit/9d46c48)) +* instances of JWKS.KeyStore are now iterable (e.g. for ... of) ([2eae293](https://github.com/panva/jose/commit/2eae293)) + ### Bug Fixes * limit calculation of missing RSA private components ([5b53cb0](https://github.com/panva/jose/commit/5b53cb0)) +* reject rsa keys without all factors and exponents with a specific message ([b0ff436](https://github.com/panva/jose/commit/b0ff436)) + +### Deprecations + +- this deprecates the use of `JWK.importKey` in favor of +`JWK.asKey` +- this deprecates the use of `JWKS.KeyStore.fromJWKS` in favor of +`JWKS.asKeyStore` + +Both `JWK.importKey` and `JWKS.KeyStore.fromJWKS` could have resulted +in the process getting blocked when large bitsize RSA private keys +were missing their components and could also result in an endless +calculation loop when the private key's private exponent was outright +invalid or tampered with. + +The new methods still allow to import private RSA keys with these +optimization key parameters missing but its disabled by default and one +should choose to enable it when working with keys from trusted sources + +It is recommended not to use @panva/jose versions with this feature in +its original on-by-default form - v1.1.0 and v1.2.0