fix(desktop): trust the system CA store in main requests (#3281)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-08 17:26:12 +02:00
1 parent 7937d5f802
commit 965bb5f634
8 files changed
+174 -5

No files matched your search

+12 -5
View File
@@ -15,6 +15,7 @@ import {
resolveDesktopSessionProxy,
sendThroughDesktopSession,
} from '@electron/main/DesktopSessionHTTP';
import {resolveDesktopTrustedCertificates} from '@electron/main/DesktopTrustedCertificates';
import {normalizeHTTPNetworkOrigin} from '@fluxer/instance_bootstrap/src/NetworkOrigin';
const logger = createChildLogger('DesktopOutboundHTTP');
@@ -135,6 +136,7 @@ interface DesktopOutboundHTTPOptions {
readonly resolveHostAddresses?: DesktopHostAddressResolver;
readonly resolveProxy?: DesktopProxyResolver;
readonly sendThroughSession?: DesktopSessionHTTPSender;
readonly trustedCertificates?: ReadonlyArray<string>;
}
interface DesktopOutboundGETRequest {
@@ -725,15 +727,12 @@ interface DesktopProxiedSend {
export class DesktopOutboundHTTP {
private readonly httpAgent = new http.Agent({keepAlive: true, maxSockets: DESKTOP_OUTBOUND_HTTP_MAX_SOCKETS});
private readonly httpsAgent = new https.Agent({keepAlive: true, maxSockets: DESKTOP_OUTBOUND_HTTP_MAX_SOCKETS});
private readonly httpsAgent: https.Agent;
private readonly originRequestHttpAgent = new http.Agent({
keepAlive: true,
maxSockets: DESKTOP_OUTBOUND_HTTP_MAX_IN_FLIGHT_PER_SERVICE,
});
private readonly originRequestHttpsAgent = new https.Agent({
keepAlive: true,
maxSockets: DESKTOP_OUTBOUND_HTTP_MAX_IN_FLIGHT_PER_SERVICE,
});
private readonly originRequestHttpsAgent: https.Agent;
private readonly bindings = new Map<string, DesktopOriginAddressBinding>();
private readonly pendingBindings = new Map<string, DesktopPendingOriginBinding>();
private readonly registeredRequirements = new Map<string, DesktopAddressRequirement>();
@@ -750,6 +749,14 @@ export class DesktopOutboundHTTP {
this.resolveHostAddresses = options.resolveHostAddresses ?? lookupAllAddresses;
this.resolveProxy = options.resolveProxy ?? resolveDesktopSessionProxy;
this.sendThroughSession = options.sendThroughSession ?? sendThroughDesktopSession;
const certificates = options.trustedCertificates ?? resolveDesktopTrustedCertificates();
const trust = certificates.length > 0 ? {ca: [...certificates]} : {};
this.httpsAgent = new https.Agent({keepAlive: true, maxSockets: DESKTOP_OUTBOUND_HTTP_MAX_SOCKETS, ...trust});
this.originRequestHttpsAgent = new https.Agent({
keepAlive: true,
maxSockets: DESKTOP_OUTBOUND_HTTP_MAX_IN_FLIGHT_PER_SERVICE,
...trust,
});
}
public async get(request: DesktopOutboundGETRequest): Promise<DesktopOutboundHTTPMessage> {
@@ -0,0 +1,94 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import assert from 'node:assert/strict';
import fs from 'node:fs';
import https from 'node:https';
import path from 'node:path';
import {describe, test} from 'node:test';
import tls from 'node:tls';
import {fileURLToPath} from 'node:url';
import {installElectronStub} from './LocalAppTestSupport.test.mjs';
installElectronStub();
const {resolveDesktopTrustedCertificates} = await import('./DesktopTrustedCertificates.ts');
const {DesktopOriginTrust, DesktopOutboundHTTP} = await import('./DesktopOutboundHTTP.ts');
const FIXTURES = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures', 'desktop-tls');
const read = (name) => fs.readFileSync(path.join(FIXTURES, name), 'utf8');
const TRUSTED_CA = read('trusted-ca.pem');
const UNTRUSTED_CA = read('untrusted-ca.pem');
async function startTLSServer() {
const server = https.createServer({cert: read('leaf.pem'), key: read('leaf.key')}, (_request, response) => {
response.writeHead(200, {'content-type': 'text/plain'});
response.end('ok');
});
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
return {port: server.address().port, server};
}
async function fetchThrough(certificates, port) {
const outboundHTTP = new DesktopOutboundHTTP({
resolveHostAddresses: async () => ['127.0.0.1'],
trustedCertificates: certificates,
});
try {
await outboundHTTP.registerAnchoredOrigins({anchorOrigin: `https://ca.example:${port}`, origins: []});
const response = await outboundHTTP.request({
body: null,
expectedOrigin: `https://ca.example:${port}`,
headers: null,
method: 'GET',
originTrust: DesktopOriginTrust.REGISTERED,
serviceName: 'test',
signal: null,
timeoutMs: 3000,
url: `https://ca.example:${port}/`,
});
response.message.resume();
return response.status;
} finally {
outboundHTTP.cleanup();
}
}
describe('desktop trusted certificates', () => {
test('merges the bundled roots with the system store without duplicates', () => {
const merged = resolveDesktopTrustedCertificates((store) => (store === 'default' ? ['a', 'b'] : ['b', 'c']));
assert.deepEqual(merged, ['a', 'b', 'c']);
});
test('keeps the bundled roots when the system store cannot be read', () => {
const merged = resolveDesktopTrustedCertificates((store) => {
if (store === 'system') throw new Error('no system store');
return ['a'];
});
assert.deepEqual(merged, ['a']);
});
test('reaches an instance whose CA is only trusted by the system store', async () => {
const {port, server} = await startTLSServer();
try {
const certificates = resolveDesktopTrustedCertificates((store) =>
store === 'system' ? [TRUSTED_CA] : tls.getCACertificates('default'),
);
assert.equal(await fetchThrough(certificates, port), 200);
} finally {
server.close();
}
});
test('still refuses an instance signed by a CA nobody trusts', async () => {
const {port, server} = await startTLSServer();
try {
const certificates = resolveDesktopTrustedCertificates((store) =>
store === 'system' ? [UNTRUSTED_CA] : tls.getCACertificates('default'),
);
await assert.rejects(fetchThrough(certificates, port));
await assert.rejects(fetchThrough(tls.getCACertificates('default'), port));
} finally {
server.close();
}
});
});
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import tls from 'node:tls';
import {createChildLogger} from '@electron/common/Logger';
const logger = createChildLogger('DesktopTrustedCertificates');
export type DesktopCertificateStore = 'default' | 'system';
export type DesktopCertificateSource = (store: DesktopCertificateStore) => ReadonlyArray<string>;
function readStore(source: DesktopCertificateSource, store: DesktopCertificateStore): ReadonlyArray<string> {
try {
return source(store);
} catch (error) {
logger.warn('Could not read a certificate store, continuing without it', {store, error});
return [];
}
}
export function resolveDesktopTrustedCertificates(
source: DesktopCertificateSource = (store) => tls.getCACertificates(store),
): ReadonlyArray<string> {
const merged = new Set<string>();
for (const certificate of readStore(source, 'default')) merged.add(certificate);
for (const certificate of readStore(source, 'system')) merged.add(certificate);
return [...merged];
}
@@ -89,6 +89,7 @@ async function loadFileDownloads({registerOrigin = true} = {}) {
throw new Error('a direct route never reaches the session');
},
},
'@electron/main/DesktopTrustedCertificates': {resolveDesktopTrustedCertificates: () => []},
};
const sandbox = {
console,
@@ -0,0 +1,5 @@
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgjRw9RoOHDfdSs087
7vd6ZmYW1AJE4+6LGqJW4RJfvs+hRANCAAQvRk8uN0d9x8o01QFpd75RxviSJhaZ
nr4iEQRdEGEs+OKic0Cm9nHwBSainqm0kO8YKlT9cq2RI1jK9rqM9hIt
-----END PRIVATE KEY-----
@@ -0,0 +1,12 @@
-----BEGIN CERTIFICATE-----
MIIBzDCCAXGgAwIBAgIUFo38/sRgT50k5RgWhjrjAqkyUxwwCgYIKoZIzj0EAwIw
ITEfMB0GA1UEAwwWRmx1eGVyIFRlc3QgdHJ1c3RlZCBDQTAgFw0yNjEwMDgxMTAw
NDVaGA8yMTI2MDkxNDExMDA0NVowFTETMBEGA1UEAwwKY2EuZXhhbXBsZTBZMBMG
ByqGSM49AgEGCCqGSM49AwEHA0IABC9GTy43R33HyjTVAWl3vlHG+JImFpmeviIR
BF0QYSz44qJzQKb2cfAFJqKeqbSQ7xgqVP1yrZEjWMr2uoz2Ei2jgZAwgY0wGwYD
VR0RBBQwEoIKY2EuZXhhbXBsZYcEfwAAATAJBgNVHRMEAjAAMA4GA1UdDwEB/wQE
AwIHgDATBgNVHSUEDDAKBggrBgEFBQcDATAdBgNVHQ4EFgQUrTpUZ2NXWO1wgiIJ
5SXBE3OlT+MwHwYDVR0jBBgwFoAUekfvEi5SI/1J8dOCNgGBW+TJQGUwCgYIKoZI
zj0EAwIDSQAwRgIhALqUP9Hm71LmvrbIuP9IIrG2efo70QPNL0n3caoCmVO8AiEA
p7IbBwavgmqy6QhAkIgH5BPAQ4n37fP0UyeZVetYFII=
-----END CERTIFICATE-----
@@ -0,0 +1,11 @@
-----BEGIN CERTIFICATE-----
MIIBqTCCAU+gAwIBAgIUM6OwmZlGxvxbg2ReQWwRMOy5H0swCgYIKoZIzj0EAwIw
ITEfMB0GA1UEAwwWRmx1eGVyIFRlc3QgdHJ1c3RlZCBDQTAgFw0yNjEwMDgxMTAw
NDVaGA8yMTI2MDkxNDExMDA0NVowITEfMB0GA1UEAwwWRmx1eGVyIFRlc3QgdHJ1
c3RlZCBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABH+hGFbBocy+br8Lt+lE
9usokqgUdcJEEE8F1DSwzKqtTXYqaNQ1IbnjnYOnMzMP3C8SH+Z+W7h4CCgbeCHo
3FCjYzBhMB0GA1UdDgQWBBR6R+8SLlIj/Unx04I2AYFb5MlAZTAfBgNVHSMEGDAW
gBR6R+8SLlIj/Unx04I2AYFb5MlAZTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB
/wQEAwIBBjAKBggqhkjOPQQDAgNIADBFAiEA9ZiceuAgmWNbt1iGI8pIEKJnEBOK
B2EbyL9bNslsOwcCICmZwV2NHSnLTVtNiPqQDkRskugRL5ywBhddeqApBdk6
-----END CERTIFICATE-----
@@ -0,0 +1,12 @@
-----BEGIN CERTIFICATE-----
MIIBrTCCAVOgAwIBAgIUXHirlkuWFHOEJ+x5lRwAyIeQH0AwCgYIKoZIzj0EAwIw
IzEhMB8GA1UEAwwYRmx1eGVyIFRlc3QgdW50cnVzdGVkIENBMCAXDTI2MTAwODEx
MDA0NVoYDzIxMjYwOTE0MTEwMDQ1WjAjMSEwHwYDVQQDDBhGbHV4ZXIgVGVzdCB1
bnRydXN0ZWQgQ0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASYetrmC6ot/0o0
jxSFQRVFuZBzfdGYnOZ41+N2Jao9/6CoxuD+pqjOJ+NAjLP5QSMBuoaYaDKRzq+r
jDlVf+JJo2MwYTAdBgNVHQ4EFgQUi/e+X9VSTPlOrhbO2BAqjCl28OAwHwYDVR0j
BBgwFoAUi/e+X9VSTPlOrhbO2BAqjCl28OAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV
HQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwIDSAAwRQIhAN9+gjPnixgQHJqspIqJG2x7
jPmFxBGV9fQiEUM14NzAAiAt4pdRQ+sHc1VHsfEeUUHS2W0+UKoc/XHTjDZtppEH
Xw==
-----END CERTIFICATE-----