From 5fef6a7430e91e5daf222eb39f4d607c80279808 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sat, 9 Mar 2019 02:50:14 -0700 Subject: [PATCH] v1.1.8: cleanup and support forced keys for verification --- .gitignore | 1 + keyfetch-test.js | 10 ++-- keyfetch.js | 138 +++++++++++++++++++++++++--------------------- package-lock.json | 2 +- package.json | 5 +- 5 files changed, 86 insertions(+), 70 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/keyfetch-test.js b/keyfetch-test.js index a7f7f8e..55ad7f1 100644 --- a/keyfetch-test.js +++ b/keyfetch-test.js @@ -1,14 +1,14 @@ 'use strict'; var keyfetch = require('./keyfetch.js'); -var testUrl = "https://example.auth0.com"; +var testIss = "https://example.auth0.com"; keyfetch.init({}); -keyfetch.oidcJwks().then(function (jwks) { +keyfetch.oidcJwks(testIss).then(function (hits) { keyfetch._clear(); - console.log(jwks); - return keyfetch.oidcJwk(jwks[0].thumbprint, "https://example.auth0.com").then(function () { - return keyfetch.oidcJwk(jwks[0].thumbprint, "https://example.auth0.com").then(function (jwk) { + console.log(hits); + return keyfetch.oidcJwk(hits[0].thumbprint, testIss).then(function () { + return keyfetch.oidcJwk(hits[0].thumbprint, testIss).then(function (jwk) { console.log(jwk); }); }); diff --git a/keyfetch.js b/keyfetch.js index 386f9ed..b1cb6a3 100644 --- a/keyfetch.js +++ b/keyfetch.js @@ -236,6 +236,22 @@ keyfetch.verify = function (opts) { throw new Error("token's 'nbf' has not been reached or could not parsed: '" + nbf + "'"); } } + + if (opts.jwks || opts.jwk) { + return overrideLookup(opts.jwks || [opts.jwk]); + } + + function overrideLookup(jwks) { + return Promise.all(jwks.map(function (jwk) { + var Keypairs = jwk.x ? Eckles : Rasha; + return Keypairs.export({ jwk: jwk }).then(function (pem) { + return Keypairs.thumbprint({ jwk: jwk }).then(function (thumb) { + return { jwk: jwk, pem: pem, thumbprint: thumb }; + }); + }); + })).then(verifyAny); + } + var kid = decoded.header.kid; var iss; var fetcher; @@ -254,81 +270,79 @@ keyfetch.verify = function (opts) { fetchOne = keyfetch.jwk; } - function verify(jwk, payload) { + var payload = jwt.split('.')[1]; // as string, as it was signed + if (kid) { + return fetchOne(kid, iss).then(verifyOne); //.catch(fetchAny); + } else { + return fetcher(iss).then(verifyAny); + } + + function verify(hit, payload) { var alg = 'SHA' + decoded.header.alg.replace(/[^\d]+/i, ''); - var sig = convertIfEcdsa(decoded.header, decoded.signature); + var sig = ecdsaAsn1SigToJwtSig(decoded.header, decoded.signature); return require('crypto') .createVerify(alg) .update(jwt.split('.')[0] + '.' + payload) - .verify(jwk.pem, sig, 'base64') + .verify(hit.pem, sig, 'base64') ; } - function convertIfEcdsa(header, b64sig) { - // ECDSA JWT signatures differ from "normal" ECDSA signatures - // https://tools.ietf.org/html/rfc7518#section-3.4 - if (!/^ES/i.test(header.alg)) { return b64sig; } - - var bufsig = Buffer.from(b64sig, 'base64'); - var hlen = bufsig.byteLength / 2; // should be even - var r = bufsig.slice(0, hlen); - var s = bufsig.slice(hlen); - // unpad positive ints less than 32 bytes wide - while (!r[0]) { r = r.slice(1); } - while (!s[0]) { s = s.slice(1); } - // pad (or re-pad) ambiguously non-negative BigInts to 33 bytes wide - if (0x80 & r[0]) { r = Buffer.concat([Buffer.from([0]), r]); } - if (0x80 & s[0]) { s = Buffer.concat([Buffer.from([0]), s]); } - - var len = 2 + r.byteLength + 2 + s.byteLength; - var head = [0x30]; - // hard code 0x80 + 1 because it won't be longer than - // two SHA512 plus two pad bytes (130 bytes <= 256) - if (len >= 0x80) { head.push(0x81); } - head.push(len); - - var buf = Buffer.concat([ - Buffer.from(head) - , Buffer.from([0x02, r.byteLength]), r - , Buffer.from([0x02, s.byteLength]), s - ]); - - return buf.toString('base64') - .replace(/-/g, '+') - .replace(/_/g, '/') - .replace(/=/g, '') - ; - } - - var payload = jwt.split('.')[1]; // as string, as it was signed - if (kid) { - return fetchOne(kid, iss).then(verifyOne); //.catch(fetchAny); - } else { - return fetchAny(); - } - - function verifyOne(jwk) { - if (true === verify(jwk, payload)) { + function verifyOne(hit) { + if (true === verify(hit, payload)) { return decoded; } throw new Error('token signature verification was unsuccessful'); } - function fetchAny() { - return fetcher(iss).then(function (jwks) { - if (jwks.some(function (jwk) { - if (kid) { - if (kid !== jwk.kid && kid !== jwk.thumbprint) { return; } - if (true === verify(jwk, payload)) { return true; } - throw new Error('token signature verification was unsuccessful'); - } else { - if (true === verify(jwk, payload)) { return true; } - } - })) { - return decoded; + function verifyAny(hits) { + if (hits.some(function (hit) { + if (kid) { + if (kid !== hit.jwk.kid && kid !== hit.thumbprint) { return; } + if (true === verify(hit, payload)) { return true; } + throw new Error('token signature verification was unsuccessful'); + } else { + if (true === verify(hit, payload)) { return true; } } - throw new Error("Retrieved a list of keys, but none of them matched the 'kid' (key id) of the token."); - }); + })) { + return decoded; + } + throw new Error("Retrieved a list of keys, but none of them matched the 'kid' (key id) of the token."); } }); }; + +function ecdsaAsn1SigToJwtSig(header, b64sig) { + // ECDSA JWT signatures differ from "normal" ECDSA signatures + // https://tools.ietf.org/html/rfc7518#section-3.4 + if (!/^ES/i.test(header.alg)) { return b64sig; } + + var bufsig = Buffer.from(b64sig, 'base64'); + var hlen = bufsig.byteLength / 2; // should be even + var r = bufsig.slice(0, hlen); + var s = bufsig.slice(hlen); + // unpad positive ints less than 32 bytes wide + while (!r[0]) { r = r.slice(1); } + while (!s[0]) { s = s.slice(1); } + // pad (or re-pad) ambiguously non-negative BigInts to 33 bytes wide + if (0x80 & r[0]) { r = Buffer.concat([Buffer.from([0]), r]); } + if (0x80 & s[0]) { s = Buffer.concat([Buffer.from([0]), s]); } + + var len = 2 + r.byteLength + 2 + s.byteLength; + var head = [0x30]; + // hard code 0x80 + 1 because it won't be longer than + // two SHA512 plus two pad bytes (130 bytes <= 256) + if (len >= 0x80) { head.push(0x81); } + head.push(len); + + var buf = Buffer.concat([ + Buffer.from(head) + , Buffer.from([0x02, r.byteLength]), r + , Buffer.from([0x02, s.byteLength]), s + ]); + + return buf.toString('base64') + .replace(/-/g, '+') + .replace(/_/g, '/') + .replace(/=/g, '') + ; +} diff --git a/package-lock.json b/package-lock.json index 927b11c..f79dbed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "keyfetch", - "version": "1.1.0", + "version": "1.1.8", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index aa4dd67..afeb70c 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,5 @@ -{ "author": { +{ + "author": { "name": "AJ ONeal", "email": "solderjs@gmail.com" }, @@ -29,5 +30,5 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, - "version": "1.1.7" + "version": "1.1.8" }