summaryrefslogtreecommitdiffstats
path: root/third_party/js/PKI.js/src/CertificateChainValidationEngine.ts
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/js/PKI.js/src/CertificateChainValidationEngine.ts')
-rw-r--r--third_party/js/PKI.js/src/CertificateChainValidationEngine.ts1828
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}`,
+ };
+ }
+ }
+
+}