@oada/certs
Version:
Generate and verify JWT signatures (OAuth dynamic client registration certificates and Trellis document integrity signatures) in the Open Ag Data Alliance (OADA) and Trellis ecosystems
229 lines • 10.8 kB
JavaScript
/**
* @license
* Copyright 2019 Open Ag Data Alliance
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.validate = exports.clearCache = exports.TRUSTED_LIST_URI = void 0;
const tslib_1 = require("tslib");
const node_jose_1 = require("node-jose");
const debug_1 = tslib_1.__importDefault(require("debug"));
const superagent_1 = tslib_1.__importDefault(require("superagent"));
const jwks_utils_js_1 = require("./jwks-utils.js");
const warn = (0, debug_1.default)('oada-certs:validate:warn');
const info = (0, debug_1.default)('oada-certs:validate:info');
const trace = (0, debug_1.default)('oada-certs:validate:trace');
exports.TRUSTED_LIST_URI = 'https://oada.github.io/oada-trusted-lists/client-registration-v2.json';
const trustedListCache = new Map();
function clearCache() {
trustedListCache.clear(); // Clear our cache of trusted lists
(0, jwks_utils_js_1.clearJWKsCache)(); // And clear the jwku library's cache of jwks sets
} // Mainly useful for testing...
exports.clearCache = clearCache;
// TS is dumb about Array.isArray
function isArray(value) {
return Array.isArray(value);
}
async function getLists(trustedListURIs, { trustedListCacheTime, timeout, }) {
const now = Date.now() / 1000; // Convert ms to sec
return Promise.all(trustedListURIs.map(async (listURI) => {
if (!trustedListCache.has(listURI) ||
trustedListCache.get(listURI).timeLastFetched <
now - trustedListCacheTime) {
// Either not cached, or cache is old
trace('listURI %s is not in cache or is stale, fetching...', listURI);
try {
const { body } = (await superagent_1.default.get(listURI).timeout(timeout));
const newCacheObject = {
timeLastFetched: now,
body,
};
trustedListCache.set(listURI, newCacheObject);
trace('Fetched list from URI %s, putting this into the cache: %o', listURI, newCacheObject);
return { listURI, ...newCacheObject };
}
catch {
warn('Unable to fetch trusted list at URI %s', listURI);
return;
}
}
// Else, we have it in the cache, so return the cached body directly
const cached = trustedListCache.get(listURI);
trace('listURI %s is in cache, returning cached value: %o', listURI, cached);
return { listURI, ...cached };
}));
}
/**
* Look in the list for a jku or jwk that matches the one on this signature:
*/
function findList(lists, { header }) {
for (const list of lists) {
if (!list?.body) {
continue;
}
const { body, listURI } = list;
// V1 trusted list: an array of strings that are all jku's (no jwk's supported in trusted list)
if (isArray(body)) {
const found = body.find((jku) => jku === header.jku); // Returns jku string
if (found) {
return found;
}
continue;
}
// V2 trusted list: an object with a list of jku's and/or jwk's
if (body.version === '2') {
// Check jku list to see if we have a match in this header:
const foundJKU = isArray(body.jkus)
? // If jkus is a list of strings of trusted URL's, see if it matches jku in header:
body.jkus.find((jku) => typeof jku === 'string' && jku.length > 0 && jku === header.jku)
: undefined;
// Check jwks key set in trusted list if there is one
const foundJWKInJWKS = (0, jwks_utils_js_1.isJWKset)(body.jwks) &&
// Search through the trusted JWKS set
header.jwk?.kid &&
(0, jwks_utils_js_1.findJWK)(header.jwk.kid, body.jwks)
? body.jwks // Keep the JWKS to use later in checking signature
: undefined;
trace('Searched list %s for jwk or jku from header, foundJKU = %s, foundJWKInJWKS = %s', listURI, foundJKU, foundJWKInJWKS);
// Returns either a JKU string, or a JWKS object
const result = foundJKU ?? foundJWKInJWKS;
if (result) {
return result;
}
}
}
return undefined;
}
async function validate(sig, { timeout = 1000, trustedListCacheTime = 3600, additionalTrustedListURIs = [], disableDefaultTrustedListURI = false, } = {}) {
const details = [];
try {
// Build the list of all the trusted lists we're going to check
const trustedListURIs = (disableDefaultTrustedListURI ? [] : [exports.TRUSTED_LIST_URI]).concat(additionalTrustedListURIs);
trace('additionalTrustedListURIs = %s', additionalTrustedListURIs);
trace('Using trustedListURIs = %s', trustedListURIs);
// ---------------------------------------------------------------------------
// Loop over all the trusted list URI's, checking if we already have in cache
// If in cache, also check that they are not stale and need to be replaced
trace(trustedListCache, 'Starting trusted lists cache check');
const lists = await getLists(trustedListURIs, {
trustedListCacheTime,
timeout,
});
// -----------------------------------------------------------------------------
// Now, look through all the lists to see if the jku on the signature is in
// any of the trusted lists
trace('List caching section finished, lists = %s', lists);
// Jwku.decodeWithoutVerify throws if the signature is invalid
let decoded;
try {
decoded = (0, jwks_utils_js_1.decodeWithoutVerify)(sig);
}
catch (error) {
details.push({
message: `Could not decode signature with jwku.decodeWithoutVerify: ${JSON.stringify(error, null, ' ')}`,
});
throw new Error('Decoding failed for signature');
}
if (!decoded?.header) {
trace(decoded, 'Decoded signature has no header');
details.push({ message: 'Decoding failed for certificate' });
throw new Error('Decoding failed for signature');
}
trace('Tried decoding the signature, resulting in decoded = %o', decoded);
// Now look in the list for a jku or jwk that matches the one on this signature:
const foundList = findList(lists, decoded);
if (!foundList) {
info('header of decoded signature does not have a jku or jwk key that ' +
'exists in any of the trusted lists. decoded.header = ', decoded.header);
details.push({
message: `Did not find trusted list corresponding to this decoded signature header: ${JSON.stringify(decoded.header)}`,
});
}
// FoundList is now either a string (jku) or object (trusted jwks)
trace('Result of search for jku or jwk that matches a trusted list entry = ', foundList);
details.push({
message: `Matched decoded header to trusted list: ${JSON.stringify(foundList, null, ' ')}`,
});
// IMPORTANT: !!foundList at this point does not know if the signature
// actually is valid and trusted, it only knows that the signature pointed
// at something in a trusted list. We don't really know if it is trusted
// until we check both that the signature pointed at something in a trusted
// list, AND the signature was signed with the private key of the trusted
// thing it pointed at. Therefore, in the next .then() block when we call
// verify with the jwk from here, if it throws then we know the signature
// couldn't be verified and will therefore be considered untrusted
let trusted = Boolean(foundList);
// If we found the jku from the header in a trusted list, then the call
// below will tell jwkForSignature to use that jku, go there and get the
// list of keys, then use the kid to lookup the jwk. If it was not found in
// a trusted list, then jwkForSignature will just return either the jwk
// from the header directly or the corresponding jwk from a jku lookup
let jwk;
try {
jwk = await (0, jwks_utils_js_1.jwkForSignature)(sig, foundList ? foundList : false, {
timeout,
});
}
catch (error) {
details.push({
message: `Failed to figure out public key (JWK) for signature. Error from jwkForSignature was:${JSON.stringify(error)}`,
});
}
if (!decoded) {
details.push({ message: 'Decoding failed for certificate' });
return {
trusted: false,
valid: false,
details,
};
}
// Now we can go ahead and verify the signature with the jwk:
let valid = false;
try {
valid = Boolean(jwk && (await node_jose_1.JWS.createVerify(await node_jose_1.JWK.asKey(jwk)).verify(sig))); // Actually returns an object with header, payload, protected, key
}
catch (error) {
details.push({
message: `Failed to verify JWT signature with public key. jwt.verify said: ${JSON.stringify(error, null, ' ')}`,
});
}
if (!valid) {
details.push({
message: 'jwt.verify says it does not verify with the given JWK. Setting valid = false, trusted = false.',
});
trusted = false;
}
// Made it all the way to the end! Return the results:
return {
trusted,
details,
valid,
...decoded,
};
}
catch (error) {
info(error, 'Error in oadacerts.validate');
details.push({
message: `Error in oadacerts.validate. err was: ${JSON.stringify(error, null, ' ')}`,
});
return {
trusted: false,
valid: false,
details,
};
}
}
exports.validate = validate;
//# sourceMappingURL=validate.js.map
;