Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions bindings/matrix-sdk-ffi/src/client_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -134,10 +136,22 @@ pub struct ClientBuilder {
disable_built_in_root_certificates: bool,
#[cfg(not(target_family = "wasm"))]
additional_root_certificates: Vec<Vec<u8>>,
#[cfg(all(not(target_family = "wasm"), feature = "native-tls"))]
client_certificate: Option<ClientCertificate>,

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<u8>,
/// 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.
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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<Self>,
certificate_data: Vec<u8>,
password: String,
) -> Arc<Self> {
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<Self>, user_agent: String) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
#[cfg(not(target_family = "wasm"))]
Expand Down
38 changes: 38 additions & 0 deletions crates/matrix-sdk/src/client/builder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down
12 changes: 12 additions & 0 deletions crates/matrix-sdk/src/http_client/native.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -149,6 +151,8 @@ pub(crate) struct HttpSettings {
pub(crate) read_timeout: Option<Duration>,
pub(crate) additional_root_certificates: Vec<Certificate>,
pub(crate) disable_built_in_root_certificates: bool,
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
pub(crate) client_identity: Option<Identity>,
}

#[cfg(not(target_family = "wasm"))]
Expand All @@ -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,
}
}
}
Expand Down Expand Up @@ -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()?)
}
}
Expand Down
Loading