From 9d46c48fd5d3ac19f8570564bf717c6edca157fb Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sat, 25 May 2019 22:34:28 +0200 Subject: [PATCH] feat: add support for JWK x5c, x5t and x5t#S256 --- README.md | 6 +-- docs/README.md | 31 ++++++++++++++ lib/help/node_alg.js | 20 +-------- lib/index.d.ts | 50 ++++++++++++++--------- lib/jwk/import.js | 10 ++++- lib/jwk/key/base.js | 69 +++++++++++++++++++++++++++++++- lib/jwk/thumbprint.js | 4 ++ lib/jwks/keystore.js | 23 +++++++++-- test/jwk/x5c_thumbprints.test.js | 65 ++++++++++++++++++++++++++++++ test/jwks/keystore.test.js | 35 +++++++++++++++- 10 files changed, 262 insertions(+), 51 deletions(-) create mode 100644 test/jwk/x5c_thumbprints.test.js diff --git a/README.md b/README.md index 9bbc3bb484..cbdc4ed2eb 100644 --- a/README.md +++ b/README.md @@ -75,16 +75,12 @@ 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 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 and for `x5c` specifically - the recipient is responsible for validating the certificate chain is trusted + or `jku` in its own code. This way you opt-in to these behaviours. - ✕ JWS detached content - one can remove/attach the payload after/before the respective operation - ✕ "none" alg support - no crypto, no use -Not Planned / PR | Use-Case | Discussion Welcome: -- ◯ `x5c`, `x5t`, `x5t#S256`, `x5u` etc `JWK.Key` fields -
diff --git a/docs/README.md b/docs/README.md index dc6b326247..66901fb5d4 100644 --- a/docs/README.md +++ b/docs/README.md @@ -33,6 +33,9 @@ I can continue maintaining it and adding new features carefree. You may also don - [key.alg](#keyalg) - [key.use](#keyuse) - [key.kid](#keykid) + - [key.x5c](#keyx5c) + - [key.x5t](#keyx5t) + - [key['x5t#S256']](#keyx5ts256) - [key.key_ops](#keykey_ops) - [key.thumbprint](#keythumbprint) - [key.type](#keytype) @@ -121,6 +124,34 @@ defined in [RFC7638][spec-thumbprint]. --- +#### `key.x5c` + +Returns the key's X.509 Certificate Chain Parameter if set + +- `string[]` + +--- + +#### `key.x5t` + +Returns the key's X.509 Certificate SHA-1 Thumbprint Parameter if set. This +property can be either be set manually by the JWK producer or left to @panva/jose to compute based +on the first certificate in the key's `x5c`. + +- `` + +--- + +#### `key['x5t#S256']` + +Returns the key's X.509 Certificate SHA-256 Thumbprint Parameter if set. This +property can be either be set manually by the JWK producer or left to @panva/jose to compute based +on the first certificate in the key's `x5c`. + +- `` + +--- + #### `key.key_ops` Returns the key's JWK Key Operations Parameter if set. If set the key can only be used for the diff --git a/lib/help/node_alg.js b/lib/help/node_alg.js index f5b95ee267..10c8802d43 100644 --- a/lib/help/node_alg.js +++ b/lib/help/node_alg.js @@ -1,19 +1 @@ -module.exports = (alg) => { - switch (alg) { - case 'RS256': - case 'PS256': - case 'HS256': - case 'ES256': - return 'sha256' - case 'RS384': - case 'PS384': - case 'HS384': - case 'ES384': - return 'sha384' - case 'RS512': - case 'PS512': - case 'HS512': - case 'ES512': - return 'sha512' - } -} +module.exports = alg => `sha${alg.substr(-3)}` diff --git a/lib/index.d.ts b/lib/index.d.ts index aeb5d614a7..fc4e75fbe6 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -4,19 +4,24 @@ import { KeyObject, PrivateKeyInput, PublicKeyInput } from 'crypto' type use = 'sig' | 'enc' type keyOperation = 'sign' | 'verify' | 'encrypt' | 'decrypt' | 'wrapKey' | 'unwrapKey' | 'deriveKey' -interface KeyParameters { +interface BasicParameters { alg?: string use?: use kid?: string key_ops?: keyOperation[] } +interface KeyParameters extends BasicParameters { + x5c?: string[] + x5t?: string + 'x5t#S256'?: string +} type ECCurve = 'P-256' | 'P-384' | 'P-521' type OKPCurve = 'Ed25519' | 'Ed448' | 'X25519' | 'X448' type keyType = 'RSA' | 'EC' | 'OKP' | 'oct' type asymmetricKeyObjectTypes = 'private' | 'public' type keyObjectTypes = asymmetricKeyObjectTypes | 'secret' -interface JWKOctKey extends KeyParameters { +interface JWKOctKey extends BasicParameters { // no x5c kty: 'oct', k?: string } @@ -73,6 +78,9 @@ export namespace JWK { key_ops?: keyOperation[] kid: string thumbprint: string + x5c?: string[] + x5t?: string + 'x5t#S256'?: string toPEM(private?: boolean, encoding?: pemEncodingOptions): string @@ -138,20 +146,22 @@ export namespace JWK { export function importKey(jwk: JWKECKey): ECKey export function importKey(jwk: JWKOKPKey): OKPKey - export function generate(kty: 'EC', crv?: ECCurve, parameters?: KeyParameters, private?: boolean): Promise - export function generate(kty: 'OKP', crv?: OKPCurve, parameters?: KeyParameters, private?: boolean): Promise - export function generate(kty: 'RSA', bitlength?: number, parameters?: KeyParameters, private?: boolean): Promise - export function generate(kty: 'oct', bitlength?: number, parameters?: KeyParameters): Promise + export function generate(kty: 'EC', crv?: ECCurve, parameters?: BasicParameters, private?: boolean): Promise + export function generate(kty: 'OKP', crv?: OKPCurve, parameters?: BasicParameters, private?: boolean): Promise + export function generate(kty: 'RSA', bitlength?: number, parameters?: BasicParameters, private?: boolean): Promise + export function generate(kty: 'oct', bitlength?: number, parameters?: BasicParameters): Promise - export function generateSync(kty: 'EC', crv?: ECCurve, parameters?: KeyParameters, private?: boolean): ECKey - export function generateSync(kty: 'OKP', crv?: OKPCurve, parameters?: KeyParameters, private?: boolean): OKPKey - export function generateSync(kty: 'RSA', bitlength?: number, parameters?: KeyParameters, private?: boolean): RSAKey - export function generateSync(kty: 'oct', bitlength?: number, parameters?: KeyParameters): OctKey + export function generateSync(kty: 'EC', crv?: ECCurve, parameters?: BasicParameters, private?: boolean): ECKey + export function generateSync(kty: 'OKP', crv?: OKPCurve, parameters?: BasicParameters, private?: boolean): OKPKey + export function generateSync(kty: 'RSA', bitlength?: number, parameters?: BasicParameters, private?: boolean): RSAKey + export function generateSync(kty: 'oct', bitlength?: number, parameters?: BasicParameters): OctKey } export namespace JWKS { - interface KeyQuery extends KeyParameters { - kty: keyType + interface KeyQuery extends BasicParameters { + kty?: keyType + x5t?: string + 'x5t#S256'?: string } class KeyStore { @@ -166,15 +176,15 @@ export namespace JWKS { toJWKS(private?: boolean): JSONWebKeySet - generate(kty: 'EC', crv?: ECCurve, parameters?: KeyParameters, private?: boolean): void - generate(kty: 'OKP', crv?: OKPCurve, parameters?: KeyParameters, private?: boolean): void - generate(kty: 'RSA', bitlength?: number, parameters?: KeyParameters, private?: boolean): void - generate(kty: 'oct', bitlength?: number, parameters?: KeyParameters): void + generate(kty: 'EC', crv?: ECCurve, parameters?: BasicParameters, private?: boolean): void + generate(kty: 'OKP', crv?: OKPCurve, parameters?: BasicParameters, private?: boolean): void + generate(kty: 'RSA', bitlength?: number, parameters?: BasicParameters, private?: boolean): void + generate(kty: 'oct', bitlength?: number, parameters?: BasicParameters): void - generateSync(kty: 'EC', crv?: ECCurve, parameters?: KeyParameters, private?: boolean): void - generateSync(kty: 'OKP', crv?: OKPCurve, parameters?: KeyParameters, private?: boolean): void - generateSync(kty: 'RSA', bitlength?: number, parameters?: KeyParameters, private?: boolean): void - generateSync(kty: 'oct', bitlength?: number, parameters?: KeyParameters): void + generateSync(kty: 'EC', crv?: ECCurve, parameters?: BasicParameters, private?: boolean): void + generateSync(kty: 'OKP', crv?: OKPCurve, parameters?: BasicParameters, private?: boolean): void + generateSync(kty: 'RSA', bitlength?: number, parameters?: BasicParameters, private?: boolean): void + generateSync(kty: 'oct', bitlength?: number, parameters?: BasicParameters): void } } diff --git a/lib/jwk/import.js b/lib/jwk/import.js index dc42677e36..61646f8975 100644 --- a/lib/jwk/import.js +++ b/lib/jwk/import.js @@ -13,7 +13,15 @@ const OctKey = require('./key/oct') const importable = new Set(['string', 'buffer', 'object']) const mergedParameters = (target = {}, source = {}) => { - return Object.assign({}, { alg: source.alg, use: source.use, kid: source.kid, key_ops: source.key_ops }, target) + return Object.assign({}, { + alg: source.alg, + key_ops: source.key_ops, + kid: source.kid, + use: source.use, + x5c: source.x5c, + x5t: source.x5t, + 'x5t#S256': source['x5t#S256'] + }, target) } const importKey = (key, parameters) => { diff --git a/lib/jwk/key/base.js b/lib/jwk/key/base.js index 902959016b..e6e6087483 100644 --- a/lib/jwk/key/base.js +++ b/lib/jwk/key/base.js @@ -1,3 +1,4 @@ +const { strict: assert } = require('assert') const { createPublicKey } = require('crypto') const { inspect } = require('util') @@ -11,7 +12,7 @@ const thumbprint = require('../thumbprint') const errors = require('../../errors') class Key { - constructor (keyObject, { alg, use, kid, key_ops: ops } = {}) { + constructor (keyObject, { alg, use, kid, key_ops: ops, x5c, x5t, 'x5t#S256': x5t256 } = {}) { if (use !== undefined) { if (typeof use !== 'string' || !USES.has(use)) { throw new TypeError('`use` must be either "sig" or "enc" string when provided') @@ -31,7 +32,7 @@ class Key { } if (ops !== undefined) { - if (!Array.isArray(ops) || !ops.length || ops.some(x => typeof x !== 'string')) { + if (!Array.isArray(ops) || !ops.length || ops.some(o => typeof o !== 'string')) { throw new TypeError('`key_ops` must be a non-empty array of strings when provided') } ops = Array.from(new Set(ops)).filter(x => OPS.has(x)) @@ -46,6 +47,33 @@ class Key { } } + if (x5c !== undefined) { + if (!Array.isArray(x5c) || !x5c.length || x5c.some(c => typeof c !== 'string')) { + throw new TypeError('`x5c` must be an array of one or more PKIX certificates when provided') + } + + x5c.forEach((cert, i) => { + let publicKey + try { + publicKey = createPublicKey({ + key: `-----BEGIN CERTIFICATE-----\n${cert}\n-----END CERTIFICATE-----`, format: 'pem' + }) + } catch (err) { + throw new errors.JWKInvalid(`\`x5c\` member at index ${i} is not a valid base64-encoded DER PKIX certificate`) + } + if (i === 0) { + try { + assert.deepEqual( + publicKey.export({ type: 'spki', format: 'der' }), + (keyObject.type === 'public' ? keyObject : createPublicKey(keyObject)).export({ type: 'spki', format: 'der' }) + ) + } catch (err) { + throw new errors.JWKInvalid('The key in the first `x5c` certificate MUST match the public key represented by the JWK') + } + } + }) + } + Object.defineProperties(this, { [KEYOBJECT]: { value: isObject(keyObject) ? undefined : keyObject }, type: { value: keyObject.type }, @@ -54,6 +82,7 @@ class Key { secret: { value: keyObject.type === 'secret' }, alg: { value: alg, enumerable: alg !== undefined }, use: { value: use, enumerable: use !== undefined }, + x5c: { value: x5c, enumerable: x5c !== undefined }, key_ops: { enumerable: ops !== undefined, ...(ops ? { get () { return [...ops] } } : { value: undefined }) @@ -68,6 +97,30 @@ class Key { configurable: true }) }, + ...(x5c ? { + x5t: { + enumerable: true, + ...(x5t ? { value: x5t } : { + get () { + Object.defineProperty(this, 'x5t', { value: thumbprint.x5t(this.x5c[0]), configurable: false }) + return this.x5t + }, + configurable: true + }) + } + } : undefined), + ...(x5c ? { + 'x5t#S256': { + enumerable: true, + ...(x5t256 ? { value: x5t256 } : { + get () { + Object.defineProperty(this, 'x5t#S256', { value: thumbprint['x5t#S256'](this.x5c[0]), configurable: false }) + return this['x5t#S256'] + }, + configurable: true + }) + } + } : undefined), thumbprint: { get () { Object.defineProperty(this, 'thumbprint', { value: thumbprint.kid(this[THUMBPRINT_MATERIAL]()), configurable: false }) @@ -131,6 +184,18 @@ class Key { result.use = this.use } + if (this.x5c) { + result.x5c = this.x5c + } + + if (this.x5t) { + result.x5t = this.x5t + } + + if (this['x5t#S256']) { + result['x5t#S256'] = this['x5t#S256'] + } + return result } diff --git a/lib/jwk/thumbprint.js b/lib/jwk/thumbprint.js index 631cfc4ef6..ac23cc55c1 100644 --- a/lib/jwk/thumbprint.js +++ b/lib/jwk/thumbprint.js @@ -2,4 +2,8 @@ const { createHash } = require('crypto') const base64url = require('../help/base64url') +const xt5 = (hash, cert) => base64url.encodeBuffer(createHash(hash).update(Buffer.from(cert, 'base64')).digest()) + module.exports.kid = components => base64url.encodeBuffer(createHash('sha256').update(JSON.stringify(components)).digest()) +module.exports.x5t = xt5.bind(undefined, 'sha1') +module.exports['x5t#S256'] = xt5.bind(undefined, 'sha256') diff --git a/lib/jwks/keystore.js b/lib/jwks/keystore.js index c81c2d3021..9adacc664d 100644 --- a/lib/jwks/keystore.js +++ b/lib/jwks/keystore.js @@ -6,7 +6,7 @@ const Key = require('../jwk/key/base') const importKey = require('../jwk/import') const { USES_MAPPING } = require('../help/consts') -const keyscore = (key, { alg, kid, use, ops }) => { +const keyscore = (key, { alg, kid, use, ops, x5t, x5t256 }) => { let score = 0 if (alg && key.alg) { @@ -17,6 +17,14 @@ const keyscore = (key, { alg, kid, use, ops }) => { score++ } + if (x5t && key.x5t) { + score++ + } + + if (x5t256 && key['x5t#S256']) { + score++ + } + if (use && key.use) { score++ } @@ -52,11 +60,12 @@ class KeyStore { return new KeyStore(...keys) } - all ({ alg, kid, use, kty, key_ops: ops } = {}) { + all ({ alg, kid, use, kty, key_ops: ops, x5t, 'x5t#S256': x5t256 } = {}) { if (ops !== undefined && (!Array.isArray(ops) || !ops.length || ops.some(x => typeof x !== 'string'))) { throw new TypeError('`key_ops` must be a non-empty array of strings') } + const search = { alg, kid, use, ops, x5t, x5t256 } return [...this.#keys] .filter((key) => { let candidate = true @@ -69,6 +78,14 @@ class KeyStore { candidate = false } + if (candidate && x5t !== undefined && key.x5t !== x5t) { + candidate = false + } + + if (candidate && x5t256 !== undefined && key['x5t#S256'] !== x5t256) { + candidate = false + } + if (candidate && kty !== undefined && key.kty !== kty) { candidate = false } @@ -91,7 +108,7 @@ class KeyStore { return candidate }) - .sort((first, second) => keyscore(second, { alg, kid, use, ops }) - keyscore(first, { alg, kid, use, ops })) + .sort((first, second) => keyscore(second, search) - keyscore(first, search)) } get (...args) { diff --git a/test/jwk/x5c_thumbprints.test.js b/test/jwk/x5c_thumbprints.test.js new file mode 100644 index 0000000000..0abc6fe4a8 --- /dev/null +++ b/test/jwk/x5c_thumbprints.test.js @@ -0,0 +1,65 @@ +const test = require('ava') + +const errors = require('../../lib/errors') + +const { JWK: { importKey } } = require('../..') + +const jwk = { + kty: 'RSA', + use: 'sig', + kid: '1b94c', + n: 'vrjOfz9Ccdgx5nQudyhdoR17V-IubWMeOZCwX_jj0hgAsz2J_pqYW08PLbK_PdiVGKPrqzmDIsLI7sA25VEnHU1uCLNwBuUiCO11_-7dYbsr4iJmG0Qu2j8DsVyT1azpJC_NG84Ty5KKthuCaPod7iI7w0LK9orSMhBEwwZDCxTWq4aYWAchc8t-emd9qOvWtVMDC2BXksRngh6X5bUYLy6AyHKvj-nUy1wgzjYQDwHMTplCoLtU-o-8SNnZ1tmRoGE9uJkBLdh5gFENabWnU5m1ZqZPdwS-qo-meMvVfJb6jJVWRpl2SUtCnYG2C32qvbWbjZ_jBPD5eunqsIo1vQ', + e: 'AQAB', + x5c: [ + 'MIIDQjCCAiqgAwIBAgIGATz/FuLiMA0GCSqGSIb3DQEBBQUAMGIxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDTzEPMA0GA1UEBxMGRGVudmVyMRwwGgYDVQQKExNQaW5nIElkZW50aXR5IENvcnAuMRcwFQYDVQQDEw5CcmlhbiBDYW1wYmVsbDAeFw0xMzAyMjEyMzI5MTVaFw0xODA4MTQyMjI5MTVaMGIxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDTzEPMA0GA1UEBxMGRGVudmVyMRwwGgYDVQQKExNQaW5nIElkZW50aXR5IENvcnAuMRcwFQYDVQQDEw5CcmlhbiBDYW1wYmVsbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL64zn8/QnHYMeZ0LncoXaEde1fiLm1jHjmQsF/449IYALM9if6amFtPDy2yvz3YlRij66s5gyLCyO7ANuVRJx1NbgizcAblIgjtdf/u3WG7K+IiZhtELto/A7Fck9Ws6SQvzRvOE8uSirYbgmj6He4iO8NCyvaK0jIQRMMGQwsU1quGmFgHIXPLfnpnfajr1rVTAwtgV5LEZ4Iel+W1GC8ugMhyr4/p1MtcIM42EA8BzE6ZQqC7VPqPvEjZ2dbZkaBhPbiZAS3YeYBRDWm1p1OZtWamT3cEvqqPpnjL1XyW+oyVVkaZdklLQp2Btgt9qr21m42f4wTw+Xrp6rCKNb0CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAh8zGlfSlcI0o3rYDPBB07aXNswb4ECNIKG0CETTUxmXl9KUL+9gGlqCz5iWLOgWsnrcKcY0vXPG9J1r9AqBNTqNgHq2G03X09266X5CpOe1zFo+Owb1zxtp3PehFdfQJ610CDLEaS9V9Rqp17hCyybEpOGVwe8fnk+fbEL2Bo3UPGrpsHzUoaGpDftmWssZkhpBJKVMJyf/RuP2SmmaIzmnw9JiSlYhzo4tpzd5rFXhjRbg4zW9C+2qok+2+qDM1iJ684gPHMIY8aLWrdgQTxkumGmTqgawR+N5MDtdPTEQ0XfIBc2cJEUyMTY5MPvACWpkA6SdS4xSvdXK3IVfOWA==' + ] +} + +test('x5c can be imported and have their X.509 cert thumbprints calculated', t => { + let key + t.notThrows(() => { key = importKey(jwk) }) + t.deepEqual(key.x5c, jwk.x5c) + const asJWK = key.toJWK() + t.deepEqual(asJWK.x5c, jwk.x5c) + ;[key.x5t, asJWK.x5t, key['x5t#S256'], asJWK['x5t#S256']].forEach((prop) => { + t.truthy(prop) + t.is(typeof prop, 'string') + }) +}) + +test('checks that x5c is an array of valid PKIX certificates', t => { + ;[[], {}, false, 1].forEach((value) => { + t.throws(() => { + importKey({ + ...jwk, + x5c: value + }) + }, { instanceOf: TypeError, message: '`x5c` must be an array of one or more PKIX certificates when provided' }) + t.throws(() => { + importKey({ + ...jwk, + x5c: [value] + }) + }, { instanceOf: TypeError, message: '`x5c` must be an array of one or more PKIX certificates when provided' }) + }) +}) + +test('checks that first x5c member must represent the key', t => { + t.throws(() => { + importKey({ + ...jwk, + x5c: [ + 'MIIC/zCCAeegAwIBAgIJYdZUZz2rikftMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNVBAMTEnBhbnZhLmV1LmF1dGgwLmNvbTAeFw0xNzEwMTgxNTExMjBaFw0zMTA2MjcxNTExMjBaMB0xGzAZBgNVBAMTEnBhbnZhLmV1LmF1dGgwLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKROvB+A+ZlFV1AXl75tVegjaCuBE7CiXHNstVZ/F6fKl6OvIRhAW3YKnJEglzVvHw0q46Nw48yBdbbKjdwGo1jbrI15D2+MYPy8xlMfDzEqNWBjOsgnA1nhFFDXD7wITwFRMtlRKVvKMa19QCmMFrpQ2qcloMne/DzSvxlEnVA6DG1SYqHR/gdK5hoRATJkwHXQ5F/nUxD3BOAyyjsU5RsGJAeVVS4Yf532xmziIbda3iV4LMUiHUb1v8Oy2sDncYF+imq/sbHGgE7dyv5R5AsYHGANgvIPMHJ1QTFSQVU0lxPy+EWnLk9abVOZYzD6O5YRdJ29UWVtQ1q5UcyrF18CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUSUcoPFHi/vm7dw1rt/IRmxvLMyowDgYDVR0PAQH/BAQDAgKEMA0GCSqGSIb3DQEBCwUAA4IBAQBcBXXBcbqliVOHkTgxocSYNUajcgIKjgeqG9RKFkbHfPuK/Hn80vQhd6mBKJTIyM7fY7DPh1/PjRsAyDQEwouHWItcM6iJBSdAkPq2DPfCkpUOi7MHrhXSouU1X4IOBvAl94k9Z8oj5k12KWVH8jZn5G03lwkWUgSfkLJ0Dh86+4sF2W4Dz2qZUXZuQbUL5eJcWRpfEZowff+T8xsiRjcIEpgfLz4nWonijtvEWESEa3bYpI9pI5OXLImgVJLGxVaUktsGIexQ6eM1AoxBYE7E+nbN/rwo30XWGbTkYecisySSYuzVn2c0xnC/8ZvW+gJ4SkzRDjlOAbm3R0r5j7b1' + ] + }) + }, { instanceOf: errors.JWKInvalid, code: 'ERR_JWK_INVALID', message: 'The key in the first `x5c` certificate MUST match the public key represented by the JWK' }) + t.throws(() => { + importKey({ + ...jwk, + x5c: [ + jwk.x5c[0], + 'MIIC/zCCAeegAwIBAgIJYdZUZz2rikftMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNVBAMTEnBhbnZhLmV1LmF1dGgwLmNvbTAeFw0xNzEwMTgxNTExMjBaFw0zMTA2MjcxNTExMjBaMB0xGzAZBgNVBAMTEnBhbnZhLmV1LmF1dGgwLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKROvB+A+ZlFV1AXl75tVegjaCuBE7CiXHNstVZ/F6fKl6OvIRhAW3YKnJEglzVvHw0q46Nw48yBdbbKjdwGo1jbrI15D2+MYPy8xlMfDzEqNWBjOsgnA1nhFFDXD7wITwFRMtlRKVvKMa19QCmMFrpQ2qcloMne/DzSvxlEnVA6DG1SYqHR/gdK5hoRATJkwHXQ5F/nUxD3BOAyyjsU5RsGJAeVVS4Yf532xmziIbda3iV4LMUiHUb1v8Oy2sDncYF+imq/sbHGgE7dyv5R5AsYHGANgvIPMHJ1QTFSQVU0lxPy+EWnLk9abVOZYzD6O5YRdJ29UWVtQ1q5UcyrF18CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUSUcoPFHi/vm7dw1rt/IRmxvLMyowDgYDVR0PAQH/BAQDAgKEMA0GCSqGSIb3DQEBCwUAA4IBAQBcBXXBcbqliVOHkTgxocSYNUajcgIKjgeqG9RKFkbHfPuK/Hn80vQhd6mBKJTIyM7fY7DPh1/PjRsAyDQEwouHWItcM6iJBSdAkPq2DPfCkpUOi7MHrhXSouU1X4IOBvAl94k9Z8oj5k12KWVH8jZn5G03lwkWUgSfkLJ0Dh86+4sF2W4Dz2qZUXZuQbUL5eJcWRpfEZowff+T8xsiRjcIEpgfLz4nWonijtvEWESEa3bYpI9pI5OXLImgVJLGxVaUktsGIexQ6eM1AoxBYE7E+nbN/rwo30XWGbTkYecisySSYuzVn2c0xnC/8ZvW+gJ4SkzRDjlOAbm3R0r5j7b1f' + ] + }) + }, { instanceOf: errors.JWKInvalid, code: 'ERR_JWK_INVALID', message: '`x5c` member at index 1 is not a valid base64-encoded DER PKIX certificate' }) +}) diff --git a/test/jwks/keystore.test.js b/test/jwks/keystore.test.js index af12836848..88cd22c480 100644 --- a/test/jwks/keystore.test.js +++ b/test/jwks/keystore.test.js @@ -1,7 +1,16 @@ const test = require('ava') const KeyStore = require('../../lib/jwks/keystore') -const { generateSync } = require('../../lib/jwk') +const { importKey, generateSync } = require('../../lib/jwk') + +const withX5C = { + kty: 'RSA', + n: 'vrjOfz9Ccdgx5nQudyhdoR17V-IubWMeOZCwX_jj0hgAsz2J_pqYW08PLbK_PdiVGKPrqzmDIsLI7sA25VEnHU1uCLNwBuUiCO11_-7dYbsr4iJmG0Qu2j8DsVyT1azpJC_NG84Ty5KKthuCaPod7iI7w0LK9orSMhBEwwZDCxTWq4aYWAchc8t-emd9qOvWtVMDC2BXksRngh6X5bUYLy6AyHKvj-nUy1wgzjYQDwHMTplCoLtU-o-8SNnZ1tmRoGE9uJkBLdh5gFENabWnU5m1ZqZPdwS-qo-meMvVfJb6jJVWRpl2SUtCnYG2C32qvbWbjZ_jBPD5eunqsIo1vQ', + e: 'AQAB', + x5c: [ + 'MIIDQjCCAiqgAwIBAgIGATz/FuLiMA0GCSqGSIb3DQEBBQUAMGIxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDTzEPMA0GA1UEBxMGRGVudmVyMRwwGgYDVQQKExNQaW5nIElkZW50aXR5IENvcnAuMRcwFQYDVQQDEw5CcmlhbiBDYW1wYmVsbDAeFw0xMzAyMjEyMzI5MTVaFw0xODA4MTQyMjI5MTVaMGIxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDTzEPMA0GA1UEBxMGRGVudmVyMRwwGgYDVQQKExNQaW5nIElkZW50aXR5IENvcnAuMRcwFQYDVQQDEw5CcmlhbiBDYW1wYmVsbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL64zn8/QnHYMeZ0LncoXaEde1fiLm1jHjmQsF/449IYALM9if6amFtPDy2yvz3YlRij66s5gyLCyO7ANuVRJx1NbgizcAblIgjtdf/u3WG7K+IiZhtELto/A7Fck9Ws6SQvzRvOE8uSirYbgmj6He4iO8NCyvaK0jIQRMMGQwsU1quGmFgHIXPLfnpnfajr1rVTAwtgV5LEZ4Iel+W1GC8ugMhyr4/p1MtcIM42EA8BzE6ZQqC7VPqPvEjZ2dbZkaBhPbiZAS3YeYBRDWm1p1OZtWamT3cEvqqPpnjL1XyW+oyVVkaZdklLQp2Btgt9qr21m42f4wTw+Xrp6rCKNb0CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAh8zGlfSlcI0o3rYDPBB07aXNswb4ECNIKG0CETTUxmXl9KUL+9gGlqCz5iWLOgWsnrcKcY0vXPG9J1r9AqBNTqNgHq2G03X09266X5CpOe1zFo+Owb1zxtp3PehFdfQJ610CDLEaS9V9Rqp17hCyybEpOGVwe8fnk+fbEL2Bo3UPGrpsHzUoaGpDftmWssZkhpBJKVMJyf/RuP2SmmaIzmnw9JiSlYhzo4tpzd5rFXhjRbg4zW9C+2qok+2+qDM1iJ684gPHMIY8aLWrdgQTxkumGmTqgawR+N5MDtdPTEQ0XfIBc2cJEUyMTY5MPvACWpkA6SdS4xSvdXK3IVfOWA==' + ] +} test('constructor', t => { t.notThrows(() => { @@ -148,6 +157,30 @@ test('.all() and .get() kid filter', t => { t.is(ks.get({ kid: 'foobar' }), k) }) +test('.all() and .get() x5t filter and sort', t => { + const k = importKey(withX5C) + const ks = new KeyStore(k) + t.deepEqual(ks.all({ x5t: 'baz' }), []) + t.deepEqual(ks.all({ x5t: '4pNenEBLv0JpLIdugWxQkOsZcK0' }), [k]) + t.is(ks.get({ x5t: 'baz' }), undefined) + t.is(ks.get({ x5t: '4pNenEBLv0JpLIdugWxQkOsZcK0' }), k) + const k2 = importKey({ ...withX5C, alg: 'RS256' }) + ks.add(k2) + t.is(ks.get({ x5t: '4pNenEBLv0JpLIdugWxQkOsZcK0', alg: 'RS256' }), k2) +}) + +test('.all() and .get() x5t#S256 filter and sort', t => { + const k = importKey(withX5C) + const ks = new KeyStore(k) + t.deepEqual(ks.all({ 'x5t#S256': 'baz' }), []) + t.deepEqual(ks.all({ 'x5t#S256': 'pJm2BBpkB8y7tCqrWM0X37WOmQTO8zQw-VpxVgBb21I' }), [k]) + t.is(ks.get({ 'x5t#S256': 'baz' }), undefined) + t.is(ks.get({ 'x5t#S256': 'pJm2BBpkB8y7tCqrWM0X37WOmQTO8zQw-VpxVgBb21I' }), k) + const k2 = importKey({ ...withX5C, alg: 'RS256' }) + ks.add(k2) + t.is(ks.get({ 'x5t#S256': 'pJm2BBpkB8y7tCqrWM0X37WOmQTO8zQw-VpxVgBb21I', alg: 'RS256' }), k2) +}) + test('.all() and .get() kty filter', t => { const ks = new KeyStore() ks.generateSync('RSA')