certificate.js 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.CertificateChainVerifier = void 0;
  4. exports.verifyCertificateChain = verifyCertificateChain;
  5. const error_1 = require("../error");
  6. const trust_1 = require("../trust");
  7. function verifyCertificateChain(leaf, certificateAuthorities) {
  8. // Filter list of trusted CAs to those which are valid for the given
  9. // leaf certificate.
  10. const cas = (0, trust_1.filterCertAuthorities)(certificateAuthorities, {
  11. start: leaf.notBefore,
  12. end: leaf.notAfter,
  13. });
  14. /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
  15. let error;
  16. for (const ca of cas) {
  17. try {
  18. const verifier = new CertificateChainVerifier({
  19. trustedCerts: ca.certChain,
  20. untrustedCert: leaf,
  21. });
  22. return verifier.verify();
  23. }
  24. catch (err) {
  25. error = err;
  26. }
  27. }
  28. // If we failed to verify the certificate chain for all of the trusted
  29. // CAs, throw the last error we encountered.
  30. throw new error_1.VerificationError({
  31. code: 'CERTIFICATE_ERROR',
  32. message: 'Failed to verify certificate chain',
  33. cause: error,
  34. });
  35. }
  36. class 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. }
  193. exports.CertificateChainVerifier = CertificateChainVerifier;
  194. // Remove duplicate certificates from the array
  195. function 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. }