From 2dbd3ed43ac46f13f0904a4b77200f74e2ca74f8 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 19 Mar 2019 17:57:42 +0100 Subject: [PATCH] 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], []) + }) + })() +})