1"use strict"; 2Object.defineProperty(exports, "__esModule", { value: true }); 3exports.verifyCertificateChain = void 0; 4/* 5Copyright 2023 The Sigstore Authors. 6 7Licensed under the Apache License, Version 2.0 (the "License"); 8you may not use this file except in compliance with the License. 9You may obtain a copy of the License at 10 11 http://www.apache.org/licenses/LICENSE-2.0 12 13Unless required by applicable law or agreed to in writing, software 14distributed under the License is distributed on an "AS IS" BASIS, 15WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16See the License for the specific language governing permissions and 17limitations under the License. 18*/ 19const error_1 = require("../error"); 20function verifyCertificateChain(opts) { 21 const verifier = new CertificateChainVerifier(opts); 22 return verifier.verify(); 23} 24exports.verifyCertificateChain = verifyCertificateChain; 25class CertificateChainVerifier { 26 constructor(opts) { 27 this.untrustedCert = opts.untrustedCert; 28 this.trustedCerts = opts.trustedCerts; 29 this.localCerts = dedupeCertificates([ 30 ...opts.trustedCerts, 31 opts.untrustedCert, 32 ]); 33 this.validAt = opts.validAt || new Date(); 34 } 35 verify() { 36 // Construct certificate path from leaf to root 37 const certificatePath = this.sort(); 38 // Perform validation checks on each certificate in the path 39 this.checkPath(certificatePath); 40 // Return verified certificate path 41 return certificatePath; 42 } 43 sort() { 44 const leafCert = this.untrustedCert; 45 // Construct all possible paths from the leaf 46 let paths = this.buildPaths(leafCert); 47 // Filter for paths which contain a trusted certificate 48 paths = paths.filter((path) => path.some((cert) => this.trustedCerts.includes(cert))); 49 if (paths.length === 0) { 50 throw new error_1.VerificationError('No trusted certificate path found'); 51 } 52 // Find the shortest of possible paths 53 const path = paths.reduce((prev, curr) => prev.length < curr.length ? prev : curr); 54 // Construct chain from shortest path 55 // Removes the last certificate in the path, which will be a second copy 56 // of the root certificate given that the root is self-signed. 57 return [leafCert, ...path].slice(0, -1); 58 } 59 // Recursively build all possible paths from the leaf to the root 60 buildPaths(certificate) { 61 const paths = []; 62 const issuers = this.findIssuer(certificate); 63 if (issuers.length === 0) { 64 throw new error_1.VerificationError('No valid certificate path found'); 65 } 66 for (let i = 0; i < issuers.length; i++) { 67 const issuer = issuers[i]; 68 // Base case - issuer is self 69 if (issuer.equals(certificate)) { 70 paths.push([certificate]); 71 continue; 72 } 73 // Recursively build path for the issuer 74 const subPaths = this.buildPaths(issuer); 75 // Construct paths by appending the issuer to each subpath 76 for (let j = 0; j < subPaths.length; j++) { 77 paths.push([issuer, ...subPaths[j]]); 78 } 79 } 80 return paths; 81 } 82 // Return all possible issuers for the given certificate 83 findIssuer(certificate) { 84 let issuers = []; 85 let keyIdentifier; 86 // Exit early if the certificate is self-signed 87 if (certificate.subject.equals(certificate.issuer)) { 88 if (certificate.verify()) { 89 return [certificate]; 90 } 91 } 92 // If the certificate has an authority key identifier, use that 93 // to find the issuer 94 if (certificate.extAuthorityKeyID) { 95 keyIdentifier = certificate.extAuthorityKeyID.keyIdentifier; 96 // TODO: Add support for authorityCertIssuer/authorityCertSerialNumber 97 // though Fulcio doesn't appear to use these 98 } 99 // Find possible issuers by comparing the authorityKeyID/subjectKeyID 100 // or issuer/subject. Potential issuers are added to the result array. 101 this.localCerts.forEach((possibleIssuer) => { 102 if (keyIdentifier) { 103 if (possibleIssuer.extSubjectKeyID) { 104 if (possibleIssuer.extSubjectKeyID.keyIdentifier.equals(keyIdentifier)) { 105 issuers.push(possibleIssuer); 106 } 107 return; 108 } 109 } 110 // Fallback to comparing certificate issuer and subject if 111 // subjectKey/authorityKey extensions are not present 112 if (possibleIssuer.subject.equals(certificate.issuer)) { 113 issuers.push(possibleIssuer); 114 } 115 }); 116 // Remove any issuers which fail to verify the certificate 117 issuers = issuers.filter((issuer) => { 118 try { 119 return certificate.verify(issuer); 120 } 121 catch (ex) { 122 return false; 123 } 124 }); 125 return issuers; 126 } 127 checkPath(path) { 128 if (path.length < 1) { 129 throw new error_1.VerificationError('Certificate chain must contain at least one certificate'); 130 } 131 // Check that all certificates are valid at the check date 132 const validForDate = path.every((cert) => cert.validForDate(this.validAt)); 133 if (!validForDate) { 134 throw new error_1.VerificationError('Certificate is not valid or expired at the specified date'); 135 } 136 // Ensure that all certificates beyond the leaf are CAs 137 const validCAs = path.slice(1).every((cert) => cert.isCA); 138 if (!validCAs) { 139 throw new error_1.VerificationError('Intermediate certificate is not a CA'); 140 } 141 // Certificate's issuer must match the subject of the next certificate 142 // in the chain 143 for (let i = path.length - 2; i >= 0; i--) { 144 if (!path[i].issuer.equals(path[i + 1].subject)) { 145 throw new error_1.VerificationError('Incorrect certificate name chaining'); 146 } 147 } 148 // Check pathlength constraints 149 for (let i = 0; i < path.length; i++) { 150 const cert = path[i]; 151 // If the certificate is a CA, check the path length 152 if (cert.extBasicConstraints?.isCA) { 153 const pathLength = cert.extBasicConstraints.pathLenConstraint; 154 // The path length, if set, indicates how many intermediate 155 // certificates (NOT including the leaf) are allowed to follow. The 156 // pathLength constraint of any intermediate CA certificate MUST be 157 // greater than or equal to it's own depth in the chain (with an 158 // adjustment for the leaf certificate) 159 if (pathLength !== undefined && pathLength < i - 1) { 160 throw new error_1.VerificationError('Path length constraint exceeded'); 161 } 162 } 163 } 164 } 165} 166// Remove duplicate certificates from the array 167function dedupeCertificates(certs) { 168 for (let i = 0; i < certs.length; i++) { 169 for (let j = i + 1; j < certs.length; j++) { 170 if (certs[i].equals(certs[j])) { 171 certs.splice(j, 1); 172 j--; 173 } 174 } 175 } 176 return certs; 177} 178