diff options
Diffstat (limited to 'third_party/js/PKI.js/src/CertificateChainValidationEngine.ts')
-rw-r--r-- | third_party/js/PKI.js/src/CertificateChainValidationEngine.ts | 1828 |
1 files changed, 1828 insertions, 0 deletions
diff --git a/third_party/js/PKI.js/src/CertificateChainValidationEngine.ts b/third_party/js/PKI.js/src/CertificateChainValidationEngine.ts new file mode 100644 index 0000000000..66d57b035c --- /dev/null +++ b/third_party/js/PKI.js/src/CertificateChainValidationEngine.ts @@ -0,0 +1,1828 @@ +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<Certificate[]>; + +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<Certificate[]> { + //#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<Certificate[]> { + // Initial variables + const localCerts: Certificate[] = []; + + //#region Building certificate path + const buildPath = async (certificate: Certificate, crypto: common.ICryptoEngine): Promise<Certificate[][]> => { + 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<FindCrlResult> => { + //#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<number> => { + //#region Get hash algorithm from certificate + const hashAlgorithm = crypto.getAlgorithmByOID<any>(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<CertificateChainValidationEngineVerifyResult> { + //#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}`, + }; + } + } + +} |