From 2dbd3ed43ac46f13f0904a4b77200f74e2ca74f8 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 19 Mar 2019 17:57:42 +0100 Subject: [PATCH 1/9] feat: add OKP Key and EdDSA sign/verify support BREAKING CHANGE: node.js minimal version is now v12.0.0 due to its added EdDSA support (crypto.sign, crypto.verify and eddsa key objects) resolves #12 --- .github/ISSUE_TEMPLATE/bug-report.md | 2 +- .travis.yml | 4 +- README.md | 12 +- docs/README.md | 49 ++-- lib/help/asn1/index.js | 6 + lib/help/asn1/one_asymmetric_key.js | 7 + lib/help/asn1/private_key.js | 5 + lib/help/key_object.js | 8 - lib/help/key_utils.js | 108 +++++++++ lib/index.d.ts | 44 +++- lib/jwa/ecdh/derive.js | 5 +- lib/jwa/ecdh/dir.js | 4 +- lib/jwa/ecdh/kw.js | 4 +- lib/jwa/ecdsa.js | 11 +- lib/jwa/eddsa.js | 22 ++ lib/jwa/index.js | 1 + lib/jwa/rsassa.js | 10 +- lib/jwa/rsassa_pss.js | 16 +- lib/jwk/generate.js | 8 + lib/jwk/import.js | 11 +- lib/jwk/key/base.js | 15 +- lib/jwk/key/okp.js | 113 ++++++++++ package.json | 3 +- test/cookbook/recipes/index.js | 3 +- test/cookbook/recipes/rfc8037.a4.ed25519.js | 35 +++ test/cookbook/rfc8037.a4.ed25519.test.js | 92 ++++++++ test/fixtures/Ed25519.key | 3 + test/fixtures/Ed25519.pem | 3 + test/fixtures/Ed448.key | 4 + test/fixtures/Ed448.pem | 4 + test/fixtures/X25519.key | 3 + test/fixtures/X25519.pem | 3 + test/fixtures/X448.key | 4 + test/fixtures/X448.pem | 4 + test/fixtures/index.js | 44 ++++ test/help/key_utils.test.js | 82 ++++++- test/jwk/ec.test.js | 10 +- test/jwk/generate.test.js | 39 +++- test/jwk/import.test.js | 2 +- test/jwk/okp_enc.test.js | 163 ++++++++++++++ test/jwk/okp_sig.test.js | 233 ++++++++++++++++++++ 41 files changed, 1090 insertions(+), 109 deletions(-) create mode 100644 lib/help/asn1/one_asymmetric_key.js create mode 100644 lib/help/asn1/private_key.js delete mode 100644 lib/help/key_object.js create mode 100644 lib/jwa/eddsa.js create mode 100644 lib/jwk/key/okp.js create mode 100644 test/cookbook/recipes/rfc8037.a4.ed25519.js create mode 100644 test/cookbook/rfc8037.a4.ed25519.test.js create mode 100644 test/fixtures/Ed25519.key create mode 100644 test/fixtures/Ed25519.pem create mode 100644 test/fixtures/Ed448.key create mode 100644 test/fixtures/Ed448.pem create mode 100644 test/fixtures/X25519.key create mode 100644 test/fixtures/X25519.pem create mode 100644 test/fixtures/X448.key create mode 100644 test/fixtures/X448.pem create mode 100644 test/jwk/okp_enc.test.js create mode 100644 test/jwk/okp_sig.test.js diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index d4dc1e2dda..3d3b708356 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -22,7 +22,7 @@ A clear and concise description of what you expected to happen. **Environment:** - @panva/jose version: [e.g. v1.0.0] - - node version: [e.g. v11.9.0] + - node version: [e.g. v12.0.0] **Additional context** Add any other context about the problem here. diff --git a/.travis.yml b/.travis.yml index 1d7c3bc236..b6f469f378 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,9 +6,9 @@ matrix: language: node_js node_js: stable script: npm run lint - - name: "Test Suite + coverage - 11.8.0" #min + - name: "Test Suite + coverage - 12.0.0" #min language: node_js - node_js: 11.8.0 + node_js: 12.0.0 script: npm run coverage after_script: npx codecov - name: "Test Suite + coverage - stable" diff --git a/README.md b/README.md index ae39934ae0..0ac606b338 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ The following specifications are implemented by @panva/jose - JSON Web Token (JWT) - [RFC7519][spec-jwt] - JSON Web Key (JWK) Thumbprint - [RFC7638][spec-thumbprint] - JWS Unencoded Payload Option - [RFC7797][spec-b64] +- CFRG Elliptic Curve Signatures (EdDSA) - [RFC8037][spec-okp] The test suite utilizes examples defined in [RFC7520][spec-cookbook] to confirm its JOSE implementation is correct. @@ -31,6 +32,7 @@ Legend: | -- | -- | -- | | RSA | ✓ | RSA | | Elliptic Curve | ✓ | EC | +| Octet Key Pair | ✓ | OKP | | Octet sequence | ✓ | oct | | Serialization | JWS Sign | JWS Verify | JWE Encrypt | JWE Decrypt | @@ -44,6 +46,7 @@ Legend: | RSASSA-PKCS1-v1_5 | ✓ | RS256, RS384, RS512 | | RSASSA-PSS | ✓ | PS256, PS384, PS512 | | ECDSA | ✓ | ES256, ES384, ES512 | +| Edwards-curve DSA | ✓ | EdDSA | | HMAC with SHA-2 | ✓ | HS256, HS384, HS512 | | JWE Key Management Algorithms | Supported || @@ -64,7 +67,7 @@ Legend: --- Pending Node.js Support 🤞: -- [RFC8037][spec-cfrg] (EdDSA, OKP kty, etc). See [#12](https://github.com/panva/jose/issues/12) +- ECDH-ES with X25519 and X448 Won't implement: - ✕ JWS embedded key / referenced verification @@ -107,8 +110,7 @@ If you or your business use @panva/jose, please consider becoming a [Patron][sup ## Usage -⚠️ Minimal Node.js version required is **v11.8.0** ⚠️ The plan is to release v1.0.0 when Node.js -v12.0.0 releases in April 2019 +For its improvements in the crypto module ⚠️ the minimal Node.js version required is **v12.0.0** ⚠️ Installing @panva/jose @@ -255,7 +257,7 @@ private API and is subject to change between any versions. #### How do I use it outside of Node.js It is **only built for Node.js** environment - it builds on top of the `crypto` module and requires -the KeyObject API that was added in Node.js v11.6.0. +the KeyObject API that was added in Node.js v11.6.0 and one-shot sign/verify API added in v12.0.0 #### How is it different from [`node-jose`][node-jose] @@ -304,13 +306,13 @@ in terms of performance and API (not having well defined errors). When Node.js v [node-jose]: https://github.com/cisco/node-jose [security-vulnerability]: https://github.com/panva/jose/issues/new?template=security-vulnerability.md [spec-b64]: https://tools.ietf.org/html/rfc7797 -[spec-cfrg]: https://tools.ietf.org/html/rfc8037 [spec-cookbook]: https://tools.ietf.org/html/rfc7520 [spec-jwa]: https://tools.ietf.org/html/rfc7518 [spec-jwe]: https://tools.ietf.org/html/rfc7516 [spec-jwk]: https://tools.ietf.org/html/rfc7517 [spec-jws]: https://tools.ietf.org/html/rfc7515 [spec-jwt]: https://tools.ietf.org/html/rfc7519 +[spec-okp]: https://tools.ietf.org/html/rfc8037 [spec-thumbprint]: https://tools.ietf.org/html/rfc7638 [suggest-feature]: https://github.com/panva/jose/issues/new?labels=enhancement&template=feature-request.md&title=proposal%3A+ [support-patreon]: https://www.patreon.com/panva diff --git a/docs/README.md b/docs/README.md index 664dce5c1e..d00374ab90 100644 --- a/docs/README.md +++ b/docs/README.md @@ -24,7 +24,7 @@ I can continue maintaining it and adding new features carefree. You may also don ## JWK (JSON Web Key) -- [Class: <JWK.Key> and <JWK.RSAKey> | <JWK.ECKey> | <JWK.OctKey>](#class-jwkkey-and-jwkrsakey--jwkeckey--jwkoctkey) +- [Class: <JWK.Key> and <JWK.RSAKey> | <JWK.ECKey> | <JWK.OKPKey> | <JWK.OctKey>](#class-jwkkey-and-jwkrsakey--jwkeckey--jwkokpkey--jwkoctkey) - [key.kty](#keykty) - [key.alg](#keyalg) - [key.use](#keyuse) @@ -58,13 +58,13 @@ const { JWK } = require('@panva/jose') --- -#### Class: `` and `` | `` | `` +#### Class: `` and `` | `` | `` | `` -``, `` and `` represent a key usable for JWS and JWE operations. +``, ``, `` and `` represent a key usable for JWS and JWE operations. The `JWK.importKey()` method is used to retrieve a key representation of an existing key or secret. `JWK.generate()` method is used to generate a new random key. -``, `` and `` inherit methods from `` and in addition +``, ``, `` and `` inherit methods from `` and in addition to the properties documented below have the respective key component properties exported as `` in their format defined by the specifications. @@ -72,14 +72,16 @@ to the properties documented below have the respective key component properties - `e, n, d, p, q, dp, dq, qi` for Private RSA Keys - `crv, x, y` for Public EC Keys - `crv, x, y, n` for Private EC Keys +- `crv, x` for Public OKP Keys +- `crv, x, n` for Private OKP Keys - `k` for Symmetric keys --- #### `key.kty` -Returns the key's JWK Key Type Parameter. 'EC', 'RSA' or 'oct' for the respective supported key -types. +Returns the key's JWK Key Type Parameter. 'EC', 'RSA', 'OKP' or 'oct' for the respective supported +key types. - `` @@ -262,7 +264,7 @@ Private keys may also be passphrase protected. [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'. -- Returns: `` | `` +- Returns: `` | `` | `` See the underlying Node.js API for details on importing private and public keys in the different formats @@ -321,11 +323,11 @@ const key = importKey(Buffer.from('8yHym6h5CG5FylbzrCn8fhxEbp3kOaTsgLaawaaJ')) #### `JWK.importKey(jwk)` JWK-formatted key import -Imports a JWK formatted key. This supports JWK formatted EC, RSA and oct keys. Asymmetrical keys -may be both private and public. +Imports a JWK formatted key. This supports JWK formatted RSA, EC, OKP and oct keys. Asymmetrical +keys may be both private and public. - `jwk`: `` - - `kty`: `` Key type. Must be 'RSA', 'EC' or 'oct'. + - `kty`: `` Key type. Must be 'RSA', 'EC', 'OKP' or 'oct'. - `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'. @@ -335,8 +337,10 @@ may be both private and public. - `e`, `n`, `d`, `p`, `q`, `dp`, `dq`, `qi` properties as `` for RSA private keys - `crv`, `x`, `y` properties as `` for EC public keys - `crv`, `x`, `y`, `d` properties as `` for EC private keys + - `crv`, `x`, properties as `` for OKP public keys + - `crv`, `x`, `d` properties as `` for OKP private keys - `k` properties as `` for secret oct keys -- Returns: `` | `` | `` +- Returns: `` | `` | `` | ``
Example (Click to expand) @@ -366,10 +370,11 @@ const key = importKey(jwk) #### `JWK.generate(kty[, crvOrSize[, options[, private]]])` generating new keys -Securely generates a new RSA, EC or oct key. +Securely generates a new RSA, EC, OKP or oct key. -- `kty`: `` Key type. Must be 'RSA', 'EC' or 'oct'. -- `crvOrSize`: `` | `` key's bit size or in case of EC keys the curve +- `kty`: `` Key type. Must be 'RSA', 'EC', 'OKP' or 'oct'. +- `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. - `kid`: `` Key ID Parameter. When not provided is computed using the method defined in @@ -378,7 +383,7 @@ Securely generates a new RSA, EC or oct key. data or signing & verifying data. Must be 'sig' or 'enc'. - `private`: `` **Default** 'true'. Is the resulting key private or public (when asymmetrical) -- Returns: `Promise` | `Promise` | `Promise` +- Returns: `Promise` | `Promise` | `Promise` | `Promise`
Example (Click to expand) @@ -406,9 +411,9 @@ const { JWK: { generate } } = require('@panva/jose') Synchronous version of `JWK.generate()` -- `kty`: `` Key type. Must be 'RSA', 'EC' or 'oct'. -- `crvOrSize`: `` | `` key's bit size or in case of EC keys the curve. **Default:** - 2048 for RSA, 'P-256' for EC and 256 for oct. +- `kty`: `` Key type. Must be 'RSA', 'EC', 'OKP' or 'oct'. +- `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 @@ -417,7 +422,7 @@ Synchronous version of `JWK.generate()` [RFC7638][spec-thumbprint] - `private`: `` **Default** 'true'. Is the resulting key private or public (when asymmetrical) -- Returns: `` | `` | `` +- Returns: `` | `` | `` | ``
Example (Click to expand) @@ -527,7 +532,7 @@ parameters is returned. - `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' -- Returns: `` | `` | `` | `` +- Returns: `` | `` | `` | `` | `` --- @@ -535,7 +540,7 @@ parameters is returned. Adds a key instance to the store unless it is already included. -- `key`: `` | `` | `` +- `key`: `` | `` | `` | `` --- @@ -543,7 +548,7 @@ Adds a key instance to the store unless it is already included. Ensures a key is removed from a store. -- `key`: `` | `` | `` +- `key`: `` | `` | `` | `` --- diff --git a/lib/help/asn1/index.js b/lib/help/asn1/index.js index 9e437f7376..ff7e357c9e 100644 --- a/lib/help/asn1/index.js +++ b/lib/help/asn1/index.js @@ -14,6 +14,12 @@ types.set('PrivateKeyInfo', PrivateKeyInfo) const PublicKeyInfo = asn1.define('PublicKeyInfo', require('./public_key_info')(AlgorithmIdentifier)) types.set('PublicKeyInfo', PublicKeyInfo) +const PrivateKey = asn1.define('PrivateKey', require('./private_key')) +types.set('PrivateKey', PrivateKey) + +const OneAsymmetricKey = asn1.define('OneAsymmetricKey', require('./one_asymmetric_key')(AlgorithmIdentifier, PrivateKey)) +types.set('OneAsymmetricKey', OneAsymmetricKey) + const RSAPrivateKey = asn1.define('RSAPrivateKey', require('./rsa_private_key')) types.set('RSAPrivateKey', RSAPrivateKey) diff --git a/lib/help/asn1/one_asymmetric_key.js b/lib/help/asn1/one_asymmetric_key.js new file mode 100644 index 0000000000..c01ec4bfd6 --- /dev/null +++ b/lib/help/asn1/one_asymmetric_key.js @@ -0,0 +1,7 @@ +module.exports = (AlgorithmIdentifier, PrivateKey) => function () { + this.seq().obj( + this.key('version').int(), + this.key('algorithm').use(AlgorithmIdentifier), + this.key('privateKey').use(PrivateKey) + ) +} diff --git a/lib/help/asn1/private_key.js b/lib/help/asn1/private_key.js new file mode 100644 index 0000000000..f07618353e --- /dev/null +++ b/lib/help/asn1/private_key.js @@ -0,0 +1,5 @@ +module.exports = function () { + this.octstr().contains().obj( + this.key('privateKey').octstr() + ) +} diff --git a/lib/help/key_object.js b/lib/help/key_object.js deleted file mode 100644 index 42ba29086a..0000000000 --- a/lib/help/key_object.js +++ /dev/null @@ -1,8 +0,0 @@ -const { createSecretKey, KeyObject } = require('crypto') - -if (KeyObject) { - module.exports = KeyObject -} else { - const SecretKeyObject = Object.getPrototypeOf(createSecretKey(Buffer.allocUnsafe(1))) - module.exports = Object.getPrototypeOf(SecretKeyObject).constructor -} diff --git a/lib/help/key_utils.js b/lib/help/key_utils.js index 356c3390d0..869f3c38d2 100644 --- a/lib/help/key_utils.js +++ b/lib/help/key_utils.js @@ -1,8 +1,11 @@ +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 oidHexToCurve = new Map([ ['06082a8648ce3d030107', 'P-256'], @@ -21,6 +24,34 @@ const crvToOidBuf = new Map([ ['P-521', Buffer.from('06052b81040023', 'hex')] ]) +const formatPem = (base64pem, descriptor) => `-----BEGIN ${descriptor} KEY-----\n${base64pem.match(/.{1,64}/g).join('\n')}\n-----END ${descriptor} KEY-----` + +const okpToJWK = { + private (crv, keyObject) { + const der = keyObject.export({ type: 'pkcs8', format: 'der' }) + const OneAsymmetricKey = asn1.get('OneAsymmetricKey') + const { privateKey: { privateKey: d } } = OneAsymmetricKey.decode(der) + + return { + ...okpToJWK.public(crv, createPublicKey(keyObject)), + d: base64url.encodeBuffer(d) + } + }, + public (crv, keyObject) { + const der = keyObject.export({ type: 'spki', format: 'der' }) + + const PublicKeyInfo = asn1.get('PublicKeyInfo') + + const { publicKey: { data: x } } = PublicKeyInfo.decode(der) + + return { + kty: 'OKP', + crv, + x: base64url.encodeBuffer(x) + } + } +} + const keyObjectToJWK = { rsa: { private (keyObject) { @@ -100,6 +131,38 @@ const keyObjectToJWK = { y: base64url.encodeBuffer(y) } } + }, + ed25519: { + private (keyObject) { + return okpToJWK.private('Ed25519', keyObject) + }, + public (keyObject) { + return okpToJWK.public('Ed25519', keyObject) + } + }, + ed448: { + private (keyObject) { + return okpToJWK.private('Ed448', keyObject) + }, + public (keyObject) { + return okpToJWK.public('Ed448', keyObject) + } + }, + x25519: { + private (keyObject) { + return okpToJWK.private('X25519', keyObject) + }, + public (keyObject) { + return okpToJWK.public('X25519', keyObject) + } + }, + x448: { + private (keyObject) { + return okpToJWK.private('X448', keyObject) + }, + public (keyObject) { + return okpToJWK.public('X448', keyObject) + } } } @@ -172,6 +235,46 @@ const jwkToPem = { publicKey: concatEcPublicKey(jwk.x, jwk.y) }, 'pem', { label: 'PUBLIC KEY' }).toString('base64') } + }, + OKP: { + private (jwk) { + const OneAsymmetricKey = asn1.get('OneAsymmetricKey') + + const b64 = OneAsymmetricKey.encode({ + version: 0, + privateKey: { privateKey: base64url.decodeToBuffer(jwk.d) }, + algorithm: { algorithm: okpCrvToOid(jwk.crv) } + }, 'der') + + // TODO: WHYYY? https://github.com/indutny/asn1.js/issues/110 + b64.write('04', 12, 1, 'hex') + + return formatPem(b64.toString('base64'), 'PRIVATE') + }, + public (jwk) { + const PublicKeyInfo = asn1.get('PublicKeyInfo') + + return PublicKeyInfo.encode({ + algorithm: { algorithm: okpCrvToOid(jwk.crv) }, + publicKey: { + unused: 0, + data: base64url.decodeToBuffer(jwk.x) + } + }, 'pem', { label: 'PUBLIC KEY' }) + } + } +} + +const okpCrvToOid = (crv) => { + switch (crv) { + case 'X25519': + return '1.3.101.110'.split('.') + case 'X448': + return '1.3.101.111'.split('.') + case 'Ed25519': + return '1.3.101.112'.split('.') + case 'Ed448': + return '1.3.101.113'.split('.') } } @@ -182,6 +285,11 @@ module.exports.jwkToPem = (jwk) => { throw new errors.JOSENotSupported(`unsupported EC key curve: ${jwk.crv}`) } break + case 'OKP': + if (!OKP_CURVES.has(jwk.crv)) { + throw new errors.JOSENotSupported(`unsupported OKP key curve: ${jwk.crv}`) + } + break case 'RSA': break default: diff --git a/lib/index.d.ts b/lib/index.d.ts index 6141b81278..93a1514e14 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -8,8 +8,9 @@ interface KeyParameters { use?: use kid?: string } -type curve = 'P-256' | 'P-384' | 'P-521' -type keyType = 'RSA' | 'EC' | 'oct' +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' @@ -38,12 +39,19 @@ export namespace JWK { interface JWKECKey extends KeyParameters { kty: 'EC' - crv: curve + crv: ECCurve x: string y: string d?: string } + interface JWKOKPKey extends KeyParameters { + kty: 'OKP' + crv: OKPCurve + x: string + d?: string + } + interface JWKRSAKey extends KeyParameters { kty: 'RSA' e: string @@ -76,7 +84,7 @@ export namespace JWK { kty: 'EC' secret: false type: asymmetricKeyObjectTypes - crv: curve + crv: ECCurve x: string y: string d?: string @@ -84,6 +92,17 @@ export namespace JWK { toJWK(private?: boolean): JWKECKey } + class OKPKey extends Key { + kty: 'OKP' + secret: false + type: asymmetricKeyObjectTypes + crv: OKPCurve + x: string + d?: string + + toJWK(private?: boolean): JWKOKPKey + } + class OctKey extends Key { kty: 'oct' type: 'secret' @@ -97,17 +116,20 @@ export namespace JWK { export function isKey(object: any): boolean - export function importKey(keyObject: KeyObject, parameters?: KeyParameters): RSAKey | ECKey | OctKey - export function importKey(key: PrivateKeyInput | PublicKeyInput | string | Buffer, parameters?: KeyParameters): RSAKey | ECKey | OctKey + export function importKey(keyObject: KeyObject, parameters?: KeyParameters): RSAKey | ECKey | OKPKey | OctKey + export function importKey(key: PrivateKeyInput | PublicKeyInput | string | Buffer, parameters?: KeyParameters): RSAKey | ECKey | OKPKey | OctKey export function importKey(jwk: JWKOctKey): OctKey export function importKey(jwk: JWKRSAKey): RSAKey export function importKey(jwk: JWKECKey): ECKey + export function importKey(jwk: JWKOKPKey): OKPKey - export function generate(kty: 'EC', crv?: curve, parameters?: KeyParameters, private?: boolean): Promise + export function generate(kty: 'EC', crv?: ECCurve, parameters?: KeyParameters, private?: boolean): Promise + export function generate(kty: 'OKP', crv?: OKPCurve, parameters?: KeyParameters, private?: boolean): Promise export function generate(kty: 'RSA', bitlength?: number, parameters?: KeyParameters, private?: boolean): Promise export function generate(kty: 'oct', bitlength?: number, parameters?: KeyParameters): Promise - export function generateSync(kty: 'EC', crv?: curve, parameters?: KeyParameters, private?: boolean): ECKey + export function generateSync(kty: 'EC', crv?: ECCurve, parameters?: KeyParameters, private?: boolean): ECKey + export function generateSync(kty: 'OKP', crv?: OKPCurve, parameters?: KeyParameters, private?: boolean): OKPKey export function generateSync(kty: 'RSA', bitlength?: number, parameters?: KeyParameters, private?: boolean): RSAKey export function generateSync(kty: 'oct', bitlength?: number, parameters?: KeyParameters): OctKey } @@ -128,11 +150,13 @@ export namespace JWKS { all(parameters?: KeyQuery): JWK.Key[] get(parameters?: KeyQuery): JWK.Key - generate(kty: 'EC', crv?: curve, parameters?: KeyParameters, private?: boolean): void + generate(kty: 'EC', crv?: ECCurve, parameters?: KeyParameters, private?: boolean): void + generate(kty: 'OKP', crv?: OKPCurve, parameters?: KeyParameters, private?: boolean): void generate(kty: 'RSA', bitlength?: number, parameters?: KeyParameters, private?: boolean): void generate(kty: 'oct', bitlength?: number, parameters?: KeyParameters): void - generateSync(kty: 'EC', crv?: curve, parameters?: KeyParameters, private?: boolean): void + generateSync(kty: 'EC', crv?: ECCurve, parameters?: KeyParameters, private?: boolean): void + generateSync(kty: 'OKP', crv?: OKPCurve, parameters?: KeyParameters, private?: boolean): void generateSync(kty: 'RSA', bitlength?: number, parameters?: KeyParameters, private?: boolean): void generateSync(kty: 'oct', bitlength?: number, parameters?: KeyParameters): void } diff --git a/lib/jwa/ecdh/derive.js b/lib/jwa/ecdh/derive.js index 91ff7a17b6..c69eccb8d9 100644 --- a/lib/jwa/ecdh/derive.js +++ b/lib/jwa/ecdh/derive.js @@ -10,13 +10,16 @@ const crvToCurve = (crv) => { return 'secp384r1' case 'P-521': return 'secp521r1' + case 'X448': + case 'X25519': + return crv } } const UNCOMPRESSED = Buffer.alloc(1, POINT_CONVERSION_UNCOMPRESSED) const pubToBuffer = (x, y) => Buffer.concat([UNCOMPRESSED, base64url.decodeToBuffer(x), base64url.decodeToBuffer(y)]) -const computeSecret = ({ crv, d }, { x, y }) => { +const computeSecret = ({ crv, d }, { x, y = '' }) => { const curve = crvToCurve(crv) const exchange = createECDH(curve) diff --git a/lib/jwa/ecdh/dir.js b/lib/jwa/ecdh/dir.js index 7ecb704919..73453f218b 100644 --- a/lib/jwa/ecdh/dir.js +++ b/lib/jwa/ecdh/dir.js @@ -6,13 +6,13 @@ const { generateSync } = require('../../jwk/generate') const derive = require('./derive') const wrapKey = (key, payload, { enc }) => { - const epk = generateSync('EC', key.crv) + const epk = generateSync(key.kty, key.crv) const derivedKey = derive(enc, KEYLENGTHS[enc], epk, key) return { wrapped: derivedKey, - header: { epk: { kty: 'EC', crv: key.crv, x: epk.x, y: epk.y } } + header: { epk: { kty: key.kty, crv: key.crv, x: epk.x, y: epk.y } } } } diff --git a/lib/jwa/ecdh/kw.js b/lib/jwa/ecdh/kw.js index d7bb40b422..9653c99b68 100644 --- a/lib/jwa/ecdh/kw.js +++ b/lib/jwa/ecdh/kw.js @@ -6,12 +6,12 @@ const { generateSync } = require('../../jwk/generate') const derive = require('./derive') const wrapKey = (wrap, derive, key, payload) => { - const epk = generateSync('EC', key.crv) + const epk = generateSync(key.kty, key.crv) const derivedKey = derive(epk, key, payload) const result = wrap({ [KEYOBJECT]: derivedKey }, payload) - result.header = { epk: { kty: 'EC', crv: key.crv, x: epk.x, y: epk.y } } + result.header = { epk: { kty: key.kty, crv: key.crv, x: epk.x, y: epk.y } } return result } diff --git a/lib/jwa/ecdsa.js b/lib/jwa/ecdsa.js index 80df4b2cd5..7384555f62 100644 --- a/lib/jwa/ecdsa.js +++ b/lib/jwa/ecdsa.js @@ -1,22 +1,17 @@ const { strict: assert } = require('assert') -const { createSign, createVerify } = require('crypto') +const { sign: signOneShot, verify: verifyOneShot } = require('crypto') const { derToJose, joseToDer } = require('../help/ecdsa_signatures') const { KEYOBJECT } = require('../help/symbols') const resolveNodeAlg = require('../help/node_alg') const sign = (jwaAlg, nodeAlg, { [KEYOBJECT]: keyObject }, payload) => { - const sign = createSign(nodeAlg) - sign.update(payload) - return derToJose(sign.sign(keyObject), jwaAlg) + return derToJose(signOneShot(nodeAlg, payload, keyObject), jwaAlg) } const verify = (jwaAlg, nodeAlg, { [KEYOBJECT]: keyObject }, payload, signature) => { - const verify = createVerify(nodeAlg) - verify.update(payload) - try { - return verify.verify(keyObject, joseToDer(signature, jwaAlg)) + return verifyOneShot(nodeAlg, payload, keyObject, joseToDer(signature, jwaAlg)) } catch (err) { return false } diff --git a/lib/jwa/eddsa.js b/lib/jwa/eddsa.js new file mode 100644 index 0000000000..d745092b92 --- /dev/null +++ b/lib/jwa/eddsa.js @@ -0,0 +1,22 @@ +const { strict: assert } = require('assert') +const { sign: signOneShot, verify: verifyOneShot } = require('crypto') + +const { KEYOBJECT } = require('../help/symbols') + +const sign = ({ [KEYOBJECT]: keyObject }, payload) => { + return signOneShot(undefined, payload, keyObject) +} + +const verify = ({ [KEYOBJECT]: keyObject }, payload, signature) => { + return verifyOneShot(undefined, payload, keyObject, signature) +} + +const ALG = 'EdDSA' + +module.exports = (JWA) => { + assert(!JWA.sign.has(ALG), `sign alg ${ALG} already registered`) + assert(!JWA.verify.has(ALG), `verify alg ${ALG} already registered`) + + JWA.sign.set(ALG, sign) + JWA.verify.set(ALG, verify) +} diff --git a/lib/jwa/index.js b/lib/jwa/index.js index 713beb4349..c935d308f3 100644 --- a/lib/jwa/index.js +++ b/lib/jwa/index.js @@ -12,6 +12,7 @@ const JWA = { // sign, verify require('./hmac')(JWA) require('./ecdsa')(JWA) +require('./eddsa')(JWA) require('./rsassa')(JWA) require('./rsassa_pss')(JWA) diff --git a/lib/jwa/rsassa.js b/lib/jwa/rsassa.js index 5dcda3248c..d638cdd7fb 100644 --- a/lib/jwa/rsassa.js +++ b/lib/jwa/rsassa.js @@ -1,19 +1,15 @@ const { strict: assert } = require('assert') -const { createSign, createVerify } = require('crypto') +const { sign: signOneShot, verify: verifyOneShot } = require('crypto') const { KEYOBJECT } = require('../help/symbols') const resolveNodeAlg = require('../help/node_alg') const sign = (nodeAlg, { [KEYOBJECT]: keyObject }, payload) => { - const sign = createSign(nodeAlg) - sign.update(payload) - return sign.sign(keyObject) + return signOneShot(nodeAlg, payload, keyObject) } const verify = (nodeAlg, { [KEYOBJECT]: keyObject }, payload, signature) => { - const verify = createVerify(nodeAlg) - verify.update(payload) - return verify.verify(keyObject, signature) + return verifyOneShot(nodeAlg, payload, keyObject, signature) } module.exports = (JWA) => { diff --git a/lib/jwa/rsassa_pss.js b/lib/jwa/rsassa_pss.js index 7e141ce1a0..f1f54ffe34 100644 --- a/lib/jwa/rsassa_pss.js +++ b/lib/jwa/rsassa_pss.js @@ -1,25 +1,19 @@ const { strict: assert } = require('assert') -const { createSign, createVerify, constants } = require('crypto') +const { sign: signOneShot, verify: verifyOneShot, constants } = require('crypto') const { KEYOBJECT } = require('../help/symbols') const resolveNodeAlg = require('../help/node_alg') -const sign = (nodeAlg, { [KEYOBJECT]: keyObject, length }, payload) => { - const sign = createSign(nodeAlg) - sign.update(payload) - - return sign.sign({ +const sign = (nodeAlg, { [KEYOBJECT]: keyObject }, payload) => { + return signOneShot(nodeAlg, payload, { key: keyObject, padding: constants.RSA_PKCS1_PSS_PADDING, saltLength: constants.RSA_PSS_SALTLEN_DIGEST }) } -const verify = (nodeAlg, { [KEYOBJECT]: keyObject, length }, payload, signature) => { - const verify = createVerify(nodeAlg) - verify.update(payload) - - return verify.verify({ +const verify = (nodeAlg, { [KEYOBJECT]: keyObject }, payload, signature) => { + return verifyOneShot(nodeAlg, payload, { key: keyObject, padding: constants.RSA_PKCS1_PSS_PADDING, saltLength: constants.RSA_PSS_SALTLEN_DIGEST diff --git a/lib/jwk/generate.js b/lib/jwk/generate.js index 8b039c530c..fbc9c0d9e7 100644 --- a/lib/jwk/generate.js +++ b/lib/jwk/generate.js @@ -4,6 +4,7 @@ const importKey = require('./import') const RSAKey = require('./key/rsa') const ECKey = require('./key/ec') +const OKPKey = require('./key/okp') const OctKey = require('./key/oct') const generate = async (kty, crvOrSize, params, generatePrivate = true) => { @@ -18,6 +19,11 @@ const generate = async (kty, crvOrSize, params, generatePrivate = true) => { await ECKey.generate(crvOrSize, generatePrivate), params ) + case 'OKP': + return importKey( + await OKPKey.generate(crvOrSize, generatePrivate), + params + ) case 'oct': return importKey( await OctKey.generate(crvOrSize, generatePrivate), @@ -34,6 +40,8 @@ const generateSync = (kty, crvOrSize, params, generatePrivate = true) => { return importKey(RSAKey.generateSync(crvOrSize, generatePrivate), params) case 'EC': return importKey(ECKey.generateSync(crvOrSize, generatePrivate), params) + case 'OKP': + return importKey(OKPKey.generateSync(crvOrSize, generatePrivate), params) case 'oct': return importKey(OctKey.generateSync(crvOrSize, generatePrivate), params) default: diff --git a/lib/jwk/import.js b/lib/jwk/import.js index bd0ce98129..8423877bf2 100644 --- a/lib/jwk/import.js +++ b/lib/jwk/import.js @@ -1,13 +1,13 @@ -const { createPublicKey, createPrivateKey, createSecretKey } = require('crypto') +const { createPublicKey, createPrivateKey, createSecretKey, KeyObject } = require('crypto') const base64url = require('../help/base64url') const isObject = require('../help/is_object') -const KeyObject = require('../help/key_object') const { jwkToPem } = require('../help/key_utils') const errors = require('../errors') const RSAKey = require('./key/rsa') const ECKey = require('./key/ec') +const OKPKey = require('./key/okp') const OctKey = require('./key/oct') const importable = new Set(['string', 'buffer', 'object']) @@ -79,8 +79,13 @@ const importKey = (key, parameters) => { return new RSAKey(keyObject, parameters) case 'ec': return new ECKey(keyObject, parameters) + case 'ed25519': + case 'ed448': + case 'x25519': + case 'x448': + return new OKPKey(keyObject, parameters) default: - throw new errors.JOSENotSupported('only RSA and EC asymmetric keys are supported') + throw new errors.JOSENotSupported('only RSA, EC and OKP asymmetric keys are supported') } } else if (secret) { return new OctKey(keyObject, parameters) diff --git a/lib/jwk/key/base.js b/lib/jwk/key/base.js index 0c470528b9..8ce098cbf4 100644 --- a/lib/jwk/key/base.js +++ b/lib/jwk/key/base.js @@ -59,17 +59,12 @@ class Key { throw new TypeError('public key cannot be exported as private') } - let members - if (priv) { - members = this.constructor[PRIVATE_MEMBERS] - } else { - members = this.constructor[PUBLIC_MEMBERS] - } + const result = Object.fromEntries( + [...this.constructor[priv ? PRIVATE_MEMBERS : PUBLIC_MEMBERS]].map(k => [k, this[k]]) + ) - const result = [...members].reduce((acc, key) => { - acc[key] = this[key] - return acc - }, { kty: this.kty, kid: this.kid }) + result.kty = this.kty + result.kid = this.kid if (this.alg) { result.alg = this.alg diff --git a/lib/jwk/key/okp.js b/lib/jwk/key/okp.js new file mode 100644 index 0000000000..71efa75765 --- /dev/null +++ b/lib/jwk/key/okp.js @@ -0,0 +1,113 @@ +const { generateKeyPairSync, generateKeyPair: async } = require('crypto') +const { promisify } = require('util') + +const { + THUMBPRINT_MATERIAL, JWK_MEMBERS, PUBLIC_MEMBERS, PRIVATE_MEMBERS +} = require('../../help/symbols') +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']) +Object.freeze(OKP_PRIVATE) + +// Octet string key pairs Key Type +class OKPKey extends Key { + constructor (...args) { + super(...args) + + Object.defineProperties(this, { + kty: { + value: 'OKP', + enumerable: true + } + }) + this[JWK_MEMBERS]() + } + + static get [PUBLIC_MEMBERS] () { + return OKP_PUBLIC + } + + static get [PRIVATE_MEMBERS] () { + return OKP_PRIVATE + } + + [THUMBPRINT_MATERIAL] () { + return { crv: this.crv, kty: 'OKP', x: this.x } + } + + algorithms (operation, { use = this.use, alg = this.alg } = {}) { + if (alg) { + return new Set(this.algorithms(operation, { alg: null, use }).has(alg) ? [alg] : undefined) + } + + switch (operation) { + case 'encrypt': + case 'decrypt': + return new Set() + case 'sign': + if (this.public || use === 'enc' || this.crv.startsWith('X')) { + return new Set() + } + + return new Set(['EdDSA']) + case 'verify': + if (use === 'enc' || this.crv.startsWith('X')) { + return new Set() + } + + return new Set(['EdDSA']) + case 'wrapKey': + 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() + case undefined: + return new Set([ + ...this.algorithms('verify'), + ...this.algorithms('wrapKey') + ]) + default: + throw new TypeError('invalid key operation') + } + } + + static async generate (crv = 'Ed25519', privat = true) { + if (!OKP_CURVES.has(crv)) { + throw new errors.JOSENotSupported(`unsupported OKP key curve: ${crv}`) + } + + const { privateKey, publicKey } = await generateKeyPair(crv.toLowerCase()) + + return privat ? privateKey : publicKey + } + + static generateSync (crv = 'Ed25519', privat = true) { + if (!OKP_CURVES.has(crv)) { + throw new errors.JOSENotSupported(`unsupported OKP key curve: ${crv}`) + } + + const { privateKey, publicKey } = generateKeyPairSync(crv.toLowerCase()) + + return privat ? privateKey : publicKey + } +} + +module.exports = OKPKey diff --git a/package.json b/package.json index d712915151..4ad1957276 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "jwks", "jws", "jwt", + "eddsa", "sign", "verify" ], @@ -48,7 +49,7 @@ "standard": "^12.0.1" }, "engines": { - "node": ">=11.8.0" + "node": ">=12.0.0" }, "ava": { "babel": false, diff --git a/test/cookbook/recipes/index.js b/test/cookbook/recipes/index.js index 1e07cd2943..be5fec5022 100644 --- a/test/cookbook/recipes/index.js +++ b/test/cookbook/recipes/index.js @@ -26,5 +26,6 @@ module.exports = new Map([ ['5.12', require('./5_12.protecting_content_only')], ['5.13', require('./5_13.encrypting_to_multiple_recipients')], ['4.1 rfc7797', require('./rfc7797.4_1.hmac-sha2_b64_false')], - ['4.2 rfc7797', require('./rfc7797.4_2.hmac-sha2_b64_false')] + ['4.2 rfc7797', require('./rfc7797.4_2.hmac-sha2_b64_false')], + ['A.4 rfc8037', require('./rfc8037.a4.ed25519')] ]) diff --git a/test/cookbook/recipes/rfc8037.a4.ed25519.js b/test/cookbook/recipes/rfc8037.a4.ed25519.js new file mode 100644 index 0000000000..82af431e80 --- /dev/null +++ b/test/cookbook/recipes/rfc8037.a4.ed25519.js @@ -0,0 +1,35 @@ +module.exports = { + title: 'Ed25519 Signature', + input: { + payload: 'Example of Ed25519 signing', + key: { + kty: 'OKP', + crv: 'Ed25519', + d: 'nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A', + x: '11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo' + }, + alg: 'EdDSA' + }, + signing: { + protected: { + alg: 'EdDSA' + } + }, + output: { + compact: 'eyJhbGciOiJFZERTQSJ9.RXhhbXBsZSBvZiBFZDI1NTE5IHNpZ25pbmc.hgyY0il_MGCjP0JzlnLWG1PPOt7-09PGcvMg3AIbQR6dWbhijcNR4ki4iylGjg5BhVsPt9g7sVvpAr_MuM0KAg', + json: { + payload: 'RXhhbXBsZSBvZiBFZDI1NTE5IHNpZ25pbmc', + signatures: [ + { + protected: 'eyJhbGciOiJFZERTQSJ9', + signature: 'hgyY0il_MGCjP0JzlnLWG1PPOt7-09PGcvMg3AIbQR6dWbhijcNR4ki4iylGjg5BhVsPt9g7sVvpAr_MuM0KAg' + } + ] + }, + json_flat: { + payload: 'RXhhbXBsZSBvZiBFZDI1NTE5IHNpZ25pbmc', + protected: 'eyJhbGciOiJFZERTQSJ9', + signature: 'hgyY0il_MGCjP0JzlnLWG1PPOt7-09PGcvMg3AIbQR6dWbhijcNR4ki4iylGjg5BhVsPt9g7sVvpAr_MuM0KAg' + } + } +} diff --git a/test/cookbook/rfc8037.a4.ed25519.test.js b/test/cookbook/rfc8037.a4.ed25519.test.js new file mode 100644 index 0000000000..821a0f3ccf --- /dev/null +++ b/test/cookbook/rfc8037.a4.ed25519.test.js @@ -0,0 +1,92 @@ +const test = require('ava') + +const recipe = require('./recipes').get('A.4 rfc8037') + +const { JWS, JWK: { importKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') + +const { input: { payload, key: jwk }, signing: { protected: header } } = recipe + +const key = importKey(jwk) + +test('OKP JWK Thumbprint Canonicalization', t => { + t.is(key.kid, 'kPrK_qmxVWaYVA9wwBF6Iuo3vVzz7TxHCTwXBygrS4k') +}) + +const keystoreEmpty = new KeyStore() +const keystoreMatchOne = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use }), key) +const keystoreMatchMore = new KeyStore(generateSync(key.kty, key.length, { alg: key.alg, use: key.use, kid: key.kid }), key, importKey(key)) +const keystoreMatchNone = new KeyStore(generateSync('EC'), generateSync('RSA')) + +test(`${recipe.title} - compact sign`, t => { + t.is(JWS.sign(payload, key, header), recipe.output.compact) +}) + +test(`${recipe.title} - flattened sign`, t => { + t.deepEqual(JWS.sign.flattened(payload, key, header), recipe.output.json_flat) +}) + +test(`${recipe.title} - general sign`, t => { + t.deepEqual(JWS.sign.general(payload, key, header), recipe.output.json) +}) + +test(`${recipe.title} - compact verify`, t => { + t.is(JWS.verify(recipe.output.compact, key), payload) +}) + +test(`${recipe.title} - flattened verify`, t => { + t.is(JWS.verify(recipe.output.json_flat, key), payload) +}) + +test(`${recipe.title} - general verify`, t => { + t.is(JWS.verify(recipe.output.json, key), payload) +}) + +;[keystoreMatchOne, keystoreMatchMore].forEach((keystore, i) => { + test(`${recipe.title} - compact verify (using keystore ${i + 1}/2)`, t => { + t.is(JWS.verify(recipe.output.compact, keystore), payload) + }) + + test(`${recipe.title} - flattened verify (using keystore ${i + 1}/2)`, t => { + t.is(JWS.verify(recipe.output.json_flat, keystore), payload) + }) + + test(`${recipe.title} - general verify (using keystore ${i + 1}/2)`, t => { + t.is(JWS.verify(recipe.output.json, keystore), payload) + }) +}) + +test(`${recipe.title} - compact verify (failing)`, t => { + t.throws(() => { + JWS.verify(recipe.output.compact, keystoreMatchNone) + }, { instanceOf: errors.JWKSNoMatchingKey, code: 'ERR_JWKS_NO_MATCHING_KEY' }) +}) + +test(`${recipe.title} - flattened verify (failing)`, t => { + t.throws(() => { + JWS.verify(recipe.output.json_flat, keystoreMatchNone) + }, { instanceOf: errors.JWKSNoMatchingKey, code: 'ERR_JWKS_NO_MATCHING_KEY' }) +}) + +test(`${recipe.title} - general verify (failing)`, t => { + t.throws(() => { + JWS.verify(recipe.output.json, keystoreMatchNone) + }, { instanceOf: errors.JWKSNoMatchingKey, code: 'ERR_JWKS_NO_MATCHING_KEY' }) +}) + +test(`${recipe.title} - compact verify (using empty keystore)`, t => { + t.throws(() => { + JWS.verify(recipe.output.compact, keystoreEmpty) + }, { instanceOf: errors.JWKSNoMatchingKey, code: 'ERR_JWKS_NO_MATCHING_KEY', message: 'no matching key found in the KeyStore' }) +}) + +test(`${recipe.title} - flattened verify (using empty keystore)`, t => { + t.throws(() => { + JWS.verify(recipe.output.json_flat, keystoreEmpty) + }, { instanceOf: errors.JWKSNoMatchingKey, code: 'ERR_JWKS_NO_MATCHING_KEY', message: 'no matching key found in the KeyStore' }) +}) + +test(`${recipe.title} - general verify (using empty keystore)`, t => { + t.throws(() => { + JWS.verify(recipe.output.json, keystoreEmpty) + }, { instanceOf: errors.JWKSNoMatchingKey, code: 'ERR_JWKS_NO_MATCHING_KEY', message: 'no matching key found in the KeyStore' }) +}) diff --git a/test/fixtures/Ed25519.key b/test/fixtures/Ed25519.key new file mode 100644 index 0000000000..7075d804fc --- /dev/null +++ b/test/fixtures/Ed25519.key @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIGpAjr6yJyzkSQbv3KVV7KF7EQTe71Ty2SYB16rX3Dfu +-----END PRIVATE KEY----- diff --git a/test/fixtures/Ed25519.pem b/test/fixtures/Ed25519.pem new file mode 100644 index 0000000000..b6cd2f3d14 --- /dev/null +++ b/test/fixtures/Ed25519.pem @@ -0,0 +1,3 @@ +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEAhaCxp2RnxBQne/i7Xf9AUatVj3YFeBWfFZrT4cqVD3U= +-----END PUBLIC KEY----- diff --git a/test/fixtures/Ed448.key b/test/fixtures/Ed448.key new file mode 100644 index 0000000000..61bb533a31 --- /dev/null +++ b/test/fixtures/Ed448.key @@ -0,0 +1,4 @@ +-----BEGIN PRIVATE KEY----- +MEcCAQAwBQYDK2VxBDsEOco+BEKj1fxqSvUkMOJl6h6X4P2f4HWwFtUQ8MAMV18O +IUMV8/Cd21xvncdI1ElF9ZmjpC4CznRY1A== +-----END PRIVATE KEY----- diff --git a/test/fixtures/Ed448.pem b/test/fixtures/Ed448.pem new file mode 100644 index 0000000000..25301691ac --- /dev/null +++ b/test/fixtures/Ed448.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MEMwBQYDK2VxAzoArkqxt4T5h/1vH3rHp8QEMRuUIPLX+o88wsOu0VcHeX0QRMGk +cmvBcyURZbPjLjJ9x5XBLsQmwJEA +-----END PUBLIC KEY----- diff --git a/test/fixtures/X25519.key b/test/fixtures/X25519.key new file mode 100644 index 0000000000..b2f60cca40 --- /dev/null +++ b/test/fixtures/X25519.key @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VuBCIEILD/13Y5R/tmcCjZVSooIcpfGvZxf+qt6dMu5FYaOC1a +-----END PRIVATE KEY----- diff --git a/test/fixtures/X25519.pem b/test/fixtures/X25519.pem new file mode 100644 index 0000000000..3d1e7b835e --- /dev/null +++ b/test/fixtures/X25519.pem @@ -0,0 +1,3 @@ +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VuAyEAYHCXnz085FKclfnx+gdiGXAyy7BhJjx0pxyE4wbXF0A= +-----END PUBLIC KEY----- diff --git a/test/fixtures/X448.key b/test/fixtures/X448.key new file mode 100644 index 0000000000..39c507d2b8 --- /dev/null +++ b/test/fixtures/X448.key @@ -0,0 +1,4 @@ +-----BEGIN PRIVATE KEY----- +MEYCAQAwBQYDK2VvBDoEOPilLIAZTQqUbFb0LhTGaqn47zN2p2yGVk+2hhQQk9C8 +8SvFqEFw73YITSIJ2NUBZnZKNz2nGkrm +-----END PRIVATE KEY----- diff --git a/test/fixtures/X448.pem b/test/fixtures/X448.pem new file mode 100644 index 0000000000..841c2bb66b --- /dev/null +++ b/test/fixtures/X448.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MEIwBQYDK2VvAzkAbceBBM+LkveTK09QojZdnHokCh7lOWxyVZrlbH3Ny3WorprD +Iir5A6heZzlRnz1elOHp7ZpPfWk= +-----END PUBLIC KEY----- diff --git a/test/fixtures/index.js b/test/fixtures/index.js index 51cfeba94b..6fd9fc414a 100644 --- a/test/fixtures/index.js +++ b/test/fixtures/index.js @@ -20,6 +20,34 @@ module.exports.JWK = { qi: 'QrCoyZm-rco2Mziyfxdziaw2S8_rofiKXi7Qz6O5loSslYJtrIXq7w8MX-TVSt6r03lLbK9gthslPRPdp68wmH-By0mfw66JtuSKOAHdHWotFOwYvkkE76O4-eY78pTE9oEzu-lu309NSPSpADd58DIRYMqwuFhbLa35Yrw3TxU' }, + Ed25519: { + kty: 'OKP', + crv: 'Ed25519', + d: 'nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A', + x: '11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo' + }, + + Ed448: { + kty: 'OKP', + crv: 'Ed448', + d: 'wdwf3Gu5aZCJufcUrSkkvqJjqPCgyd6R5Cmx3zkNNh90JYYzOoXC7ptuxTTGWQumHeUjohkQyPT_', + x: 'NAh0EO9nwdXZkR_2KrY_2A66oH_654oEcoFbtUprlF8AvrXnQ0rlcg1VxJvlp85lR23CuX8jNnKA' + }, + + X25519: { + kty: 'OKP', + crv: 'X25519', + d: 'sP_XdjlH-2ZwKNlVKighyl8a9nF_6q3p0y7kVho4LVo', + x: 'YHCXnz085FKclfnx-gdiGXAyy7BhJjx0pxyE4wbXF0A' + }, + + X448: { + kty: 'OKP', + crv: 'X448', + d: 'bceBBM-LkveTK09QojZdnHokCh7lOWxyVZrlbH3Ny3WorprDIir5A6heZzlRnz1elOHp7ZpPfWk', + x: 'rmZOFmJPUVLlQDeG2_V4pgMmTidTtD_GGTq1gMKx9hJfAqTlC9K-qaJBhSYQtS1xHBkfUREKa3I' + }, + 'P-256': { kty: 'EC', crv: 'P-256', @@ -52,6 +80,22 @@ module.exports.PEM = { private: readFileSync(join(__dirname, 'rsa.key')), public: readFileSync(join(__dirname, 'rsa.pem')) }, + 'Ed25519': { + private: readFileSync(join(__dirname, 'Ed25519.key')), + public: readFileSync(join(__dirname, 'Ed25519.pem')) + }, + 'Ed448': { + private: readFileSync(join(__dirname, 'Ed448.key')), + public: readFileSync(join(__dirname, 'Ed448.pem')) + }, + 'X25519': { + private: readFileSync(join(__dirname, 'X25519.key')), + public: readFileSync(join(__dirname, 'X25519.pem')) + }, + 'X448': { + private: readFileSync(join(__dirname, 'X448.key')), + public: readFileSync(join(__dirname, 'X448.pem')) + }, 'P-256': { private: readFileSync(join(__dirname, 'P-256.key')), public: readFileSync(join(__dirname, 'P-256.pem')) diff --git a/test/help/key_utils.test.js b/test/help/key_utils.test.js index 023947ef96..f8d020b1d7 100644 --- a/test/help/key_utils.test.js +++ b/test/help/key_utils.test.js @@ -6,18 +6,24 @@ const { keyObjectToJWK, jwkToPem } = require('../../lib/help/key_utils') const { JWK: fixtures } = require('../fixtures') const clone = obj => JSON.parse(JSON.stringify(obj)) -test('jwkToPem only works for EC and RSA', t => { +test('jwkToPem only works for EC, RSA and OKP', t => { t.throws(() => { - jwkToPem({ kty: 'OKP' }) - }, { instanceOf: errors.JOSENotSupported, message: 'unsupported key type: OKP' }) + jwkToPem({ kty: 'foo' }) + }, { instanceOf: errors.JOSENotSupported, message: 'unsupported key type: foo' }) }) -test('jwkToPem only handles known curves', t => { +test('jwkToPem only handles known EC curves', t => { t.throws(() => { jwkToPem({ kty: 'EC', crv: 'foo' }) }, { instanceOf: errors.JOSENotSupported, message: 'unsupported EC key curve: foo' }) }) +test('jwkToPem only handles known OKP curves', t => { + t.throws(() => { + jwkToPem({ kty: 'OKP', crv: 'foo' }) + }, { instanceOf: errors.JOSENotSupported, message: 'unsupported OKP key curve: foo' }) +}) + test('RSA Public key', t => { const expected = fixtures.RSA_PUBLIC const pem = createPublicKey(jwkToPem(expected)) @@ -34,6 +40,74 @@ test('RSA Private key', t => { t.deepEqual(actual, expected) }) +test('Ed25519 Public key', t => { + const expected = clone(fixtures.Ed25519) + delete expected.d + const pem = createPublicKey(jwkToPem(expected)) + const actual = keyObjectToJWK(pem) + + t.deepEqual(actual, expected) +}) + +test('Ed25519 Private key', t => { + const expected = fixtures.Ed25519 + const pem = createPrivateKey(jwkToPem(expected)) + const actual = keyObjectToJWK(pem) + + t.deepEqual(actual, expected) +}) + +test('Ed448 Public key', t => { + const expected = clone(fixtures.Ed448) + delete expected.d + const pem = createPublicKey(jwkToPem(expected)) + const actual = keyObjectToJWK(pem) + + t.deepEqual(actual, expected) +}) + +test('Ed448 Private key', t => { + const expected = fixtures.Ed448 + const pem = createPrivateKey(jwkToPem(expected)) + const actual = keyObjectToJWK(pem) + + t.deepEqual(actual, expected) +}) + +test('X25519 Public key', t => { + const expected = clone(fixtures.X25519) + delete expected.d + const pem = createPublicKey(jwkToPem(expected)) + const actual = keyObjectToJWK(pem) + + t.deepEqual(actual, expected) +}) + +test('X25519 Private key', t => { + const expected = fixtures.X25519 + const pem = createPrivateKey(jwkToPem(expected)) + const actual = keyObjectToJWK(pem) + + t.deepEqual(actual, expected) +}) + +test('X448 Public key', t => { + const expected = clone(fixtures.X448) + delete expected.d + const pem = createPublicKey(jwkToPem(expected)) + const actual = keyObjectToJWK(pem) + + t.deepEqual(actual, expected) +}) + +test('X448 Private key', t => { + const expected = fixtures.X448 + const pem = createPrivateKey(jwkToPem(expected)) + const actual = keyObjectToJWK(pem) + + t.deepEqual(actual, expected) +}) + test('EC P-256 Public key', t => { const expected = clone(fixtures['P-256']) delete expected.d diff --git a/test/jwk/ec.test.js b/test/jwk/ec.test.js index 2f2dc0ab3d..ade0635ba7 100644 --- a/test/jwk/ec.test.js +++ b/test/jwk/ec.test.js @@ -24,12 +24,10 @@ test('Unusable with unsupported curves', t => { }) Object.entries({ - 'P-256': [256, 'rDd6H6t9-nJUoz72nTpz8tInvypVWhE2iQoPznj8ZY8'], - 'P-384': [384, '5gebayAhpztJCs4Pxo-z1hhsN0upoyG2NAoKpiiH2b0'], - 'P-521': [512, 'BQtkbSY3xgN4M2ZP3IHMLG7-Rp1L29teCMfNqgJHtTY'] -}).forEach(([crv, [len, kid]]) => { - const alg = `ES${len}` - + 'P-256': ['ES256', 'rDd6H6t9-nJUoz72nTpz8tInvypVWhE2iQoPznj8ZY8'], + 'P-384': ['ES384', '5gebayAhpztJCs4Pxo-z1hhsN0upoyG2NAoKpiiH2b0'], + 'P-521': ['ES512', 'BQtkbSY3xgN4M2ZP3IHMLG7-Rp1L29teCMfNqgJHtTY'] +}).forEach(([crv, [alg, kid]]) => { // private ;(() => { const keyObject = createPrivateKey(fixtures.PEM[crv].private) diff --git a/test/jwk/generate.test.js b/test/jwk/generate.test.js index d62e0571b8..b417c619ee 100644 --- a/test/jwk/generate.test.js +++ b/test/jwk/generate.test.js @@ -13,6 +13,25 @@ const { JWK: { generate, generateSync }, errors } = require('../..') ['RSA', 2048, { use: 'enc', alg: 'RSA-OAEP' }], ['RSA', 2048, { alg: 'PS256' }], ['RSA', 2048, { alg: 'RSA-OAEP' }], + ['OKP'], + ['OKP', undefined, undefined, true], + ['OKP', undefined, undefined, false], + ['OKP', 'Ed25519'], + ['OKP', 'Ed25519', { use: 'sig' }], + // ['OKP', 'Ed25519', { use: 'sig', alg: 'EdDSA' }], + // ['OKP', 'Ed25519', { alg: 'EdDSA' }], + ['OKP', 'Ed448'], + ['OKP', 'Ed448', { use: 'sig' }], + // ['OKP', 'Ed448', { use: 'sig', alg: 'EdDSA' }], + // ['OKP', 'Ed448', { alg: 'EdDSA' }], + ['OKP', 'X25519'], + ['OKP', 'X25519', { use: 'enc' }], + // ['OKP', 'X25519', { use: 'enc', alg: 'ECDH-ES' }], + // ['OKP', 'X25519', { alg: 'ECDH-ES' }], + ['OKP', 'X448'], + ['OKP', 'X448', { use: 'enc' }], + // ['OKP', 'X448', { use: 'enc', alg: 'ECDH-ES' }], + // ['OKP', 'X448', { alg: 'ECDH-ES' }], ['EC'], ['EC', undefined, undefined, true], ['EC', undefined, undefined, false], @@ -109,14 +128,26 @@ const { JWK: { generate, generateSync }, errors } = require('../..') test('fails to generateSync unsupported kty', t => { t.throws(() => { - generateSync('OKP') - }, { instanceOf: errors.JOSENotSupported, message: 'unsupported key type: OKP' }) + generateSync('foo') + }, { instanceOf: errors.JOSENotSupported, message: 'unsupported key type: foo' }) }) test('fails to generate unsupported kty', async t => { await t.throwsAsync(() => { - return generate('OKP') - }, { instanceOf: errors.JOSENotSupported, message: 'unsupported key type: OKP' }) + return generate('foo') + }, { instanceOf: errors.JOSENotSupported, message: 'unsupported key type: foo' }) +}) + +test('fails to generate unsupported OKP crv', async t => { + await t.throwsAsync(() => { + return generate('OKP', 'foo') + }, { instanceOf: errors.JOSENotSupported, message: 'unsupported OKP key curve: foo' }) +}) + +test('fails to generateSync unsupported OKP crv', async t => { + await t.throws(() => { + return generateSync('OKP', 'foo') + }, { instanceOf: errors.JOSENotSupported, message: 'unsupported OKP key curve: foo' }) }) test('fails to generateSync unsupported EC crv', t => { diff --git a/test/jwk/import.test.js b/test/jwk/import.test.js index ff8d00f377..cdd29dd01a 100644 --- a/test/jwk/import.test.js +++ b/test/jwk/import.test.js @@ -72,6 +72,6 @@ test('failed to import throws an error', t => { test(`fails to import unsupported PEM ${i + 1}/4`, t => { t.throws(() => { importKey(unsupported) - }, { instanceOf: errors.JOSENotSupported, code: 'ERR_JOSE_NOT_SUPPORTED', message: 'only RSA and EC asymmetric keys are supported' }) + }, { instanceOf: errors.JOSENotSupported, code: 'ERR_JOSE_NOT_SUPPORTED', message: 'only RSA, EC and OKP asymmetric keys are supported' }) }) }) diff --git a/test/jwk/okp_enc.test.js b/test/jwk/okp_enc.test.js new file mode 100644 index 0000000000..e26656360d --- /dev/null +++ b/test/jwk/okp_enc.test.js @@ -0,0 +1,163 @@ +const test = require('ava') +const { createPrivateKey, createPublicKey } = require('crypto') +const { hasProperty, hasNoProperties, hasProperties } = require('../macros') +const fixtures = require('../fixtures') + +const OKPKey = require('../../lib/jwk/key/okp') + +test(`OKP key .algorithms invalid operation`, t => { + const key = new OKPKey(createPrivateKey(fixtures.PEM['X25519'].private)) + t.throws(() => key.algorithms('foo'), { instanceOf: TypeError, message: 'invalid key operation' }) +}) + +Object.entries({ + X25519: 'P-c1F5P-1BckI7vasmrM8384J2IBYaYc_EtEXxOZYuI', + X448: 'a-2MwPMAhM3QY0zU0YBP9lzipRk67tsOY9uUhiT2Fos' +}).forEach(([crv, kid]) => { + const alg = 'ECDH-ES' + + // private + ;(() => { + const keyObject = createPrivateKey(fixtures.PEM[crv].private) + const key = new OKPKey(keyObject) + + test(`${crv} OKP Private key (with alg)`, hasProperty, new OKPKey(keyObject, { alg }), 'alg', alg) + test(`${crv} OKP Private key (with kid)`, hasProperty, new OKPKey(keyObject, { kid: 'foobar' }), 'kid', 'foobar') + test(`${crv} OKP Private key (with use)`, hasProperty, new OKPKey(keyObject, { use: 'enc' }), 'use', 'enc') + test(`${crv} OKP Private key`, hasNoProperties, key, 'k', 'e', 'n', 'p', 'q', 'dp', 'dq', 'qi', 'y') + test(`${crv} OKP Private key`, hasProperties, key, 'x', 'd') + test(`${crv} OKP Private key`, hasProperty, key, 'alg', undefined) + test(`${crv} OKP Private key`, hasProperty, key, 'kid', kid) + test(`${crv} OKP Private key`, hasProperty, key, 'kty', 'OKP') + test(`${crv} OKP Private key`, hasProperty, key, 'private', true) + test(`${crv} OKP Private key`, hasProperty, key, 'public', false) + test(`${crv} OKP Private key`, hasProperty, key, 'secret', false) + test(`${crv} OKP Private key`, hasProperty, key, 'type', 'private') + test(`${crv} OKP Private key`, hasProperty, key, 'use', undefined) + + test(`${crv} OKP Private key algorithms (no operation)`, t => { + const result = key.algorithms() + t.is(result.constructor, Set) + t.deepEqual([...result], [])//, 'ECDH-ES', 'ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW']) + }) + + test(`${crv} OKP Private key algorithms (no operation, w/ alg)`, t => { + const key = new OKPKey(keyObject, { alg }) + const result = key.algorithms() + t.is(result.constructor, Set) + t.deepEqual([...result], [])// [alg]) + }) + + test(`${crv} OKP Private key does not support sign alg (no use)`, t => { + const result = key.algorithms('sign') + t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + + test(`${crv} OKP Private key does not support verify alg (no use)`, t => { + const result = key.algorithms('verify') + t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + + test(`${crv} OKP Private key .algorithms("encrypt")`, t => { + const result = key.algorithms('encrypt') + t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + + test(`${crv} OKP Private key .algorithms("decrypt")`, t => { + const result = key.algorithms('decrypt') + t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + + test(`${crv} OKP Private key .algorithms("wrapKey")`, t => { + const result = key.algorithms('wrapKey') + t.is(result.constructor, Set) + t.deepEqual([...result], [])// ['ECDH-ES', 'ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW']) + }) + + test(`${crv} OKP Private key .algorithms("wrapKey") when use is sig`, t => { + const sigKey = new OKPKey(keyObject, { use: 'sig' }) + const result = sigKey.algorithms('wrapKey') + t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + + test(`${crv} OKP 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']) + }) + })() + + // public + ;(() => { + const keyObject = createPublicKey(fixtures.PEM[crv].public) + const key = new OKPKey(keyObject) + + test(`${crv} OKP Public key (with alg)`, hasProperty, new OKPKey(keyObject, { alg }), 'alg', alg) + test(`${crv} OKP Public key (with kid)`, hasProperty, new OKPKey(keyObject, { kid: 'foobar' }), 'kid', 'foobar') + test(`${crv} OKP Public key (with use)`, hasProperty, new OKPKey(keyObject, { use: 'sig' }), 'use', 'sig') + test(`${crv} OKP Public key`, hasNoProperties, key, 'k', 'e', 'n', 'p', 'q', 'dp', 'dq', 'qi', 'd', 'y') + test(`${crv} OKP Public key`, hasProperties, key, 'x') + test(`${crv} OKP Public key`, hasProperty, key, 'alg', undefined) + test(`${crv} OKP Public key`, hasProperty, key, 'kid', kid) + test(`${crv} OKP Public key`, hasProperty, key, 'kty', 'OKP') + test(`${crv} OKP Public key`, hasProperty, key, 'private', false) + test(`${crv} OKP Public key`, hasProperty, key, 'public', true) + test(`${crv} OKP Public key`, hasProperty, key, 'secret', false) + test(`${crv} OKP Public key`, hasProperty, key, 'type', 'public') + test(`${crv} OKP Public key`, hasProperty, key, 'use', undefined) + + test(`${crv} OKP Public key algorithms (no operation)`, t => { + const result = key.algorithms() + t.is(result.constructor, Set) + t.deepEqual([...result], [])//, 'ECDH-ES', 'ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW']) + }) + + test(`${crv} OKP Public key algorithms (no operation, w/ alg)`, t => { + const key = new OKPKey(keyObject, { alg }) + const result = key.algorithms() + t.is(result.constructor, Set) + t.deepEqual([...result], [])// [alg]) + }) + + test(`${crv} OKP Public key cannot sign`, t => { + const result = key.algorithms('sign') + t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + + test(`${crv} OKP Public key does not support verify alg (no use)`, t => { + const result = key.algorithms('verify') + t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + + test(`${crv} OKP Public key .algorithms("encrypt")`, t => { + const result = key.algorithms('encrypt') + t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + + test(`${crv} OKP Public key .algorithms("decrypt")`, t => { + const result = key.algorithms('decrypt') + t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + + test(`${crv} OKP Public key .algorithms("wrapKey")`, t => { + const result = key.algorithms('wrapKey') + t.is(result.constructor, Set) + t.deepEqual([...result], [])// ['ECDH-ES', 'ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW']) + }) + + test(`${crv} OKP Public key .algorithms("unwrapKey")`, t => { + const result = key.algorithms('unwrapKey') + t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + })() +}) diff --git a/test/jwk/okp_sig.test.js b/test/jwk/okp_sig.test.js new file mode 100644 index 0000000000..03c999bcf4 --- /dev/null +++ b/test/jwk/okp_sig.test.js @@ -0,0 +1,233 @@ +const test = require('ava') +const { createPrivateKey, createPublicKey } = require('crypto') +const { hasProperty, hasNoProperties, hasProperties } = require('../macros') +const fixtures = require('../fixtures') + +const OKPKey = require('../../lib/jwk/key/okp') + +test(`OKP key .algorithms invalid operation`, t => { + const key = new OKPKey(createPrivateKey(fixtures.PEM['Ed25519'].private)) + t.throws(() => key.algorithms('foo'), { instanceOf: TypeError, message: 'invalid key operation' }) +}) + +Object.entries({ + Ed25519: 'YeOxXoX_a0317nVDSwtlinj0RuJnSI0lYnxCM6qSC4c', + Ed448: 'eaEfshTya3PWdLWK4CfotnZcHKNJbpQviiTOqwOyFfE' +}).forEach(([crv, kid]) => { + const alg = 'EdDSA' + + // private + ;(() => { + const keyObject = createPrivateKey(fixtures.PEM[crv].private) + const key = new OKPKey(keyObject) + + test(`${crv} OKP Private key (with alg)`, hasProperty, new OKPKey(keyObject, { alg }), 'alg', alg) + test(`${crv} OKP Private key (with kid)`, hasProperty, new OKPKey(keyObject, { kid: 'foobar' }), 'kid', 'foobar') + test(`${crv} OKP Private key (with use)`, hasProperty, new OKPKey(keyObject, { use: 'sig' }), 'use', 'sig') + test(`${crv} OKP Private key`, hasNoProperties, key, 'k', 'e', 'n', 'p', 'q', 'dp', 'dq', 'qi', 'y') + test(`${crv} OKP Private key`, hasProperties, key, 'x', 'd') + test(`${crv} OKP Private key`, hasProperty, key, 'alg', undefined) + test(`${crv} OKP Private key`, hasProperty, key, 'kid', kid) + test(`${crv} OKP Private key`, hasProperty, key, 'kty', 'OKP') + test(`${crv} OKP Private key`, hasProperty, key, 'private', true) + test(`${crv} OKP Private key`, hasProperty, key, 'public', false) + test(`${crv} OKP Private key`, hasProperty, key, 'secret', false) + test(`${crv} OKP Private key`, hasProperty, key, 'type', 'private') + test(`${crv} OKP Private key`, hasProperty, key, 'use', undefined) + + test(`${crv} OKP Private key algorithms (no operation)`, t => { + const result = key.algorithms() + t.is(result.constructor, Set) + t.deepEqual([...result], [alg]) + }) + + test(`${crv} OKP Private key algorithms (no operation, w/ alg)`, t => { + const key = new OKPKey(keyObject, { alg }) + const result = key.algorithms() + t.is(result.constructor, Set) + t.deepEqual([...result], [alg]) + }) + + test(`${crv} OKP Private key supports sign alg (no use)`, t => { + const result = key.algorithms('sign') + t.is(result.constructor, Set) + t.deepEqual([...result], [alg]) + }) + + test(`${crv} OKP Private key supports verify alg (no use)`, t => { + const result = key.algorithms('verify') + t.is(result.constructor, Set) + t.deepEqual([...result], [alg]) + }) + + test(`${crv} OKP Private key supports sign alg when \`use\` is "sig")`, t => { + const sigKey = new OKPKey(keyObject, { use: 'sig' }) + const result = sigKey.algorithms('sign') + t.is(result.constructor, Set) + t.deepEqual([...result], [alg]) + }) + + test(`${crv} OKP Private key supports verify alg when \`use\` is "sig")`, t => { + const sigKey = new OKPKey(keyObject, { use: 'sig' }) + const result = sigKey.algorithms('verify') + t.is(result.constructor, Set) + t.deepEqual([...result], [alg]) + }) + + test(`${crv} OKP Private key supports single sign alg when \`alg\` is set)`, t => { + const sigKey = new OKPKey(keyObject, { alg }) + const result = sigKey.algorithms('sign') + t.is(result.constructor, Set) + t.deepEqual([...result], [alg]) + }) + + test(`${crv} OKP Private key supports single verify alg when \`alg\` is set)`, t => { + const sigKey = new OKPKey(keyObject, { alg }) + const result = sigKey.algorithms('verify') + t.is(result.constructor, Set) + t.deepEqual([...result], [alg]) + }) + + test(`${crv} OKP Private key .algorithms("encrypt")`, t => { + const result = key.algorithms('encrypt') + t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + + test(`${crv} OKP Private key .algorithms("decrypt")`, t => { + const result = key.algorithms('decrypt') + t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + + test(`${crv} OKP Private key .algorithms("wrapKey")`, t => { + const result = key.algorithms('wrapKey') + t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + + test(`${crv} OKP Private key .algorithms("wrapKey") when use is sig`, t => { + const sigKey = new OKPKey(keyObject, { use: 'sig' }) + const result = sigKey.algorithms('wrapKey') + t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + + test(`${crv} OKP Private key .algorithms("unwrapKey")`, t => { + const result = key.algorithms('unwrapKey') + t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + + test(`${crv} OKP Private key .algorithms("unwrapKey") when use is sig`, t => { + const sigKey = new OKPKey(keyObject, { use: 'sig' }) + const result = sigKey.algorithms('unwrapKey') + t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + })() + + // public + ;(() => { + const keyObject = createPublicKey(fixtures.PEM[crv].public) + const key = new OKPKey(keyObject) + + test(`${crv} OKP Public key (with alg)`, hasProperty, new OKPKey(keyObject, { alg }), 'alg', alg) + test(`${crv} OKP Public key (with kid)`, hasProperty, new OKPKey(keyObject, { kid: 'foobar' }), 'kid', 'foobar') + test(`${crv} OKP Public key (with use)`, hasProperty, new OKPKey(keyObject, { use: 'sig' }), 'use', 'sig') + test(`${crv} OKP Public key`, hasNoProperties, key, 'k', 'e', 'n', 'p', 'q', 'dp', 'dq', 'qi', 'd', 'y') + test(`${crv} OKP Public key`, hasProperties, key, 'x') + test(`${crv} OKP Public key`, hasProperty, key, 'alg', undefined) + test(`${crv} OKP Public key`, hasProperty, key, 'kid', kid) + test(`${crv} OKP Public key`, hasProperty, key, 'kty', 'OKP') + test(`${crv} OKP Public key`, hasProperty, key, 'private', false) + test(`${crv} OKP Public key`, hasProperty, key, 'public', true) + test(`${crv} OKP Public key`, hasProperty, key, 'secret', false) + test(`${crv} OKP Public key`, hasProperty, key, 'type', 'public') + test(`${crv} OKP Public key`, hasProperty, key, 'use', undefined) + + test(`${crv} OKP Public key algorithms (no operation)`, t => { + const result = key.algorithms() + t.is(result.constructor, Set) + t.deepEqual([...result], [alg]) + }) + + test(`${crv} OKP Public key algorithms (no operation, w/ alg)`, t => { + const key = new OKPKey(keyObject, { alg }) + const result = key.algorithms() + t.is(result.constructor, Set) + t.deepEqual([...result], [alg]) + }) + + test(`${crv} OKP Public key cannot sign`, t => { + const result = key.algorithms('sign') + t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + + test(`${crv} OKP Public key supports verify alg (no use)`, t => { + const result = key.algorithms('verify') + t.is(result.constructor, Set) + t.deepEqual([...result], [alg]) + }) + + test(`${crv} OKP Public key cannot sign even when \`use\` is "sig")`, t => { + const sigKey = new OKPKey(keyObject, { use: 'sig' }) + const result = sigKey.algorithms('sign') + t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + + test(`${crv} OKP Public key supports verify alg when \`use\` is "sig")`, t => { + const sigKey = new OKPKey(keyObject, { use: 'sig' }) + const result = sigKey.algorithms('verify') + t.is(result.constructor, Set) + t.deepEqual([...result], [alg]) + }) + + test(`${crv} OKP Public key cannot sign even when \`alg\` is set)`, t => { + const sigKey = new OKPKey(keyObject, { alg }) + const result = sigKey.algorithms('sign') + t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + + test(`${crv} OKP Public key supports single verify alg when \`alg\` is set)`, t => { + const sigKey = new OKPKey(keyObject, { alg }) + const result = sigKey.algorithms('verify') + t.is(result.constructor, Set) + t.deepEqual([...result], [alg]) + }) + + test(`${crv} OKP Public key .algorithms("encrypt")`, t => { + const result = key.algorithms('encrypt') + t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + + test(`${crv} OKP Public key .algorithms("decrypt")`, t => { + const result = key.algorithms('decrypt') + t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + + test(`${crv} OKP Public key .algorithms("wrapKey")`, t => { + const result = key.algorithms('wrapKey') + t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + + test(`${crv} OKP Public key .algorithms("wrapKey") when use is sig`, t => { + const sigKey = new OKPKey(keyObject, { use: 'sig' }) + const result = sigKey.algorithms('wrapKey') + t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + + test(`${crv} OKP Public key .algorithms("unwrapKey")`, t => { + const result = key.algorithms('unwrapKey') + t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + })() +}) From 1159b0df4e415deeb44a856a55413732f9fc2552 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Mon, 1 Apr 2019 20:24:22 +0200 Subject: [PATCH 2/9] feat: add key.toPEM() export function with optional encryption --- docs/README.md | 39 +++++++++++++++++++++++++++++++++++++++ lib/help/key_utils.js | 8 ++++---- lib/index.d.ts | 7 +++++++ lib/jwa/ecdh/derive.js | 4 ++-- lib/jwk/key/base.js | 31 +++++++++++++++++++++++++++++++ test/cookbook/jwk.test.js | 34 ++++++++++++++++++++++++++++++++++ 6 files changed, 117 insertions(+), 6 deletions(-) diff --git a/docs/README.md b/docs/README.md index d00374ab90..ee4003485d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -36,6 +36,7 @@ I can continue maintaining it and adding new features carefree. You may also don - [key.secret](#keysecret) - [key.algorithms([operation])](#keyalgorithmsoperation) - [key.toJWK([private])](#keytojwkprivate) + - [key.toPEM([private[, encoding]])](#keytopemprivate-encoding) - JWK.importKey - [JWK.importKey(key[, options]) asymmetric key import](#jwkimportkeykey-options-asymmetric-key-import) - [JWK.importKey(secret[, options]) secret key import](#jwkimportkeysecret-options-secret-key-import) @@ -245,6 +246,44 @@ key.toJWK(true) --- +#### `key.toPEM([private[, encoding]])` + +Exports an asymmetric key as a PEM string with specified encoding and optional encryption for private keys. + +- `private`: `` When true exports keys as private. **Default:** 'false' +- `encoding`: `` See below +- Returns: `` + +For public key export, the following encoding options can be used: + +- `type`: `` Must be one of 'pkcs1' (RSA only) or 'spki'. **Default:** 'spki' + + +For private key export, the following encoding options can be used: + +- `type`: `` Must be one of 'pkcs1' (RSA only), 'pkcs8' or 'sec1' (EC only). **Default:** 'pkcs8' +- `cipher`: `` If specified, the private key will be encrypted with the given cipher and + passphrase using PKCS#5 v2.0 password based encryption. **Default**: 'undefined' (no encryption) +- `passphrase`: `` | `` The passphrase to use for encryption. **Default**: 'undefined' (no encryption) + +
+ Example (Click to expand) + +```js +const { JWK: { generateSync } } = require('@panva/jose') + +const key = generateSync('RSA', 2048) +key.toPEM() +// -----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEATPpxgDY7XU8cYX9Rb44xxXDO6zP\nzELVOHTcutCiXS9HZvUrZsnG7U/SPj0AT1hsH6lTUK4uFr7GG7KWgsf1Aw==\n-----END PUBLIC KEY-----\n +key.toPEM(true) +// -----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgUdAzlvX4i+RJS2BL\nQrqRj/ndTbpqugX61Ih9X+rvAcShRANCAAQBM+nGANjtdTxxhf1FvjjHFcM7rM/M\nQtU4dNy60KJdL0dm9StmycbtT9I+PQBPWGwfqVNQri4WvsYbspaCx/UD\n-----END PRIVATE KEY-----\n +key.toPEM(true, { passphrase: 'super-strong', cipher: 'aes-256-cbc' }) +// -----BEGIN ENCRYPTED PRIVATE KEY-----\nMIHsMFcGCSqGSIb3DQEFDTBKMCkGCSqGSIb3DQEFDDAcBAjjeqsgorjSqwICCAAw\nDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEJFcyG1ZBe2FZuvXIqiRFUcEgZD5\nWzt2XIUGIEZQIUUpJ1naaIFKiZvBcFAXhqG5KJ6PgaohgcmRUK8OZTA9Ome+uXB+\n9PLLfKscOsyr0gkd45gYYNRDLYwbQSqDQ4g8pHrCVjR+R3mh1nk8jIkOxSppwzmF\n7aoCmnQo7oXRy1+kRZL7OfwAD5gAXnsIA42D9RgOG1XIiBYTvAITcFVX0UPh0zM=\n-----END ENCRYPTED PRIVATE KEY-----\n +``` +
+ +--- + #### `JWK.importKey(key[, options])` asymmetric key import Imports an asymmetric private or public key. Supports importing JWK formatted keys (private, public, diff --git a/lib/help/key_utils.js b/lib/help/key_utils.js index 869f3c38d2..7da8adf5c9 100644 --- a/lib/help/key_utils.js +++ b/lib/help/key_utils.js @@ -198,7 +198,7 @@ const jwkToPem = { dp: base64url.decodeToBuffer(jwk.dp), dq: base64url.decodeToBuffer(jwk.dq), qi: base64url.decodeToBuffer(jwk.qi) - }, 'pem', { label: 'RSA PRIVATE KEY' }).toString('base64') + }, 'pem', { label: 'RSA PRIVATE KEY' }) }, public (jwk) { const RSAPublicKey = asn1.get('RSAPublicKey') @@ -207,7 +207,7 @@ const jwkToPem = { version: 0, n: base64url.decodeToBuffer(jwk.n), e: base64url.decodeToBuffer(jwk.e) - }, 'pem', { label: 'RSA PUBLIC KEY' }).toString('base64') + }, 'pem', { label: 'RSA PUBLIC KEY' }) } }, EC: { @@ -222,7 +222,7 @@ const jwkToPem = { value: crvToOid.get(jwk.crv) }, publicKey: concatEcPublicKey(jwk.x, jwk.y) - }, 'pem', { label: 'EC PRIVATE KEY' }).toString('base64') + }, 'pem', { label: 'EC PRIVATE KEY' }) }, public (jwk) { const PublicKeyInfo = asn1.get('PublicKeyInfo') @@ -233,7 +233,7 @@ const jwkToPem = { parameters: crvToOidBuf.get(jwk.crv) }, publicKey: concatEcPublicKey(jwk.x, jwk.y) - }, 'pem', { label: 'PUBLIC KEY' }).toString('base64') + }, 'pem', { label: 'PUBLIC KEY' }) } }, OKP: { diff --git a/lib/index.d.ts b/lib/index.d.ts index 93a1514e14..2d69896b26 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -17,6 +17,12 @@ type keyObjectTypes = asymmetricKeyObjectTypes | 'secret' export namespace JWK { + interface pemEncodingOptions { + type?: string + cipher?: string + passphrase?: string + } + class Key { kty: keyType type: keyObjectTypes @@ -28,6 +34,7 @@ export namespace JWK { kid: string thumbprint: string + toPEM(private?: boolean, encoding?: pemEncodingOptions): string algorithms(operation?: keyOperation): Set } diff --git a/lib/jwa/ecdh/derive.js b/lib/jwa/ecdh/derive.js index c69eccb8d9..a9debd8d1b 100644 --- a/lib/jwa/ecdh/derive.js +++ b/lib/jwa/ecdh/derive.js @@ -24,8 +24,8 @@ const computeSecret = ({ crv, d }, { x, y = '' }) => { const exchange = createECDH(curve) exchange.setPrivateKey(base64url.decodeToBuffer(d)) - let secret = exchange.computeSecret(pubToBuffer(x, y)) - return secret + + return exchange.computeSecret(pubToBuffer(x, y)) } const concat = (key, length, value) => { diff --git a/lib/jwk/key/base.js b/lib/jwk/key/base.js index 8ce098cbf4..785d61cf1f 100644 --- a/lib/jwk/key/base.js +++ b/lib/jwk/key/base.js @@ -1,3 +1,5 @@ +const { createPublicKey } = require('crypto') + const { keyObjectToJWK } = require('../../help/key_utils') const { THUMBPRINT_MATERIAL, PUBLIC_MEMBERS, PRIVATE_MEMBERS, JWK_MEMBERS } = require('../../help/symbols') const { KEYOBJECT } = require('../../help/symbols') @@ -54,6 +56,35 @@ class Key { }) } + toPEM (priv = false, encoding = {}) { + if (this.secret) { + throw new TypeError('symmetric keys cannot be exported as PEM') + } + + if (priv && this.public === true) { + throw new TypeError('public key cannot be exported as private') + } + + const { type = priv ? 'pkcs8' : 'spki', cipher, passphrase } = encoding + + let keyObject = this[KEYOBJECT] + + if (!priv) { + if (this.private) { + keyObject = createPublicKey(keyObject) + } + if (cipher || passphrase) { + throw new TypeError('cipher and passphrase can only be applied when exporting private keys') + } + } + + if (priv) { + return keyObject.export({ format: 'pem', type, cipher, passphrase }) + } + + return keyObject.export({ format: 'pem', type }) + } + toJWK (priv = false) { if (priv && this.public === true) { throw new TypeError('public key cannot be exported as private') diff --git a/test/cookbook/jwk.test.js b/test/cookbook/jwk.test.js index ee97e2f762..23aac3c72b 100644 --- a/test/cookbook/jwk.test.js +++ b/test/cookbook/jwk.test.js @@ -7,35 +7,66 @@ const { JWK: { importKey }, JWKS: { KeyStore } } = require('../..') test('public EC', t => { const jwk = recipes.get('3.1') const key = importKey(jwk) + t.true(key.toPEM().includes('BEGIN PUBLIC KEY')) t.deepEqual(key.toJWK(), jwk) t.deepEqual(key.toJWK(false), jwk) t.throws(() => { key.toJWK(true) }, { instanceOf: TypeError, message: 'public key cannot be exported as private' }) + t.throws(() => { + key.toPEM(false, { cipher: 'aes-256-cbc' }) + }, { instanceOf: TypeError, message: 'cipher and passphrase can only be applied when exporting private keys' }) + t.throws(() => { + key.toPEM(false, { passphrase: 'top secret' }) + }, { instanceOf: TypeError, message: 'cipher and passphrase can only be applied when exporting private keys' }) + t.throws(() => { + key.toPEM(true) + }, { instanceOf: TypeError, message: 'public key cannot be exported as private' }) }) test('private EC', t => { const jwk = recipes.get('3.2') const key = importKey(jwk) + t.true(key.toPEM(true, { cipher: 'aes-256-cbc', passphrase: 'top secret' }).includes('BEGIN ENCRYPTED PRIVATE KEY')) + t.true(key.toPEM(true, { type: 'sec1' }).includes('BEGIN EC PRIVATE KEY')) + t.true(key.toPEM(true, { type: 'sec1', cipher: 'aes-256-cbc', passphrase: 'top secret' }).includes('ENCRYPTED')) + t.true(key.toPEM(true).includes('BEGIN PRIVATE KEY')) + t.true(key.toPEM().includes('BEGIN PUBLIC KEY')) t.deepEqual(key.toJWK(true), jwk) const { d, ...pub } = jwk t.deepEqual(key.toJWK(), pub) t.deepEqual(key.toJWK(false), pub) + t.throws(() => { + key.toPEM(false, { cipher: 'aes-256-cbc' }) + }, { instanceOf: TypeError, message: 'cipher and passphrase can only be applied when exporting private keys' }) + t.throws(() => { + key.toPEM(false, { passphrase: 'top secret' }) + }, { instanceOf: TypeError, message: 'cipher and passphrase can only be applied when exporting private keys' }) }) test('public RSA', t => { const jwk = recipes.get('3.3') const key = importKey(jwk) + t.true(key.toPEM().includes('BEGIN PUBLIC KEY')) t.deepEqual(key.toJWK(), jwk) t.deepEqual(key.toJWK(false), jwk) t.throws(() => { key.toJWK(true) }, { instanceOf: TypeError, message: 'public key cannot be exported as private' }) + t.throws(() => { + key.toPEM(true) + }, { instanceOf: TypeError, message: 'public key cannot be exported as private' }) }) test('private RSA', t => { const jwk = recipes.get('3.4') const key = importKey(jwk) + t.true(key.toPEM(true, { type: 'pkcs1' }).includes('BEGIN RSA PRIVATE KEY')) + t.true(key.toPEM(true, { cipher: 'aes-256-cbc', passphrase: 'top secret', type: 'pkcs1' }).includes('ENCRYPTED')) + t.true(key.toPEM(true, { type: 'pkcs1', cipher: 'aes-256-cbc', passphrase: 'top secret' }).includes('BEGIN RSA PRIVATE KEY')) + t.true(key.toPEM(true, { cipher: 'aes-256-cbc', passphrase: 'top secret' }).includes('BEGIN ENCRYPTED PRIVATE KEY')) + t.true(key.toPEM(true).includes('BEGIN PRIVATE KEY')) + t.true(key.toPEM().includes('BEGIN PUBLIC KEY')) t.deepEqual(key.toJWK(true), jwk) const { d, dp, dq, p, q, qi, ...pub } = jwk t.deepEqual(key.toJWK(), pub) @@ -45,6 +76,9 @@ test('private RSA', t => { test('oct (1/2)', t => { const jwk = recipes.get('3.5') const key = importKey(jwk) + t.throws(() => { + key.toPEM() + }, { instanceOf: TypeError, message: 'symmetric keys cannot be exported as PEM' }) t.deepEqual(key.toJWK(true), jwk) const { k, ...pub } = jwk t.deepEqual(key.toJWK(), pub) From a8ef20e45051f83ccd5b72f49aa3ec4a2f07375f Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 3 Apr 2019 14:35:17 +0200 Subject: [PATCH 3/9] refactor: use private instance fields where possible --- lib/errors.js | 7 ++-- lib/jwe/encrypt.js | 81 ++++++++++++++++++++++---------------------- lib/jwks/keystore.js | 20 +++++------ lib/jws/sign.js | 31 ++++++++--------- 4 files changed, 70 insertions(+), 69 deletions(-) diff --git a/lib/errors.js b/lib/errors.js index f483cdd528..a6a345c08f 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -35,18 +35,19 @@ class JOSEError extends Error { } const isMulti = e => e instanceof JOSEMultiError -const ERRORS = Symbol('ERRORS') class JOSEMultiError extends JOSEError { + #errors + constructor (errors) { super() let i while ((i = errors.findIndex(isMulti)) && i !== -1) { errors.splice(i, 1, ...errors[i]) } - this[ERRORS] = errors + this.#errors = errors } * [Symbol.iterator] () { - for (const error of this[ERRORS]) { + for (const error of this.#errors) { yield error } } diff --git a/lib/jwe/encrypt.js b/lib/jwe/encrypt.js index 65ffc70d8d..3f0607c290 100644 --- a/lib/jwe/encrypt.js +++ b/lib/jwe/encrypt.js @@ -15,15 +15,16 @@ const serializers = require('./serializers') const generateCEK = require('./generate_cek') const validateHeaders = require('./validate_headers') -const AAD = Symbol('AAD') -const CEK = Symbol('CEK') -const CLEARTEXT = Symbol('CLEARTEXT') const PROCESS_RECIPIENT = Symbol('PROCESS_RECIPIENT') -const PROTECTED = Symbol('PROTECTED') -const RECIPIENTS = Symbol('RECIPIENTS') -const UNPROTECTED = Symbol('UNPROTECTED') class Encrypt { + #aad + #cek + #unprotected + #protected + #cleartext + #recipients + constructor (cleartext, protectedHeader, unprotectedHeader, aad) { if (!Buffer.isBuffer(cleartext) && typeof cleartext !== 'string') { throw new TypeError('cleartext argument must be a Buffer or a string') @@ -43,13 +44,11 @@ class Encrypt { throw new TypeError('unprotectedHeader argument must be a plain object when provided') } - Object.assign(this, { - [CLEARTEXT]: cleartext, - [RECIPIENTS]: [], - [PROTECTED]: protectedHeader ? deepClone(protectedHeader) : undefined, - [UNPROTECTED]: unprotectedHeader ? deepClone(unprotectedHeader) : undefined, - [AAD]: aad - }) + this.#recipients = [] + this.#cleartext = cleartext + this.#aad = aad + this.#unprotected = unprotectedHeader ? deepClone(unprotectedHeader) : undefined + this.#protected = protectedHeader ? deepClone(protectedHeader) : undefined } /* @@ -64,7 +63,7 @@ class Encrypt { throw new TypeError('header argument must be a plain object when provided') } - this[RECIPIENTS].push({ + this.#recipients.push({ key, header: header ? deepClone(header) : undefined }) @@ -76,7 +75,9 @@ class Encrypt { * @private */ [PROCESS_RECIPIENT] (recipient) { - const { [PROTECTED]: protectedHeader, [UNPROTECTED]: unprotectedHeader, [RECIPIENTS]: { length: recipientCount } } = this + const unprotectedHeader = this.#unprotected + const protectedHeader = this.#protected + const { length: recipientCount } = this.#recipients const jweHeader = { ...protectedHeader, @@ -107,7 +108,7 @@ class Encrypt { if (protectedHeader) { protectedHeader.alg = alg } else { - this[PROTECTED] = { alg } + this.#protected = { alg } } } else { if (recipient.header) { @@ -122,11 +123,11 @@ class Encrypt { let generatedHeader if (key.kty === 'oct' && alg === 'dir') { - this[CEK] = importKey(key[KEYOBJECT], { use: 'enc', alg: enc }) + 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 } = wrapKey(alg, key, this.#cek[KEYOBJECT].export(), { enc, alg })) if (alg === 'ECDH-ES') { - this[CEK] = importKey(createSecretKey(wrapped), { use: 'enc', alg: enc }) + this.#cek = importKey(createSecretKey(wrapped), { use: 'enc', alg: enc }) } } @@ -150,58 +151,58 @@ class Encrypt { throw new TypeError('serialization must be one of "compact", "flattened", "general"') } - if (!this[RECIPIENTS].length) { + if (!this.#recipients.length) { throw new JWEInvalid('missing recipients') } - serializer.validate(this[PROTECTED], this[UNPROTECTED], this[AAD], this[RECIPIENTS]) + serializer.validate(this.#protected, this.#unprotected, this.#aad, this.#recipients) - let enc = validateHeaders(this[PROTECTED], this[UNPROTECTED], this[RECIPIENTS], false, this[PROTECTED] ? this[PROTECTED].crit : undefined) + let enc = validateHeaders(this.#protected, this.#unprotected, this.#recipients, false, this.#protected ? this.#protected.crit : undefined) if (!enc) { enc = 'A128CBC-HS256' - if (this[PROTECTED]) { - this[PROTECTED].enc = enc + if (this.#protected) { + this.#protected.enc = enc } else { - this[PROTECTED] = { enc } + this.#protected = { enc } } } const final = {} - this[CEK] = generateCEK(enc) + this.#cek = generateCEK(enc) - this[RECIPIENTS].forEach(this[PROCESS_RECIPIENT].bind(this)) + this.#recipients.forEach(this[PROCESS_RECIPIENT].bind(this)) const iv = generateIV(enc) final.iv = base64url.encodeBuffer(iv) - if (this[RECIPIENTS].length === 1 && this[RECIPIENTS][0].generatedHeader) { - const [{ generatedHeader }] = this[RECIPIENTS] - delete this[RECIPIENTS][0].generatedHeader - this[PROTECTED] = Object.assign({}, this[PROTECTED], generatedHeader) + if (this.#recipients.length === 1 && this.#recipients[0].generatedHeader) { + const [{ generatedHeader }] = this.#recipients + delete this.#recipients[0].generatedHeader + this.#protected = Object.assign({}, this.#protected, generatedHeader) } - if (this[PROTECTED]) { - final.protected = base64url.JSON.encode(this[PROTECTED]) + if (this.#protected) { + final.protected = base64url.JSON.encode(this.#protected) } - final.unprotected = this[UNPROTECTED] + final.unprotected = this.#unprotected let aad - if (this[AAD]) { - final.aad = base64url.encode(this[AAD]) + if (this.#aad) { + final.aad = base64url.encode(this.#aad) aad = Buffer.concat([Buffer.from(final.protected || ''), Buffer.from('.'), Buffer.from(final.aad)]) } else { aad = Buffer.from(final.protected || '') } - let cleartext = this[CLEARTEXT] - if (this[PROTECTED] && 'zip' in this[PROTECTED]) { + let cleartext = this.#cleartext + if (this.#protected && 'zip' in this.#protected) { cleartext = deflateRawSync(cleartext) } - const { ciphertext, tag } = encrypt(enc, this[CEK], cleartext, { iv, aad }) + const { ciphertext, tag } = encrypt(enc, this.#cek, cleartext, { iv, aad }) final.tag = base64url.encodeBuffer(tag) final.ciphertext = base64url.encodeBuffer(ciphertext) - return serializer(final, this[RECIPIENTS]) + return serializer(final, this.#recipients) } } diff --git a/lib/jwks/keystore.js b/lib/jwks/keystore.js index 70c185e3bc..46e66aa99e 100644 --- a/lib/jwks/keystore.js +++ b/lib/jwks/keystore.js @@ -3,8 +3,6 @@ const { generate, generateSync } = require('../jwk/generate') const Key = require('../jwk/key/base') const importKey = require('../jwk/import') -const KEYS = Symbol('keys') - const keyscore = (key, { alg, kid, use }) => { let score = 0 @@ -24,6 +22,8 @@ const keyscore = (key, { alg, kid, use }) => { } class KeyStore { + #keys + constructor (...keys) { while (keys.some(Array.isArray)) { keys = keys.flat() @@ -32,7 +32,7 @@ class KeyStore { throw new TypeError('all keys must be an instances of a key instantiated by JWK.importKey') } - Object.defineProperty(this, KEYS, { value: new Set(keys) }) + this.#keys = new Set(keys); } static fromJWKS (jwks) { @@ -46,7 +46,7 @@ class KeyStore { } all ({ alg, kid, use, kty, operation } = {}) { - return [...this[KEYS]] + return [...this.#keys] .filter((key) => { let candidate = true @@ -80,7 +80,7 @@ class KeyStore { throw new TypeError('key must be an instance of a key instantiated by JWK.importKey') } - this[KEYS].add(key) + this.#keys.add(key) } remove (key) { @@ -88,23 +88,23 @@ class KeyStore { throw new TypeError('key must be an instance of a key instantiated by JWK.importKey') } - this[KEYS].delete(key) + this.#keys.delete(key) } toJWKS (priv = false) { - return { keys: [...this[KEYS].values()].map(key => key.toJWK(priv)) } + return { keys: [...this.#keys.values()].map(key => key.toJWK(priv)) } } async generate (...args) { - this[KEYS].add(await generate(...args)) + this.#keys.add(await generate(...args)) } generateSync (...args) { - this[KEYS].add(generateSync(...args)) + this.#keys.add(generateSync(...args)) } get size () { - return this[KEYS].size + return this.#keys.size } } diff --git a/lib/jws/sign.js b/lib/jws/sign.js index 9bae4cdd4a..06fa97f6ff 100644 --- a/lib/jws/sign.js +++ b/lib/jws/sign.js @@ -8,12 +8,13 @@ const { check, sign } = require('../jwa') const serializers = require('./serializers') -const RECIPIENTS = Symbol('RECIPIENTS') -const PAYLOAD = Symbol('PAYLOAD') const PROCESS_RECIPIENT = Symbol('PROCESS_RECIPIENT') -const B64 = Symbol('b64') class Sign { + #b64 + #payload + #recipients + constructor (payload) { if (typeof payload === 'string') { payload = base64url.encode(payload) @@ -25,10 +26,8 @@ class Sign { throw new TypeError('payload argument must be a Buffer, string or an object') } - Object.assign(this, { - [PAYLOAD]: payload, - [RECIPIENTS]: [] - }) + this.#payload = payload + this.#recipients = [] } /* @@ -51,7 +50,7 @@ class Sign { throw new JWSInvalid('JWS Protected and JWS Unprotected Header Parameter names must be disjoint') } - this[RECIPIENTS].push({ + this.#recipients.push({ key, protectedHeader: protectedHeader ? deepClone(protectedHeader) : undefined, unprotectedHeader: unprotectedHeader ? deepClone(unprotectedHeader) : undefined @@ -89,19 +88,19 @@ class Sign { } if (joseHeader.protected.crit && joseHeader.protected.crit.includes('b64')) { - if (B64 in this && this[B64] !== joseHeader.protected.b64) { + if (this.#b64 !== undefined && this.#b64 !== joseHeader.protected.b64) { throw new JWSInvalid('the "b64" Header Parameter value MUST be the same for all recipients') } else { - this[B64] = joseHeader.protected.b64 + this.#b64 = joseHeader.protected.b64 } if (!joseHeader.protected.b64) { - this[PAYLOAD] = base64url.decode(this[PAYLOAD]) + this.#payload = base64url.decode(this.#payload) } } recipient.header = unprotectedHeader recipient.protected = Object.keys(joseHeader.protected).length ? base64url.JSON.encode(joseHeader.protected) : '' - recipient.signature = base64url.encodeBuffer(sign(alg, key, Buffer.from(`${recipient.protected}.${this[PAYLOAD]}`))) + recipient.signature = base64url.encodeBuffer(sign(alg, key, Buffer.from(`${recipient.protected}.${this.#payload}`))) } /* @@ -113,15 +112,15 @@ class Sign { throw new TypeError('serialization must be one of "compact", "flattened", "general"') } - if (!this[RECIPIENTS].length) { + if (!this.#recipients.length) { throw new JWSInvalid('missing recipients') } - serializer.validate(this, this[RECIPIENTS]) + serializer.validate(this, this.#recipients) - this[RECIPIENTS].forEach(this[PROCESS_RECIPIENT].bind(this)) + this.#recipients.forEach(this[PROCESS_RECIPIENT].bind(this)) - return serializer(this[PAYLOAD], this[RECIPIENTS]) + return serializer(this.#payload, this.#recipients) } } From 4ace4be99df809afa76365532c912604411807c8 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 4 Apr 2019 11:41:36 +0200 Subject: [PATCH 4/9] test: check kid/thumbprint is not the same when kid is provided --- test/jwk/{sanity.test.js => general.test.js} | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) rename test/jwk/{sanity.test.js => general.test.js} (76%) diff --git a/test/jwk/sanity.test.js b/test/jwk/general.test.js similarity index 76% rename from test/jwk/sanity.test.js rename to test/jwk/general.test.js index 5e639575ee..e3f3c0eca2 100644 --- a/test/jwk/sanity.test.js +++ b/test/jwk/general.test.js @@ -1,6 +1,6 @@ const test = require('ava') -const { JWK: { generateSync, isKey } } = require('../..') +const { JWK: { generateSync, isKey, importKey } } = require('../..') test('.isKey() only key objects return true', t => { ;[[], false, true, null, Infinity, 0].forEach((val) => { @@ -37,3 +37,10 @@ test('"kid" must be a non-empty string', t => { ) }) }) + +test('"kid" from JWK is used when available and its different from thumbprint', t => { + const { kid: generatedThumbprint, ...jwk } = generateSync('oct').toJWK(true) + const key = importKey({ ...jwk, kid: 'foo' }) + t.is(key.kid, 'foo') + t.is(key.thumbprint, generatedThumbprint) +}) From 23b874cc20895e9c3472b3ecc8d4cd4fdf5b6289 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 9 Apr 2019 20:47:41 +0200 Subject: [PATCH 5/9] 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) From 857dc2b51b589559b5aaceed15f2fe304bc17a32 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 11 Apr 2019 10:04:53 +0200 Subject: [PATCH 6/9] fix: fail to import invalid PEM formatted strings and buffers --- lib/jwk/import.js | 6 +++++- test/jwk/import.test.js | 10 ++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/jwk/import.js b/lib/jwk/import.js index bcba412a9e..7b1583c23e 100644 --- a/lib/jwk/import.js +++ b/lib/jwk/import.js @@ -67,7 +67,11 @@ const importKey = (key, parameters) => { publicKey = createPublicKey(key) } catch (err) {} try { - secret = createSecretKey(Buffer.isBuffer(key) ? key : Buffer.from(key)) + // this is to filter out invalid PEM keys and certs, i'll rather have them fail import then + // have them imported as symmetric "oct" keys + if (!key.includes('-----BEGIN')) { + secret = createSecretKey(Buffer.isBuffer(key) ? key : Buffer.from(key)) + } } catch (err) {} } diff --git a/test/jwk/import.test.js b/test/jwk/import.test.js index cdd29dd01a..ce01ff6b73 100644 --- a/test/jwk/import.test.js +++ b/test/jwk/import.test.js @@ -37,6 +37,16 @@ test('parameters must be a plain object', t => { }) Object.entries(fixtures.PEM).forEach(([type, { private: priv, public: pub }]) => { + test(`fails to import ${type} as invalid string`, t => { + t.throws(() => { + importKey(priv.toString('ascii').replace(/\n/g, '')) + }, { instanceOf: errors.JWKImportFailed, code: 'ERR_JWK_IMPORT_FAILED' }) + }) + test(`fails to import ${type} as invalid buffer`, t => { + t.throws(() => { + importKey(Buffer.from(priv.toString('ascii').replace(/\n/g, ''))) + }, { instanceOf: errors.JWKImportFailed, code: 'ERR_JWK_IMPORT_FAILED' }) + }) test(`${type} private can be imported as a string`, t => { const k = importKey(priv.toString('ascii')) t.true(k.private) From dafeced271a11440f460e58b610e777d690dadfe Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sun, 21 Apr 2019 19:10:47 +0200 Subject: [PATCH 7/9] docs: fix readme typos --- docs/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/README.md b/docs/README.md index a52c7c654f..90a9216698 100644 --- a/docs/README.md +++ b/docs/README.md @@ -563,7 +563,7 @@ Returns the number of keys in the keystore. #### `keystore.all([parameters])` -Retrieves an array of keys matching the provider parameters, returns all if none are provided. The +Retrieves an array of keys matching the provided parameters, returns all if none are provided. The returned array is sorted by relevance based on the parameters. Keys with the exact algorithm or use specified by the parameters are first. @@ -582,7 +582,7 @@ specified by the parameters are first. #### `keystore.get([parameters])` -Retrieves a single key matching the provider parameters. The most relevant Key based on the +Retrieves a single key matching the provided parameters. The most relevant Key based on the parameters is returned. - `parameters`: `` @@ -666,11 +666,11 @@ ks.size ## JWT (JSON Web Token) - + - [JWT.sign(payload, key[, options])](#jwtsignpayload-key-options) - [JWT.verify(token, keyOrStore[, options])](#jwtverifytoken-keyorstore-options) - [JWT.decode(token[, options])](#jwtdecodetoken-options) - + ```js const { JWT } = require('@panva/jose') @@ -1219,7 +1219,7 @@ Verifies the provided JWE in either serialization with a given `` or `< ## Errors - + - [Class: <TypeError>](#class-typeerror) - [Class: <JOSEError>](#class-joseerror) - [Class: <JOSEAlgNotWhitelisted>](#class-josealgnotwhitelisted) @@ -1236,7 +1236,7 @@ Verifies the provided JWE in either serialization with a given `` or `< - [Class: <JWSVerificationFailed>](#class-jwsverificationfailed) - [Class: <JWTClaimInvalid>](#class-jwtclaiminvalid) - [Class: <JWTMalformed>](#class-jwtmalformed) - + The following errors are expected to be thrown by @panva/jose runtime and have their prototypes From 2c20b44649becc7dad23e3013fabec579d5fe502 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Mon, 22 Apr 2019 19:30:04 +0200 Subject: [PATCH 8/9] chore: dev dependency updates --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4ad1957276..26a6dadee5 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "@commitlint/config-conventional": "^7.5.0", "ava": "^1.2.1", "husky": "^1.3.1", - "nyc": "^13.3.0", + "nyc": "^14.0.0", "standard": "^12.0.1" }, "engines": { From 6a32da6937c602f88fc2c527651a66d8574e6612 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 23 Apr 2019 14:31:28 +0200 Subject: [PATCH 9/9] chore(release): 1.0.0 --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f5f76703d..953f371e4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,33 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. + +# [1.0.0](https://github.com/panva/jose/compare/v0.12.0...v1.0.0) (2019-04-23) + + +### Bug Fixes + +* fail to import invalid PEM formatted strings and buffers ([857dc2b](https://github.com/panva/jose/commit/857dc2b)) + + +### Features + +* add JWK key_ops support, fix .algorithms() op returns ([23b874c](https://github.com/panva/jose/commit/23b874c)) +* add key.toPEM() export function with optional encryption ([1159b0d](https://github.com/panva/jose/commit/1159b0d)) +* add OKP Key and EdDSA sign/verify support ([2dbd3ed](https://github.com/panva/jose/commit/2dbd3ed)), closes [#12](https://github.com/panva/jose/issues/12) + + +### BREAKING CHANGES + +* key.algorithms(op) un+wrapKey was split into correct +wrapKey/unwrapKey/deriveKey returns +* keystore.all and keystore.get `operation` option was +removed, `key_ops: string[]` supersedes it +* node.js minimal version is now v12.0.0 due to its +added EdDSA support (crypto.sign, crypto.verify and eddsa key objects) + + + # [0.12.0](https://github.com/panva/jose/compare/v0.11.5...v0.12.0) (2019-04-07) diff --git a/package.json b/package.json index 26a6dadee5..6b1c39396d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@panva/jose", - "version": "0.12.0", + "version": "1.0.0", "description": "JSON Web Almost Everything - JWA, JWS, JWE, JWK, JWT, JWKS for Node.js with minimal dependencies", "keywords": [ "compact",