certificate.js 8.4 KB

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