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