Skip to content

Commit

Permalink
fix: limit calculation of missing RSA private components
Browse files Browse the repository at this point in the history
- this deprecates the use of `JWK.importKey` in favor of
`JWK.asKey`
- this deprecates the use of `JWKS.KeyStore.fromJWKS` in favor of
`JWKS.asKeyStore`

Both `JWK.importKey` and `JWKS.KeyStore.fromJWKS` could have resulted
in the process getting blocked when large bitsize RSA private keys
were missing their components and could also result in an endless
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
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
its original on-by-default form - v1.1.0 and v1.2.0 These will
  • Loading branch information
panva committed Jun 20, 2019
1 parent 80cdd4f commit 5b53cb0
Show file tree
Hide file tree
Showing 53 changed files with 359 additions and 245 deletions.
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.

# [1.2.0](https://github.com/panva/jose/compare/v1.1.0...v1.2.0) (2019-05-25)
# YANKED [1.2.0](https://github.com/panva/jose/compare/v1.1.0...v1.2.0) (2019-05-25)


### Features
Expand All @@ -12,7 +12,7 @@ All notable changes to this project will be documented in this file. See [standa



# [1.1.0](https://github.com/panva/jose/compare/v1.0.2...v1.1.0) (2019-05-23)
# YANKED [1.1.0](https://github.com/panva/jose/compare/v1.0.2...v1.1.0) (2019-05-23)


### Bug Fixes
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ Pending Node.js Support 🤞:

Won't implement:
- ✕ JWS embedded key / referenced verification
- one can decode the header and pass the (`x5c`, `jwk`) to `JWK.importKey` and validate with that
- 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.
- ✕ JWS detached content
Expand Down Expand Up @@ -137,14 +137,14 @@ const {
Prepare your Keys and KeyStores. See the [documentation][documentation-jwk] for more.

```js
const key = jose.JWK.importKey(fs.readFileSync('path/to/key/file'))
const key = jose.JWK.asKey(fs.readFileSync('path/to/key/file'))

const jwk = { kty: 'EC',
kid: 'dl4M_fcI7XoFCsQ22PYrQBkuxZ2pDcbDimcdFmmXM98',
crv: 'P-256',
x: 'v37avifcL-xgh8cy6IFzcINqqmFLc2JF20XUpn4Y2uQ',
y: 'QTwy27XgP7ZMOdGOSopAHB-FU1JMQn3J9GEWGtUXreQ' }
const anotherKey = jose.JWK.importKey(jwk)
const anotherKey = jose.JWK.asKey(jwk)

const keystore = new jose.JWK.KeyStore(key, key2)
```
Expand Down
92 changes: 57 additions & 35 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ I can continue maintaining it and adding new features carefree. You may also don
- [key.algorithms([operation])](#keyalgorithmsoperation)
- [key.toJWK([private])](#keytojwkprivate)
- [key.toPEM([private[, encoding]])](#keytopemprivate-encoding)
- JWK.importKey
- [JWK.importKey(key[, options]) asymmetric key import](#jwkimportkeykey-options-asymmetric-key-import)
- [JWK.importKey(secret[, options]) secret key import](#jwkimportkeysecret-options-secret-key-import)
- [JWK.importKey(jwk) JWK-formatted key import](#jwkimportkeyjwk-jwk-formatted-key-import)
- JWK.asKey
- [JWK.asKey(key[, options]) asymmetric key import](#jwkaskeykey-options-asymmetric-key-import)
- [JWK.asKey(secret[, options]) secret key import](#jwkaskeysecret-options-secret-key-import)
- [JWK.asKey(jwk[, options]) JWK-formatted key import](#jwkaskeyjwk-options-jwk-formatted-key-import)
- [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)
Expand All @@ -60,7 +60,7 @@ how to get a `<JWK.Key>` instances generated or instantiated from existing key m

```js
const { JWK } = require('@panva/jose')
// { importKey: [Function: importKey],
// { asKey: [Function: asKey],
// generate: [AsyncFunction: generate],
// generateSync: [Function: generateSync] }
```
Expand All @@ -70,7 +70,7 @@ const { JWK } = require('@panva/jose')
#### Class: `<JWK.Key>` and `<JWK.RSAKey>` &vert; `<JWK.ECKey>` &vert; `<JWK.OKPKey>` &vert; `<JWK.OctKey>`

`<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.
The `JWK.asKey()` 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>`, `<JWK.OKPKey>` and `<JWK.OctKey>` inherit methods from `<JWK.Key>` and in addition
Expand Down Expand Up @@ -330,7 +330,7 @@ key.toPEM(true, { passphrase: 'super-strong', cipher: 'aes-256-cbc' })

---

#### `JWK.importKey(key[, options])` asymmetric key import
#### `JWK.asKey(key[, options])` asymmetric key import

Imports an asymmetric private or public key. Supports importing JWK formatted keys (private, public,
secrets), `pem` and `der` formatted private and public keys, `pem` formatted X.509 certificates.
Expand Down Expand Up @@ -362,9 +362,9 @@ formats

```js
const { readFileSync } = require('fs')
const { JWK: { importKey } } = require('@panva/jose')
const { JWK: { asKey } } = require('@panva/jose')

const key = importKey(readFileSync('path/to/key/file'))
const key = asKey(readFileSync('path/to/key/file'))
// ECKey {
// kty: 'EC',
// public: true,
Expand All @@ -377,7 +377,7 @@ const key = importKey(readFileSync('path/to/key/file'))

---

#### `JWK.importKey(secret[, options])` secret key import
#### `JWK.asKey(secret[, options])` secret key import

Imports a symmetric key.

Expand All @@ -394,9 +394,9 @@ Imports a symmetric key.
<summary><em><strong>Example</strong></em> (Click to expand)</summary>

```js
const { JWK: { importKey } } = require('@panva/jose')
const { JWK: { asKey } } = require('@panva/jose')

const key = importKey(Buffer.from('8yHym6h5CG5FylbzrCn8fhxEbp3kOaTsgLaawaaJ'))
const key = asKey(Buffer.from('8yHym6h5CG5FylbzrCn8fhxEbp3kOaTsgLaawaaJ'))
// OctKey {
// kty: 'oct',
// kid: [Getter],
Expand All @@ -406,7 +406,7 @@ const key = importKey(Buffer.from('8yHym6h5CG5FylbzrCn8fhxEbp3kOaTsgLaawaaJ'))

---

#### `JWK.importKey(jwk)` JWK-formatted key import
#### `JWK.asKey(jwk[, options])` JWK-formatted key import

Imports a JWK formatted key. This supports JWK formatted RSA, EC, OKP and oct keys. Asymmetrical
keys may be both private and public.
Expand All @@ -420,18 +420,28 @@ keys may be both private and public.
[RFC7638][spec-thumbprint]
- `e`, `n` properties as `<string>` for RSA public keys
- `e`, `n`, `d`, `p`, `q`, `dp`, `dq`, `qi` properties as `<string>` for RSA private keys
- `e`, `n`, `d` properties as `<string>` for RSA private keys without optimization parametes (only
with `calculateMissingRSAPrimes` option, see below)
- `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
- `options`: `<Object>`
- `calculateMissingRSAPrimes`: `<boolean>` **Default** 'false'. This option is really only in
effect when importing private RSA JWK keys, by default, keys without the optimization private
key parameters (p, q, dp, dq, qi) won't imported because their calculation is heavy and prone
to blocking the process. Setting this option to true will enable these keys to be imported,
albeit at your own risk. Depending on the key size the calculation takes long and it should
only be used for JWK keys from trusted sources.
- Returns: `<JWK.RSAKey>` &vert; `<JWK.ECKey>` &vert; `<JWK.OKPKey>` &vert; `<JWK.OctKey>`


<details>
<summary><em><strong>Example</strong></em> (Click to expand)</summary>

```js
const { JWK: { importKey } } = require('@panva/jose')
const { JWK: { asKey } } = require('@panva/jose')
const jwk = {
kty: 'RSA',
kid: 'r1LkbBo3925Rb2ZFFrKyU3MVex9T2817Kx0vbi6i_Kc',
Expand All @@ -440,7 +450,7 @@ const jwk = {
n: 'xwQ72P9z9OYshiQ-ntDYaPnnfwG6u9JAdLMZ5o0dmjlcyrvwQRdoFIKPnO65Q8mh6F_LDSxjxa2Yzo_wdjhbPZLjfUJXgCzm54cClXzT5twzo7lzoAfaJlkTsoZc2HFWqmcri0BuzmTFLZx2Q7wYBm0pXHmQKF0V-C1O6NWfd4mfBhbM-I1tHYSpAMgarSm22WDMDx-WWI7TEzy2QhaBVaENW9BKaKkJklocAZCxk18WhR0fckIGiWiSM5FcU1PY2jfGsTmX505Ub7P5Dz75Ygqrutd5tFrcqyPAtPTFDk8X1InxkkUwpP3nFU5o50DGhwQolGYKPGtQ-ZtmbOfcWQ'
}

const key = importKey(jwk)
const key = asKey(jwk)
// RSAKey {
// kty: 'RSA',
// public: true,
Expand Down Expand Up @@ -563,7 +573,7 @@ Returns 'true' if the value is an instance of `<JWK.Key>`.
- [keystore.generate(...)](#keystoregenerate)
- [keystore.generateSync(...)](#keystoregeneratesync)
- [keystore.toJWKS([private])](#keystoretojwksprivate)
- [JWKS.KeyStore.fromJWKS(jwks)](#jwkskeystorefromjwksjwks)
- [JWKS.asKeyStore(jwks[, options])](#jwksaskeystorejwks-options)
<!-- TOC JWKS END -->

```js
Expand All @@ -584,7 +594,7 @@ an existing store.

Creates a new KeyStore, either empty or populated.

- `keys`: `<JWK.Key[]>` Array of key keys instantiated by `JWK.importKey()`
- `keys`: `<JWK.Key[]>` Array of key keys instantiated by `JWK.asKey()`
- Returns: `<JWKS.KeyStore>`

---
Expand Down Expand Up @@ -671,29 +681,41 @@ Exports the keystore to a JSON Web Key Set formatted object.

---

#### `JWKS.KeyStore.fromJWKS(jwks)`
#### `JWKS.asKeyStore(jwks[, options])`

Creates a new KeyStore from a JSON Web Key Set.

- `jwks`: `<Object>` JWKS formatted object (`{ keys: [{ kty: '...', ... }, ...] }`)
- `options`: `<Object>`
- `calculateMissingRSAPrimes`: `<boolean>` **Default** 'false'. This option is really only in
effect when the JWKS contains private RSA JWK keys, by default, keys without the optimization
private key parameters (p, q, dp, dq, qi) won't imported because their calculation is heavy and
prone to blocking the process. Setting this option to true will enable these keys to be
imported, albeit at your own risk. Depending on the key size the calculation takes long and it
should only be used for JWKS from trusted sources.
- Returns: `<JWKS.KeyStore>`

<details>
<summary><em><strong>Example</strong></em> (Click to expand)</summary>

```js
const { JWKS: { KeyStore } } = require('@panva/jose')
const jwks = { keys:
[ { kty: 'RSA',
kid: 'gqUcZ2TjhmNrVOd1d27tedkabhOTs9WghMHIyjIBn7Y',
e: 'AQAB',
n:
'vi1Aui6R0rUL_7pdcFKKMhBF25h4x8WiTZ4w66eNZhwIp48lz-vBuyUUrSR-RwcuvnxlXdjBdSaN-PZkNRDv2bXE3mVtjZgoYyzQlGLJ1wduQaBXIkrQWxc7yzL91MvtP1kWwFHHrQHZRlpiFQQm9gNCy2wXCTbWGT9kjrR1W1bkwhmOKK4rF-hMgaCNDrtEQ6xWknxV8aXW4itouJ0pJv8xplc6J14f_SNq6arVUcAZ26EzJYC2fcvqwsrnKzvW7QxQGQzh-u9Tn82Tl14Omh1KDV8C7Vb_m8XClv_9zOrKBGdaTl1zgINyMEaa_IMophnBgK_kAXvtVvEThQ93GQ',
use: 'enc' } ] }
const ks = KeyStore.fromJWKS(jwks)
const { JWKS: { KeyStore, asKeyStore } } = require('@panva/jose')
const jwks = {
keys: [
{ kty: 'RSA',
kid: 'gqUcZ2TjhmNrVOd1d27tedkabhOTs9WghMHIyjIBn7Y',
e: 'AQAB',
n:
'vi1Aui6R0rUL_7pdcFKKMhBF25h4x8WiTZ4w66eNZhwIp48lz-vBuyUUrSR-RwcuvnxlXdjBdSaN-PZkNRDv2bXE3mVtjZgoYyzQlGLJ1wduQaBXIkrQWxc7yzL91MvtP1kWwFHHrQHZRlpiFQQm9gNCy2wXCTbWGT9kjrR1W1bkwhmOKK4rF-hMgaCNDrtEQ6xWknxV8aXW4itouJ0pJv8xplc6J14f_SNq6arVUcAZ26EzJYC2fcvqwsrnKzvW7QxQGQzh-u9Tn82Tl14Omh1KDV8C7Vb_m8XClv_9zOrKBGdaTl1zgINyMEaa_IMophnBgK_kAXvtVvEThQ93GQ',
use: 'enc' }
]
}
const ks = asKeyStore(jwks)
// KeyStore {}
ks.size
// 1
ks instanceof KeyStore
// true
```
</details>

Expand Down Expand Up @@ -750,7 +772,7 @@ that will be used to sign with is either provided as part of the 'options.algori

```js
const { JWT, JWK } = require('@panva/jose')
const key = JWK.importKey({
const key = JWK.asKey({
kty: 'oct',
k: 'hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg'
})
Expand Down Expand Up @@ -820,7 +842,7 @@ Verifies the claims and signature of a JSON Web Token.
```js
const { JWK, JWT } = require('@panva/jose')

const key = JWK.importKey({
const key = JWK.asKey({
kty: 'oct',
k: 'hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg'
})
Expand Down Expand Up @@ -914,11 +936,11 @@ signatures of the same payload) using the General JWS JSON Serialization Syntax.
```js
const { JWK, JWS } = require('@panva/jose')

const key = JWK.importKey({
const key = JWK.asKey({
kty: 'oct',
k: 'hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg'
})
const key2 = JWK.importKey({
const key2 = JWK.asKey({
kty: 'oct',
k: 'AAPapAv4LbFbiVawEjagUBluYqN5rhna-8nuldDvOx8'
})
Expand Down Expand Up @@ -997,7 +1019,7 @@ provided `<JWK.Key>` instance.
```js
const { JWK, JWS } = require('@panva/jose')

const key = JWK.importKey({
const key = JWK.asKey({
kty: 'oct',
k: 'hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg'
})
Expand Down Expand Up @@ -1031,7 +1053,7 @@ inferred from the provided `<JWK.Key>` instance.
```js
const { JWK, JWS } = require('@panva/jose')

const key = JWK.importKey({
const key = JWK.asKey({
kty: 'oct',
k: 'hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg'
})
Expand Down Expand Up @@ -1073,11 +1095,11 @@ Verifies the provided JWS in either serialization with a given `<JWK.Key>` or `<
```js
const { JWK, JWS, JWKS } = require('@panva/jose')

const key = JWK.importKey({
const key = JWK.asKey({
kty: 'oct',
k: 'hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg'
})
const key2 = JWK.importKey({
const key2 = JWK.asKey({
kty: 'oct',
k: 'AAPapAv4LbFbiVawEjagUBluYqN5rhna-8nuldDvOx8'
})
Expand Down
12 changes: 10 additions & 2 deletions lib/help/base64url.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const b64uRegExp = /^[a-zA-Z0-9_-]*$/

const fromBase64 = (base64) => {
return base64.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_')
}
Expand All @@ -14,11 +16,17 @@ const encodeBuffer = (buf) => {
return fromBase64(buf.toString('base64'))
}

const decode = (input, encoding = 'utf8') => {
return Buffer.from(toBase64(input), 'base64').toString(encoding)
const decode = (input) => {
if (!b64uRegExp.test(input)) {
throw new TypeError('input is not a valid base64url encoded string')
}
return Buffer.from(toBase64(input), 'base64').toString('utf8')
}

const decodeToBuffer = (input) => {
if (!b64uRegExp.test(input)) {
throw new TypeError('input is not a valid base64url encoded string')
}
return Buffer.from(toBase64(input), 'base64')
}

Expand Down
12 changes: 7 additions & 5 deletions lib/help/key_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ const concatEcPublicKey = (x, y) => ({

const jwkToPem = {
RSA: {
private (jwk) {
private (jwk, { calculateMissingRSAPrimes }) {
const RSAPrivateKey = asn1.get('RSAPrivateKey')

if ('oth' in jwk) {
Expand All @@ -197,10 +197,12 @@ const jwkToPem = {

if (jwk.p || jwk.q || jwk.dp || jwk.dq || jwk.qi) {
if (!(jwk.p && jwk.q && jwk.dp && jwk.dq && jwk.qi)) {
throw new errors.JWKImportFailed('all other private key parameters must be present when any one of them is present')
throw new errors.JWKInvalid('all other private key parameters must be present when any one of them is present')
}
} else {
} else if (calculateMissingRSAPrimes) {
jwk = computePrimes(jwk)
} else if (!calculateMissingRSAPrimes) {
throw new errors.JOSENotSupported('importing private RSA keys without all other private key parameters is not enabled, see documentation and its advisory on how and when its ok to enable it')
}

return RSAPrivateKey.encode({
Expand Down Expand Up @@ -293,7 +295,7 @@ const okpCrvToOid = (crv) => {
}
}

module.exports.jwkToPem = (jwk) => {
module.exports.jwkToPem = (jwk, { calculateMissingRSAPrimes = false } = {}) => {
switch (jwk.kty) {
case 'EC':
if (!EC_CURVES.has(jwk.crv)) {
Expand All @@ -312,7 +314,7 @@ module.exports.jwkToPem = (jwk) => {
}

if (jwk.d) {
return jwkToPem[jwk.kty].private(jwk)
return jwkToPem[jwk.kty].private(jwk, { calculateMissingRSAPrimes })
}

return jwkToPem[jwk.kty].public(jwk)
Expand Down
Loading

0 comments on commit 5b53cb0

Please sign in to comment.