• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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