diff --git a/Cargo.toml b/Cargo.toml index 0689f6264..ea42287ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,10 +59,14 @@ tokio-rustls = { version = "0.26", features = [ "tls12", "ring", ], default-features = false } -rustls-platform-verifier = "0.5" rustls-pki-types = "1.11" tokio-tungstenite = { version = "0.26", features = ["rustls-tls-native-roots", "rustls-tls-webpki-roots"] } tungstenite = { version = "0.26", features = ["rustls-tls-native-roots", "rustls-tls-webpki-roots"] } +rustls-native-certs = "0.8" +webpki-roots = "1.0" + +[target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies] +rustls-platform-verifier = "0.6" [target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies] tokio-native-tls = "0.3" diff --git a/src/config.rs b/src/config.rs index 282034287..07cb571f6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,7 +5,7 @@ use std::{ net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, ops::{Deref, DerefMut}, path::{Path, PathBuf}, - sync::{Mutex, RwLock}, + sync::{atomic::AtomicBool, Mutex, RwLock}, time::{Duration, Instant, SystemTime}, }; @@ -72,6 +72,11 @@ lazy_static::lazy_static! { pub static ref BUILTIN_SETTINGS: RwLock> = Default::default(); } +#[cfg(target_os = "android")] +lazy_static::lazy_static! { + pub static ref ANDROID_RUSTLS_PLATFORM_VERIFIER_INITIALIZED: AtomicBool = AtomicBool::new(false); +} + lazy_static::lazy_static! { pub static ref APP_DIR: RwLock = Default::default(); } diff --git a/src/lib.rs b/src/lib.rs index ef99c7bae..1f2d53dcd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -57,10 +57,14 @@ pub use toml; pub use uuid; pub mod fingerprint; pub use flexi_logger; -pub mod websocket; pub mod stream; +pub mod websocket; +#[cfg(any(target_os = "android", target_os = "ios"))] +pub use rustls_platform_verifier; pub use stream::Stream; pub use whoami; +#[cfg(not(any(target_os = "macos", target_os = "windows")))] +pub mod verifier; pub type SessionID = uuid::Uuid; diff --git a/src/proxy.rs b/src/proxy.rs index e32778f21..1271c906a 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -56,7 +56,6 @@ const MAXIMUM_RESPONSE_HEADERS: usize = 16; const DEFINE_TIME_OUT: u64 = 600; pub trait IntoUrl { - // Besides parsing as a valid `Url`, the `Url` must be a valid // `http::Uri`, in that it makes sense to use in a network request. fn into_url(self) -> Result; @@ -456,14 +455,14 @@ impl Proxy { T: IntoTargetAddr<'a>, { use std::convert::TryFrom; - let verifier = rustls_platform_verifier::tls_config(); - let url_domain = self.intercept.get_domain()?; + let url_domain = self.intercept.get_domain()?; let domain = rustls_pki_types::ServerName::try_from(url_domain.as_str()) .map_err(|e| ProxyError::AddressResolutionFailed(e.to_string()))? .to_owned(); - - let tls_connector = TlsConnector::from(std::sync::Arc::new(verifier)); + let client_config = crate::verifier::client_config() + .map_err(|e| ProxyError::IoError(std::io::Error::other(e)))?; + let tls_connector = TlsConnector::from(std::sync::Arc::new(client_config)); let stream = tls_connector.connect(domain, io).await?; self.http_connect(stream, target).await } diff --git a/src/verifier.rs b/src/verifier.rs new file mode 100644 index 000000000..55b7fd99b --- /dev/null +++ b/src/verifier.rs @@ -0,0 +1,190 @@ +use crate::ResultType; +#[cfg(any(target_os = "android", target_os = "ios"))] +use rustls_pki_types::{ServerName, UnixTime}; +use std::sync::Arc; +use tokio_rustls::rustls::{self, client::WebPkiServerVerifier, ClientConfig}; +#[cfg(any(target_os = "android", target_os = "ios"))] +use tokio_rustls::rustls::{ + client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}, + DigitallySignedStruct, Error as TLSError, SignatureScheme, +}; + +/// A certificate verifier that tries a primary verifier first, +/// and falls back to a platform verifier if the primary fails. +#[cfg(any(target_os = "android", target_os = "ios"))] +#[derive(Debug)] +struct FallbackPlatformVerifier { + primary: Arc, + fallback: Arc, +} + +#[cfg(any(target_os = "android", target_os = "ios"))] +impl FallbackPlatformVerifier { + fn with_platform_fallback( + primary: Arc, + provider: Arc, + ) -> Result { + #[cfg(target_os = "android")] + if !crate::config::ANDROID_RUSTLS_PLATFORM_VERIFIER_INITIALIZED + .load(std::sync::atomic::Ordering::Relaxed) + { + return Err(TLSError::General( + "rustls-platform-verifier not initialized".to_string(), + )); + } + let fallback = Arc::new(rustls_platform_verifier::Verifier::new(provider)?); + Ok(Self { primary, fallback }) + } +} + +#[cfg(any(target_os = "android", target_os = "ios"))] +impl ServerCertVerifier for FallbackPlatformVerifier { + fn verify_server_cert( + &self, + end_entity: &rustls_pki_types::CertificateDer<'_>, + intermediates: &[rustls_pki_types::CertificateDer<'_>], + server_name: &ServerName<'_>, + ocsp_response: &[u8], + now: UnixTime, + ) -> Result { + match self.primary.verify_server_cert( + end_entity, + intermediates, + server_name, + ocsp_response, + now, + ) { + Ok(verified) => Ok(verified), + Err(primary_err) => { + match self.fallback.verify_server_cert( + end_entity, + intermediates, + server_name, + ocsp_response, + now, + ) { + Ok(verified) => Ok(verified), + Err(fallback_err) => { + log::error!( + "Both primary and fallback verifiers failed to verify server certificate, primary error: {:?}, fallback error: {:?}", + primary_err, + fallback_err + ); + Err(primary_err) + } + } + } + } + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &rustls_pki_types::CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + // Both WebPkiServerVerifier and rustls_platform_verifier use the same signature verification implementation. + // https://github.com/rustls/rustls/blob/1ee126adb3352a2dcd72420dcd6040351a6ddc1e/rustls/src/webpki/server_verifier.rs#L278 + // https://github.com/rustls/rustls/blob/1ee126adb3352a2dcd72420dcd6040351a6ddc1e/rustls/src/crypto/mod.rs#L17 + // https://github.com/rustls/rustls-platform-verifier/blob/1099f161bfc5e3ac7f90aad88b1bf788e72906cb/rustls-platform-verifier/src/verification/android.rs#L9 + // https://github.com/rustls/rustls-platform-verifier/blob/1099f161bfc5e3ac7f90aad88b1bf788e72906cb/rustls-platform-verifier/src/verification/apple.rs#L6 + self.primary.verify_tls12_signature(message, cert, dss) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &rustls_pki_types::CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + // Same implementation as verify_tls12_signature. + self.primary.verify_tls13_signature(message, cert, dss) + } + + fn supported_verify_schemes(&self) -> Vec { + // Both WebPkiServerVerifier and rustls_platform_verifier use the same crypto provider, + // so their supported signature schemes are identical. + // https://github.com/rustls/rustls/blob/1ee126adb3352a2dcd72420dcd6040351a6ddc1e/rustls/src/webpki/server_verifier.rs#L172C52-L172C85 + // https://github.com/rustls/rustls-platform-verifier/blob/1099f161bfc5e3ac7f90aad88b1bf788e72906cb/rustls-platform-verifier/src/verification/android.rs#L327 + // https://github.com/rustls/rustls-platform-verifier/blob/1099f161bfc5e3ac7f90aad88b1bf788e72906cb/rustls-platform-verifier/src/verification/apple.rs#L304 + self.primary.supported_verify_schemes() + } +} + +fn webpki_server_verifier( + provider: Arc, +) -> ResultType> { + // Load root certificates from both bundled webpki_roots and system-native certificate stores. + // This approach is consistent with how reqwest and tokio-tungstenite handle root certificates. + // https://github.com/snapview/tokio-tungstenite/blob/35d110c24c9d030d1608ec964d70c789dfb27452/src/tls.rs#L95 + // https://github.com/seanmonstar/reqwest/blob/b126ca49da7897e5d676639cdbf67a0f6838b586/src/async_impl/client.rs#L643 + let mut root_cert_store = rustls::RootCertStore::empty(); + root_cert_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); + let rustls_native_certs::CertificateResult { certs, errors, .. } = + rustls_native_certs::load_native_certs(); + if !errors.is_empty() { + log::warn!("native root CA certificate loading errors: {errors:?}"); + } + root_cert_store.add_parsable_certificates(certs); + + // Build verifier using with_root_certificates behavior (WebPkiServerVerifier without CRLs). + // Both reqwest and tokio-tungstenite use this approach. + // https://github.com/seanmonstar/reqwest/blob/b126ca49da7897e5d676639cdbf67a0f6838b586/src/async_impl/client.rs#L749 + // https://github.com/snapview/tokio-tungstenite/blob/35d110c24c9d030d1608ec964d70c789dfb27452/src/tls.rs#L127 + // https://github.com/rustls/rustls/blob/1ee126adb3352a2dcd72420dcd6040351a6ddc1e/rustls/src/client/builder.rs#L47 + // with_root_certificates creates a WebPkiServerVerifier without revocation checking: + // https://github.com/rustls/rustls/blob/1ee126adb3352a2dcd72420dcd6040351a6ddc1e/rustls/src/webpki/server_verifier.rs#L177 + // https://github.com/rustls/rustls/blob/1ee126adb3352a2dcd72420dcd6040351a6ddc1e/rustls/src/webpki/server_verifier.rs#L168 + // Since no CRL is provided (as is the case here), we must explicitly set allow_unknown_revocation_status() + // to match the behavior of with_root_certificates, which allows unknown revocation status by default. + // https://github.com/rustls/rustls/blob/1ee126adb3352a2dcd72420dcd6040351a6ddc1e/rustls/src/webpki/server_verifier.rs#L37 + // Note: build() only returns an error if the root certificate store is empty, which won't happen here. + let verifier = rustls::client::WebPkiServerVerifier::builder_with_provider( + Arc::new(root_cert_store), + provider.clone(), + ) + .allow_unknown_revocation_status() + .build() + .map_err(|e| anyhow::anyhow!(e))?; + Ok(verifier) +} + +pub fn client_config() -> ResultType { + // Use the default builder which uses the default protocol versions and crypto provider. + // The with_protocol_versions API has been removed in rustls master branch: + // https://github.com/rustls/rustls/pull/2599 + // This approach is consistent with tokio-tungstenite's usage: + // https://github.com/snapview/tokio-tungstenite/blob/35d110c24c9d030d1608ec964d70c789dfb27452/src/tls.rs#L126 + let config_builder = rustls::ClientConfig::builder(); + let provider = config_builder.crypto_provider().clone(); + let webpki_verifier = webpki_server_verifier(provider.clone())?; + #[cfg(any(target_os = "android", target_os = "ios"))] + { + match FallbackPlatformVerifier::with_platform_fallback(webpki_verifier.clone(), provider) { + Ok(fallback_verifier) => { + let config = config_builder + .dangerous() + .with_custom_certificate_verifier(Arc::new(fallback_verifier)) + .with_no_client_auth(); + Ok(config) + } + Err(e) => { + log::error!( + "Failed to create fallback verifier: {:?}, use webpki verifier instead", + e + ); + let config = config_builder + .with_webpki_verifier(webpki_verifier) + .with_no_client_auth(); + Ok(config) + } + } + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + let config = config_builder + .with_webpki_verifier(webpki_verifier) + .with_no_client_auth(); + Ok(config) + } +} diff --git a/src/websocket.rs b/src/websocket.rs index e317a2f21..011a584b3 100644 --- a/src/websocket.rs +++ b/src/websocket.rs @@ -43,8 +43,42 @@ impl WsFramedStream { .into_client_request() .map_err(|e| Error::new(ErrorKind::Other, e))?; - let (stream, _) = - timeout(Duration::from_millis(ms_timeout), connect_async(request)).await??; + let stream; + #[cfg(any(target_os = "android", target_os = "ios"))] + { + let is_wss = url_str.starts_with("wss://"); + if is_wss { + use std::sync::Arc; + use tokio_tungstenite::{connect_async_tls_with_config, Connector}; + + let connector = match crate::verifier::client_config() { + Ok(client_config) => Some(Connector::Rustls(Arc::new(client_config))), + Err(e) => { + log::warn!( + "Failed to get client config: {:?}, fallback to default connector", + e + ); + None + } + }; + let (s, _) = timeout( + Duration::from_millis(ms_timeout), + connect_async_tls_with_config(request, None, false, connector), + ) + .await??; + stream = s; + } else { + let (s, _) = + timeout(Duration::from_millis(ms_timeout), connect_async(request)).await??; + stream = s; + } + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + let (s, _) = + timeout(Duration::from_millis(ms_timeout), connect_async(request)).await??; + stream = s; + } let addr = match stream.get_ref() { MaybeTlsStream::Plain(tcp) => tcp.peer_addr()?,