Skip to content

Commit

Permalink
feat: add OKP Key and EdDSA sign/verify support
Browse files Browse the repository at this point in the history
resolves #12
  • Loading branch information
panva committed Apr 7, 2019
1 parent d51cfb5 commit c2781c1
Show file tree
Hide file tree
Showing 39 changed files with 1,085 additions and 103 deletions.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 |
Expand All @@ -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 ||
Expand All @@ -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
Expand Down Expand Up @@ -304,13 +307,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
Expand Down
49 changes: 27 additions & 22 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ I can continue maintaining it and adding new features carefree. You may also don
## JWK (JSON Web Key)

<!-- TOC JWK START -->
- [Class: &lt;JWK.Key&gt; and &lt;JWK.RSAKey&gt; &vert; &lt;JWK.ECKey&gt; &vert; &lt;JWK.OctKey&gt;](#class-jwkkey-and-jwkrsakey--jwkeckey--jwkoctkey)
- [Class: &lt;JWK.Key&gt; and &lt;JWK.RSAKey&gt; &vert; &lt;JWK.ECKey&gt; &vert; &lt;JWK.OKPKey&gt; &vert; &lt;JWK.OctKey&gt;](#class-jwkkey-and-jwkrsakey--jwkeckey--jwkokpkey--jwkoctkey)
- [key.kty](#keykty)
- [key.alg](#keyalg)
- [key.use](#keyuse)
Expand Down Expand Up @@ -58,28 +58,30 @@ const { JWK } = require('@panva/jose')

---

#### Class: `<JWK.Key>` and `<JWK.RSAKey>` &vert; `<JWK.ECKey>` &vert; `<JWK.OctKey>`
#### Class: `<JWK.Key>` and `<JWK.RSAKey>` &vert; `<JWK.ECKey>` &vert; `<JWK.OKPKey>` &vert; `<JWK.OctKey>`

`<JWK.RSAKey>`, `<JWK.ECKey>` and `<JWK.OctKey>` represent a key usable for JWS and JWE operations.
`<JWK.RSAKey>`, `<JWK.ECKey>`, `<JWK.OKPKey>` and `<JWK.OctKey>` 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.

`<JWK.RSAKey>`, `<JWK.ECKey>` and `<JWK.OctKey>` inherit methods from `<JWK.Key>` and in addition
`<JWK.RSAKey>`, `<JWK.ECKey>`, `<JWK.OKPKey>` and `<JWK.OctKey>` inherit methods from `<JWK.Key>` and in addition
to the properties documented below have the respective key component properties exported as
`<string>` in their format defined by the specifications.

- `e, n` for Public RSA Keys
- `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.

- `<string>`

Expand Down Expand Up @@ -262,7 +264,7 @@ Private keys may also be passphrase protected.
[RFC7638][spec-thumbprint]
- `use`: `<string>` option indicates whether the key is to be used for encrypting & decrypting
data or signing & verifying data. Must be 'sig' or 'enc'.
- Returns: `<JWK.RSAKey>` &vert; `<JWK.ECKey>`
- Returns: `<JWK.RSAKey>` &vert; `<JWK.ECKey>` &vert; `<JWK.OKPKey>`

See the underlying Node.js API for details on importing private and public keys in the different
formats
Expand Down Expand Up @@ -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`: `<Object>`
- `kty`: `<string>` Key type. Must be 'RSA', 'EC' or 'oct'.
- `kty`: `<string>` Key type. Must be 'RSA', 'EC', 'OKP' or 'oct'.
- `alg`: `<string>` option identifies the algorithm intended for use with the key.
- `use`: `<string>` option indicates whether the key is to be used for encrypting & decrypting
data or signing & verifying data. Must be 'sig' or 'enc'.
Expand All @@ -335,8 +337,10 @@ may be both private and public.
- `e`, `n`, `d`, `p`, `q`, `dp`, `dq`, `qi` properties as `<string>` for RSA private keys
- `crv`, `x`, `y` properties as `<string>` for EC public keys
- `crv`, `x`, `y`, `d` properties as `<string>` for EC private keys
- `crv`, `x`, properties as `<string>` for OKP public keys
- `crv`, `x`, `d` properties as `<string>` for OKP private keys
- `k` properties as `<string>` for secret oct keys
- Returns: `<JWK.RSAKey>` &vert; `<JWK.ECKey>` &vert; `<JWK.OctKey>`
- Returns: `<JWK.RSAKey>` &vert; `<JWK.ECKey>` &vert; `<JWK.OKPKey>` &vert; `<JWK.OctKey>`

<details>
<summary><em><strong>Example</strong></em> (Click to expand)</summary>
Expand Down Expand Up @@ -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`: `<string>` Key type. Must be 'RSA', 'EC' or 'oct'.
- `crvOrSize`: `<number>` &vert; `<string>` key's bit size or in case of EC keys the curve
- `kty`: `<string>` Key type. Must be 'RSA', 'EC', 'OKP' or 'oct'.
- `crvOrSize`: `<number>` &vert; `<string>` 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`: `<Object>`
- `alg`: `<string>` option identifies the algorithm intended for use with the key.
- `kid`: `<string>` Key ID Parameter. When not provided is computed using the method defined in
Expand All @@ -378,7 +383,7 @@ Securely generates a new RSA, EC or oct key.
data or signing & verifying data. Must be 'sig' or 'enc'.
- `private`: `<boolean>` **Default** 'true'. Is the resulting key private or public (when
asymmetrical)
- Returns: `Promise<JWK.RSAKey>` &vert; `Promise<JWK.ECKey>` &vert; `Promise<JWK.OctKey>`
- Returns: `Promise<JWK.RSAKey>` &vert; `Promise<JWK.ECKey>` &vert; `Promise<JWK.OKPKey>` &vert; `Promise<JWK.OctKey>`

<details>
<summary><em><strong>Example</strong></em> (Click to expand)</summary>
Expand Down Expand Up @@ -406,9 +411,9 @@ const { JWK: { generate } } = require('@panva/jose')

Synchronous version of `JWK.generate()`

- `kty`: `<string>` Key type. Must be 'RSA', 'EC' or 'oct'.
- `crvOrSize`: `<number>` &vert; `<string>` 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`: `<string>` Key type. Must be 'RSA', 'EC', 'OKP' or 'oct'.
- `crvOrSize`: `<number>` &vert; `<string>` 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`: `<Object>`
- `alg`: `<string>` option identifies the algorithm intended for use with the key.
- `use`: `<string>` option indicates whether the key is to be used for encrypting & decrypting
Expand All @@ -417,7 +422,7 @@ Synchronous version of `JWK.generate()`
[RFC7638][spec-thumbprint]
- `private`: `<boolean>` **Default** 'true'. Is the resulting key private or public (when
asymmetrical)
- Returns: `<JWK.RSAKey>` &vert; `<JWK.ECKey>` &vert; `<JWK.OctKey>`
- Returns: `<JWK.RSAKey>` &vert; `<JWK.ECKey>` &vert; `<JWK.OKPKey>` &vert; `<JWK.OctKey>`

<details>
<summary><em><strong>Example</strong></em> (Click to expand)</summary>
Expand Down Expand Up @@ -527,23 +532,23 @@ parameters is returned.
- `kid`: `<string>` Key ID to filter for.
- `operation`: `<string>` Further specify the operation a given alg must be valid for. Must be one
of 'encrypt', 'decrypt', 'sign', 'verify', 'wrapKey', 'unwrapKey'
- Returns: `<JWK.RSAKey>` &vert; `<JWK.ECKey>` &vert; `<JWK.OctKey>` &vert; `<undefined>`
- Returns: `<JWK.RSAKey>` &vert; `<JWK.ECKey>` &vert; `<JWK.OKPKey>` &vert; `<JWK.OctKey>` &vert; `<undefined>`

---

#### `keystore.add(key)`

Adds a key instance to the store unless it is already included.

- `key`: `<JWK.RSAKey>` &vert; `<JWK.ECKey>` &vert; `<JWK.OctKey>`
- `key`: `<JWK.RSAKey>` &vert; `<JWK.ECKey>` &vert; `<JWK.OKPKey>` &vert; `<JWK.OctKey>`

---

#### `keystore.remove(key)`

Ensures a key is removed from a store.

- `key`: `<JWK.RSAKey>` &vert; `<JWK.ECKey>` &vert; `<JWK.OctKey>`
- `key`: `<JWK.RSAKey>` &vert; `<JWK.ECKey>` &vert; `<JWK.OKPKey>` &vert; `<JWK.OctKey>`

---

Expand Down
6 changes: 6 additions & 0 deletions lib/help/asn1/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
7 changes: 7 additions & 0 deletions lib/help/asn1/one_asymmetric_key.js
Original file line number Diff line number Diff line change
@@ -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)
)
}
5 changes: 5 additions & 0 deletions lib/help/asn1/private_key.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = function () {
this.octstr().contains().obj(
this.key('privateKey').octstr()
)
}
8 changes: 0 additions & 8 deletions lib/help/key_object.js

This file was deleted.

108 changes: 108 additions & 0 deletions lib/help/key_utils.js
Original file line number Diff line number Diff line change
@@ -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'],
Expand All @@ -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) {
Expand Down Expand Up @@ -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)
}
}
}

Expand Down Expand Up @@ -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('.')
}
}

Expand All @@ -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:
Expand Down
Loading

0 comments on commit c2781c1

Please sign in to comment.