Skip to content

feat(client): support custom ServerCertVerifier in TlsOptions#1271

Open
brucearctor wants to merge 5 commits into
temporalio:mainfrom
brucearctor:feat/rustls-client-config
Open

feat(client): support custom ServerCertVerifier in TlsOptions#1271
brucearctor wants to merge 5 commits into
temporalio:mainfrom
brucearctor:feat/rustls-client-config

Conversation

@brucearctor
Copy link
Copy Markdown
Contributor

Summary

Adds an optional server_cert_verifier field to TlsOptions that accepts an Arc<dyn ServerCertVerifier>. When set, the client uses tonic's tls_config_with_verifier() to replace the default WebPKI certificate verification.

Closes #1248

Motivation

The current TlsOptions API only supports static PEM-encoded certificates, which prevents advanced TLS patterns like:

  • Certificate pinning — verifying a specific certificate fingerprint
  • Custom trust-domain validation — e.g., SAN-URI extraction for SPIFFE/mesh environments
  • Federated root certificate stores — composing multiple trust roots from different sources

What this PR does

  1. Adds server_cert_verifier: Option<Arc<dyn ServerCertVerifier>> to TlsOptions
  2. Updates add_tls_to_channel to call Endpoint::tls_config_with_verifier() when the custom verifier is set
  3. Re-exports ServerCertVerifier from the crate root for user convenience
  4. Handles Debug impldyn ServerCertVerifier doesn't implement Debug, so TlsOptions gets a manual Debug impl

What this does NOT cover

  • ResolvesClientCert / dynamic client cert rotation — tonic 0.14 does not support injecting a pre-built Arc<rustls::ClientConfig>, so ResolvesClientCert cannot be passed through. This would require either:
    • An upstream change to tonic to accept Arc<rustls::ClientConfig> directly
    • Bypassing tonic's TLS config entirely to build the hyper/rustls connector manually

I scoped this PR to what tonic 0.14 supports today. If the maintainers want the full Arc<ClientConfig> passthrough, I'm happy to pursue the tonic upstream approach as a follow-up.

Design decisions

  • Optional field vs enum: Chose an optional field on the existing struct over an enum wrapper to avoid breaking the public API. An enum-based ClientTlsConfig could be a follow-up if preferred.
  • Feature-gating: Not feature-gated since tokio-rustls is already a transitive dependency via tonic. The ServerCertVerifier trait is always available when tonic's TLS features are enabled.
  • server_root_ca_cert ignored when verifier is set: This matches tonic's behavior — tls_config_with_verifier errors if CA certs are also configured.

Example usage

use std::sync::Arc;
use temporalio_client::{TlsOptions, ServerCertVerifier};

// Implement your custom verifier
struct MyCertPinner { /* ... */ }
impl ServerCertVerifier for MyCertPinner { /* ... */ }

let tls = TlsOptions {
    server_cert_verifier: Some(Arc::new(MyCertPinner { /* ... */ })),
    domain: Some("my-temporal.example.com".to_string()),
    ..Default::default()
};

Testing

  • cargo check — full workspace builds clean
  • cargo fmt --all — no formatting changes
  • cargo lint — no warnings
  • cargo test -p temporalio-client — 112 tests + 1 doctest pass

@brucearctor brucearctor requested a review from a team as a code owner May 14, 2026 03:25
@brucearctor brucearctor force-pushed the feat/rustls-client-config branch from 4946144 to d3f3150 Compare May 14, 2026 03:42
Comment thread crates/client/src/lib.rs Outdated
Comment on lines +416 to +418
// When a custom verifier is set, server_root_ca_cert is ignored because
// tonic's tls_config_with_verifier replaces the default WebPKI verifier entirely.
if tls_cfg.server_cert_verifier.is_none() {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably just have these options be mutually exclusive (in the sense that this can just produce an error) rather than silently ignoring.

Comment thread crates/client/src/lib.rs Outdated
Comment on lines +1552 to +1594
#[test]
fn tls_options_with_verifier_default_is_none() {
let opts = TlsOptions::default();
assert!(opts.server_cert_verifier.is_none());
}

#[test]
fn tls_options_with_verifier_clone() {
let opts = TlsOptions {
server_cert_verifier: Some(Arc::new(MockVerifier)),
..Default::default()
};
let cloned = opts.clone();
assert!(cloned.server_cert_verifier.is_some());
}

#[test]
fn tls_options_debug_shows_custom_when_verifier_set() {
let opts = TlsOptions {
server_cert_verifier: Some(Arc::new(MockVerifier)),
domain: Some("test.example.com".to_string()),
..Default::default()
};
let debug_str = format!("{opts:?}");
assert!(
debug_str.contains("<custom>"),
"Debug output should show <custom> for verifier, got: {debug_str}"
);
assert!(
debug_str.contains("test.example.com"),
"Debug output should show domain, got: {debug_str}"
);
}

#[test]
fn tls_options_debug_shows_none_when_no_verifier() {
let opts = TlsOptions::default();
let debug_str = format!("{opts:?}");
assert!(
debug_str.contains("server_cert_verifier: None"),
"Debug output should show None for verifier, got: {debug_str}"
);
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these can all just be dropped, very obvious tests.

Comment on lines +150 to +153
/// This is useful for:
/// - Certificate pinning
/// - Custom trust-domain validation (e.g., SAN-URI extraction)
/// - Federated root certificate stores
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to include some warning here about the fact that this allows you to make dangerously insecure connections

Comment thread crates/client/src/lib.rs Outdated
pub use retry::RetryOptions;
/// Re-export the `ServerCertVerifier` trait so that users can implement custom TLS
/// server certificate verification without depending on `tokio-rustls` directly.
pub use tokio_rustls::rustls::client::danger::ServerCertVerifier;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I want to re-export this specifically because this import path is communicating some important information with the danger segment. I think a little friction here isn't a bad thing.

Add an optional `server_cert_verifier` field to `TlsOptions` that
accepts an `Arc<dyn ServerCertVerifier>`. When set, the client uses
tonic's `tls_config_with_verifier()` to replace the default WebPKI
certificate verification.

This enables:
- Certificate pinning
- Custom trust-domain validation (e.g., SAN-URI extraction)
- Federated root certificate stores

When a custom verifier is set, `server_root_ca_cert` is ignored
(since tonic enforces mutual exclusion), while `domain` is still
respected for the `:authority` header / origin override.

The `ServerCertVerifier` trait is re-exported from the crate root
for convenience, so users don't need to depend on `tokio-rustls`
directly.

Note: This does not cover `ResolvesClientCert` (dynamic client cert
rotation), which would require either bypassing tonic's TLS config
or upstream changes to tonic. See discussion in the linked issue.

Closes temporalio#1248
- Return InvalidConfig error when both server_root_ca_cert and
  server_cert_verifier are set (instead of silently ignoring)
- Add WARNING doc comment about insecure connections
- Move ServerCertVerifier re-export to pub mod danger to preserve
  the danger signal in the import path
- Drop trivial tests per reviewer request
@brucearctor brucearctor force-pushed the feat/rustls-client-config branch from f25841b to ef49309 Compare May 20, 2026 01:35
Comment thread crates/client/src/lib.rs Outdated
@Sushisource
Copy link
Copy Markdown
Member

@brucearctor Thanks again! Looks like there's one small rustfmt needed.

By the way, you've been making a lot of useful contributions and we really appreciate that. We have a program for highlighting top contributors, we can ship you some swag or something too. I didn't immediately see you in our community Slack. If you join, you can DM me and we can set it up! https://t.mp/slack

Alternatively feel free to find me on LinkedIn and send me a message there too. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request] Accept a pre-built rustls::ClientConfig in client TLS options

2 participants