Skip to content

Commit

Permalink
feat: allow JWK.asKey inputs for sign/verify/encrypt/decrypt operations
Browse files Browse the repository at this point in the history
  • Loading branch information
panva committed Nov 27, 2019
1 parent 56ef58e commit 5e1009a
Show file tree
Hide file tree
Showing 16 changed files with 237 additions and 91 deletions.
45 changes: 33 additions & 12 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,11 @@ If you or your business use `jose`, please consider becoming a [sponsor][support
- [JWK.isKey(object)](#jwkiskeyobject)
<!-- TOC JWK END -->

All `jose` operations require `<JWK.Key>` or `<JWKS.KeyStore>` as arguments. Here's
how to get a `<JWK.Key>` instances generated or instantiated from existing key material.
All sign and encrypt operations require `<JWK.Key>` or `JWK.asKey()` compatible input.
All verify and decrypt operations require `<JWK.Key>`, `<JWKS.KeyStore>`, or `JWK.asKey()` compatible input.

Whenever you're re-using the same key input for an operation it is recommended that you instantiate
the `<JWK.Key>` instance. Here's how to get a `<JWK.Key>` instances generated or instantiated from existing key material.


```js
Expand Down Expand Up @@ -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 `<JWK.Key>` instance.

- `payload`: `<Object>` JWT Claims Set
- `key`: `<JWK.Key>` The key to sign with.
- `key`: `<JWK.Key>` The key to sign with. Any `JWK.asKey()` compatible input also works.
`<JWK.Key>` instances are recommended for performance purposes when re-using the same key for
every operation.
- `options`: `<Object>`
- `algorithm`: `<string>` The algorithm to use
- `audience`: `<string>` &vert; `string[]` JWT Audience, "aud" claim value, if provided it will replace
Expand Down Expand Up @@ -817,7 +822,9 @@ Verifies the claims and signature of a JSON Web Token.
- `token`: `<String>` JSON Web Token to verify
- `keyOrStore`: `<JWK.Key>` &vert; `<JWKS.KeyStore>` The key or store to verify with. When
`<JWKS.KeyStore>` 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.
`<JWK.Key>` instances are recommended for performance purposes when re-using the same key for
every operation.
- `options`: `<Object>`
- `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
Expand Down Expand Up @@ -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 `<JWK.Key>` instance.

- `key`: `<JWK.Key>` The key to sign with.
- `key`: `<JWK.Key>` The key to sign with. Any `JWK.asKey()` compatible input also works.
`<JWK.Key>` instances are recommended for performance purposes when re-using the same key for
every operation.
- `protected`: `<Object>` Protected Header for this recipient
- `header`: `<Object>` Unprotected Header for this recipient

Expand All @@ -1029,7 +1038,9 @@ provided `<JWK.Key>` instance.

- `payload`: `<Object>` &vert; `<string>` &vert; `<Buffer>` The payload that will be signed. When `<Object>`
it will be automatically serialized to JSON before signing
- `key`: `<JWK.Key>` The key to sign with.
- `key`: `<JWK.Key>` The key to sign with. Any `JWK.asKey()` compatible input also works.
`<JWK.Key>` instances are recommended for performance purposes when re-using the same key for
every operation.
- `protected`: `<Object>` Protected Header
- Returns: `<string>`

Expand Down Expand Up @@ -1062,7 +1073,9 @@ inferred from the provided `<JWK.Key>` instance.

- `payload`: `<Object>` &vert; `<string>` &vert; `<Buffer>` The payload that will be signed. When `<Object>`
it will be automatically serialized to JSON before signing
- `key`: `<JWK.Key>` The key to sign with.
- `key`: `<JWK.Key>` The key to sign with. Any `JWK.asKey()` compatible input also works.
`<JWK.Key>` instances are recommended for performance purposes when re-using the same key for
every operation.
- `protected`: `<Object>` Protected Header
- `header`: `<Object>` Unprotected Header
- Returns: `<Object>`
Expand Down Expand Up @@ -1099,7 +1112,8 @@ Verifies the provided JWS in either serialization with a given `<JWK.Key>` or `<
- `keyOrStore`: `<JWK.Key>` &vert; `<JWKS.KeyStore>` The key or store to verify with. When
`<JWKS.KeyStore>` 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. `<JWK.Key>` instances are
recommended for performance purposes when re-using the same key for every operation.
- `options`: `<Object>`
- `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
Expand Down Expand Up @@ -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 `<JWK.Key>` instance.

- `key`: `<JWK.Key>` The key to use for Key Management or Direct Encryption
- `key`: `<JWK.Key>` The key to use for Key Management or Direct Encryption. Any `JWK.asKey()`
compatible input also works. `<JWK.Key>` instances are recommended for performance purposes when
re-using the same key for every operation.
- `header`: `<Object>` JWE Per-Recipient Unprotected Header

---
Expand All @@ -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 `<JWK.Key>` instance.

- `cleartext`: `<string>` &vert; `<Buffer>` The cleartext that will be encrypted.
- `key`: `<JWK.Key>` The key to use for Key Management or Direct Encryption
- `key`: `<JWK.Key>` The key to use for Key Management or Direct Encryption. Any `JWK.asKey()`
compatible input also works. `<JWK.Key>` instances are recommended for performance purposes when
re-using the same key for every operation.
- `protected`: `<Object>` JWE Protected Header
- Returns: `<string>`

Expand All @@ -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 `<JWK.Key>` instance.

- `cleartext`: `<string>` &vert; `<Buffer>` The cleartext that will be encrypted.
- `key`: `<JWK.Key>` The key to use for Key Management or Direct Encryption
- `key`: `<JWK.Key>` The key to use for Key Management or Direct Encryption. Any `JWK.asKey()`
compatible input also works. `<JWK.Key>` instances are recommended for performance purposes when
re-using the same key for every operation.
- `protected`: `<Object>` JWE Protected Header
- `unprotected`: `<Object>` JWE Shared Unprotected Header
- `aad`: `<string>` &vert; `<Buffer>` JWE Additional Authenticated Data
Expand All @@ -1287,7 +1307,8 @@ Verifies the provided JWE in either serialization with a given `<JWK.Key>` or `<
- `keyOrStore`: `<JWK.Key>` &vert; `<JWKS.KeyStore>` The key or store to decrypt with. When
`<JWKS.KeyStore>` 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. `<JWK.Key>` instances are
recommended for performance purposes when re-using the same key for every operation.
- `options`: `<Object>`
- `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' -
Expand Down
35 changes: 35 additions & 0 deletions lib/help/get_key.js
Original file line number Diff line number Diff line change
@@ -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)
}
}
6 changes: 2 additions & 4 deletions lib/jwe/decrypt.js
Original file line number Diff line number Diff line change
@@ -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')
Expand Down Expand Up @@ -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')
Expand Down
6 changes: 2 additions & 4 deletions lib/jwe/encrypt.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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')
Expand Down
8 changes: 4 additions & 4 deletions lib/jwk/import.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Object>
} else if (typeof key === 'object' && key && 'kty' in key && key.kty === 'oct') { // symmetric key <Object>
try {
secret = createSecretKey(base64url.decodeToBuffer(key.k))
} catch (err) {
Expand All @@ -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 <Object>
} else if (typeof key === 'object' && key && 'kty' in key) { // assume JWK formatted asymmetric key <Object>
({ calculateMissingRSAPrimes = false } = parameters || { calculateMissingRSAPrimes })
let pem

Expand All @@ -81,7 +81,7 @@ const asKey = (key, parameters, { calculateMissingRSAPrimes = false } = {}) => {
}

parameters = mergedParameters({}, key)
} else { // <Object> | <string> | <Buffer> passed to crypto.createPrivateKey or crypto.createPublicKey or <Buffer> passed to crypto.createSecretKey
} else if (key && (typeof key === 'object' || typeof key === 'string')) { // <Object> | <string> | <Buffer> passed to crypto.createPrivateKey or crypto.createPublicKey or <Buffer> passed to crypto.createSecretKey
try {
privateKey = createPrivateKey(key)
} catch (err) {}
Expand Down Expand Up @@ -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
Expand Down
6 changes: 2 additions & 4 deletions lib/jws/sign.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down Expand Up @@ -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')
Expand Down
6 changes: 2 additions & 4 deletions lib/jws/verify.js
Original file line number Diff line number Diff line change
@@ -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')

Expand All @@ -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')
Expand Down
3 changes: 3 additions & 0 deletions lib/jwt/sign.js
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions lib/jwt/verify.js
Original file line number Diff line number Diff line change
@@ -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')
Expand Down Expand Up @@ -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 {
Expand Down
78 changes: 78 additions & 0 deletions test/help/get_key.test.js
Original file line number Diff line number Diff line change
@@ -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' })
})
}
})
Loading

0 comments on commit 5e1009a

Please sign in to comment.