From 211d7af5db50ad8f19e86120dc6d43cd66a0a1fe Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 22 May 2019 11:29:43 +0200 Subject: [PATCH] feat: add secp256k1 EC Key curve and ES256K This is as per - https://tools.ietf.org/html/draft-ietf-cose-webauthn-algorithms-01 - https://mailarchive.ietf.org/arch/msg/cose/91MouVA43DefbpQOB7l5daCEeIc --- CHANGELOG.md | 2 +- P-256K/index.js | 6 + README.md | 30 +++- lib/help/consts.js | 4 +- lib/help/ecdsa_signatures.js | 1 + lib/help/key_utils.js | 7 +- lib/help/node_alg.js | 2 +- lib/index.d.ts | 2 +- lib/jwa/ecdh/derive.js | 4 + lib/jwa/ecdsa.js | 2 +- lib/jwk/key/ec.js | 11 ++ lib/jwk/key/secp256k1_crv.js | 6 + package.json | 1 + test/fixtures/index.js | 24 +++ test/fixtures/secp256k1.key | 5 + test/fixtures/secp256k1.pem | 4 + test/help/P-256K.key_utils.test.js | 26 +++ test/jwe/smoke.P-256K.test.js | 31 ++++ test/jwe/smoke.test.js | 86 +-------- test/jwk/P-256K.import.test.js | 270 +++++++++++++++++++++++++++++ test/jwk/ec.test.js | 3 + test/jwk/generate.test.js | 7 + test/jwk/import.test.js | 2 + test/jws/smoke.P-256K.test.js | 19 ++ test/jws/smoke.test.js | 60 +------ test/macros/index.js | 143 +++++++++++++++ 26 files changed, 613 insertions(+), 145 deletions(-) create mode 100644 P-256K/index.js create mode 100644 lib/jwk/key/secp256k1_crv.js create mode 100644 test/fixtures/secp256k1.key create mode 100644 test/fixtures/secp256k1.pem create mode 100644 test/help/P-256K.key_utils.test.js create mode 100644 test/jwe/smoke.P-256K.test.js create mode 100644 test/jwk/P-256K.import.test.js create mode 100644 test/jws/smoke.P-256K.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index a28db5f778..3874ee3fbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,7 +30,7 @@ calculation loop when the private key's private exponent was outright invalid or tampered with. The new methods still allow to import private RSA keys with these -optimization key parameters missing but its disabled by default and one +optimization key parameters missing but it is disabled by default and one should choose to enable it when working with keys from trusted sources It is recommended not to use @panva/jose versions with this feature in diff --git a/P-256K/index.js b/P-256K/index.js new file mode 100644 index 0000000000..7d66971a27 --- /dev/null +++ b/P-256K/index.js @@ -0,0 +1,6 @@ +// rename 'secp256k1' to 'P-256K' + +const { rename } = require('../lib/jwk/key/secp256k1_crv') +rename('P-256K') + +module.exports = require('../lib') diff --git a/README.md b/README.md index 8d37aa6819..4880ca68e7 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ The following specifications are implemented by @panva/jose - JSON Web Key Thumbprint - [RFC7638][spec-thumbprint] - JWS Unencoded Payload Option - [RFC7797][spec-b64] - CFRG Elliptic Curve Signatures (EdDSA) - [RFC8037][spec-okp] +- secp256k1 curve EC Key support - [JOSE Registrations for WebAuthn Algorithms][draft-secp256k1] The test suite utilizes examples defined in [RFC7520][spec-cookbook] to confirm its JOSE implementation is correct. @@ -47,7 +48,7 @@ Legend: | -- | -- | -- | | RSASSA-PKCS1-v1_5 | ✓ | RS256, RS384, RS512 | | RSASSA-PSS | ✓ | PS256, PS384, PS512 | -| ECDSA | ✓ | ES256, ES384, ES512 | +| ECDSA | ✓ | ES256, ES256K, ES384, ES512 | | Edwards-curve DSA | ✓ | EdDSA | | HMAC with SHA-2 | ✓ | HS256, HS384, HS512 | @@ -247,6 +248,32 @@ jose.JWE.decrypt( ) ``` +#### secp256k1 + +Note: the secp256k1 JOSE parameters registration and the RFC is still in a draft state. If the WG +draft changes its mind about the parameter names again the new values will be propagated as a MINOR +library version. + +When you require `@panva/jose` you can work with `secp256k1` EC keys right away, the EC JWK `crv` +used is as per the specification `secp256k1`. + +```js +const jose = require('@panva/jose') +let key = jose.JWK.generateSync('EC', 'secp256k1') +key = jose.JWK.asKey(fs.readFileSync('path/to/key/file')) +key.crv === 'secp256k1' +``` + +For legacy reasons the unregistered EC JWK `crv` value `P-256K` is also supported but you must +require `@panva/jose` like so to use it: + +```js +const jose = require('@panva/jose/P-256K') +let key = jose.JWK.generateSync('EC', 'P-256K') +key = jose.JWK.asKey(fs.readFileSync('path/to/key/file')) +key.crv === 'P-256K' +``` + ## FAQ #### Semver? @@ -315,6 +342,7 @@ in terms of performance and API (not having well defined errors). [spec-jws]: https://tools.ietf.org/html/rfc7515 [spec-jwt]: https://tools.ietf.org/html/rfc7519 [spec-okp]: https://tools.ietf.org/html/rfc8037 +[draft-secp256k1]: https://tools.ietf.org/html/draft-ietf-cose-webauthn-algorithms-01 [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/lib/help/consts.js b/lib/help/consts.js index ca18059b2c..6f6b819f30 100644 --- a/lib/help/consts.js +++ b/lib/help/consts.js @@ -1,3 +1,5 @@ +const { name: secp256k1 } = require('../jwk/key/secp256k1_crv') + module.exports.KEYOBJECT = Symbol('KEYOBJECT') module.exports.PRIVATE_MEMBERS = Symbol('PRIVATE_MEMBERS') module.exports.PUBLIC_MEMBERS = Symbol('PUBLIC_MEMBERS') @@ -18,7 +20,7 @@ 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.EC_CURVES = new Set(['P-256', secp256k1, 'P-384', 'P-521']) module.exports.ECDH_ALGS = ['ECDH-ES', 'ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW'] module.exports.KEYLENGTHS = { diff --git a/lib/help/ecdsa_signatures.js b/lib/help/ecdsa_signatures.js index 0d08138cdd..ee2e742458 100644 --- a/lib/help/ecdsa_signatures.js +++ b/lib/help/ecdsa_signatures.js @@ -10,6 +10,7 @@ const getParamSize = keySize => ((keySize / 8) | 0) + (keySize % 8 === 0 ? 0 : 1 const paramBytesForAlg = { ES256: getParamSize(256), + ES256K: getParamSize(256), ES384: getParamSize(384), ES512: getParamSize(521) } diff --git a/lib/help/key_utils.js b/lib/help/key_utils.js index 3814849a10..c3ff9a2397 100644 --- a/lib/help/key_utils.js +++ b/lib/help/key_utils.js @@ -1,24 +1,29 @@ const { createPublicKey } = require('crypto') -const base64url = require('./base64url') +const { name: secp256k1 } = require('../jwk/key/secp256k1_crv') const errors = require('../errors') + +const base64url = require('./base64url') const asn1 = require('./asn1') const computePrimes = require('./rsa_primes') const { OKP_CURVES, EC_CURVES } = require('./consts') const oidHexToCurve = new Map([ ['06082a8648ce3d030107', 'P-256'], + ['06052b8104000a', secp256k1], ['06052b81040022', 'P-384'], ['06052b81040023', 'P-521'] ]) const EC_KEY_OID = '1.2.840.10045.2.1'.split('.') const crvToOid = new Map([ ['P-256', '1.2.840.10045.3.1.7'.split('.')], + [secp256k1, '1.3.132.0.10'.split('.')], ['P-384', '1.3.132.0.34'.split('.')], ['P-521', '1.3.132.0.35'.split('.')] ]) const crvToOidBuf = new Map([ ['P-256', Buffer.from('06082a8648ce3d030107', 'hex')], + [secp256k1, Buffer.from('06052b8104000a', 'hex')], ['P-384', Buffer.from('06052b81040022', 'hex')], ['P-521', Buffer.from('06052b81040023', 'hex')] ]) diff --git a/lib/help/node_alg.js b/lib/help/node_alg.js index 10c8802d43..10a15665de 100644 --- a/lib/help/node_alg.js +++ b/lib/help/node_alg.js @@ -1 +1 @@ -module.exports = alg => `sha${alg.substr(-3)}` +module.exports = alg => `sha${alg.substr(2, 3)}` diff --git a/lib/index.d.ts b/lib/index.d.ts index 8d112ae843..821e0f59c7 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -15,7 +15,7 @@ interface KeyParameters extends BasicParameters { x5t?: string 'x5t#S256'?: string } -type ECCurve = 'P-256' | 'P-384' | 'P-521' +type ECCurve = 'P-256' | 'secp256k1' | 'P-384' | 'P-521' type OKPCurve = 'Ed25519' | 'Ed448' | 'X25519' | 'X448' type keyType = 'RSA' | 'EC' | 'OKP' | 'oct' type asymmetricKeyObjectTypes = 'private' | 'public' diff --git a/lib/jwa/ecdh/derive.js b/lib/jwa/ecdh/derive.js index a9debd8d1b..755a946578 100644 --- a/lib/jwa/ecdh/derive.js +++ b/lib/jwa/ecdh/derive.js @@ -1,6 +1,7 @@ const { createECDH, createHash, constants: { POINT_CONVERSION_UNCOMPRESSED } } = require('crypto') const base64url = require('../../help/base64url') +const { name: secp256k1 } = require('../../jwk/key/secp256k1_crv') const crvToCurve = (crv) => { switch (crv) { @@ -10,9 +11,12 @@ const crvToCurve = (crv) => { return 'secp384r1' case 'P-521': return 'secp521r1' + case 'secp256k1': case 'X448': case 'X25519': return crv + case secp256k1: + return 'secp256k1' } } diff --git a/lib/jwa/ecdsa.js b/lib/jwa/ecdsa.js index afdfb173e0..b71ac41bdc 100644 --- a/lib/jwa/ecdsa.js +++ b/lib/jwa/ecdsa.js @@ -18,7 +18,7 @@ const verify = (jwaAlg, nodeAlg, { [KEYOBJECT]: keyObject }, payload, signature) } module.exports = (JWA) => { - ['ES256', 'ES384', 'ES512'].forEach((jwaAlg) => { + ['ES256', 'ES384', 'ES512', 'ES256K'].forEach((jwaAlg) => { const nodeAlg = resolveNodeAlg(jwaAlg) assert(!JWA.sign.has(jwaAlg), `sign alg ${jwaAlg} already registered`) diff --git a/lib/jwk/key/ec.js b/lib/jwk/key/ec.js index 7ba0a5446f..65d15610fe 100644 --- a/lib/jwk/key/ec.js +++ b/lib/jwk/key/ec.js @@ -7,6 +7,7 @@ const { } = require('../../help/consts') const errors = require('../../errors') +const { name: secp256k1 } = require('./secp256k1_crv') const Key = require('./base') @@ -21,6 +22,8 @@ const crvToDSA = (crv) => { switch (crv) { case 'P-256': return 'ES256' + case secp256k1: + return 'ES256K' case 'P-384': return 'ES384' case 'P-521': @@ -115,6 +118,10 @@ class ECKey extends Key { throw new errors.JOSENotSupported(`unsupported EC key curve: ${crv}`) } + if (crv === secp256k1 && crv !== 'secp256k1') { + crv = 'secp256k1' + } + const { privateKey, publicKey } = await generateKeyPair('ec', { namedCurve: crv }) return privat ? privateKey : publicKey @@ -125,6 +132,10 @@ class ECKey extends Key { throw new errors.JOSENotSupported(`unsupported EC key curve: ${crv}`) } + if (crv === secp256k1 && crv !== 'secp256k1') { + crv = 'secp256k1' + } + const { privateKey, publicKey } = generateKeyPairSync('ec', { namedCurve: crv }) return privat ? privateKey : publicKey diff --git a/lib/jwk/key/secp256k1_crv.js b/lib/jwk/key/secp256k1_crv.js new file mode 100644 index 0000000000..4dc4d2d647 --- /dev/null +++ b/lib/jwk/key/secp256k1_crv.js @@ -0,0 +1,6 @@ +module.exports = { + name: 'secp256k1', + rename (value) { + module.exports.name = value + } +} diff --git a/package.json b/package.json index fcbdf41e8a..99c133319a 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "jwks", "jws", "jwt", + "secp256k1", "sign", "verify" ], diff --git a/test/fixtures/index.js b/test/fixtures/index.js index 6fd9fc414a..13bb60804f 100644 --- a/test/fixtures/index.js +++ b/test/fixtures/index.js @@ -56,6 +56,22 @@ module.exports.JWK = { d: '_i_1Ac5oVmbBxGvEvOEFHMpzMXKZi8voUx8I3Gl6IxY' }, + 'secp256k1': { + kty: 'EC', + crv: 'secp256k1', + x: 'VRaLqtMjg_JRaDzkbfit7zonkOGDZ42qbZyljhqsg3U', + y: '5qgTxoRAf0hJxcphVg1NE9r0Xv-HHZyVIJxEbo6SAsQ', + d: 'xTAmXNRL8ksBlr-F3yXDrUdRDn1gyIvY_PC2e_iUK7c' + }, + + 'P-256K': { + kty: 'EC', + crv: 'P-256K', + x: 'VRaLqtMjg_JRaDzkbfit7zonkOGDZ42qbZyljhqsg3U', + y: '5qgTxoRAf0hJxcphVg1NE9r0Xv-HHZyVIJxEbo6SAsQ', + d: 'xTAmXNRL8ksBlr-F3yXDrUdRDn1gyIvY_PC2e_iUK7c' + }, + 'P-384': { kty: 'EC', crv: 'P-384', @@ -100,6 +116,14 @@ module.exports.PEM = { private: readFileSync(join(__dirname, 'P-256.key')), public: readFileSync(join(__dirname, 'P-256.pem')) }, + 'secp256k1': { + private: readFileSync(join(__dirname, 'secp256k1.key')), + public: readFileSync(join(__dirname, 'secp256k1.pem')) + }, + 'P-256K': { + private: readFileSync(join(__dirname, 'secp256k1.key')), + public: readFileSync(join(__dirname, 'secp256k1.pem')) + }, 'P-384': { private: readFileSync(join(__dirname, 'P-384.key')), public: readFileSync(join(__dirname, 'P-384.pem')) diff --git a/test/fixtures/secp256k1.key b/test/fixtures/secp256k1.key new file mode 100644 index 0000000000..149e95c8a3 --- /dev/null +++ b/test/fixtures/secp256k1.key @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGEAgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawIBAQQgxTAmXNRL8ksBlr+F3yXD +rUdRDn1gyIvY/PC2e/iUK7ehRANCAARVFouq0yOD8lFoPORt+K3vOieQ4YNnjapt +nKWOGqyDdeaoE8aEQH9IScXKYVYNTRPa9F7/hx2clSCcRG6OkgLE +-----END PRIVATE KEY----- diff --git a/test/fixtures/secp256k1.pem b/test/fixtures/secp256k1.pem new file mode 100644 index 0000000000..7affcba985 --- /dev/null +++ b/test/fixtures/secp256k1.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEVRaLqtMjg/JRaDzkbfit7zonkOGDZ42q +bZyljhqsg3XmqBPGhEB/SEnFymFWDU0T2vRe/4cdnJUgnERujpICxA== +-----END PUBLIC KEY----- diff --git a/test/help/P-256K.key_utils.test.js b/test/help/P-256K.key_utils.test.js new file mode 100644 index 0000000000..2b646df505 --- /dev/null +++ b/test/help/P-256K.key_utils.test.js @@ -0,0 +1,26 @@ +// require 'secp256k1' renamed to 'P-256K' +require('../../P-256K') + +const test = require('ava') +const { createPublicKey, createPrivateKey } = require('crypto') + +const { keyObjectToJWK, jwkToPem } = require('../../lib/help/key_utils') +const { JWK: fixtures } = require('../fixtures') +const clone = obj => JSON.parse(JSON.stringify(obj)) + +test('EC P-256K Public key', t => { + const expected = clone(fixtures['P-256K']) + delete expected.d + const pem = createPublicKey(jwkToPem(expected)) + const actual = keyObjectToJWK(pem) + + t.deepEqual(actual, expected) +}) + +test('EC P-256K Private key', t => { + const expected = fixtures['P-256K'] + const pem = createPrivateKey(jwkToPem(expected)) + const actual = keyObjectToJWK(pem) + + t.deepEqual(actual, expected) +}) diff --git a/test/jwe/smoke.P-256K.test.js b/test/jwe/smoke.P-256K.test.js new file mode 100644 index 0000000000..b8ddf37ac3 --- /dev/null +++ b/test/jwe/smoke.P-256K.test.js @@ -0,0 +1,31 @@ +// require 'secp256k1' renamed to 'P-256K' +require('../../P-256K') + +const test = require('ava') + +const { JWK: { asKey } } = require('../..') + +const ENCS = [ + 'A128GCM', + 'A192GCM', + 'A256GCM', + 'A128CBC-HS256', + 'A192CBC-HS384', + 'A256CBC-HS512' +] + +const type = 'P-256K' +const { private: key, public: pub } = require('../fixtures').PEM[type] + +const { JWE: { success, failure } } = require('../macros') + +const eKey = asKey(pub) +const dKey = asKey(key) + +;[...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) + test(`key ${type} > alg ${alg} > ${enc} (negative cases)`, failure, eKey, dKey, alg, enc) + }) +}) diff --git a/test/jwe/smoke.test.js b/test/jwe/smoke.test.js index b63c14a83e..69752a2941 100644 --- a/test/jwe/smoke.test.js +++ b/test/jwe/smoke.test.js @@ -2,10 +2,8 @@ const test = require('ava') const { randomBytes } = require('crypto') -const { encrypt, decrypt } = require('../../lib/jwe') -const { JWK: { asKey, generateSync }, errors } = require('../..') +const { JWK: { asKey, generateSync } } = require('../..') -const PAYLOAD = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c' const ENCS = [ 'A128GCM', 'A192GCM', @@ -17,87 +15,11 @@ const ENCS = [ const fixtures = require('../fixtures') -const success = (t, eKey, dKey, alg, enc) => { - const encrypted = encrypt(PAYLOAD, eKey, { alg, enc }) - t.truthy(encrypted) - const decrypted = decrypt(encrypted, dKey) - t.deepEqual(decrypted, Buffer.from(PAYLOAD)) - const encrypted2 = encrypt(PAYLOAD, dKey, { alg, enc }) - t.truthy(encrypted2) - const decrypted2 = decrypt(encrypted, dKey) - t.deepEqual(decrypted2, Buffer.from(PAYLOAD)) -} - -const failure = (t, eKey, dKey, alg, enc) => { - const encrypted = encrypt.flattened(PAYLOAD, eKey, { alg, enc }) - t.truthy(encrypted) - if (encrypted.encrypted_key) { - const orig = encrypted.encrypted_key - - if (encrypted.encrypted_key.startsWith('-')) { - encrypted.encrypted_key = `Q${encrypted.encrypted_key.substr(1)}` - } else { - encrypted.encrypted_key = `-${encrypted.encrypted_key.substr(1)}` - } - t.throws(() => { - decrypt(encrypted, dKey) - }, { instanceOf: errors.JWEDecryptionFailed, code: 'ERR_JWE_DECRYPTION_FAILED' }) - encrypted.encrypted_key = encrypted.encrypted_key.substr(4) - t.throws(() => { - decrypt(encrypted, dKey) - }, { instanceOf: errors.JWEDecryptionFailed, code: 'ERR_JWE_DECRYPTION_FAILED' }) - encrypted.encrypted_key = orig - } - - (() => { - const orig = encrypted.protected - - if (encrypted.protected.startsWith('-')) { - encrypted.protected = `Q${encrypted.protected.substr(1)}` - } else { - encrypted.protected = `-${encrypted.protected.substr(1)}` - } - t.throws(() => { - decrypt(encrypted, dKey) - }, { instanceOf: errors.JWEInvalid, code: 'ERR_JWE_INVALID', message: 'could not parse JWE protected header' }) - encrypted.protected = orig - })() - - ;(() => { - const orig = encrypted.protected - delete encrypted.protected - t.throws(() => { - decrypt(encrypted, dKey) - }, { instanceOf: errors.JWEInvalid, code: 'ERR_JWE_INVALID', message: 'missing Key Management algorithm' }) - encrypted.protected = orig - })() - - ;['iv', 'ciphertext', 'tag'].forEach((prop) => { - const orig = encrypted[prop] - - if (encrypted[prop].startsWith('-')) { - encrypted[prop] = `Q${encrypted[prop].substr(1)}` - } else { - encrypted[prop] = `-${encrypted[prop].substr(1)}` - } - t.throws(() => { - decrypt(encrypted, dKey) - }, { instanceOf: errors.JWEDecryptionFailed, code: 'ERR_JWE_DECRYPTION_FAILED' }) - encrypted[prop] = orig - }) - - ;['iv', 'tag'].forEach((prop) => { - const orig = encrypted[prop] - - encrypted[prop] = encrypted[prop].substr(4) - t.throws(() => { - decrypt(encrypted, dKey) - }, { instanceOf: errors.JWEInvalid, code: 'ERR_JWE_INVALID' }) - encrypted[prop] = orig - }) -} +const { JWE: { success, failure } } = require('../macros') Object.entries(fixtures.PEM).forEach(([type, { private: key, public: pub }]) => { + if (type === 'P-256K') return + const eKey = asKey(pub) const dKey = asKey(key) diff --git a/test/jwk/P-256K.import.test.js b/test/jwk/P-256K.import.test.js new file mode 100644 index 0000000000..da68ce834f --- /dev/null +++ b/test/jwk/P-256K.import.test.js @@ -0,0 +1,270 @@ +// require 'secp256k1' renamed to 'P-256K' +require('../../P-256K') + +const test = require('ava') +const { createPrivateKey, createPublicKey } = require('crypto') +const { hasProperty, hasNoProperties, hasProperties } = require('../macros') +const fixtures = require('../fixtures') + +const ECKey = require('../../lib/jwk/key/ec') + +Object.entries({ + 'P-256K': ['ES256K', 'zZYrH69YCAAihM7ZCoRj90VI55H5MmQscSpf-JuUS50'] +}).forEach(([crv, [alg, kid]]) => { + // private + ;(() => { + const keyObject = createPrivateKey(fixtures.PEM[crv].private) + const key = new ECKey(keyObject) + + test(`${crv} EC Private key`, hasProperty, key, 'crv', crv) + test(`${crv} EC Private key (with alg)`, hasProperty, new ECKey(keyObject, { alg }), 'alg', alg) + test(`${crv} EC Private key (with kid)`, hasProperty, new ECKey(keyObject, { kid: 'foobar' }), 'kid', 'foobar') + test(`${crv} EC Private key (with use)`, hasProperty, new ECKey(keyObject, { use: 'sig' }), 'use', 'sig') + test(`${crv} EC Private key`, hasNoProperties, key, 'k', 'e', 'n', 'p', 'q', 'dp', 'dq', 'qi') + test(`${crv} EC Private key`, hasProperties, key, 'x', 'y', 'd') + test(`${crv} EC Private key`, hasProperty, key, 'alg', undefined) + test(`${crv} EC Private key`, hasProperty, key, 'kid', kid) + test(`${crv} EC Private key`, hasProperty, key, 'kty', 'EC') + test(`${crv} EC Private key`, hasProperty, key, 'private', true) + test(`${crv} EC Private key`, hasProperty, key, 'public', false) + test(`${crv} EC Private key`, hasProperty, key, 'secret', false) + test(`${crv} EC Private key`, hasProperty, key, 'type', 'private') + test(`${crv} EC Private key`, hasProperty, key, 'use', undefined) + + test(`${crv} EC Private key algorithms (no operation)`, t => { + const result = key.algorithms() + t.is(result.constructor, Set) + t.deepEqual([...result], [alg, 'ECDH-ES', 'ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW']) + }) + + test(`${crv} EC Private key algorithms (no operation, w/ alg)`, t => { + const key = new ECKey(keyObject, { alg }) + const result = key.algorithms() + t.is(result.constructor, Set) + t.deepEqual([...result], [alg]) + }) + + test(`${crv} EC Private key supports sign alg (no use)`, t => { + const result = key.algorithms('sign') + t.is(result.constructor, Set) + t.deepEqual([...result], [alg]) + }) + + test(`${crv} EC Private key supports verify alg (no use)`, t => { + const result = key.algorithms('verify') + t.is(result.constructor, Set) + t.deepEqual([...result], [alg]) + }) + + test(`${crv} EC Private key supports sign alg when \`use\` is "sig")`, t => { + const sigKey = new ECKey(keyObject, { use: 'sig' }) + const result = sigKey.algorithms('sign') + t.is(result.constructor, Set) + t.deepEqual([...result], [alg]) + }) + + test(`${crv} EC Private key supports verify alg when \`use\` is "sig")`, t => { + const sigKey = new ECKey(keyObject, { use: 'sig' }) + const result = sigKey.algorithms('verify') + t.is(result.constructor, Set) + t.deepEqual([...result], [alg]) + }) + + test(`${crv} EC Private key supports single sign alg when \`alg\` is set)`, t => { + const sigKey = new ECKey(keyObject, { alg }) + const result = sigKey.algorithms('sign') + t.is(result.constructor, Set) + t.deepEqual([...result], [alg]) + }) + + test(`${crv} EC Private key supports single verify alg when \`alg\` is set)`, t => { + const sigKey = new ECKey(keyObject, { alg }) + const result = sigKey.algorithms('verify') + t.is(result.constructor, Set) + t.deepEqual([...result], [alg]) + }) + + test(`${crv} EC Private key no sign support when \`use\` is "enc"`, t => { + const encKey = new ECKey(keyObject, { use: 'enc' }) + const result = encKey.algorithms('sign') + t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + + test(`${crv} EC Private key no verify support when \`use\` is "enc"`, t => { + const encKey = new ECKey(keyObject, { use: 'enc' }) + const result = encKey.algorithms('verify') + t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + + test(`${crv} EC Private key .algorithms("encrypt")`, t => { + const result = key.algorithms('encrypt') + t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + + test(`${crv} EC Private key .algorithms("decrypt")`, t => { + const result = key.algorithms('decrypt') + t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + + 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']) + }) + + test(`${crv} EC Private key .algorithms("wrapKey") when use is sig`, t => { + const sigKey = new ECKey(keyObject, { use: 'sig' }) + const result = sigKey.algorithms('wrapKey') + t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + + test(`${crv} EC Private key .algorithms("unwrapKey")`, t => { + const result = key.algorithms('unwrapKey') + t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + + test(`${crv} EC Private key .algorithms("deriveKey") when use is sig`, t => { + const sigKey = new ECKey(keyObject, { use: 'sig' }) + const result = sigKey.algorithms('deriveKey') + t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + })() + + // public + ;(() => { + const keyObject = createPublicKey(fixtures.PEM[crv].public) + const key = new ECKey(keyObject) + + test(`${crv} EC Public key`, hasProperty, key, 'crv', crv) + test(`${crv} EC Public key (with alg)`, hasProperty, new ECKey(keyObject, { alg }), 'alg', alg) + test(`${crv} EC Public key (with kid)`, hasProperty, new ECKey(keyObject, { kid: 'foobar' }), 'kid', 'foobar') + test(`${crv} EC Public key (with use)`, hasProperty, new ECKey(keyObject, { use: 'sig' }), 'use', 'sig') + test(`${crv} EC Public key`, hasNoProperties, key, 'k', 'e', 'n', 'p', 'q', 'dp', 'dq', 'qi', 'd') + test(`${crv} EC Public key`, hasProperties, key, 'x', 'y') + test(`${crv} EC Public key`, hasProperty, key, 'alg', undefined) + test(`${crv} EC Public key`, hasProperty, key, 'kid', kid) + test(`${crv} EC Public key`, hasProperty, key, 'kty', 'EC') + test(`${crv} EC Public key`, hasProperty, key, 'private', false) + test(`${crv} EC Public key`, hasProperty, key, 'public', true) + test(`${crv} EC Public key`, hasProperty, key, 'secret', false) + test(`${crv} EC Public key`, hasProperty, key, 'type', 'public') + test(`${crv} EC Public key`, hasProperty, key, 'use', undefined) + + test(`${crv} EC Public key algorithms (no operation)`, t => { + const result = key.algorithms() + t.is(result.constructor, Set) + t.deepEqual([...result], [alg, 'ECDH-ES', 'ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW']) + }) + + test(`${crv} EC Public key algorithms (no operation, w/ alg)`, t => { + const key = new ECKey(keyObject, { alg }) + const result = key.algorithms() + t.is(result.constructor, Set) + t.deepEqual([...result], [alg]) + }) + + test(`${crv} EC Public key cannot sign`, t => { + const result = key.algorithms('sign') + t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + + test(`${crv} EC Public key supports verify alg (no use)`, t => { + const result = key.algorithms('verify') + t.is(result.constructor, Set) + t.deepEqual([...result], [alg]) + }) + + test(`${crv} EC Public key cannot sign even when \`use\` is "sig")`, t => { + const sigKey = new ECKey(keyObject, { use: 'sig' }) + const result = sigKey.algorithms('sign') + t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + + test(`${crv} EC Public key supports verify alg when \`use\` is "sig")`, t => { + const sigKey = new ECKey(keyObject, { use: 'sig' }) + const result = sigKey.algorithms('verify') + t.is(result.constructor, Set) + t.deepEqual([...result], [alg]) + }) + + test(`${crv} EC Public key cannot sign even when \`alg\` is set)`, t => { + const sigKey = new ECKey(keyObject, { alg }) + const result = sigKey.algorithms('sign') + t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + + test(`${crv} EC Public key supports single verify alg when \`alg\` is set)`, t => { + const sigKey = new ECKey(keyObject, { alg }) + const result = sigKey.algorithms('verify') + t.is(result.constructor, Set) + t.deepEqual([...result], [alg]) + }) + + test(`${crv} EC Public key no sign support when \`use\` is "enc"`, t => { + const encKey = new ECKey(keyObject, { use: 'enc' }) + const result = encKey.algorithms('sign') + t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + + test(`${crv} EC Public key no verify support when \`use\` is "enc"`, t => { + const encKey = new ECKey(keyObject, { use: 'enc' }) + const result = encKey.algorithms('verify') + t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + + test(`${crv} EC Public key .algorithms("encrypt")`, t => { + const result = key.algorithms('encrypt') + t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + + test(`${crv} EC Public key .algorithms("decrypt")`, t => { + const result = key.algorithms('decrypt') + t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + + 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']) + }) + + test(`${crv} EC Public key .algorithms("wrapKey") when use is sig`, t => { + const sigKey = new ECKey(keyObject, { use: 'sig' }) + const result = sigKey.algorithms('wrapKey') + t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + + test(`${crv} EC Public key .algorithms("unwrapKey")`, t => { + const result = key.algorithms('unwrapKey') + t.is(result.constructor, Set) + t.deepEqual([...result], []) + }) + })() +}) diff --git a/test/jwk/ec.test.js b/test/jwk/ec.test.js index 2d4575986d..80964b0fcb 100644 --- a/test/jwk/ec.test.js +++ b/test/jwk/ec.test.js @@ -25,6 +25,7 @@ test('Unusable with unsupported curves', t => { Object.entries({ 'P-256': ['ES256', 'rDd6H6t9-nJUoz72nTpz8tInvypVWhE2iQoPznj8ZY8'], + 'secp256k1': ['ES256K', 'kWx_DzFzKNHUQz1FkNzj8KmSRingv9EQQzdVY3td21w'], 'P-384': ['ES384', '5gebayAhpztJCs4Pxo-z1hhsN0upoyG2NAoKpiiH2b0'], 'P-521': ['ES512', 'BQtkbSY3xgN4M2ZP3IHMLG7-Rp1L29teCMfNqgJHtTY'] }).forEach(([crv, [alg, kid]]) => { @@ -33,6 +34,7 @@ Object.entries({ const keyObject = createPrivateKey(fixtures.PEM[crv].private) const key = new ECKey(keyObject) + test(`${crv} EC Private key`, hasProperty, key, 'crv', crv) test(`${crv} EC Private key (with alg)`, hasProperty, new ECKey(keyObject, { alg }), 'alg', alg) test(`${crv} EC Private key (with kid)`, hasProperty, new ECKey(keyObject, { kid: 'foobar' }), 'kid', 'foobar') test(`${crv} EC Private key (with use)`, hasProperty, new ECKey(keyObject, { use: 'sig' }), 'use', 'sig') @@ -164,6 +166,7 @@ Object.entries({ const keyObject = createPublicKey(fixtures.PEM[crv].public) const key = new ECKey(keyObject) + test(`${crv} EC Public key`, hasProperty, key, 'crv', crv) test(`${crv} EC Public key (with alg)`, hasProperty, new ECKey(keyObject, { alg }), 'alg', alg) test(`${crv} EC Public key (with kid)`, hasProperty, new ECKey(keyObject, { kid: 'foobar' }), 'kid', 'foobar') test(`${crv} EC Public key (with use)`, hasProperty, new ECKey(keyObject, { use: 'sig' }), 'use', 'sig') diff --git a/test/jwk/generate.test.js b/test/jwk/generate.test.js index b417c619ee..8055295baa 100644 --- a/test/jwk/generate.test.js +++ b/test/jwk/generate.test.js @@ -42,6 +42,13 @@ const { JWK: { generate, generateSync }, errors } = require('../..') ['EC', 'P-256', { use: 'enc', alg: 'ECDH-ES' }], ['EC', 'P-256', { alg: 'ES256' }], ['EC', 'P-256', { alg: 'ECDH-ES' }], + ['EC', 'secp256k1'], + ['EC', 'secp256k1', { use: 'sig' }], + ['EC', 'secp256k1', { use: 'enc' }], + ['EC', 'secp256k1', { use: 'sig', alg: 'ES256K' }], + ['EC', 'secp256k1', { use: 'enc', alg: 'ECDH-ES' }], + ['EC', 'secp256k1', { alg: 'ES256K' }], + ['EC', 'secp256k1', { alg: 'ECDH-ES' }], ['EC', 'P-384'], ['EC', 'P-384', { use: 'sig' }], ['EC', 'P-384', { use: 'enc' }], diff --git a/test/jwk/import.test.js b/test/jwk/import.test.js index d0a8adbf74..c84b56b7f5 100644 --- a/test/jwk/import.test.js +++ b/test/jwk/import.test.js @@ -37,6 +37,8 @@ test('parameters must be a plain object', t => { }) Object.entries(fixtures.PEM).forEach(([type, { private: priv, public: pub }]) => { + if (type === 'P-256K') return + test(`fails to import ${type} as invalid string`, t => { t.throws(() => { asKey(priv.toString('ascii').replace(/\n/g, '')) diff --git a/test/jws/smoke.P-256K.test.js b/test/jws/smoke.P-256K.test.js new file mode 100644 index 0000000000..2a91518fbd --- /dev/null +++ b/test/jws/smoke.P-256K.test.js @@ -0,0 +1,19 @@ +// require 'secp256k1' renamed to 'P-256K' +require('../../P-256K') + +const test = require('ava') + +const { JWK: { asKey } } = require('../..') + +const type = 'P-256K' +const { private: key, public: pub } = require('../fixtures').PEM[type] + +const { JWS: { success, failure } } = require('../macros') + +const sKey = asKey(key) +const vKey = asKey(pub) + +sKey.algorithms('sign').forEach((alg) => { + test(`key ${type} > alg ${alg}`, success, sKey, vKey, alg) + test(`key ${type} > alg ${alg} (negative cases)`, failure, sKey, vKey, alg) +}) diff --git a/test/jws/smoke.test.js b/test/jws/smoke.test.js index 17e014f5c0..27839c29ea 100644 --- a/test/jws/smoke.test.js +++ b/test/jws/smoke.test.js @@ -1,66 +1,14 @@ const test = require('ava') -const { sign, verify } = require('../../lib/jws') -const { JWK: { asKey, generateSync }, errors } = require('../..') - -const PAYLOAD = {} +const { JWK: { asKey, generateSync } } = require('../..') const fixtures = require('../fixtures') -const success = (t, sKey, vKey, alg) => { - const signed = sign(PAYLOAD, sKey, { alg }) - t.truthy(signed) - const verified = verify(signed, vKey) - t.deepEqual(verified, {}) -} - -const failure = (t, sKey, vKey, alg) => { - const signed = sign.flattened(PAYLOAD, sKey, { alg }) - t.truthy(signed) - - ;(() => { - const orig = signed.protected - - if (signed.protected.startsWith('-')) { - signed.protected = `Q${signed.protected.substr(1)}` - } else { - signed.protected = `-${signed.protected.substr(1)}` - } - t.throws(() => { - verify(signed, vKey) - }, { instanceOf: errors.JWSInvalid, code: 'ERR_JWS_INVALID', message: 'could not parse JWS protected header' }) - signed.protected = orig - })() - - ;(() => { - const orig = signed.protected - delete signed.protected - t.throws(() => { - verify(signed, vKey) - }, { instanceOf: errors.JWSInvalid, code: 'ERR_JWS_INVALID', message: 'missing JWS signature algorithm' }) - signed.protected = orig - })() - - ;(() => { - const orig = signed.signature - - if (signed.signature.startsWith('-')) { - signed.signature = `Q${signed.signature.substr(1)}` - } else { - signed.signature = `-${signed.signature.substr(1)}` - } - t.throws(() => { - verify(signed, vKey) - }, { instanceOf: errors.JWSVerificationFailed, code: 'ERR_JWS_VERIFICATION_FAILED' }) - signed.signature = signed.signature.substr(4) - t.throws(() => { - verify(signed, vKey) - }, { instanceOf: errors.JWSVerificationFailed, code: 'ERR_JWS_VERIFICATION_FAILED' }) - signed.signature = orig - })() -} +const { JWS: { success, failure } } = require('../macros') Object.entries(fixtures.PEM).forEach(([type, { private: key, public: pub }]) => { + if (type === 'P-256K') return + const sKey = asKey(key) const vKey = asKey(pub) diff --git a/test/macros/index.js b/test/macros/index.js index 4aaeeee12d..722f3a4fb8 100644 --- a/test/macros/index.js +++ b/test/macros/index.js @@ -1,6 +1,9 @@ const { inspect } = require('util') +const errors = require('../../lib/errors') const base64url = require('../../lib/help/base64url') +const { encrypt, decrypt } = require('../../lib/jwe') +const { sign, verify } = require('../../lib/jws') const hasProperty = (t, obj, property, value, assertion = 'is') => { t.true(property in obj) @@ -44,7 +47,147 @@ const compactJwt = (t, jwt, eHeader, ePayload) => { t.deepEqual(aPayload, ePayload) } +const JWSPAYLOAD = {} +const JWS = { + success (t, sKey, vKey, alg) { + const signed = sign(JWSPAYLOAD, sKey, { alg }) + t.truthy(signed) + const verified = verify(signed, vKey) + t.deepEqual(verified, {}) + }, + failure (t, sKey, vKey, alg) { + const signed = sign.flattened(JWSPAYLOAD, sKey, { alg }) + t.truthy(signed) + + ;(() => { + const orig = signed.protected + + if (signed.protected.startsWith('-')) { + signed.protected = `Q${signed.protected.substr(1)}` + } else { + signed.protected = `-${signed.protected.substr(1)}` + } + t.throws(() => { + verify(signed, vKey) + }, { instanceOf: errors.JWSInvalid, code: 'ERR_JWS_INVALID', message: 'could not parse JWS protected header' }) + signed.protected = orig + })() + + ;(() => { + const orig = signed.protected + delete signed.protected + t.throws(() => { + verify(signed, vKey) + }, { instanceOf: errors.JWSInvalid, code: 'ERR_JWS_INVALID', message: 'missing JWS signature algorithm' }) + signed.protected = orig + })() + + ;(() => { + const orig = signed.signature + + if (signed.signature.startsWith('-')) { + signed.signature = `Q${signed.signature.substr(1)}` + } else { + signed.signature = `-${signed.signature.substr(1)}` + } + t.throws(() => { + verify(signed, vKey) + }, { instanceOf: errors.JWSVerificationFailed, code: 'ERR_JWS_VERIFICATION_FAILED' }) + signed.signature = signed.signature.substr(4) + t.throws(() => { + verify(signed, vKey) + }, { instanceOf: errors.JWSVerificationFailed, code: 'ERR_JWS_VERIFICATION_FAILED' }) + signed.signature = orig + })() + } +} + +const JWEPAYLOAD = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c' +const JWE = { + success (t, eKey, dKey, alg, enc) { + const encrypted = encrypt(JWEPAYLOAD, eKey, { alg, enc }) + t.truthy(encrypted) + const decrypted = decrypt(encrypted, dKey) + t.deepEqual(decrypted, Buffer.from(JWEPAYLOAD)) + const encrypted2 = encrypt(JWEPAYLOAD, dKey, { alg, enc }) + t.truthy(encrypted2) + const decrypted2 = decrypt(encrypted, dKey) + t.deepEqual(decrypted2, Buffer.from(JWEPAYLOAD)) + }, + + failure (t, eKey, dKey, alg, enc) { + const encrypted = encrypt.flattened(JWEPAYLOAD, eKey, { alg, enc }) + t.truthy(encrypted) + if (encrypted.encrypted_key) { + const orig = encrypted.encrypted_key + + if (encrypted.encrypted_key.startsWith('-')) { + encrypted.encrypted_key = `Q${encrypted.encrypted_key.substr(1)}` + } else { + encrypted.encrypted_key = `-${encrypted.encrypted_key.substr(1)}` + } + t.throws(() => { + decrypt(encrypted, dKey) + }, { instanceOf: errors.JWEDecryptionFailed, code: 'ERR_JWE_DECRYPTION_FAILED' }) + encrypted.encrypted_key = encrypted.encrypted_key.substr(4) + t.throws(() => { + decrypt(encrypted, dKey) + }, { instanceOf: errors.JWEDecryptionFailed, code: 'ERR_JWE_DECRYPTION_FAILED' }) + encrypted.encrypted_key = orig + } + + (() => { + const orig = encrypted.protected + + if (encrypted.protected.startsWith('-')) { + encrypted.protected = `Q${encrypted.protected.substr(1)}` + } else { + encrypted.protected = `-${encrypted.protected.substr(1)}` + } + t.throws(() => { + decrypt(encrypted, dKey) + }, { instanceOf: errors.JWEInvalid, code: 'ERR_JWE_INVALID', message: 'could not parse JWE protected header' }) + encrypted.protected = orig + })() + + ;(() => { + const orig = encrypted.protected + delete encrypted.protected + t.throws(() => { + decrypt(encrypted, dKey) + }, { instanceOf: errors.JWEInvalid, code: 'ERR_JWE_INVALID', message: 'missing Key Management algorithm' }) + encrypted.protected = orig + })() + + ;['iv', 'ciphertext', 'tag'].forEach((prop) => { + const orig = encrypted[prop] + + if (encrypted[prop].startsWith('-')) { + encrypted[prop] = `Q${encrypted[prop].substr(1)}` + } else { + encrypted[prop] = `-${encrypted[prop].substr(1)}` + } + t.throws(() => { + decrypt(encrypted, dKey) + }, { instanceOf: errors.JWEDecryptionFailed, code: 'ERR_JWE_DECRYPTION_FAILED' }) + encrypted[prop] = orig + }) + + ;['iv', 'tag'].forEach((prop) => { + const orig = encrypted[prop] + + encrypted[prop] = encrypted[prop].substr(4) + t.throws(() => { + decrypt(encrypted, dKey) + }, { instanceOf: errors.JWEInvalid, code: 'ERR_JWE_INVALID' }) + encrypted[prop] = orig + }) + } +} + module.exports.compactJwt = compactJwt module.exports.hasNoProperties = hasNoProperties module.exports.hasProperties = hasProperties module.exports.hasProperty = hasProperty +module.exports.JWE = JWE +module.exports.JWS = JWS