Skip to content

Commit

Permalink
feat: add ECDH-ES with X25519 and X448 OKP keys
Browse files Browse the repository at this point in the history
  • Loading branch information
panva committed Feb 13, 2020
1 parent 594c3e4 commit 38369ea
Show file tree
Hide file tree
Showing 7 changed files with 230 additions and 42 deletions.
8 changes: 3 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -303,9 +303,7 @@ jose.JWE.decrypt(
| RSAES OAEP || RSA-OAEP, RSA-OAEP-256<sup>[3]</sup>, RSA-OAEP-384<sup>[3]</sup>, RSA-OAEP-512<sup>[3]</sup> |
| RSAES-PKCS1-v1_5 || RSA1_5 |
| PBES2 || PBES2-HS256+A128KW<sup>[1]</sup>, PBES2-HS384+A192KW<sup>[1]</sup>, PBES2-HS512+A256KW<sup>[1]</sup> |
| ECDH-ES (for all EC keys) || ECDH-ES, ECDH-ES+A128KW<sup>[1]</sup>, ECDH-ES+A192KW<sup>[1]</sup>, ECDH-ES+A256KW<sup>[1]</sup> |
| ECDH-ES (for OKP X25519) | ✓ <sup>via [plugin][plugin-x25519]</sup> | ECDH-ES, ECDH-ES+A128KW, ECDH-ES+A192KW, ECDH-ES+A256KW |
| ECDH-ES (for OKP X448) |||
| ECDH-ES | ✓<sup>[4]</sup> | ECDH-ES, ECDH-ES+A128KW<sup>[1]</sup>, ECDH-ES+A192KW<sup>[1]</sup>, ECDH-ES+A256KW<sup>[1]</sup> |
| (X)ChaCha | ✓ <sup>via [plugin][plugin-chacha]</sup> | C20PKW, XC20PKW, ECDH-ES+C20PKW, ECDH-ES+XC20PKW |

| JWE Content Encryption Algorithms | Supported ||
Expand All @@ -330,7 +328,8 @@ Legend:
<sup>2</sup> Unsecured JWS is [supported][documentation-none] for the JWS and JWT sign and verify
operations but it is an entirely opt-in behaviour, downgrade attacks are prevented by the required
use of a special `JWK.Key`-like object that cannot be instantiated through the key import API
<sup>3</sup> RSAES OAEP using SHA-2 and MGF1 with SHA-2 is only supported when Node.js >= 12.9.0 runtime is detected
<sup>3</sup> RSAES OAEP using SHA-2 and MGF1 with SHA-2 is only supported when Node.js >= 12.9.0 runtime is detected
<sup>4</sup> ECDH-ES with X25519 and X448 keys is only supported when Node.js >= 13.9.0 runtime is detected

## FAQ

Expand Down Expand Up @@ -409,5 +408,4 @@ in terms of performance and API (not having well defined errors).
[suggest-feature]: https://github.com/panva/jose/issues/new?labels=enhancement&template=feature-request.md&title=proposal%3A+
[support-sponsor]: https://github.com/sponsors/panva
[sponsor-auth0]: https://auth0.com/overview?utm_source=GHsponsor&utm_medium=GHsponsor&utm_campaign=panva-jose&utm_content=auth
[plugin-x25519]: https://github.com/panva/jose-x25519-ecdh
[plugin-chacha]: https://github.com/panva/jose-chacha
5 changes: 3 additions & 2 deletions lib/help/runtime_support.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
const { KeyObject, sign, verify } = require('crypto')
const { diffieHellman, KeyObject, sign, verify } = require('crypto')

const [major, minor] = process.version.substr(1).split('.').map(x => parseInt(x, 10))

module.exports = {
oaepHashSupported: major > 12 || (major === 12 && minor >= 9),
keyObjectSupported: !!KeyObject && major >= 12,
edDSASupported: !!sign && !!verify,
dsaEncodingSupported: major > 13 || (major === 13 && minor >= 2) || (major === 12 && minor >= 16)
dsaEncodingSupported: major > 13 || (major === 13 && minor >= 2) || (major === 12 && minor >= 16),
improvedDH: !!diffieHellman
}
64 changes: 37 additions & 27 deletions lib/jwa/ecdh/compute_secret.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,43 @@
const { createECDH, constants: { POINT_CONVERSION_UNCOMPRESSED } } = require('crypto')

const base64url = require('../../help/base64url')
const { name: secp256k1 } = require('../../jwk/key/secp256k1_crv')

const crvToCurve = (crv) => {
switch (crv) {
case 'P-256':
return 'prime256v1'
case 'P-384':
return 'secp384r1'
case 'P-521':
return 'secp521r1'
case 'secp256k1':
case 'X448':
case 'X25519':
return crv
case secp256k1:
return 'secp256k1'
const { improvedDH } = require('../../help/runtime_support')

if (improvedDH) {
const { diffieHellman } = require('crypto')

const { KeyObject } = require('../../help/key_object')
const importKey = require('../../jwk/import')

module.exports = ({ keyObject: privateKey }, publicKey) => {
if (!(publicKey instanceof KeyObject)) {
({ keyObject: publicKey } = importKey(publicKey))
}

return diffieHellman({ privateKey, publicKey })
}
}
} else {
const { createECDH, constants: { POINT_CONVERSION_UNCOMPRESSED } } = require('crypto')

const UNCOMPRESSED = Buffer.alloc(1, POINT_CONVERSION_UNCOMPRESSED)
const pubToBuffer = (x, y) => Buffer.concat([UNCOMPRESSED, base64url.decodeToBuffer(x), base64url.decodeToBuffer(y)])
const base64url = require('../../help/base64url')

module.exports = ({ crv, d }, { x, y = '' }) => {
const curve = crvToCurve(crv)
const exchange = createECDH(curve)
const crvToCurve = (crv) => {
switch (crv) {
case 'P-256':
return 'prime256v1'
case 'P-384':
return 'secp384r1'
case 'P-521':
return 'secp521r1'
}
}

const UNCOMPRESSED = Buffer.alloc(1, POINT_CONVERSION_UNCOMPRESSED)
const pubToBuffer = (x, y) => Buffer.concat([UNCOMPRESSED, base64url.decodeToBuffer(x), base64url.decodeToBuffer(y)])

exchange.setPrivateKey(base64url.decodeToBuffer(d))
module.exports = ({ crv, d }, { x, y }) => {
const curve = crvToCurve(crv)
const exchange = createECDH(curve)

return exchange.computeSecret(pubToBuffer(x, y))
exchange.setPrivateKey(base64url.decodeToBuffer(d))

return exchange.computeSecret(pubToBuffer(x, y))
}
}
5 changes: 5 additions & 0 deletions lib/jwa/ecdh/dir.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const { improvedDH } = require('../../help/runtime_support')
const { KEYLENGTHS } = require('../../registry')
const { generateSync } = require('../../jwk/generate')
const { name: secp256k1 } = require('../../jwk/key/secp256k1_crv')
Expand All @@ -24,4 +25,8 @@ module.exports = (JWA, JWK) => {
JWA.keyManagementEncrypt.set('ECDH-ES', wrapKey)
JWA.keyManagementDecrypt.set('ECDH-ES', unwrapKey)
JWK.EC.deriveKey['ECDH-ES'] = key => (key.use === 'enc' || key.use === undefined) && key.crv !== secp256k1

if (improvedDH) {
JWK.OKP.deriveKey['ECDH-ES'] = key => (key.use === 'enc' || key.use === undefined) && key.keyObject.asymmetricKeyType.startsWith('x')
}
}
5 changes: 5 additions & 0 deletions lib/jwa/ecdh/kw.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const { improvedDH } = require('../../help/runtime_support')
const { KEYOBJECT } = require('../../help/consts')
const { generateSync } = require('../../jwk/generate')
const { name: secp256k1 } = require('../../jwk/key/secp256k1_crv')
Expand Down Expand Up @@ -36,6 +37,10 @@ module.exports = (JWA, JWK) => {
JWA.keyManagementEncrypt.set(jwaAlg, wrapKey.bind(undefined, kwWrap, derive.bind(undefined, jwaAlg, keylen)))
JWA.keyManagementDecrypt.set(jwaAlg, unwrapKey.bind(undefined, kwUnwrap, derive.bind(undefined, jwaAlg, keylen)))
JWK.EC.deriveKey[jwaAlg] = key => (key.use === 'enc' || key.use === undefined) && key.crv !== secp256k1

if (improvedDH) {
JWK.OKP.deriveKey[jwaAlg] = key => (key.use === 'enc' || key.use === undefined) && key.keyObject.asymmetricKeyType.startsWith('x')
}
}
})
}
Expand Down
21 changes: 13 additions & 8 deletions test/jwk/okp_enc.test.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
const test = require('ava')

const { keyObjectSupported } = require('../../lib/help/runtime_support')
const { improvedDH, keyObjectSupported } = require('../../lib/help/runtime_support')

if ('electron' in process.versions || !keyObjectSupported) return

if (!improvedDH) {
require('./okp_enc_no_dh')
return
}

const { createPrivateKey, createPublicKey } = require('crypto')
const { hasProperty, hasNoProperties, hasProperties } = require('../macros')
const fixtures = require('../fixtures')
Expand Down Expand Up @@ -43,14 +48,14 @@ Object.entries({
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'])
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])
t.deepEqual([...result], [alg])
})

test(`${crv} OKP Private key does not support sign alg (no use)`, t => {
Expand Down Expand Up @@ -80,7 +85,7 @@ Object.entries({
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'])
t.deepEqual([...result], [])
})

test(`${crv} OKP Private key .algorithms("wrapKey") when use is sig`, t => {
Expand All @@ -93,7 +98,7 @@ Object.entries({
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'])
t.deepEqual([...result], [])
})
})()

Expand All @@ -119,14 +124,14 @@ Object.entries({
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'])
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])
t.deepEqual([...result], [alg])
})

test(`${crv} OKP Public key cannot sign`, t => {
Expand Down Expand Up @@ -156,7 +161,7 @@ Object.entries({
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'])
t.deepEqual([...result], [])
})

test(`${crv} OKP Public key .algorithms("unwrapKey")`, t => {
Expand Down
164 changes: 164 additions & 0 deletions test/jwk/okp_enc_no_dh.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
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], [])
})
})()
})

0 comments on commit 38369ea

Please sign in to comment.