mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 04:55:42 +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,
|
||||
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-----
|
||||
Reference in new issue
Block a user