fix(desktop): trust locally added CAs in Chromium on Linux (#3315)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-08 20:21:04 +02:00
1 parent 4b1afc953f
commit b146b8ee33
23 files changed
+1109 -1

No files matched your search

@@ -0,0 +1,109 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
export const CHROMIUM_SPKI_BLOCKLIST_SOURCE = 'chromium 152.0.7977.78 net/cert/cert_verify_proc_blocklist.inc';
export const CHROMIUM_SPKI_BLOCKLIST: ReadonlySet<string> = new Set([
'04dde9aa9a79f61498682325fa0870276707fb9ca95384120b46893268494fc9',
'0c43ea8bcde9fc3bca165664ac8215567e3489d5393a0c81e1a79141992e1953',
'12132360a33bfdc6c3bf7b7fab26a1684874e72c1263c1f5de565bb49ef03753',
'127da27a9e45f082280b31bf1e561520389f96659093b2697c40fe8600236c8c',
'130ad4e0633521290531b6651f5759b0bc7bc656709ff8f365c2143b0389b6f6',
'15e7ae40cc4b3f7222a5a6fe3e7dc47f6e46ee9a2251839db296d62ada2a0df7',
'1528397da212890a830b0b95a59968cef234773779df5181cf10fa647534bb65',
'1af56c98ff043ef92bebff54cebb4dd67a25ba956c817f3e6dd3c1e52eb584c1',
'1f4224cec84fc99ced881ff6fcfd3e21f8c519c547aa6a5dd3de247302ce50d1',
'25da1ad58bbfcfb227d8723b1857d4c18e7baa7417b4f9eff9366b5e869f8b39',
'2c998e761160c3b06d82faa9fdc7545d9bda9eb60310f992aa510a6280b74245',
'2dc4cb591f7ef0663441646bcf5c0e9dbcded77ca02945193cefc6edb1740614',
'30efe41382476c3380f02f7e23e66ba2f867b059ee1ea68796b441b85b5d1256',
'32ecc96f912f96d889e73088cd031c7ded2c651c805016157a23b6f32f798a3b',
'36ea96128c89839fb621f8ad0e1ee0b9c2206f62ab7b4da2c6765893c9b7ced2',
'381a3fc7a8b082fa28613a4d07f2c7553f4e1918ee07caa9e8b7cede5a9ca06a',
'383e0e137c37bfb9db29f9a8e45e9ff8dd4c30e440fec2acd3dba7b6c720b993',
'394cff589e689312cfc071ee0bc19fe4c606216ce543429de6db62e42dbb3bc1',
'3edbd9ace639ba1a2d4ad04718711fda23e859b2fbf5d137d424045e7919dfb9',
'455b87e96f1cea2f8b6dae0808ec24738fd92b7fd306757198bf389d755c0b6c',
'490b6ec6beb2d6034720b5149b6b29cd35515988cc16af854148b07b9b1f8a11',
'4bb8f35ba1e126f8dde1b0c420625ed86dce61a7bddadbdea9aba578ff13145e',
'4cdb060f3cfe4c3d3f5e31c300fd68a91e0d1e5f46b64e4895f20e1b5cf8269f',
'4eada9b5311e718199d98ea82b95005cba93198ab1f97efcbe8dc6201628f8af',
'4f7162b974491c98585ec28fe759aa00c330d0b465190a896cc4b616231831fc',
'57809446eaf114843854fe636ed9bcb552e3c616663bc44cc95acf5650016d3e',
'5c722cb70fb311f21e0da0e7d12ebc8e05f60796bc49cf511849d5bc62030382',
'5e53f26467f894fde53b3fa406a440cbb3b076bb5b758fe4834ad66500208907',
'6adb8e3e055460922d1501cb97f94c6a02e39c8f2774ca408825b7b58379dc14',
'707d364e72ae521431dd953897f9c4846d5b8c324298fe53fbd4ada1f2d1157f',
'7165e991ade7916d86b466abebb6e457ca931c804e58ce1fbabae509156ffb43',
'781a4cf2e92452f3ee01d0c381a4214f3904165c390adbd61fcd11244e09b2dc',
'7aedddf36b18f8acb7379fe1ce183212b2350d0788abe0e82457be9badad6d54',
'7afe4b071a2f1f46f8ba944a26d584d5960b92fb48c3ba1b7cab84905f32aacd',
'7cd295b75544808abd9409466f0837c5aadc02e33b6150c6644de0a09659f23c',
'7d5e3f50508197b9a478b11340b7dce20a3c4de49c48c9a294158a895c44a21b',
'7e7058ea35ad4359654159973f560187f16d19c514b939c5055672d1d2a518ac',
'87bfd8afa3af5b429d09a9aa54ee61364f5ae11131e438fc41095343cd16b135',
'8a2affbd1a1c5d1bdccbb7f548ba995f966806b3fd0c3a00fae2e52f3c853989',
'8e12d0cb3b7df3ea2257579489fd8658c95603ea6cf4b73163a41eb7b7e93fee',
'8f591f7aa4dc3efe9490c38a4692c9011ed128f1de595569406d77b6fa1f6b4c',
'927a1b8562280576d048c50321ada43d8703d2d9521a18c28b8c46cc6aae4efd',
'93ca2d436cae7f68d2b4256ca175c985ce39926dc9f7eeaeecf2f8970fb97802',
'962e4b54bb98a7ee5d5feb9633f991d3c3300e9514dade7b0d4f828c794f8e87',
'968dba69fbff15bf3762089431ade5a7ead4b7eaf1be70026810bc57d1c64f6e',
'99ba4784f9b08512902eb0c3c86df0ec049eac9b65f77a9ba42be9d6ebce320f',
'9b8a93decccfbafcf4d04d3442128fb35218cfe437a3d8d0328c99f89089e450',
'9c59a3ccaea4699842b068cfc52cf945db51986957c832cdb18ca73849fbb9ee',
'9d98a1fb60538c4cc4857ff1a8c8034faf6fc592093f619994b2c813d250b864',
'9dd55fc573f546cb6a3831d1112d8710a6f4f82dc87f5fae9d3a1a028dd36e4b',
'a0cf53f422651e39317ae31af64577be450fa376e289ed8342b7fc133c697419',
'a903af8c07bb91b0d9e3f3a30c6d53339fc5bd47e5d6bdb476598860c068a024',
'a9b55a9b5531bbf7c71a1e4920efe796c2b67968f55a6ce5cb62172ed9945bca',
'af591518e2e6c60ebbfc0907afaa49bc4051d45e7f214abfee7512ee00f661ed',
'b13fa2e6131a888a01f3d62056fb0efbe999eb6b6e14927613e02ba8b8fb046e',
'b3182e289ae34ddf2be643ab79c244301605fa0f1eaae6d10fb929600af84df0',
'b4d5c920415ed0cc4f5dbc7f542636762e80da6625f33f2b6ad6db68bdbab29a',
'b5ba8dd7f89564c2889d3d6453c84998c77824919b64ea0835aa62986591be50',
'b89bcbb8acd474c1bea7dad65037f48dcecc9dfaa0612c3c2445956419df32fe',
'c0ed205346bbbde06eb560f5cee02a3634e2474a7e76cf8fbef563bb117dd0e3',
'c173f0626456ca854ff2a7f0b133a7cf4d0211e552f24b3e33ade8c59f0a424c',
'c4387d45364a313fbfe79812b35b815d42852ab03b06f11589638021c8f2cb44',
'c601234e2b9325dc92e3eabac19600b0b49947d4d04d8c99d32127493ea028f8',
'c63d68c648a18b77641c427a669d61c9768a55f4fcd0322eac96c57700299cf1',
'c8fddc75cb1bdbb58c07b4ea847287f626659dd66bc10a26add9b575b3a0a38d',
'cdee9f3305572a677e1a6c82dc1e02a35b11cae6a684338c9f37fe1ac8daec23',
'ced43902ab5fb57b442322dc0e172a4fb55f7178b808f94e780a6fd6cc6bd818',
'd31ec39285b7a5319d0157db420ed87c743e333bbc77f8771f70464f436a6049',
'd34b255b2fe7d1a09656cbab6409f73c796ec7d66af73653ecc39af978297310',
'd6a18443d348db994f934ccd8e635d833a27ac1e56f8afaf7c97cb4f43eab68b',
'db15c0062b520f318a19dacfecd64f9e7a3fbe609fd586796f20ae028e8e3058',
'db1d13ec42a2cba3673ba67af2def812e9c35566617576d95b4d6face3ef0ae8',
'dd3eeb779bee07f9efdac382408b28d142fa842c78e8bc0e33348d57b92f0583',
'de8f05074ec0318e7e7e8d3190dae8b00894f0e8dddfd3913d01759b4f79b05d',
'e42f24bd4d37f4aa2e56b979d83d1e65219fe0e9e3a382a1b3cb66c93955de75',
'e6e136c86154f32c3e49f47cfc6b338ff2dc61ce14fc7589b3b56a1450132701',
'e7b932ae7e9bdc701d771d6f39e8a653449eea43bdb47bd91022950d9179d87e',
'e849c7176c93df65f64b616982366e5663117812b6fa2bc0c8fa8aeaee4181cc',
'ea08c8d45d52ca593de524f0513ca6418da9859f7b08ef13ff9dd7bf612d6a37',
'ea87f462deefffbd7775aa2a4b7e0fcb91c22eee6df69ed90100ccc73b311476',
'ef551284715232de92e246c323329362b132493bb16b589e4775520beb871a56',
'f034f642ca1d9e88e9efeafcb15c7c937aa19e04b080f27335e1da70d1ca1201',
'f2bbe04c5dc70d763e89c5a0527048cd9ecd39eb621e2072ff9a5f8432571aa0',
'f30e8f61011d65873ccb81b40fa6219749b994f01fa24d0201d521c2435603ca',
'f5293d47ed38d4c31b2d42dee3b5b3ace97ca26ca2ac0365e3622ee802131fbb',
'fa00bec73dd99795df1162c789987004c26cbf90af4db442f66220de41354ac9',
'fcd76cca2347e5cd5b39347f51cf43654b69a2bfc9073670a6be47d8701e6e0e',
'b0fcce78c1664e293544c143e3d2689f723f5b6e6317107e163d22ba8069794a',
'c56217b7a828c7341c0ae7a590d8790d4def536652e60ae5b8bdfa26978fe09c',
'e47c5cd2dc8babb4e53f8a4983920275ef6ffaacb089e87a2c1fbe5a585f05ed',
'39736588b94a4ce767f731cad53f4cbe44137e321eadcaef8ce79a229bbca989',
'ea12705de7c48f6fcce2cb8dbc542e0fc38ac38e08880dd04a02ef67c93ae135',
'a6aca1ec9809cc5b4821ff9d29c5ebe651960b91b1f19cc89b55ef87818a9509',
'02a95f43431019e9dc225f05f419330190deb4a3f1869caac9842b403dcbee77',
'8928c59398b0f171c0f96fdae6ab8dd0f48ee06d174da10c404ac00143c7a749',
'6c0702e86e0238abdb974f44a30e1a75f627cc50b4e7ce6bb1c0923b98d0e81c',
'f806c390ef8d1165ef61bc77ef66de4b245a31f5bebf1029038ff31c50a63f59',
'cef8710dff5e87a2830daa85e884f082033de58ed395a10f7e7c7334b5bfbe58',
'5be28fcf7e31ba64fb2459e278ef032e3a09b61851952d6e85f746150fcadff1',
'29ec436daffa63d25f502cc9952277581876f6893feb783a0444a6f5e73aa422',
'36ce75edea806f0985145683294e54dbdff02fc821870cf5067a308d1504be3a',
'a4e9af01416e3a029b5d35e5b119de00cfe156c5cf95fc823cf6d05e3c1a8237',
'3472e4f16c570e0dd388aaaa4a64a34a4b939f1ca770996b5be0037c1aded9c1',
]);
@@ -0,0 +1,91 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import assert from 'node:assert/strict';
import {X509Certificate} from 'node:crypto';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import {describe, test} from 'node:test';
import {fileURLToPath} from 'node:url';
import {installElectronStub} from './LocalAppTestSupport.test.mjs';
installElectronStub();
const {buildDesktopTrustAnchors, verifyAgainstDesktopSystemTrust} = await import('./DesktopSystemTrustVerifier.ts');
const {resolveDesktopLocallyAddedCertificates} = await import('./DesktopTrustedCertificates.ts');
const FIXTURES = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures', 'desktop-trust');
const read = (name) => fs.readFileSync(path.join(FIXTURES, `${name}.pem`), 'utf8');
const AUTHORITY_INVALID = -202;
const DEFER = -3;
const ACCEPT = 0;
function chain(names) {
let certificate;
for (const name of [...names].reverse()) certificate = {data: read(name), issuerCert: certificate};
return certificate;
}
function verify(names, {hostname = 'host.example', errorCode = AUTHORITY_INVALID, anchors = ['root']} = {}) {
const store = buildDesktopTrustAnchors(anchors.map(read));
return verifyAgainstDesktopSystemTrust({hostname, certificate: chain(names), errorCode}, () => store);
}
describe('desktop system trust verifier', () => {
test('accepts a chain that ends at a locally added CA', () => {
assert.equal(verify(['leaf', 'int']), ACCEPT);
assert.equal(verify(['leaf', 'int'], {hostname: 'HOST.example.'}), ACCEPT);
assert.equal(verify(['leaf', 'int'], {hostname: '127.0.0.1'}), ACCEPT);
assert.equal(verify(['leaf', 'int'], {hostname: '[::1]'}), ACCEPT);
assert.equal(verify(['ec_leaf']), ACCEPT);
assert.equal(verify(['wc_leaf'], {hostname: 'a.lan.home.example'}), ACCEPT);
});
test('defers to Chromium for other errors, anchors and hosts', () => {
assert.equal(verify(['leaf', 'int'], {errorCode: -201}), DEFER);
assert.equal(verify(['leaf', 'int'], {anchors: ['pl_int']}), DEFER);
assert.equal(verify(['leaf', 'int'], {hostname: 'other.example'}), DEFER);
assert.equal(verify(['wc_leaf'], {hostname: 'a.b.lan.home.example'}), DEFER);
assert.equal(verify(['wc_leaf'], {hostname: 'a.home.example'}), DEFER);
assert.equal(verify(['wc_leaf'], {hostname: 'victim.co.uk'}), DEFER);
});
test('defers on constrained, weak or unknown certificates in the path', () => {
assert.equal(verify(['nc_leaf', 'nc_int']), DEFER);
assert.equal(verify(['nc_leaf'], {anchors: ['nc_int']}), DEFER);
assert.equal(verify(['eku_leaf', 'eku_int']), DEFER);
assert.equal(verify(['pl_leaf', 'pl_int2', 'pl_int']), DEFER);
assert.equal(verify(['crit_leaf']), DEFER);
assert.equal(verify(['sha1_leaf']), DEFER);
assert.equal(verify(['weak_leaf']), DEFER);
});
test('rejects non-DER booleans and Chromium blocklisted keys', () => {
assert.equal(verify(['bool_ff_leaf']), ACCEPT);
assert.equal(verify(['bool_01_leaf']), DEFER);
assert.equal(buildDesktopTrustAnchors([read('kz_root')]).size, 0);
});
test('anchors only on admin added CAs that the distro bundle still trusts', () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'desktop-trust-'));
const write = (file, content) => {
fs.mkdirSync(path.dirname(path.join(root, file)), {recursive: true});
fs.writeFileSync(path.join(root, file), content);
};
const der = (name) => new X509Certificate(read(name)).raw;
try {
assert.deepEqual(resolveDesktopLocallyAddedCertificates(root), []);
write('etc/ssl/certs/ca-certificates.crt', [read('root'), read('int'), read('kz_root')].join('\n'));
write('usr/local/share/ca-certificates/site/root.crt', read('root'));
write('usr/local/share/ca-certificates/int.der', der('int'));
write('usr/local/share/ca-certificates/deselected.crt', read('pl_int'));
write('cert.pem', read('pl_int'));
process.env.SSL_CERT_FILE = path.join(root, 'cert.pem');
const added = resolveDesktopLocallyAddedCertificates(root).map((pem) => new X509Certificate(pem).subject);
assert.deepEqual(added.sort(), ['CN=Trust Test Int', 'CN=Trust Test Root']);
} finally {
delete process.env.SSL_CERT_FILE;
fs.rmSync(root, {recursive: true, force: true});
}
});
});
@@ -0,0 +1,448 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {createHash, X509Certificate} from 'node:crypto';
import net from 'node:net';
import {createChildLogger} from '@electron/common/Logger';
import {CHROMIUM_SPKI_BLOCKLIST, CHROMIUM_SPKI_BLOCKLIST_SOURCE} from '@electron/main/DesktopChromiumSPKIBlocklist';
import {resolveDesktopLocallyAddedCertificates} from '@electron/main/DesktopTrustedCertificates';
import type {Certificate, Session} from 'electron';
const logger = createChildLogger('DesktopSystemTrustVerifier');
const CHROMIUM_DEFAULT_RESULT = -3;
const CHROMIUM_ACCEPT = 0;
const CHROMIUM_CERT_AUTHORITY_INVALID = -202;
const MAX_CHAIN_LENGTH = 6;
const MAX_SIGNATURE_CHECKS = 25;
const MIN_RSA_BITS = 2048;
const MAX_RSA_BITS = 8192;
const MIN_WILDCARD_BASE_LABELS = 3;
const DER_TRUE = 0xff;
const OID = {
basicConstraints: '2.5.29.19',
keyUsage: '2.5.29.15',
subjectAltName: '2.5.29.17',
extendedKeyUsage: '2.5.29.37',
nameConstraints: '2.5.29.30',
policyConstraints: '2.5.29.36',
inhibitAnyPolicy: '2.5.29.54',
serverAuth: '1.3.6.1.5.5.7.3.1',
anyExtendedKeyUsage: '2.5.29.37.0',
} as const;
const ALLOWED_CRITICAL_EXTENSIONS: ReadonlySet<string> = new Set([
OID.basicConstraints,
OID.keyUsage,
OID.subjectAltName,
OID.extendedKeyUsage,
]);
const FORBIDDEN_EXTENSIONS: ReadonlySet<string> = new Set([
OID.nameConstraints,
OID.policyConstraints,
OID.inhibitAnyPolicy,
]);
const ALLOWED_SIGNATURE_ALGORITHMS: ReadonlySet<string> = new Set([
'1.2.840.113549.1.1.11',
'1.2.840.113549.1.1.12',
'1.2.840.113549.1.1.13',
'1.2.840.10045.4.3.2',
'1.2.840.10045.4.3.3',
'1.2.840.10045.4.3.4',
'1.3.101.112',
]);
const ALLOWED_EC_CURVES: ReadonlySet<string> = new Set(['prime256v1', 'secp384r1']);
const KEY_USAGE_DIGITAL_SIGNATURE = 0;
const KEY_USAGE_KEY_ENCIPHERMENT = 2;
const KEY_USAGE_KEY_CERT_SIGN = 5;
const TAG_BOOLEAN = 0x01;
const TAG_INTEGER = 0x02;
const TAG_BIT_STRING = 0x03;
const TAG_OCTET_STRING = 0x04;
const TAG_OID = 0x06;
const TAG_SEQUENCE = 0x30;
const TAG_VERSION = 0xa0;
const TAG_EXTENSIONS = 0xa3;
const TAG_SAN_DNS = 0x82;
const TAG_SAN_IP = 0x87;
class MalformedCertificateError extends Error {
constructor() {
super('Malformed certificate DER');
this.name = 'MalformedCertificateError';
}
}
interface DerNode {
readonly tag: number;
readonly content: Buffer;
}
interface Extension {
readonly critical: boolean;
readonly value: Buffer;
}
interface ParsedCertificate {
readonly x509: X509Certificate;
readonly signatureAlgorithm: string;
readonly extensions: ReadonlyMap<string, Extension>;
readonly isCA: boolean;
readonly pathLength: number | null;
}
interface DesktopSystemTrustRequest {
readonly hostname: string;
readonly certificate: Certificate;
readonly errorCode: number;
}
export type DesktopTrustAnchors = ReadonlyMap<string, ReadonlyArray<ParsedCertificate>>;
function readNodes(buffer: Buffer): Array<DerNode> {
const nodes: Array<DerNode> = [];
let offset = 0;
while (offset < buffer.length) {
if (offset + 2 > buffer.length) throw new MalformedCertificateError();
const tag = buffer[offset];
if ((tag & 0x1f) === 0x1f) throw new MalformedCertificateError();
let length = buffer[offset + 1];
let cursor = offset + 2;
if (length & 0x80) {
const size = length & 0x7f;
if (size === 0 || size > 4 || cursor + size > buffer.length) throw new MalformedCertificateError();
length = 0;
for (let index = 0; index < size; index++) length = length * 256 + buffer[cursor + index];
cursor += size;
}
if (cursor + length > buffer.length) throw new MalformedCertificateError();
nodes.push({tag, content: buffer.subarray(cursor, cursor + length)});
offset = cursor + length;
}
return nodes;
}
function expect(node: DerNode | undefined, tag: number): DerNode {
if (node == null || node.tag !== tag) throw new MalformedCertificateError();
return node;
}
function only(buffer: Buffer, tag: number): DerNode {
const nodes = readNodes(buffer);
if (nodes.length !== 1) throw new MalformedCertificateError();
return expect(nodes[0], tag);
}
function readDerTrue(node: DerNode): true {
if (node.content.length !== 1 || node.content[0] !== DER_TRUE) throw new MalformedCertificateError();
return true;
}
function decodeOid(content: Buffer): string {
if (content.length === 0) throw new MalformedCertificateError();
const parts: Array<number> = [];
let value = 0;
for (const byte of content) {
value = value * 128 + (byte & 0x7f);
if (value > Number.MAX_SAFE_INTEGER / 128) throw new MalformedCertificateError();
if ((byte & 0x80) === 0) {
parts.push(value);
value = 0;
}
}
if ((content[content.length - 1] & 0x80) !== 0) throw new MalformedCertificateError();
const first = parts.shift() ?? 0;
const head = first < 80 ? [Math.floor(first / 40), first % 40] : [2, first - 80];
return [...head, ...parts].join('.');
}
function parseExtensions(tbs: ReadonlyArray<DerNode>): Map<string, Extension> {
const extensions = new Map<string, Extension>();
const wrapper = tbs.find((node) => node.tag === TAG_EXTENSIONS);
if (wrapper == null) return extensions;
for (const entry of readNodes(only(wrapper.content, TAG_SEQUENCE).content)) {
const fields = readNodes(expect(entry, TAG_SEQUENCE).content);
const oid = decodeOid(expect(fields[0], TAG_OID).content);
const hasCritical = fields[1]?.tag === TAG_BOOLEAN;
const critical = hasCritical && readDerTrue(fields[1]);
const value = expect(fields[hasCritical ? 2 : 1], TAG_OCTET_STRING).content;
if (fields.length !== (hasCritical ? 3 : 2) || extensions.has(oid)) throw new MalformedCertificateError();
extensions.set(oid, {critical, value});
}
return extensions;
}
function parseBasicConstraints(extension: Extension | undefined): {isCA: boolean; pathLength: number | null} {
if (extension == null) return {isCA: false, pathLength: null};
const fields = readNodes(only(extension.value, TAG_SEQUENCE).content);
let index = 0;
let isCA = false;
if (fields[index]?.tag === TAG_BOOLEAN) {
isCA = readDerTrue(fields[index]);
index++;
}
let pathLength: number | null = null;
if (fields[index]?.tag === TAG_INTEGER) {
const content = fields[index].content;
if (content.length === 0 || content.length > 2 || (content[0] & 0x80) !== 0) throw new MalformedCertificateError();
pathLength = content.readUIntBE(0, content.length);
index++;
}
if (index !== fields.length) throw new MalformedCertificateError();
return {isCA, pathLength};
}
function parseCertificate(x509: X509Certificate): ParsedCertificate {
const parts = readNodes(only(x509.raw, TAG_SEQUENCE).content);
if (parts.length !== 3) throw new MalformedCertificateError();
const tbs = readNodes(expect(parts[0], TAG_SEQUENCE).content);
const algorithm = readNodes(expect(parts[1], TAG_SEQUENCE).content);
const signatureAlgorithm = decodeOid(expect(algorithm[0], TAG_OID).content);
if (tbs[0]?.tag !== TAG_VERSION && tbs.some((node) => node.tag === TAG_EXTENSIONS)) {
throw new MalformedCertificateError();
}
const extensions = parseExtensions(tbs);
return {x509, signatureAlgorithm, extensions, ...parseBasicConstraints(extensions.get(OID.basicConstraints))};
}
function keyUsageHas(certificate: ParsedCertificate, bit: number): boolean | null {
const extension = certificate.extensions.get(OID.keyUsage);
if (extension == null) return null;
const bits = only(extension.value, TAG_BIT_STRING).content;
const byte = bits[1 + (bit >> 3)];
return byte != null && (byte & (0x80 >> (bit & 7))) !== 0;
}
function extendedKeyUsages(certificate: ParsedCertificate): ReadonlyArray<string> | null {
const extension = certificate.extensions.get(OID.extendedKeyUsage);
if (extension == null) return null;
return readNodes(only(extension.value, TAG_SEQUENCE).content).map((node) => decodeOid(expect(node, TAG_OID).content));
}
function allowsServerAuth(certificate: ParsedCertificate): boolean {
const usages = extendedKeyUsages(certificate);
return usages == null || usages.includes(OID.serverAuth) || usages.includes(OID.anyExtendedKeyUsage);
}
function hasAcceptableKey(certificate: ParsedCertificate): boolean {
const key = certificate.x509.publicKey;
const details = key.asymmetricKeyDetails ?? {};
switch (key.asymmetricKeyType) {
case 'rsa': {
const bits = details.modulusLength ?? 0;
return bits >= MIN_RSA_BITS && bits <= MAX_RSA_BITS;
}
case 'ec':
return details.namedCurve != null && ALLOWED_EC_CURVES.has(details.namedCurve);
case 'ed25519':
return true;
default:
return false;
}
}
function isBlocklisted(certificate: ParsedCertificate): boolean {
const spki = certificate.x509.publicKey.export({type: 'spki', format: 'der'});
return CHROMIUM_SPKI_BLOCKLIST.has(createHash('sha256').update(spki).digest('hex'));
}
function isCurrentlyValid(certificate: ParsedCertificate, now: number): boolean {
return Date.parse(certificate.x509.validFrom) <= now && now <= Date.parse(certificate.x509.validTo);
}
function passesCommonChecks(certificate: ParsedCertificate, now: number): boolean {
for (const [oid, extension] of certificate.extensions) {
if (FORBIDDEN_EXTENSIONS.has(oid)) return false;
if (extension.critical && !ALLOWED_CRITICAL_EXTENSIONS.has(oid)) return false;
}
return isCurrentlyValid(certificate, now) && hasAcceptableKey(certificate) && !isBlocklisted(certificate);
}
function isUsableAuthority(certificate: ParsedCertificate, now: number): boolean {
return (
certificate.isCA &&
keyUsageHas(certificate, KEY_USAGE_KEY_CERT_SIGN) !== false &&
allowsServerAuth(certificate) &&
passesCommonChecks(certificate, now)
);
}
function isUsableIntermediate(certificate: ParsedCertificate, now: number): boolean {
return ALLOWED_SIGNATURE_ALGORITHMS.has(certificate.signatureAlgorithm) && isUsableAuthority(certificate, now);
}
function isUsableLeaf(certificate: ParsedCertificate, now: number): boolean {
if (certificate.isCA || !ALLOWED_SIGNATURE_ALGORITHMS.has(certificate.signatureAlgorithm)) return false;
const digitalSignature = keyUsageHas(certificate, KEY_USAGE_DIGITAL_SIGNATURE);
if (digitalSignature === false && keyUsageHas(certificate, KEY_USAGE_KEY_ENCIPHERMENT) === false) return false;
return allowsServerAuth(certificate) && passesCommonChecks(certificate, now);
}
function normalizeHost(hostname: string): string {
return hostname
.replace(/^\[(.*)\]$/u, '$1')
.replace(/\.$/u, '')
.toLowerCase();
}
function ipBytes(host: string): Buffer | null {
if (net.isIPv4(host)) return Buffer.from(host.split('.').map(Number));
if (!net.isIPv6(host)) return null;
const [head = '', tail = ''] = host.split('::');
const words = (part: string) => (part === '' ? [] : part.split(':'));
const headWords = words(head);
const tailWords = words(tail);
const all = host.includes('::')
? [...headWords, ...Array(8 - headWords.length - tailWords.length).fill('0'), ...tailWords]
: headWords;
if (all.length !== 8) return null;
return Buffer.from(all.flatMap((word) => [parseInt(word, 16) >> 8, parseInt(word, 16) & 0xff]));
}
function matchesDnsName(pattern: string, host: string): boolean {
const name = pattern.replace(/\.$/u, '').toLowerCase();
if (!name.includes('*')) return name === host;
if (!name.startsWith('*.') || name.indexOf('*', 1) !== -1) return false;
const base = name.slice(2);
if (base.split('.').length < MIN_WILDCARD_BASE_LABELS) return false;
const dot = host.indexOf('.');
return dot > 0 && host.slice(dot + 1) === base;
}
function matchesHost(leaf: ParsedCertificate, hostname: string): boolean {
const extension = leaf.extensions.get(OID.subjectAltName);
if (extension == null || typeof hostname !== 'string') return false;
const host = normalizeHost(hostname);
const address = ipBytes(host);
for (const name of readNodes(only(extension.value, TAG_SEQUENCE).content)) {
if (address != null && name.tag === TAG_SAN_IP && name.content.equals(address)) return true;
if (address == null && name.tag === TAG_SAN_DNS && matchesDnsName(name.content.toString('latin1'), host)) {
return true;
}
}
return false;
}
interface PathSearch {
readonly intermediates: ReadonlyArray<ParsedCertificate>;
readonly anchors: DesktopTrustAnchors;
readonly now: number;
readonly signatures: Map<ParsedCertificate, Map<ParsedCertificate, boolean>>;
signatureChecks: number;
}
function isIssuedBy(search: PathSearch, certificate: ParsedCertificate, issuer: ParsedCertificate): boolean {
if (!certificate.x509.checkIssued(issuer.x509)) return false;
let byIssuer = search.signatures.get(certificate);
if (byIssuer == null) {
byIssuer = new Map();
search.signatures.set(certificate, byIssuer);
}
const cached = byIssuer.get(issuer);
if (cached != null) return cached;
if (search.signatureChecks >= MAX_SIGNATURE_CHECKS) return false;
search.signatureChecks++;
const verified = certificate.x509.verify(issuer.x509.publicKey);
byIssuer.set(issuer, verified);
return verified;
}
function withinPathLength(authority: ParsedCertificate, intermediatesBelow: number): boolean {
return authority.pathLength == null || intermediatesBelow <= authority.pathLength;
}
function buildsPath(search: PathSearch, current: ParsedCertificate, path: ReadonlyArray<ParsedCertificate>): boolean {
const below = path.length - 1;
for (const anchor of search.anchors.get(current.x509.issuer) ?? []) {
if (
isCurrentlyValid(anchor, search.now) &&
withinPathLength(anchor, below) &&
isIssuedBy(search, current, anchor)
) {
return true;
}
}
if (path.length >= MAX_CHAIN_LENGTH - 1) return false;
for (const candidate of search.intermediates) {
if (path.includes(candidate) || !withinPathLength(candidate, below)) continue;
if (isIssuedBy(search, current, candidate) && buildsPath(search, candidate, [...path, candidate])) return true;
}
return false;
}
function presentedChain(certificate: Certificate): Array<X509Certificate> {
const chain: Array<X509Certificate> = [];
let current: Certificate | undefined = certificate;
while (current != null) {
if (chain.length >= MAX_CHAIN_LENGTH) throw new MalformedCertificateError();
chain.push(new X509Certificate(current.data));
current = current.issuerCert;
}
return chain;
}
function chainsToDesktopTrustAnchor(
certificate: Certificate,
hostname: string,
anchors: DesktopTrustAnchors,
now: number,
): boolean {
const [leafX509, ...rest] = presentedChain(certificate);
if (leafX509 == null) return false;
const leaf = parseCertificate(leafX509);
if (!isUsableLeaf(leaf, now) || !matchesHost(leaf, hostname)) return false;
const intermediates = rest.map(parseCertificate).filter((candidate) => isUsableIntermediate(candidate, now));
return buildsPath({intermediates, anchors, now, signatures: new Map(), signatureChecks: 0}, leaf, [leaf]);
}
export function buildDesktopTrustAnchors(
certificates: ReadonlyArray<string>,
now: number = Date.now(),
): DesktopTrustAnchors {
const anchors = new Map<string, Array<ParsedCertificate>>();
for (const pem of certificates) {
let anchor: ParsedCertificate;
try {
anchor = parseCertificate(new X509Certificate(pem));
} catch {
continue;
}
if (!isUsableAuthority(anchor, now)) continue;
const bySubject = anchors.get(anchor.x509.subject);
if (bySubject == null) anchors.set(anchor.x509.subject, [anchor]);
else bySubject.push(anchor);
}
return anchors;
}
export function verifyAgainstDesktopSystemTrust(
request: DesktopSystemTrustRequest,
loadAnchors: () => DesktopTrustAnchors,
): number {
if (request.errorCode !== CHROMIUM_CERT_AUTHORITY_INVALID) return CHROMIUM_DEFAULT_RESULT;
try {
const anchors = loadAnchors();
if (anchors.size > 0 && chainsToDesktopTrustAnchor(request.certificate, request.hostname, anchors, Date.now())) {
logger.debug('Accepted a certificate that chains to a locally added CA', {hostname: request.hostname});
return CHROMIUM_ACCEPT;
}
} catch (error) {
logger.warn('Could not check a certificate against locally added CAs', {hostname: request.hostname, error});
}
return CHROMIUM_DEFAULT_RESULT;
}
export function installDesktopSystemTrustVerifier(target: Session): void {
if (process.platform !== 'linux') return;
const anchors = buildDesktopTrustAnchors(resolveDesktopLocallyAddedCertificates());
if (anchors.size === 0) return;
logger.info('Trusting locally added CAs for Chromium requests', {
count: anchors.size,
blocklist: CHROMIUM_SPKI_BLOCKLIST_SOURCE,
});
target.setCertificateVerifyProc((request, callback) => {
callback(verifyAgainstDesktopSystemTrust(request, () => anchors));
});
}
@@ -1,5 +1,8 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {X509Certificate} from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import tls from 'node:tls';
import {createChildLogger} from '@electron/common/Logger';
@@ -25,3 +28,92 @@ export function resolveDesktopTrustedCertificates(
for (const certificate of readStore(source, 'system')) merged.add(certificate);
return [...merged];
}
interface DistroTrustLayout {
readonly bundle: string;
readonly anchorDirectories: ReadonlyArray<string>;
}
const DISTRO_TRUST_LAYOUTS: ReadonlyArray<DistroTrustLayout> = Object.freeze([
{bundle: 'etc/ssl/certs/ca-certificates.crt', anchorDirectories: ['usr/local/share/ca-certificates']},
{
bundle: 'etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem',
anchorDirectories: ['etc/pki/ca-trust/source/anchors', 'usr/share/pki/ca-trust-source/anchors'],
},
{
bundle: 'etc/ca-certificates/extracted/tls-ca-bundle.pem',
anchorDirectories: ['etc/ca-certificates/trust-source/anchors', 'usr/share/ca-certificates/trust-source/anchors'],
},
{
bundle: 'var/lib/ca-certificates/ca-bundle.pem',
anchorDirectories: ['etc/pki/trust/anchors', 'usr/share/pki/trust/anchors'],
},
]);
const PEM_BLOCK = /-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/gu;
const MAX_ANCHOR_DEPTH = 4;
const MAX_ANCHOR_FILES = 512;
const MAX_ANCHOR_FILE_BYTES = 1024 * 1024;
function pemBody(pem: string): string {
return pem.replace(/-----[^-]+-----|\s/gu, '');
}
function readBundle(file: string): ReadonlySet<string> | null {
try {
return new Set((fs.readFileSync(file, 'utf8').match(PEM_BLOCK) ?? []).map(pemBody));
} catch {
return null;
}
}
function anchorFiles(directory: string, depth: number, files: Array<string>): void {
let entries: Array<fs.Dirent>;
try {
entries = fs.readdirSync(directory, {withFileTypes: true});
} catch {
return;
}
for (const entry of entries) {
if (files.length >= MAX_ANCHOR_FILES) return;
const target = path.join(directory, entry.name);
if (entry.isDirectory()) {
if (depth < MAX_ANCHOR_DEPTH) anchorFiles(target, depth + 1, files);
} else if (entry.isFile() || entry.isSymbolicLink()) {
files.push(target);
}
}
}
function readAnchorFile(file: string): ReadonlyArray<X509Certificate> {
try {
const stat = fs.statSync(file);
if (!stat.isFile() || stat.size > MAX_ANCHOR_FILE_BYTES) return [];
const content = fs.readFileSync(file);
const blocks = content.toString('latin1').match(PEM_BLOCK);
if (blocks == null) return [new X509Certificate(content)];
return blocks.flatMap((block) => {
try {
return [new X509Certificate(block)];
} catch {
return [];
}
});
} catch {
return [];
}
}
export function resolveDesktopLocallyAddedCertificates(root: string = '/'): ReadonlyArray<string> {
const added = new Map<string, string>();
for (const layout of DISTRO_TRUST_LAYOUTS) {
const bundle = readBundle(path.join(root, layout.bundle));
if (bundle == null) continue;
const files: Array<string> = [];
for (const directory of layout.anchorDirectories) anchorFiles(path.join(root, directory), 0, files);
for (const certificate of files.flatMap(readAnchorFile)) {
const body = certificate.raw.toString('base64');
if (bundle.has(body)) added.set(body, certificate.toString());
}
}
return [...added.values()];
}
@@ -0,0 +1,18 @@
-----BEGIN CERTIFICATE-----
MIIC5TCCAc2gAwIBAgIBTjANBgkqhkiG9w0BAQsFADAaMRgwFgYDVQQDDA9UcnVz
dCBUZXN0IFJvb3QwIhgPMjAyNTAxMDEwMDAwMDBaGA8yMTI1MDEwMTAwMDAwMFow
DDEKMAgGA1UEAwwBeDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALdv
Oze1slSycxDpCnpZYKClaHOXTRWnzAI8mHMhoiEpgjmXrbN1T75+aruMCQAr2VSB
1YOkxzf8Ntsb0ACfaSEJj2AwcCR6NfXoYbbSIE+V//1HkD/BbrOCUb81wubNFxjd
RZpJjhr6x6wb7DDM5rh8ly2t92N6r9Kh6il7Nssf27v2UZbb8XsjPgKxz2YmCtT8
Ry5ZiwGes9e4qEzV650Fl4xVjOiKKTJMy7ByNnm7guqL8ALhyHoIeRZUmTF85D4I
Pp90AkkMv9qjYllv5pBDHMjj68VldwhTYRTGAvhZjy8eIXA0OQXbpySPL5i8TZVM
O4jLoJFm3Kn5WIvdWO8CAwEAAaNAMD4wDgYDVR0PAQEBBAQDAgWgMBMGA1UdJQQM
MAoGCCsGAQUFBwMBMBcGA1UdEQQQMA6CDGhvc3QuZXhhbXBsZTANBgkqhkiG9w0B
AQsFAAOCAQEAftI9a2g+PexV+mSm/HHLxeGuWdK0IB5Dts/WhisGgAnxjabbZHJI
UZ6GoD5iQqX8RpZMqF2Cilq1oDfBUoWIujHG4JEzTj3WA0vQOE5wgcsmO4QcypNJ
cEn2KezBgDqzWbJ1hTRA3wwlzzOqJIHStt+4QjwetFvhM36R2o/6WzDlfyKmPSIi
1+eQYA7vl2DWr/j13+Oo3e4X/Uqr9KIVJPNYCdg73oRmi98gTi4aNvBcCb2Pz7ty
8lVgQ3iCysa2LYqk14pfGLm2Rs4QeYC0rRI0z3/Jbu5W81Y09FhW+5yyp9pJXmjO
ZxT7ZtgZsmmqp8iW7vpyQjQMhBA+HKtTBg==
-----END CERTIFICATE-----
@@ -0,0 +1,18 @@
-----BEGIN CERTIFICATE-----
MIIC5TCCAc2gAwIBAgIBTTANBgkqhkiG9w0BAQsFADAaMRgwFgYDVQQDDA9UcnVz
dCBUZXN0IFJvb3QwIhgPMjAyNTAxMDEwMDAwMDBaGA8yMTI1MDEwMTAwMDAwMFow
DDEKMAgGA1UEAwwBeDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALdv
Oze1slSycxDpCnpZYKClaHOXTRWnzAI8mHMhoiEpgjmXrbN1T75+aruMCQAr2VSB
1YOkxzf8Ntsb0ACfaSEJj2AwcCR6NfXoYbbSIE+V//1HkD/BbrOCUb81wubNFxjd
RZpJjhr6x6wb7DDM5rh8ly2t92N6r9Kh6il7Nssf27v2UZbb8XsjPgKxz2YmCtT8
Ry5ZiwGes9e4qEzV650Fl4xVjOiKKTJMy7ByNnm7guqL8ALhyHoIeRZUmTF85D4I
Pp90AkkMv9qjYllv5pBDHMjj68VldwhTYRTGAvhZjy8eIXA0OQXbpySPL5i8TZVM
O4jLoJFm3Kn5WIvdWO8CAwEAAaNAMD4wDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQM
MAoGCCsGAQUFBwMBMBcGA1UdEQQQMA6CDGhvc3QuZXhhbXBsZTANBgkqhkiG9w0B
AQsFAAOCAQEAddtLsvQf8S/uuGyOMUgV8NKw4Ki1kX+BTr5xzsq1A01WgCfdYG5u
HL32V6uATwDzO3cH3piDoGLoXAF0sstnciBnfeibCgeBcOrj/8PTXGrRch5kkHnw
kK3OBZQSutT4BafIKoVIpiH95+3t/5sy2dWSkt1NGQN7noPBgddqSTmDiq4nxx+i
LQ+NhT5sX+AkWB6GZNyxnvEC1UtUJiY7k8DQIvV5vLPj4+oCa7XRiY/8rFzVoyBo
5Yq5FP5WgyGXKlWLhtwIeaD+7pAYjrUSnrTR9gWJsJ0l6/vCWhH3aT2vbPf9JUwS
3aHt4faDpfGbcvhlkXWyKHS7T3XYgnFf4A==
-----END CERTIFICATE-----
@@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDWjCCAkKgAwIBAgIUA+GJ9S/l9MoO3gPga8JwUYNpaewwDQYJKoZIhvcNAQEL
BQAwGjEYMBYGA1UEAwwPVHJ1c3QgVGVzdCBSb290MCAXDTI2MTAwODE3MTk0NVoY
DzIxMjYwOTE0MTcxOTQ1WjAMMQowCAYDVQQDDAF4MIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEAvD4+ud9Fp9RwYMtvhL366FKGNhEL6Y6oTouFjU97eQ4a
jTBIgizBsc2CwOoFyNAIK5UDRw68Kvs3m417L07BiDoTh4Y8J/62CKAGsJ5S2KAw
xgZh3BIyvs0gLGa5CAy0FqZ5tzCe546XHZdeGg4ETW3zEQmkB1Zq1ZwtCPX86pr4
N2MrKthotsaRug7ZQ5I8KC0XI819zdA2Bi6LvXIUCilDaWfZb9oeRUsrNCBzTvX4
/uL2sMJDwuiP1Dfk7heqnjsvP2s9N+0k6uw3thIBisTy4F2yhCpfCi9WT+OJZoEH
UjLgOAR0H2fhAVPBR9YTPtVwy7di0PMWsYdfYGvB4QIDAQABo4GjMIGgMAwGA1Ud
EwEB/wQCMAAwDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB8G
A1UdIwQYMBaAFDl9W8k1XKQCubIn46G/oYwjeDkqMBcGA1UdEQQQMA6CDGhvc3Qu
ZXhhbXBsZTASBgkrBgEEAYaNHwEBAf8EAgUAMB0GA1UdDgQWBBQmoqL8NAcX6wuB
1XmVPq0bFnxPKDANBgkqhkiG9w0BAQsFAAOCAQEAYZQW4q0zeprbMilnHRk6C6Td
sXOaOHhB5DiRZW1wjhe3LO1zPLe/nckzqFG+g+jTFa7nLuC6APRlj1yk0pCoyWiD
zkHtdPDy1VdkkIOMEcPdMVPOgEPcIZYFZY77sMz4Ac8d6qRlAcqhJDKwapaQYGZV
ddy1pwPNZxk3EL0DcXYRThD/VOk3FuxZRZseuFNUkdyQ8a14bv+2mrHCFHcYxH26
Q73GvjMsyuL2oncG/ZhFza2kXoOUzi+ZHWh/KIDtWGruXLeFYDvEssaUUVykP2mV
0hy/XEGdOZmUP88GJ4N4CYu1SSn1shGiho42KLrXwpnDqOp43+bAUIOJAYnV1A==
-----END CERTIFICATE-----
@@ -0,0 +1,16 @@
-----BEGIN CERTIFICATE-----
MIICezCCAWOgAwIBAgIUA+GJ9S/l9MoO3gPga8JwUYNpaecwDQYJKoZIhvcNAQEL
BQAwGjEYMBYGA1UEAwwPVHJ1c3QgVGVzdCBSb290MCAXDTI2MTAwODE3MTk0NFoY
DzIxMjYwOTE0MTcxOTQ0WjAMMQowCAYDVQQDDAF4MFkwEwYHKoZIzj0CAQYIKoZI
zj0DAQcDQgAEHHjSNNp5dsZfAnkKCGhzX70d/Ulaz8UFm9caIp6vOi2y1EXi10dZ
VAEgXal5HYc27/wbIFqLztnfofpu+0SlEqOBjzCBjDAMBgNVHRMBAf8EAjAAMA4G
A1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAfBgNVHSMEGDAWgBQ5
fVvJNVykArmyJ+Ohv6GMI3g5KjAXBgNVHREEEDAOggxob3N0LmV4YW1wbGUwHQYD
VR0OBBYEFNjhzyL6G+did2jKx0TJnTj2NhWHMA0GCSqGSIb3DQEBCwUAA4IBAQCs
ITZEqDiKXMtJvSrnfJ6ZGwuQBy/AU3tbw/DlCvTr/UMfcZk8qJ92otWf3MRgKOU7
XBgvCdruxCcG0ST3GqLq9PmGVt+fMwsx5FGF/qzWlDK8ZkFi0Adzsptw732v3/0H
BR8rh+59/NUmyLnag6Xw+nOY0boCVJ87CtNgBNsVmUL65YmVgCHEopen7Fv1q5ms
uGgi+HtWxfGnSiz+4Upw6RdcNmedH+F5M8wzZ4q4Iv4Wmupz3iaLdwGcucpuuC1R
6HIrDOEk+TOBofytCEd+iDorjjIXxNsgBDQf/gjKCvtHh7OVX0MFgQysgPNyIDC6
9mhFYM4YwMfhsMauBm5x
-----END CERTIFICATE-----
@@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDNDCCAhygAwIBAgIUA+GJ9S/l9MoO3gPga8JwUYNpaeowDQYJKoZIhvcNAQEL
BQAwGjEYMBYGA1UEAwwPVHJ1c3QgVGVzdCBSb290MCAXDTI2MTAwODE3MTk0NFoY
DzIxMjYwOTE0MTcxOTQ0WjASMRAwDgYDVQQDDAdFS1UgSW50MIIBIjANBgkqhkiG
9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4v27QD9YFXG8+oPhwytCyErQCaV2PYeHGVm/
l0D0zc+9EHQfI6UbKC/1KjRXgMjmsFNGpGEAj2xVwXMsN04QmsxBhSrrOqrH36Fw
adSj5ewBaKf5/jaFOkJ8O1t4NOl4Ki0Fle4Op6r6IJZwzGpy4qNqlYbgRoPj8H7J
R9BufEtoC18jXcQNlwQP/4go7sq4YwtbFjOJ9mK/mlaAeD9zCb3LApLcYyQyqQaQ
bFEOMifJhCNE6WxbrTXH/+nTEM7nmPaGFqzovAf+0PBNjTkYSd+NqZJDD/fvU3XF
VMiqlAWj8R/UP03KKoTemqey9rXxbocxLTh0wJr/gPjOt7/+VQIDAQABo3gwdjAP
BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUMG5Sf79f
OgByO3UsgJsjlV8lvfEwHwYDVR0jBBgwFoAUOX1byTVcpAK5sifjob+hjCN4OSow
EwYDVR0lBAwwCgYIKwYBBQUHAwIwDQYJKoZIhvcNAQELBQADggEBAFprsUH/ZoMC
QgPZUnW33Tc5tnsEMBgREiqU5LDKdXAy9Qko1b/C2JaDIefYntBcu7eIlLVAfhSs
cBk+XhgditEjAG5yYjV/NPZRh/+L4OjVcVmFRtCtQ+z+DGwVWGn3PzbUL228izFK
djJOitXUPFGu8U1sbA/zv6Z+DpngnLkRWFwJz8cl84beyCnaExXh2MIg3AleDNdf
FoVU0lylC47nx9FHqEthOtGqRn7BHi49htqcCXd4E4OE3c8CLKU0WTRP8ofjK0FW
IdwXVsxWSheDVWRa1Gs3OOsv2TPWRyaRhEQ9mcCws7nVMbhZ94HIC3Rixy3YcLPY
vQXMYpMWrgo=
-----END CERTIFICATE-----
@@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDPjCCAiagAwIBAgIUOuSVRC3VFKpaTRwNlFx7hNY+hPUwDQYJKoZIhvcNAQEL
BQAwEjEQMA4GA1UEAwwHRUtVIEludDAgFw0yNjEwMDgxNzE5NDRaGA8yMTI2MDkx
NDE3MTk0NFowDDEKMAgGA1UEAwwBeDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
AQoCggEBAMYnHmYegqC2h4F4iCFCMo/jGGtOLPmVj4QoX676VEq7Ilw1qMoLfKwe
neNYPt0odizU1wWGf0SuEZ3QrWT5kgh14yzu0AQPboYUpMeYwkIjSxclrMbKqN63
bVT8BVYYuAnOV+4wGKLtSjZS2c3QcJKUijp+C50jyYdJ3DHYuiOviiaCSsCL7jo5
W6I4q5GfyifGA+T089YnLtlQYljhsqz2CWRNsxObD0RcjiAls23ZUAARKVU5cZ/V
PO2H/zF7tSuRSA8nE3/HepSO5pOcAS0/Gz0AEqmr9FdGztixhJ55RLfMIAGDDM3G
DC1wzg/Q0iG53vuJpmIEVpfx3ED5i/sCAwEAAaOBjzCBjDAMBgNVHRMBAf8EAjAA
MA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAfBgNVHSMEGDAW
gBQwblJ/v186AHI7dSyAmyOVXyW98TAXBgNVHREEEDAOggxob3N0LmV4YW1wbGUw
HQYDVR0OBBYEFNtHkj6LRNuSCFCEDvWcb/vaxmOKMA0GCSqGSIb3DQEBCwUAA4IB
AQCymlwRyp0JOXJltxNG5qHLpes+juIN9p5COTs9FhvAkpC58W+w1ynkN23J1UVk
WA5G1t5BOO/eyU7lZkdabDA/Mb0hvxdWIG4a028EUkd8vgkJEeDjPbjSZDPaIdL8
RAq9lq88LcetWP9g8NOZRFBhDbigphqYmwGNzk/J1ZIYhOfsNr0SU5BU/xe4ogT1
WmcBkaPw9g+7yPsevNXNmaMPlcKRiEL9AJA4rMZSU8aLnZ4+WXRleQnhSzHdBWPx
Li2jb41I39ECmY7gXPCJqRAORcaov4AO8WZCNVKqRLd/uFmOddpBABlPN9RyiyIn
pivGrcARZfTLYxHrOpQu74gz
-----END CERTIFICATE-----
@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDJjCCAg6gAwIBAgIUA+GJ9S/l9MoO3gPga8JwUYNpaeYwDQYJKoZIhvcNAQEL
BQAwGjEYMBYGA1UEAwwPVHJ1c3QgVGVzdCBSb290MCAXDTI2MTAwODE3MTk0M1oY
DzIxMjYwOTE0MTcxOTQzWjAZMRcwFQYDVQQDDA5UcnVzdCBUZXN0IEludDCCASIw
DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKTTaf/jUVe80O44xs2+jRQSnv7w
zKiGMUnDMTwLheS3NlOuXUJAPfggtOUmp8fWdLUi9tmvwO/jSjOZZLnQ2fz9bGiV
Oovs9y9TgywdxOH1nnCMQ+hKLw0cMdk8X2Pm8wGZdGEblEcPJBXFF09JX1t6y3h6
cHlV5tpxdAUjNNdxjREnXclixWUPBDF4IziRrg4fOi+aB5FFREhhKLGlvJJ0NNq5
t9N3quFHoQCAGvwrnQQXsey58zRF58yK1AS6VrNMvkYRhcs/N+5W8Cxb6hVhthhz
0n96GgJ1BcGwpSGPC/69WpZHQot9VrT6qUoKJHUg3fnTHyzodJcRkHyAsKUCAwEA
AaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE
FGa9d7RPsqY3+fnUCK3+sbIHKdCZMB8GA1UdIwQYMBaAFDl9W8k1XKQCubIn46G/
oYwjeDkqMA0GCSqGSIb3DQEBCwUAA4IBAQDJsifCCoydUHayqGMitWtCnAhts+hz
bOtYafchsnsmgn+LyetPC5ghdocn7QBHFZqmwoSfmZG2xrS5YfwSNJH5a65meFF1
Ca0eqxo0KZcNt3YSMH5bEOf1aphw9ame/UqNB89ZjJjSoIF2/Us/Yr2uLO0LLBmK
01HF1JqWFIXUOZ7g0VPhR1ulV7apYFvgXUtj0A9PUWOcJb+lxrepU4uWbV4TN0BM
EveNKX91kIzujemL0L1LDKpqU76lUG9sPAVrareKBDsDP9h4xGOJLzsybK0tpFHS
5rXCepsRIh2h9liL4ofxyIrD3bMKlSd/mo5hC6EaaHCLh4XtRuw8Iw93
-----END CERTIFICATE-----
@@ -0,0 +1,35 @@
-----BEGIN CERTIFICATE-----
MIIGFDCCA/ygAwIBAgIUKH3ODOPG96qjP/ll526pjIJKWdswDQYJKoZIhvcNAQEL
BQAwVjE4MDYGA1UEAxMvSW5mb3JtYXRpb24gU2VjdXJpdHkgQ2VydGlmaWNhdGlv
biBBdXRob3JpdHkgQ0ExDTALBgNVBAoTBElTQ0ExCzAJBgNVBAYTAktaMB4XDTIw
MDIyODA0MDgwM1oXDTQwMDIyODA0MDgwM1owVjE4MDYGA1UEAxMvSW5mb3JtYXRp
b24gU2VjdXJpdHkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgQ0ExDTALBgNVBAoT
BElTQ0ExCzAJBgNVBAYTAktaMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC
AgEA2Q8hjYT89iYfD+gdxiaKw5NNRq0sfKDj1xAXbtmhQh+tA5DhnVPehqFybVFH
ZBsyMwmL9SA4Bp/2ytE7CmnDefqg9xfRuNX2p6RzUgrsarYaZgXgaPW+g/TZ+nXU
uGgYb1I01XOg5xhFr65cjIM5DX5L9tr90tzRE1afjeUyvT0AYkSodQzJmweQG1nz
WQ9INEf//smQf6Dvguwe6pCZye8RcwbdH8P+1U5uHtmIKGSWVRzX347M2TGREtOe
arAblsfmB75t/eIracWIP0+goQSf8O942GrhBpqlMHYhPYn1ALYpgJVTp/mlIYtl
GtDDEeu8LHbF5z0orEBLp+x5aAbqbwIUXdvIejehFS669Mxb2p5K9XDbFWJJ5osn
6pAKFdTx7oo0HvGQP6vVEdsHkzZMenhj8U76jDlJKLAZi2Rww3Usfy0obssPT1Iu
R8JLjqpVgwXwKMaeLsGf4GUkqBLNsj72fNK5NGCJWeRged0G+jqaP2/Lf2UCbMxV
pAmoAciNdVexY9ytH+wPXLLKbGPIKAKPqepF3vhX4yHoL2Q+gR9Hx194r6hRdfHV
AKknDoG6/MWWzaqEieec28oXCNZwXdkPWJyssa1E6W1fojL/xhdvmiHUFUeR3rgs
xiMOdxvhFusKbuHjYpGwb9/D6+D7hUQGlSZ/8UMW3hfWiJcCAwEAAaOB2TCB1jAP
BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUqH3ODOPG
96qjP/ll526pjIJKWdswgZMGA1UdIwSBizCBiIAUqH3ODOPG96qjP/ll526pjIJK
WduhWqRYMFYxODA2BgNVBAMTL0luZm9ybWF0aW9uIFNlY3VyaXR5IENlcnRpZmlj
YXRpb24gQXV0aG9yaXR5IENBMQ0wCwYDVQQKEwRJU0NBMQswCQYDVQQGEwJLWoIU
KH3ODOPG96qjP/ll526pjIJKWdswDQYJKoZIhvcNAQELBQADggIBAD3Qw2GXqOJ8
BUqdN382O6KwdDljJzVhgxkOzcC6avfisoZ+Vhj74dy7qoMmQ84M+yXnA7BCCzHr
wBYsDKUwLT/WiHKcjToFtsejMteofiU/M0/ljigoI4Fbcl5w0tl8C47wAm5R2qAJ
sU4re9npst1z+AX4Ll7BurgsBKtTvA3PtZtRlsPm3zd7xS2xCGRaybXdMkLYWDRZ
J6R+A/iK1NM7UzAeEYpCyhDZo/OCrFgNWDpqwVTiOe3CPqM3aEBPPbohTbjMV9Gj
L0g7m1j2BI+X3ByHvTEYqZia36HOgmy4jrOuMsWeSqXw/RjTINBWtOzQDnm7Enrq
RpjR1kpugkAzNvGFSEUvkj7moKo++ixH46iOjSNGexPx21ktQijUXAvQ9ETO9EsJ
jmUNOiTd+iRjUEnACaGn+od99CsgEXBDq9EtP65KedzXIy5MTu6fMULSSLaQdzLh
Vd827PEzgMvNCj45pqVisVJ6l+N73J7yLrtq6Ad6BQ1N+jC0MMiiYn2r7E5Gt54j
+GzQMZgHgdoHbk6tiUw3UXrM8CGzaHJhSu/g/oUkGU0EDKlduHigTNCuHLX9uuvw
gNglHhejbQE8ai7ApOwoEmZMXBH9B+oFkwU5CSN1AoNDiQIUlzqS2mv41GU7q2iX
k3zu7DCQingYbxGab/J8R/e9aTMNhD8W
-----END CERTIFICATE-----
@@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIUEPlNJxZrwg60AMmYq4fl4iKEp6MwDQYJKoZIhvcNAQEL
BQAwGTEXMBUGA1UEAwwOVHJ1c3QgVGVzdCBJbnQwIBcNMjYxMDA4MTcxOTQ0WhgP
MjEyNjA5MTQxNzE5NDRaMAwxCjAIBgNVBAMMAXgwggEiMA0GCSqGSIb3DQEBAQUA
A4IBDwAwggEKAoIBAQDh/WlixnDuMipvbyvaXShC9Oa6b8fG7focttANGZtwwCpX
+TkEO6NTRvlwOBCdj1zGZEPuEQDZtbVgyz0DIxCuDK8sdlY7UgZjUY9ifThKhAmA
ux7CC2UcDLnr7P16ixVd3z1ev14zWqkyl0wZPG6udBoIyLUo/DzLj8j/YQQC0+Zt
JxGrtY2mwYbwyj0OFafkVrWGLe8EJZV8IrV7N0+OGGUi9ZHuvzbtgP4fc8xJj7Md
zQtxa2vA+Uq02PHfR+iYyAFow1taltelKV+mbfVJypy6RgQEnyIlEPpACMXD4HoU
QLG4dZAca0KFzdgI1IsxTSeC5SJxKk0RYp3ElyfJAgMBAAGjgacwgaQwDAYDVR0T
AQH/BAIwADAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwHwYD
VR0jBBgwFoAUZr13tE+ypjf5+dQIrf6xsgcp0JkwLwYDVR0RBCgwJoIMaG9zdC5l
eGFtcGxlhwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMB0GA1UdDgQWBBRgGEvI6VZt
KCBl88e6Icmez0rXPjANBgkqhkiG9w0BAQsFAAOCAQEABG/ol4K9If/YAfPGGqfn
7FVzGQOjgviCaWpQEQueBAmSLDSjgCyN4pRoZuFwIjzc0m0UkL8fQdS2ypF9dtHq
dIFCZRlBugqpTwzLvuPwJk6pPcPkwqpBB3jIJo25PFo6LokH/sA9sNnndprLmlwA
6iBC8wh3Nn81ge9Go2tM/yueOHAlFkSIMUXlq2/qPkRPSk/I9S9K94ESML4mLVY3
SQ/ysv6G3Djjucey2Nasm5JJukzjgcUd047Yrnw88SDpO/jcmpoGwuCfmqMTr5gS
pBo8L1D4yJaG1CQM/OLlOBnDQrAyCUZ696nVQH/U5zJhjTR3wdzt9UeyE9kQEOJr
RA==
-----END CERTIFICATE-----
@@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDQzCCAiugAwIBAgIUA+GJ9S/l9MoO3gPga8JwUYNpaekwDQYJKoZIhvcNAQEL
BQAwGjEYMBYGA1UEAwwPVHJ1c3QgVGVzdCBSb290MCAXDTI2MTAwODE3MTk0NFoY
DzIxMjYwOTE0MTcxOTQ0WjARMQ8wDQYDVQQDDAZOQyBJbnQwggEiMA0GCSqGSIb3
DQEBAQUAA4IBDwAwggEKAoIBAQDBpGTjp6pklNYbUn+TAOFFojcQOry16ECScxx7
VGZKHMYxBa1/9LCoAeS/z81CaiPIK6dv6kHOvKh3OA4VJinfoT03MPFo77tBtgie
Fgyt7GgROHMQHjjnSHCeMrv4UgF7qRgUMDpBOa+mXHx+gw1hLGSajnHMByQYjPSn
FUdn99c3GfA435sInwsneK3Z6K6w9dnaGfa5/cNcGs/LdFRARuBzutVFm0ZFz44E
nVoMeHppw3OBb4ZEA18Gn8cERW06+yA4/De98dzo8gPVt3IMwjx4Y5TPX5xm8JeI
vofKiHPofvTHnBlqCzvUeOxUafQUUKaexyygooiiB4NEymwfAgMBAAGjgYcwgYQw
DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHWJVS3H
Wh0c4hnE52bOWg2s/aGvMB8GA1UdIwQYMBaAFDl9W8k1XKQCubIn46G/oYwjeDkq
MCEGA1UdHgEB/wQXMBWgEzARgg9hbGxvd2VkLmV4YW1wbGUwDQYJKoZIhvcNAQEL
BQADggEBAKkFGxFm6cibc6Y853pNWoDN30fvkbt1e8YGf/Ws+E7e18wf9JM+HxfU
cCF5NFI9rZAScaLPifcoewxRnO0lqMtkNYk52/iJYE8WHkcn0/sCxluuHpD5HOVI
7YEbniFcPGBhTM/62An1FGtsCFd02vjP6dZ1m79fxgONYVKsMpJoiGZsYeScFixE
J0qpqXsJ8cfvhH0FkxQeSX28PHRKV9cay2ElmP8yAelpA9DGeowcE67KeIa2GH0E
75DIZlqa8+HTYSXGZOBKlEdPtLWtluGS/ewhDNN8jTmWu4rxPLvUU4VcnoOxHWri
WUK2t8zRGPlReSvQhSwZC54hBJx4e+c=
-----END CERTIFICATE-----
@@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDPTCCAiWgAwIBAgIUetjuFkPGZNknh/9qYSrAMnZYVfAwDQYJKoZIhvcNAQEL
BQAwETEPMA0GA1UEAwwGTkMgSW50MCAXDTI2MTAwODE3MTk0NFoYDzIxMjYwOTE0
MTcxOTQ0WjAMMQowCAYDVQQDDAF4MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
CgKCAQEAyhDyS5OTWf9yoZbBFCY69Em5fa2AVKSXnjQsLn/LRSfPrMyXp0doKdmI
gmhNsaFcEFMxyavyD8V2uMfpgg7QMEoEvRXZXdQkNPpT1k2WRPtkkQReig9vOtsc
sKL4oK3uU099jFaVOC5CmDUDElHTdTK83/WP2x2cLoPi2cfj7vwrmc2y9x2LGkzC
pOoKsDYEFMRZ2cyffSiudC1uT8kYKI8c32AO9hDwJF1sEyaqIzTVBh4P3zOw/g6O
dsWSSxFIHd+pbFjMXQSIePIpMSMuYVs5Pgz8mMuEJSNnWsfr6a5phEiezyM4rcMs
MZ6NCUqK9XThsxLXGw1QVPatbyTXLQIDAQABo4GPMIGMMAwGA1UdEwEB/wQCMAAw
DgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB8GA1UdIwQYMBaA
FHWJVS3HWh0c4hnE52bOWg2s/aGvMBcGA1UdEQQQMA6CDGhvc3QuZXhhbXBsZTAd
BgNVHQ4EFgQUqEPPZG734QvkFZT6gtaRMTCcK2owDQYJKoZIhvcNAQELBQADggEB
AEy+iqqtgRXNk2CCClBLi2HrU2dBjr6FdbYNWPyWZ2FrvQigiAsbkzxql4bKlE6Q
jRGhf57nELD3W5jVbkh57X6Km5cGQcyBhPvpZzYjbMk9dDOWeyd8zyxnrZsOaXoo
Q0djdYu+uyqcBMvz4OfZn+GDybXRdwzY5PgeSSjRMbDhR7XIIMOiBmOv1A6sJoGC
6qqoo4nQPKrCGHBBNVv8sy78ZUTgDs2vGX9+Sx0VUyrAlrXLDF/6vxgjhTCEED6p
FX3agZtNVgkbj/B3I3sML4DzDZ+SMtlyyeOhuJgdWkI5YnptfDfhtPG1G1SHTBvL
u03WVlBWeiYY6to1/OD7SGc=
-----END CERTIFICATE-----
@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDITCCAgmgAwIBAgIUA+GJ9S/l9MoO3gPga8JwUYNpaeswDQYJKoZIhvcNAQEL
BQAwGjEYMBYGA1UEAwwPVHJ1c3QgVGVzdCBSb290MCAXDTI2MTAwODE3MTk0NFoY
DzIxMjYwOTE0MTcxOTQ0WjARMQ8wDQYDVQQDDAZQTCBJbnQwggEiMA0GCSqGSIb3
DQEBAQUAA4IBDwAwggEKAoIBAQCvpCVepJ8mvuB1zxlU2n9jMSDCDEDydVkBvcKW
fQQr2e8OWKcQ6y86DQ8ZCyLNWe+L4pZTMyV1Wq/T0jIo7vEJ3PfJjzcjaiT6yW+k
ZhxuaA4bWtnt8OArTNXRThTavkw1Ef7euhYL1degRZUxDJEpm86zNnwfv6YBRwoF
y6F1oh4Z7nTJyPs9xc63MrgATOltJ3t2gHjAOXdRyWglUBYRae2IWofgQScSk7jT
0T1KwD2ce5Q6RvCNpuhyXlzvZ6y9YcnijY1EjV5I0rnFwIV3u1PQQJt5v6RxL4rL
g2uJrnAhnKGBGonhl4QcpUw0nazcGa5jMHh8iZv9p18qf23fAgMBAAGjZjBkMBIG
A1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBQ2CJRo
uL2JbMe/bo3463fl8YjK/zAfBgNVHSMEGDAWgBQ5fVvJNVykArmyJ+Ohv6GMI3g5
KjANBgkqhkiG9w0BAQsFAAOCAQEAhyqPUkysJw0CV07uTU39sdYnYS6LyDqFKEwE
1ToZ+1u4pL5QZRo8yJ65psR0+9w8NTsSnmKS3bgkTqxq08VsC/P5x/wZFeL02JAg
4mzfDn8Xb/FxACQDs5QafRyU6MXHhHI0m6ndS5EhLnWu/IPKcoUAxPWstlLe6ixR
VXY7FXjiraiU1smT97tHxbd2woVwaoJxPlaK1NupLMYY8KGNTOHBn6gLMWspdl/J
bi/3JkNc5E9TCILFBAtWH9sWw3/PY8Mb7ir7k88DWHq3HJIDSYTDt0tOpdUfAke4
M/QdhHDS2pFQYCZxtSwK3RZ3OQHTfcZY7L2ospNTIsjsujGLZw==
-----END CERTIFICATE-----
@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDFjCCAf6gAwIBAgIUI9bf0eiAxYZ4G3OLjOVETtL3aOQwDQYJKoZIhvcNAQEL
BQAwETEPMA0GA1UEAwwGUEwgSW50MCAXDTI2MTAwODE3MTk0NVoYDzIxMjYwOTE0
MTcxOTQ1WjASMRAwDgYDVQQDDAdQTCBJbnQyMIIBIjANBgkqhkiG9w0BAQEFAAOC
AQ8AMIIBCgKCAQEAzeWnMybz8DmgEmziIs39MNQsnWvmqnhh7q10kDdOZDwV0v7C
rdDCQ6vPf3jaLt79jMvWURAQz1zJU4bpNsYw53+yqrraNw999jPyKI8k6ZJw1QPs
Na3VBxnulZb4sc8HQYD1RMXDrGeJwE07+AVFw1ej8FrWRsERLd5O4/gY2+eW1Sxi
aXkjzgA/iYpZW3p4+KKHGT93sseU7N7Alm54DgcHiksRGoQPwBSempty0p17f/R5
PlvQX4/MmmLcpreEcOAiVtJDTf6qq8zXaogaCGvyEjYb/m4wfdRkuPBp03T9JA1I
VWugtXPqp+2oR/XDF9O2ZD9VlysagJNctoY5OQIDAQABo2MwYTAPBgNVHRMBAf8E
BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUkR09mkxkUjE/r7WNfrt1
T3fICNIwHwYDVR0jBBgwFoAUNgiUaLi9iWzHv26N+Ot35fGIyv8wDQYJKoZIhvcN
AQELBQADggEBAJXeXPehboZGaAV85eEfasMW9cd17zNXFh7/WWE+9b7Rwz+5q6RY
G4yMizRFlSqorBiCidN9lKBqmCEETEkMjD1UI8iXQWXcMBHSUI9xvfdJiZ+I7xrV
Cjq4A5KJeKhayjlMTb27GlUV1g+irdh7g/rCDtN84LRGOMhpKZTcd6DerQCx6zrA
QxCP3vMexoyYh9Rv/90K7CO7mSXuplPGUX2zV6TUhwTR09M4+J+quvA32aamc7Wc
mtqoRTnG8HPh5ey2hjKJAjrbAA2edGDWaQb+0uOQyFhtJAHRpoGu6Mv7bt4Kk5Jb
A/WlHsdIlt1q1lS0iOCSLOZVaiaJB9y2Ce4=
-----END CERTIFICATE-----
@@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDPjCCAiagAwIBAgIUTTXPR16Bluu+wyeTi8GeJHAm+4QwDQYJKoZIhvcNAQEL
BQAwEjEQMA4GA1UEAwwHUEwgSW50MjAgFw0yNjEwMDgxNzE5NDVaGA8yMTI2MDkx
NDE3MTk0NVowDDEKMAgGA1UEAwwBeDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
AQoCggEBALXpnV/pQz+nAooMhg52ANUSOZup3lVVRoSoqGiEZRoBUqEM9tVRbd9W
u2ZlWGqdquvxmR1IZtqvgW3cjp1+Fr4eoi/GCbGPXgdiNMVnmjP54M+qY93hMriR
TYA3KHVXhYR/XUnVuXsgHRnIeYC2yPx/W+pn5A58tQqODvPp/AFMCi/nWlitCOI+
/UpMzuYVBm+vzAxBBCY8t/7MnTp7IyccxIShrDf9AHcGG2FPm9ZHZMSUeL7Dqnx+
OXxZcGGSZfOdJRfIqUoOkO6KJqnha34SMSke3pj35IeTS9FLGecGY1UaWnN44SlO
IJgKze7MNXNNxxCKONjsnGsYzSJg01kCAwEAAaOBjzCBjDAMBgNVHRMBAf8EAjAA
MA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAfBgNVHSMEGDAW
gBSRHT2aTGRSMT+vtY1+u3VPd8gI0jAXBgNVHREEEDAOggxob3N0LmV4YW1wbGUw
HQYDVR0OBBYEFEJ84ALu2SsxcnbD5p7C4pLStNcVMA0GCSqGSIb3DQEBCwUAA4IB
AQC4O6dQxcm6Grf4Q//XMo4zMBI/HcosD7Pqt8ToMfOMlmcpV5y9iuWD04K4yrrJ
KqhNdzqtuIPw/tPwDXfFX/vw2n3wvUXx2W4AP2951FnC5dWuE+CxgQA9kXjiqOiw
pZIUAqO+hjXfIswOtocMxmwCKvCbibh6DS4p+GMhDXFYPiBx60155DenUtfdfxps
0xgs42n/+UzesZhF+cUqvEtF8VrfkP2SpHrJXwxaII/0Yc0yqxCfd5xMAUw+ccP2
FWoBMm4QSPI62OmXSXk3lGjn8zZAABilPONqo2nsSv2HcOH82OqvleloRVXnVIMK
58Dib6o7IcPlgsnoH/tKoy7J
-----END CERTIFICATE-----
@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDBjCCAe6gAwIBAgIUU93kzExCdrzi3lnMnhSZgjnowIswDQYJKoZIhvcNAQEL
BQAwGjEYMBYGA1UEAwwPVHJ1c3QgVGVzdCBSb290MCAXDTI2MTAwODE3MTk0M1oY
DzIxMjYwOTE0MTcxOTQzWjAaMRgwFgYDVQQDDA9UcnVzdCBUZXN0IFJvb3QwggEi
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDc4SkwOK60qqk09EjIPTRGvSnH
sOPJu0A1+IfRKsdfdMk6pTXL1Xm01nOLLqbsW4oz3rRfFQJyZPOSpPzzkCAdjKfp
y8KtKUZjzlnz8ADOoaeGLCnCyELw09anO/JsbnDv0snPMInYS5R2Onq5+M6qlEdH
iSe9uG0qiWx0OGXBkoDs+zF+Buc5ilYEKVNHZBho3V3qUoFjKKEE2O9DyKJ1qL+v
KgvTg0Z6KPjiyvlkkMRdn2UCHsJl5XTxU2dLLbs99F3/GSGbVjk+UXcUcJp4ZpA/
/oQd99g4MYqKGDJIItFYQ0WQDTk1s2rEzqaOKS9DA83eXCwgvDBfOGaY+HCjAgMB
AAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQW
BBQ5fVvJNVykArmyJ+Ohv6GMI3g5KjANBgkqhkiG9w0BAQsFAAOCAQEAZKD6bPnD
8l6xoyaai0AhqEx03MK1rBLiji2iK65FznWOFlRJCJX6MH5fJbGyafCHiM7fkTOw
BX2hY/hXC8Yg4AtsOvf6hRNYHEy0Z0Nj7qfCY9BpJXHzMvPLkJqqJB0UJisr5EDk
FKNiBiXftwbAUeUXuzncfviKbXroHlT5oLHBvApb6jhxIPhb7dBrbpN2urRfMzZu
ezbK+jWUB89h79xupK4ph7B8zFY5swiIZ3bjXDFqlNDzyksgzqqWOoZqyI3FoCJY
msORGKupBklLJgm78AjDJdu1/lhGfLflM9pspYi7A8Kct6fQerw2SJ+5xoFw9WKz
CUKllHzH9kJnew==
-----END CERTIFICATE-----
@@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDRjCCAi6gAwIBAgIUA+GJ9S/l9MoO3gPga8JwUYNpae0wDQYJKoZIhvcNAQEF
BQAwGjEYMBYGA1UEAwwPVHJ1c3QgVGVzdCBSb290MCAXDTI2MTAwODE3MTk0NVoY
DzIxMjYwOTE0MTcxOTQ1WjAMMQowCAYDVQQDDAF4MIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEAj3JW39sTg/nPYlrrd1wcXpP4Ya2WmjizYVfXIujTj+ud
mOYFUM373ZCPo+VgyT4HP4DM6sq4p3vO6EuuVzyJ8soeFEnrDeikeQAnQbKw/2Zt
3S87iFMurZ34pMAWDauxUxVcONnwuf7meNME4aQoBWjPHbcGWke/Ri3xPNHKOODQ
3nt3HHr6wIBALOJXnUMWWRjovn4GcUxO0O1OGghr6taoI2MCsrBGbvv2BZ4TXtVd
anz5sQx1l9cAi9HlvnqpcIl07mkpvJ4nXf8wlmESZPpgrlKmI17R3CFhtoI0lhNW
WCx5KetPhgKYQmqHYCF5REdV0gjNbQqyyIv2KEnGTQIDAQABo4GPMIGMMAwGA1Ud
EwEB/wQCMAAwDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB8G
A1UdIwQYMBaAFDl9W8k1XKQCubIn46G/oYwjeDkqMBcGA1UdEQQQMA6CDGhvc3Qu
ZXhhbXBsZTAdBgNVHQ4EFgQU0IoQbg1eN6pJMBqiJndYE2R3Z4gwDQYJKoZIhvcN
AQEFBQADggEBACgpJLT7wIynXT7UIO9ysQZZHICrTW92oopBh6CxnwzJuLsTjG4g
bQEnGO6NHQIBC3SCilyaEC9tDfDIpjJwfzyX4DuH/7UfAA6+xnJWxbaUFfAssiAG
DGTChKZlmZvRhf7V415QKKbEWeQz9I+j+X47at6aki/64BAmMw7ErDPHQukg74kB
zQvNFvkpSVcO6dgbYKP/+F/x3drAierIvJkfhcox/HoZSwcgKGDp2VFcfEKxUK91
PkyDw78ONQANiM53PNSAjfSRVzRzW3PsPjNiF+OHGg+FNp6Cp8VbS6fSyhN6qf2q
RXlF1J0mj226t9l9v7JkWfjTkiktRBnJoXU=
-----END CERTIFICATE-----
@@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDZTCCAk2gAwIBAgIUA+GJ9S/l9MoO3gPga8JwUYNpae8wDQYJKoZIhvcNAQEL
BQAwGjEYMBYGA1UEAwwPVHJ1c3QgVGVzdCBSb290MCAXDTI2MTAwODE3MjAyNFoY
DzIxMjYwOTE0MTcyMDI0WjAMMQowCAYDVQQDDAF4MIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEApbYcbze7NDc8K8nCy4yoCy++oUFvoppPJpPaPb7SpRRD
0AHISMpE09/yXr2+NNVf80yzhmsSfuej7cq4TMYN2m6cZUVe9r+lCZC7QztfpZdO
9UZLWv/S53rWcLA/rtH2yg5Q1DRgt2uoY/FlCmMJt3cgrr3H7R9vCtQUrhSayGyY
PfPwyrwkL6Rja3ztkKPlco83CgqRnWMOwG6gpxcqTAcyzH1WAcyDkTWmhkr+1G50
oL852GdVV+kO0rLNAI4+bPdsbPOKk4HikaTHHE0SQV78eb0Z5X/D0wFGu1iGopJ/
BBGSym2LYibMbNLvkaWrnAT2vdVP1n/6afcJxAjj+wIDAQABo4GuMIGrMAwGA1Ud
EwEB/wQCMAAwDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB8G
A1UdIwQYMBaAFDl9W8k1XKQCubIn46G/oYwjeDkqMDYGA1UdEQQvMC2CEioubGFu
LmhvbWUuZXhhbXBsZYIOKi5ob21lLmV4YW1wbGWCByouY28udWswHQYDVR0OBBYE
FNDwHsBO73UfLTCvRi/q/XHb0o6dMA0GCSqGSIb3DQEBCwUAA4IBAQDLXwJYrYI+
efdLGjymEXT0CR/M67x6BIQKB8w0onIGyz26WedaTX+LKuji+WGgenY/AkIEC6AL
T4rv6dBVjxHv4+x8TgnzU+aWNOEEdDWNeaXc8sb+3ZZzm5mkPooVWfVdnej9i/G1
WHjAFo/p0RHs613UuGRoZ6ZCmMomNYs9nwFoz6EmHUseARwj4dCAkdih+Z64j3NR
610hnoRsirKk/Ihjcn8RxUTVrBA+N6RkbIJByCeNzyhNzSmfNi8XvyvCqjjiUNon
b83rGCcF+V0px7NaZU8QwUJPDDekcUo/k8mQ+LjjLEAHByTJ9+Ap+3lhRuv21DWg
kJIAoFUaST7C
-----END CERTIFICATE-----
@@ -0,0 +1,17 @@
-----BEGIN CERTIFICATE-----
MIICwjCCAaqgAwIBAgIUA+GJ9S/l9MoO3gPga8JwUYNpae4wDQYJKoZIhvcNAQEL
BQAwGjEYMBYGA1UEAwwPVHJ1c3QgVGVzdCBSb290MCAXDTI2MTAwODE3MTk0NVoY
DzIxMjYwOTE0MTcxOTQ1WjAMMQowCAYDVQQDDAF4MIGfMA0GCSqGSIb3DQEBAQUA
A4GNADCBiQKBgQDWzciJ+1PQ5RoweAE4xbNqaEnbwIyOR0+NwdUt02tcjezWlQfy
A9hWb/DkETcfw6LQx3u458vmBXM1YVmnYAhnd2P8OIwlQ0Se2xQ17iKJdpoa7IQ/
aTkF1yjBvFxD8DoDRojpVupPI+mh5wg+wCCvgznX+fpZYkXEPorTFeDe2QIDAQAB
o4GPMIGMMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoG
CCsGAQUFBwMBMB8GA1UdIwQYMBaAFDl9W8k1XKQCubIn46G/oYwjeDkqMBcGA1Ud
EQQQMA6CDGhvc3QuZXhhbXBsZTAdBgNVHQ4EFgQU/l8az3OuVEjRNlmpfraqM0+p
NT8wDQYJKoZIhvcNAQELBQADggEBANJhIs3MqR/U3CZshOZSduYdDz3xXUbqg5K6
/z6EynXzF+DW26LiwZcWa3bJxfi15NbQ3urMu9UueyX2JJ1tffRNRnwWEPRG6qZ5
oD4y7hyrPvbPSVMCGTZJPX1mBi1Fo0ijPNtrziujNw9xEuFL/3Exan3xelJJpg8J
jLdVYzvzgzqT/m0Q1gy9ceRb6VJhtUN1ingYLyUvRwiih64pLLslBwvluEGrHqZ8
f6JmpvxKX5kjaWXH8M6ZjgJofmD2vaCDjnb3Y7dZAMNlxMpDXhW0hxQEjycnh3dZ
cpLB7DwofmR/ov/wE/tVmjA+lqR6Fp8fpyy5vtvGCeVMnJv32/I=
-----END CERTIFICATE-----
+7 -1
View File
@@ -58,6 +58,7 @@ import {
import {recordDesktopLastRoute} from '@electron/main/DesktopLastRoute';
import {cleanupDesktopOutboundHTTP} from '@electron/main/DesktopOutboundHTTP';
import {registerDesktopRuntimeConfigHandlers} from '@electron/main/DesktopRuntimeConfigIpc';
import {installDesktopSystemTrustVerifier} from '@electron/main/DesktopSystemTrustVerifier';
import {destroyDesktopTray, hasActiveDesktopTray, initializeDesktopTray} from '@electron/main/DesktopTray';
import {registerDisplayMediaHandlers} from '@electron/main/DisplayMedia';
import {initializeDockMenu} from '@electron/main/DockMenu';
@@ -119,7 +120,7 @@ import {
DesktopLegacyImportPhase,
readDesktopLegacyImportPhase,
} from '@fluxer/desktop_ipc/src/StorageContract';
import {app, dialog, ipcMain, netLog, shell} from 'electron';
import {app, dialog, ipcMain, netLog, session, shell} from 'electron';
import log from 'electron-log';
log.transports.file.level = 'info';
@@ -395,6 +396,11 @@ if (launchConfigurationError) {
log.error('[Init] Failed to configure the host resolver:', error);
}
await runStartupPhaseAsync('launch-net-log', startLaunchNetLog);
try {
runStartupPhase('system-trust', () => installDesktopSystemTrustVerifier(session.defaultSession));
} catch (error) {
log.error('[Init] Failed to install the system trust verifier:', error);
}
try {
await runStartupPhaseAsync('desktop-debug-info', async () => {
logDesktopDebugInfo(await getDesktopDebugInfo(userDataConfig.base, {nativeProbes: false}));