import { getParametersValue, isEqualBuffer } from "pvutils"; import { getAlgorithmByOID, stringPrep } from "./common.js"; //************************************************************************************** export default class CertificateChainValidationEngine { //********************************************************************************** /** * Constructor for CertificateChainValidationEngine class * @param {Object} [parameters={}] * @param {Object} [parameters.schema] asn1js parsed value to initialize the class from */ constructor(parameters = {}) { //region Internal properties of the object /** * @type {Array.} * @desc Array of pre-defined trusted (by user) certificates */ this.trustedCerts = getParametersValue(parameters, "trustedCerts", this.defaultValues("trustedCerts")); /** * @type {Array.} * @desc Array with certificate chain. Could be only one end-user certificate in there! */ this.certs = getParametersValue(parameters, "certs", this.defaultValues("certs")); /** * @type {Array.} * @desc Array of all CRLs for all certificates from certificate chain */ this.crls = getParametersValue(parameters, "crls", this.defaultValues("crls")); /** * @type {Array} * @desc Array of all OCSP responses */ this.ocsps = getParametersValue(parameters, "ocsps", this.defaultValues("ocsps")); /** * @type {Date} * @desc The date at which the check would be */ this.checkDate = getParametersValue(parameters, "checkDate", this.defaultValues("checkDate")); /** * @type {Function} * @desc The date at which the check would be */ this.findOrigin = getParametersValue(parameters, "findOrigin", this.defaultValues("findOrigin")); /** * @type {Function} * @desc 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(passedWhenNotRevValues = false) { //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 certificate's issuer crls.push(..._this.crls.filter(element => element.issuer.isEqual(certificate.issuer))); if(crls.length === 0) { return { status: 2, 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: 3, 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 = 2; let crlResult = { status: 0, statusMessage: "" }; //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 === 0) { 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(passedWhenNotRevValues === false) { throw { result: false, resultCode: 11, resultMessage: `No revocation values found for one of certificates: ${crlResult.statusMessage}` }; } } } else { if(ocspResult === 2) { return { result: false, resultCode: 11, resultMessage: "No revocation values found for one of certificates" }; } } //endregion //region Check we do have links to revocation values inside issuer's certificate if((ocspResult === 2) && (crlResult.status === 2) && passedWhenNotRevValues) { const issuerCertificate = path[i + 1]; let extensionFound = false; if("extensions" in issuerCertificate) { for(const extension of issuerCertificate.extensions) { switch(extension.extnID) { case "2.5.29.31": // CRLDistributionPoints case "2.5.29.46": // FreshestCRL case "1.3.6.1.5.5.7.1.1": // AuthorityInfoAccess extensionFound = true; break; default: } } } if(extensionFound) { throw { result: false, resultCode: 11, resultMessage: `No revocation values found for one of certificates: ${crlResult.statusMessage}` }; } } //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) { /// Compare two dNSName values /// DNS from name /// Constraint for DNS from name /// Boolean result - valid or invalid the "name" against the "constraint" //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) { /// Compare two rfc822Name values /// E-mail address from name /// Constraint for e-mail address from name /// Boolean result - valid or invalid the "name" against the "constraint" //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) { /// Compare two uniformResourceIdentifier values /// uniformResourceIdentifier from name /// Constraint for uniformResourceIdentifier from name /// Boolean result - valid or invalid the "name" against the "constraint" //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) { /// Compare two iPAddress values /// iPAddress from name /// Constraint for iPAddress from name /// Boolean result - valid or invalid the "name" against the "constraint" //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) { /// Compare two directoryName values /// directoryName from name /// Constraint for directoryName from name /// Boolean flag - should be comparision interrupted after first match or we need to match all "constraints" parts /// Boolean result - valid or invalid the "name" against the "constraint" //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 passedWhenNotRevValues = false; if("passedWhenNotRevValues" in parameters) passedWhenNotRevValues = parameters.passedWhenNotRevValues; 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(passedWhenNotRevValues); //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 }; } } //********************************************************************************** } //**************************************************************************************