1"use strict"; 2Object.defineProperty(exports, "__esModule", { value: true }); 3exports.CertificateChainVerifier = exports.verifyCertificateChain = void 0; 4const error_1 = require("../error"); 5const trust_1 = require("../trust"); 6function verifyCertificateChain(leaf, certificateAuthorities) { 7 // Filter list of trusted CAs to those which are valid for the given 8 // leaf certificate. 9 const cas = (0, trust_1.filterCertAuthorities)(certificateAuthorities, { 10 start: leaf.notBefore, 11 end: leaf.notAfter, 12 }); 13 /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ 14 let error; 15 for (const ca of cas) { 16 try { 17 const verifier = new CertificateChainVerifier({ 18 trustedCerts: ca.certChain, 19 untrustedCert: leaf, 20 }); 21 return verifier.verify(); 22 } 23 catch (err) { 24 error = err; 25 } 26 } 27 // If we failed to verify the certificate chain for all of the trusted 28 // CAs, throw the last error we encountered. 29 throw new error_1.VerificationError({ 30 code: 'CERTIFICATE_ERROR', 31 message: 'Failed to verify certificate chain', 32 cause: error, 33 }); 34} 35exports.verifyCertificateChain = verifyCertificateChain; 36class CertificateChainVerifier { 37 constructor(opts) { 38 this.untrustedCert = opts.untrustedCert; 39 this.trustedCerts = opts.trustedCerts; 40 this.localCerts = dedupeCertificates([ 41 ...opts.trustedCerts, 42 opts.untrustedCert, 43 ]); 44 } 45 verify() { 46 // Construct certificate path from leaf to root 47 const certificatePath = this.sort(); 48 // Perform validation checks on each certificate in the path 49 this.checkPath(certificatePath); 50 // Return verified certificate path 51 return certificatePath; 52 } 53 sort() { 54 const leafCert = this.untrustedCert; 55 // Construct all possible paths from the leaf 56 let paths = this.buildPaths(leafCert); 57 // Filter for paths which contain a trusted certificate 58 paths = paths.filter((path) => path.some((cert) => this.trustedCerts.includes(cert))); 59 if (paths.length === 0) { 60 throw new error_1.VerificationError({ 61 code: 'CERTIFICATE_ERROR', 62 message: 'no trusted certificate path found', 63 }); 64 } 65 // Find the shortest of possible paths 66 /* istanbul ignore next */ 67 const path = paths.reduce((prev, curr) => prev.length < curr.length ? prev : curr); 68 // Construct chain from shortest path 69 // Removes the last certificate in the path, which will be a second copy 70 // of the root certificate given that the root is self-signed. 71 return [leafCert, ...path].slice(0, -1); 72 } 73 // Recursively build all possible paths from the leaf to the root 74 buildPaths(certificate) { 75 const paths = []; 76 const issuers = this.findIssuer(certificate); 77 if (issuers.length === 0) { 78 throw new error_1.VerificationError({ 79 code: 'CERTIFICATE_ERROR', 80 message: 'no valid certificate path found', 81 }); 82 } 83 for (let i = 0; i < issuers.length; i++) { 84 const issuer = issuers[i]; 85 // Base case - issuer is self 86 if (issuer.equals(certificate)) { 87 paths.push([certificate]); 88 continue; 89 } 90 // Recursively build path for the issuer 91 const subPaths = this.buildPaths(issuer); 92 // Construct paths by appending the issuer to each subpath 93 for (let j = 0; j < subPaths.length; j++) { 94 paths.push([issuer, ...subPaths[j]]); 95 } 96 } 97 return paths; 98 } 99 // Return all possible issuers for the given certificate 100 findIssuer(certificate) { 101 let issuers = []; 102 let keyIdentifier; 103 // Exit early if the certificate is self-signed 104 if (certificate.subject.equals(certificate.issuer)) { 105 if (certificate.verify()) { 106 return [certificate]; 107 } 108 } 109 // If the certificate has an authority key identifier, use that 110 // to find the issuer 111 if (certificate.extAuthorityKeyID) { 112 keyIdentifier = certificate.extAuthorityKeyID.keyIdentifier; 113 // TODO: Add support for authorityCertIssuer/authorityCertSerialNumber 114 // though Fulcio doesn't appear to use these 115 } 116 // Find possible issuers by comparing the authorityKeyID/subjectKeyID 117 // or issuer/subject. Potential issuers are added to the result array. 118 this.localCerts.forEach((possibleIssuer) => { 119 if (keyIdentifier) { 120 if (possibleIssuer.extSubjectKeyID) { 121 if (possibleIssuer.extSubjectKeyID.keyIdentifier.equals(keyIdentifier)) { 122 issuers.push(possibleIssuer); 123 } 124 return; 125 } 126 } 127 // Fallback to comparing certificate issuer and subject if 128 // subjectKey/authorityKey extensions are not present 129 if (possibleIssuer.subject.equals(certificate.issuer)) { 130 issuers.push(possibleIssuer); 131 } 132 }); 133 // Remove any issuers which fail to verify the certificate 134 issuers = issuers.filter((issuer) => { 135 try { 136 return certificate.verify(issuer); 137 } 138 catch (ex) { 139 /* istanbul ignore next - should never error */ 140 return false; 141 } 142 }); 143 return issuers; 144 } 145 checkPath(path) { 146 /* istanbul ignore if */ 147 if (path.length < 1) { 148 throw new error_1.VerificationError({ 149 code: 'CERTIFICATE_ERROR', 150 message: 'certificate chain must contain at least one certificate', 151 }); 152 } 153 // Ensure that all certificates beyond the leaf are CAs 154 const validCAs = path.slice(1).every((cert) => cert.isCA); 155 if (!validCAs) { 156 throw new error_1.VerificationError({ 157 code: 'CERTIFICATE_ERROR', 158 message: 'intermediate certificate is not a CA', 159 }); 160 } 161 // Certificate's issuer must match the subject of the next certificate 162 // in the chain 163 for (let i = path.length - 2; i >= 0; i--) { 164 /* istanbul ignore if */ 165 if (!path[i].issuer.equals(path[i + 1].subject)) { 166 throw new error_1.VerificationError({ 167 code: 'CERTIFICATE_ERROR', 168 message: 'incorrect certificate name chaining', 169 }); 170 } 171 } 172 // Check pathlength constraints 173 for (let i = 0; i < path.length; i++) { 174 const cert = path[i]; 175 // If the certificate is a CA, check the path length 176 if (cert.extBasicConstraints?.isCA) { 177 const pathLength = cert.extBasicConstraints.pathLenConstraint; 178 // The path length, if set, indicates how many intermediate 179 // certificates (NOT including the leaf) are allowed to follow. The 180 // pathLength constraint of any intermediate CA certificate MUST be 181 // greater than or equal to it's own depth in the chain (with an 182 // adjustment for the leaf certificate) 183 if (pathLength !== undefined && pathLength < i - 1) { 184 throw new error_1.VerificationError({ 185 code: 'CERTIFICATE_ERROR', 186 message: 'path length constraint exceeded', 187 }); 188 } 189 } 190 } 191 } 192} 193exports.CertificateChainVerifier = CertificateChainVerifier; 194// Remove duplicate certificates from the array 195function dedupeCertificates(certs) { 196 for (let i = 0; i < certs.length; i++) { 197 for (let j = i + 1; j < certs.length; j++) { 198 if (certs[i].equals(certs[j])) { 199 certs.splice(j, 1); 200 j--; 201 } 202 } 203 } 204 return certs; 205} 206