mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 21:45:31 +09:00
fix(desktop): trust the system CA store in main requests (#3281)
This commit is contained in:
1 parent
7937d5f802
commit
965bb5f634
8 files changed
+174
-5
No files matched your search
@@ -15,6 +15,7 @@ import {
|
|||||||
resolveDesktopSessionProxy,
|
resolveDesktopSessionProxy,
|
||||||
sendThroughDesktopSession,
|
sendThroughDesktopSession,
|
||||||
} from '@electron/main/DesktopSessionHTTP';
|
} from '@electron/main/DesktopSessionHTTP';
|
||||||
|
import {resolveDesktopTrustedCertificates} from '@electron/main/DesktopTrustedCertificates';
|
||||||
import {normalizeHTTPNetworkOrigin} from '@fluxer/instance_bootstrap/src/NetworkOrigin';
|
import {normalizeHTTPNetworkOrigin} from '@fluxer/instance_bootstrap/src/NetworkOrigin';
|
||||||
|
|
||||||
const logger = createChildLogger('DesktopOutboundHTTP');
|
const logger = createChildLogger('DesktopOutboundHTTP');
|
||||||
@@ -135,6 +136,7 @@ interface DesktopOutboundHTTPOptions {
|
|||||||
readonly resolveHostAddresses?: DesktopHostAddressResolver;
|
readonly resolveHostAddresses?: DesktopHostAddressResolver;
|
||||||
readonly resolveProxy?: DesktopProxyResolver;
|
readonly resolveProxy?: DesktopProxyResolver;
|
||||||
readonly sendThroughSession?: DesktopSessionHTTPSender;
|
readonly sendThroughSession?: DesktopSessionHTTPSender;
|
||||||
|
readonly trustedCertificates?: ReadonlyArray<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DesktopOutboundGETRequest {
|
interface DesktopOutboundGETRequest {
|
||||||
@@ -725,15 +727,12 @@ interface DesktopProxiedSend {
|
|||||||
|
|
||||||
export class DesktopOutboundHTTP {
|
export class DesktopOutboundHTTP {
|
||||||
private readonly httpAgent = new http.Agent({keepAlive: true, maxSockets: DESKTOP_OUTBOUND_HTTP_MAX_SOCKETS});
|
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({
|
private readonly originRequestHttpAgent = new http.Agent({
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
maxSockets: DESKTOP_OUTBOUND_HTTP_MAX_IN_FLIGHT_PER_SERVICE,
|
maxSockets: DESKTOP_OUTBOUND_HTTP_MAX_IN_FLIGHT_PER_SERVICE,
|
||||||
});
|
});
|
||||||
private readonly originRequestHttpsAgent = new https.Agent({
|
private readonly originRequestHttpsAgent: https.Agent;
|
||||||
keepAlive: true,
|
|
||||||
maxSockets: DESKTOP_OUTBOUND_HTTP_MAX_IN_FLIGHT_PER_SERVICE,
|
|
||||||
});
|
|
||||||
private readonly bindings = new Map<string, DesktopOriginAddressBinding>();
|
private readonly bindings = new Map<string, DesktopOriginAddressBinding>();
|
||||||
private readonly pendingBindings = new Map<string, DesktopPendingOriginBinding>();
|
private readonly pendingBindings = new Map<string, DesktopPendingOriginBinding>();
|
||||||
private readonly registeredRequirements = new Map<string, DesktopAddressRequirement>();
|
private readonly registeredRequirements = new Map<string, DesktopAddressRequirement>();
|
||||||
@@ -750,6 +749,14 @@ export class DesktopOutboundHTTP {
|
|||||||
this.resolveHostAddresses = options.resolveHostAddresses ?? lookupAllAddresses;
|
this.resolveHostAddresses = options.resolveHostAddresses ?? lookupAllAddresses;
|
||||||
this.resolveProxy = options.resolveProxy ?? resolveDesktopSessionProxy;
|
this.resolveProxy = options.resolveProxy ?? resolveDesktopSessionProxy;
|
||||||
this.sendThroughSession = options.sendThroughSession ?? sendThroughDesktopSession;
|
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> {
|
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');
|
throw new Error('a direct route never reaches the session');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'@electron/main/DesktopTrustedCertificates': {resolveDesktopTrustedCertificates: () => []},
|
||||||
};
|
};
|
||||||
const sandbox = {
|
const sandbox = {
|
||||||
console,
|
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-----
|
||||||
Reference in new issue
Block a user