Skip to content

Commit

Permalink
feat: add opt-in support for Unsecured JWS algorithm "none"
Browse files Browse the repository at this point in the history
  • Loading branch information
panva committed Jan 23, 2020
1 parent 0f8bf88 commit 3a6d17f
Show file tree
Hide file tree
Showing 11 changed files with 365 additions and 127 deletions.
165 changes: 65 additions & 100 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,82 +28,6 @@ Available JWT validation profiles
- OAuth 2.0 JWT Access Tokens (`at+JWT`) - [JWT Profile for OAuth 2.0 Access Tokens][draft-ietf-oauth-access-token-jwt]
- OIDC Logout Token (`logout_token`) - [OpenID Connect Back-Channel Logout 1.0][spec-oidc-logout_token]

<details>
<summary><em><strong>Detailed feature matrix</strong></em> (Click to expand)</summary><br>

Legend:
- **** Implemented
- **** Missing node crypto support / won't implement
- **** TBD

| JWK Key Types | Supported | `kty` |
| -- | -- | -- |
| RSA || RSA |
| Elliptic Curve || EC (P-256, secp256k1, P-384, P-521) |
| Octet Key Pair || OKP (Ed25519, Ed448, X25519, X448) |
| Octet sequence || oct |

| Serialization | JWS Sign | JWS Verify | JWE Encrypt | JWE Decrypt |
| -- | -- | -- | -- | -- |
| Compact |||||
| General JSON |||||
| Flattened JSON |||||

| JWS Algorithms | Supported ||
| -- | -- | -- |
| RSASSA-PKCS1-v1_5 || RS256, RS384, RS512 |
| RSASSA-PSS || PS256, PS384, PS512 |
| ECDSA || ES256, ES256K, ES384, ES512 |
| Edwards-curve DSA || EdDSA |
| HMAC with SHA-2 || HS256, HS384, HS512 |

| JWE Key Management Algorithms | Supported ||
| -- | -- | -- |
| AES || A128KW, A192KW, A256KW |
| AES GCM || A128GCMKW, A192GCMKW, A256GCMKW |
| Direct Key Agreement || dir |
| RSAES OAEP || RSA-OAEP, RSA-OAEP-256 |
| RSAES-PKCS1-v1_5 || RSA1_5 |
| PBES2 || PBES2-HS256+A128KW, PBES2-HS384+A192KW, PBES2-HS512+A256KW |
| ECDH-ES (for all EC keys) || ECDH-ES, ECDH-ES+A128KW, ECDH-ES+A192KW, ECDH-ES+A256KW |
| ECDH-ES (for OKP X25519) | ✓ via [plugin][plugin-x25519] | ECDH-ES, ECDH-ES+A128KW, ECDH-ES+A192KW, ECDH-ES+A256KW |
| ECDH-ES (for OKP X448) |||
| (X)ChaCha | ✓ via [plugin][plugin-chacha] | C20PKW, X20CPKW, ECDH-ES+C20PKW, ECDH-ES+XC20PKW |

| JWE Content Encryption Algorithms | Supported ||
| -- | -- | -- |
| AES GCM || A128GCM, A192GCM, A256GCM |
| AES_CBC_HMAC_SHA2 || A128CBC-HS256, A192CBC-HS384, A256CBC-HS512 |
| (X)ChaCha | ✓ via [plugin][plugin-chacha] | C20P, X20CP |

| JWT profile validation | Supported | profile option value |
| -- | -- | -- |
| ID Token - [OpenID Connect Core 1.0][spec-oidc-id_token] || `id_token` |
| JWT Access Tokens [JWT Profile for OAuth 2.0 Access Tokens][draft-ietf-oauth-access-token-jwt] || `at+JWT` |
| Logout Token - [OpenID Connect Back-Channel Logout 1.0][spec-oidc-logout_token] || `logout_token` |
| JARM - [JWT Secured Authorization Response Mode for OAuth 2.0][draft-jarm] |||

Notes
- RSA-OAEP-256 JWE algorithm is only supported when Node.js >= 12.9.0 runtime is detected
- Importing X.509 certificates and handling `x5c` is only supported when Node.js >= 12.0.0 runtime is detected
- OKP keys are only supported when Node.js >= 12.0.0 runtime is detected
- See [#electron-support](#electron-support) for electron exceptions

---

Pending Node.js Support 🤞:
- ECDH-ES with X25519 and X448 - see [nodejs/node#26626](https://github.com/nodejs/node/pull/26626)

Won't implement:
- ✕ JWS embedded key / referenced verification
- one can decode the header and pass the (`x5c`, `jwk`) to `JWK.asKey` and validate with that
key, similarly the application can handle fetching and then instantiating the referenced `x5u`
or `jku` in its own code. This way you opt-in to these behaviours.
- ✕ "none" alg support
- no crypto, no use

</details>

<br>

Have a question about using `jose`? - [ask][ask].
Expand All @@ -128,15 +52,6 @@ If you or your business use `jose`, please consider becoming a [sponsor][support
- [JWS (JSON Web Signature)][documentation-jws]
- [JWE (JSON Web Encryption)][documentation-jwe]

## Plugins

There are two plugin extensions with functionality which is either not available in Node.js `crypto`
module yet and therefore needs a crypto polyfill (libsodium), or are not IETF WG standards/drafts
"worthy" of landing in the core library.

- [jose-chacha][plugin-chacha] adds aead_chacha20_poly1305 and aead_xchacha20_poly1305 based algorithms (individual draft)
- [jose-x25519-ecdh][plugin-x25519] adds OKP X25519 curve keys ECDH-ES support (missing Node.js `crypto` support)

## Usage

For the best performance Node.js version **>=12.0.0** is recommended, but **^10.13.0** lts/dubnium
Expand Down Expand Up @@ -356,17 +271,66 @@ jose.JWE.decrypt(
)
```

#### Electron Support
## Detailed Support Matrix

| JWK Key Types | Supported | `kty` value | `crv` values |
| -- | -- | -- | -- |
| RSA || RSA ||
| Elliptic Curve || EC | P-256, secp256k1<sup>[1]</sup>, P-384, P-521 |
| Octet Key Pair || OKP | Ed25519, Ed448<sup>[1]</sup>, X25519<sup>[1]</sup>, X448<sup>[1]</sup> |
| Octet sequence || oct ||

| Serialization | JWS Sign | JWS Verify | JWE Encrypt | JWE Decrypt |
| -- | -- | -- | -- | -- |
| Compact |||||
| General JSON |||||
| Flattened JSON |||||

| JWS Algorithms | Supported ||
| -- | -- | -- |
| RSASSA-PKCS1-v1_5 || RS256, RS384, RS512 |
| RSASSA-PSS || PS256, PS384, PS512 |
| ECDSA || ES256, ES256K<sup>[1]</sup>, ES384, ES512 |
| Edwards-curve DSA || EdDSA |
| HMAC with SHA-2 || HS256, HS384, HS512 |
| Unsecured JWS || none<sup>[2]</sup> |

Electron >=6.0.0 runtime is supported to the extent of the crypto engine BoringSSL feature parity
with standard Node.js OpenSSL. The following is disabled in Electron runtime because of its lack of
[support](https://github.com/panva/jose/blob/master/test/electron/electron.test.js).
| JWE Key Management Algorithms | Supported ||
| -- | -- | -- |
| AES || A128KW<sup>[1]</sup>, A192KW<sup>[1]</sup>, A256KW<sup>[1]</sup> |
| AES GCM || A128GCMKW, A192GCMKW, A256GCMKW |
| Direct Key Agreement || dir |
| RSAES OAEP || RSA-OAEP, RSA-OAEP-256<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) |||
| (X)ChaCha | ✓ <sup>via [plugin][plugin-chacha]</sup> | C20PKW, XC20PKW, ECDH-ES+C20PKW, ECDH-ES+XC20PKW |

| JWE Content Encryption Algorithms | Supported ||
| -- | -- | -- |
| AES GCM || A128GCM, A192GCM, A256GCM |
| AES_CBC_HMAC_SHA2 || A128CBC-HS256, A192CBC-HS384, A256CBC-HS512 |
| (X)ChaCha | ✓ <sup>via [plugin][plugin-chacha]</sup> | C20P, X20CP |

| JWT profile validation | Supported | profile option value |
| -- | -- | -- |
| ID Token - [OpenID Connect Core 1.0][spec-oidc-id_token] || `id_token` |
| JWT Access Tokens [JWT Profile for OAuth 2.0 Access Tokens][draft-ietf-oauth-access-token-jwt] || `at+JWT` |
| Logout Token - [OpenID Connect Back-Channel Logout 1.0][spec-oidc-logout_token] || `logout_token` |
| JARM - [JWT Secured Authorization Response Mode for OAuth 2.0][draft-jarm] |||

Legend:
- **** Implemented
- **** Missing node crypto support / won't implement
- **** TBD

- JWE `A128KW`, `A192KW` and `A256KW` algorithms are not available, this also means that other JWAs
depending on those are not working, those are `ECDH-ES+A128KW`, `ECDH-ES+A192KW`,
`ECDH-ES+A256KW`, `PBES2-HS256+A128KW`, `PBES2-HS384+A192KW`, `PBES2-HS512+A256KW`)
- OKP curves `Ed448`, `X25519` and `X448` are not supported
- EC curve `secp256k1` is not supported
<sup>1</sup> Not supported in Electron due to Electron's use of BoringSSL
<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` instance that cannot be instantiated through the key import API.
<sup>3</sup> RSA-OAEP-256 is only supported when Node.js >= 12.9.0 runtime is detected

## FAQ

Expand Down Expand Up @@ -418,12 +382,13 @@ in terms of performance and API (not having well defined errors).

[ask]: https://github.com/panva/jose/issues/new?labels=question&template=question.md&title=question%3A+
[bug]: https://github.com/panva/jose/issues/new?labels=bug&template=bug-report.md&title=bug%3A+
[documentation-jwe]: https://github.com/panva/jose/blob/master/docs/README.md#jwe-json-web-encryption
[documentation-jwk]: https://github.com/panva/jose/blob/master/docs/README.md#jwk-json-web-key
[documentation-jwks]: https://github.com/panva/jose/blob/master/docs/README.md#jwks-json-web-key-set
[documentation-jws]: https://github.com/panva/jose/blob/master/docs/README.md#jws-json-web-signature
[documentation-jwt]: https://github.com/panva/jose/blob/master/docs/README.md#jwt-json-web-token
[documentation]: https://github.com/panva/jose/blob/master/docs/README.md
[documentation-jwe]: /docs/README.md#jwe-json-web-encryption
[documentation-jwk]: /docs/README.md#jwk-json-web-key
[documentation-jwks]: /docs/README.md#jwks-json-web-key-set
[documentation-jws]: /docs/README.md#jws-json-web-signature
[documentation-jwt]: /docs/README.md#jwt-json-web-token
[documentation-none]: /docs/README.md#jwknone
[documentation]: /docs/README.md
[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
Expand Down
49 changes: 49 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ If you or your business use `jose`, please consider becoming a [sponsor][support
- [JWK.generate(kty[, crvOrSize[, options[, private]]]) generating new keys](#jwkgeneratekty-crvorsize-options-private-generating-new-keys)
- [JWK.generateSync(kty[, crvOrSize[, options[, private]]])](#jwkgeneratesynckty-crvorsize-options-private)
- [JWK.isKey(object)](#jwkiskeyobject)
- [JWK.None](#jwknone)
<!-- TOC JWK END -->

All sign and encrypt operations require `<JWK.Key>` or `JWK.asKey()` compatible input.
Expand Down Expand Up @@ -574,6 +575,54 @@ Returns 'true' if the value is an instance of `<JWK.Key>`.

---

#### `JWK.None`

`JWK.None` is a special key object that can be used with JWS/JWT sign and verify whenever you want
to opt-in for the `none` Unsecured JWS algorithm. Using this key fulfills the requirements given by
the [specification](https://tools.ietf.org/html/rfc7518#section-3.6), namely:

- Implementations MUST NOT accept Unsecured JWSs by default.
- Implementations that support Unsecured JWSs MUST NOT accept such objects as valid unless the
application specifies that it is acceptable for a specific object to not be integrity protected.

```js
const { JWK: { None, generateSync }, JWT, JWS } = require('jose')
const anActualKey = generateSync('RSA')

const signedJWT = JWT.sign({ sub: 'John Doe' }, anActualKey)
JWT.verify(signedJWT, None)
// Thrown:
// JWKKeySupport: the key does not support PS256 verify algorithm
// name: 'JWKKeySupport',
// code: 'ERR_JWK_KEY_SUPPORT'

const unsecuredJWT = JWT.sign({ sub: 'John Doe' }, None)
// eyJhbGciOiJub25lIn0.eyJzdWIiOiJKb2huIERvZSIsImlhdCI6MTU3OTc5NDM2Mn0.

JWT.verify(unsecuredJWT, anActualKey)
// Thrown:
// JWKKeySupport: the key does not support none verify algorithm
// name: 'JWKKeySupport',
// code: 'ERR_JWK_KEY_SUPPORT'

JWT.verify(unsecuredJWT, None)
// { sub: 'John Doe', iat: 1579794362 }

const unsecuredJWS = JWS.sign('foobar', None)
// eyJhbGciOiJub25lIn0.Zm9vYmFy.

JWS.verify(unsecuredJWS, anActualKey)
// Thrown:
// JWKKeySupport: the key does not support none verify algorithm
// name: 'JWKKeySupport',
// code: 'ERR_JWK_KEY_SUPPORT'

JWS.verify(unsecuredJWS, None)
// foobar
```

---

## JWKS (JSON Web Key Set)

<!-- TOC JWKS START -->
Expand Down
1 change: 1 addition & 0 deletions lib/jwa/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require('./ecdsa')(JWA, JWK)
require('./eddsa')(JWA, JWK)
require('./rsassa_pss')(JWA, JWK)
require('./rsassa')(JWA, JWK)
require('./none')(JWA)

// encrypt, decrypt
require('./aes_cbc_hmac_sha2')(JWA, JWK)
Expand Down
14 changes: 14 additions & 0 deletions lib/jwa/none.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const { strict: assert } = require('assert')

const sign = (key, payload) => Buffer.from('')
const verify = (key, payload, signature) => !signature.length

module.exports = (JWA, JWK) => {
const jwaAlg = 'none'

assert(!JWA.sign.has(jwaAlg), `sign alg ${jwaAlg} already registered`)
assert(!JWA.verify.has(jwaAlg), `verify alg ${jwaAlg} already registered`)

JWA.sign.set(jwaAlg, sign)
JWA.verify.set(jwaAlg, verify)
}
13 changes: 8 additions & 5 deletions lib/jwk/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
const Key = require('./key/base')
const None = require('./key/none')
const importKey = require('./import')
const { generate, generateSync } = require('./generate')
const generate = require('./generate')

module.exports.asKey = importKey
module.exports.generate = generate
module.exports.generateSync = generateSync
module.exports.isKey = input => input instanceof Key
module.exports = {
...generate,
asKey: importKey,
isKey: input => input instanceof Key,
None
}

/* deprecated */
Object.defineProperty(module.exports, 'importKey', {
Expand Down
33 changes: 33 additions & 0 deletions lib/jwk/key/none.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const { inspect } = require('util')

const Key = require('./base')

class NoneKey extends Key {
constructor () {
super({ type: 'unsecured' }, { alg: 'none' })
Object.defineProperties(this, {
kid: { value: undefined },
thumbprint: { value: undefined },
toJWK: { value: undefined },
toPEM: { value: undefined }
})
}

/* c8 ignore next 3 */
[inspect.custom] () {
return 'None {}'
}

algorithms (operation) {
switch (operation) {
case 'sign':
case 'verify':
case undefined:
return new Set(['none'])
default:
return new Set()
}
}
}

module.exports = new NoneKey({ type: 'unsecured' }, { alg: 'none' })
11 changes: 5 additions & 6 deletions lib/jwks/keystore.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ const { deprecate, inspect } = require('util')

const isObject = require('../help/is_object')
const { generate, generateSync } = require('../jwk/generate')
const Key = require('../jwk/key/base')
const importKey = require('../jwk/import')
const { USES_MAPPING } = require('../help/consts')
const { None, isKey, asKey: importKey } = require('../jwk')

const keyscore = (key, { alg, use, ops }) => {
let score = 0
Expand Down Expand Up @@ -45,8 +44,8 @@ class KeyStore {
return acc
}, [])
}
if (keys.some(k => !(k instanceof Key))) {
throw new TypeError('all keys must be an instances of a key instantiated by JWK.asKey')
if (keys.some(k => !isKey(k) || k === None)) {
throw new TypeError('all keys must be instances of a key instantiated by JWK.asKey')
}

i(this).keys = new Set(keys)
Expand Down Expand Up @@ -113,15 +112,15 @@ class KeyStore {
}

add (key) {
if (!(key instanceof Key)) {
if (!isKey(key) || key === None) {
throw new TypeError('key must be an instance of a key instantiated by JWK.asKey')
}

i(this).keys.add(key)
}

remove (key) {
if (!(key instanceof Key)) {
if (!isKey(key)) {
throw new TypeError('key must be an instance of a key instantiated by JWK.asKey')
}

Expand Down
2 changes: 1 addition & 1 deletion test/jwks/keystore.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ test('constructor', t => {
test('constructor only accepts Key instances created through JWK.asKey', t => {
t.throws(() => {
new KeyStore({}) // eslint-disable-line no-new
}, { instanceOf: TypeError, message: 'all keys must be an instances of a key instantiated by JWK.asKey' })
}, { instanceOf: TypeError, message: 'all keys must be instances of a key instantiated by JWK.asKey' })
})

test('.generate()', async t => {
Expand Down
Loading

0 comments on commit 3a6d17f

Please sign in to comment.