@@ -2,7 +2,6 @@ import { systemCertsAsync } from 'system-ca';
22import type { Options as SystemCAOptions } from 'system-ca' ;
33import { promises as fs } from 'fs' ;
44import { rootCertificates } from 'tls' ;
5- import { X509Certificate } from 'crypto' ;
65
76// A bit more generic than SecureContextOptions['ca'] because of Uint8Array -> Buffer + readonly
87type NodeJSCAOption = string | Uint8Array | readonly ( string | Uint8Array ) [ ] ;
@@ -51,131 +50,6 @@ export function mergeCA(...args: (NodeJSCAOption | undefined)[]): string {
5150 return [ ...ca ] . join ( '\n' ) ;
5251}
5352
54- export type ParsedX509Cert = { pem : string ; parsed : X509Certificate | null } ;
55-
56- /**
57- * Safely parse provided certs, push any encountered errors to the provided
58- * messages array
59- */
60- export function parseCACerts (
61- ca : NodeJSCAOption ,
62- messages : string [ ]
63- ) : ParsedX509Cert [ ] {
64- ca = Array . isArray ( ca ) ? ca : [ ca ] ;
65- return ca . map ( ( cert ) => {
66- const pem = certToString ( cert ) ;
67- let parsed : X509Certificate | null = null ;
68- try {
69- parsed = new X509Certificate ( pem ) ;
70- } catch ( err : unknown ) {
71- // Most definitely should happen never or extremely rarely, in case it
72- // does, if this cert will affect the TLS connection verification, the
73- // connection will most definitely fail and we'll see it in the logs. For
74- // that reason we're just logging, but not throwing an error here
75- messages . push (
76- `Unable to parse certificate: ${
77- err && typeof err === 'object' && 'message' in err
78- ? String ( err . message )
79- : String ( err )
80- } `
81- ) ;
82- }
83- return { pem, parsed } ;
84- } ) ;
85- }
86-
87- function certificateHasMatchingIssuer (
88- cert : X509Certificate ,
89- certs : ParsedX509Cert [ ]
90- ) {
91- return (
92- cert . checkIssued ( cert ) ||
93- certs . some ( ( { parsed : issuer } ) => {
94- return issuer && cert . checkIssued ( issuer ) ;
95- } )
96- ) ;
97- }
98-
99- const withRemovedMissingIssuerCache = new WeakMap <
100- ParsedX509Cert [ ] ,
101- {
102- ca : ParsedX509Cert [ ] ;
103- messages : string [ ] ;
104- }
105- > ( ) ;
106-
107- // TODO(COMPASS-8253): Remove this in favor of OpenSSL's X509_V_FLAG_PARTIAL_CHAIN
108- // See linked tickets for details on why we need this (tl;dr: the system certificate
109- // store may contain intermediate certficiates without the corresponding trusted root,
110- // and OpenSSL does not seem to accept that)
111- export function removeCertificatesWithoutIssuer (
112- ca : ParsedX509Cert [ ] ,
113- messages : string [ ]
114- ) : ParsedX509Cert [ ] {
115- const result :
116- | {
117- ca : ParsedX509Cert [ ] ;
118- messages : string [ ] ;
119- }
120- | undefined = withRemovedMissingIssuerCache . get ( ca ) ;
121-
122- if ( result ) {
123- messages . push ( ...result . messages ) ;
124- return result . ca ;
125- }
126-
127- const _messages : string [ ] = [ ] ;
128- const filteredCAlist = ca . filter ( ( cert ) => {
129- // If cert was not parsed, we want to keep it in the list. The case should
130- // be generally very rare, but in case it happens and this cert will affect
131- // the TLS handshake, it will show up in the logs as the connection error
132- // anyway, so it's safe to keep it
133- const keep = ! cert . parsed || certificateHasMatchingIssuer ( cert . parsed , ca ) ;
134- if ( ! keep && cert . parsed ) {
135- const { parsed } = cert ;
136- _messages . push (
137- `Removing certificate for '${ parsed . subject } ' because issuer '${ parsed . issuer } ' could not be found (serial no '${ parsed . serialNumber } ')`
138- ) ;
139- }
140- return keep ;
141- } ) ;
142- withRemovedMissingIssuerCache . set ( ca , {
143- ca : filteredCAlist ,
144- messages : _messages ,
145- } ) ;
146- messages . push ( ..._messages ) ;
147- return filteredCAlist ;
148- }
149-
150- /**
151- * Sorts cerificates by the Not After value. Items that are higher in the list
152- * get picked up first by the CA issuer finding logic
153- *
154- * @see {@link https://jira.mongodb.org/browse/COMPASS-8322 }
155- */
156- export function sortByExpirationDate ( ca : ParsedX509Cert [ ] ) {
157- return ca . slice ( ) . sort ( ( a , b ) => {
158- if ( ! a . parsed || ! b . parsed ) {
159- return 0 ;
160- }
161- return (
162- new Date ( b . parsed . validTo ) . getTime ( ) -
163- new Date ( a . parsed . validTo ) . getTime ( )
164- ) ;
165- } ) ;
166- }
167-
168- const nodeVersion = process . versions . node . split ( '.' ) . map ( Number ) ;
169-
170- export function tlsSupportsAllowPartialTrustChainFlag ( ) : boolean {
171- // TODO: Remove this flag and all X.509 parsing here once all our products
172- // are at least on these Node.js versions
173- return (
174- ( nodeVersion [ 0 ] >= 22 && nodeVersion [ 1 ] >= 9 ) || // https://github.com/nodejs/node/commit/c2bf0134c
175- ( nodeVersion [ 0 ] === 20 && nodeVersion [ 1 ] >= 18 )
176- ) ; // https://github.com/nodejs/node/commit/1b3420274
177- }
178-
17953// Thin wrapper around system-ca, which merges:
18054// - Explicit CA options passed as options
18155// - The Node.js TLS root store
@@ -184,8 +58,7 @@ export async function systemCA(
18458 existingOptions : {
18559 ca ?: NodeJSCAOption ;
18660 tlsCAFile ?: string | null | undefined ;
187- } = { } ,
188- allowCertificatesWithoutIssuer ?: boolean // defaults to false
61+ } = { }
18962) : Promise < {
19063 ca : string ;
19164 systemCACount : number ;
@@ -203,43 +76,21 @@ export async function systemCA(
20376
20477 let systemCertsError : Error | undefined ;
20578 let asyncFallbackError : Error | undefined ;
206- let systemCerts : ParsedX509Cert [ ] = [ ] ;
79+ let systemCerts : string [ ] = [ ] ;
20780
20881 const messages : string [ ] = [ ] ;
20982
210- const _tlsSupportsAllowPartialTrustChainFlag =
211- tlsSupportsAllowPartialTrustChainFlag ( ) ;
21283 try {
21384 const systemCertsResult = await systemCertsCached ( ) ;
21485 asyncFallbackError = systemCertsResult . asyncFallbackError ;
215- if ( _tlsSupportsAllowPartialTrustChainFlag ) {
216- systemCerts = systemCertsResult . certs . map ( ( pem ) => ( {
217- pem,
218- parsed : null ,
219- } ) ) ;
220- } else {
221- systemCerts = parseCACerts ( systemCertsResult . certs , messages ) ;
222- }
86+ systemCerts = systemCertsResult . certs ;
22387 } catch ( err : any ) {
22488 systemCertsError = err ;
22589 }
22690
227- if (
228- ! (
229- allowCertificatesWithoutIssuer ??
230- ! ! process . env . DEVTOOLS_ALLOW_CERTIFICATES_WITHOUT_ISSUER
231- ) &&
232- ! _tlsSupportsAllowPartialTrustChainFlag
233- ) {
234- systemCerts = removeCertificatesWithoutIssuer ( systemCerts , messages ) ;
235- }
236-
23791 return {
23892 ca : mergeCA (
239- ( _tlsSupportsAllowPartialTrustChainFlag
240- ? systemCerts
241- : sortByExpirationDate ( systemCerts )
242- ) . map ( ( { pem } ) => pem ) ,
93+ systemCerts ,
24394 rootCertificates ,
24495 existingOptions . ca ,
24596 await readTLSCAFilePromise
0 commit comments