diff --git a/packages/devtools-proxy-support/src/agent.spec.ts b/packages/devtools-proxy-support/src/agent.spec.ts index e3255a1..ea15c34 100644 --- a/packages/devtools-proxy-support/src/agent.spec.ts +++ b/packages/devtools-proxy-support/src/agent.spec.ts @@ -11,7 +11,6 @@ import type { Server as TLSServer } from 'tls'; import { createServer as createTLSServer } from 'tls'; import { promises as fs } from 'fs'; import type { AddressInfo } from 'net'; -import { tlsSupportsAllowPartialTrustChainFlag } from './system-ca'; describe('createAgent', function () { let setup: HTTPServerProxyTestSetup; @@ -393,11 +392,7 @@ q/I2+0j6dAkOGcK/68z7qQXByeGri3n28a1Kn6o= }); it('can connect using partial trust chains in the system CA list', async function () { - if ( - process.platform !== 'linux' || - !tlsSupportsAllowPartialTrustChainFlag() - ) - return this.skip(); // only really mock-able on Linux + if (process.platform !== 'linux') return this.skip(); // only really mock-able on Linux resetSystemCACache({ env: { SSL_CERT_FILE: path.join(fixtures, 'ca.pem'), diff --git a/packages/devtools-proxy-support/src/system-ca.spec.ts b/packages/devtools-proxy-support/src/system-ca.spec.ts deleted file mode 100644 index 5ddc894..0000000 --- a/packages/devtools-proxy-support/src/system-ca.spec.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { expect } from 'chai'; -import type { ParsedX509Cert } from './system-ca'; -import { - parseCACerts, - removeCertificatesWithoutIssuer, - sortByExpirationDate, -} from './system-ca'; - -describe('system-ca helpers', function () { - describe('removeCertificatesWithoutIssuer', function () { - it('removes certificates that do not have an issuer', function () { - const certs = [ - `-----BEGIN CERTIFICATE----- -MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAwTzELMAkG -A1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2VhcmNoIEdyb3VwMRUw -EwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4WhcNMzUwNjA0MTEwNDM4WjBP -MQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3Jv -dXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC -ggIBAK3oJHP0FDfzm54rVygch77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj -/RQSa78f0uoxmyF+0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7i -S4+3mX6UA5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW -T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyHB5T0Y3Hs -LuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UCB5iPNgiV5+I3lg02 -dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUvKBds0pjBqAlkd25HN7rOrFle -aJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWnOlFuhjuefXKnEgV4We0+UXgVCwOPjdAv -BbI+e0ocS3MFEvzG6uBQE3xDk3SzynTnjh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymC -zLq9gwQbooMDQaHWBfEbwrbwqHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC -1CLQJ13hef4Y53CIrU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIB -BjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq -hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZLubhzEFnT -IZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ3BebYhtF8GaV0nxv -wuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KKNFtY2PwByVS5uCbMiogziUwt -hDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5ORAzI4JMPJ+GslWYHb4phowim57iaztX -OoJwTdwJx4nLCgdNbOhdjsnvzqvHu7UrTkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIu -vtd7u+Nxe5AW0wdeRlN8NwdCjNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1N -bdWhscdCb+ZAJzVcoyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4k -qKOJ2qxq4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA -mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57demyPxgcY -xn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= ------END CERTIFICATE-----`, - `-----BEGIN CERTIFICATE----- -MIIFYDCCBEigAwIBAgIQQAF3ITfU6UK47naqPGQKtzANBgkqhkiG9w0BAQsFADA/ -MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT -DkRTVCBSb290IENBIFgzMB4XDTIxMDEyMDE5MTQwM1oXDTI0MDkzMDE4MTQwM1ow -TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh -cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwggIiMA0GCSqGSIb3DQEB -AQUAA4ICDwAwggIKAoICAQCt6CRz9BQ385ueK1coHIe+3LffOJCMbjzmV6B493XC -ov71am72AE8o295ohmxEk7axY/0UEmu/H9LqMZshftEzPLpI9d1537O4/xLxIZpL -wYqGcWlKZmZsj348cL+tKSIG8+TA5oCu4kuPt5l+lAOf00eXfJlII1PoOK5PCm+D -LtFJV4yAdLbaL9A4jXsDcCEbdfIwPPqPrt3aY6vrFk/CjhFLfs8L6P+1dy70sntK -4EwSJQxwjQMpoOFTJOwT2e4ZvxCzSow/iaNhUd6shweU9GNx7C7ib1uYgeGJXDR5 -bHbvO5BieebbpJovJsXQEOEO3tkQjhb7t/eo98flAgeYjzYIlefiN5YNNnWe+w5y -sR2bvAP5SQXYgd0FtCrWQemsAXaVCg/Y39W9Eh81LygXbNKYwagJZHduRze6zqxZ -Xmidf3LWicUGQSk+WT7dJvUkyRGnWqNMQB9GoZm1pzpRboY7nn1ypxIFeFntPlF4 -FQsDj43QLwWyPntKHEtzBRL8xurgUBN8Q5N0s8p0544fAQjQMNRbcTa0B7rBMDBc -SLeCO5imfWCKoqMpgsy6vYMEG6KDA0Gh1gXxG8K28Kh8hjtGqEgqiNx2mna/H2ql -PRmP6zjzZN7IKw0KKP/32+IVQtQi0Cdd4Xn+GOdwiK1O5tmLOsbdJ1Fu/7xk9TND -TwIDAQABo4IBRjCCAUIwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw -SwYIKwYBBQUHAQEEPzA9MDsGCCsGAQUFBzAChi9odHRwOi8vYXBwcy5pZGVudHJ1 -c3QuY29tL3Jvb3RzL2RzdHJvb3RjYXgzLnA3YzAfBgNVHSMEGDAWgBTEp7Gkeyxx -+tvhS5B1/8QVYIWJEDBUBgNVHSAETTBLMAgGBmeBDAECATA/BgsrBgEEAYLfEwEB -ATAwMC4GCCsGAQUFBwIBFiJodHRwOi8vY3BzLnJvb3QteDEubGV0c2VuY3J5cHQu -b3JnMDwGA1UdHwQ1MDMwMaAvoC2GK2h0dHA6Ly9jcmwuaWRlbnRydXN0LmNvbS9E -U1RST09UQ0FYM0NSTC5jcmwwHQYDVR0OBBYEFHm0WeZ7tuXkAXOACIjIGlj26Ztu -MA0GCSqGSIb3DQEBCwUAA4IBAQAKcwBslm7/DlLQrt2M51oGrS+o44+/yQoDFVDC -5WxCu2+b9LRPwkSICHXM6webFGJueN7sJ7o5XPWioW5WlHAQU7G75K/QosMrAdSW -9MUgNTP52GE24HGNtLi1qoJFlcDyqSMo59ahy2cI2qBDLKobkx/J3vWraV0T9VuG -WCLKTVXkcGdtwlfFRjlBz4pYg1htmf5X6DYO8A4jqv2Il9DjXA6USbW1FzXSLr9O -he8Y4IWS6wY7bCkjCWDcRQJMEhg76fsO3txE+FiYruq9RUWhiF1myv4Q6W+CyBFC -Dfvp7OOGAN6dEOM4+qR9sdjoSYKEBpsr6GtPAQw4dy753ec5 ------END CERTIFICATE-----`, - ]; - const messages = []; - const filtered = removeCertificatesWithoutIssuer( - parseCACerts(certs, messages), - messages - ); - expect( - filtered.map((cert) => { - return cert.pem; - }) - ).to.deep.equal([certs[0]]); - expect(messages).to.deep.eq([ - `Removing certificate for 'C=US\nO=Internet Security Research Group\nCN=ISRG Root X1' because issuer 'O=Digital Signature Trust Co.\nCN=DST Root CA X3' could not be found (serial no '4001772137D4E942B8EE76AA3C640AB7')`, - ]); - }); - }); - - describe('sortByExpirationDate', function () { - it('sorts certs by expiration date in descending order (higher expiration date on top)', function () { - const mockCerts = [ - { - serialNumber: '01', - validTo: new Date(Date.now() + 10_000).toUTCString(), - }, - { - serialNumber: '02', - validTo: new Date(Date.now() - 60_000).toUTCString(), - }, - { - serialNumber: '03', - validTo: new Date(Date.now() - 50_000).toUTCString(), - }, - { - serialNumber: '04', - validTo: new Date(Date.now() + 30_000).toUTCString(), - }, - { - serialNumber: '05', - validTo: new Date(Date.now() + 20_000).toUTCString(), - }, - { - serialNumber: '06', - validTo: new Date(Date.now() + 20_000).toUTCString(), - }, - ]; - - const sorted = sortByExpirationDate( - mockCerts.map((parsed) => { - return { pem: '', parsed } as ParsedX509Cert; - }) - ); - - expect( - sorted.map((cert) => { - return cert.parsed?.serialNumber; - }) - ).to.deep.eq(['04', '05', '06', '01', '03', '02']); - }); - }); -}); diff --git a/packages/devtools-proxy-support/src/system-ca.ts b/packages/devtools-proxy-support/src/system-ca.ts index be97037..36326c9 100644 --- a/packages/devtools-proxy-support/src/system-ca.ts +++ b/packages/devtools-proxy-support/src/system-ca.ts @@ -2,7 +2,6 @@ import { systemCertsAsync } from 'system-ca'; import type { Options as SystemCAOptions } from 'system-ca'; import { promises as fs } from 'fs'; import { rootCertificates } from 'tls'; -import { X509Certificate } from 'crypto'; // A bit more generic than SecureContextOptions['ca'] because of Uint8Array -> Buffer + readonly type NodeJSCAOption = string | Uint8Array | readonly (string | Uint8Array)[]; @@ -51,131 +50,6 @@ export function mergeCA(...args: (NodeJSCAOption | undefined)[]): string { return [...ca].join('\n'); } -export type ParsedX509Cert = { pem: string; parsed: X509Certificate | null }; - -/** - * Safely parse provided certs, push any encountered errors to the provided - * messages array - */ -export function parseCACerts( - ca: NodeJSCAOption, - messages: string[] -): ParsedX509Cert[] { - ca = Array.isArray(ca) ? ca : [ca]; - return ca.map((cert) => { - const pem = certToString(cert); - let parsed: X509Certificate | null = null; - try { - parsed = new X509Certificate(pem); - } catch (err: unknown) { - // Most definitely should happen never or extremely rarely, in case it - // does, if this cert will affect the TLS connection verification, the - // connection will most definitely fail and we'll see it in the logs. For - // that reason we're just logging, but not throwing an error here - messages.push( - `Unable to parse certificate: ${ - err && typeof err === 'object' && 'message' in err - ? String(err.message) - : String(err) - }` - ); - } - return { pem, parsed }; - }); -} - -function certificateHasMatchingIssuer( - cert: X509Certificate, - certs: ParsedX509Cert[] -) { - return ( - cert.checkIssued(cert) || - certs.some(({ parsed: issuer }) => { - return issuer && cert.checkIssued(issuer); - }) - ); -} - -const withRemovedMissingIssuerCache = new WeakMap< - ParsedX509Cert[], - { - ca: ParsedX509Cert[]; - messages: string[]; - } ->(); - -// TODO(COMPASS-8253): Remove this in favor of OpenSSL's X509_V_FLAG_PARTIAL_CHAIN -// See linked tickets for details on why we need this (tl;dr: the system certificate -// store may contain intermediate certficiates without the corresponding trusted root, -// and OpenSSL does not seem to accept that) -export function removeCertificatesWithoutIssuer( - ca: ParsedX509Cert[], - messages: string[] -): ParsedX509Cert[] { - const result: - | { - ca: ParsedX509Cert[]; - messages: string[]; - } - | undefined = withRemovedMissingIssuerCache.get(ca); - - if (result) { - messages.push(...result.messages); - return result.ca; - } - - const _messages: string[] = []; - const filteredCAlist = ca.filter((cert) => { - // If cert was not parsed, we want to keep it in the list. The case should - // be generally very rare, but in case it happens and this cert will affect - // the TLS handshake, it will show up in the logs as the connection error - // anyway, so it's safe to keep it - const keep = !cert.parsed || certificateHasMatchingIssuer(cert.parsed, ca); - if (!keep && cert.parsed) { - const { parsed } = cert; - _messages.push( - `Removing certificate for '${parsed.subject}' because issuer '${parsed.issuer}' could not be found (serial no '${parsed.serialNumber}')` - ); - } - return keep; - }); - withRemovedMissingIssuerCache.set(ca, { - ca: filteredCAlist, - messages: _messages, - }); - messages.push(..._messages); - return filteredCAlist; -} - -/** - * Sorts cerificates by the Not After value. Items that are higher in the list - * get picked up first by the CA issuer finding logic - * - * @see {@link https://jira.mongodb.org/browse/COMPASS-8322} - */ -export function sortByExpirationDate(ca: ParsedX509Cert[]) { - return ca.slice().sort((a, b) => { - if (!a.parsed || !b.parsed) { - return 0; - } - return ( - new Date(b.parsed.validTo).getTime() - - new Date(a.parsed.validTo).getTime() - ); - }); -} - -const nodeVersion = process.versions.node.split('.').map(Number); - -export function tlsSupportsAllowPartialTrustChainFlag(): boolean { - // TODO: Remove this flag and all X.509 parsing here once all our products - // are at least on these Node.js versions - return ( - (nodeVersion[0] >= 22 && nodeVersion[1] >= 9) || // https://github.com/nodejs/node/commit/c2bf0134c - (nodeVersion[0] === 20 && nodeVersion[1] >= 18) - ); // https://github.com/nodejs/node/commit/1b3420274 -} - // Thin wrapper around system-ca, which merges: // - Explicit CA options passed as options // - The Node.js TLS root store @@ -184,8 +58,7 @@ export async function systemCA( existingOptions: { ca?: NodeJSCAOption; tlsCAFile?: string | null | undefined; - } = {}, - allowCertificatesWithoutIssuer?: boolean // defaults to false + } = {} ): Promise<{ ca: string; systemCACount: number; @@ -203,43 +76,21 @@ export async function systemCA( let systemCertsError: Error | undefined; let asyncFallbackError: Error | undefined; - let systemCerts: ParsedX509Cert[] = []; + let systemCerts: string[] = []; const messages: string[] = []; - const _tlsSupportsAllowPartialTrustChainFlag = - tlsSupportsAllowPartialTrustChainFlag(); try { const systemCertsResult = await systemCertsCached(); asyncFallbackError = systemCertsResult.asyncFallbackError; - if (_tlsSupportsAllowPartialTrustChainFlag) { - systemCerts = systemCertsResult.certs.map((pem) => ({ - pem, - parsed: null, - })); - } else { - systemCerts = parseCACerts(systemCertsResult.certs, messages); - } + systemCerts = systemCertsResult.certs; } catch (err: any) { systemCertsError = err; } - if ( - !( - allowCertificatesWithoutIssuer ?? - !!process.env.DEVTOOLS_ALLOW_CERTIFICATES_WITHOUT_ISSUER - ) && - !_tlsSupportsAllowPartialTrustChainFlag - ) { - systemCerts = removeCertificatesWithoutIssuer(systemCerts, messages); - } - return { ca: mergeCA( - (_tlsSupportsAllowPartialTrustChainFlag - ? systemCerts - : sortByExpirationDate(systemCerts) - ).map(({ pem }) => pem), + systemCerts, rootCertificates, existingOptions.ca, await readTLSCAFilePromise