Skip to content

Commit 5b53cb0

Browse files
committed
fix: limit calculation of missing RSA private components
- 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
1 parent 80cdd4f commit 5b53cb0

File tree

53 files changed

+359
-245
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+359
-245
lines changed

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
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.
44

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

77

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

1313

1414

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

1717

1818
### Bug Fixes

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ Pending Node.js Support 🤞:
7373

7474
Won't implement:
7575
- ✕ JWS embedded key / referenced verification
76-
- one can decode the header and pass the (`x5c`, `jwk`) to `JWK.importKey` and validate with that
76+
- one can decode the header and pass the (`x5c`, `jwk`) to `JWK.asKey` and validate with that
7777
key, similarly the application can handle fetching and then instantiating the referenced `x5u`
7878
or `jku` in its own code. This way you opt-in to these behaviours.
7979
- ✕ JWS detached content
@@ -137,14 +137,14 @@ const {
137137
Prepare your Keys and KeyStores. See the [documentation][documentation-jwk] for more.
138138

139139
```js
140-
const key = jose.JWK.importKey(fs.readFileSync('path/to/key/file'))
140+
const key = jose.JWK.asKey(fs.readFileSync('path/to/key/file'))
141141

142142
const jwk = { kty: 'EC',
143143
kid: 'dl4M_fcI7XoFCsQ22PYrQBkuxZ2pDcbDimcdFmmXM98',
144144
crv: 'P-256',
145145
x: 'v37avifcL-xgh8cy6IFzcINqqmFLc2JF20XUpn4Y2uQ',
146146
y: 'QTwy27XgP7ZMOdGOSopAHB-FU1JMQn3J9GEWGtUXreQ' }
147-
const anotherKey = jose.JWK.importKey(jwk)
147+
const anotherKey = jose.JWK.asKey(jwk)
148148

149149
const keystore = new jose.JWK.KeyStore(key, key2)
150150
```

docs/README.md

Lines changed: 57 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,10 @@ I can continue maintaining it and adding new features carefree. You may also don
4545
- [key.algorithms([operation])](#keyalgorithmsoperation)
4646
- [key.toJWK([private])](#keytojwkprivate)
4747
- [key.toPEM([private[, encoding]])](#keytopemprivate-encoding)
48-
- JWK.importKey
49-
- [JWK.importKey(key[, options]) asymmetric key import](#jwkimportkeykey-options-asymmetric-key-import)
50-
- [JWK.importKey(secret[, options]) secret key import](#jwkimportkeysecret-options-secret-key-import)
51-
- [JWK.importKey(jwk) JWK-formatted key import](#jwkimportkeyjwk-jwk-formatted-key-import)
48+
- JWK.asKey
49+
- [JWK.asKey(key[, options]) asymmetric key import](#jwkaskeykey-options-asymmetric-key-import)
50+
- [JWK.asKey(secret[, options]) secret key import](#jwkaskeysecret-options-secret-key-import)
51+
- [JWK.asKey(jwk[, options]) JWK-formatted key import](#jwkaskeyjwk-options-jwk-formatted-key-import)
5252
- [JWK.generate(kty[, crvOrSize[, options[, private]]]) generating new keys](#jwkgeneratekty-crvorsize-options-private-generating-new-keys)
5353
- [JWK.generateSync(kty[, crvOrSize[, options[, private]]])](#jwkgeneratesynckty-crvorsize-options-private)
5454
- [JWK.isKey(object)](#jwkiskeyobject)
@@ -60,7 +60,7 @@ how to get a `<JWK.Key>` instances generated or instantiated from existing key m
6060

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

7272
`<JWK.RSAKey>`, `<JWK.ECKey>`, `<JWK.OKPKey>` and `<JWK.OctKey>` represent a key usable for JWS and JWE operations.
73-
The `JWK.importKey()` method is used to retrieve a key representation of an existing key or secret.
73+
The `JWK.asKey()` method is used to retrieve a key representation of an existing key or secret.
7474
`JWK.generate()` method is used to generate a new random key.
7575

7676
`<JWK.RSAKey>`, `<JWK.ECKey>`, `<JWK.OKPKey>` and `<JWK.OctKey>` inherit methods from `<JWK.Key>` and in addition
@@ -330,7 +330,7 @@ key.toPEM(true, { passphrase: 'super-strong', cipher: 'aes-256-cbc' })
330330

331331
---
332332

333-
#### `JWK.importKey(key[, options])` asymmetric key import
333+
#### `JWK.asKey(key[, options])` asymmetric key import
334334

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

363363
```js
364364
const { readFileSync } = require('fs')
365-
const { JWK: { importKey } } = require('@panva/jose')
365+
const { JWK: { asKey } } = require('@panva/jose')
366366

367-
const key = importKey(readFileSync('path/to/key/file'))
367+
const key = asKey(readFileSync('path/to/key/file'))
368368
// ECKey {
369369
// kty: 'EC',
370370
// public: true,
@@ -377,7 +377,7 @@ const key = importKey(readFileSync('path/to/key/file'))
377377

378378
---
379379

380-
#### `JWK.importKey(secret[, options])` secret key import
380+
#### `JWK.asKey(secret[, options])` secret key import
381381

382382
Imports a symmetric key.
383383

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

396396
```js
397-
const { JWK: { importKey } } = require('@panva/jose')
397+
const { JWK: { asKey } } = require('@panva/jose')
398398

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

407407
---
408408

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

411411
Imports a JWK formatted key. This supports JWK formatted RSA, EC, OKP and oct keys. Asymmetrical
412412
keys may be both private and public.
@@ -420,18 +420,28 @@ keys may be both private and public.
420420
[RFC7638][spec-thumbprint]
421421
- `e`, `n` properties as `<string>` for RSA public keys
422422
- `e`, `n`, `d`, `p`, `q`, `dp`, `dq`, `qi` properties as `<string>` for RSA private keys
423+
- `e`, `n`, `d` properties as `<string>` for RSA private keys without optimization parametes (only
424+
with `calculateMissingRSAPrimes` option, see below)
423425
- `crv`, `x`, `y` properties as `<string>` for EC public keys
424426
- `crv`, `x`, `y`, `d` properties as `<string>` for EC private keys
425427
- `crv`, `x`, properties as `<string>` for OKP public keys
426428
- `crv`, `x`, `d` properties as `<string>` for OKP private keys
427429
- `k` properties as `<string>` for secret oct keys
430+
- `options`: `<Object>`
431+
- `calculateMissingRSAPrimes`: `<boolean>` **Default** 'false'. This option is really only in
432+
effect when importing private RSA JWK keys, by default, keys without the optimization private
433+
key parameters (p, q, dp, dq, qi) won't imported because their calculation is heavy and prone
434+
to blocking the process. Setting this option to true will enable these keys to be imported,
435+
albeit at your own risk. Depending on the key size the calculation takes long and it should
436+
only be used for JWK keys from trusted sources.
428437
- Returns: `<JWK.RSAKey>` &vert; `<JWK.ECKey>` &vert; `<JWK.OKPKey>` &vert; `<JWK.OctKey>`
429438

439+
430440
<details>
431441
<summary><em><strong>Example</strong></em> (Click to expand)</summary>
432442

433443
```js
434-
const { JWK: { importKey } } = require('@panva/jose')
444+
const { JWK: { asKey } } = require('@panva/jose')
435445
const jwk = {
436446
kty: 'RSA',
437447
kid: 'r1LkbBo3925Rb2ZFFrKyU3MVex9T2817Kx0vbi6i_Kc',
@@ -440,7 +450,7 @@ const jwk = {
440450
n: 'xwQ72P9z9OYshiQ-ntDYaPnnfwG6u9JAdLMZ5o0dmjlcyrvwQRdoFIKPnO65Q8mh6F_LDSxjxa2Yzo_wdjhbPZLjfUJXgCzm54cClXzT5twzo7lzoAfaJlkTsoZc2HFWqmcri0BuzmTFLZx2Q7wYBm0pXHmQKF0V-C1O6NWfd4mfBhbM-I1tHYSpAMgarSm22WDMDx-WWI7TEzy2QhaBVaENW9BKaKkJklocAZCxk18WhR0fckIGiWiSM5FcU1PY2jfGsTmX505Ub7P5Dz75Ygqrutd5tFrcqyPAtPTFDk8X1InxkkUwpP3nFU5o50DGhwQolGYKPGtQ-ZtmbOfcWQ'
441451
}
442452

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

569579
```js
@@ -584,7 +594,7 @@ an existing store.
584594

585595
Creates a new KeyStore, either empty or populated.
586596

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

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

672682
---
673683

674-
#### `JWKS.KeyStore.fromJWKS(jwks)`
684+
#### `JWKS.asKeyStore(jwks[, options])`
675685

676686
Creates a new KeyStore from a JSON Web Key Set.
677687

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

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

684701
```js
685-
const { JWKS: { KeyStore } } = require('@panva/jose')
686-
const jwks = { keys:
687-
[ { kty: 'RSA',
688-
kid: 'gqUcZ2TjhmNrVOd1d27tedkabhOTs9WghMHIyjIBn7Y',
689-
e: 'AQAB',
690-
n:
691-
'vi1Aui6R0rUL_7pdcFKKMhBF25h4x8WiTZ4w66eNZhwIp48lz-vBuyUUrSR-RwcuvnxlXdjBdSaN-PZkNRDv2bXE3mVtjZgoYyzQlGLJ1wduQaBXIkrQWxc7yzL91MvtP1kWwFHHrQHZRlpiFQQm9gNCy2wXCTbWGT9kjrR1W1bkwhmOKK4rF-hMgaCNDrtEQ6xWknxV8aXW4itouJ0pJv8xplc6J14f_SNq6arVUcAZ26EzJYC2fcvqwsrnKzvW7QxQGQzh-u9Tn82Tl14Omh1KDV8C7Vb_m8XClv_9zOrKBGdaTl1zgINyMEaa_IMophnBgK_kAXvtVvEThQ93GQ',
692-
use: 'enc' } ] }
693-
const ks = KeyStore.fromJWKS(jwks)
702+
const { JWKS: { KeyStore, asKeyStore } } = require('@panva/jose')
703+
const jwks = {
704+
keys: [
705+
{ kty: 'RSA',
706+
kid: 'gqUcZ2TjhmNrVOd1d27tedkabhOTs9WghMHIyjIBn7Y',
707+
e: 'AQAB',
708+
n:
709+
'vi1Aui6R0rUL_7pdcFKKMhBF25h4x8WiTZ4w66eNZhwIp48lz-vBuyUUrSR-RwcuvnxlXdjBdSaN-PZkNRDv2bXE3mVtjZgoYyzQlGLJ1wduQaBXIkrQWxc7yzL91MvtP1kWwFHHrQHZRlpiFQQm9gNCy2wXCTbWGT9kjrR1W1bkwhmOKK4rF-hMgaCNDrtEQ6xWknxV8aXW4itouJ0pJv8xplc6J14f_SNq6arVUcAZ26EzJYC2fcvqwsrnKzvW7QxQGQzh-u9Tn82Tl14Omh1KDV8C7Vb_m8XClv_9zOrKBGdaTl1zgINyMEaa_IMophnBgK_kAXvtVvEThQ93GQ',
710+
use: 'enc' }
711+
]
712+
}
713+
const ks = asKeyStore(jwks)
694714
// KeyStore {}
695715
ks.size
696716
// 1
717+
ks instanceof KeyStore
718+
// true
697719
```
698720
</details>
699721

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

751773
```js
752774
const { JWT, JWK } = require('@panva/jose')
753-
const key = JWK.importKey({
775+
const key = JWK.asKey({
754776
kty: 'oct',
755777
k: 'hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg'
756778
})
@@ -820,7 +842,7 @@ Verifies the claims and signature of a JSON Web Token.
820842
```js
821843
const { JWK, JWT } = require('@panva/jose')
822844

823-
const key = JWK.importKey({
845+
const key = JWK.asKey({
824846
kty: 'oct',
825847
k: 'hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg'
826848
})
@@ -914,11 +936,11 @@ signatures of the same payload) using the General JWS JSON Serialization Syntax.
914936
```js
915937
const { JWK, JWS } = require('@panva/jose')
916938

917-
const key = JWK.importKey({
939+
const key = JWK.asKey({
918940
kty: 'oct',
919941
k: 'hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg'
920942
})
921-
const key2 = JWK.importKey({
943+
const key2 = JWK.asKey({
922944
kty: 'oct',
923945
k: 'AAPapAv4LbFbiVawEjagUBluYqN5rhna-8nuldDvOx8'
924946
})
@@ -997,7 +1019,7 @@ provided `<JWK.Key>` instance.
9971019
```js
9981020
const { JWK, JWS } = require('@panva/jose')
9991021

1000-
const key = JWK.importKey({
1022+
const key = JWK.asKey({
10011023
kty: 'oct',
10021024
k: 'hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg'
10031025
})
@@ -1031,7 +1053,7 @@ inferred from the provided `<JWK.Key>` instance.
10311053
```js
10321054
const { JWK, JWS } = require('@panva/jose')
10331055

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

1076-
const key = JWK.importKey({
1098+
const key = JWK.asKey({
10771099
kty: 'oct',
10781100
k: 'hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg'
10791101
})
1080-
const key2 = JWK.importKey({
1102+
const key2 = JWK.asKey({
10811103
kty: 'oct',
10821104
k: 'AAPapAv4LbFbiVawEjagUBluYqN5rhna-8nuldDvOx8'
10831105
})

lib/help/base64url.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
const b64uRegExp = /^[a-zA-Z0-9_-]*$/
2+
13
const fromBase64 = (base64) => {
24
return base64.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_')
35
}
@@ -14,11 +16,17 @@ const encodeBuffer = (buf) => {
1416
return fromBase64(buf.toString('base64'))
1517
}
1618

17-
const decode = (input, encoding = 'utf8') => {
18-
return Buffer.from(toBase64(input), 'base64').toString(encoding)
19+
const decode = (input) => {
20+
if (!b64uRegExp.test(input)) {
21+
throw new TypeError('input is not a valid base64url encoded string')
22+
}
23+
return Buffer.from(toBase64(input), 'base64').toString('utf8')
1924
}
2025

2126
const decodeToBuffer = (input) => {
27+
if (!b64uRegExp.test(input)) {
28+
throw new TypeError('input is not a valid base64url encoded string')
29+
}
2230
return Buffer.from(toBase64(input), 'base64')
2331
}
2432

lib/help/key_utils.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ const concatEcPublicKey = (x, y) => ({
188188

189189
const jwkToPem = {
190190
RSA: {
191-
private (jwk) {
191+
private (jwk, { calculateMissingRSAPrimes }) {
192192
const RSAPrivateKey = asn1.get('RSAPrivateKey')
193193

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

198198
if (jwk.p || jwk.q || jwk.dp || jwk.dq || jwk.qi) {
199199
if (!(jwk.p && jwk.q && jwk.dp && jwk.dq && jwk.qi)) {
200-
throw new errors.JWKImportFailed('all other private key parameters must be present when any one of them is present')
200+
throw new errors.JWKInvalid('all other private key parameters must be present when any one of them is present')
201201
}
202-
} else {
202+
} else if (calculateMissingRSAPrimes) {
203203
jwk = computePrimes(jwk)
204+
} else if (!calculateMissingRSAPrimes) {
205+
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')
204206
}
205207

206208
return RSAPrivateKey.encode({
@@ -293,7 +295,7 @@ const okpCrvToOid = (crv) => {
293295
}
294296
}
295297

296-
module.exports.jwkToPem = (jwk) => {
298+
module.exports.jwkToPem = (jwk, { calculateMissingRSAPrimes = false } = {}) => {
297299
switch (jwk.kty) {
298300
case 'EC':
299301
if (!EC_CURVES.has(jwk.crv)) {
@@ -312,7 +314,7 @@ module.exports.jwkToPem = (jwk) => {
312314
}
313315

314316
if (jwk.d) {
315-
return jwkToPem[jwk.kty].private(jwk)
317+
return jwkToPem[jwk.kty].private(jwk, { calculateMissingRSAPrimes })
316318
}
317319

318320
return jwkToPem[jwk.kty].public(jwk)

0 commit comments

Comments
 (0)