Skip to content

feat(rustls): implement TlsAcceptCallbacks support for rustls backend#833

Open
jsulmont wants to merge 5 commits intocloudflare:mainfrom
jsulmont:feat/rustls-tls-accept-callbacks
Open

feat(rustls): implement TlsAcceptCallbacks support for rustls backend#833
jsulmont wants to merge 5 commits intocloudflare:mainfrom
jsulmont:feat/rustls-tls-accept-callbacks

Conversation

@jsulmont
Copy link
Copy Markdown

@jsulmont jsulmont commented Mar 6, 2026

Summary

  • TlsSettings::with_callbacks() — previously returned an error ("Certificate callbacks are not supported with feature rustls"). Now accepts a TlsAcceptCallbacks and threads it through to Acceptor and 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 completes
  • handshake_with_callback() — now builds a populated TlsRef and passes it to the callback, matching the OpenSSL/BoringSSL path's behavior
  • Added set_certificate_chain_file() / set_private_key_file() builder methods on TlsSettings for use with the callbacks constructor
  • Added test_handshake_complete_callback integration test

Motivation

This enables downstream projects to use mTLS peer certificate inspection in post-handshake callbacks when using the rustls feature, which previously only worked with the OpenSSL/BoringSSL backend.

Test plan

  • cargo check -p pingora-core --features rustls
  • cargo clippy -p pingora-core --features rustls --tests
  • cargo fmt -p pingora-core -- --check
  • cargo test -p pingora-core --features rustls -- server::tests::test_handshake_complete_callback

 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
Jan van Lindt added 4 commits March 6, 2026 16:10
…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
@drcaramelsyrup drcaramelsyrup requested a review from johnhurt March 6, 2026 18:33
@drcaramelsyrup drcaramelsyrup added enhancement New feature or request help wanted Extra attention is needed labels Mar 6, 2026
@johnhurt
Copy link
Copy Markdown
Contributor

johnhurt commented Mar 6, 2026

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

@johnhurt
Copy link
Copy Markdown
Contributor

johnhurt commented Mar 27, 2026

@fabian4 @nojima — this PR implements TlsAcceptCallbacks for the rustls backend and extends TlsRef with peer cert chain and cipher info. It's the largest of the group but has a real integration test. Note it adds a couple of #[cfg(feature = "rustls")] fields to core structs which we'll review on our side. Would appreciate your review of the rustls-specific parts. (Per #835)

.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));
Copy link
Copy Markdown
Contributor

@fabian4 fabian4 Mar 30, 2026

Choose a reason for hiding this comment

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

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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same as above

Comment on lines +51 to +56
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."
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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
            }
        }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Currently, get_ssl on TlsStream is a default implementation that simply returns None.

impl<T> Ssl for TlsStream<T> {
fn get_ssl_digest(&self) -> Option<Arc<SslDigest>> {
self.ssl_digest()
}
fn selected_alpn_proto(&self) -> Option<ALPN> {
let st = self.tls.stream.as_ref();
if let Some(stream) = st {
let proto = stream.get_ref().1.alpn_protocol();
match proto {
None => None,
Some(raw) => ALPN::from_wire_selected(raw),
}
} else {
None
}
}
}

/// Return the TLS info if the connection is over TLS
fn get_ssl(&self) -> Option<&TlsRef> {
None
}

It would be good to provide a proper implementation here.

@nojima
Copy link
Copy Markdown
Contributor

nojima commented Apr 2, 2026

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 TlsRef is defined as an alias for SslRef, giving comprehensive access to information within the TLS connection. In contrast, the TlsRef in this PR only holds peer_certs and cipher, meaning information such as the TLS version or SNI cannot be retrieved.

  1. Should TlsRef be defined as a standalone struct, as in the current PR's approach, where Pingora populates it with the necessary data? Or
  2. Should TlsRef be defined as an alias for some rustls type (e.g. ServerConnection), similar to how the openssl version aliases SslRef, allowing users to access whatever they need?

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

Labels

enhancement New feature or request help wanted Extra attention is needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants