@@ -31,50 +31,39 @@ 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 ) ;
@@ -89,23 +78,97 @@ export function removeCertificatesWithoutIssuer(ca: string[]): {
89
78
}
90
79
return { pem, parsed } ;
91
80
} ) ;
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 (
81
+ }
82
+
83
+ function doesCertificateHasMatchingIssuer (
84
+ { parsed } : ParsedX509Cert ,
85
+ certs : ParsedX509Cert [ ]
86
+ ) {
87
+ return (
88
+ ! parsed ||
89
+ parsed . checkIssued ( parsed ) ||
90
+ certs . some ( ( { parsed : issuer } ) => {
91
+ return issuer && parsed . checkIssued ( issuer ) ;
92
+ } )
93
+ ) ;
94
+ }
95
+
96
+ const withRemovedMissingIssuerCache = new WeakMap <
97
+ ParsedX509Cert [ ] ,
98
+ {
99
+ ca : ParsedX509Cert [ ] ;
100
+ messages : string [ ] ;
101
+ }
102
+ > ( ) ;
103
+
104
+ // TODO(COMPASS-8253): Remove this in favor of OpenSSL's X509_V_FLAG_PARTIAL_CHAIN
105
+ // See linked tickets for details on why we need this (tl;dr: the system certificate
106
+ // store may contain intermediate certficiates without the corresponding trusted root,
107
+ // and OpenSSL does not seem to accept that)
108
+ export function removeCertificatesWithoutIssuer (
109
+ ca : ParsedX509Cert [ ] ,
110
+ messages : string [ ]
111
+ ) : ParsedX509Cert [ ] {
112
+ const result :
113
+ | {
114
+ ca : ParsedX509Cert [ ] ;
115
+ messages : string [ ] ;
116
+ }
117
+ | undefined = withRemovedMissingIssuerCache . get ( ca ) ;
118
+
119
+ if ( result ) {
120
+ messages . push ( ...result . messages ) ;
121
+ return result . ca ;
122
+ }
123
+
124
+ const _messages : string [ ] = [ ] ;
125
+ const filteredCAlist = ca . filter ( ( cert ) => {
126
+ const keep = doesCertificateHasMatchingIssuer ( cert , ca ) ;
127
+ if ( ! keep && cert . parsed ) {
128
+ const { parsed } = cert ;
129
+ _messages . push (
101
130
`Removing certificate for '${ parsed . subject } ' because issuer '${ parsed . issuer } ' could not be found (serial no '${ parsed . serialNumber } ')`
102
131
) ;
103
132
}
104
133
return keep ;
105
134
} ) ;
106
- result = { ca : caWithParsedCerts . map ( ( { pem } ) => pem ) , messages } ;
107
- pemWithParsedCache . set ( ca , result ) ;
108
- return result ;
135
+ withRemovedMissingIssuerCache . set ( ca , {
136
+ ca : filteredCAlist ,
137
+ messages : _messages ,
138
+ } ) ;
139
+ messages . push ( ..._messages ) ;
140
+ return filteredCAlist ;
141
+ }
142
+
143
+ function hasExpired ( cert : X509Certificate ) {
144
+ return new Date ( cert . validTo ) . getTime ( ) < Date . now ( ) ;
145
+ }
146
+
147
+ /**
148
+ * Pushes all expired certs to the bottom of the list (wihout moving other
149
+ * certs) to make sure that they are not accidentally picked up by (maybe)
150
+ * openssl in some corner-cases
151
+ *
152
+ * @see {@link https://jira.mongodb.org/browse/COMPASS-8322 }
153
+ */
154
+ export function sortByExpirationDate ( ca : ParsedX509Cert [ ] ) {
155
+ return ca . slice ( ) . sort ( ( a , b ) => {
156
+ if ( ! a . parsed || ! b . parsed ) {
157
+ return 0 ;
158
+ }
159
+ const aExpired = hasExpired ( a . parsed ) ;
160
+ const bExpired = hasExpired ( b . parsed ) ;
161
+ if ( aExpired && bExpired ) {
162
+ return 0 ;
163
+ }
164
+ if ( aExpired ) {
165
+ return 1 ;
166
+ }
167
+ if ( bExpired ) {
168
+ return - 1 ;
169
+ }
170
+ return 0 ;
171
+ } ) ;
109
172
}
110
173
111
174
// Thin wrapper around system-ca, which merges:
@@ -135,11 +198,14 @@ export async function systemCA(
135
198
136
199
let systemCertsError : Error | undefined ;
137
200
let asyncFallbackError : Error | undefined ;
138
- let systemCerts : string [ ] = [ ] ;
139
- let messages : string [ ] = [ ] ;
201
+ let systemCerts : ParsedX509Cert [ ] = [ ] ;
202
+
203
+ const messages : string [ ] = [ ] ;
140
204
141
205
try {
142
- ( { certs : systemCerts , asyncFallbackError } = await systemCertsCached ( ) ) ;
206
+ const systemCertsResult = await systemCertsCached ( ) ;
207
+ asyncFallbackError = systemCertsResult . asyncFallbackError ;
208
+ systemCerts = parseCACerts ( systemCertsResult . certs , messages ) ;
143
209
} catch ( err : any ) {
144
210
systemCertsError = err ;
145
211
}
@@ -150,14 +216,14 @@ export async function systemCA(
150
216
! ! process . env . DEVTOOLS_ALLOW_CERTIFICATES_WITHOUT_ISSUER
151
217
)
152
218
) {
153
- const reducedList = removeCertificatesWithoutIssuer ( systemCerts ) ;
154
- systemCerts = reducedList . ca ;
155
- messages = messages . concat ( reducedList . messages ) ;
219
+ systemCerts = removeCertificatesWithoutIssuer ( systemCerts , messages ) ;
156
220
}
157
221
158
222
return {
159
223
ca : mergeCA (
160
- systemCerts ,
224
+ sortByExpirationDate ( systemCerts ) . map ( ( cert ) => {
225
+ return cert . pem ;
226
+ } ) ,
161
227
rootCertificates ,
162
228
existingOptions . ca ,
163
229
await readTLSCAFilePromise
0 commit comments