diff --git a/docs/README.md b/docs/README.md index 9e62d2aa30..c00321d62f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -53,8 +53,11 @@ If you or your business use `jose`, please consider becoming a [sponsor][support - [JWK.isKey(object)](#jwkiskeyobject) -All `jose` operations require `` or `` as arguments. Here's -how to get a `` instances generated or instantiated from existing key material. +All sign and encrypt operations require `` or `JWK.asKey()` compatible input. +All verify and decrypt operations require ``, ``, or `JWK.asKey()` compatible input. + +Whenever you're re-using the same key input for an operation it is recommended that you instantiate +the `` instance. Here's how to get a `` instances generated or instantiated from existing key material. ```js @@ -756,7 +759,9 @@ that will be used to sign with is either provided as part of the 'options.algori 'options.header.alg' or inferred from the provided `` instance. - `payload`: `` JWT Claims Set -- `key`: `` The key to sign with. +- `key`: `` The key to sign with. Any `JWK.asKey()` compatible input also works. + `` instances are recommended for performance purposes when re-using the same key for + every operation. - `options`: `` - `algorithm`: `` The algorithm to use - `audience`: `` | `string[]` JWT Audience, "aud" claim value, if provided it will replace @@ -817,7 +822,9 @@ Verifies the claims and signature of a JSON Web Token. - `token`: `` JSON Web Token to verify - `keyOrStore`: `` | `` The key or store to verify with. When `` instance is provided a selection of possible candidate keys will be done and the - operation will succeed if just one key matches. + operation will succeed if just one key matches. Any `JWK.asKey()` compatible input also works. + `` instances are recommended for performance purposes when re-using the same key for + every operation. - `options`: `` - `algorithms`: `string[]` Array of expected signing algorithms. JWT signed with an algorithm not found in this option will be rejected. **Default:** accepts all algorithms available on the @@ -1002,7 +1009,9 @@ Creates a new Sign object for the provided payload, intended for one or more rec Adds a recipient to the JWS, the Algorithm that will be used to sign with is either provided as part of the Protected or Unprotected Header or inferred from the provided `` instance. -- `key`: `` The key to sign with. +- `key`: `` The key to sign with. Any `JWK.asKey()` compatible input also works. + `` instances are recommended for performance purposes when re-using the same key for + every operation. - `protected`: `` Protected Header for this recipient - `header`: `` Unprotected Header for this recipient @@ -1029,7 +1038,9 @@ provided `` instance. - `payload`: `` | `` | `` The payload that will be signed. When `` it will be automatically serialized to JSON before signing -- `key`: `` The key to sign with. +- `key`: `` The key to sign with. Any `JWK.asKey()` compatible input also works. + `` instances are recommended for performance purposes when re-using the same key for + every operation. - `protected`: `` Protected Header - Returns: `` @@ -1062,7 +1073,9 @@ inferred from the provided `` instance. - `payload`: `` | `` | `` The payload that will be signed. When `` it will be automatically serialized to JSON before signing -- `key`: `` The key to sign with. +- `key`: `` The key to sign with. Any `JWK.asKey()` compatible input also works. + `` instances are recommended for performance purposes when re-using the same key for + every operation. - `protected`: `` Protected Header - `header`: `` Unprotected Header - Returns: `` @@ -1099,7 +1112,8 @@ Verifies the provided JWS in either serialization with a given `` or `< - `keyOrStore`: `` | `` The key or store to verify with. When `` instance is provided a selection of possible candidate keys will be done and the operation will succeed if just one key or signature (in case of General JWS JSON Serialization - Syntax) matches. + Syntax) matches. Any `JWK.asKey()` compatible input also works. `` instances are + recommended for performance purposes when re-using the same key for every operation. - `options`: `` - `algorithms`: `string[]` Array of Algorithms to accept, when the signature does not use an algorithm from this list the verification will fail. **Default:** 'undefined' - accepts all @@ -1232,7 +1246,9 @@ Adds a recipient to the JWE, the Algorithm that will be used to wrap or derive t Encryption Key (CEK) is either provided as part of the combined JWE Header for the recipient or inferred from the provided `` instance. -- `key`: `` The key to use for Key Management or Direct Encryption +- `key`: `` The key to use for Key Management or Direct Encryption. Any `JWK.asKey()` + compatible input also works. `` instances are recommended for performance purposes when + re-using the same key for every operation. - `header`: `` JWE Per-Recipient Unprotected Header --- @@ -1258,7 +1274,9 @@ will be used to wrap or derive the Content Encryption Key (CEK) is either provid Protected Header or inferred from the provided `` instance. - `cleartext`: `` | `` The cleartext that will be encrypted. -- `key`: `` The key to use for Key Management or Direct Encryption +- `key`: `` The key to use for Key Management or Direct Encryption. Any `JWK.asKey()` + compatible input also works. `` instances are recommended for performance purposes when + re-using the same key for every operation. - `protected`: `` JWE Protected Header - Returns: `` @@ -1271,7 +1289,9 @@ that will be used to wrap or derive the Content Encryption Key (CEK) is either p the combined JWE Header or inferred from the provided `` instance. - `cleartext`: `` | `` The cleartext that will be encrypted. -- `key`: `` The key to use for Key Management or Direct Encryption +- `key`: `` The key to use for Key Management or Direct Encryption. Any `JWK.asKey()` + compatible input also works. `` instances are recommended for performance purposes when + re-using the same key for every operation. - `protected`: `` JWE Protected Header - `unprotected`: `` JWE Shared Unprotected Header - `aad`: `` | `` JWE Additional Authenticated Data @@ -1287,7 +1307,8 @@ Verifies the provided JWE in either serialization with a given `` or `< - `keyOrStore`: `` | `` The key or store to decrypt with. When `` instance is provided a selection of possible candidate keys will be done and the operation will succeed if just one key or signature (in case of General JWE JSON Serialization - Syntax) matches. + Syntax) matches. Any `JWK.asKey()` compatible input also works. `` instances are + recommended for performance purposes when re-using the same key for every operation. - `options`: `` - `algorithms`: `string[]` Array of Algorithms to accept, when the JWE does not use an Key Management algorithm from this list the decryption will fail. **Default:** 'undefined' - diff --git a/lib/help/get_key.js b/lib/help/get_key.js new file mode 100644 index 0000000000..e8720c34fe --- /dev/null +++ b/lib/help/get_key.js @@ -0,0 +1,35 @@ +const errors = require('../errors') +const Key = require('../jwk/key/base') +const importKey = require('../jwk/import') +const { KeyStore } = require('../jwks/keystore') + +module.exports = (input, keyStoreAllowed = false) => { + if (input instanceof KeyStore) { + if (!keyStoreAllowed) { + throw new TypeError('key argument for this operation must not be a JWKS.KeyStore instance') + } + + return input + } + + if (input instanceof Key) { + return input + } + + try { + return importKey(input) + } catch (err) { + if (err instanceof errors.JOSEError && !(err instanceof errors.JWKImportFailed)) { + throw err + } + + let msg + if (keyStoreAllowed) { + msg = 'key must be an instance of a key instantiated by JWK.asKey, a valid JWK.asKey input, or a JWKS.KeyStore instance' + } else { + msg = 'key must be an instance of a key instantiated by JWK.asKey, or a valid JWK.asKey input' + } + + throw new TypeError(msg) + } +} diff --git a/lib/jwe/decrypt.js b/lib/jwe/decrypt.js index 9832f9e01b..b1adb0abe6 100644 --- a/lib/jwe/decrypt.js +++ b/lib/jwe/decrypt.js @@ -1,8 +1,8 @@ const { inflateRawSync } = require('zlib') const base64url = require('../help/base64url') +const getKey = require('../help/get_key') const { KeyStore } = require('../jwks') -const Key = require('../jwk/key/base') const errors = require('../errors') const { check, decrypt, keyManagementDecrypt } = require('../jwa') const JWK = require('../jwk') @@ -41,9 +41,7 @@ const combineHeader = (prot = {}, unprotected = {}, header = {}) => { * @public */ 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.asKey or a JWKS.KeyStore') - } + key = getKey(key, true) if (algorithms !== undefined && (!Array.isArray(algorithms) || algorithms.some(s => typeof s !== 'string' || !s))) { throw new TypeError('"algorithms" option must be an array of non-empty strings') diff --git a/lib/jwe/encrypt.js b/lib/jwe/encrypt.js index 0b937c1864..20560ea74c 100644 --- a/lib/jwe/encrypt.js +++ b/lib/jwe/encrypt.js @@ -3,10 +3,10 @@ const { deflateRawSync } = require('zlib') const { KEYOBJECT } = require('../help/consts') const generateIV = require('../help/generate_iv') const base64url = require('../help/base64url') +const getKey = require('../help/get_key') const isObject = require('../help/is_object') const { createSecretKey } = require('../help/key_object') const deepClone = require('../help/deep_clone') -const Key = require('../jwk/key/base') const importKey = require('../jwk/import') const { JWEInvalid } = require('../errors') const { check, keyManagementEncrypt, encrypt } = require('../jwa') @@ -57,9 +57,7 @@ class Encrypt { * @public */ recipient (key, header) { - if (!(key instanceof Key)) { - throw new TypeError('key must be an instance of a key instantiated by JWK.asKey') - } + key = getKey(key) if (header !== undefined && !isObject(header)) { throw new TypeError('header argument must be a plain object when provided') diff --git a/lib/jwk/import.js b/lib/jwk/import.js index e5dd747fed..cfab92c875 100644 --- a/lib/jwk/import.js +++ b/lib/jwk/import.js @@ -49,7 +49,7 @@ const asKey = (key, parameters, { calculateMissingRSAPrimes = false } = {}) => { secret = key break } - } else if (typeof key === 'object' && 'kty' in key && key.kty === 'oct') { // symmetric key + } else if (typeof key === 'object' && key && 'kty' in key && key.kty === 'oct') { // symmetric key try { secret = createSecretKey(base64url.decodeToBuffer(key.k)) } catch (err) { @@ -62,7 +62,7 @@ const asKey = (key, parameters, { calculateMissingRSAPrimes = false } = {}) => { } } parameters = mergedParameters(parameters, key) - } else if (typeof key === 'object' && 'kty' in key) { // assume JWK formatted asymmetric key + } else if (typeof key === 'object' && key && 'kty' in key) { // assume JWK formatted asymmetric key ({ calculateMissingRSAPrimes = false } = parameters || { calculateMissingRSAPrimes }) let pem @@ -81,7 +81,7 @@ const asKey = (key, parameters, { calculateMissingRSAPrimes = false } = {}) => { } parameters = mergedParameters({}, key) - } else { // | | passed to crypto.createPrivateKey or crypto.createPublicKey or passed to crypto.createSecretKey + } else if (key && (typeof key === 'object' || typeof key === 'string')) { // | | passed to crypto.createPrivateKey or crypto.createPublicKey or passed to crypto.createSecretKey try { privateKey = createPrivateKey(key) } catch (err) {} @@ -119,7 +119,7 @@ const asKey = (key, parameters, { calculateMissingRSAPrimes = false } = {}) => { return new OctKey(keyObject, parameters) } - throw new errors.JWKImportFailed('import failed') + throw new errors.JWKImportFailed('key import failed') } module.exports = asKey diff --git a/lib/jws/sign.js b/lib/jws/sign.js index d7c8adf834..3412152df3 100644 --- a/lib/jws/sign.js +++ b/lib/jws/sign.js @@ -2,9 +2,9 @@ const base64url = require('../help/base64url') const isDisjoint = require('../help/is_disjoint') const isObject = require('../help/is_object') const deepClone = require('../help/deep_clone') -const Key = require('../jwk/key/base') const { JWSInvalid } = require('../errors') const { check, sign } = require('../jwa') +const getKey = require('../help/get_key') const serializers = require('./serializers') @@ -39,9 +39,7 @@ class Sign { * @public */ recipient (key, protectedHeader, unprotectedHeader) { - if (!(key instanceof Key)) { - throw new TypeError('key must be an instance of a key instantiated by JWK.asKey') - } + key = getKey(key) if (protectedHeader !== undefined && !isObject(protectedHeader)) { throw new TypeError('protectedHeader argument must be a plain object when provided') diff --git a/lib/jws/verify.js b/lib/jws/verify.js index c75d556799..8433ad191a 100644 --- a/lib/jws/verify.js +++ b/lib/jws/verify.js @@ -1,8 +1,8 @@ const base64url = require('../help/base64url') const isDisjoint = require('../help/is_disjoint') let validateCrit = require('../help/validate_crit') +const getKey = require('../help/get_key') const { KeyStore } = require('../jwks') -const Key = require('../jwk/key/base') const errors = require('../errors') const { check, verify } = require('../jwa') @@ -15,9 +15,7 @@ const SINGLE_RECIPIENT = new Set(['compact', 'flattened']) * @public */ const jwsVerify = (skipDisjointCheck, serialization, jws, key, { crit = [], complete = false, algorithms, parse = true, encoding = 'utf8' } = {}) => { - if (!(key instanceof Key) && !(key instanceof KeyStore)) { - throw new TypeError('key must be an instance of a key instantiated by JWK.asKey or a JWKS.KeyStore') - } + key = getKey(key, true) if (algorithms !== undefined && (!Array.isArray(algorithms) || algorithms.some(s => typeof s !== 'string' || !s))) { throw new TypeError('"algorithms" option must be an array of non-empty strings') diff --git a/lib/jwt/sign.js b/lib/jwt/sign.js index d1104b5618..9e9f312ec0 100644 --- a/lib/jwt/sign.js +++ b/lib/jwt/sign.js @@ -1,6 +1,7 @@ const isObject = require('../help/is_object') const secs = require('../help/secs') const epoch = require('../help/epoch') +const getKey = require('../help/get_key') const JWS = require('../jws') const isString = require('./shared_validations').isString.bind(undefined, TypeError) @@ -74,6 +75,8 @@ module.exports = (payload, key, options = {}) => { nbf: notBefore ? unix + secs(notBefore) : payload.nbf } + key = getKey(key) + return JWS.sign(payload, key, { ...header, alg: algorithm || header.alg, diff --git a/lib/jwt/verify.js b/lib/jwt/verify.js index 0cdb5fc8bb..e9107b21af 100644 --- a/lib/jwt/verify.js +++ b/lib/jwt/verify.js @@ -1,6 +1,7 @@ const isObject = require('../help/is_object') const epoch = require('../help/epoch') const secs = require('../help/secs') +const getKey = require('../help/get_key') const JWS = require('../jws') const { KeyStore } = require('../jwks') const { JWTClaimInvalid } = require('../errors') @@ -217,6 +218,8 @@ module.exports = (token, key, options = {}) => { throw new JWTClaimInvalid('azp mismatch') } + key = getKey(key, true) + if (complete && key instanceof KeyStore) { ({ key } = JWS.verify(token, key, { crit, algorithms, complete: true })) } else { diff --git a/test/help/get_key.test.js b/test/help/get_key.test.js new file mode 100644 index 0000000000..e0904a526b --- /dev/null +++ b/test/help/get_key.test.js @@ -0,0 +1,78 @@ +const test = require('ava') + +const { createSecretKey, generateKeyPairSync } = require('crypto') + +const { keyObjectSupported } = require('../../lib/help/runtime_support') +const errors = require('../../lib/errors') +const getKey = require('../../lib/help/get_key') +const { JWKS, JWK } = require('../..') + +test('key must not be a KeyStore instance unless keyStoreAllowed is true', t => { + const ks = new JWKS.KeyStore() + t.throws(() => { + getKey(ks) + }, { instanceOf: TypeError, message: 'key argument for this operation must not be a JWKS.KeyStore instance' }) + t.is(getKey(ks, true), ks) +}) + +test('Key instances are passed through', async t => { + const k = await JWK.generate('oct') + t.is(getKey(k, true), k) +}) + +test('JWK is instantiated', async t => { + const jwk = (await JWK.generate('RSA')).toJWK() + const key = getKey(jwk) + t.truthy(key) + t.true(JWK.isKey(key)) +}) + +if (keyObjectSupported) { + test('KeyObject is instantiated', async t => { + const key = getKey(createSecretKey(Buffer.from('foo'))) + t.truthy(key) + t.true(JWK.isKey(key)) + }) +} + +test('Buffer is instantiated', async t => { + const key = getKey(Buffer.from('foo')) + t.truthy(key) + t.true(JWK.isKey(key)) + t.is(key.kty, 'oct') +}) + +test('oct tring is instantiated', async t => { + const key = getKey(Buffer.from('foo')) + t.truthy(key) + t.true(JWK.isKey(key)) + t.is(key.kty, 'oct') +}) + +test('PEM key is instantiated', async t => { + const pem = (await JWK.generate('RSA')).toPEM() + const key = getKey(pem) + t.truthy(key) + t.true(JWK.isKey(key)) + t.is(key.kty, 'RSA') +}) + +test('invalid inputs throw TypeError', t => { + ;[{}, new Object(), false, null, Infinity, 0, 1].forEach((val) => { // eslint-disable-line no-new-object + t.throws(() => { + getKey(val) + }, { instanceOf: TypeError, message: 'key must be an instance of a key instantiated by JWK.asKey, or a valid JWK.asKey input' }) + t.throws(() => { + getKey(val, true) + }, { instanceOf: TypeError, message: 'key must be an instance of a key instantiated by JWK.asKey, a valid JWK.asKey input, or a JWKS.KeyStore instance' }) + }) + + if (keyObjectSupported && !('electron' in process.versions)) { + const { privateKey, publicKey } = generateKeyPairSync('dsa', { modulusLength: 1024 }) + ;[privateKey, publicKey].forEach((val) => { + t.throws(() => { + getKey(val) + }, { instanceOf: errors.JOSENotSupported, code: 'ERR_JOSE_NOT_SUPPORTED', message: 'only RSA, EC and OKP asymmetric keys are supported' }) + }) + } +}) diff --git a/test/jwe/sanity.test.js b/test/jwe/sanity.test.js index 07fe907413..86538d2e23 100644 --- a/test/jwe/sanity.test.js +++ b/test/jwe/sanity.test.js @@ -34,14 +34,6 @@ test('compact parts length check', t => { }, { instanceOf: errors.JWEInvalid, code: 'ERR_JWE_INVALID', message: 'JWE malformed or invalid serialization' }) }) -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.asKey or a JWKS.KeyStore' }) - }) -}) - test('JWE no alg specified but cannot resolve', t => { const k1 = generateSync('RSA', undefined, { alg: 'foo' }) t.throws(() => { @@ -310,14 +302,6 @@ test('JWE encrypt aad rejects non buffers and non strings', t => { }) }) -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.asKey' }) - }) -}) - test('JWE must have recipients', t => { const encrypt = new JWE.Encrypt('foo') t.throws(() => { diff --git a/test/jwe/smoke.test.js b/test/jwe/smoke.test.js index 7500bd9c41..bfb26aa17e 100644 --- a/test/jwe/smoke.test.js +++ b/test/jwe/smoke.test.js @@ -2,7 +2,7 @@ const test = require('ava') const { randomBytes } = require('crypto') -const { edDSASupported } = require('../../lib/help/runtime_support') +const { edDSASupported, keyObjectSupported } = require('../../lib/help/runtime_support') const { JWK: { asKey, generateSync } } = require('../..') const ENCS = [ @@ -30,17 +30,37 @@ Object.entries(fixtures.PEM).forEach(([type, { private: key, public: pub }]) => ENCS.forEach((enc) => { if (alg === 'ECDH-ES' && ['A192CBC-HS384', 'A256CBC-HS512'].includes(enc)) return test(`key ${type} > alg ${alg} > ${enc}`, success, eKey, dKey, alg, enc) + test(`key ${type} > alg ${alg} > ${enc} (key as bare input)`, success, pub, key, alg, enc) + if (keyObjectSupported) { + test(`key ${type} > alg ${alg} > ${enc} (key as keyObject)`, success, eKey.keyObject, dKey.keyObject, alg, enc) + } + test(`key ${type} > alg ${alg} > ${enc} (key as JWK)`, success, eKey.toJWK(false), dKey.toJWK(true), alg, enc) test(`key ${type} > alg ${alg} > ${enc} (negative cases)`, failure, eKey, dKey, alg, enc) + test(`key ${type} > alg ${alg} > ${enc} (negative cases, key as bare input)`, failure, pub, key, alg, enc) + if (keyObjectSupported) { + test(`key ${type} > alg ${alg} > ${enc} (negative cases, key as keyObject)`, failure, eKey.keyObject, dKey.keyObject, alg, enc) + } + test(`key ${type} > alg ${alg} > ${enc} (negative cases, key as JWK)`, failure, eKey.toJWK(false), dKey.toJWK(true), alg, enc) }) }) }) ;[16, 24, 32, 48, 64].forEach((len) => { - const sym = asKey(randomBytes(len)) + const sk = randomBytes(len) + const sym = asKey(sk) ;[...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) - test(`key ${sym.kty} > alg ${alg} > ${enc} (negative cases)`, failure, sym, sym, alg, enc) + test(`key ${sym.kty} > alg ${alg} > ${enc} (key as bare input)`, success, sk, sk, alg, enc) + if (keyObjectSupported) { + test(`key ${sym.kty} > alg ${alg} > ${enc} (key as keyobject)`, success, sym.keyObject, sym.keyObject, alg, enc) + } + test(`key ${sym.kty} > alg ${alg} > ${enc} (key as JWK)`, success, sym.toJWK(true), sym.toJWK(true), alg, enc) + test(`key ${sym.kty} > alg ${alg} > ${enc} (negative cases, key as bare input)`, failure, sk, sk, alg, enc) + if (keyObjectSupported) { + test(`key ${sym.kty} > alg ${alg} > ${enc} (negative cases, key as keyobject)`, failure, sym.keyObject, sym.keyObject, alg, enc) + } + test(`key ${sym.kty} > alg ${alg} > ${enc} (negative cases, key as JWK)`, failure, sym.toJWK(true), sym.toJWK(true), alg, enc) }) }) }) diff --git a/test/jwk/oct.test.js b/test/jwk/oct.test.js index e43a0bb30a..1edb7b2174 100644 --- a/test/jwk/oct.test.js +++ b/test/jwk/oct.test.js @@ -170,5 +170,5 @@ test('they may be imported so long as there was no k', t => { kid: '4p9o4_DcKoT6Qg2BI_mSgMP_MsXwFqogKuI26CunKAM', k: undefined }) - }, { instanceOf: errors.JWKImportFailed, message: 'import failed' }) + }, { instanceOf: errors.JWKImportFailed, message: 'key import failed' }) }) diff --git a/test/jws/sanity.test.js b/test/jws/sanity.test.js index 518c89962d..70e87a5dfc 100644 --- a/test/jws/sanity.test.js +++ b/test/jws/sanity.test.js @@ -34,14 +34,6 @@ test('compact parts length check', t => { }, { instanceOf: errors.JWSInvalid, code: 'ERR_JWS_INVALID', message: 'JWS malformed or invalid serialization' }) }) -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.asKey or a JWKS.KeyStore' }) - }) -}) - test('JWS sign accepts buffer', t => { const k = generateSync('oct') JWS.sign(Buffer.from('foo'), k) @@ -69,14 +61,6 @@ test('JWS sign rejects other', t => { }) }) -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.asKey' }) - }) -}) - test('JWS sign protectedHeader rejects non objects or undefined', t => { const k = generateSync('oct') ;[[], false, true, null, Infinity, 0, Buffer.from('foo')].forEach((val) => { diff --git a/test/jws/smoke.test.js b/test/jws/smoke.test.js index 4c5680e762..8992d8478c 100644 --- a/test/jws/smoke.test.js +++ b/test/jws/smoke.test.js @@ -1,6 +1,8 @@ const test = require('ava') -const { edDSASupported } = require('../../lib/help/runtime_support') +const { randomBytes } = require('crypto') + +const { edDSASupported, keyObjectSupported } = require('../../lib/help/runtime_support') const { JWK: { asKey, generateSync } } = require('../..') const fixtures = require('../fixtures') @@ -17,14 +19,35 @@ Object.entries(fixtures.PEM).forEach(([type, { private: key, public: pub }]) => sKey.algorithms('sign').forEach((alg) => { test(`key ${type} > alg ${alg}`, success, sKey, vKey, alg) + test(`key ${type} > alg ${alg} (key as bare input)`, success, key, pub, alg) + if (keyObjectSupported) { + test(`key ${type} > alg ${alg} (key as keyObject)`, success, sKey.keyObject, vKey.keyObject, alg) + } + test(`key ${type} > alg ${alg} (key as JWK)`, success, sKey.toJWK(true), vKey.toJWK(false), alg) test(`key ${type} > alg ${alg} (negative cases)`, failure, sKey, vKey, alg) + test(`key ${type} > alg ${alg} (negative cases, key as bare input)`, failure, key, pub, alg) + if (keyObjectSupported) { + test(`key ${type} > alg ${alg} (negative cases, key as keyObject)`, failure, sKey.keyObject, vKey.keyObject, alg) + } + test(`key ${type} > alg ${alg} (negative cases, key as JWK)`, failure, sKey.toJWK(true), vKey.toJWK(false), alg) }) }) -const sym = generateSync('oct') +const sk = randomBytes(32) +const sym = asKey(sk) 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) + test(`key oct > alg ${alg}`, success, sym, sym, alg) + test(`key oct > alg ${alg} (key as bare input)`, success, sk, sk, alg) + if (keyObjectSupported) { + test(`key oct > alg ${alg} (key as keyObject)`, success, sym.keyObject, sym.keyObject, alg) + } + test(`key oct > alg ${alg} (key as JWK)`, success, sym.toJWK(true), sym.toJWK(true), alg) + test(`key oct > alg ${alg} (negative cases)`, failure, sym, sym, alg) + test(`key oct > alg ${alg} (negative cases, key as bare input)`, failure, sk, sk, alg) + if (keyObjectSupported) { + test(`key oct > alg ${alg} (negative cases, key as keyObject)`, failure, sym.keyObject, sym.keyObject, alg) + } + test(`key oct > alg ${alg} (negative cases, key as JWK)`, failure, sym.toJWK(true), sym.toJWK(true), alg) }) { diff --git a/types/index.d.ts b/types/index.d.ts index 43120ae384..76592670c0 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -24,6 +24,10 @@ export type keyType = 'RSA' | 'EC' | 'OKP' | 'oct'; export type asymmetricKeyObjectTypes = 'private' | 'public'; export type keyObjectTypes = asymmetricKeyObjectTypes | 'secret'; export type JWTProfiles = 'id_token'; +export type KeyInput = PrivateKeyInput | PublicKeyInput | string | Buffer; + +type ProduceKeyInput = JWK.Key | KeyObject | KeyInput | JWKOctKey | JWKRSAKey | JWKECKey | JWKOKPKey; +type ConsumeKeyInput = ProduceKeyInput | JWKS.KeyStore; export interface JWKOctKey extends BasicParameters { // no x5c kty: 'oct'; @@ -145,8 +149,6 @@ export namespace JWK { toJWK(private?: boolean): JWKOctKey; } - type KeyInput = PrivateKeyInput | PublicKeyInput | string | Buffer; - function isKey(object: any): boolean; function asKey(key: KeyObject | KeyInput, parameters?: KeyParameters): RSAKey | ECKey | OKPKey | OctKey; @@ -238,17 +240,17 @@ export namespace JWS { class Sign { constructor(payload: string | Buffer | object); - recipient(key: JWK.Key, protected?: object, header?: object): void; + recipient(key: ProduceKeyInput, protected?: object, header?: object): void; sign(serialization: 'compact'): string; sign(serialization: 'flattened'): FlattenedJWS; sign(serialization: 'general'): GeneralJWS; } - function sign(payload: string | Buffer | object, key: JWK.Key, protected?: object): string; + function sign(payload: string | Buffer | object, key: ProduceKeyInput, protected?: object): string; namespace sign { - function flattened(payload: string | Buffer | object, key: JWK.Key, protected?: object, header?: object): FlattenedJWS; - function general(payload: string | Buffer | object, key: JWK.Key, protected?: object, header?: object): GeneralJWS; + function flattened(payload: string | Buffer | object, key: ProduceKeyInput, protected?: object, header?: object): FlattenedJWS; + function general(payload: string | Buffer | object, key: ProduceKeyInput, protected?: object, header?: object): GeneralJWS; } interface VerifyOptions { @@ -266,10 +268,10 @@ export namespace JWS { header?: object; } - function verify(jws: string | FlattenedJWS | GeneralJWS, key: JWK.Key | JWKS.KeyStore, options?: VerifyOptions): string | object; - function verify(jws: string | FlattenedJWS | GeneralJWS, key: JWK.Key | JWKS.KeyStore, options?: VerifyOptions): Buffer; - function verify(jws: string | FlattenedJWS | GeneralJWS, key: JWK.Key | JWKS.KeyStore, options?: VerifyOptions): completeVerification; - function verify(jws: string | FlattenedJWS | GeneralJWS, key: JWK.Key | JWKS.KeyStore, options?: VerifyOptions): completeVerification; + function verify(jws: string | FlattenedJWS | GeneralJWS, key: ConsumeKeyInput, options?: VerifyOptions): string | object; + function verify(jws: string | FlattenedJWS | GeneralJWS, key: ConsumeKeyInput, options?: VerifyOptions): Buffer; + function verify(jws: string | FlattenedJWS | GeneralJWS, key: ConsumeKeyInput, options?: VerifyOptions): completeVerification; + function verify(jws: string | FlattenedJWS | GeneralJWS, key: ConsumeKeyInput, options?: VerifyOptions): completeVerification; } export namespace JWE { @@ -296,17 +298,17 @@ export namespace JWE { class Encrypt { constructor(cleartext: string | Buffer, protected?: object, unprotected?: object, aad?: string); - recipient(key: JWK.Key, header?: object): void; + recipient(key: ProduceKeyInput, header?: object): void; encrypt(serialization: 'compact'): string; encrypt(serialization: 'flattened'): FlattenedJWE; encrypt(serialization: 'general'): GeneralJWE; } - function encrypt(payload: string | Buffer, key: JWK.Key, protected?: object): string; + function encrypt(payload: string | Buffer, key: ProduceKeyInput, protected?: object): string; namespace encrypt { - function flattened(payload: string | Buffer, key: JWK.Key, protected?: object, header?: object, aad?: string): FlattenedJWE; - function general(payload: string | Buffer, key: JWK.Key, protected?: object, header?: object, aad?: string): GeneralJWE; + function flattened(payload: string | Buffer, key: ProduceKeyInput, protected?: object, header?: object, aad?: string): FlattenedJWE; + function general(payload: string | Buffer, key: ProduceKeyInput, protected?: object, header?: object, aad?: string): GeneralJWE; } interface DecryptOptions { @@ -325,8 +327,8 @@ export namespace JWE { protected?: object; } - function decrypt(jwe: string | FlattenedJWE | GeneralJWE, key: JWK.Key | JWKS.KeyStore, options?: DecryptOptions): Buffer; - function decrypt(jwe: string | FlattenedJWE | GeneralJWE, key: JWK.Key | JWKS.KeyStore, options?: DecryptOptions): completeDecrypt; + function decrypt(jwe: string | FlattenedJWE | GeneralJWE, key: ConsumeKeyInput, options?: DecryptOptions): Buffer; + function decrypt(jwe: string | FlattenedJWE | GeneralJWE, key: ConsumeKeyInput, options?: DecryptOptions): completeDecrypt; } export namespace JWT { @@ -362,8 +364,9 @@ export namespace JWT { crit?: string[]; profile?: JWTProfiles; } - function verify(jwt: string, key: JWK.Key | JWKS.KeyStore, options?: VerifyOptions): object; - function verify(jwt: string, key: JWK.Key | JWKS.KeyStore, options?: VerifyOptions): completeResult; + + function verify(jwt: string, key: ConsumeKeyInput, options?: VerifyOptions): object; + function verify(jwt: string, key: ConsumeKeyInput, options?: VerifyOptions): completeResult; interface SignOptions { iat?: boolean; @@ -379,7 +382,7 @@ export namespace JWT { nonce?: string; now?: Date; } - function sign(payload: object, key: JWK.Key, options?: SignOptions): string; + function sign(payload: object, key: ProduceKeyInput, options?: SignOptions): string; } export namespace errors {