src/CertificateChainValidationEngine.js
import { getParametersValue, isEqualBuffer } from "pvutils";
import { getAlgorithmByOID, stringPrep } from "./common.js";
//**************************************************************************************
export default class CertificateChainValidationEngine
{
//**********************************************************************************
/**
* Constructor for CertificateChainValidationEngine class
* @param {Object} [parameters={}]
* @property {Object} [schema] asn1js parsed value
*/
constructor(parameters = {})
{
//region Internal properties of the object
/**
* @type {Array.<Certificate>}
* @description Array of pre-defined trusted (by user) certificates
*/
this.trustedCerts = getParametersValue(parameters, "trustedCerts", this.defaultValues("trustedCerts"));
/**
* @type {Array.<Certificate>}
* @description Array with certificate chain. Could be only one end-user certificate in there!
*/
this.certs = getParametersValue(parameters, "certs", this.defaultValues("certs"));
/**
* @type {Array.<CertificateRevocationList>}
* @description Array of all CRLs for all certificates from certificate chain
*/
this.crls = getParametersValue(parameters, "crls", this.defaultValues("crls"));
/**
* @type {Array}
* @description Array of all OCSP responses
*/
this.ocsps = getParametersValue(parameters, "ocsps", this.defaultValues("ocsps"));
/**
* @type {Date}
* @description The date at which the check would be
*/
this.checkDate = getParametersValue(parameters, "checkDate", this.defaultValues("checkDate"));
/**
* @type {Function}
* @description The date at which the check would be
*/
this.findOrigin = getParametersValue(parameters, "findOrigin", this.defaultValues("findOrigin"));
/**
* @type {Function}
* @description The date at which the check would be
*/
this.findIssuer = getParametersValue(parameters, "findIssuer", this.defaultValues("findIssuer"));
//endregion
}
//**********************************************************************************
static defaultFindOrigin(certificate, validationEngine)
{
//region Firstly encode TBS for certificate
if(certificate.tbs.byteLength === 0)
certificate.tbs = certificate.encodeTBS();
//endregion
//region Search in Intermediate Certificates
for(const localCert of validationEngine.certs)
{
//region Firstly encode TBS for certificate
if(localCert.tbs.byteLength === 0)
localCert.tbs = localCert.encodeTBS();
//endregion
if(isEqualBuffer(certificate.tbs, localCert.tbs))
return "Intermediate Certificates";
}
//endregion
//region Search in Trusted Certificates
for(const trustedCert of validationEngine.trustedCerts)
{
//region Firstly encode TBS for certificate
if(trustedCert.tbs.byteLength === 0)
trustedCert.tbs = trustedCert.encodeTBS();
//endregion
if(isEqualBuffer(certificate.tbs, trustedCert.tbs))
return "Trusted Certificates";
}
//endregion
return "Unknown";
}
//**********************************************************************************
async defaultFindIssuer(certificate, validationEngine)
{
//region Initial variables
let result = [];
let keyIdentifier = null;
let authorityCertIssuer = null;
let authorityCertSerialNumber = null;
//endregion
//region Speed-up searching in case of self-signed certificates
if(certificate.subject.isEqual(certificate.issuer))
{
try
{
const verificationResult = await certificate.verify();
if(verificationResult === true)
return [certificate];
}
catch(ex)
{
}
}
//endregion
//region Find values to speed-up search
if("extensions" in certificate)
{
for(const extension of certificate.extensions)
{
if(extension.extnID === "2.5.29.35") // AuthorityKeyIdentifier
{
if("keyIdentifier" in extension.parsedValue)
keyIdentifier = extension.parsedValue.keyIdentifier;
else
{
if("authorityCertIssuer" in extension.parsedValue)
authorityCertIssuer = extension.parsedValue.authorityCertIssuer;
if("authorityCertSerialNumber" in extension.parsedValue)
authorityCertSerialNumber = extension.parsedValue.authorityCertSerialNumber;
}
break;
}
}
}
//endregion
//region Aux function
function checkCertificate(possibleIssuer)
{
//region Firstly search for appropriate extensions
if(keyIdentifier !== null)
{
if("extensions" in possibleIssuer)
{
let extensionFound = false;
for(const extension of possibleIssuer.extensions)
{
if(extension.extnID === "2.5.29.14") // SubjectKeyIdentifier
{
extensionFound = true;
if(isEqualBuffer(extension.parsedValue.valueBlock.valueHex, keyIdentifier.valueBlock.valueHex))
result.push(possibleIssuer);
break;
}
}
if(extensionFound)
return;
}
}
//endregion
//region Now search for authorityCertSerialNumber
let authorityCertSerialNumberEqual = false;
if(authorityCertSerialNumber !== null)
authorityCertSerialNumberEqual = possibleIssuer.serialNumber.isEqual(authorityCertSerialNumber);
//endregion
//region And at least search for Issuer data
if(authorityCertIssuer !== null)
{
if(possibleIssuer.subject.isEqual(authorityCertIssuer))
{
if(authorityCertSerialNumberEqual)
result.push(possibleIssuer);
}
}
else
{
if(certificate.issuer.isEqual(possibleIssuer.subject))
result.push(possibleIssuer);
}
//endregion
}
//endregion
//region Search in Trusted Certificates
for(const trustedCert of validationEngine.trustedCerts)
checkCertificate(trustedCert);
//endregion
//region Search in Intermediate Certificates
for(const intermediateCert of validationEngine.certs)
checkCertificate(intermediateCert);
//endregion
//region Now perform certificate verification checking
for(let i = 0; i < result.length; i++)
{
try
{
const verificationResult = await certificate.verify(result[i]);
if(verificationResult === false)
result.splice(i, 1);
}
catch(ex)
{
result.splice(i, 1); // Something wrong, remove the certificate
}
}
//endregion
return result;
}
//**********************************************************************************
/**
* Return default values for all class members
* @param {string} memberName String name for a class member
*/
defaultValues(memberName)
{
switch(memberName)
{
case "trustedCerts":
return [];
case "certs":
return [];
case "crls":
return [];
case "ocsps":
return [];
case "checkDate":
return new Date();
case "findOrigin":
return CertificateChainValidationEngine.defaultFindOrigin;
case "findIssuer":
return this.defaultFindIssuer;
default:
throw new Error(`Invalid member name for CertificateChainValidationEngine class: ${memberName}`);
}
}
//**********************************************************************************
async sort()
{
//region Initial variables
const localCerts = [];
const _this = this;
//endregion
//region Building certificate path
async function buildPath(certificate)
{
const result = [];
//region Aux function checking array for unique elements
function checkUnique(array)
{
let unique = true;
for(let i = 0; i < array.length; i++)
{
for(let j = 0; j < array.length; j++)
{
if(j === i)
continue;
if(array[i] === array[j])
{
unique = false;
break;
}
}
if(!unique)
break;
}
return unique;
}
//endregion
const findIssuerResult = await _this.findIssuer(certificate, _this);
if(findIssuerResult.length === 0)
throw new Error("No valid certificate paths found");
for(let i = 0; i < findIssuerResult.length; i++)
{
if(isEqualBuffer(findIssuerResult[i].tbs, certificate.tbs))
{
result.push([findIssuerResult[i]]);
continue;
}
const buildPathResult = await buildPath(findIssuerResult[i]);
for(let j = 0; j < buildPathResult.length; j++)
{
const copy = buildPathResult[j].slice();
copy.splice(0, 0, findIssuerResult[i]);
if(checkUnique(copy))
result.push(copy);
else
result.push(buildPathResult[j]);
}
}
return result;
}
//endregion
//region Find CRL for specific certificate
async function findCRL(certificate)
{
//region Initial variables
const issuerCertificates = [];
const crls = [];
const crlsAndCertificates = [];
//endregion
//region Find all possible CRL issuers
issuerCertificates.push(...localCerts.filter(element => certificate.issuer.isEqual(element.subject)));
if(issuerCertificates.length === 0)
{
return {
status: 1,
statusMessage: "No certificate's issuers"
};
}
//endregion
//region Find all CRLs for crtificate's issuer
crls.push(..._this.crls.filter(element => element.issuer.isEqual(certificate.issuer)));
if(crls.length === 0)
{
return {
status: 1,
statusMessage: "No CRLs for specific certificate issuer"
};
}
//endregion
//region Find specific certificate of issuer for each CRL
for(let i = 0; i < crls.length; i++)
{
//region Check "nextUpdate" for the CRL
// The "nextUpdate" is older than "checkDate".
// Thus we should do have another, updated CRL.
// Thus the CRL assumed to be invalid.
if(crls[i].nextUpdate.value < _this.checkDate)
continue;
//endregion
for(let j = 0; j < issuerCertificates.length; j++)
{
try
{
const result = await crls[i].verify({ issuerCertificate: issuerCertificates[j] });
if(result)
{
crlsAndCertificates.push({
crl: crls[i],
certificate: issuerCertificates[j]
});
break;
}
}
catch(ex)
{
}
}
}
//endregion
if(crlsAndCertificates.length)
{
return {
status: 0,
statusMessage: "",
result: crlsAndCertificates
};
}
return {
status: 1,
statusMessage: "No valid CRLs found"
};
}
//endregion
//region Find OCSP for specific certificate
async function findOCSP(certificate, issuerCertificate)
{
//region Get hash algorithm from certificate
const hashAlgorithm = getAlgorithmByOID(certificate.signatureAlgorithm.algorithmId);
if(("name" in hashAlgorithm) === false)
return 1;
if(("hash" in hashAlgorithm) === false)
return 1;
//endregion
//region Search for OCSP response for the certificate
for(let i = 0; i < _this.ocsps.length; i++)
{
const result = await _this.ocsps[i].getCertificateStatus(certificate, issuerCertificate);
if(result.isForCertificate)
{
if(result.status === 0)
return 0;
return 1;
}
}
//endregion
return 2;
}
//endregion
//region Check for certificate to be CA
async function checkForCA(certificate, needToCheckCRL = false)
{
//region Initial variables
let isCA = false;
let mustBeCA = false;
let keyUsagePresent = false;
let cRLSign = false;
//endregion
if("extensions" in certificate)
{
for(let j = 0; j < certificate.extensions.length; j++)
{
if((certificate.extensions[j].critical === true) &&
(("parsedValue" in certificate.extensions[j]) === false))
{
return {
result: false,
resultCode: 6,
resultMessage: `Unable to parse critical certificate extension: ${certificate.extensions[j].extnID}`
};
}
if(certificate.extensions[j].extnID === "2.5.29.15") // KeyUsage
{
keyUsagePresent = true;
const view = new Uint8Array(certificate.extensions[j].parsedValue.valueBlock.valueHex);
if((view[0] & 0x04) === 0x04) // Set flag "keyCertSign"
mustBeCA = true;
if((view[0] & 0x02) === 0x02) // Set flag "cRLSign"
cRLSign = true;
}
if(certificate.extensions[j].extnID === "2.5.29.19") // BasicConstraints
{
if("cA" in certificate.extensions[j].parsedValue)
{
if(certificate.extensions[j].parsedValue.cA === true)
isCA = true;
}
}
}
if((mustBeCA === true) && (isCA === false))
{
return {
result: false,
resultCode: 3,
resultMessage: "Unable to build certificate chain - using \"keyCertSign\" flag set without BasicConstaints"
};
}
if((keyUsagePresent === true) && (isCA === true) && (mustBeCA === false))
{
return {
result: false,
resultCode: 4,
resultMessage: "Unable to build certificate chain - \"keyCertSign\" flag was not set"
};
}
// noinspection OverlyComplexBooleanExpressionJS
if((isCA === true) && (keyUsagePresent === true) && ((needToCheckCRL) && (cRLSign === false)))
{
return {
result: false,
resultCode: 5,
resultMessage: "Unable to build certificate chain - intermediate certificate must have \"cRLSign\" key usage flag"
};
}
}
if(isCA === false)
{
return {
result: false,
resultCode: 7,
resultMessage: "Unable to build certificate chain - more than one possible end-user certificate"
};
}
return {
result: true,
resultCode: 0,
resultMessage: ""
};
}
//endregion
//region Basic check for certificate path
async function basicCheck(path, checkDate)
{
//region Check that all dates are valid
for(let i = 0; i < path.length; i++)
{
if((path[i].notBefore.value > checkDate) ||
(path[i].notAfter.value < checkDate))
{
return {
result: false,
resultCode: 8,
resultMessage: "The certificate is either not yet valid or expired"
};
}
}
//endregion
//region Check certificate name chain
// We should have at least two certificates: end entity and trusted root
if(path.length < 2)
{
return {
result: false,
resultCode: 9,
resultMessage: "Too short certificate path"
};
}
for(let i = (path.length - 2); i >= 0; i--)
{
//region Check that we do not have a "self-signed" certificate
if(path[i].issuer.isEqual(path[i].subject) === false)
{
if(path[i].issuer.isEqual(path[i + 1].subject) === false)
{
return {
result: false,
resultCode: 10,
resultMessage: "Incorrect name chaining"
};
}
}
//endregion
}
//endregion
//region Check each certificate (except "trusted root") to be non-revoked
if((_this.crls.length !== 0) || (_this.ocsps.length !== 0)) // If CRLs and OCSPs are empty then we consider all certificates to be valid
{
for(let i = 0; i < (path.length - 1); i++)
{
//region Initial variables
let ocspResult;
let crlResult;
//endregion
//region Check OCSPs first
if(_this.ocsps.length !== 0)
{
ocspResult = await findOCSP(path[i], path[i + 1]);
switch(ocspResult)
{
case 0:
continue;
case 1:
return {
result: false,
resultCode: 12,
resultMessage: "One of certificates was revoked via OCSP response"
};
case 2: // continue to check the certificate with CRL
break;
default:
}
}
//endregion
//region Check CRLs
if(_this.crls.length !== 0)
{
crlResult = await findCRL(path[i]);
if(crlResult.status)
{
throw {
result: false,
resultCode: 11,
resultMessage: `No revocation values found for one of certificates: ${crlResult.statusMessage}`
};
}
for(let j = 0; j < crlResult.result.length; j++)
{
//region Check that the CRL issuer certificate have not been revoked
const isCertificateRevoked = crlResult.result[j].crl.isCertificateRevoked(path[i]);
if(isCertificateRevoked)
{
return {
result: false,
resultCode: 12,
resultMessage: "One of certificates had been revoked"
};
}
//endregion
//region Check that the CRL issuer certificate is a CA certificate
const isCertificateCA = await checkForCA(crlResult.result[j].certificate, true);
if(isCertificateCA.result === false)
{
return {
result: false,
resultCode: 13,
resultMessage: "CRL issuer certificate is not a CA certificate or does not have crlSign flag"
};
}
//endregion
}
}
else
{
if(ocspResult === 2)
{
return {
result: false,
resultCode: 11,
resultMessage: "No revocation values found for one of certificates"
};
}
}
//endregion
}
}
//endregion
//region Check each certificate (except "end entity") in the path to be a CA certificate
for(let i = 1; i < path.length; i++)
{
const result = await checkForCA(path[i]);
if(result.result === false)
{
return {
result: false,
resultCode: 14,
resultMessage: "One of intermediate certificates is not a CA certificate"
};
}
}
//endregion
return {
result: true
};
}
//endregion
//region Do main work
//region Initialize "localCerts" by value of "_this.certs" + "_this.trustedCerts" arrays
localCerts.push(..._this.trustedCerts);
localCerts.push(..._this.certs);
//endregion
//region Check all certificates for been unique
for(let i = 0; i < localCerts.length; i++)
{
for(let j = 0; j < localCerts.length; j++)
{
if(i === j)
continue;
if(isEqualBuffer(localCerts[i].tbs, localCerts[j].tbs))
{
localCerts.splice(j, 1);
i = 0;
break;
}
}
}
//endregion
//region Initial variables
let result;
const certificatePath = [localCerts[localCerts.length - 1]]; // The "end entity" certificate must be the least in "certs" array
//endregion
//region Build path for "end entity" certificate
result = await buildPath(localCerts[localCerts.length - 1]);
if(result.length === 0)
{
return {
result: false,
resultCode: 60,
resultMessage: "Unable to find certificate path"
};
}
//endregion
//region Exclude certificate paths not ended with "trusted roots"
for(let i = 0; i < result.length; i++)
{
let found = false;
for(let j = 0; j < (result[i]).length; j++)
{
const certificate = (result[i])[j];
for(let k = 0; k < _this.trustedCerts.length; k++)
{
if(isEqualBuffer(certificate.tbs, _this.trustedCerts[k].tbs))
{
found = true;
break;
}
}
if(found)
break;
}
if(!found)
{
result.splice(i, 1);
i = 0;
}
}
if(result.length === 0)
{
throw {
result: false,
resultCode: 97,
resultMessage: "No valid certificate paths found"
};
}
//endregion
//region Find shortest certificate path (for the moment it is the only criteria)
let shortestLength = result[0].length;
let shortestIndex = 0;
for(let i = 0; i < result.length; i++)
{
if(result[i].length < shortestLength)
{
shortestLength = result[i].length;
shortestIndex = i;
}
}
//endregion
//region Create certificate path for basic check
for(let i = 0; i < result[shortestIndex].length; i++)
certificatePath.push((result[shortestIndex])[i]);
//endregion
//region Perform basic checking for all certificates in the path
result = await basicCheck(certificatePath, _this.checkDate);
if(result.result === false)
throw result;
//endregion
return certificatePath;
//endregion
}
//**********************************************************************************
/**
* Major verification function for certificate chain.
* @param {{initialPolicySet, initialExplicitPolicy, initialPolicyMappingInhibit, initialInhibitPolicy, initialPermittedSubtreesSet, initialExcludedSubtreesSet, initialRequiredNameForms}} [parameters]
* @returns {Promise}
*/
async verify(parameters = {})
{
//region Auxiliary functions for name constraints checking
function compareDNSName(name, constraint)
{
/// <summary>Compare two dNSName values</summary>
/// <param name="name" type="String">DNS from name</param>
/// <param name="constraint" type="String">Constraint for DNS from name</param>
/// <returns type="Boolean">Boolean result - valid or invalid the "name" against the "constraint"</returns>
//region Make a "string preparation" for both name and constrain
const namePrepared = stringPrep(name);
const constraintPrepared = stringPrep(constraint);
//endregion
//region Make a "splitted" versions of "constraint" and "name"
const nameSplitted = namePrepared.split(".");
const constraintSplitted = constraintPrepared.split(".");
//endregion
//region Length calculation and additional check
const nameLen = nameSplitted.length;
const constrLen = constraintSplitted.length;
if((nameLen === 0) || (constrLen === 0) || (nameLen < constrLen))
return false;
//endregion
//region Check that no part of "name" has zero length
for(let i = 0; i < nameLen; i++)
{
if(nameSplitted[i].length === 0)
return false;
}
//endregion
//region Check that no part of "constraint" has zero length
for(let i = 0; i < constrLen; i++)
{
if(constraintSplitted[i].length === 0)
{
if(i === 0)
{
if(constrLen === 1)
return false;
continue;
}
return false;
}
}
//endregion
//region Check that "name" has a tail as "constraint"
for(let i = 0; i < constrLen; i++)
{
if(constraintSplitted[constrLen - 1 - i].length === 0)
continue;
if(nameSplitted[nameLen - 1 - i].localeCompare(constraintSplitted[constrLen - 1 - i]) !== 0)
return false;
}
//endregion
return true;
}
function compareRFC822Name(name, constraint)
{
/// <summary>Compare two rfc822Name values</summary>
/// <param name="name" type="String">E-mail address from name</param>
/// <param name="constraint" type="String">Constraint for e-mail address from name</param>
/// <returns type="Boolean">Boolean result - valid or invalid the "name" against the "constraint"</returns>
//region Make a "string preparation" for both name and constrain
const namePrepared = stringPrep(name);
const constraintPrepared = stringPrep(constraint);
//endregion
//region Make a "splitted" versions of "constraint" and "name"
const nameSplitted = namePrepared.split("@");
const constraintSplitted = constraintPrepared.split("@");
//endregion
//region Splitted array length checking
if((nameSplitted.length === 0) || (constraintSplitted.length === 0) || (nameSplitted.length < constraintSplitted.length))
return false;
//endregion
if(constraintSplitted.length === 1)
{
const result = compareDNSName(nameSplitted[1], constraintSplitted[0]);
if(result)
{
//region Make a "splitted" versions of domain name from "constraint" and "name"
const ns = nameSplitted[1].split(".");
const cs = constraintSplitted[0].split(".");
//endregion
if(cs[0].length === 0)
return true;
return ns.length === cs.length;
}
return false;
}
return (namePrepared.localeCompare(constraintPrepared) === 0);
}
function compareUniformResourceIdentifier(name, constraint)
{
/// <summary>Compare two uniformResourceIdentifier values</summary>
/// <param name="name" type="String">uniformResourceIdentifier from name</param>
/// <param name="constraint" type="String">Constraint for uniformResourceIdentifier from name</param>
/// <returns type="Boolean">Boolean result - valid or invalid the "name" against the "constraint"</returns>
//region Make a "string preparation" for both name and constrain
let namePrepared = stringPrep(name);
const constraintPrepared = stringPrep(constraint);
//endregion
//region Find out a major URI part to compare with
const ns = namePrepared.split("/");
const cs = constraintPrepared.split("/");
if(cs.length > 1) // Malformed constraint
return false;
if(ns.length > 1) // Full URI string
{
for(let i = 0; i < ns.length; i++)
{
if((ns[i].length > 0) && (ns[i].charAt(ns[i].length - 1) !== ":"))
{
const nsPort = ns[i].split(":");
namePrepared = nsPort[0];
break;
}
}
}
//endregion
const result = compareDNSName(namePrepared, constraintPrepared);
if(result)
{
//region Make a "splitted" versions of "constraint" and "name"
const nameSplitted = namePrepared.split(".");
const constraintSplitted = constraintPrepared.split(".");
//endregion
if(constraintSplitted[0].length === 0)
return true;
return nameSplitted.length === constraintSplitted.length;
}
return false;
}
function compareIPAddress(name, constraint)
{
/// <summary>Compare two iPAddress values</summary>
/// <param name="name" type="in_window.org.pkijs.asn1.OCTETSTRING">iPAddress from name</param>
/// <param name="constraint" type="in_window.org.pkijs.asn1.OCTETSTRING">Constraint for iPAddress from name</param>
/// <returns type="Boolean">Boolean result - valid or invalid the "name" against the "constraint"</returns>
//region Common variables
const nameView = new Uint8Array(name.valueBlock.valueHex);
const constraintView = new Uint8Array(constraint.valueBlock.valueHex);
//endregion
//region Work with IPv4 addresses
if((nameView.length === 4) && (constraintView.length === 8))
{
for(let i = 0; i < 4; i++)
{
if((nameView[i] ^ constraintView[i]) & constraintView[i + 4])
return false;
}
return true;
}
//endregion
//region Work with IPv6 addresses
if((nameView.length === 16) && (constraintView.length === 32))
{
for(let i = 0; i < 16; i++)
{
if((nameView[i] ^ constraintView[i]) & constraintView[i + 16])
return false;
}
return true;
}
//endregion
return false;
}
function compareDirectoryName(name, constraint)
{
/// <summary>Compare two directoryName values</summary>
/// <param name="name" type="in_window.org.pkijs.simpl.RDN">directoryName from name</param>
/// <param name="constraint" type="in_window.org.pkijs.simpl.RDN">Constraint for directoryName from name</param>
/// <param name="any" type="Boolean">Boolean flag - should be comparision interrupted after first match or we need to match all "constraints" parts</param>
/// <returns type="Boolean">Boolean result - valid or invalid the "name" against the "constraint"</returns>
//region Initial check
if((name.typesAndValues.length === 0) || (constraint.typesAndValues.length === 0))
return true;
if(name.typesAndValues.length < constraint.typesAndValues.length)
return false;
//endregion
//region Initial variables
let result = true;
let nameStart = 0;
//endregion
for(let i = 0; i < constraint.typesAndValues.length; i++)
{
let localResult = false;
for(let j = nameStart; j < name.typesAndValues.length; j++)
{
localResult = name.typesAndValues[j].isEqual(constraint.typesAndValues[i]);
if(name.typesAndValues[j].type === constraint.typesAndValues[i].type)
result = result && localResult;
if(localResult === true)
{
if((nameStart === 0) || (nameStart === j))
{
nameStart = j + 1;
break;
}
else // Structure of "name" must be the same with "constraint"
return false;
}
}
if(localResult === false)
return false;
}
return (nameStart === 0) ? false : result;
}
//endregion
try
{
//region Initial checks
if(this.certs.length === 0)
throw "Empty certificate array";
//endregion
//region Get input variables
let initialPolicySet = [];
initialPolicySet.push("2.5.29.32.0"); // "anyPolicy"
let initialExplicitPolicy = false;
let initialPolicyMappingInhibit = false;
let initialInhibitPolicy = false;
let initialPermittedSubtreesSet = []; // Array of "simpl.x509.GeneralSubtree"
let initialExcludedSubtreesSet = []; // Array of "simpl.x509.GeneralSubtree"
let initialRequiredNameForms = []; // Array of "simpl.x509.GeneralSubtree"
if("initialPolicySet" in parameters)
initialPolicySet = parameters.initialPolicySet;
if("initialExplicitPolicy" in parameters)
initialExplicitPolicy = parameters.initialExplicitPolicy;
if("initialPolicyMappingInhibit" in parameters)
initialPolicyMappingInhibit = parameters.initialPolicyMappingInhibit;
if("initialInhibitPolicy" in parameters)
initialInhibitPolicy = parameters.initialInhibitPolicy;
if("initialPermittedSubtreesSet" in parameters)
initialPermittedSubtreesSet = parameters.initialPermittedSubtreesSet;
if("initialExcludedSubtreesSet" in parameters)
initialExcludedSubtreesSet = parameters.initialExcludedSubtreesSet;
if("initialRequiredNameForms" in parameters)
initialRequiredNameForms = parameters.initialRequiredNameForms;
let explicitPolicyIndicator = initialExplicitPolicy;
let policyMappingInhibitIndicator = initialPolicyMappingInhibit;
let inhibitAnyPolicyIndicator = initialInhibitPolicy;
const pendingConstraints = new Array(3);
pendingConstraints[0] = false; // For "explicitPolicyPending"
pendingConstraints[1] = false; // For "policyMappingInhibitPending"
pendingConstraints[2] = false; // For "inhibitAnyPolicyPending"
let explicitPolicyPending = 0;
let policyMappingInhibitPending = 0;
let inhibitAnyPolicyPending = 0;
let permittedSubtrees = initialPermittedSubtreesSet;
let excludedSubtrees = initialExcludedSubtreesSet;
const requiredNameForms = initialRequiredNameForms;
let pathDepth = 1;
//endregion
//region Sorting certificates in the chain array
this.certs = await this.sort();
//endregion
//region Work with policies
//region Support variables
const allPolicies = []; // Array of all policies (string values)
allPolicies.push("2.5.29.32.0"); // Put "anyPolicy" at first place
const policiesAndCerts = []; // In fact "array of array" where rows are for each specific policy, column for each certificate and value is "true/false"
const anyPolicyArray = new Array(this.certs.length - 1); // Minus "trusted anchor"
for(let ii = 0; ii < (this.certs.length - 1); ii++)
anyPolicyArray[ii] = true;
policiesAndCerts.push(anyPolicyArray);
const policyMappings = new Array(this.certs.length - 1); // Array of "PolicyMappings" for each certificate
const certPolicies = new Array(this.certs.length - 1); // Array of "CertificatePolicies" for each certificate
let explicitPolicyStart = (explicitPolicyIndicator) ? (this.certs.length - 1) : (-1);
//endregion
//region Gather all neccessary information from certificate chain
for(let i = (this.certs.length - 2); i >= 0; i--, pathDepth++)
{
if("extensions" in this.certs[i])
{
//region Get information about certificate extensions
for(let j = 0; j < this.certs[i].extensions.length; j++)
{
//region CertificatePolicies
if(this.certs[i].extensions[j].extnID === "2.5.29.32")
{
certPolicies[i] = this.certs[i].extensions[j].parsedValue;
//region Remove entry from "anyPolicies" for the certificate
for(let s = 0; s < allPolicies.length; s++)
{
if(allPolicies[s] === "2.5.29.32.0")
{
delete (policiesAndCerts[s])[i];
break;
}
}
//endregion
for(let k = 0; k < this.certs[i].extensions[j].parsedValue.certificatePolicies.length; k++)
{
let policyIndex = (-1);
//region Try to find extension in "allPolicies" array
for(let s = 0; s < allPolicies.length; s++)
{
if(this.certs[i].extensions[j].parsedValue.certificatePolicies[k].policyIdentifier === allPolicies[s])
{
policyIndex = s;
break;
}
}
//endregion
if(policyIndex === (-1))
{
allPolicies.push(this.certs[i].extensions[j].parsedValue.certificatePolicies[k].policyIdentifier);
const certArray = new Array(this.certs.length - 1);
certArray[i] = true;
policiesAndCerts.push(certArray);
}
else
(policiesAndCerts[policyIndex])[i] = true;
}
}
//endregion
//region PolicyMappings
if(this.certs[i].extensions[j].extnID === "2.5.29.33")
{
if(policyMappingInhibitIndicator)
{
return {
result: false,
resultCode: 98,
resultMessage: "Policy mapping prohibited"
};
}
policyMappings[i] = this.certs[i].extensions[j].parsedValue;
}
//endregion
//region PolicyConstraints
if(this.certs[i].extensions[j].extnID === "2.5.29.36")
{
if(explicitPolicyIndicator === false)
{
//region requireExplicitPolicy
if(this.certs[i].extensions[j].parsedValue.requireExplicitPolicy === 0)
{
explicitPolicyIndicator = true;
explicitPolicyStart = i;
}
else
{
if(pendingConstraints[0] === false)
{
pendingConstraints[0] = true;
explicitPolicyPending = this.certs[i].extensions[j].parsedValue.requireExplicitPolicy;
}
else
explicitPolicyPending = (explicitPolicyPending > this.certs[i].extensions[j].parsedValue.requireExplicitPolicy) ? this.certs[i].extensions[j].parsedValue.requireExplicitPolicy : explicitPolicyPending;
}
//endregion
//region inhibitPolicyMapping
if(this.certs[i].extensions[j].parsedValue.inhibitPolicyMapping === 0)
policyMappingInhibitIndicator = true;
else
{
if(pendingConstraints[1] === false)
{
pendingConstraints[1] = true;
policyMappingInhibitPending = this.certs[i].extensions[j].parsedValue.inhibitPolicyMapping + 1;
}
else
policyMappingInhibitPending = (policyMappingInhibitPending > (this.certs[i].extensions[j].parsedValue.inhibitPolicyMapping + 1)) ? (this.certs[i].extensions[j].parsedValue.inhibitPolicyMapping + 1) : policyMappingInhibitPending;
}
//endregion
}
}
//endregion
//region InhibitAnyPolicy
if(this.certs[i].extensions[j].extnID === "2.5.29.54")
{
if(inhibitAnyPolicyIndicator === false)
{
if(this.certs[i].extensions[j].parsedValue.valueBlock.valueDec === 0)
inhibitAnyPolicyIndicator = true;
else
{
if(pendingConstraints[2] === false)
{
pendingConstraints[2] = true;
inhibitAnyPolicyPending = this.certs[i].extensions[j].parsedValue.valueBlock.valueDec;
}
else
inhibitAnyPolicyPending = (inhibitAnyPolicyPending > this.certs[i].extensions[j].parsedValue.valueBlock.valueDec) ? this.certs[i].extensions[j].parsedValue.valueBlock.valueDec : inhibitAnyPolicyPending;
}
}
}
//endregion
}
//endregion
//region Check "inhibitAnyPolicyIndicator"
if(inhibitAnyPolicyIndicator === true)
{
let policyIndex = (-1);
//region Find "anyPolicy" index
for(let searchAnyPolicy = 0; searchAnyPolicy < allPolicies.length; searchAnyPolicy++)
{
if(allPolicies[searchAnyPolicy] === "2.5.29.32.0")
{
policyIndex = searchAnyPolicy;
break;
}
}
//endregion
if(policyIndex !== (-1))
delete (policiesAndCerts[0])[i]; // Unset value to "undefined" for "anyPolicies" value for current certificate
}
//endregion
//region Process with "pending constraints"
if(explicitPolicyIndicator === false)
{
if(pendingConstraints[0] === true)
{
explicitPolicyPending--;
if(explicitPolicyPending === 0)
{
explicitPolicyIndicator = true;
explicitPolicyStart = i;
pendingConstraints[0] = false;
}
}
}
if(policyMappingInhibitIndicator === false)
{
if(pendingConstraints[1] === true)
{
policyMappingInhibitPending--;
if(policyMappingInhibitPending === 0)
{
policyMappingInhibitIndicator = true;
pendingConstraints[1] = false;
}
}
}
if(inhibitAnyPolicyIndicator === false)
{
if(pendingConstraints[2] === true)
{
inhibitAnyPolicyPending--;
if(inhibitAnyPolicyPending === 0)
{
inhibitAnyPolicyIndicator = true;
pendingConstraints[2] = false;
}
}
}
//endregion
}
}
//endregion
//region Working with policy mappings
for(let i = 0; i < (this.certs.length - 1); i++)
{
//region Check that there is "policy mapping" for level "i + 1"
if((i < (this.certs.length - 2)) && (typeof policyMappings[i + 1] !== "undefined"))
{
for(let k = 0; k < policyMappings[i + 1].mappings.length; k++)
{
//region Check that we do not have "anyPolicy" in current mapping
if((policyMappings[i + 1].mappings[k].issuerDomainPolicy === "2.5.29.32.0") || (policyMappings[i + 1].mappings[k].subjectDomainPolicy === "2.5.29.32.0"))
{
return {
result: false,
resultCode: 99,
resultMessage: "The \"anyPolicy\" should not be a part of policy mapping scheme"
};
}
//endregion
//region Initial variables
let issuerDomainPolicyIndex = (-1);
let subjectDomainPolicyIndex = (-1);
//endregion
//region Search for index of policies indedes
for(let n = 0; n < allPolicies.length; n++)
{
if(allPolicies[n] === policyMappings[i + 1].mappings[k].issuerDomainPolicy)
issuerDomainPolicyIndex = n;
if(allPolicies[n] === policyMappings[i + 1].mappings[k].subjectDomainPolicy)
subjectDomainPolicyIndex = n;
}
//endregion
//region Delete existing "issuerDomainPolicy" because on the level we mapped the policy to another one
if(typeof (policiesAndCerts[issuerDomainPolicyIndex])[i] !== "undefined")
delete (policiesAndCerts[issuerDomainPolicyIndex])[i];
//endregion
//region Check all policies for the certificate
for(let j = 0; j < certPolicies[i].certificatePolicies.length; j++)
{
if(policyMappings[i + 1].mappings[k].subjectDomainPolicy === certPolicies[i].certificatePolicies[j].policyIdentifier)
{
//region Set mapped policy for current certificate
if((issuerDomainPolicyIndex !== (-1)) && (subjectDomainPolicyIndex !== (-1)))
{
for(let m = 0; m <= i; m++)
{
if(typeof (policiesAndCerts[subjectDomainPolicyIndex])[m] !== "undefined")
{
(policiesAndCerts[issuerDomainPolicyIndex])[m] = true;
delete (policiesAndCerts[subjectDomainPolicyIndex])[m];
}
}
}
//endregion
}
}
//endregion
}
}
//endregion
}
//endregion
//region Working with "explicitPolicyIndicator" and "anyPolicy"
for(let i = 0; i < allPolicies.length; i++)
{
if(allPolicies[i] === "2.5.29.32.0")
{
for(let j = 0; j < explicitPolicyStart; j++)
delete (policiesAndCerts[i])[j];
}
}
//endregion
//region Create "set of authorities-constrained policies"
const authConstrPolicies = [];
for(let i = 0; i < policiesAndCerts.length; i++)
{
let found = true;
for(let j = 0; j < (this.certs.length - 1); j++)
{
let anyPolicyFound = false;
if((j < explicitPolicyStart) && (allPolicies[i] === "2.5.29.32.0") && (allPolicies.length > 1))
{
found = false;
break;
}
if(typeof (policiesAndCerts[i])[j] === "undefined")
{
if(j >= explicitPolicyStart)
{
//region Search for "anyPolicy" in the policy set
for(let k = 0; k < allPolicies.length; k++)
{
if(allPolicies[k] === "2.5.29.32.0")
{
if((policiesAndCerts[k])[j] === true)
anyPolicyFound = true;
break;
}
}
//endregion
}
if(!anyPolicyFound)
{
found = false;
break;
}
}
}
if(found === true)
authConstrPolicies.push(allPolicies[i]);
}
//endregion
//region Create "set of user-constrained policies"
let userConstrPolicies = [];
if((initialPolicySet.length === 1) && (initialPolicySet[0] === "2.5.29.32.0") && (explicitPolicyIndicator === false))
userConstrPolicies = initialPolicySet;
else
{
if((authConstrPolicies.length === 1) && (authConstrPolicies[0] === "2.5.29.32.0"))
userConstrPolicies = initialPolicySet;
else
{
for(let i = 0; i < authConstrPolicies.length; i++)
{
for(let j = 0; j < initialPolicySet.length; j++)
{
if((initialPolicySet[j] === authConstrPolicies[i]) || (initialPolicySet[j] === "2.5.29.32.0"))
{
userConstrPolicies.push(authConstrPolicies[i]);
break;
}
}
}
}
}
//endregion
//region Combine output object
const policyResult = {
result: (userConstrPolicies.length > 0),
resultCode: 0,
resultMessage: (userConstrPolicies.length > 0) ? "" : "Zero \"userConstrPolicies\" array, no intersections with \"authConstrPolicies\"",
authConstrPolicies,
userConstrPolicies,
explicitPolicyIndicator,
policyMappings,
certificatePath: this.certs
};
if(userConstrPolicies.length === 0)
return policyResult;
//endregion
//endregion
//region Work with name constraints
//region Check a result from "policy checking" part
if(policyResult.result === false)
return policyResult;
//endregion
//region Check all certificates, excluding "trust anchor"
pathDepth = 1;
for(let i = (this.certs.length - 2); i >= 0; i--, pathDepth++)
{
//region Support variables
let subjectAltNames = [];
let certPermittedSubtrees = [];
let certExcludedSubtrees = [];
//endregion
if("extensions" in this.certs[i])
{
for(let j = 0; j < this.certs[i].extensions.length; j++)
{
//region NameConstraints
if(this.certs[i].extensions[j].extnID === "2.5.29.30")
{
if("permittedSubtrees" in this.certs[i].extensions[j].parsedValue)
certPermittedSubtrees = certPermittedSubtrees.concat(this.certs[i].extensions[j].parsedValue.permittedSubtrees);
if("excludedSubtrees" in this.certs[i].extensions[j].parsedValue)
certExcludedSubtrees = certExcludedSubtrees.concat(this.certs[i].extensions[j].parsedValue.excludedSubtrees);
}
//endregion
//region SubjectAltName
if(this.certs[i].extensions[j].extnID === "2.5.29.17")
subjectAltNames = subjectAltNames.concat(this.certs[i].extensions[j].parsedValue.altNames);
//endregion
}
}
//region Checking for "required name forms"
let formFound = (requiredNameForms.length <= 0);
for(let j = 0; j < requiredNameForms.length; j++)
{
switch(requiredNameForms[j].base.type)
{
case 4: // directoryName
{
if(requiredNameForms[j].base.value.typesAndValues.length !== this.certs[i].subject.typesAndValues.length)
continue;
formFound = true;
for(let k = 0; k < this.certs[i].subject.typesAndValues.length; k++)
{
if(this.certs[i].subject.typesAndValues[k].type !== requiredNameForms[j].base.value.typesAndValues[k].type)
{
formFound = false;
break;
}
}
if(formFound === true)
break;
}
break;
default: // ??? Probably here we should reject the certificate ???
}
}
if(formFound === false)
{
policyResult.result = false;
policyResult.resultCode = 21;
policyResult.resultMessage = "No neccessary name form found";
throw policyResult;
}
//endregion
//region Checking for "permited sub-trees"
//region Make groups for all types of constraints
const constrGroups = []; // Array of array for groupped constraints
constrGroups[0] = []; // rfc822Name
constrGroups[1] = []; // dNSName
constrGroups[2] = []; // directoryName
constrGroups[3] = []; // uniformResourceIdentifier
constrGroups[4] = []; // iPAddress
for(let j = 0; j < permittedSubtrees.length; j++)
{
switch(permittedSubtrees[j].base.type)
{
//region rfc822Name
case 1:
constrGroups[0].push(permittedSubtrees[j]);
break;
//endregion
//region dNSName
case 2:
constrGroups[1].push(permittedSubtrees[j]);
break;
//endregion
//region directoryName
case 4:
constrGroups[2].push(permittedSubtrees[j]);
break;
//endregion
//region uniformResourceIdentifier
case 6:
constrGroups[3].push(permittedSubtrees[j]);
break;
//endregion
//region iPAddress
case 7:
constrGroups[4].push(permittedSubtrees[j]);
break;
//endregion
//region default
default:
//endregion
}
}
//endregion
//region Check name constraints groupped by type, one-by-one
for(let p = 0; p < 5; p++)
{
let groupPermitted = false;
let valueExists = false;
const group = constrGroups[p];
for(let j = 0; j < group.length; j++)
{
switch(p)
{
//region rfc822Name
case 0:
if(subjectAltNames.length > 0)
{
for(let k = 0; k < subjectAltNames.length; k++)
{
if(subjectAltNames[k].type === 1) // rfc822Name
{
valueExists = true;
groupPermitted = groupPermitted || compareRFC822Name(subjectAltNames[k].value, group[j].base.value);
}
}
}
else // Try to find out "emailAddress" inside "subject"
{
for(let k = 0; k < this.certs[i].subject.typesAndValues.length; k++)
{
if((this.certs[i].subject.typesAndValues[k].type === "1.2.840.113549.1.9.1") || // PKCS#9 e-mail address
(this.certs[i].subject.typesAndValues[k].type === "0.9.2342.19200300.100.1.3")) // RFC1274 "rfc822Mailbox" e-mail address
{
valueExists = true;
groupPermitted = groupPermitted || compareRFC822Name(this.certs[i].subject.typesAndValues[k].value.valueBlock.value, group[j].base.value);
}
}
}
break;
//endregion
//region dNSName
case 1:
if(subjectAltNames.length > 0)
{
for(let k = 0; k < subjectAltNames.length; k++)
{
if(subjectAltNames[k].type === 2) // dNSName
{
valueExists = true;
groupPermitted = groupPermitted || compareDNSName(subjectAltNames[k].value, group[j].base.value);
}
}
}
break;
//endregion
//region directoryName
case 2:
valueExists = true;
groupPermitted = compareDirectoryName(this.certs[i].subject, group[j].base.value);
break;
//endregion
//region uniformResourceIdentifier
case 3:
if(subjectAltNames.length > 0)
{
for(let k = 0; k < subjectAltNames.length; k++)
{
if(subjectAltNames[k].type === 6) // uniformResourceIdentifier
{
valueExists = true;
groupPermitted = groupPermitted || compareUniformResourceIdentifier(subjectAltNames[k].value, group[j].base.value);
}
}
}
break;
//endregion
//region iPAddress
case 4:
if(subjectAltNames.length > 0)
{
for(let k = 0; k < subjectAltNames.length; k++)
{
if(subjectAltNames[k].type === 7) // iPAddress
{
valueExists = true;
groupPermitted = groupPermitted || compareIPAddress(subjectAltNames[k].value, group[j].base.value);
}
}
}
break;
//endregion
//region default
default:
//endregion
}
if(groupPermitted)
break;
}
if((groupPermitted === false) && (group.length > 0) && valueExists)
{
policyResult.result = false;
policyResult.resultCode = 41;
policyResult.resultMessage = "Failed to meet \"permitted sub-trees\" name constraint";
throw policyResult;
}
}
//endregion
//endregion
//region Checking for "excluded sub-trees"
let excluded = false;
for(let j = 0; j < excludedSubtrees.length; j++)
{
switch(excludedSubtrees[j].base.type)
{
//region rfc822Name
case 1:
if(subjectAltNames.length >= 0)
{
for(let k = 0; k < subjectAltNames.length; k++)
{
if(subjectAltNames[k].type === 1) // rfc822Name
excluded = excluded || compareRFC822Name(subjectAltNames[k].value, excludedSubtrees[j].base.value);
}
}
else // Try to find out "emailAddress" inside "subject"
{
for(let k = 0; k < this.certs[i].subject.typesAndValues.length; k++)
{
if((this.certs[i].subject.typesAndValues[k].type === "1.2.840.113549.1.9.1") || // PKCS#9 e-mail address
(this.certs[i].subject.typesAndValues[k].type === "0.9.2342.19200300.100.1.3")) // RFC1274 "rfc822Mailbox" e-mail address
excluded = excluded || compareRFC822Name(this.certs[i].subject.typesAndValues[k].value.valueBlock.value, excludedSubtrees[j].base.value);
}
}
break;
//endregion
//region dNSName
case 2:
if(subjectAltNames.length > 0)
{
for(let k = 0; k < subjectAltNames.length; k++)
{
if(subjectAltNames[k].type === 2) // dNSName
excluded = excluded || compareDNSName(subjectAltNames[k].value, excludedSubtrees[j].base.value);
}
}
break;
//endregion
//region directoryName
case 4:
excluded = excluded || compareDirectoryName(this.certs[i].subject, excludedSubtrees[j].base.value);
break;
//endregion
//region uniformResourceIdentifier
case 6:
if(subjectAltNames.length > 0)
{
for(let k = 0; k < subjectAltNames.length; k++)
{
if(subjectAltNames[k].type === 6) // uniformResourceIdentifier
excluded = excluded || compareUniformResourceIdentifier(subjectAltNames[k].value, excludedSubtrees[j].base.value);
}
}
break;
//endregion
//region iPAddress
case 7:
if(subjectAltNames.length > 0)
{
for(let k = 0; k < subjectAltNames.length; k++)
{
if(subjectAltNames[k].type === 7) // iPAddress
excluded = excluded || compareIPAddress(subjectAltNames[k].value, excludedSubtrees[j].base.value);
}
}
break;
//endregion
//region default
default: // No action, but probably here we need to create a warning for "malformed constraint"
//endregion
}
if(excluded)
break;
}
if(excluded === true)
{
policyResult.result = false;
policyResult.resultCode = 42;
policyResult.resultMessage = "Failed to meet \"excluded sub-trees\" name constraint";
throw policyResult;
}
//endregion
//region Append "cert_..._subtrees" to "..._subtrees"
permittedSubtrees = permittedSubtrees.concat(certPermittedSubtrees);
excludedSubtrees = excludedSubtrees.concat(certExcludedSubtrees);
//endregion
}
//endregion
return policyResult;
//endregion
}
catch(error)
{
if(error instanceof Object)
{
if("resultMessage" in error)
return error;
if("message" in error)
{
return {
result: false,
resultCode: -1,
resultMessage: error.message
};
}
}
return {
result: false,
resultCode: -1,
resultMessage: error
};
}
}
//**********************************************************************************
}
//**************************************************************************************