@@ -2,6 +2,7 @@ use pyo3::exceptions::{PyException, PyRuntimeError, PyValueError};
22use pyo3:: prelude:: * ;
33use std:: collections:: HashMap ;
44use std:: str:: FromStr ;
5+ use std:: sync:: Arc ;
56use std:: time:: Duration ;
67use temporalio_client:: tonic:: {
78 self ,
@@ -11,6 +12,14 @@ use temporalio_client::{
1112 ClientKeepAliveOptions as CoreClientKeepAliveConfig , Connection , ConnectionOptions ,
1213 DnsLoadBalancingOptions , GrpcCompression , HttpConnectProxyOptions , RetryOptions ,
1314} ;
15+ use tokio_rustls:: rustls:: client:: danger:: {
16+ HandshakeSignatureValid , ServerCertVerified , ServerCertVerifier ,
17+ } ;
18+ use tokio_rustls:: rustls:: client:: WebPkiServerVerifier ;
19+ use tokio_rustls:: rustls:: crypto:: CryptoProvider ;
20+ use tokio_rustls:: rustls:: pki_types:: pem:: PemObject ;
21+ use tokio_rustls:: rustls:: pki_types:: { CertificateDer , ServerName , UnixTime } ;
22+ use tokio_rustls:: rustls:: { self , DigitallySignedStruct , RootCertStore , SignatureScheme } ;
1423use tracing:: warn;
1524use url:: Url ;
1625
@@ -48,6 +57,7 @@ struct ClientTlsConfig {
4857 domain : Option < String > ,
4958 client_cert : Option < Vec < u8 > > ,
5059 client_private_key : Option < Vec < u8 > > ,
60+ verification_server_name : Option < String > ,
5161}
5262
5363#[ derive( FromPyObject ) ]
@@ -301,8 +311,22 @@ impl TryFrom<ClientTlsConfig> for temporalio_client::TlsOptions {
301311 type Error = PyErr ;
302312
303313 fn try_from ( conf : ClientTlsConfig ) -> PyResult < Self > {
314+ let mut server_root_ca_cert = conf. server_root_ca_cert ;
315+ let server_cert_verifier = match conf. verification_server_name {
316+ None => None ,
317+ Some ( name) => {
318+ // The CA bundle is consumed by the verifier's own root store; a
319+ // custom verifier cannot be combined with roots on the connection.
320+ let ca_cert = server_root_ca_cert. take ( ) . ok_or_else ( || {
321+ PyValueError :: new_err (
322+ "Must have server root CA cert when verification server name is set" ,
323+ )
324+ } ) ?;
325+ Some ( fixed_server_name_verifier ( & name, & ca_cert) ?)
326+ }
327+ } ;
304328 Ok ( temporalio_client:: TlsOptions {
305- server_root_ca_cert : conf . server_root_ca_cert ,
329+ server_root_ca_cert,
306330 domain : conf. domain ,
307331 client_tls_options : match ( conf. client_cert , conf. client_private_key ) {
308332 ( None , None ) => None ,
@@ -318,11 +342,92 @@ impl TryFrom<ClientTlsConfig> for temporalio_client::TlsOptions {
318342 ) )
319343 }
320344 } ,
321- server_cert_verifier : None ,
345+ server_cert_verifier,
322346 } )
323347 }
324348}
325349
350+ /// Builds a standard WebPKI verifier over the given root CA bundle that
351+ /// checks the certificate against `verification_server_name` rather than the
352+ /// connection's server name, leaving SNI/`:authority` to follow the
353+ /// connected host (or `domain` when set).
354+ fn fixed_server_name_verifier (
355+ verification_server_name : & str ,
356+ ca_cert_pem : & [ u8 ] ,
357+ ) -> PyResult < Arc < dyn ServerCertVerifier > > {
358+ let certs = CertificateDer :: pem_slice_iter ( ca_cert_pem)
359+ . collect :: < Result < Vec < _ > , _ > > ( )
360+ . map_err ( |err| {
361+ PyValueError :: new_err ( format ! ( "Invalid server root CA cert PEM: {err:?}" ) )
362+ } ) ?;
363+ // Root loading and provider selection mirror tonic's default (no custom
364+ // verifier) client path: unparsable certificates in the bundle are
365+ // skipped, and the provider is the process default if one is installed,
366+ // else ring, as with the connection's `tls-ring` feature.
367+ let mut roots = RootCertStore :: empty ( ) ;
368+ roots. add_parsable_certificates ( certs) ;
369+ let provider = CryptoProvider :: get_default ( )
370+ . cloned ( )
371+ . unwrap_or_else ( || Arc :: new ( rustls:: crypto:: ring:: default_provider ( ) ) ) ;
372+ let inner = WebPkiServerVerifier :: builder_with_provider ( roots. into ( ) , provider)
373+ . build ( )
374+ . map_err ( |err| {
375+ PyValueError :: new_err ( format ! ( "Failed building certificate verifier: {err}" ) )
376+ } ) ?;
377+ let server_name = ServerName :: try_from ( verification_server_name. to_owned ( ) )
378+ . map_err ( |err| PyValueError :: new_err ( format ! ( "Invalid verification server name: {err}" ) ) ) ?;
379+ Ok ( Arc :: new ( FixedServerNameVerifier { inner, server_name } ) )
380+ }
381+
382+ /// Delegates to the standard WebPKI verifier, but verifies the certificate
383+ /// against a fixed server name instead of the connection's server name.
384+ #[ derive( Debug ) ]
385+ struct FixedServerNameVerifier {
386+ inner : Arc < WebPkiServerVerifier > ,
387+ server_name : ServerName < ' static > ,
388+ }
389+
390+ impl ServerCertVerifier for FixedServerNameVerifier {
391+ fn verify_server_cert (
392+ & self ,
393+ end_entity : & CertificateDer < ' _ > ,
394+ intermediates : & [ CertificateDer < ' _ > ] ,
395+ _server_name : & ServerName < ' _ > ,
396+ ocsp_response : & [ u8 ] ,
397+ now : UnixTime ,
398+ ) -> Result < ServerCertVerified , rustls:: Error > {
399+ self . inner . verify_server_cert (
400+ end_entity,
401+ intermediates,
402+ & self . server_name ,
403+ ocsp_response,
404+ now,
405+ )
406+ }
407+
408+ fn verify_tls12_signature (
409+ & self ,
410+ message : & [ u8 ] ,
411+ cert : & CertificateDer < ' _ > ,
412+ dss : & DigitallySignedStruct ,
413+ ) -> Result < HandshakeSignatureValid , rustls:: Error > {
414+ self . inner . verify_tls12_signature ( message, cert, dss)
415+ }
416+
417+ fn verify_tls13_signature (
418+ & self ,
419+ message : & [ u8 ] ,
420+ cert : & CertificateDer < ' _ > ,
421+ dss : & DigitallySignedStruct ,
422+ ) -> Result < HandshakeSignatureValid , rustls:: Error > {
423+ self . inner . verify_tls13_signature ( message, cert, dss)
424+ }
425+
426+ fn supported_verify_schemes ( & self ) -> Vec < SignatureScheme > {
427+ self . inner . supported_verify_schemes ( )
428+ }
429+ }
430+
326431impl From < ClientRetryConfig > for RetryOptions {
327432 fn from ( conf : ClientRetryConfig ) -> Self {
328433 RetryOptions {
0 commit comments