diff --git a/Client/tauri-client/src-tauri/src/livekit_proxy.rs b/Client/tauri-client/src-tauri/src/livekit_proxy.rs index abbaed19..7547143e 100644 --- a/Client/tauri-client/src-tauri/src/livekit_proxy.rs +++ b/Client/tauri-client/src-tauri/src/livekit_proxy.rs @@ -68,20 +68,20 @@ impl LiveKitProxyState { // --------------------------------------------------------------------------- /// Tauri store file for certificate fingerprints (shared with ws_proxy). -const CERTS_STORE: &str = "certs.json"; +pub(crate) const CERTS_STORE: &str = "certs.json"; /// Verifies the server certificate against a known SHA-256 fingerprint. /// Reuses the fingerprint stored by ws_proxy's TOFU handshake for the same /// host, so LiveKit connections are pinned to the same certificate the user /// already trusted during WebSocket setup. #[derive(Debug)] -struct PinnedVerifier { +pub(crate) struct PinnedVerifier { /// Expected SHA-256 colon-hex fingerprint (e.g. "aa:bb:cc:..."). expected_fingerprint: String, } impl PinnedVerifier { - fn new(expected_fingerprint: String) -> Self { + pub(crate) fn new(expected_fingerprint: String) -> Self { Self { expected_fingerprint } } } @@ -155,12 +155,12 @@ impl rustls::client::danger::ServerCertVerifier for PinnedVerifier { /// Produce the cert store key matching ws_proxy's format. /// ws_proxy extracts the host from "wss://host/path" which omits port 443. /// We normalise by stripping the default ":443" suffix so the keys match. -fn cert_store_key(remote_host: &str) -> String { +pub(crate) fn cert_store_key(remote_host: &str) -> String { remote_host.strip_suffix(":443").unwrap_or(remote_host).to_string() } /// Load the stored certificate fingerprint for a host from the Tauri cert store. -fn load_stored_fingerprint( +pub(crate) fn load_stored_fingerprint( app: &tauri::AppHandle, host: &str, ) -> Result, String> { diff --git a/Client/tauri-client/src-tauri/src/update_commands.rs b/Client/tauri-client/src-tauri/src/update_commands.rs index 3956c896..932dd325 100644 --- a/Client/tauri-client/src-tauri/src/update_commands.rs +++ b/Client/tauri-client/src-tauri/src/update_commands.rs @@ -1,7 +1,10 @@ +use std::sync::Arc; use serde::Serialize; use tauri::AppHandle; use tauri_plugin_updater::UpdaterExt; +use crate::livekit_proxy::{cert_store_key, load_stored_fingerprint, PinnedVerifier}; + #[derive(Serialize)] pub struct UpdateCheckResult { pub available: bool, @@ -9,6 +12,42 @@ pub struct UpdateCheckResult { pub body: Option, } +/// Extract the host (with port if non-443) from an https:// URL for cert store lookup. +fn extract_host_for_cert_store(server_url: &str) -> Result { + let parsed = url::Url::parse(server_url) + .map_err(|e| format!("failed to parse server URL: {e}"))?; + let host = parsed.host_str() + .ok_or_else(|| "server URL has no host".to_string())?; + let port = parsed.port().unwrap_or(443); + let raw = if port == 443 { + host.to_string() + } else { + format!("{host}:{port}") + }; + Ok(cert_store_key(&raw)) +} + +/// Build a rustls ClientConfig that validates the server cert against the +/// TOFU-pinned fingerprint. Falls back to system certs if no fingerprint +/// is stored (server uses a real CA cert). +fn build_tls_config(app: &AppHandle, server_url: &str) -> Result, String> { + let store_key = extract_host_for_cert_store(server_url)?; + let fingerprint = load_stored_fingerprint(app, &store_key)?; + match fingerprint { + Some(fp) => { + let config = rustls::ClientConfig::builder() + .dangerous() + .with_custom_certificate_verifier(Arc::new(PinnedVerifier::new(fp))) + .with_no_client_auth(); + Ok(Some(config)) + } + None => { + // No TOFU fingerprint stored — use system TLS (works for CA-signed certs). + Ok(None) + } + } +} + /// Validate that a server URL is safe for the updater to connect to. fn validate_server_url(server_url: &str) -> Result<(), String> { let trimmed = server_url.trim_end_matches('/'); @@ -50,14 +89,20 @@ pub async fn check_client_update( .parse() .map_err(|e: url::ParseError| format!("bad endpoint URL: {e}"))?; - // OwnCord is self-hosted and commonly uses self-signed TLS certs. - // The updater connects to the user's own server, so accept invalid certs - // (the update artifact itself is verified via Ed25519 signature). - let updater = app + // Use TOFU-pinned certificate for self-signed servers, or system certs + // for CA-signed servers. Never blindly accept invalid certs (BUG-134). + let tls_config = build_tls_config(&app, &server_url)?; + let mut builder = app .updater_builder() .endpoints(vec![url]) - .map_err(|e| format!("failed to set endpoints: {e}"))? - .configure_client(|client| client.danger_accept_invalid_certs(true)) + .map_err(|e| format!("failed to set endpoints: {e}"))?; + if let Some(config) = tls_config { + let config = Arc::new(config); + builder = builder.configure_client(move |client| { + client.use_preconfigured_tls((*config).clone()) + }); + } + let updater = builder .build() .map_err(|e| format!("failed to build updater: {e}"))?; @@ -106,11 +151,19 @@ pub async fn download_and_install_update( .parse() .map_err(|e: url::ParseError| format!("bad endpoint URL: {e}"))?; - let updater = app + // Use TOFU-pinned certificate for self-signed servers (BUG-134). + let tls_config = build_tls_config(&app, &server_url)?; + let mut builder = app .updater_builder() .endpoints(vec![url]) - .map_err(|e| format!("failed to set endpoints: {e}"))? - .configure_client(|client| client.danger_accept_invalid_certs(true)) + .map_err(|e| format!("failed to set endpoints: {e}"))?; + if let Some(config) = tls_config { + let config = Arc::new(config); + builder = builder.configure_client(move |client| { + client.use_preconfigured_tls((*config).clone()) + }); + } + let updater = builder .build() .map_err(|e| format!("failed to build updater: {e}"))?;