diff --git a/bindings/matrix-sdk-ffi/src/client_builder.rs b/bindings/matrix-sdk-ffi/src/client_builder.rs index c9c06cf4860..54751db5b04 100644 --- a/bindings/matrix-sdk-ffi/src/client_builder.rs +++ b/bindings/matrix-sdk-ffi/src/client_builder.rs @@ -5,6 +5,8 @@ use std::{num::NonZeroUsize, sync::Arc, time::Duration}; #[cfg(not(target_family = "wasm"))] use matrix_sdk::reqwest::Certificate; +#[cfg(all(not(target_family = "wasm"), feature = "native-tls"))] +use matrix_sdk::reqwest::Identity; use matrix_sdk::{ encryption::{BackupDownloadStrategy, EncryptionSettings}, event_cache::EventCacheError, @@ -134,10 +136,22 @@ pub struct ClientBuilder { disable_built_in_root_certificates: bool, #[cfg(not(target_family = "wasm"))] additional_root_certificates: Vec>, + #[cfg(all(not(target_family = "wasm"), feature = "native-tls"))] + client_certificate: Option, threading_support: ThreadingSupport, } +/// Client certificate data for mTLS authentication. +#[cfg(all(not(target_family = "wasm"), feature = "native-tls"))] +#[derive(Clone)] +struct ClientCertificate { + /// PKCS#12 certificate data in DER format. + data: Vec, + /// Password to decrypt the PKCS#12 data. + password: String, +} + /// The timeout applies to each read operation, and resets after a successful /// read. This is more appropriate for detecting stalled connections when the /// size isn’t known beforehand. @@ -167,6 +181,8 @@ impl ClientBuilder { additional_root_certificates: Default::default(), #[cfg(not(target_family = "wasm"))] disable_built_in_root_certificates: false, + #[cfg(all(not(target_family = "wasm"), feature = "native-tls"))] + client_certificate: None, encryption_settings: EncryptionSettings { auto_enable_cross_signing: false, backup_download_strategy: @@ -443,6 +459,15 @@ impl ClientBuilder { if let Some(user_agent) = builder.user_agent { inner_builder = inner_builder.user_agent(user_agent); } + + #[cfg(feature = "native-tls")] + if let Some(client_cert) = builder.client_certificate { + let identity = Identity::from_pkcs12_der(&client_cert.data, &client_cert.password) + .map_err(|e| ClientBuildError::Generic { + message: format!("Failed to parse client certificate: {e:?}"), + })?; + inner_builder = inner_builder.client_certificate(identity); + } } if !builder.disable_automatic_token_refresh { @@ -595,6 +620,34 @@ impl ClientBuilder { Arc::new(builder) } + /// Set a client certificate for mutual TLS authentication (mTLS). + /// + /// This enables mTLS by providing a PKCS#12 client certificate that will + /// be presented to the server during the TLS handshake. + /// + /// Note: This method only has an effect when the `native-tls` feature is + /// enabled. PKCS#12 client certificates are not supported with + /// `rustls-tls`. + /// + /// # Arguments + /// + /// * `certificate_data` - The PKCS#12/PFX certificate data in DER format. + /// * `password` - The password to decrypt the PKCS#12 data. + #[allow(unused_variables, unused_mut)] + pub fn client_certificate( + self: Arc, + certificate_data: Vec, + password: String, + ) -> Arc { + let mut builder = unwrap_or_clone_arc(self); + #[cfg(all(not(target_family = "wasm"), feature = "native-tls"))] + { + builder.client_certificate = + Some(ClientCertificate { data: certificate_data, password }); + } + Arc::new(builder) + } + pub fn user_agent(self: Arc, user_agent: String) -> Arc { let mut builder = unwrap_or_clone_arc(self); #[cfg(not(target_family = "wasm"))] diff --git a/crates/matrix-sdk/src/client/builder/mod.rs b/crates/matrix-sdk/src/client/builder/mod.rs index f9d0f5d7700..f373a6df5fa 100644 --- a/crates/matrix-sdk/src/client/builder/mod.rs +++ b/crates/matrix-sdk/src/client/builder/mod.rs @@ -381,6 +381,44 @@ impl ClientBuilder { self } + /// Set a client certificate (identity) for mutual TLS authentication. + /// + /// This enables mTLS by providing a client certificate that will be + /// presented to the server during the TLS handshake. + /// + /// Internally this will call the + /// [`reqwest::ClientBuilder::identity()`] method. + /// + /// # Arguments + /// + /// * `identity` - A [`reqwest::Identity`] containing the client certificate + /// and private key. See [`reqwest::Identity`] documentation for the + /// available constructors for different TLS backends. + /// + /// # Examples + /// + /// ```ignore + /// use std::fs; + /// use matrix_sdk::Client; + /// + /// // With rustls-tls: use from_pkcs8_pem with separate cert and key + /// let cert_pem = fs::read("client-cert.pem")?; + /// let key_pem = fs::read("client-key.pem")?; + /// let identity = reqwest::Identity::from_pkcs8_pem(&cert_pem, &key_pem)?; + /// + /// // With native-tls: use from_pkcs12_der with PKCS#12 bundle + /// // let p12_data = fs::read("client.p12")?; + /// // let identity = reqwest::Identity::from_pkcs12_der(&p12_data, "password")?; + /// + /// let client_config = Client::builder().client_certificate(identity); + /// # anyhow::Ok(()) + /// ``` + #[cfg(all(not(target_family = "wasm"), any(feature = "native-tls", feature = "rustls-tls")))] + pub fn client_certificate(mut self, identity: reqwest::Identity) -> Self { + self.http_settings().client_identity = Some(identity); + self + } + /// Specify a [`reqwest::Client`] instance to handle sending requests and /// receiving responses. /// diff --git a/crates/matrix-sdk/src/http_client/native.rs b/crates/matrix-sdk/src/http_client/native.rs index 3109f1397de..6d89344f125 100644 --- a/crates/matrix-sdk/src/http_client/native.rs +++ b/crates/matrix-sdk/src/http_client/native.rs @@ -24,6 +24,8 @@ use bytes::Bytes; use bytesize::ByteSize; use eyeball::SharedObservable; use http::header::CONTENT_LENGTH; +#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] +use reqwest::Identity; use reqwest::{Certificate, tls}; use ruma::api::{IncomingResponse, OutgoingRequest, error::FromHttpResponseError}; use tracing::{debug, info, warn}; @@ -149,6 +151,8 @@ pub(crate) struct HttpSettings { pub(crate) read_timeout: Option, pub(crate) additional_root_certificates: Vec, pub(crate) disable_built_in_root_certificates: bool, + #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] + pub(crate) client_identity: Option, } #[cfg(not(target_family = "wasm"))] @@ -162,6 +166,8 @@ impl Default for HttpSettings { read_timeout: None, additional_root_certificates: Default::default(), disable_built_in_root_certificates: false, + #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] + client_identity: None, } } } @@ -211,6 +217,12 @@ impl HttpSettings { http_client = http_client.proxy(reqwest::Proxy::all(p.as_str())?); } + #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] + if let Some(identity) = &self.client_identity { + info!("Setting client identity for mTLS"); + http_client = http_client.identity(identity.clone()); + } + Ok(http_client.build()?) } }