Skip to content

Commit 0adb91b

Browse files
author
Shaun Murphy
committed
feat(macos): add mTLS client certificate and CA pinning support
Add two new WebViewBuilder methods for mutual TLS authentication: - `with_client_certificate(p12_data, password)`: provide a PKCS#12 client certificate for TLS client authentication. Uses SecPKCS12Import to extract the identity in memory without keychain access. - `with_trusted_ca(der_data)`: pin a custom CA certificate for server trust evaluation. Uses SecTrustSetAnchorCertificates to trust the CA without importing it into the system trust store. Both methods avoid keychain operations and user prompts (Touch ID, password dialogs). The certificate data is stored in memory and used directly during WKNavigationDelegate authentication challenges. Currently implemented for macOS/iOS. Windows/Linux/Android store the data for future use. Closes #1706
1 parent 7c1a31d commit 0adb91b

5 files changed

Lines changed: 287 additions & 0 deletions

File tree

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,11 @@ objc2-foundation = { version = "0.3.0", default-features = false, features = [
157157
"NSValue",
158158
"NSRange",
159159
"NSRunLoop",
160+
"NSURLAuthenticationChallenge",
161+
"NSURLCredential",
162+
"NSURLProtectionSpace",
163+
"NSURLSession",
164+
"NSArray",
160165
] }
161166

162167
[target.'cfg(target_os = "ios")'.dependencies]

src/lib.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -808,6 +808,39 @@ struct WebViewAttributes<'a> {
808808
/// behavior will be disabled.
809809
/// - **macOS / Linux / Android / iOS**: Unsupported and ignored.
810810
pub general_autofill_enabled: bool,
811+
812+
/// PKCS#12 client certificate data for mTLS (mutual TLS) authentication.
813+
///
814+
/// When set, the WebView will present this client certificate during TLS handshakes
815+
/// that request client authentication. The data should be a valid PKCS#12 (.p12/.pfx)
816+
/// bundle containing the client certificate and private key.
817+
///
818+
/// The certificate is extracted in memory via platform-specific APIs during
819+
/// authentication challenges. No keychain or certificate store access is needed.
820+
///
821+
/// ## Platform-specific
822+
///
823+
/// - **macOS / iOS**: Uses `SecPKCS12Import` in the `WKNavigationDelegate`.
824+
/// - **Windows / Linux / Android**: Not yet supported.
825+
pub client_certificate_p12: Option<Vec<u8>>,
826+
827+
/// Password for the PKCS#12 client certificate bundle.
828+
///
829+
/// Required when [`client_certificate_p12`](Self::client_certificate_p12) is set.
830+
/// Use an empty string if the bundle has no password.
831+
pub client_certificate_password: Option<String>,
832+
833+
/// DER-encoded CA certificate for server trust pinning.
834+
///
835+
/// When set, the WebView will trust servers presenting certificates signed by this CA,
836+
/// even if the CA is not in the system trust store. This is useful for self-signed
837+
/// certificates without requiring system-level certificate installation or user prompts.
838+
///
839+
/// ## Platform-specific
840+
///
841+
/// - **macOS / iOS**: Uses `SecTrustSetAnchorCertificates` in the `WKNavigationDelegate`.
842+
/// - **Windows / Linux / Android**: Not yet supported.
843+
pub trusted_ca_certificate: Option<Vec<u8>>,
811844
}
812845

813846
impl Default for WebViewAttributes<'_> {
@@ -851,6 +884,9 @@ impl Default for WebViewAttributes<'_> {
851884
background_throttling: None,
852885
javascript_disabled: false,
853886
general_autofill_enabled: true,
887+
client_certificate_p12: None,
888+
client_certificate_password: None,
889+
trusted_ca_certificate: None,
854890
}
855891
}
856892
}
@@ -1444,6 +1480,42 @@ impl<'a> WebViewBuilder<'a> {
14441480
self
14451481
}
14461482

1483+
/// Set a PKCS#12 (.p12/.pfx) client certificate for mTLS authentication.
1484+
///
1485+
/// The WebView will present this certificate when a server requests client
1486+
/// authentication during the TLS handshake. The bundle should contain the
1487+
/// client certificate and its private key. The certificate is extracted
1488+
/// in memory; no keychain or certificate store access is needed.
1489+
///
1490+
/// ## Platform-specific
1491+
///
1492+
/// - **macOS / iOS**: Handled via `SecPKCS12Import` in the navigation delegate.
1493+
/// - **Windows / Linux / Android**: Not yet supported, data is stored for future use.
1494+
pub fn with_client_certificate(
1495+
mut self,
1496+
p12_data: impl Into<Vec<u8>>,
1497+
password: impl Into<String>,
1498+
) -> Self {
1499+
self.attrs.client_certificate_p12 = Some(p12_data.into());
1500+
self.attrs.client_certificate_password = Some(password.into());
1501+
self
1502+
}
1503+
1504+
/// Set a DER-encoded CA certificate for server trust pinning.
1505+
///
1506+
/// The WebView will trust servers presenting certificates signed by this CA,
1507+
/// even if the CA is not in the system trust store. This avoids the need for
1508+
/// system-level certificate installation or user authentication prompts.
1509+
///
1510+
/// ## Platform-specific
1511+
///
1512+
/// - **macOS / iOS**: Handled via `SecTrustSetAnchorCertificates` in the navigation delegate.
1513+
/// - **Windows / Linux / Android**: Not yet supported, data is stored for future use.
1514+
pub fn with_trusted_ca(mut self, der_data: impl Into<Vec<u8>>) -> Self {
1515+
self.attrs.trusted_ca_certificate = Some(der_data.into());
1516+
self
1517+
}
1518+
14471519
/// Consume the builder and create the [`WebView`] from a type that implements [`HasWindowHandle`].
14481520
///
14491521
/// # Platform-specific:

src/wkwebview/class/wry_navigation_delegate.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ use std::sync::{Arc, Mutex};
66

77
use objc2::{define_class, msg_send, rc::Retained, runtime::NSObject, MainThreadOnly};
88
use objc2_foundation::{MainThreadMarker, NSObjectProtocol};
9+
#[cfg(target_os = "macos")]
10+
use objc2_foundation::{
11+
NSURLAuthenticationChallenge, NSURLCredential, NSURLSessionAuthChallengeDisposition,
12+
};
913
use objc2_web_kit::{
1014
WKDownload, WKNavigation, WKNavigationAction, WKNavigationActionPolicy, WKNavigationDelegate,
1115
WKNavigationResponse, WKNavigationResponsePolicy,
@@ -37,6 +41,9 @@ pub struct WryNavigationDelegateIvars {
3741
pub download_delegate: Option<Retained<WryDownloadDelegate>>,
3842
pub on_page_load_handler: Option<Box<dyn Fn(PageLoadEvent)>>,
3943
pub on_web_content_process_terminate_handler: Option<Box<dyn Fn()>>,
44+
pub client_certificate_p12: Option<Vec<u8>>,
45+
pub client_certificate_password: Option<String>,
46+
pub trusted_ca_certificate: Option<Vec<u8>>,
4047
}
4148

4249
define_class!(
@@ -102,6 +109,21 @@ define_class!(
102109
fn web_content_process_did_terminate(&self, webview: &WKWebView) {
103110
web_content_process_did_terminate(self, webview);
104111
}
112+
113+
#[cfg(target_os = "macos")]
114+
#[unsafe(method(webView:didReceiveAuthenticationChallenge:completionHandler:))]
115+
fn did_receive_authentication_challenge(
116+
&self,
117+
_webview: &WKWebView,
118+
challenge: &NSURLAuthenticationChallenge,
119+
handler: &block2::Block<
120+
dyn Fn(NSURLSessionAuthChallengeDisposition, *mut NSURLCredential),
121+
>,
122+
) {
123+
crate::wkwebview::navigation_auth::did_receive_authentication_challenge(
124+
self, challenge, handler,
125+
);
126+
}
105127
}
106128
);
107129

@@ -115,6 +137,9 @@ impl WryNavigationDelegate {
115137
download_delegate: Option<Retained<WryDownloadDelegate>>,
116138
on_page_load_handler: Option<Box<dyn Fn(PageLoadEvent, String)>>,
117139
on_web_content_process_terminate_handler: Option<Box<dyn Fn()>>,
140+
client_certificate_p12: Option<Vec<u8>>,
141+
client_certificate_password: Option<String>,
142+
trusted_ca_certificate: Option<Vec<u8>>,
118143
mtm: MainThreadMarker,
119144
) -> Retained<Self> {
120145
let navigation_policy_function = Box::new(move |url: String| -> bool {
@@ -151,6 +176,9 @@ impl WryNavigationDelegate {
151176
download_delegate,
152177
on_page_load_handler,
153178
on_web_content_process_terminate_handler,
179+
client_certificate_p12,
180+
client_certificate_password,
181+
trusted_ca_certificate,
154182
});
155183

156184
unsafe { msg_send![super(delegate), init] }

src/wkwebview/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ mod download;
66
#[cfg(target_os = "macos")]
77
mod drag_drop;
88
mod navigation;
9+
mod navigation_auth;
910
#[cfg(feature = "mac-proxy")]
1011
mod proxy;
1112
#[cfg(target_os = "macos")]
@@ -590,6 +591,9 @@ impl InnerWebView {
590591
download_delegate.clone(),
591592
attributes.on_page_load_handler,
592593
pl_attrs.on_web_content_process_terminate_handler,
594+
attributes.client_certificate_p12,
595+
attributes.client_certificate_password,
596+
attributes.trusted_ca_certificate,
593597
mtm,
594598
);
595599

src/wkwebview/navigation_auth.rs

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
// Copyright 2020-2024 Tauri Programme within The Commons Conservancy
2+
// SPDX-License-Identifier: Apache-2.0
3+
// SPDX-License-Identifier: MIT
4+
5+
//! Authentication challenge handling for mTLS (mutual TLS) connections.
6+
7+
use objc2::runtime::AnyObject;
8+
use objc2::msg_send;
9+
use objc2::rc::Retained;
10+
use objc2::DeclaredClass;
11+
use objc2_foundation::{
12+
NSData, NSString,
13+
NSURLAuthenticationChallenge, NSURLCredential,
14+
NSURLSessionAuthChallengeDisposition,
15+
};
16+
17+
use super::class::wry_navigation_delegate::WryNavigationDelegate;
18+
19+
#[link(name = "Security", kind = "framework")]
20+
extern "C" {
21+
fn SecCertificateCreateWithData(
22+
allocator: *const std::ffi::c_void,
23+
data: *const AnyObject,
24+
) -> *mut std::ffi::c_void;
25+
fn SecTrustSetAnchorCertificates(
26+
trust: *const std::ffi::c_void,
27+
anchors: *const AnyObject,
28+
) -> i32;
29+
fn SecTrustSetAnchorCertificatesOnly(
30+
trust: *const std::ffi::c_void,
31+
only: bool,
32+
) -> i32;
33+
fn SecTrustEvaluateWithError(
34+
trust: *const std::ffi::c_void,
35+
error: *mut *mut std::ffi::c_void,
36+
) -> bool;
37+
fn SecPKCS12Import(
38+
pkcs12: *const AnyObject,
39+
options: *const AnyObject,
40+
items: *mut *mut AnyObject,
41+
) -> i32;
42+
}
43+
44+
pub(crate) fn did_receive_authentication_challenge(
45+
delegate: &WryNavigationDelegate,
46+
challenge: &NSURLAuthenticationChallenge,
47+
handler: &block2::Block<
48+
dyn Fn(NSURLSessionAuthChallengeDisposition, *mut NSURLCredential),
49+
>,
50+
) {
51+
unsafe {
52+
let protection_space = challenge.protectionSpace();
53+
let auth_method = protection_space.authenticationMethod();
54+
55+
let server_trust_method = NSString::from_str("NSURLAuthenticationMethodServerTrust");
56+
let client_cert_method = NSString::from_str("NSURLAuthenticationMethodClientCertificate");
57+
58+
// Server trust challenge: pin CA cert if provided
59+
if auth_method.isEqualToString(&server_trust_method) {
60+
if let Some(ref ca_der) = delegate.ivars().trusted_ca_certificate {
61+
let ns_data = NSData::with_bytes(ca_der);
62+
let ca_cert = SecCertificateCreateWithData(
63+
std::ptr::null(),
64+
Retained::as_ptr(&ns_data) as *const AnyObject,
65+
);
66+
67+
if !ca_cert.is_null() {
68+
let server_trust: *const std::ffi::c_void =
69+
msg_send![&*protection_space, serverTrust];
70+
if !server_trust.is_null() {
71+
let cert_obj = ca_cert as *mut AnyObject;
72+
let array: Retained<AnyObject> = msg_send![
73+
objc2::runtime::AnyClass::get(c"NSArray").unwrap(),
74+
arrayWithObject: cert_obj
75+
];
76+
SecTrustSetAnchorCertificates(
77+
server_trust,
78+
Retained::as_ptr(&array) as *const AnyObject,
79+
);
80+
SecTrustSetAnchorCertificatesOnly(server_trust, true);
81+
82+
let mut error: *mut std::ffi::c_void = std::ptr::null_mut();
83+
if SecTrustEvaluateWithError(server_trust, &mut error) {
84+
let credential: *mut NSURLCredential = msg_send![
85+
objc2::runtime::AnyClass::get(c"NSURLCredential").unwrap(),
86+
credentialForTrust: server_trust
87+
];
88+
handler.call((
89+
NSURLSessionAuthChallengeDisposition::UseCredential,
90+
credential,
91+
));
92+
return;
93+
}
94+
}
95+
}
96+
}
97+
98+
// Fallback: accept server trust from system store
99+
let server_trust: *const std::ffi::c_void =
100+
msg_send![&*protection_space, serverTrust];
101+
if !server_trust.is_null() {
102+
let credential: *mut NSURLCredential = msg_send![
103+
objc2::runtime::AnyClass::get(c"NSURLCredential").unwrap(),
104+
credentialForTrust: server_trust
105+
];
106+
handler.call((
107+
NSURLSessionAuthChallengeDisposition::UseCredential,
108+
credential,
109+
));
110+
} else {
111+
handler.call((
112+
NSURLSessionAuthChallengeDisposition::PerformDefaultHandling,
113+
std::ptr::null_mut(),
114+
));
115+
}
116+
return;
117+
}
118+
119+
// Client certificate challenge: extract identity from PKCS#12 data
120+
if auth_method.isEqualToString(&client_cert_method) {
121+
if let Some(ref p12_data) = delegate.ivars().client_certificate_p12 {
122+
let password = delegate
123+
.ivars()
124+
.client_certificate_password
125+
.as_deref()
126+
.unwrap_or("");
127+
let ns_data = NSData::with_bytes(p12_data);
128+
let ns_password = NSString::from_str(password);
129+
130+
// kSecImportExportPassphrase = "passphrase"
131+
let passphrase_key = NSString::from_str("passphrase");
132+
let options: Retained<AnyObject> = msg_send![
133+
objc2::runtime::AnyClass::get(c"NSDictionary").unwrap(),
134+
dictionaryWithObject: &*ns_password,
135+
forKey: &*passphrase_key
136+
];
137+
138+
let mut items: *mut AnyObject = std::ptr::null_mut();
139+
let status = SecPKCS12Import(
140+
Retained::as_ptr(&ns_data) as *const AnyObject,
141+
Retained::as_ptr(&options) as *const AnyObject,
142+
&mut items,
143+
);
144+
145+
if status == 0 && !items.is_null() {
146+
let count: usize = msg_send![items, count];
147+
if count > 0 {
148+
let first: *mut AnyObject = msg_send![items, objectAtIndex: 0usize];
149+
// kSecImportItemIdentity = "identity"
150+
let identity_key = NSString::from_str("identity");
151+
let identity: *mut std::ffi::c_void =
152+
msg_send![first, objectForKey: &*identity_key];
153+
154+
if !identity.is_null() {
155+
let credential: *mut NSURLCredential = msg_send![
156+
objc2::runtime::AnyClass::get(c"NSURLCredential").unwrap(),
157+
credentialWithIdentity: identity,
158+
certificates: std::ptr::null::<AnyObject>(),
159+
persistence: 0isize // NSURLCredentialPersistenceNone
160+
];
161+
handler.call((
162+
NSURLSessionAuthChallengeDisposition::UseCredential,
163+
credential,
164+
));
165+
return;
166+
}
167+
}
168+
}
169+
}
170+
}
171+
172+
// Default handling for all other challenges
173+
handler.call((
174+
NSURLSessionAuthChallengeDisposition::PerformDefaultHandling,
175+
std::ptr::null_mut(),
176+
));
177+
}
178+
}

0 commit comments

Comments
 (0)