mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 04:55:42 +09:00
fix(desktop): trust CAs in the windows intermediate store (#3494)
This commit is contained in:
1 parent
4f88872a3c
commit
4b8ce8bf63
8 files changed
+290
-9
No files matched your search
+1
@@ -149,6 +149,7 @@ dependencies = [
|
||||
"napi-derive",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"schannel",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"url",
|
||||
|
||||
@@ -23,3 +23,6 @@ url = "2.5.8"
|
||||
|
||||
[build-dependencies]
|
||||
napi-build = "2.4.3"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
schannel = "0.1.29"
|
||||
@@ -33,4 +33,5 @@ export declare const connect:
|
||||
) => NativeGatewayConnection)
|
||||
| null;
|
||||
export declare const warmup: (() => void) | null;
|
||||
export declare const windowsIntermediateStoreRoots: (() => Array<Buffer>) | null;
|
||||
export declare const loadError: Error | null;
|
||||
@@ -58,5 +58,9 @@ try {
|
||||
module.exports = {
|
||||
connect: binding ? binding.connect : null,
|
||||
warmup: binding && typeof binding.warmup === 'function' ? binding.warmup : null,
|
||||
windowsIntermediateStoreRoots:
|
||||
binding && typeof binding.windowsIntermediateStoreRoots === 'function'
|
||||
? binding.windowsIntermediateStoreRoots
|
||||
: null,
|
||||
loadError,
|
||||
};
|
||||
@@ -639,14 +639,13 @@ fn ensure_rustls_crypto_provider() {
|
||||
fn build_tls_client_config() -> std::result::Result<Arc<rustls::ClientConfig>, String> {
|
||||
ensure_rustls_crypto_provider();
|
||||
let native = rustls_native_certs::load_native_certs();
|
||||
if native.certs.is_empty() {
|
||||
let roots = trust_roots(native.certs, &intermediate_store_roots());
|
||||
if roots.is_empty() {
|
||||
return Err(format!(
|
||||
"no native root CA certificates found (errors: {:?})",
|
||||
"no root CA certificates found (errors: {:?})",
|
||||
native.errors
|
||||
));
|
||||
}
|
||||
let mut roots = rustls::RootCertStore::empty();
|
||||
roots.add_parsable_certificates(native.certs);
|
||||
Ok(Arc::new(
|
||||
rustls::ClientConfig::builder()
|
||||
.with_root_certificates(roots)
|
||||
@@ -654,6 +653,118 @@ fn build_tls_client_config() -> std::result::Result<Arc<rustls::ClientConfig>, S
|
||||
))
|
||||
}
|
||||
|
||||
fn trust_roots(
|
||||
native: Vec<rustls::pki_types::CertificateDer<'static>>,
|
||||
extra: &[rustls::pki_types::CertificateDer<'static>],
|
||||
) -> rustls::RootCertStore {
|
||||
let mut roots = rustls::RootCertStore::empty();
|
||||
roots.add_parsable_certificates(native);
|
||||
roots.add_parsable_certificates(extra.iter().cloned());
|
||||
roots
|
||||
}
|
||||
|
||||
#[cfg(any(windows, test))]
|
||||
fn der_element(input: &[u8]) -> Option<(u8, &[u8], &[u8])> {
|
||||
let (&tag, rest) = input.split_first()?;
|
||||
let (&first, rest) = rest.split_first()?;
|
||||
let (length, rest) = if first & 0x80 == 0 {
|
||||
(usize::from(first), rest)
|
||||
} else {
|
||||
let size = usize::from(first & 0x7f);
|
||||
if size == 0 || size > 4 || rest.len() < size {
|
||||
return None;
|
||||
}
|
||||
let (bytes, rest) = rest.split_at(size);
|
||||
(
|
||||
bytes
|
||||
.iter()
|
||||
.fold(0usize, |length, &byte| (length << 8) | usize::from(byte)),
|
||||
rest,
|
||||
)
|
||||
};
|
||||
if rest.len() < length {
|
||||
return None;
|
||||
}
|
||||
let (content, rest) = rest.split_at(length);
|
||||
Some((tag, content, rest))
|
||||
}
|
||||
|
||||
#[cfg(any(windows, test))]
|
||||
fn is_self_issued(certificate: &[u8]) -> bool {
|
||||
let Some((0x30, certificate, [])) = der_element(certificate) else {
|
||||
return false;
|
||||
};
|
||||
let Some((0x30, tbs, _)) = der_element(certificate) else {
|
||||
return false;
|
||||
};
|
||||
let mut fields = std::iter::successors(der_element(tbs), |&(_, _, rest)| der_element(rest))
|
||||
.map(|(tag, content, _)| (tag, content));
|
||||
let mut serial = fields.next();
|
||||
if matches!(serial, Some((0xa0, _))) {
|
||||
serial = fields.next();
|
||||
}
|
||||
let (
|
||||
Some((0x02, _)),
|
||||
Some((0x30, _)),
|
||||
Some((0x30, issuer)),
|
||||
Some((0x30, _)),
|
||||
Some((0x30, subject)),
|
||||
) = (
|
||||
serial,
|
||||
fields.next(),
|
||||
fields.next(),
|
||||
fields.next(),
|
||||
fields.next(),
|
||||
)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
issuer == subject
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn intermediate_store_roots() -> Vec<rustls::pki_types::CertificateDer<'static>> {
|
||||
use schannel::{cert_context::ValidUses, cert_store::CertStore};
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
const SERVER_AUTH: &str = "1.3.6.1.5.5.7.3.1";
|
||||
const ANY_EXTENDED_KEY_USAGE: &str = "2.5.29.37.0";
|
||||
let store_certificates = |name: &str| {
|
||||
[
|
||||
CertStore::open_current_user(name),
|
||||
CertStore::open_local_machine(name),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.flat_map(|store| store.certs().collect::<Vec<_>>())
|
||||
};
|
||||
let disallowed: BTreeSet<Vec<u8>> = store_certificates("Disallowed")
|
||||
.map(|certificate| certificate.to_der().to_vec())
|
||||
.collect();
|
||||
store_certificates("CA")
|
||||
.filter(|certificate| {
|
||||
certificate.is_time_valid().unwrap_or(false)
|
||||
&& match certificate.valid_uses() {
|
||||
Ok(ValidUses::All) => true,
|
||||
Ok(ValidUses::Oids(oids)) => oids
|
||||
.iter()
|
||||
.any(|oid| oid == SERVER_AUTH || oid == ANY_EXTENDED_KEY_USAGE),
|
||||
Err(_) => false,
|
||||
}
|
||||
})
|
||||
.map(|certificate| certificate.to_der().to_vec())
|
||||
.filter(|der| is_self_issued(der) && !disallowed.contains(der))
|
||||
.collect::<BTreeSet<_>>()
|
||||
.into_iter()
|
||||
.map(rustls::pki_types::CertificateDer::from)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn intermediate_store_roots() -> Vec<rustls::pki_types::CertificateDer<'static>> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn tls_client_config() -> std::result::Result<Arc<rustls::ClientConfig>, String> {
|
||||
TLS_CLIENT_CONFIG
|
||||
.get_or_init(build_tls_client_config)
|
||||
@@ -1045,6 +1156,14 @@ impl EventDelivery for NapiEventDelivery {
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(js_name = "windowsIntermediateStoreRoots")]
|
||||
pub fn windows_intermediate_store_roots() -> Vec<Buffer> {
|
||||
intermediate_store_roots()
|
||||
.into_iter()
|
||||
.map(|certificate| Buffer::from(certificate.to_vec()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn warmup() -> Result<()> {
|
||||
let runtime = transport_runtime()?;
|
||||
@@ -1225,6 +1344,122 @@ mod tests {
|
||||
assert_eq!(event.kind, EVENT_KIND_OPEN, "{:?}", event.message);
|
||||
}
|
||||
|
||||
const TLS_FIXTURE_HOST: &str = "ca.example";
|
||||
const TLS_FIXTURE_LEAF: &[u8] =
|
||||
include_bytes!("../../../src/main/fixtures/desktop-tls/leaf.pem");
|
||||
const TLS_FIXTURE_LEAF_KEY: &[u8] =
|
||||
include_bytes!("../../../src/main/fixtures/desktop-tls/leaf.key");
|
||||
const TLS_FIXTURE_TRUSTED_CA: &[u8] =
|
||||
include_bytes!("../../../src/main/fixtures/desktop-tls/trusted-ca.pem");
|
||||
const TLS_FIXTURE_UNTRUSTED_CA: &[u8] =
|
||||
include_bytes!("../../../src/main/fixtures/desktop-tls/untrusted-ca.pem");
|
||||
|
||||
fn fixture_certificate(pem: &[u8]) -> rustls::pki_types::CertificateDer<'static> {
|
||||
use rustls::pki_types::pem::PemObject;
|
||||
rustls::pki_types::CertificateDer::from_pem_slice(pem).expect("fixture certificate")
|
||||
}
|
||||
|
||||
fn fixture_server_config() -> Arc<rustls::ServerConfig> {
|
||||
use rustls::pki_types::pem::PemObject;
|
||||
ensure_rustls_crypto_provider();
|
||||
Arc::new(
|
||||
rustls::ServerConfig::builder()
|
||||
.with_no_client_auth()
|
||||
.with_single_cert(
|
||||
vec![fixture_certificate(TLS_FIXTURE_LEAF)],
|
||||
rustls::pki_types::PrivateKeyDer::from_pem_slice(TLS_FIXTURE_LEAF_KEY)
|
||||
.expect("fixture key"),
|
||||
)
|
||||
.expect("fixture server config"),
|
||||
)
|
||||
}
|
||||
|
||||
fn pump(
|
||||
from: &mut rustls::Connection,
|
||||
to: &mut rustls::Connection,
|
||||
) -> std::result::Result<(), rustls::Error> {
|
||||
let mut bytes = Vec::new();
|
||||
while from.wants_write() {
|
||||
from.write_tls(&mut bytes).expect("write tls");
|
||||
}
|
||||
let mut cursor = bytes.as_slice();
|
||||
while !cursor.is_empty() {
|
||||
to.read_tls(&mut cursor).expect("read tls");
|
||||
to.process_new_packets()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handshake(roots: rustls::RootCertStore) -> std::result::Result<(), rustls::Error> {
|
||||
ensure_rustls_crypto_provider();
|
||||
let client_config = rustls::ClientConfig::builder()
|
||||
.with_root_certificates(roots)
|
||||
.with_no_client_auth();
|
||||
let mut client = rustls::Connection::Client(
|
||||
rustls::ClientConnection::new(
|
||||
Arc::new(client_config),
|
||||
rustls::pki_types::ServerName::try_from(TLS_FIXTURE_HOST).expect("server name"),
|
||||
)
|
||||
.expect("client connection"),
|
||||
);
|
||||
let mut server = rustls::Connection::Server(
|
||||
rustls::ServerConnection::new(fixture_server_config()).expect("server connection"),
|
||||
);
|
||||
while client.is_handshaking() || server.is_handshaking() {
|
||||
pump(&mut client, &mut server).expect("server accepts the client flight");
|
||||
pump(&mut server, &mut client)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trust_roots_add_extra_anchors_to_the_native_store() {
|
||||
let unknown_issuer = Err(rustls::Error::InvalidCertificate(
|
||||
rustls::CertificateError::UnknownIssuer,
|
||||
));
|
||||
assert_eq!(handshake(trust_roots(Vec::new(), &[])), unknown_issuer);
|
||||
assert_eq!(
|
||||
handshake(trust_roots(
|
||||
vec![fixture_certificate(TLS_FIXTURE_UNTRUSTED_CA)],
|
||||
&[]
|
||||
)),
|
||||
unknown_issuer
|
||||
);
|
||||
assert_eq!(
|
||||
handshake(trust_roots(
|
||||
vec![fixture_certificate(TLS_FIXTURE_UNTRUSTED_CA)],
|
||||
&[fixture_certificate(TLS_FIXTURE_TRUSTED_CA)]
|
||||
)),
|
||||
Ok(())
|
||||
);
|
||||
assert_eq!(
|
||||
handshake(trust_roots(
|
||||
vec![fixture_certificate(TLS_FIXTURE_TRUSTED_CA)],
|
||||
&[]
|
||||
)),
|
||||
Ok(())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_self_issued_certificates_count_as_intermediate_store_roots() {
|
||||
assert!(is_self_issued(&fixture_certificate(TLS_FIXTURE_TRUSTED_CA)));
|
||||
assert!(is_self_issued(&fixture_certificate(
|
||||
TLS_FIXTURE_UNTRUSTED_CA
|
||||
)));
|
||||
let leaf = fixture_certificate(TLS_FIXTURE_LEAF);
|
||||
assert!(!is_self_issued(&leaf));
|
||||
let root = fixture_certificate(TLS_FIXTURE_TRUSTED_CA);
|
||||
assert!(!is_self_issued(&root[..root.len() - 1]));
|
||||
let mut trailing = root.to_vec();
|
||||
trailing.push(0);
|
||||
assert!(!is_self_issued(&trailing));
|
||||
assert!(!is_self_issued(&[]));
|
||||
assert!(!is_self_issued(&[0x30, 0x84, 0xff, 0xff, 0xff, 0xff]));
|
||||
#[cfg(not(windows))]
|
||||
assert!(windows_intermediate_store_roots().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connection_targets_reject_unusable_urls() {
|
||||
for url in [
|
||||
|
||||
@@ -66,6 +66,17 @@ describe('desktop system trust verifier', () => {
|
||||
assert.equal(buildDesktopTrustAnchors([read('kz_root')]).size, 0);
|
||||
});
|
||||
|
||||
test('anchors on DER roots read from the Windows intermediate store', () => {
|
||||
const der = (name) => new X509Certificate(read(name)).raw;
|
||||
const store = buildDesktopTrustAnchors([der('root')]);
|
||||
const request = {hostname: 'host.example', certificate: chain(['leaf', 'int']), errorCode: AUTHORITY_INVALID};
|
||||
assert.equal(
|
||||
verifyAgainstDesktopSystemTrust(request, () => store),
|
||||
ACCEPT,
|
||||
);
|
||||
assert.equal(buildDesktopTrustAnchors([der('kz_root'), Buffer.from('not a certificate')]).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) => {
|
||||
|
||||
@@ -5,6 +5,7 @@ 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 {readWindowsIntermediateStoreRoots} from '@electron/main/GatewaySocketNativeBoundary';
|
||||
import type {Certificate, Session} from 'electron';
|
||||
|
||||
const logger = createChildLogger('DesktopSystemTrustVerifier');
|
||||
@@ -398,14 +399,14 @@ function chainsToDesktopTrustAnchor(
|
||||
}
|
||||
|
||||
export function buildDesktopTrustAnchors(
|
||||
certificates: ReadonlyArray<string>,
|
||||
certificates: ReadonlyArray<string | Buffer>,
|
||||
now: number = Date.now(),
|
||||
): DesktopTrustAnchors {
|
||||
const anchors = new Map<string, Array<ParsedCertificate>>();
|
||||
for (const pem of certificates) {
|
||||
for (const certificate of certificates) {
|
||||
let anchor: ParsedCertificate;
|
||||
try {
|
||||
anchor = parseCertificate(new X509Certificate(pem));
|
||||
anchor = parseCertificate(new X509Certificate(certificate));
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
@@ -434,12 +435,23 @@ export function verifyAgainstDesktopSystemTrust(
|
||||
return CHROMIUM_DEFAULT_RESULT;
|
||||
}
|
||||
|
||||
function resolvePlatformAnchorCertificates(): ReadonlyArray<string | Buffer> {
|
||||
switch (process.platform) {
|
||||
case 'linux':
|
||||
return resolveDesktopLocallyAddedCertificates();
|
||||
case 'win32':
|
||||
return readWindowsIntermediateStoreRoots();
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function installDesktopSystemTrustVerifier(target: Session): void {
|
||||
if (process.platform !== 'linux') return;
|
||||
const anchors = buildDesktopTrustAnchors(resolveDesktopLocallyAddedCertificates());
|
||||
const anchors = buildDesktopTrustAnchors(resolvePlatformAnchorCertificates());
|
||||
if (anchors.size === 0) return;
|
||||
logger.info('Trusting locally added CAs for Chromium requests', {
|
||||
count: anchors.size,
|
||||
platform: process.platform,
|
||||
blocklist: CHROMIUM_SPKI_BLOCKLIST_SOURCE,
|
||||
});
|
||||
target.setCertificateVerifyProc((request, callback) => {
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface GatewaySocketBoundary {
|
||||
type GatewaySocketNativeModule = {
|
||||
connect: GatewaySocketConnectBinding | null;
|
||||
warmup?: (() => void) | null;
|
||||
windowsIntermediateStoreRoots?: (() => Array<Buffer>) | null;
|
||||
loadError: Error | null;
|
||||
};
|
||||
|
||||
@@ -34,6 +35,7 @@ const requireModule = createRequire(import.meta.url);
|
||||
|
||||
let bindingCache: GatewaySocketConnectBinding | null | undefined;
|
||||
let warmupCache: (() => void) | null = null;
|
||||
let windowsIntermediateStoreRootsCache: (() => Array<Buffer>) | null = null;
|
||||
|
||||
export function loadGatewaySocketBinding(): GatewaySocketConnectBinding | null {
|
||||
if (bindingCache !== undefined) return bindingCache;
|
||||
@@ -47,6 +49,8 @@ export function loadGatewaySocketBinding(): GatewaySocketConnectBinding | null {
|
||||
const nativeModule = requireModule(GATEWAY_SOCKET_ADDON_PACKAGE) as GatewaySocketNativeModule;
|
||||
bindingCache = nativeModule.connect ?? null;
|
||||
warmupCache = bindingCache === null ? null : (nativeModule.warmup ?? null);
|
||||
windowsIntermediateStoreRootsCache =
|
||||
bindingCache === null ? null : (nativeModule.windowsIntermediateStoreRoots ?? null);
|
||||
} catch (error) {
|
||||
log.warn('[NativeGateway] The native gateway addon is unavailable, using the browser transport', error);
|
||||
bindingCache = null;
|
||||
@@ -63,6 +67,16 @@ export function warmGatewaySocketTransport(): void {
|
||||
}
|
||||
}
|
||||
|
||||
export function readWindowsIntermediateStoreRoots(): ReadonlyArray<Buffer> {
|
||||
if (loadGatewaySocketBinding() === null || windowsIntermediateStoreRootsCache === null) return [];
|
||||
try {
|
||||
return windowsIntermediateStoreRootsCache();
|
||||
} catch (error) {
|
||||
log.warn('[NativeGateway] Failed to read the Windows intermediate certificate store', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function isNativeGatewayAvailable(): boolean {
|
||||
return loadGatewaySocketBinding() !== null;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user