From 23b874cc20895e9c3472b3ecc8d4cd4fdf5b6289 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 9 Apr 2019 20:47:41 +0200 Subject: [PATCH] feat: add JWK key_ops support, fix .algorithms() op returns BREAKING CHANGE: key.algorithms(op) un+wrapKey was split into correct wrapKey/unwrapKey/deriveKey returns BREAKING CHANGE: keystore.all and keystore.get `operation` option was removed, `key_ops: string[]` supersedes it --- README.md | 1 - docs/README.md | 62 ++++++++++++++++----- lib/errors.js | 2 + lib/help/consts.js | 31 +++++++++++ lib/help/key_lengths.js | 10 ---- lib/help/key_utils.js | 4 +- lib/help/symbols.js | 5 -- lib/index.d.ts | 6 +- lib/jwa/aes_cbc_hmac_sha2.js | 2 +- lib/jwa/aes_gcm.js | 2 +- lib/jwa/aes_gcm_kw.js | 8 +-- lib/jwa/aes_kw.js | 10 ++-- lib/jwa/ecdh/dir.js | 10 ++-- lib/jwa/ecdh/kw.js | 14 ++--- lib/jwa/ecdsa.js | 2 +- lib/jwa/eddsa.js | 2 +- lib/jwa/hmac.js | 2 +- lib/jwa/index.js | 35 ++++++++---- lib/jwa/pbes2.js | 14 ++--- lib/jwa/rsaes.js | 10 ++-- lib/jwa/rsassa.js | 2 +- lib/jwa/rsassa_pss.js | 2 +- lib/jwe/decrypt.js | 13 +++-- lib/jwe/encrypt.js | 11 ++-- lib/jwe/generate_cek.js | 2 +- lib/jwk/import.js | 2 +- lib/jwk/key/base.js | 46 +++++++++++++-- lib/jwk/key/ec.js | 41 +++++++++----- lib/jwk/key/oct.js | 51 ++++++++++------- lib/jwk/key/okp.js | 41 ++++++++------ lib/jwk/key/rsa.js | 24 ++++++-- lib/jwks/keystore.js | 41 ++++++++++++-- lib/jws/verify.js | 2 +- test/jwa/sanity.test.js | 8 ++- test/jwe/smoke.test.js | 4 +- test/jwk/ec.test.js | 18 +++++- test/jwk/key_ops.test.js | 105 +++++++++++++++++++++++++++++++++++ test/jwk/oct.test.js | 11 +++- test/jwk/rsa.test.js | 12 ++++ test/jwks/keystore.test.js | 61 ++++++++++++++++++++ 40 files changed, 556 insertions(+), 173 deletions(-) create mode 100644 lib/help/consts.js delete mode 100644 lib/help/key_lengths.js delete mode 100644 lib/help/symbols.js create mode 100644 test/jwk/key_ops.test.js diff --git a/README.md b/README.md index 0ac606b338..91063ce4b9 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,6 @@ Won't implement: - no crypto, no use Not Planned / PR | Use-Case | Discussion Welcome: -- ◯ automatically adding `kid` reference to JWS / JWE Headers - ◯ `x5c`, `x5t`, `x5t#S256`, `x5u` etc `JWK.Key` fields diff --git a/docs/README.md b/docs/README.md index ee4003485d..a52c7c654f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -29,6 +29,7 @@ I can continue maintaining it and adding new features carefree. You may also don - [key.alg](#keyalg) - [key.use](#keyuse) - [key.kid](#keykid) + - [key.key_ops](#keykey_ops) - [key.thumbprint](#keythumbprint) - [key.type](#keytype) - [key.public](#keypublic) @@ -116,6 +117,16 @@ defined in [RFC7638][spec-thumbprint]. --- +#### `key.key_ops` + +Returns the key's JWK Key Operations Parameter if set. If set the key can only be used for the +specified operations. Supported values are 'sign', 'verify', 'encrypt', 'decrypt', 'wrapKey', +'unwrapKey' and 'deriveKey'. + +- `string[]` + +--- + #### `key.thumbprint` Returns the key's JWK Key thumbprint calculated using the method defined in [RFC7638][spec-thumbprint]. @@ -415,11 +426,15 @@ Securely generates a new RSA, EC, OKP or oct key. - `crvOrSize`: `` | `` key's bit size or in case of OKP and EC keys the curve **Default:** 2048 for RSA, 'P-256' for EC, 'Ed25519' for OKP and 256 for oct. - `options`: `` - - `alg`: `` option identifies the algorithm intended for use with the key. + - `alg`: `` Key Algorithm Parameter. It identifies the algorithm intended for use with the + key. - `kid`: `` Key ID Parameter. When not provided is computed using the method defined in - [RFC7638][spec-thumbprint] - - `use`: `` option indicates whether the key is to be used for encrypting & decrypting - data or signing & verifying data. Must be 'sig' or 'enc'. + [RFC7638][spec-thumbprint]. + - `use`: `` Public Key Use Parameter. Indicates whether the key is to be used for + encrypting & decrypting data or signing & verifying data. Must be 'sig' or 'enc'. + - `key_ops`: `string[]` Key Operations Parameter. If set, the key can only be used for the + specified operations. Supported values are 'sign', 'verify', 'encrypt', 'decrypt', 'wrapKey', + 'unwrapKey' and 'deriveKey'. - `private`: `` **Default** 'true'. Is the resulting key private or public (when asymmetrical) - Returns: `Promise` | `Promise` | `Promise` | `Promise` @@ -454,11 +469,15 @@ Synchronous version of `JWK.generate()` - `crvOrSize`: `` | `` key's bit size or in case of OKP and EC keys the curve. **Default:** 2048 for RSA, 'P-256' for EC, 'Ed25519' for OKP and 256 for oct. - `options`: `` - - `alg`: `` option identifies the algorithm intended for use with the key. - - `use`: `` option indicates whether the key is to be used for encrypting & decrypting - data or signing & verifying data. Must be 'sig' or 'enc'. + - `alg`: `` Key Algorithm Parameter. It identifies the algorithm intended for use with the + key. - `kid`: `` Key ID Parameter. When not provided is computed using the method defined in - [RFC7638][spec-thumbprint] + [RFC7638][spec-thumbprint]. + - `use`: `` Public Key Use Parameter. Indicates whether the key is to be used for + encrypting & decrypting data or signing & verifying data. Must be 'sig' or 'enc'. + - `key_ops`: `string[]` Key Operations Parameter. If set, the key can only be used for the + specified operations. Supported values are 'sign', 'verify', 'encrypt', 'decrypt', 'wrapKey', + 'unwrapKey' and 'deriveKey'. - `private`: `` **Default** 'true'. Is the resulting key private or public (when asymmetrical) - Returns: `` | `` | `` | `` @@ -551,10 +570,12 @@ specified by the parameters are first. - `parameters`: `` - `kty`: `` Key Type to filter for. - `alg`: `` Key supported algorithm to filter for. - - `use`: `` Key use to filter for. - `kid`: `` Key ID to filter for. - - `operation`: `` Further specify the operation a given alg must be valid for. Must be one - of 'encrypt', 'decrypt', 'sign', 'verify', 'wrapKey', 'unwrapKey' + - `use`: `` Filter keys with the specified use defined. Keys missing "use" parameter will + be matched but rank lower then ones with an exact match. + - `key_ops`: `string[]` Filter keys with specified key_ops defined (if key_ops is defined on the + key). Keys missing "key_ops" parameter will be matched but rank lower then ones with matching + entries. - Returns: `` Array of key instances or an empty array when none are matching the parameters. --- @@ -567,10 +588,12 @@ parameters is returned. - `parameters`: `` - `kty`: `` Key Type to filter for. - `alg`: `` Key supported algorithm to filter for. - - `use`: `` Key use to filter for. - `kid`: `` Key ID to filter for. - - `operation`: `` Further specify the operation a given alg must be valid for. Must be one - of 'encrypt', 'decrypt', 'sign', 'verify', 'wrapKey', 'unwrapKey' + - `use`: `` Filter keys with the specified use defined. Keys missing "use" parameter will + be matched but rank lower then ones with an exact match. + - `key_ops`: `string[]` Filter keys with specified key_ops defined (if key_ops is defined on the + key). Keys missing "key_ops" parameter will be matched but rank lower then ones with matching + entries. - Returns: `` | `` | `` | `` | `` --- @@ -1206,6 +1229,7 @@ Verifies the provided JWE in either serialization with a given `` or `< - [Class: <JWEDecryptionFailed>](#class-jwedecryptionfailed) - [Class: <JWEInvalid>](#class-jweinvalid) - [Class: <JWKImportFailed>](#class-jwkimportfailed) +- [Class: <JWKKeyInvalid>](#class-jwkkeyinvalid) - [Class: <JWKKeySupport>](#class-jwkkeysupport) - [Class: <JWKSNoMatchingKey>](#class-jwksnomatchingkey) - [Class: <JWSInvalid>](#class-jwsinvalid) @@ -1311,6 +1335,16 @@ if (err.code === 'ERR_JWK_IMPORT_FAILED') { } ``` +#### Class: `JWKKeyInvalid` + +Thrown when key's parameters are invalid, e.g. key_ops and use values are inconsistent. + +```js +if (err.code === 'ERR_JWK_INVALID') { + // ... +} +``` + #### Class: `JWKKeySupport` Thrown when a key does not support the request algorithm. diff --git a/lib/errors.js b/lib/errors.js index a6a345c08f..da42f5f2fc 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -6,6 +6,7 @@ const CODES = { JWEDecryptionFailed: 'ERR_JWE_DECRYPTION_FAILED', JWEInvalid: 'ERR_JWE_INVALID', JWKImportFailed: 'ERR_JWK_IMPORT_FAILED', + JWKInvalid: 'ERR_JWK_INVALID', JWKKeySupport: 'ERR_JWK_KEY_SUPPORT', JWKSNoMatchingKey: 'ERR_JWKS_NO_MATCHING_KEY', JWSInvalid: 'ERR_JWS_INVALID', @@ -63,6 +64,7 @@ module.exports.JWEDecryptionFailed = class JWEDecryptionFailed extends JOSEError module.exports.JWEInvalid = class JWEInvalid extends JOSEError {} module.exports.JWKImportFailed = class JWKImportFailed extends JOSEError {} +module.exports.JWKInvalid = class JWKInvalid extends JOSEError {} module.exports.JWKKeySupport = class JWKKeySupport extends JOSEError {} module.exports.JWKSNoMatchingKey = class JWKSNoMatchingKey extends JOSEError {} diff --git a/lib/help/consts.js b/lib/help/consts.js new file mode 100644 index 0000000000..ca18059b2c --- /dev/null +++ b/lib/help/consts.js @@ -0,0 +1,31 @@ +module.exports.KEYOBJECT = Symbol('KEYOBJECT') +module.exports.PRIVATE_MEMBERS = Symbol('PRIVATE_MEMBERS') +module.exports.PUBLIC_MEMBERS = Symbol('PUBLIC_MEMBERS') +module.exports.THUMBPRINT_MATERIAL = Symbol('THUMBPRINT_MATERIAL') +module.exports.JWK_MEMBERS = Symbol('JWK_MEMBERS') +module.exports.KEY_MANAGEMENT_ENCRYPT = Symbol('KEY_MANAGEMENT_ENCRYPT') +module.exports.KEY_MANAGEMENT_DECRYPT = Symbol('KEY_MANAGEMENT_DECRYPT') + +const USES_MAPPING = { + sig: new Set(['sign', 'verify']), + enc: new Set(['encrypt', 'decrypt', 'wrapKey', 'unwrapKey', 'deriveKey']) +} +const OPS = new Set([...USES_MAPPING.sig, ...USES_MAPPING.enc]) +const USES = new Set(Object.keys(USES_MAPPING)) + +module.exports.USES_MAPPING = USES_MAPPING +module.exports.OPS = OPS +module.exports.USES = USES + +module.exports.OKP_CURVES = new Set(['Ed25519', 'Ed448', 'X25519', 'X448']) +module.exports.EC_CURVES = new Set(['P-256', 'P-384', 'P-521']) +module.exports.ECDH_ALGS = ['ECDH-ES', 'ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW'] + +module.exports.KEYLENGTHS = { + 'A128CBC-HS256': 256, + 'A192CBC-HS384': 384, + 'A256CBC-HS512': 512, + 'A128GCM': 128, + 'A192GCM': 192, + 'A256GCM': 256 +} diff --git a/lib/help/key_lengths.js b/lib/help/key_lengths.js deleted file mode 100644 index 8cb2774ba0..0000000000 --- a/lib/help/key_lengths.js +++ /dev/null @@ -1,10 +0,0 @@ -const KEYLENGTHS = { - 'A128CBC-HS256': 256, - 'A192CBC-HS384': 384, - 'A256CBC-HS512': 512, - 'A128GCM': 128, - 'A192GCM': 192, - 'A256GCM': 256 -} - -module.exports = KEYLENGTHS diff --git a/lib/help/key_utils.js b/lib/help/key_utils.js index 7da8adf5c9..3494127235 100644 --- a/lib/help/key_utils.js +++ b/lib/help/key_utils.js @@ -3,9 +3,7 @@ const { createPublicKey } = require('crypto') const base64url = require('./base64url') const errors = require('../errors') const asn1 = require('./asn1') - -const EC_CURVES = new Set(['P-256', 'P-384', 'P-521']) -const OKP_CURVES = new Set(['Ed25519', 'Ed448', 'X25519', 'X448']) +const { OKP_CURVES, EC_CURVES } = require('./consts') const oidHexToCurve = new Map([ ['06082a8648ce3d030107', 'P-256'], diff --git a/lib/help/symbols.js b/lib/help/symbols.js deleted file mode 100644 index cfe5a251c3..0000000000 --- a/lib/help/symbols.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports.KEYOBJECT = Symbol('KEYOBJECT') -module.exports.PRIVATE_MEMBERS = Symbol('PRIVATE_MEMBERS') -module.exports.PUBLIC_MEMBERS = Symbol('PUBLIC_MEMBERS') -module.exports.THUMBPRINT_MATERIAL = Symbol('THUMBPRINT_MATERIAL') -module.exports.JWK_MEMBERS = Symbol('JWK_MEMBERS') diff --git a/lib/index.d.ts b/lib/index.d.ts index 2d69896b26..0b144378c3 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -3,15 +3,16 @@ import { KeyObject, PrivateKeyInput, PublicKeyInput } from 'crypto' type use = 'sig' | 'enc' +type keyOperation = 'sign' | 'verify' | 'encrypt' | 'decrypt' | 'wrapKey' | 'unwrapKey' | 'deriveKey' interface KeyParameters { alg?: string use?: use kid?: string + key_ops?: keyOperation[] } type ECCurve = 'P-256' | 'P-384' | 'P-521' type OKPCurve = 'Ed25519' | 'Ed448' | 'X25519' | 'X448' type keyType = 'RSA' | 'EC' | 'OKP' | 'oct' -type keyOperation = 'encrypt' | 'decrypt' | 'sign' | 'verify' | 'wrapKey' | 'unwrapKey' type asymmetricKeyObjectTypes = 'private' | 'public' type keyObjectTypes = asymmetricKeyObjectTypes | 'secret' @@ -31,6 +32,7 @@ export namespace JWK { secret: boolean alg?: string use?: use + key_ops?: keyOperation[] kid: string thumbprint: string @@ -144,7 +146,6 @@ export namespace JWK { export namespace JWKS { interface KeyQuery extends KeyParameters { kty: keyType - operation: keyOperation } class KeyStore { @@ -341,6 +342,7 @@ export namespace errors { export class JWEInvalid extends JOSEError {} export class JWKImportFailed extends JOSEError {} + export class JWKInvalid extends JOSEError {} export class JWKKeySupport extends JOSEError {} export class JWKSNoMatchingKey extends JOSEError {} diff --git a/lib/jwa/aes_cbc_hmac_sha2.js b/lib/jwa/aes_cbc_hmac_sha2.js index 2ac83eb482..264c8b6f6a 100644 --- a/lib/jwa/aes_cbc_hmac_sha2.js +++ b/lib/jwa/aes_cbc_hmac_sha2.js @@ -3,7 +3,7 @@ const { createCipheriv, createDecipheriv } = require('crypto') const uint64be = require('../help/uint64be') const timingSafeEqual = require('../help/timing_safe_equal') -const { KEYOBJECT } = require('../help/symbols') +const { KEYOBJECT } = require('../help/consts') const { JWEInvalid, JWEDecryptionFailed } = require('../errors') const checkInput = function (size, iv, tag) { diff --git a/lib/jwa/aes_gcm.js b/lib/jwa/aes_gcm.js index fc81ec223b..221dcd6dd9 100644 --- a/lib/jwa/aes_gcm.js +++ b/lib/jwa/aes_gcm.js @@ -1,7 +1,7 @@ const { strict: assert } = require('assert') const { createCipheriv, createDecipheriv } = require('crypto') -const { KEYOBJECT } = require('../help/symbols') +const { KEYOBJECT } = require('../help/consts') const { JWEInvalid, JWEDecryptionFailed } = require('../errors') const checkInput = function (size, iv, tag) { diff --git a/lib/jwa/aes_gcm_kw.js b/lib/jwa/aes_gcm_kw.js index 1b89f6f116..eb5121941c 100644 --- a/lib/jwa/aes_gcm_kw.js +++ b/lib/jwa/aes_gcm_kw.js @@ -5,14 +5,14 @@ const base64url = require('../help/base64url') module.exports = (JWA) => { ['A128GCMKW', 'A192GCMKW', 'A256GCMKW'].forEach((jwaAlg) => { - assert(!JWA.wrapKey.has(jwaAlg), `wrapKey alg ${jwaAlg} already registered`) - assert(!JWA.unwrapKey.has(jwaAlg), `unwrapKey alg ${jwaAlg} already registered`) + assert(!JWA.keyManagementEncrypt.has(jwaAlg), `keyManagementEncrypt alg ${jwaAlg} already registered`) + assert(!JWA.keyManagementDecrypt.has(jwaAlg), `keyManagementDecrypt alg ${jwaAlg} already registered`) const encAlg = jwaAlg.substr(0, 7) const encrypt = JWA.encrypt.get(encAlg) const decrypt = JWA.decrypt.get(encAlg) - JWA.wrapKey.set(jwaAlg, (key, payload) => { + JWA.keyManagementEncrypt.set(jwaAlg, (key, payload) => { const iv = generateIV(jwaAlg) const { ciphertext, tag } = encrypt(key, payload, { iv }) return { @@ -20,6 +20,6 @@ module.exports = (JWA) => { header: { tag: base64url.encodeBuffer(tag), iv: base64url.encodeBuffer(iv) } } }) - JWA.unwrapKey.set(jwaAlg, decrypt) + JWA.keyManagementDecrypt.set(jwaAlg, decrypt) }) } diff --git a/lib/jwa/aes_kw.js b/lib/jwa/aes_kw.js index 0b3dd35a0e..0483b0b055 100644 --- a/lib/jwa/aes_kw.js +++ b/lib/jwa/aes_kw.js @@ -3,7 +3,7 @@ const { createCipheriv, createDecipheriv } = require('crypto') const uint64be = require('../help/uint64be') const timingSafeEqual = require('../help/timing_safe_equal') -const { KEYOBJECT } = require('../help/symbols') +const { KEYOBJECT } = require('../help/consts') const checkInput = (data) => { if (data !== undefined && data.length % 8 !== 0) { @@ -89,10 +89,10 @@ module.exports = (JWA) => { ['A128KW', 'A192KW', 'A256KW'].forEach((jwaAlg) => { const size = parseInt(jwaAlg.substr(1, 3), 10) - assert(!JWA.wrapKey.has(jwaAlg), `wrapKey alg ${jwaAlg} already registered`) - assert(!JWA.unwrapKey.has(jwaAlg), `unwrapKey alg ${jwaAlg} already registered`) + assert(!JWA.keyManagementEncrypt.has(jwaAlg), `keyManagementEncrypt alg ${jwaAlg} already registered`) + assert(!JWA.keyManagementDecrypt.has(jwaAlg), `keyManagementDecrypt alg ${jwaAlg} already registered`) - JWA.wrapKey.set(jwaAlg, wrapKey.bind(undefined, size)) - JWA.unwrapKey.set(jwaAlg, unwrapKey.bind(undefined, size)) + JWA.keyManagementEncrypt.set(jwaAlg, wrapKey.bind(undefined, size)) + JWA.keyManagementDecrypt.set(jwaAlg, unwrapKey.bind(undefined, size)) }) } diff --git a/lib/jwa/ecdh/dir.js b/lib/jwa/ecdh/dir.js index 73453f218b..9aae306812 100644 --- a/lib/jwa/ecdh/dir.js +++ b/lib/jwa/ecdh/dir.js @@ -1,6 +1,6 @@ const { strict: assert } = require('assert') -const KEYLENGTHS = require('../../help/key_lengths') +const { KEYLENGTHS } = require('../../help/consts') const { generateSync } = require('../../jwk/generate') const derive = require('./derive') @@ -23,9 +23,9 @@ const unwrapKey = (key, payload, { apu, apv, epk, enc }) => { const ALG = 'ECDH-ES' module.exports = (JWA) => { - assert(!JWA.wrapKey.has(ALG), `wrapKey alg ${ALG} already registered`) - assert(!JWA.unwrapKey.has(ALG), `unwrapKey alg ${ALG} already registered`) + assert(!JWA.keyManagementEncrypt.has(ALG), `keyManagementEncrypt alg ${ALG} already registered`) + assert(!JWA.keyManagementDecrypt.has(ALG), `keyManagementDecrypt alg ${ALG} already registered`) - JWA.wrapKey.set(ALG, wrapKey) - JWA.unwrapKey.set(ALG, unwrapKey) + JWA.keyManagementEncrypt.set(ALG, wrapKey) + JWA.keyManagementDecrypt.set(ALG, unwrapKey) } diff --git a/lib/jwa/ecdh/kw.js b/lib/jwa/ecdh/kw.js index 9653c99b68..12990b67c9 100644 --- a/lib/jwa/ecdh/kw.js +++ b/lib/jwa/ecdh/kw.js @@ -1,6 +1,6 @@ const { strict: assert } = require('assert') -const { KEYOBJECT } = require('../../help/symbols') +const { KEYOBJECT } = require('../../help/consts') const { generateSync } = require('../../jwk/generate') const derive = require('./derive') @@ -24,15 +24,15 @@ const unwrapKey = (unwrap, derive, key, payload, { apu, apv, epk }) => { module.exports = (JWA) => { ['ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW'].forEach((jwaAlg) => { - assert(!JWA.wrapKey.has(jwaAlg), `wrapKey alg ${jwaAlg} already registered`) - assert(!JWA.unwrapKey.has(jwaAlg), `unwrapKey alg ${jwaAlg} already registered`) + assert(!JWA.keyManagementEncrypt.has(jwaAlg), `keyManagementEncrypt alg ${jwaAlg} already registered`) + assert(!JWA.keyManagementDecrypt.has(jwaAlg), `keyManagementDecrypt alg ${jwaAlg} already registered`) const kw = jwaAlg.substr(-6) - const kwWrap = JWA.wrapKey.get(kw) - const kwUnwrap = JWA.unwrapKey.get(kw) + const kwWrap = JWA.keyManagementEncrypt.get(kw) + const kwUnwrap = JWA.keyManagementDecrypt.get(kw) const keylen = parseInt(jwaAlg.substr(9, 3), 10) - JWA.wrapKey.set(jwaAlg, wrapKey.bind(undefined, kwWrap, derive.bind(undefined, jwaAlg, keylen))) - JWA.unwrapKey.set(jwaAlg, unwrapKey.bind(undefined, kwUnwrap, derive.bind(undefined, jwaAlg, keylen))) + JWA.keyManagementEncrypt.set(jwaAlg, wrapKey.bind(undefined, kwWrap, derive.bind(undefined, jwaAlg, keylen))) + JWA.keyManagementDecrypt.set(jwaAlg, unwrapKey.bind(undefined, kwUnwrap, derive.bind(undefined, jwaAlg, keylen))) }) } diff --git a/lib/jwa/ecdsa.js b/lib/jwa/ecdsa.js index 7384555f62..afdfb173e0 100644 --- a/lib/jwa/ecdsa.js +++ b/lib/jwa/ecdsa.js @@ -2,7 +2,7 @@ const { strict: assert } = require('assert') const { sign: signOneShot, verify: verifyOneShot } = require('crypto') const { derToJose, joseToDer } = require('../help/ecdsa_signatures') -const { KEYOBJECT } = require('../help/symbols') +const { KEYOBJECT } = require('../help/consts') const resolveNodeAlg = require('../help/node_alg') const sign = (jwaAlg, nodeAlg, { [KEYOBJECT]: keyObject }, payload) => { diff --git a/lib/jwa/eddsa.js b/lib/jwa/eddsa.js index d745092b92..7ebf188535 100644 --- a/lib/jwa/eddsa.js +++ b/lib/jwa/eddsa.js @@ -1,7 +1,7 @@ const { strict: assert } = require('assert') const { sign: signOneShot, verify: verifyOneShot } = require('crypto') -const { KEYOBJECT } = require('../help/symbols') +const { KEYOBJECT } = require('../help/consts') const sign = ({ [KEYOBJECT]: keyObject }, payload) => { return signOneShot(undefined, payload, keyObject) diff --git a/lib/jwa/hmac.js b/lib/jwa/hmac.js index f4aab782c2..0b090147ef 100644 --- a/lib/jwa/hmac.js +++ b/lib/jwa/hmac.js @@ -1,7 +1,7 @@ const { strict: assert } = require('assert') const { createHmac } = require('crypto') -const { KEYOBJECT } = require('../help/symbols') +const { KEYOBJECT } = require('../help/consts') const timingSafeEqual = require('../help/timing_safe_equal') const resolveNodeAlg = require('../help/node_alg') diff --git a/lib/jwa/index.js b/lib/jwa/index.js index c935d308f3..1d35cfc492 100644 --- a/lib/jwa/index.js +++ b/lib/jwa/index.js @@ -1,10 +1,11 @@ const { JWKKeySupport, JOSENotSupported } = require('../errors') +const { KEY_MANAGEMENT_ENCRYPT, KEY_MANAGEMENT_DECRYPT } = require('../help/consts') const JWA = { sign: new Map(), verify: new Map(), - wrapKey: new Map(), - unwrapKey: new Map(), + keyManagementEncrypt: new Map(), + keyManagementDecrypt: new Map(), encrypt: new Map(), decrypt: new Map() } @@ -24,17 +25,29 @@ require('./aes_gcm')(JWA) require('./rsaes')(JWA) require('./aes_gcm_kw')(JWA) require('./aes_kw')(JWA) + +// deriveKey require('./pbes2')(JWA) require('./ecdh/kw')(JWA) require('./ecdh/dir')(JWA) const check = (key, op, alg) => { + let label + let keyOp + if (op === 'keyManagementEncrypt') { + label = 'key management (encryption)' + keyOp = KEY_MANAGEMENT_ENCRYPT + } else if (op === 'keyManagementDecrypt') { + label = 'key management (decryption)' + keyOp = KEY_MANAGEMENT_DECRYPT + } + if (JWA[op].has(alg)) { - if (!key.algorithms(op).has(alg)) { - throw new JWKKeySupport(`the key does not support ${alg} ${op} algorithm`) + if (!key.algorithms(keyOp).has(alg)) { + throw new JWKKeySupport(`the key does not support ${alg} ${label || op} algorithm`) } } else { - throw new JOSENotSupported(`unsupported ${op} alg: ${alg}`) + throw new JOSENotSupported(`unsupported ${label || op} alg: ${alg}`) } } @@ -48,13 +61,13 @@ module.exports = { check(key, 'verify', alg) return JWA.verify.get(alg)(key, payload, signature) }, - wrapKey: (alg, key, payload, opts) => { - check(key, 'wrapKey', alg) - return JWA.wrapKey.get(alg)(key, payload, opts) + keyManagementEncrypt: (alg, key, payload, opts) => { + check(key, 'keyManagementEncrypt', alg) + return JWA.keyManagementEncrypt.get(alg)(key, payload, opts) }, - unwrapKey: (alg, key, payload, opts) => { - check(key, 'unwrapKey', alg) - return JWA.unwrapKey.get(alg)(key, payload, opts) + keyManagementDecrypt: (alg, key, payload, opts) => { + check(key, 'keyManagementDecrypt', alg) + return JWA.keyManagementDecrypt.get(alg)(key, payload, opts) }, encrypt: (alg, key, cleartext, opts) => { check(key, 'encrypt', alg) diff --git a/lib/jwa/pbes2.js b/lib/jwa/pbes2.js index e6da914f02..f716936b89 100644 --- a/lib/jwa/pbes2.js +++ b/lib/jwa/pbes2.js @@ -1,7 +1,7 @@ const { strict: assert } = require('assert') const { pbkdf2Sync: pbkdf2, randomBytes } = require('crypto') -const { KEYOBJECT } = require('../help/symbols') +const { KEYOBJECT } = require('../help/consts') const base64url = require('../help/base64url') const SALT_LENGTH = 16 @@ -40,16 +40,16 @@ const unwrapKey = (keylen, sha, concat, unwrap, { [KEYOBJECT]: keyObject }, payl module.exports = (JWA) => { ['PBES2-HS256+A128KW', 'PBES2-HS384+A192KW', 'PBES2-HS512+A256KW'].forEach((jwaAlg) => { - assert(!JWA.wrapKey.has(jwaAlg), `wrapKey alg ${jwaAlg} already registered`) - assert(!JWA.unwrapKey.has(jwaAlg), `unwrapKey alg ${jwaAlg} already registered`) + assert(!JWA.keyManagementEncrypt.has(jwaAlg), `keyManagementEncrypt alg ${jwaAlg} already registered`) + assert(!JWA.keyManagementDecrypt.has(jwaAlg), `keyManagementDecrypt alg ${jwaAlg} already registered`) const kw = jwaAlg.substr(-6) - const kwWrap = JWA.wrapKey.get(kw) - const kwUnwrap = JWA.unwrapKey.get(kw) + const kwWrap = JWA.keyManagementEncrypt.get(kw) + const kwUnwrap = JWA.keyManagementDecrypt.get(kw) const keylen = parseInt(jwaAlg.substr(13, 3), 10) / 8 const sha = `sha${jwaAlg.substr(8, 3)}` - JWA.wrapKey.set(jwaAlg, wrapKey.bind(undefined, keylen, sha, concatSalt.bind(undefined, jwaAlg), kwWrap)) - JWA.unwrapKey.set(jwaAlg, unwrapKey.bind(undefined, keylen, sha, concatSalt.bind(undefined, jwaAlg), kwUnwrap)) + JWA.keyManagementEncrypt.set(jwaAlg, wrapKey.bind(undefined, keylen, sha, concatSalt.bind(undefined, jwaAlg), kwWrap)) + JWA.keyManagementDecrypt.set(jwaAlg, unwrapKey.bind(undefined, keylen, sha, concatSalt.bind(undefined, jwaAlg), kwUnwrap)) }) } diff --git a/lib/jwa/rsaes.js b/lib/jwa/rsaes.js index f60d9cd0c2..328a0cf56c 100644 --- a/lib/jwa/rsaes.js +++ b/lib/jwa/rsaes.js @@ -1,7 +1,7 @@ const { strict: assert } = require('assert') const { publicEncrypt, privateDecrypt, constants } = require('crypto') -const { KEYOBJECT } = require('../help/symbols') +const { KEYOBJECT } = require('../help/consts') const resolvePadding = (alg) => { switch (alg) { @@ -24,10 +24,10 @@ module.exports = (JWA) => { ['RSA1_5', 'RSA-OAEP'].forEach((jwaAlg) => { const padding = resolvePadding(jwaAlg) - assert(!JWA.wrapKey.has(jwaAlg), `wrapKey alg ${jwaAlg} already registered`) - assert(!JWA.unwrapKey.has(jwaAlg), `unwrapKey alg ${jwaAlg} already registered`) + assert(!JWA.keyManagementEncrypt.has(jwaAlg), `keyManagementEncrypt alg ${jwaAlg} already registered`) + assert(!JWA.keyManagementDecrypt.has(jwaAlg), `keyManagementDecrypt alg ${jwaAlg} already registered`) - JWA.wrapKey.set(jwaAlg, wrapKey.bind(undefined, padding)) - JWA.unwrapKey.set(jwaAlg, unwrapKey.bind(undefined, padding)) + JWA.keyManagementEncrypt.set(jwaAlg, wrapKey.bind(undefined, padding)) + JWA.keyManagementDecrypt.set(jwaAlg, unwrapKey.bind(undefined, padding)) }) } diff --git a/lib/jwa/rsassa.js b/lib/jwa/rsassa.js index d638cdd7fb..9f15ce6783 100644 --- a/lib/jwa/rsassa.js +++ b/lib/jwa/rsassa.js @@ -1,7 +1,7 @@ const { strict: assert } = require('assert') const { sign: signOneShot, verify: verifyOneShot } = require('crypto') -const { KEYOBJECT } = require('../help/symbols') +const { KEYOBJECT } = require('../help/consts') const resolveNodeAlg = require('../help/node_alg') const sign = (nodeAlg, { [KEYOBJECT]: keyObject }, payload) => { diff --git a/lib/jwa/rsassa_pss.js b/lib/jwa/rsassa_pss.js index f1f54ffe34..c62d6bd45d 100644 --- a/lib/jwa/rsassa_pss.js +++ b/lib/jwa/rsassa_pss.js @@ -1,7 +1,7 @@ const { strict: assert } = require('assert') const { sign: signOneShot, verify: verifyOneShot, constants } = require('crypto') -const { KEYOBJECT } = require('../help/symbols') +const { KEYOBJECT } = require('../help/consts') const resolveNodeAlg = require('../help/node_alg') const sign = (nodeAlg, { [KEYOBJECT]: keyObject }, payload) => { diff --git a/lib/jwe/decrypt.js b/lib/jwe/decrypt.js index b465257b7b..26439e2843 100644 --- a/lib/jwe/decrypt.js +++ b/lib/jwe/decrypt.js @@ -1,11 +1,12 @@ const { createSecretKey } = require('crypto') const { inflateRawSync } = require('zlib') +const { KEY_MANAGEMENT_DECRYPT } = require('../help/consts') const base64url = require('../help/base64url') const KeyStore = require('../jwks/keystore') const Key = require('../jwk/key/base') const errors = require('../errors') -const { check, decrypt, unwrapKey } = require('../jwa') +const { check, decrypt, keyManagementDecrypt } = require('../jwa') const JWK = require('../jwk') const generateCEK = require('./generate_cek') @@ -94,9 +95,9 @@ const jweDecrypt = (skipValidateHeaders, serialization, jwe, key, { crit = [], c const keystore = key let keys if (opts.alg === 'dir') { - keys = keystore.all({ kid: opts.kid, alg: opts.enc, operation: 'decrypt' }) + keys = keystore.all({ kid: opts.kid, alg: opts.enc, key_ops: ['decrypt'] }) } else { - keys = keystore.all({ kid: opts.kid, alg: opts.alg, operation: 'unwrapKey' }) + keys = keystore.all({ kid: opts.kid, alg: opts.alg, key_ops: ['unwrapKey'] }) } switch (keys.length) { case 0: @@ -126,16 +127,16 @@ const jweDecrypt = (skipValidateHeaders, serialization, jwe, key, { crit = [], c } } - check(key, ...(alg === 'dir' ? ['decrypt', enc] : ['unwrapKey', alg])) + check(key, ...(alg === 'dir' ? ['decrypt', enc] : ['keyManagementDecrypt', alg])) try { if (alg === 'dir') { cek = JWK.importKey(key, { alg: enc, use: 'enc' }) } else if (alg === 'ECDH-ES') { - const unwrapped = unwrapKey(alg, key, undefined, opts) + const unwrapped = keyManagementDecrypt(alg, key, undefined, opts) cek = JWK.importKey(createSecretKey(unwrapped), { alg: enc, use: 'enc' }) } else { - const unwrapped = unwrapKey(alg, key, base64url.decodeToBuffer(encryptedKey), opts) + const unwrapped = keyManagementDecrypt(alg, key, base64url.decodeToBuffer(encryptedKey), opts) cek = JWK.importKey(createSecretKey(unwrapped), { alg: enc, use: 'enc' }) } } catch (err) { diff --git a/lib/jwe/encrypt.js b/lib/jwe/encrypt.js index 3f0607c290..0c9dec7278 100644 --- a/lib/jwe/encrypt.js +++ b/lib/jwe/encrypt.js @@ -1,7 +1,7 @@ const { createSecretKey } = require('crypto') const { deflateRawSync } = require('zlib') -const { KEYOBJECT } = require('../help/symbols') +const { KEYOBJECT } = require('../help/consts') const generateIV = require('../help/generate_iv') const base64url = require('../help/base64url') const isObject = require('../help/is_object') @@ -9,7 +9,7 @@ const deepClone = require('../help/deep_clone') const Key = require('../jwk/key/base') const importKey = require('../jwk/import') const { JWEInvalid } = require('../errors') -const { check, wrapKey, encrypt } = require('../jwa') +const { check, keyManagementEncrypt, encrypt } = require('../jwa') const serializers = require('./serializers') const generateCEK = require('./generate_cek') @@ -92,12 +92,13 @@ class Encrypt { if (alg === 'dir') { check(key, 'encrypt', enc) } else if (alg) { - check(key, 'wrapKey', alg) + check(key, 'keyManagementEncrypt', alg) } else { alg = [...key.algorithms('wrapKey')][0] + alg = alg || [...key.algorithms('deriveKey')][0] if (alg === 'ECDH-ES' && recipientCount !== 1) { - alg = [...key.algorithms('wrapKey')][1] + alg = [...key.algorithms('deriveKey')][1] } if (!alg) { @@ -125,7 +126,7 @@ class Encrypt { if (key.kty === 'oct' && alg === 'dir') { this.#cek = importKey(key[KEYOBJECT], { use: 'enc', alg: enc }) } else { - ({ wrapped, header: generatedHeader } = wrapKey(alg, key, this.#cek[KEYOBJECT].export(), { enc, alg })) + ({ wrapped, header: generatedHeader } = keyManagementEncrypt(alg, key, this.#cek[KEYOBJECT].export(), { enc, alg })) if (alg === 'ECDH-ES') { this.#cek = importKey(createSecretKey(wrapped), { use: 'enc', alg: enc }) } diff --git a/lib/jwe/generate_cek.js b/lib/jwe/generate_cek.js index 92ddf77a50..49aa304bbe 100644 --- a/lib/jwe/generate_cek.js +++ b/lib/jwe/generate_cek.js @@ -1,6 +1,6 @@ const { randomBytes, createSecretKey } = require('crypto') -const KEYLENGTHS = require('../help/key_lengths') +const { KEYLENGTHS } = require('../help/consts') const importKey = require('../jwk/import') module.exports = alg => importKey(createSecretKey(randomBytes(KEYLENGTHS[alg] / 8)), { use: 'enc', alg }) diff --git a/lib/jwk/import.js b/lib/jwk/import.js index 8423877bf2..bcba412a9e 100644 --- a/lib/jwk/import.js +++ b/lib/jwk/import.js @@ -13,7 +13,7 @@ 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 }, target) + return Object.assign({}, { alg: source.alg, use: source.use, kid: source.kid, key_ops: source.key_ops }, target) } const importKey = (key, parameters) => { diff --git a/lib/jwk/key/base.js b/lib/jwk/key/base.js index 785d61cf1f..b8e00e8bf8 100644 --- a/lib/jwk/key/base.js +++ b/lib/jwk/key/base.js @@ -1,15 +1,17 @@ const { createPublicKey } = require('crypto') +const { inspect } = require('util') const { keyObjectToJWK } = require('../../help/key_utils') -const { THUMBPRINT_MATERIAL, PUBLIC_MEMBERS, PRIVATE_MEMBERS, JWK_MEMBERS } = require('../../help/symbols') -const { KEYOBJECT } = require('../../help/symbols') +const { + THUMBPRINT_MATERIAL, PUBLIC_MEMBERS, PRIVATE_MEMBERS, JWK_MEMBERS, KEYOBJECT, + USES_MAPPING, OPS, USES +} = require('../../help/consts') const isObject = require('../../help/is_object') const thumbprint = require('../thumbprint') - -const USES = new Set(['sig', 'enc']) +const errors = require('../../errors') class Key { - constructor (keyObject, { alg, use, kid } = {}) { + constructor (keyObject, { alg, use, kid, key_ops: ops } = {}) { if (use !== undefined) { if (typeof use !== 'string' || !USES.has(use)) { throw new TypeError('`use` must be either "sig" or "enc" string when provided') @@ -28,6 +30,22 @@ class Key { } } + if (ops !== undefined) { + if (!Array.isArray(ops) || !ops.length || ops.some(x => typeof x !== '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)) + } + + if (ops && use) { + if ( + (use === 'enc' && ops.some(x => USES_MAPPING.sig.has(x))) || + (use === 'sig' && ops.some(x => USES_MAPPING.enc.has(x))) + ) { + throw new errors.JWKInvalid('inconsistent JWK "use" and "key_ops"') + } + } + Object.defineProperties(this, { [KEYOBJECT]: { value: isObject(keyObject) ? undefined : keyObject }, type: { value: keyObject.type }, @@ -36,6 +54,10 @@ class Key { secret: { value: keyObject.type === 'secret' }, alg: { value: alg, enumerable: alg !== undefined }, use: { value: use, enumerable: use !== undefined }, + key_ops: { + enumerable: ops !== undefined, + ...(ops ? { get () { return [...ops] } } : { value: undefined }) + }, kid: { enumerable: true, ...(kid ? { value: kid } : { @@ -101,6 +123,10 @@ class Key { result.alg = this.alg } + if (this.key_ops && this.key_ops.length) { + result.key_ops = this.key_ops + } + if (this.use) { result.use = this.use } @@ -133,6 +159,16 @@ class Key { }, {})) } + /* istanbul ignore next */ + [inspect.custom] () { + return `${this.constructor.name} ${inspect(this.toJWK(false), { + depth: Infinity, + colors: process.stdout.isTTY, + compact: false, + sorted: true + })}` + } + /* istanbul ignore next */ [THUMBPRINT_MATERIAL] () { throw new Error(`"[THUMBPRINT_MATERIAL]()" is not implemented on ${this.constructor.name}`) diff --git a/lib/jwk/key/ec.js b/lib/jwk/key/ec.js index 60bcd5ec96..dfaf498156 100644 --- a/lib/jwk/key/ec.js +++ b/lib/jwk/key/ec.js @@ -1,16 +1,17 @@ const { generateKeyPairSync, generateKeyPair: async } = require('crypto') const { promisify } = require('util') -const { THUMBPRINT_MATERIAL, PUBLIC_MEMBERS, PRIVATE_MEMBERS, JWK_MEMBERS } = require('../../help/symbols') +const { + THUMBPRINT_MATERIAL, JWK_MEMBERS, PUBLIC_MEMBERS, EC_CURVES, + PRIVATE_MEMBERS, KEY_MANAGEMENT_DECRYPT, KEY_MANAGEMENT_ENCRYPT, ECDH_ALGS +} = require('../../help/consts') + const errors = require('../../errors') -const EC_CURVES = new Set(['P-256', 'P-384', 'P-521']) const Key = require('./base') const generateKeyPair = promisify(async) -const WRAP_ALGS = ['ECDH-ES', 'ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW'] - const EC_PUBLIC = new Set(['crv', 'x', 'y']) Object.freeze(EC_PUBLIC) const EC_PRIVATE = new Set([...EC_PUBLIC, 'd']) @@ -53,12 +54,27 @@ class ECKey extends Key { return { crv: this.crv, kty: 'EC', x: this.x, y: this.y } } - algorithms (operation, { use = this.use, alg = this.alg } = {}) { + algorithms (operation, /* second argument is private API */ { use = this.use, alg = this.alg, key_ops: ops = this.key_ops } = {}) { if (alg) { - return new Set(this.algorithms(operation, { alg: null, use }).has(alg) ? [alg] : undefined) + return new Set(this.algorithms(operation, { alg: null }).has(alg) ? [alg] : undefined) + } + + if (operation === KEY_MANAGEMENT_ENCRYPT) { + operation = 'deriveKey' + } else if (operation === KEY_MANAGEMENT_DECRYPT) { + if (this.public) { + return new Set() + } + operation = 'deriveKey' + } + + if (operation && ops && !ops.includes(operation)) { + return new Set() } switch (operation) { + case 'wrapKey': + case 'unwrapKey': case 'encrypt': case 'decrypt': return new Set() @@ -74,23 +90,18 @@ class ECKey extends Key { } return new Set([crvToDSA(this.crv)]) - case 'wrapKey': + case 'deriveKey': if (use === 'sig') { return new Set() } - return new Set(WRAP_ALGS) - case 'unwrapKey': - if (this.public || use === 'sig') { - return new Set() - } - - return new Set(WRAP_ALGS) + return new Set(ECDH_ALGS) case undefined: // just the ops needed to return all algs regardless of its use return new Set([ + ...this.algorithms('sign'), ...this.algorithms('verify'), - ...this.algorithms('wrapKey') + ...this.algorithms('deriveKey') ]) default: throw new TypeError('invalid key operation') diff --git a/lib/jwk/key/oct.js b/lib/jwk/key/oct.js index 9c1bc22e1f..0e5d21e163 100644 --- a/lib/jwk/key/oct.js +++ b/lib/jwk/key/oct.js @@ -1,15 +1,16 @@ const { randomBytes, createSecretKey } = require('crypto') const base64url = require('../../help/base64url') -const { KEYOBJECT } = require('../../help/symbols') -const { THUMBPRINT_MATERIAL, PUBLIC_MEMBERS, PRIVATE_MEMBERS } = require('../../help/symbols') +const { KEYOBJECT } = require('../../help/consts') +const { + THUMBPRINT_MATERIAL, PUBLIC_MEMBERS, PRIVATE_MEMBERS, + KEY_MANAGEMENT_DECRYPT, KEY_MANAGEMENT_ENCRYPT +} = require('../../help/consts') const Key = require('./base') const ENC_ALGS = new Set(['A128CBC-HS256', 'A128GCM', 'A192CBC-HS384', 'A192GCM', 'A256CBC-HS512', 'A256GCM']) const ENC_LEN = new Set([128, 192, 256, 384, 512]) -const PBES2 = ['PBES2-HS256+A128KW', 'PBES2-HS384+A192KW', 'PBES2-HS512+A256KW'] -const SIG_ALGS = ['HS256', 'HS384', 'HS512'] const WRAP_LEN = new Set([128, 192, 256]) const OCT_PUBLIC = new Set() @@ -66,16 +67,33 @@ class OctKey extends Key { return { k: this.k, kty: 'oct' } } - algorithms (operation, { use = this.use, alg = this.alg } = {}) { + algorithms (operation, /* second argument is private API */ { use = this.use, alg = this.alg, key_ops: ops = this.key_ops } = {}) { if (!this[KEYOBJECT]) { return new Set() } + if (operation === KEY_MANAGEMENT_ENCRYPT || operation === KEY_MANAGEMENT_DECRYPT) { + return new Set([ + ...this.algorithms('wrapKey'), + ...this.algorithms('deriveKey') + ]) + } + + if (operation && ops && !ops.includes(operation)) { + return new Set() + } + if (alg) { - return new Set(this.algorithms(operation, { alg: null, use }).has(alg) ? [alg] : undefined) + return new Set(this.algorithms(operation, { alg: null }).has(alg) ? [alg] : undefined) } switch (operation) { + case 'deriveKey': + if (use === 'sig') { + return new Set() + } + + return new Set(['PBES2-HS256+A128KW', 'PBES2-HS384+A192KW', 'PBES2-HS512+A256KW']) case 'encrypt': case 'decrypt': if (this.use === 'sig' || !ENC_LEN.has(this.length)) { @@ -89,29 +107,24 @@ class OctKey extends Key { return new Set() } - return new Set(SIG_ALGS) + return new Set(['HS256', 'HS384', 'HS512']) case 'wrapKey': case 'unwrapKey': - if (use === 'sig') { + if (use === 'sig' || !WRAP_LEN.has(this.length)) { return new Set() } - const algs = new Set() - - if (WRAP_LEN.has(this.length)) { - algs.add(`A${this.length}KW`) - algs.add(`A${this.length}GCMKW`) - } - - PBES2.forEach(Set.prototype.add.bind(algs)) - - return algs + return new Set([`A${this.length}KW`, `A${this.length}GCMKW`]) case undefined: return new Set([ // just the ops needed to return all algs regardless of its use - symmetric keys ...this.algorithms('encrypt'), + ...this.algorithms('decrypt'), ...this.algorithms('sign'), - ...this.algorithms('wrapKey') + ...this.algorithms('verify'), + ...this.algorithms('wrapKey'), + ...this.algorithms('unwrapKey'), + ...this.algorithms('deriveKey') ]) default: throw new TypeError('invalid key operation') diff --git a/lib/jwk/key/okp.js b/lib/jwk/key/okp.js index 71efa75765..54498f1afd 100644 --- a/lib/jwk/key/okp.js +++ b/lib/jwk/key/okp.js @@ -2,17 +2,15 @@ const { generateKeyPairSync, generateKeyPair: async } = require('crypto') const { promisify } = require('util') const { - THUMBPRINT_MATERIAL, JWK_MEMBERS, PUBLIC_MEMBERS, PRIVATE_MEMBERS -} = require('../../help/symbols') + THUMBPRINT_MATERIAL, JWK_MEMBERS, PUBLIC_MEMBERS, + PRIVATE_MEMBERS, KEY_MANAGEMENT_DECRYPT, KEY_MANAGEMENT_ENCRYPT, OKP_CURVES +} = require('../../help/consts') const errors = require('../../errors') const Key = require('./base') -const OKP_CURVES = new Set(['Ed25519', 'Ed448', 'X25519', 'X448']) const generateKeyPair = promisify(async) -// const WRAP_ALGS = ['ECDH-ES', 'ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW'] - const OKP_PUBLIC = new Set(['crv', 'x']) Object.freeze(OKP_PUBLIC) const OKP_PRIVATE = new Set([...OKP_PUBLIC, 'd']) @@ -44,12 +42,27 @@ class OKPKey extends Key { return { crv: this.crv, kty: 'OKP', x: this.x } } - algorithms (operation, { use = this.use, alg = this.alg } = {}) { + algorithms (operation, /* second argument is private API */ { use = this.use, alg = this.alg, key_ops: ops = this.key_ops } = {}) { if (alg) { - return new Set(this.algorithms(operation, { alg: null, use }).has(alg) ? [alg] : undefined) + return new Set(this.algorithms(operation, { alg: null }).has(alg) ? [alg] : undefined) + } + + if (operation === KEY_MANAGEMENT_ENCRYPT) { + operation = 'deriveKey' + } else if (operation === KEY_MANAGEMENT_DECRYPT) { + if (this.public) { + return new Set() + } + operation = 'deriveKey' + } + + if (operation && ops && !ops.includes(operation)) { + return new Set() } switch (operation) { + case 'wrapKey': + case 'unwrapKey': case 'encrypt': case 'decrypt': return new Set() @@ -65,24 +78,18 @@ class OKPKey extends Key { } return new Set(['EdDSA']) - case 'wrapKey': + case 'deriveKey': if (use === 'sig' || this.crv.startsWith('Ed')) { return new Set() } - // return new Set(WRAP_ALGS) - return new Set() - case 'unwrapKey': - if (this.public || use === 'sig' || this.crv.startsWith('Ed')) { - return new Set() - } - - // return new Set(WRAP_ALGS) + // return new Set(ECDH_ALGS) return new Set() case undefined: return new Set([ + ...this.algorithms('sign'), ...this.algorithms('verify'), - ...this.algorithms('wrapKey') + ...this.algorithms('deriveKey') ]) default: throw new TypeError('invalid key operation') diff --git a/lib/jwk/key/rsa.js b/lib/jwk/key/rsa.js index 06ae7f727b..5134aac0b0 100644 --- a/lib/jwk/key/rsa.js +++ b/lib/jwk/key/rsa.js @@ -1,7 +1,10 @@ const { generateKeyPairSync, generateKeyPair: async } = require('crypto') const { promisify } = require('util') -const { THUMBPRINT_MATERIAL, PUBLIC_MEMBERS, PRIVATE_MEMBERS, JWK_MEMBERS } = require('../../help/symbols') +const { + THUMBPRINT_MATERIAL, JWK_MEMBERS, PUBLIC_MEMBERS, + PRIVATE_MEMBERS, KEY_MANAGEMENT_DECRYPT, KEY_MANAGEMENT_ENCRYPT +} = require('../../help/consts') const Key = require('./base') @@ -76,12 +79,23 @@ class RSAKey extends Key { return { e: this.e, kty: 'RSA', n: this.n } } - algorithms (operation, { use = this.use, alg = this.alg } = {}) { + algorithms (operation, /* second argument is private API */ { use = this.use, alg = this.alg, key_ops: ops = this.key_ops } = {}) { if (alg) { - return new Set(this.algorithms(operation, { alg: null, use }).has(alg) ? [alg] : undefined) + return new Set(this.algorithms(operation, { alg: null }).has(alg) ? [alg] : undefined) + } + + if (operation === KEY_MANAGEMENT_ENCRYPT) { + operation = 'wrapKey' + } else if (operation === KEY_MANAGEMENT_DECRYPT) { + operation = 'unwrapKey' + } + + if (operation && ops && !ops.includes(operation)) { + return new Set() } switch (operation) { + case 'deriveKey': case 'encrypt': case 'decrypt': return new Set() @@ -112,8 +126,10 @@ class RSAKey extends Key { case undefined: // just the ops needed to return all algs regardless of its use return new Set([ + ...this.algorithms('sign'), ...this.algorithms('verify'), - ...this.algorithms('wrapKey') + ...this.algorithms('wrapKey'), + ...this.algorithms('unwrapKey') ]) default: throw new TypeError('invalid key operation') diff --git a/lib/jwks/keystore.js b/lib/jwks/keystore.js index 46e66aa99e..147e11ca33 100644 --- a/lib/jwks/keystore.js +++ b/lib/jwks/keystore.js @@ -1,9 +1,12 @@ +const { inspect } = require('util') + const isObject = require('../help/is_object') const { generate, generateSync } = require('../jwk/generate') const Key = require('../jwk/key/base') const importKey = require('../jwk/import') +const { USES_MAPPING } = require('../help/consts') -const keyscore = (key, { alg, kid, use }) => { +const keyscore = (key, { alg, kid, use, ops }) => { let score = 0 if (alg && key.alg) { @@ -18,6 +21,10 @@ const keyscore = (key, { alg, kid, use }) => { score++ } + if (ops && key.key_ops) { + score++ + } + return score } @@ -45,12 +52,16 @@ class KeyStore { return new KeyStore(...keys) } - all ({ alg, kid, use, kty, operation } = {}) { + all ({ alg, kid, use, kty, key_ops: ops } = {}) { + 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') + } + return [...this.#keys] .filter((key) => { let candidate = true - if (alg !== undefined && !key.algorithms(operation).has(alg)) { + if (alg !== undefined && !key.algorithms().has(alg)) { candidate = false } @@ -66,9 +77,21 @@ class KeyStore { candidate = false } + if (candidate && ops !== undefined && (key.key_ops !== undefined || key.use !== undefined)) { + let keyOps + if (key.key_ops) { + keyOps = new Set(key.key_ops) + } else { + keyOps = USES_MAPPING[key.use] + } + if (ops.some(x => !keyOps.has(x))) { + candidate = false + } + } + return candidate }) - .sort((first, second) => keyscore(second, { alg, kid, use }) - keyscore(first, { alg, kid, use })) + .sort((first, second) => keyscore(second, { alg, kid, use, ops }) - keyscore(first, { alg, kid, use, ops })) } get (...args) { @@ -106,6 +129,16 @@ class KeyStore { get size () { return this.#keys.size } + + /* istanbul ignore next */ + [inspect.custom] () { + return `${this.constructor.name} ${inspect(this.toJWKS(false), { + depth: Infinity, + colors: process.stdout.isTTY, + compact: false, + sorted: true + })}` + } } module.exports = KeyStore diff --git a/lib/jws/verify.js b/lib/jws/verify.js index 7cc401ae40..16910f8a52 100644 --- a/lib/jws/verify.js +++ b/lib/jws/verify.js @@ -87,7 +87,7 @@ const jwsVerify = (skipDisjointCheck, serialization, jws, key, { crit = [], comp if (key instanceof KeyStore) { const keystore = key - const keys = keystore.all({ kid: combinedHeader.kid, alg: combinedHeader.alg, operation: 'verify' }) + const keys = keystore.all({ kid: combinedHeader.kid, alg: combinedHeader.alg, key_ops: ['verify'] }) switch (keys.length) { case 0: throw new errors.JWKSNoMatchingKey() diff --git a/test/jwa/sanity.test.js b/test/jwa/sanity.test.js index 5fed31972c..cd6cef7829 100644 --- a/test/jwa/sanity.test.js +++ b/test/jwa/sanity.test.js @@ -3,10 +3,14 @@ const test = require('ava') const { errors } = require('../..') const JWA = require('../../lib/jwa') -;['sign', 'verify', 'wrapKey', 'unwrapKey', 'encrypt', 'decrypt'].forEach((op) => { +;['sign', 'verify', 'keyManagementEncrypt', 'keyManagementDecrypt', 'encrypt', 'decrypt'].forEach((op) => { + let label + if (op.startsWith('keyManagement')) { + label = `key management (${op.substr(13).toLowerCase()}ion)` + } test(`JWA.${op} will not accept an "unimplemented" algorithm`, t => { t.throws(() => { JWA[op]('foo') - }, { instanceOf: errors.JOSENotSupported, code: 'ERR_JOSE_NOT_SUPPORTED', message: `unsupported ${op} alg: foo` }) + }, { instanceOf: errors.JOSENotSupported, code: 'ERR_JOSE_NOT_SUPPORTED', message: `unsupported ${label || op} alg: foo` }) }) }) diff --git a/test/jwe/smoke.test.js b/test/jwe/smoke.test.js index 1bdd78cd68..a460de7e28 100644 --- a/test/jwe/smoke.test.js +++ b/test/jwe/smoke.test.js @@ -101,7 +101,7 @@ Object.entries(fixtures.PEM).forEach(([type, { private: key, public: pub }]) => const eKey = importKey(pub) const dKey = importKey(key) - eKey.algorithms('wrapKey').forEach((alg) => { + ;[...eKey.algorithms('wrapKey'), ...eKey.algorithms('deriveKey')].forEach((alg) => { 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) @@ -112,7 +112,7 @@ Object.entries(fixtures.PEM).forEach(([type, { private: key, public: pub }]) => ;[16, 24, 32, 48, 64].forEach((len) => { const sym = importKey(randomBytes(len)) - sym.algorithms('wrapKey').forEach((alg) => { + ;[...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) diff --git a/test/jwk/ec.test.js b/test/jwk/ec.test.js index ade0635ba7..2d4575986d 100644 --- a/test/jwk/ec.test.js +++ b/test/jwk/ec.test.js @@ -129,6 +129,12 @@ Object.entries({ test(`${crv} EC Private key .algorithms("wrapKey")`, t => { const result = key.algorithms('wrapKey') t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + + test(`${crv} EC Private key .algorithms("deriveKey")`, t => { + const result = key.algorithms('deriveKey') + t.is(result.constructor, Set) t.deepEqual([...result], ['ECDH-ES', 'ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW']) }) @@ -142,12 +148,12 @@ Object.entries({ test(`${crv} EC Private key .algorithms("unwrapKey")`, t => { const result = key.algorithms('unwrapKey') t.is(result.constructor, Set) - t.deepEqual([...result], ['ECDH-ES', 'ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW']) + t.deepEqual([...result], []) }) - test(`${crv} EC Private key .algorithms("unwrapKey") when use is sig`, t => { + test(`${crv} EC Private key .algorithms("deriveKey") when use is sig`, t => { const sigKey = new ECKey(keyObject, { use: 'sig' }) - const result = sigKey.algorithms('unwrapKey') + const result = sigKey.algorithms('deriveKey') t.is(result.constructor, Set) t.deepEqual([...result], []) }) @@ -254,6 +260,12 @@ Object.entries({ test(`${crv} EC Public key .algorithms("wrapKey")`, t => { const result = key.algorithms('wrapKey') t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + + test(`${crv} EC Public key .algorithms("deriveKey")`, t => { + const result = key.algorithms('deriveKey') + t.is(result.constructor, Set) t.deepEqual([...result], ['ECDH-ES', 'ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW']) }) diff --git a/test/jwk/key_ops.test.js b/test/jwk/key_ops.test.js new file mode 100644 index 0000000000..94be1ca5aa --- /dev/null +++ b/test/jwk/key_ops.test.js @@ -0,0 +1,105 @@ +const test = require('ava') + +const crypto = require('crypto') +const errors = require('../../lib/errors') +const importKey = require('../../lib/jwk/import') +const { generateSync } = require('../../lib/jwk/generate') + +const jwk = importKey('foo').toJWK(true) + +test('key_ops ignores unrecognized values', t => { + importKey({ ...jwk, key_ops: ['sign', 'verify', 'foo'] }) + t.pass() +}) + +test('key_ops ignores duplicate values', t => { + const k = importKey({ ...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' }) + t.pass() +}) + +test('key_ops are part of toJWK', t => { + const k = importKey({ ...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' }) + }, { 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: [] }) + }, { 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] }) + }, { 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 => { + t.throws(() => { + importKey({ ...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 => { + const { publicKey } = crypto.generateKeyPairSync('ed25519') + + t.throws(() => { + importKey(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 => { + const { publicKey } = crypto.generateKeyPairSync('ed25519') + + t.throws(() => { + importKey(publicKey, { use: 'enc', key_ops: ['sign'] }) + }, { instanceOf: errors.JWKInvalid, code: 'ERR_JWK_INVALID' }) +}) + +test('PEM importKey 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'] }) + }, { instanceOf: errors.JWKInvalid, code: 'ERR_JWK_INVALID' }) +}) + +test('RSA key key_ops', t => { + const k = generateSync('RSA', 2048, { key_ops: ['sign'] }) + t.deepEqual([...k.algorithms()], ['PS256', 'RS256', 'PS384', 'RS384', 'PS512', 'RS512']) + t.deepEqual([...k.algorithms('sign')], ['PS256', 'RS256', 'PS384', 'RS384', 'PS512', 'RS512']) + t.deepEqual([...k.algorithms('verify')], []) +}) + +test('EC key key_ops', t => { + const k = generateSync('EC', 'P-256', { key_ops: ['verify'] }) + t.deepEqual([...k.algorithms()], ['ES256']) + t.deepEqual([...k.algorithms('verify')], ['ES256']) + t.deepEqual([...k.algorithms('sign')], []) +}) + +test('oct key key_ops', t => { + const k = generateSync('oct', 256, { key_ops: ['verify'] }) + t.deepEqual([...k.algorithms()], ['HS256', 'HS384', 'HS512']) + t.deepEqual([...k.algorithms('verify')], ['HS256', 'HS384', 'HS512']) + t.deepEqual([...k.algorithms('sign')], []) +}) + +test('OKP key key_ops', t => { + const k = generateSync('OKP', 'Ed25519', { key_ops: ['verify'] }) + t.deepEqual([...k.algorithms()], ['EdDSA']) + t.deepEqual([...k.algorithms('verify')], ['EdDSA']) + t.deepEqual([...k.algorithms('sign')], []) +}) diff --git a/test/jwk/oct.test.js b/test/jwk/oct.test.js index 6b1695f38b..48c328d701 100644 --- a/test/jwk/oct.test.js +++ b/test/jwk/oct.test.js @@ -83,13 +83,20 @@ test('no verify support when `use` is "enc"', t => { t.deepEqual([...result], []) }) -test(`oct keys (odd bits) wrap/unwrap algorithms only have "PBES2"`, t => { +test(`oct keys (odd bits) deriveKey algorithms only have "PBES2"`, t => { const key = generateSync('oct', 136) - const result = key.algorithms('wrapKey') + const result = key.algorithms('deriveKey') t.is(result.constructor, Set) t.deepEqual([...result], ['PBES2-HS256+A128KW', 'PBES2-HS384+A192KW', 'PBES2-HS512+A256KW']) }) +test(`oct keys (odd bits) wrap/unwrap algorithms cant wrap`, t => { + const key = generateSync('oct', 136) + const result = key.algorithms('wrapKey') + t.is(result.constructor, Set) + t.deepEqual([...result], []) +}) + ;[128, 192, 256].forEach((len) => { test(`oct keys (${len} bits) wrap/unwrap algorithms have "KW / GCMKW"`, t => { const key = generateSync('oct', len) diff --git a/test/jwk/rsa.test.js b/test/jwk/rsa.test.js index 003386b184..8fb32c5f2d 100644 --- a/test/jwk/rsa.test.js +++ b/test/jwk/rsa.test.js @@ -104,6 +104,12 @@ test(`RSA key .algorithms invalid operation`, t => { t.deepEqual([...result], []) }) + test('RSA Private key .algorithms("deriveKey")', t => { + const result = key.algorithms('deriveKey') + t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + test('RSA Private key .algorithms("decrypt")', t => { const result = key.algorithms('decrypt') t.is(result.constructor, Set) @@ -230,6 +236,12 @@ test(`RSA key .algorithms invalid operation`, t => { t.deepEqual([...result], []) }) + test('RSA Public key .algorithms("deriveKey")', t => { + const result = key.algorithms('deriveKey') + t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + test('RSA Public key .algorithms("decrypt")', t => { const result = key.algorithms('decrypt') t.is(result.constructor, Set) diff --git a/test/jwks/keystore.test.js b/test/jwks/keystore.test.js index c83941df14..7aba9bdeb3 100644 --- a/test/jwks/keystore.test.js +++ b/test/jwks/keystore.test.js @@ -61,6 +61,67 @@ test('.remove()', t => { }, { instanceOf: TypeError, message: 'key must be an instance of a key instantiated by JWK.importKey' }) }) +test('.all() key_ops must be an array', t => { + const ks = new KeyStore() + t.throws(() => { + ks.all({ key_ops: 'wrapKey' }) + }, { instanceOf: TypeError, message: '`key_ops` must be a non-empty array of strings' }) +}) + +test('.all() key_ops must not be empty', t => { + const ks = new KeyStore() + t.throws(() => { + ks.all({ key_ops: [] }) + }, { instanceOf: TypeError, message: '`key_ops` must be a non-empty array of strings' }) +}) + +test('.all() key_ops must only contain strings', t => { + const ks = new KeyStore() + t.throws(() => { + ks.all({ key_ops: ['wrapKey', true] }) + }, { instanceOf: TypeError, message: '`key_ops` must be a non-empty array of strings' }) +}) + +test('.all() with key_ops when keys have key_ops', t => { + const k = generateSync('RSA', undefined, { key_ops: ['sign', 'verify'] }) + const ks = new KeyStore(k) + t.deepEqual(ks.all({ key_ops: ['wrapKey'] }), []) + t.deepEqual(ks.all({ key_ops: ['sign', 'wrapKey'] }), []) + t.deepEqual(ks.all({ key_ops: ['sign'] }), [k]) + t.deepEqual(ks.all({ key_ops: ['verify'] }), [k]) + t.deepEqual(ks.all({ key_ops: ['sign', 'verify'] }), [k]) + t.is(ks.get({ key_ops: ['wrapKey'] }), undefined) + t.is(ks.get({ key_ops: ['sign', 'wrapKey'] }), undefined) + t.is(ks.get({ key_ops: ['sign'] }), k) + t.is(ks.get({ key_ops: ['verify'] }), k) + t.is(ks.get({ key_ops: ['sign', 'verify'] }), k) +}) + +test('.all() with key_ops when keys have derived key_ops from use', t => { + const k = generateSync('RSA', undefined, { use: 'sig' }) + const ks = new KeyStore(k) + t.deepEqual(ks.all({ key_ops: ['wrapKey'] }), []) + t.deepEqual(ks.all({ key_ops: ['sign', 'wrapKey'] }), []) + t.deepEqual(ks.all({ key_ops: ['sign'] }), [k]) + t.deepEqual(ks.all({ key_ops: ['verify'] }), [k]) + t.deepEqual(ks.all({ key_ops: ['sign', 'verify'] }), [k]) + t.is(ks.get({ key_ops: ['wrapKey'] }), undefined) + t.is(ks.get({ key_ops: ['sign', 'wrapKey'] }), undefined) + t.is(ks.get({ key_ops: ['sign'] }), k) + t.is(ks.get({ key_ops: ['verify'] }), k) + t.is(ks.get({ key_ops: ['sign', 'verify'] }), k) +}) + +test('.get() with key_ops ranks keys with defined key_ops higher', t => { + const k = generateSync('RSA') + const k2 = generateSync('RSA', undefined, { use: 'sig' }) + const k3 = generateSync('RSA', undefined, { key_ops: ['sign', 'verify'] }) + const ks = new KeyStore(k, k2, k3) + + t.deepEqual(ks.all({ key_ops: ['sign'] }), [k3, k, k2]) + t.deepEqual(ks.get({ key_ops: ['sign'] }), k3) +}) + test('.all() and .get() use filter', t => { const k = generateSync('RSA', undefined, { use: 'sig' }) const ks = new KeyStore(k)