Skip to content

Commit

Permalink
feat: add opt-in objects to verify using embedded JWS Header public keys
Browse files Browse the repository at this point in the history
  • Loading branch information
panva committed May 4, 2020
1 parent 5c78888 commit 7c1cab1
Show file tree
Hide file tree
Showing 13 changed files with 351 additions and 20 deletions.
24 changes: 24 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ If you or your business use `jose`, please consider becoming a [sponsor][support
- [JWK.generateSync(kty[, crvOrSize[, options[, private]]])](#jwkgeneratesynckty-crvorsize-options-private)
- [JWK.isKey(object)](#jwkiskeyobject)
- [JWK.None](#jwknone)
- [JWK.EmbeddedJWK](#jwkembeddedjwk)
- [JWK.EmbeddedX5C](#jwkembeddedx5c)
<!-- TOC JWK END -->

All sign and encrypt operations require `<JWK.Key>` or `JWK.asKey()` compatible input.
Expand Down Expand Up @@ -623,6 +625,28 @@ JWS.verify(unsecuredJWS, None)

---

#### `JWK.EmbeddedJWK`

`JWK.EmbeddedJWK` is a special key object that can be used with the JWS/JWT verify operations
whenever you want to opt-in to verify signatures with a public key embedded in the JWS Header `jwk`
parameter. It is recommended to combine this with the verify `algorithms` option to whitelist
JWS algorithms to accept as well as the `complete` option set to `true` if you need to work with the
instantiated `JWK.Key` from the token.

---

#### `JWK.EmbeddedX5C`

`JWK.EmbeddedX5C` is a special key object that can be used with the JWS/JWT verify operations
whenever you want to opt-in to verify signatures with a public key embedded in the first JWS Header
`x5c` parameter. It is recommended to combine this with the verify `algorithms` option to whitelist
JWS algorithms to accept as well as the `complete` option set to `true` if you need to work with the
instantiated `JWK.Key` from the token. ⚠️ the x5c members are all validated to be certificates but
their chain or trust is not validated. Unfortunately Node.js does not have any good tools to do that
reliably.

---

## JWKS (JSON Web Key Set)

<!-- TOC JWKS START -->
Expand Down
2 changes: 1 addition & 1 deletion lib/help/key_object.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ if (keyObjectSupported) {
}

const pemToDer = pem => Buffer.from(pem.replace(/(?:-----(?:BEGIN|END)(?: (?:RSA|EC))? (?:PRIVATE|PUBLIC) KEY-----|\s)/g, ''), 'base64')
const derToPem = (der, label) => `-----BEGIN ${label}-----${EOL}${der.toString('base64').match(/.{1,64}/g).join(EOL)}${EOL}-----END ${label}-----`
const derToPem = (der, label) => `-----BEGIN ${label}-----${EOL}${(der.toString('base64').match(/.{1,64}/g) || []).join(EOL)}${EOL}-----END ${label}-----`
const unsupported = (input) => {
const label = typeof input === 'string' ? input : `OID ${input.join('.')}`
throw new errors.JOSENotSupported(`${label} is not supported in your Node.js runtime version`)
Expand Down
2 changes: 1 addition & 1 deletion lib/help/key_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const asn1 = require('./asn1')
const computePrimes = require('./rsa_primes')
const { OKP_CURVES, EC_CURVES } = require('../registry')

const formatPem = (base64pem, descriptor) => `-----BEGIN ${descriptor} KEY-----${EOL}${base64pem.match(/.{1,64}/g).join(EOL)}${EOL}-----END ${descriptor} KEY-----`
const formatPem = (base64pem, descriptor) => `-----BEGIN ${descriptor} KEY-----${EOL}${(base64pem.match(/.{1,64}/g) || []).join(EOL)}${EOL}-----END ${descriptor} KEY-----`

const okpToJWK = {
private (crv, keyObject) {
Expand Down
6 changes: 5 additions & 1 deletion lib/jwk/index.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
const Key = require('./key/base')
const None = require('./key/none')
const EmbeddedJWK = require('./key/embedded.jwk')
const EmbeddedX5C = require('./key/embedded.x5c')
const importKey = require('./import')
const generate = require('./generate')

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

/* deprecated */
Expand Down
2 changes: 1 addition & 1 deletion lib/jwk/key/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class Key {
let publicKey
try {
publicKey = createPublicKey({
key: `-----BEGIN CERTIFICATE-----${EOL}${cert.match(/.{1,64}/g).join(EOL)}${EOL}-----END CERTIFICATE-----`, format: 'pem'
key: `-----BEGIN CERTIFICATE-----${EOL}${(cert.match(/.{1,64}/g) || []).join(EOL)}${EOL}-----END CERTIFICATE-----`, format: 'pem'
})
} catch (err) {
throw new errors.JWKInvalid(`\`x5c\` member at index ${i} is not a valid base64-encoded DER PKIX certificate`)
Expand Down
27 changes: 27 additions & 0 deletions lib/jwk/key/embedded.jwk.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const { inspect } = require('util')

const Key = require('./base')

class EmbeddedJWK extends Key {
constructor () {
super({ type: 'embedded' })
Object.defineProperties(this, {
kid: { value: undefined },
kty: { value: undefined },
thumbprint: { value: undefined },
toJWK: { value: undefined },
toPEM: { value: undefined }
})
}

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

algorithms () {
return new Set()
}
}

module.exports = new EmbeddedJWK()
27 changes: 27 additions & 0 deletions lib/jwk/key/embedded.x5c.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const { inspect } = require('util')

const Key = require('./base')

class EmbeddedX5C extends Key {
constructor () {
super({ type: 'embedded' })
Object.defineProperties(this, {
kid: { value: undefined },
kty: { value: undefined },
thumbprint: { value: undefined },
toJWK: { value: undefined },
toPEM: { value: undefined }
})
}

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

algorithms () {
return new Set()
}
}

module.exports = new EmbeddedX5C()
3 changes: 2 additions & 1 deletion lib/jwk/key/none.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class NoneKey extends Key {
super({ type: 'unsecured' }, { alg: 'none' })
Object.defineProperties(this, {
kid: { value: undefined },
kty: { value: undefined },
thumbprint: { value: undefined },
toJWK: { value: undefined },
toPEM: { value: undefined }
Expand All @@ -30,4 +31,4 @@ class NoneKey extends Key {
}
}

module.exports = new NoneKey({ type: 'unsecured' }, { alg: 'none' })
module.exports = new NoneKey()
6 changes: 3 additions & 3 deletions lib/jwks/keystore.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const { deprecate, inspect } = require('util')
const isObject = require('../help/is_object')
const { generate, generateSync } = require('../jwk/generate')
const { USES_MAPPING } = require('../help/consts')
const { None, isKey, asKey: importKey } = require('../jwk')
const { isKey, asKey: importKey } = require('../jwk')

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

Expand Down Expand Up @@ -107,7 +107,7 @@ class KeyStore {
}

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

Expand Down
22 changes: 22 additions & 0 deletions lib/jws/verify.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
const { EOL } = require('os')

const base64url = require('../help/base64url')
const isDisjoint = require('../help/is_disjoint')
const isObject = require('../help/is_object')
let validateCrit = require('../help/validate_crit')
const getKey = require('../help/get_key')
const { KeyStore } = require('../jwks')
const errors = require('../errors')
const { check, verify } = require('../jwa')
const JWK = require('../jwk')

const { detect: resolveSerialization } = require('./serializers')

Expand Down Expand Up @@ -125,6 +129,24 @@ const jwsVerify = (skipDisjointCheck, serialization, jws, key, { crit = [], comp
}
}

if (key === JWK.EmbeddedJWK) {
if (!isObject(combinedHeader.jwk)) {
throw new errors.JWSInvalid('JWS Header Parameter "jwk" must be a JSON object')
}
key = JWK.asKey(combinedHeader.jwk)
if (key.type !== 'public') {
throw new errors.JWSInvalid('JWS Header Parameter "jwk" must be a public key')
}
} else if (key === JWK.EmbeddedX5C) {
if (!Array.isArray(combinedHeader.x5c) || !combinedHeader.x5c.length || combinedHeader.x5c.some(c => typeof c !== 'string' || !c)) {
throw new errors.JWSInvalid('JWS Header Parameter "x5c" must be a JSON array of certificate value strings')
}
key = JWK.asKey(
`-----BEGIN CERTIFICATE-----${EOL}${(combinedHeader.x5c[0].match(/.{1,64}/g) || []).join(EOL)}${EOL}-----END CERTIFICATE-----`,
{ x5c: combinedHeader.x5c }
)
}

check(key, 'verify', alg)

const toBeVerified = Buffer.concat([
Expand Down
Loading

0 comments on commit 7c1cab1

Please sign in to comment.