feat(rustls): implement TlsAcceptCallbacks support for rustls backend#833
feat(rustls): implement TlsAcceptCallbacks support for rustls backend#833jsulmont wants to merge 5 commits intocloudflare:mainfrom
Conversation
TlsSettings::with_callbacks() was previously unimplemented and returned an error. This change adds full callback support to the rustls TLS path: - TlsRef now carries peer certificates and cipher suite name - TlsStream::build_tls_ref() extracts session state after handshake - handshake_with_callback() passes populated TlsRef to callbacks - Added set_certificate_chain_file/set_private_key_file setters - Added test_handshake_complete_callback integration test
…nSSL backend with_single_cert() runs webpki validation on the server's own certificate chain, rejecting certs with unrecognized critical extensions. Replace with CertifiedKey::new() + with_cert_resolver() which loads the cert+key without upfront validation, matching how the OpenSSL backend behaves. Also re-exports sign::CertifiedKey, ResolvesServerCert, and ClientHello from pingora-rustls for downstream use.
Add ConnectorOptions::server_cert_verifier to allow users to provide a custom rustls ServerCertVerifier for upstream connections. When set, the connector bypasses webpki for CA loading, server cert validation, and client cert validation — all three of which reject certificates with unrecognized critical extensions. Changes: - ConnectorOptions: add server_cert_verifier field (rustls feature-gated) - TlsConnector: use dangerous().with_custom_certificate_verifier() and with_client_cert_resolver() when custom verifier is provided - CustomServerCertVerifier: generalize delegate from WebPkiServerVerifier to dyn ServerCertVerifier for use with custom verifiers - ProxyServiceBuilder: add connector_options() builder method to override the default ConnectorOptions derived from ServerConf - pingora-rustls: re-export ResolvesClientCert and sign module
…rt paths in build()
|
I think this is a great PR, and I would like to accept it. As contributors to this project have (unfortunately) realized, we are slow on reviews and ingestion, and extra slow when things are included that relate to parts of pingora that we don't use internally (like rustls or s2n integration). To help with that, I am hoping that we can find someone or organization that is willing to be a trusted reviewer for rustls-related prs. (Hence the help-wanted label). If you or someone you know would like to volunteer to take that on, let us know here. We (and the other rustls pingorians) would appreciate it. cc @hargut |
|
@fabian4 @nojima — this PR implements |
| .key_provider | ||
| .load_private_key(key) | ||
| .expect("Failed to load server private key"); | ||
| let certified_key = Arc::new(pingora_rustls::sign::CertifiedKey::new(certs, signing_key)); |
There was a problem hiding this comment.
I think it would be safer to keep an explicit cert/key consistency check here, as we previously did with with_single_cert(...), so that we only relax Web PKI policy validation without also dropping the more basic key/cert match validation.
| let provider = builder.crypto_provider().clone(); | ||
| let signing_key = provider | ||
| .key_provider | ||
| .load_private_key(key) |
There was a problem hiding this comment.
Also, the cert/key consistency check seems to be skipped here as well.
| } | ||
|
|
||
| /// Set the path to the certificate chain file (PEM format). | ||
| pub fn set_certificate_chain_file(&mut self, path: &str) { |
There was a problem hiding this comment.
The behavior is inconsistent with the corresponding function in the openssl version. The openssl version of this function returns a Result type. For example, it returns an error when a non-existent file is specified. Having rustls version behave the same way would be more convenient for users.
| } | ||
|
|
||
| /// Set the path to the private key file (PEM format). | ||
| pub fn set_private_key_file(&mut self, path: &str) { |
| assert!( | ||
| !self.cert_path.is_empty() && !self.key_path.is_empty(), | ||
| "Certificate and key paths must be set before calling build(). \ | ||
| When using with_callbacks(), call set_certificate_chain_file() \ | ||
| and set_private_key_file() first." | ||
| ); |
There was a problem hiding this comment.
In the openssl version, it is permitted to omit the certificate and private key at initialization. This kind of setup is useful when you want to return different server certificates depending on information received from the client during the handshake.
|
|
||
| // NOTE: certificate_callback is not invoked for rustls. Dynamic cert selection | ||
| // should use a custom ResolvesServerCert instead. | ||
| warn!("certificate_callback is not supported with the rustls backend; use ResolvesServerCert for dynamic cert selection"); |
There was a problem hiding this comment.
Can pingora users currently plug in their own implementation of ResolvesServerCert?
| { | ||
| let tls_ref = TlsRef; | ||
| // Build TlsRef with connection state for the callback | ||
| let tls_ref = tls_stream.build_tls_ref(); |
There was a problem hiding this comment.
IO already has get_ssl method that returns a Option<&TlsRef>. Rather than creating a new function, you should call this method instead.
if let Some(ref stream) = tls_stream.stream {
if let Some(tls_ref) = stream.get_ref().0.get_ssl() {
// do something with tls_ref
}
}There was a problem hiding this comment.
Currently, get_ssl on TlsStream is a default implementation that simply returns None.
pingora/pingora-core/src/protocols/tls/rustls/stream.rs
Lines 230 to 247 in 9a4eee3
pingora/pingora-core/src/protocols/mod.rs
Lines 57 to 60 in 9a4eee3
It would be good to provide a proper implementation here.
|
I'm not sure if this is the right place to discuss this, but I'd like to open a discussion about how the rustls version of TlsRef should be defined. The OpenSSL version of
|
Summary
TlsSettings::with_callbacks()— previously returned an error ("Certificate callbacks are not supported with feature rustls"). Now accepts aTlsAcceptCallbacksand threads it through toAcceptorand the handshake path.TlsRef— extended from an empty struct to carry peer certificate chain (Vec<CertificateDer>) and negotiated cipher suite name, with public accessors:peer_certificate_der(),peer_cert_chain_der(),current_cipher_name()TlsStream::build_tls_ref()— extracts connection state from the underlying rustls session after handshake completeshandshake_with_callback()— now builds a populatedTlsRefand passes it to the callback, matching the OpenSSL/BoringSSL path's behaviorset_certificate_chain_file()/set_private_key_file()builder methods onTlsSettingsfor use with the callbacks constructortest_handshake_complete_callbackintegration testMotivation
This enables downstream projects to use mTLS peer certificate inspection in post-handshake callbacks when using the
rustlsfeature, which previously only worked with the OpenSSL/BoringSSL backend.Test plan
cargo check -p pingora-core --features rustlscargo clippy -p pingora-core --features rustls --testscargo fmt -p pingora-core -- --checkcargo test -p pingora-core --features rustls -- server::tests::test_handshake_complete_callback