@@ -31,54 +31,47 @@ function systemCertsCached(systemCAOpts: SystemCAOptions = {}): Promise<{
31
31
return systemCertsCachePromise ;
32
32
}
33
33
34
+ function certToString ( cert : string | Uint8Array ) {
35
+ return typeof cert === 'string'
36
+ ? cert
37
+ : Buffer . from ( cert . buffer , cert . byteOffset , cert . byteLength ) . toString (
38
+ 'utf8'
39
+ ) ;
40
+ }
41
+
34
42
export function mergeCA ( ...args : ( NodeJSCAOption | undefined ) [ ] ) : string {
35
43
const ca = new Set < string > ( ) ;
36
44
for ( const item of args ) {
37
45
if ( ! item ) continue ;
38
- const caList : readonly ( string | Uint8Array ) [ ] = Array . isArray ( item )
39
- ? item
40
- : [ item ] ;
46
+ const caList = Array . isArray ( item ) ? item : [ item ] ;
41
47
for ( const cert of caList ) {
42
- const asString =
43
- typeof cert === 'string'
44
- ? cert
45
- : Buffer . from ( cert . buffer , cert . byteOffset , cert . byteLength ) . toString (
46
- 'utf8'
47
- ) ;
48
- ca . add ( asString ) ;
48
+ ca . add ( certToString ( cert ) ) ;
49
49
}
50
50
}
51
51
return [ ...ca ] . join ( '\n' ) ;
52
52
}
53
53
54
- const pemWithParsedCache = new WeakMap <
55
- string [ ] ,
56
- {
57
- ca : string [ ] ;
58
- messages : string [ ] ;
59
- }
60
- > ( ) ;
61
- // TODO(COMPASS-8253): Remove this in favor of OpenSSL's X509_V_FLAG_PARTIAL_CHAIN
62
- // See linked tickets for details on why we need this (tl;dr: the system certificate
63
- // store may contain intermediate certficiates without the corresponding trusted root,
64
- // and OpenSSL does not seem to accept that)
65
- export function removeCertificatesWithoutIssuer ( ca : string [ ] ) : {
66
- ca : string [ ] ;
67
- messages : string [ ] ;
68
- } {
69
- let result :
70
- | {
71
- ca : string [ ] ;
72
- messages : string [ ] ;
73
- }
74
- | undefined = pemWithParsedCache . get ( ca ) ;
54
+ export type ParsedX509Cert = { pem : string ; parsed : X509Certificate | null } ;
75
55
76
- const messages : string [ ] = [ ] ;
77
- let caWithParsedCerts = ca . map ( ( pem ) => {
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 ) ;
78
67
let parsed : X509Certificate | null = null ;
79
68
try {
80
69
parsed = new X509Certificate ( pem ) ;
81
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
82
75
messages . push (
83
76
`Unable to parse certificate: ${
84
77
err && typeof err === 'object' && 'message' in err
@@ -89,23 +82,87 @@ export function removeCertificatesWithoutIssuer(ca: string[]): {
89
82
}
90
83
return { pem, parsed } ;
91
84
} ) ;
92
- caWithParsedCerts = caWithParsedCerts . filter ( ( { parsed } ) => {
93
- const keep =
94
- ! parsed ||
95
- parsed . checkIssued ( parsed ) ||
96
- caWithParsedCerts . find (
97
- ( { parsed : issuer } ) => issuer && parsed . checkIssued ( issuer )
98
- ) ;
99
- if ( ! keep ) {
100
- messages . push (
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 (
101
137
`Removing certificate for '${ parsed . subject } ' because issuer '${ parsed . issuer } ' could not be found (serial no '${ parsed . serialNumber } ')`
102
138
) ;
103
139
}
104
140
return keep ;
105
141
} ) ;
106
- result = { ca : caWithParsedCerts . map ( ( { pem } ) => pem ) , messages } ;
107
- pemWithParsedCache . set ( ca , result ) ;
108
- return result ;
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
+ } ) ;
109
166
}
110
167
111
168
// Thin wrapper around system-ca, which merges:
@@ -135,11 +192,14 @@ export async function systemCA(
135
192
136
193
let systemCertsError : Error | undefined ;
137
194
let asyncFallbackError : Error | undefined ;
138
- let systemCerts : string [ ] = [ ] ;
139
- let messages : string [ ] = [ ] ;
195
+ let systemCerts : ParsedX509Cert [ ] = [ ] ;
196
+
197
+ const messages : string [ ] = [ ] ;
140
198
141
199
try {
142
- ( { certs : systemCerts , asyncFallbackError } = await systemCertsCached ( ) ) ;
200
+ const systemCertsResult = await systemCertsCached ( ) ;
201
+ asyncFallbackError = systemCertsResult . asyncFallbackError ;
202
+ systemCerts = parseCACerts ( systemCertsResult . certs , messages ) ;
143
203
} catch ( err : any ) {
144
204
systemCertsError = err ;
145
205
}
@@ -150,14 +210,14 @@ export async function systemCA(
150
210
! ! process . env . DEVTOOLS_ALLOW_CERTIFICATES_WITHOUT_ISSUER
151
211
)
152
212
) {
153
- const reducedList = removeCertificatesWithoutIssuer ( systemCerts ) ;
154
- systemCerts = reducedList . ca ;
155
- messages = messages . concat ( reducedList . messages ) ;
213
+ systemCerts = removeCertificatesWithoutIssuer ( systemCerts , messages ) ;
156
214
}
157
215
158
216
return {
159
217
ca : mergeCA (
160
- systemCerts ,
218
+ sortByExpirationDate ( systemCerts ) . map ( ( cert ) => {
219
+ return cert . pem ;
220
+ } ) ,
161
221
rootCertificates ,
162
222
existingOptions . ca ,
163
223
await readTLSCAFilePromise
0 commit comments