/* eslint-disable no-underscore-dangle */
const _ = require('lodash');
const platforms = require('./platforms');
const connectors = require('./connectors');
const { inspectCertificate, validateCertificate } = require('./certificates');
const { getCertificateAuthorities } = require('./certificateAuthorities');
const { calculateChainPaths, extractPath } = require('./utils');
const { inMemory } = require('./storage');
const { ROOT_CERT_PATH } = require('./constants');
const { getCachedCertificate, addCachedCertificate } = require('./cachedCertificates');
/**
* @private
* Follows a predicted certificate chain path in order to locate and validate certificates.
* @param {object} context The session context.
* @param {object} paths The path names to follow.
* @param {string} name The next path name to find.
* @param {string} addr The next address to find.
* @param {string} forAddr The next for address to find.
* @param {string} target The target address we're trying to reach.
* @returns {Array} A list of all acquired certificates.
*/
async function _resolveCertificateChain(context, paths, name, addr, forAddr, isContract, options) {
const { target, cache } = options;
const { pathName, platformName } = extractPath(name);
if (paths.length === 0 || (addr === target && paths[0] !== ROOT_CERT_PATH)) {
// if we ran out of paths to validate, then we successfully walked across the chain.
return [];
}
// a contract has been encounted which means we've crossed a token boundary.
if (forAddr && isContract) {
await context.platforms[platformName].setTokenContract(forAddr);
}
// acquire certificate.
let data;
if (cache || name.startsWith('@')) {
data = await getCachedCertificate(context, name);
}
if (!data) {
try {
const cert = await context.platforms[platformName].downloadCertificate(pathName);
data = await inspectCertificate(cert);
if (cache) {
await addCachedCertificate(context, cert, true);
}
} catch (err) {
// if we can't get data from the blockchain, then the chain validation is incomplete.
return [null];
}
}
// if we don't find a certificate that matches our criteria, then the chain is broken.
if (!data) {
return [null];
}
// analyze certificate.
const nextAddress = data.certificate.requestAddress;
const nextfor = data.certificate.forAddress;
const nextIsContract = data.certificate.contractNonce !== undefined;
const { status, error } = await validateCertificate(data, addr);
data.status = status;
// if something bad happened, then we can't complete the chain.
if (error) {
return [null];
}
// if we found the target address, then we completed the chain.
if (addr === target) {
return [data];
}
return [data,
...(await _resolveCertificateChain(
context,
paths.slice(1),
`${paths[0]}@${platformName}`,
nextAddress,
nextfor,
nextIsContract,
options,
)),
];
// TODO: add cycle detection.
}
/**
* Resolves certificate chain information for a given certificate.
* Note: requires an Internet connection.
* @param {object} context The session context.
* @param {*} certData The certificate to inspect chain data from.
* @returns {Promise<object>} Any located certificates, as well as whether the chain status.
*/
async function resolveCertificateChain(context, certData, options) {
const data = await inspectCertificate(certData);
if ((await validateCertificate(data)).error) {
throw new Error('The provided certificate is not valid.');
}
const targetAddress = data.signatureAddress;
const targetName = data.certificate.subject.name;
const { pathName: certPath, platformName: certPlatform } = extractPath(targetName);
const paths = [
certPath,
...calculateChainPaths(certPath),
].reverse().slice(1);
const CAs = (await getCertificateAuthorities(context))
.filter((i) => i.platform === certPlatform
|| platforms[certPlatform].getCompatiblePlatforms().indexOf(i.platform) >= 0);
// TODO: reseach whether this process could be performed in parallel safely.
for (let i = 0; i < CAs.length; i += 1) {
// eslint-disable-next-line no-await-in-loop
const chain = await _resolveCertificateChain(
context,
paths,
`@${certPlatform}`,
CAs[i].address,
CAs[i].forAddress,
true,
{ target: targetAddress, ...options },
);
if (chain.length === 0 || chain[chain.length - 1]) {
return { status: 'Complete', chain };
}
if (chain.length > 0) {
return { status: 'Incomplete', chain };
}
}
return { status: 'CA Not Found', chain: [] };
}
/**
* Verifies whether a certificate and its chain of certificates is valid.
* @param {object} context The session context.
* @param {*} certData The certificate to validate chain data from.
* @returns {Promise<string>} Returns "Verified" if verified, otherwise returns an error message.
*/
async function validateCertificateChain(context, certData) {
const { status } = await resolveCertificateChain(context, certData);
if (status === 'Complete') {
return { status: 'Valid' };
}
return { status: 'Invalid', error: status };
}
async function downloadCertificate(context, path, { cache }) {
const [pathName, platformName] = path.split('@');
const CAs = (await getCertificateAuthorities(context))
.filter((i) => i.platform === platformName
|| platforms[platformName].getCompatiblePlatforms().indexOf(i.platform) >= 0);
// TODO: reseach whether this process could be performed in parallel safely.
for (let i = 0; i < CAs.length; i += 1) {
// eslint-disable-next-line no-await-in-loop
await context.platforms[platformName].setTokenContract(CAs[i].forAddress);
if (cache) {
// eslint-disable-next-line no-await-in-loop
const data = await getCachedCertificate(context, path);
if (data) {
return data;
}
}
// eslint-disable-next-line no-await-in-loop
const result = await context.platforms[platformName].downloadCertificate(pathName);
if (result) {
return result;
}
}
return null;
}
/**
* Creates a session context for performing lookups against a blockchain.
* @param {*} platformOptions The platform(s) to use.
* @param {*} storage Storage and caching options.
* @returns {Promise<object>} A session context.
*/
async function createSessionContext(platformOptions, storage = null) {
const platformConnectors = {};
await Promise.all(_.keys(platformOptions).map(async (platform) => {
let options = {}; let args = [];
if (platformOptions[platform]) {
if (_.isArray(platformOptions[platform])) {
args = platformOptions[platform];
} else {
options = platformOptions[platform];
}
}
const [platformName, network] = platform.split(':');
platformConnectors[platform] = await connectors[platformName](network, options, ...args);
}));
return {
platforms: platformConnectors,
storage: storage || (await inMemory()),
};
}
module.exports = {
resolveCertificateChain,
validateCertificateChain,
downloadCertificate,
createSessionContext,
};