feat(client): support custom ServerCertVerifier in TlsOptions#1271
feat(client): support custom ServerCertVerifier in TlsOptions#1271brucearctor wants to merge 5 commits into
Conversation
4946144 to
d3f3150
Compare
| // 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() { |
There was a problem hiding this comment.
We should probably just have these options be mutually exclusive (in the sense that this can just produce an error) rather than silently ignoring.
| #[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}" | ||
| ); | ||
| } |
There was a problem hiding this comment.
I think these can all just be dropped, very obvious tests.
| /// This is useful for: | ||
| /// - Certificate pinning | ||
| /// - Custom trust-domain validation (e.g., SAN-URI extraction) | ||
| /// - Federated root certificate stores |
There was a problem hiding this comment.
I think we need to include some warning here about the fact that this allows you to make dangerously insecure connections
| 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; |
There was a problem hiding this comment.
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
f25841b to
ef49309
Compare
|
@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! |
Summary
Adds an optional
server_cert_verifierfield toTlsOptionsthat accepts anArc<dyn ServerCertVerifier>. When set, the client uses tonic'stls_config_with_verifier()to replace the default WebPKI certificate verification.Closes #1248
Motivation
The current
TlsOptionsAPI only supports static PEM-encoded certificates, which prevents advanced TLS patterns like:What this PR does
server_cert_verifier: Option<Arc<dyn ServerCertVerifier>>toTlsOptionsadd_tls_to_channelto callEndpoint::tls_config_with_verifier()when the custom verifier is setServerCertVerifierfrom the crate root for user convenienceDebugimpl —dyn ServerCertVerifierdoesn't implementDebug, soTlsOptionsgets a manualDebugimplWhat this does NOT cover
ResolvesClientCert/ dynamic client cert rotation — tonic 0.14 does not support injecting a pre-builtArc<rustls::ClientConfig>, soResolvesClientCertcannot be passed through. This would require either:Arc<rustls::ClientConfig>directlyI 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
ClientTlsConfigcould be a follow-up if preferred.tokio-rustlsis already a transitive dependency via tonic. TheServerCertVerifiertrait is always available when tonic's TLS features are enabled.server_root_ca_certignored when verifier is set: This matches tonic's behavior —tls_config_with_verifiererrors if CA certs are also configured.Example usage
Testing
cargo check— full workspace builds cleancargo fmt --all— no formatting changescargo lint— no warningscargo test -p temporalio-client— 112 tests + 1 doctest pass