import * as asn1js from "asn1js"; import * as pvtsutils from "pvtsutils"; import * as pvutils from "pvutils"; import { AuthorityKeyIdentifier } from "./AuthorityKeyIdentifier"; import { BasicOCSPResponse } from "./BasicOCSPResponse"; import { Certificate } from "./Certificate"; import { CertificateRevocationList } from "./CertificateRevocationList"; import * as common from "./common"; import * as Helpers from "./Helpers"; import { GeneralName } from "./GeneralName"; import { id_AnyPolicy, id_AuthorityInfoAccess, id_AuthorityKeyIdentifier, id_BasicConstraints, id_CertificatePolicies, id_CRLDistributionPoints, id_FreshestCRL, id_InhibitAnyPolicy, id_KeyUsage, id_NameConstraints, id_PolicyConstraints, id_PolicyMappings, id_SubjectAltName, id_SubjectKeyIdentifier } from "./ObjectIdentifiers"; import { RelativeDistinguishedNames } from "./RelativeDistinguishedNames"; import { GeneralSubtree } from "./GeneralSubtree"; import { EMPTY_STRING } from "./constants"; const TRUSTED_CERTS = "trustedCerts"; const CERTS = "certs"; const CRLS = "crls"; const OCSPS = "ocsps"; const CHECK_DATE = "checkDate"; const FIND_ORIGIN = "findOrigin"; const FIND_ISSUER = "findIssuer"; export enum ChainValidationCode { unknown = -1, success = 0, noRevocation = 11, noPath = 60, noValidPath = 97, } export class ChainValidationError extends Error { public static readonly NAME = "ChainValidationError"; public code: ChainValidationCode; constructor(code: ChainValidationCode, message: string) { super(message); this.name = ChainValidationError.NAME; this.code = code; this.message = message; } } export interface CertificateChainValidationEngineVerifyResult { result: boolean; resultCode: number; resultMessage: string; error?: Error | ChainValidationError; authConstrPolicies?: string[]; userConstrPolicies?: string[]; explicitPolicyIndicator?: boolean; policyMappings?: string[]; certificatePath?: Certificate[]; } export type FindOriginCallback = (certificate: Certificate, validationEngine: CertificateChainValidationEngine) => string; export type FindIssuerCallback = (certificate: Certificate, validationEngine: CertificateChainValidationEngine, crypto?: common.ICryptoEngine) => Promise; export interface CertificateChainValidationEngineParameters { trustedCerts?: Certificate[]; certs?: Certificate[]; crls?: CertificateRevocationList[]; ocsps?: BasicOCSPResponse[]; checkDate?: Date; findOrigin?: FindOriginCallback; findIssuer?: FindIssuerCallback; } interface CrlAndCertificate { crl: CertificateRevocationList; certificate: Certificate; } interface FindCrlResult { status: number; statusMessage: string; result?: CrlAndCertificate[]; } export interface CertificateChainValidationEngineVerifyParams { initialPolicySet?: string[]; initialExplicitPolicy?: boolean; initialPolicyMappingInhibit?: boolean; initialInhibitPolicy?: boolean; initialPermittedSubtreesSet?: GeneralSubtree[]; initialExcludedSubtreesSet?: GeneralSubtree[]; initialRequiredNameForms?: GeneralSubtree[]; passedWhenNotRevValues?: boolean; } /** * Returns `true` if the certificate is in the trusted list, otherwise `false` * @param cert A certificate that is expected to be in the trusted list * @param trustedList List of trusted certificates * @returns */ function isTrusted(cert: Certificate, trustedList: Certificate[]): boolean { for (let i = 0; i < trustedList.length; i++) { if (pvtsutils.BufferSourceConverter.isEqual(cert.tbsView, trustedList[i].tbsView)) { return true; } } return false; } /** * Represents a chain-building engine for {@link Certificate} certificates. * * @example * ```js The following example demonstrates how to verify certificate chain * const rootCa = pkijs.Certificate.fromBER(certRaw1); * const intermediateCa = pkijs.Certificate.fromBER(certRaw2); * const leafCert = pkijs.Certificate.fromBER(certRaw3); * const crl1 = pkijs.CertificateRevocationList.fromBER(crlRaw1); * const ocsp1 = pkijs.BasicOCSPResponse.fromBER(ocspRaw1); * * const chainEngine = new pkijs.CertificateChainValidationEngine({ * certs: [rootCa, intermediateCa, leafCert], * crls: [crl1], * ocsps: [ocsp1], * checkDate: new Date("2015-07-13"), // optional * trustedCerts: [rootCa], * }); * * const chain = await chainEngine.verify(); * ``` */ export class CertificateChainValidationEngine { /** * Array of pre-defined trusted (by user) certificates */ public trustedCerts: Certificate[]; /** * Array with certificate chain. Could be only one end-user certificate in there! */ public certs: Certificate[]; /** * Array of all CRLs for all certificates from certificate chain */ public crls: CertificateRevocationList[]; /** * Array of all OCSP responses */ public ocsps: BasicOCSPResponse[]; /** * The date at which the check would be */ public checkDate: Date; /** * The date at which the check would be */ public findOrigin: FindOriginCallback; /** * The date at which the check would be */ public findIssuer: FindIssuerCallback; /** * Constructor for CertificateChainValidationEngine class * @param parameters */ constructor(parameters: CertificateChainValidationEngineParameters = {}) { //#region Internal properties of the object this.trustedCerts = pvutils.getParametersValue(parameters, TRUSTED_CERTS, this.defaultValues(TRUSTED_CERTS)); this.certs = pvutils.getParametersValue(parameters, CERTS, this.defaultValues(CERTS)); this.crls = pvutils.getParametersValue(parameters, CRLS, this.defaultValues(CRLS)); this.ocsps = pvutils.getParametersValue(parameters, OCSPS, this.defaultValues(OCSPS)); this.checkDate = pvutils.getParametersValue(parameters, CHECK_DATE, this.defaultValues(CHECK_DATE)); this.findOrigin = pvutils.getParametersValue(parameters, FIND_ORIGIN, this.defaultValues(FIND_ORIGIN)); this.findIssuer = pvutils.getParametersValue(parameters, FIND_ISSUER, this.defaultValues(FIND_ISSUER)); //#endregion } public static defaultFindOrigin(certificate: Certificate, validationEngine: CertificateChainValidationEngine): string { //#region Firstly encode TBS for certificate if (certificate.tbsView.byteLength === 0) { certificate.tbsView = new Uint8Array(certificate.encodeTBS().toBER()); } //#endregion //#region Search in Intermediate Certificates for (const localCert of validationEngine.certs) { //#region Firstly encode TBS for certificate if (localCert.tbsView.byteLength === 0) { localCert.tbsView = new Uint8Array(localCert.encodeTBS().toBER()); } //#endregion if (pvtsutils.BufferSourceConverter.isEqual(certificate.tbsView, localCert.tbsView)) return "Intermediate Certificates"; } //#endregion //#region Search in Trusted Certificates for (const trustedCert of validationEngine.trustedCerts) { //#region Firstly encode TBS for certificate if (trustedCert.tbsView.byteLength === 0) trustedCert.tbsView = new Uint8Array(trustedCert.encodeTBS().toBER()); //#endregion if (pvtsutils.BufferSourceConverter.isEqual(certificate.tbsView, trustedCert.tbsView)) return "Trusted Certificates"; } //#endregion return "Unknown"; } public async defaultFindIssuer(certificate: Certificate, validationEngine: CertificateChainValidationEngine, crypto = common.getCrypto(true)): Promise { //#region Initial variables const result: Certificate[] = []; let keyIdentifier: asn1js.OctetString | null = null; let authorityCertIssuer: GeneralName[] | null = null; let authorityCertSerialNumber: asn1js.Integer | null = null; //#endregion //#region Speed-up searching in case of self-signed certificates if (certificate.subject.isEqual(certificate.issuer)) { try { const verificationResult = await certificate.verify(undefined, crypto); if (verificationResult) { return [certificate]; } } catch (ex) { // nothing } } //#endregion //#region Find values to speed-up search if (certificate.extensions) { for (const extension of certificate.extensions) { if (extension.extnID === id_AuthorityKeyIdentifier && extension.parsedValue instanceof AuthorityKeyIdentifier) { if (extension.parsedValue.keyIdentifier) { keyIdentifier = extension.parsedValue.keyIdentifier; } else { if (extension.parsedValue.authorityCertIssuer) { authorityCertIssuer = extension.parsedValue.authorityCertIssuer; } if (extension.parsedValue.authorityCertSerialNumber) { authorityCertSerialNumber = extension.parsedValue.authorityCertSerialNumber; } } break; } } } //#endregion // Aux function function checkCertificate(possibleIssuer: Certificate): void { //#region Firstly search for appropriate extensions if (keyIdentifier !== null) { if (possibleIssuer.extensions) { let extensionFound = false; for (const extension of possibleIssuer.extensions) { if (extension.extnID === id_SubjectKeyIdentifier && extension.parsedValue) { extensionFound = true; if (pvtsutils.BufferSourceConverter.isEqual(extension.parsedValue.valueBlock.valueHex, keyIdentifier.valueBlock.valueHexView)) { 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 } // Search in Trusted Certificates for (const trustedCert of validationEngine.trustedCerts) { checkCertificate(trustedCert); } // Search in Intermediate Certificates for (const intermediateCert of validationEngine.certs) { checkCertificate(intermediateCert); } // Now perform certificate verification checking for (let i = 0; i < result.length; i++) { try { const verificationResult = await certificate.verify(result[i], crypto); if (verificationResult === false) result.splice(i, 1); } catch (ex) { result.splice(i, 1); // Something wrong, remove the certificate } } return result; } /** * Returns default values for all class members * @param memberName String name for a class member * @returns Default value */ public defaultValues(memberName: typeof TRUSTED_CERTS): Certificate[]; public defaultValues(memberName: typeof CERTS): Certificate[]; public defaultValues(memberName: typeof CRLS): CertificateRevocationList[]; public defaultValues(memberName: typeof OCSPS): BasicOCSPResponse[]; public defaultValues(memberName: typeof CHECK_DATE): Date; public defaultValues(memberName: typeof FIND_ORIGIN): FindOriginCallback; public defaultValues(memberName: typeof FIND_ISSUER): FindIssuerCallback; public defaultValues(memberName: string): any { switch (memberName) { case TRUSTED_CERTS: return []; case CERTS: return []; case CRLS: return []; case OCSPS: return []; case CHECK_DATE: return new Date(); case FIND_ORIGIN: return CertificateChainValidationEngine.defaultFindOrigin; case FIND_ISSUER: return this.defaultFindIssuer; default: throw new Error(`Invalid member name for CertificateChainValidationEngine class: ${memberName}`); } } public async sort(passedWhenNotRevValues = false, crypto = common.getCrypto(true)): Promise { // Initial variables const localCerts: Certificate[] = []; //#region Building certificate path const buildPath = async (certificate: Certificate, crypto: common.ICryptoEngine): Promise => { const result: Certificate[][] = []; // Aux function checking array for unique elements function checkUnique(array: Certificate[]): boolean { 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; } if (isTrusted(certificate, this.trustedCerts)) { return [[certificate]]; } const findIssuerResult = await this.findIssuer(certificate, this, crypto); if (findIssuerResult.length === 0) { throw new Error("No valid certificate paths found"); } for (let i = 0; i < findIssuerResult.length; i++) { if (pvtsutils.BufferSourceConverter.isEqual(findIssuerResult[i].tbsView, certificate.tbsView)) { result.push([findIssuerResult[i]]); continue; } const buildPathResult = await buildPath(findIssuerResult[i], crypto); 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 const findCRL = async (certificate: Certificate): Promise => { //#region Initial variables const issuerCertificates: Certificate[] = []; const crls: CertificateRevocationList[] = []; const crlsAndCertificates: CrlAndCertificate[] = []; //#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(o => o.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++) { const crl = crls[i]; //#region Check "nextUpdate" for the CRL // The "nextUpdate" is older than CHECK_DATE. // Thus we should do have another, updated CRL. // Thus the CRL assumed to be invalid. if (crl.nextUpdate && crl.nextUpdate.value < this.checkDate) { continue; } //#endregion for (let j = 0; j < issuerCertificates.length; j++) { try { const result = await crls[i].verify({ issuerCertificate: issuerCertificates[j] }, crypto); if (result) { crlsAndCertificates.push({ crl: crls[i], certificate: issuerCertificates[j] }); break; } } catch (ex) { // nothing } } } //#endregion if (crlsAndCertificates.length) { return { status: 0, statusMessage: EMPTY_STRING, result: crlsAndCertificates }; } return { status: 3, statusMessage: "No valid CRLs found" }; }; //#endregion //#region Find OCSP for specific certificate const findOCSP = async (certificate: Certificate, issuerCertificate: Certificate): Promise => { //#region Get hash algorithm from certificate const hashAlgorithm = crypto.getAlgorithmByOID(certificate.signatureAlgorithm.algorithmId); if (!hashAlgorithm.name) { return 1; } if (!hashAlgorithm.hash) { return 1; } //#endregion //#region Search for OCSP response for the certificate for (let i = 0; i < this.ocsps.length; i++) { const ocsp = this.ocsps[i]; const result = await ocsp.getCertificateStatus(certificate, issuerCertificate, crypto); 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: Certificate, needToCheckCRL = false) { //#region Initial variables let isCA = false; let mustBeCA = false; let keyUsagePresent = false; let cRLSign = false; //#endregion if (certificate.extensions) { for (let j = 0; j < certificate.extensions.length; j++) { const extension = certificate.extensions[j]; if (extension.critical && !extension.parsedValue) { return { result: false, resultCode: 6, resultMessage: `Unable to parse critical certificate extension: ${extension.extnID}` }; } if (extension.extnID === id_KeyUsage) // KeyUsage { keyUsagePresent = true; const view = new Uint8Array(extension.parsedValue.valueBlock.valueHex); if ((view[0] & 0x04) === 0x04) // Set flag "keyCertSign" mustBeCA = true; if ((view[0] & 0x02) === 0x02) // Set flag "cRLSign" cRLSign = true; } if (extension.extnID === id_BasicConstraints) // BasicConstraints { if ("cA" in extension.parsedValue) { if (extension.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 BasicConstraints" }; } if ((keyUsagePresent === true) && (isCA === true) && (mustBeCA === false)) { return { result: false, resultCode: 4, resultMessage: "Unable to build certificate chain - \"keyCertSign\" flag was not set" }; } 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: EMPTY_STRING }; } //#endregion //#region Basic check for certificate path const basicCheck = async (path: Certificate[], checkDate: Date): Promise<{ result: boolean; resultCode?: number; resultMessage?: string; }> => { //#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: FindCrlResult = { status: 0, statusMessage: EMPTY_STRING }; //#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 && crlResult.result) { 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 new ChainValidationError(ChainValidationCode.noRevocation, `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 (issuerCertificate.extensions) { for (const extension of issuerCertificate.extensions) { switch (extension.extnID) { case id_CRLDistributionPoints: case id_FreshestCRL: case id_AuthorityInfoAccess: extensionFound = true; break; default: } } } if (extensionFound) { throw new ChainValidationError(ChainValidationCode.noRevocation, `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 (const [i, cert] of path.entries()) { if (!i) { // Skip entity certificate continue; } const result = await checkForCA(cert); if (!result.result) { 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 (pvtsutils.BufferSourceConverter.isEqual(localCerts[i].tbsView, localCerts[j].tbsView)) { localCerts.splice(j, 1); i = 0; break; } } } //#endregion const leafCert = localCerts[localCerts.length - 1]; //#region Initial variables let result; const certificatePath = [leafCert]; // The "end entity" certificate must be the least in CERTS array //#endregion //#region Build path for "end entity" certificate result = await buildPath(leafCert, crypto); if (result.length === 0) { throw new ChainValidationError(ChainValidationCode.noPath, "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 (pvtsutils.BufferSourceConverter.isEqual(certificate.tbsView, this.trustedCerts[k].tbsView)) { found = true; break; } } if (found) break; } if (!found) { result.splice(i, 1); i = 0; } } if (result.length === 0) { throw new ChainValidationError(ChainValidationCode.noValidPath, "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 parameters * @param crypto Crypto engine * @returns */ async verify(parameters: CertificateChainValidationEngineVerifyParams = {}, crypto = common.getCrypto(true)): Promise { //#region Auxiliary functions for name constraints checking /** * Compare two dNSName values * @param name DNS from name * @param constraint Constraint for DNS from name * @returns Boolean result - valid or invalid the "name" against the "constraint" */ function compareDNSName(name: string, constraint: string): boolean { //#region Make a "string preparation" for both name and constrain const namePrepared = Helpers.stringPrep(name); const constraintPrepared = Helpers.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; } /** * Compare two rfc822Name values * @param name E-mail address from name * @param constraint Constraint for e-mail address from name * @returns Boolean result - valid or invalid the "name" against the "constraint" */ function compareRFC822Name(name: string, constraint: string): boolean { //#region Make a "string preparation" for both name and constrain const namePrepared = Helpers.stringPrep(name); const constraintPrepared = Helpers.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); } /** * Compare two uniformResourceIdentifier values * @param name uniformResourceIdentifier from name * @param constraint Constraint for uniformResourceIdentifier from name * @returns Boolean result - valid or invalid the "name" against the "constraint" */ function compareUniformResourceIdentifier(name: string, constraint: string): boolean { //#region Make a "string preparation" for both name and constrain let namePrepared = Helpers.stringPrep(name); const constraintPrepared = Helpers.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; } /** * Compare two iPAddress values * @param name iPAddress from name * @param constraint Constraint for iPAddress from name * @returns Boolean result - valid or invalid the "name" against the "constraint" */ function compareIPAddress(name: asn1js.OctetString, constraint: asn1js.OctetString): boolean { //#region Common variables const nameView = name.valueBlock.valueHexView; const constraintView = constraint.valueBlock.valueHexView; //#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; } /** * Compare two directoryName values * @param name directoryName from name * @param constraint Constraint for directoryName from name * @returns Boolean result - valid or invalid the "name" against the "constraint" */ function compareDirectoryName(name: RelativeDistinguishedNames, constraint: RelativeDistinguishedNames): boolean { //#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 new Error("Empty certificate array"); //#endregion //#region Get input variables const passedWhenNotRevValues = parameters.passedWhenNotRevValues || false; const initialPolicySet = parameters.initialPolicySet || [id_AnyPolicy]; const initialExplicitPolicy = parameters.initialExplicitPolicy || false; const initialPolicyMappingInhibit = parameters.initialPolicyMappingInhibit || false; const initialInhibitPolicy = parameters.initialInhibitPolicy || false; const initialPermittedSubtreesSet = parameters.initialPermittedSubtreesSet || []; const initialExcludedSubtreesSet = parameters.initialExcludedSubtreesSet || []; const initialRequiredNameForms = parameters.initialRequiredNameForms || []; let explicitPolicyIndicator = initialExplicitPolicy; let policyMappingInhibitIndicator = initialPolicyMappingInhibit; let inhibitAnyPolicyIndicator = initialInhibitPolicy; const pendingConstraints = [ false, // For "explicitPolicyPending" false, // For "policyMappingInhibitPending" 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, crypto); //#endregion //#region Work with policies //#region Support variables const allPolicies: string[] = []; // Array of all policies (string values) allPolicies.push(id_AnyPolicy); // 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 necessary information from certificate chain for (let i = (this.certs.length - 2); i >= 0; i--, pathDepth++) { const cert = this.certs[i]; if (cert.extensions) { //#region Get information about certificate extensions for (let j = 0; j < cert.extensions.length; j++) { const extension = cert.extensions[j]; //#region CertificatePolicies if (extension.extnID === id_CertificatePolicies) { certPolicies[i] = extension.parsedValue; //#region Remove entry from "anyPolicies" for the certificate for (let s = 0; s < allPolicies.length; s++) { if (allPolicies[s] === id_AnyPolicy) { delete (policiesAndCerts[s])[i]; break; } } //#endregion for (let k = 0; k < extension.parsedValue.certificatePolicies.length; k++) { let policyIndex = (-1); const policyId = extension.parsedValue.certificatePolicies[k].policyIdentifier; //#region Try to find extension in "allPolicies" array for (let s = 0; s < allPolicies.length; s++) { if (policyId === allPolicies[s]) { policyIndex = s; break; } } //#endregion if (policyIndex === (-1)) { allPolicies.push(policyId); const certArray = new Array(this.certs.length - 1); certArray[i] = true; policiesAndCerts.push(certArray); } else (policiesAndCerts[policyIndex])[i] = true; } } //#endregion //#region PolicyMappings if (extension.extnID === id_PolicyMappings) { if (policyMappingInhibitIndicator) { return { result: false, resultCode: 98, resultMessage: "Policy mapping prohibited" }; } policyMappings[i] = extension.parsedValue; } //#endregion //#region PolicyConstraints if (extension.extnID === id_PolicyConstraints) { if (explicitPolicyIndicator === false) { //#region requireExplicitPolicy if (extension.parsedValue.requireExplicitPolicy === 0) { explicitPolicyIndicator = true; explicitPolicyStart = i; } else { if (pendingConstraints[0] === false) { pendingConstraints[0] = true; explicitPolicyPending = extension.parsedValue.requireExplicitPolicy; } else explicitPolicyPending = (explicitPolicyPending > extension.parsedValue.requireExplicitPolicy) ? extension.parsedValue.requireExplicitPolicy : explicitPolicyPending; } //#endregion //#region inhibitPolicyMapping if (extension.parsedValue.inhibitPolicyMapping === 0) policyMappingInhibitIndicator = true; else { if (pendingConstraints[1] === false) { pendingConstraints[1] = true; policyMappingInhibitPending = extension.parsedValue.inhibitPolicyMapping + 1; } else policyMappingInhibitPending = (policyMappingInhibitPending > (extension.parsedValue.inhibitPolicyMapping + 1)) ? (extension.parsedValue.inhibitPolicyMapping + 1) : policyMappingInhibitPending; } //#endregion } } //#endregion //#region InhibitAnyPolicy if (extension.extnID === id_InhibitAnyPolicy) { if (inhibitAnyPolicyIndicator === false) { if (extension.parsedValue.valueBlock.valueDec === 0) inhibitAnyPolicyIndicator = true; else { if (pendingConstraints[2] === false) { pendingConstraints[2] = true; inhibitAnyPolicyPending = extension.parsedValue.valueBlock.valueDec; } else inhibitAnyPolicyPending = (inhibitAnyPolicyPending > extension.parsedValue.valueBlock.valueDec) ? extension.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] === id_AnyPolicy) { 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 === id_AnyPolicy) || (policyMappings[i + 1].mappings[k].subjectDomainPolicy === id_AnyPolicy)) { 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 indexes 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] === id_AnyPolicy) { 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] === id_AnyPolicy) && (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] === id_AnyPolicy) { 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: string[] = []; if ((initialPolicySet.length === 1) && (initialPolicySet[0] === id_AnyPolicy) && (explicitPolicyIndicator === false)) userConstrPolicies = initialPolicySet; else { if ((authConstrPolicies.length === 1) && (authConstrPolicies[0] === id_AnyPolicy)) 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] === id_AnyPolicy)) { userConstrPolicies.push(authConstrPolicies[i]); break; } } } } } //#endregion //#region Combine output object const policyResult: CertificateChainValidationEngineVerifyResult = { result: (userConstrPolicies.length > 0), resultCode: 0, resultMessage: (userConstrPolicies.length > 0) ? EMPTY_STRING : "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++) { const cert = this.certs[i]; //#region Support variables let subjectAltNames: GeneralName[] = []; let certPermittedSubtrees: GeneralSubtree[] = []; let certExcludedSubtrees: GeneralSubtree[] = []; //#endregion if (cert.extensions) { for (let j = 0; j < cert.extensions.length; j++) { const extension = cert.extensions[j]; //#region NameConstraints if (extension.extnID === id_NameConstraints) { if ("permittedSubtrees" in extension.parsedValue) certPermittedSubtrees = certPermittedSubtrees.concat(extension.parsedValue.permittedSubtrees); if ("excludedSubtrees" in extension.parsedValue) certExcludedSubtrees = certExcludedSubtrees.concat(extension.parsedValue.excludedSubtrees); } //#endregion //#region SubjectAltName if (extension.extnID === id_SubjectAltName) subjectAltNames = subjectAltNames.concat(extension.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 !== cert.subject.typesAndValues.length) continue; formFound = true; for (let k = 0; k < cert.subject.typesAndValues.length; k++) { if (cert.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 necessary name form found"; throw policyResult; } //#endregion //#region Checking for "permited sub-trees" //#region Make groups for all types of constraints const constrGroups: GeneralSubtree[][] = [ // Array of array for groupped constraints [], // rfc822Name [], // dNSName [], // directoryName [], // uniformResourceIdentifier [], // 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 < cert.subject.typesAndValues.length; k++) { if ((cert.subject.typesAndValues[k].type === "1.2.840.113549.1.9.1") || // PKCS#9 e-mail address (cert.subject.typesAndValues[k].type === "0.9.2342.19200300.100.1.3")) // RFC1274 "rfc822Mailbox" e-mail address { valueExists = true; groupPermitted = groupPermitted || compareRFC822Name(cert.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(cert.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 < cert.subject.typesAndValues.length; k++) { if ((cert.subject.typesAndValues[k].type === "1.2.840.113549.1.9.1") || // PKCS#9 e-mail address (cert.subject.typesAndValues[k].type === "0.9.2342.19200300.100.1.3")) // RFC1274 "rfc822Mailbox" e-mail address excluded = excluded || compareRFC822Name(cert.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(cert.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 Error) { if (error instanceof ChainValidationError) { return { result: false, resultCode: error.code, resultMessage: error.message, error: error, }; } return { result: false, resultCode: ChainValidationCode.unknown, resultMessage: error.message, error: error, }; } if (error && typeof error === "object" && "resultMessage" in error) { return error as CertificateChainValidationEngineVerifyResult; } return { result: false, resultCode: -1, resultMessage: `${error}`, }; } } }