rust: bssl-tls: Introduce custom certificate verification

Bug: 479599893

Signed-off-by: Xiangfei Ding <xfding@google.com>
Change-Id: Iedb81134808f540f7f2d34c1c20927416a6a6964
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/91087
Presubmit-BoringSSL-Verified: boringssl-scoped@luci-project-accounts.iam.gserviceaccount.com <boringssl-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Adam Langley <agl@google.com>
diff --git a/rust/bssl-tls/src/connection.rs b/rust/bssl-tls/src/connection.rs
index 0dbb7c9..e7a26ae 100644
--- a/rust/bssl-tls/src/connection.rs
+++ b/rust/bssl-tls/src/connection.rs
@@ -107,6 +107,14 @@
         }
     }
 
+    fn get_connection_methods(&mut self) -> &mut methods::RustConnectionMethods<M> {
+        unsafe {
+            // Safety: the validity of the handle `self.0` is witnessed by
+            // `self`.
+            get_connection_methods(self.ptr())
+        }
+    }
+
     /// Disable session creation.
     pub fn disable_session(&mut self) -> &mut Self {
         let ptr = self.ptr();
@@ -211,8 +219,7 @@
         } else {
             *waker_data = Some(waker.clone());
         }
-        let methods = self.get_connection_methods();
-        methods.set_waker(waker);
+        self.get_connection_methods().set_waker(waker);
     }
 }
 
diff --git a/rust/bssl-tls/src/connection/credentials.rs b/rust/bssl-tls/src/connection/credentials.rs
index a612bbc..08baed7 100644
--- a/rust/bssl-tls/src/connection/credentials.rs
+++ b/rust/bssl-tls/src/connection/credentials.rs
@@ -12,7 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-use alloc::ffi::CString;
+use alloc::{
+    boxed::Box,
+    ffi::CString, //
+};
 use core::{
     ffi::CStr,
     ptr::null, //
@@ -37,14 +40,19 @@
     credentials::{
         CertificateVerificationMode,
         SignatureAlgorithm,
-        TlsCredential, //
+        TlsCredential,
+        VerifyCertificate,
+        cert_cb, //
     },
     errors::Error,
     ffi::slice_into_ffi_raw_parts,
     has_duplicates, //
 };
 
-impl<R, M> TlsConnectionBuilder<R, M> {
+impl<R, M> TlsConnectionBuilder<R, M>
+where
+    M: HasTlsConnectionMethod,
+{
     /// Configure the certificate verification mode.
     pub fn with_certificate_verification_mode(
         &mut self,
@@ -52,10 +60,50 @@
     ) -> &mut Self {
         let ctx = self.ptr();
         unsafe {
-            // Safety: `ctx` is still valid here, `mode` has a correct value by construction and
-            // `NULL` is a valid callback handle.
+            // Safety: this method only updates the mode value.
+            bssl_sys::SSL_set_verify(ctx, mode as _, None);
+        }
+        self
+    }
+
+    /// Configure the certificate verifier.
+    ///
+    /// See [`VerifyCertificate`] for how to implement a custom verifier.
+    ///
+    /// If raw public key authentication, per [RFC 7250], is configured,
+    /// the authentication through this mechanism will **fail** unless a certificate verifier
+    /// is configured.
+    ///
+    /// [RFC 7250]: <https://datatracker.ietf.org/doc/html/rfc7250>
+    pub fn with_certificate_verifier<V>(
+        &mut self,
+        mode: CertificateVerificationMode,
+        verifier: V,
+    ) -> &mut Self
+    where
+        V: VerifyCertificate + 'static,
+    {
+        let ctx = self.ptr();
+        unsafe {
+            // Safety: we only install our own vtable.
+            bssl_sys::SSL_set_custom_verify(
+                ctx,
+                mode as _,
+                Some(cert_cb::<super::methods::RustConnectionMethods<M>>),
+            );
+        }
+        self.get_connection_methods().verify_certificate_methods = Some(Box::new(verifier) as _);
+        self
+    }
+
+    /// Remove custom certificate verifier.
+    pub fn without_certificate_verifier(&mut self, mode: CertificateVerificationMode) -> &mut Self {
+        let ctx = self.ptr();
+        unsafe {
+            // Safety: we only uninstall the vtable.
             bssl_sys::SSL_set_custom_verify(ctx, mode as _, None);
         }
+        self.get_connection_methods().verify_certificate_methods = None;
         self
     }
 }
diff --git a/rust/bssl-tls/src/connection/lifecycle.rs b/rust/bssl-tls/src/connection/lifecycle.rs
index 71e9e48..032cec2 100644
--- a/rust/bssl-tls/src/connection/lifecycle.rs
+++ b/rust/bssl-tls/src/connection/lifecycle.rs
@@ -165,8 +165,7 @@
 impl<R> TlsConnection<R, TlsMode> {
     /// Inspect if the connection is suspended for which reason, after invocation of I/O methods.
     pub fn take_pending_reason(&mut self) -> Option<TlsRetryReason> {
-        let methods = self.get_connection_methods();
-        methods.take_pending_reason()
+        self.get_connection_methods().take_pending_reason()
     }
 }
 
diff --git a/rust/bssl-tls/src/connection/methods.rs b/rust/bssl-tls/src/connection/methods.rs
index 7a05231..0d1745d 100644
--- a/rust/bssl-tls/src/connection/methods.rs
+++ b/rust/bssl-tls/src/connection/methods.rs
@@ -31,12 +31,17 @@
 
 use crate::{
     Methods,
+    VerifyCertificateMethods,
     abort_on_panic,
     context::{
         DtlsMode,
         QuicMode,
         TlsMode, //
     },
+    credentials::{
+        VerifyCertificate,
+        VerifyCertificateTask, //
+    },
     errors::TlsRetryReason,
     io::RustBioHandle,
     methods::drop_box_rust_methods, //
@@ -46,6 +51,8 @@
 pub(super) struct RustConnectionMethods<Mode> {
     /// A handle to a `BIO` managed by this crate.
     pub bio: Option<RustBioHandle>,
+    /// Certificate verifier handle.
+    pub verify_certificate_methods: Option<Box<dyn VerifyCertificate>>,
     /// A mailbox to propagate IO retrying reasons.
     pub pending_reason: Option<TlsRetryReason>,
     _p: PhantomData<fn() -> Mode>,
@@ -55,6 +62,7 @@
     pub fn new() -> Self {
         Self {
             bio: None,
+            verify_certificate_methods: None,
             pending_reason: None,
             _p: PhantomData,
         }
@@ -91,6 +99,12 @@
     }
 }
 
+impl<Mode: HasTlsConnectionMethod> VerifyCertificateMethods for RustConnectionMethods<Mode> {
+    fn verify_certificate_methods(&self) -> Option<&dyn VerifyCertificate> {
+        self.verify_certificate_methods.as_deref()
+    }
+}
+
 // NOTE(@xfding): the reason that we are not using the `register_ex_data` macro is because
 // declarative macros today cannot handle generics well enough.
 fn register_tls_connection_vtable<Mode: HasTlsConnectionMethod>() -> c_int {
@@ -114,7 +128,6 @@
 /// Safety:
 /// - `ssl` must be a `SSL` object constructed by [`crate::connection::TlsConnection`].
 /// - `ssl` must be exclusively owned.
-#[allow(unused)] // This will be used in the following patch to support async I/O.
 pub(crate) unsafe fn waker_data_from_ssl(ssl: NonNull<bssl_sys::SSL>) -> Option<Waker> {
     unsafe {
         // Safety: `ssl` outlives `'a` and is constructed by `TlsConnection`.
@@ -135,6 +148,18 @@
     }
 }
 
+/// Safety:
+/// - `ssl` must be constructed from `TlsConnection` and outlived by `'a`.
+/// - `ssl` must be exclusively owned.
+pub(crate) unsafe fn verify_cert_task_from_ssl<'a>(
+    ssl: NonNull<bssl_sys::SSL>,
+) -> &'a mut Option<Box<dyn VerifyCertificateTask>> {
+    unsafe {
+        // Safety: `ssl` outlives `'a` and is constructed by `TlsConnection`.
+        <ExDataRegistration as ExData<Option<Box<dyn VerifyCertificateTask>>>>::get_mut(ssl)
+    }
+}
+
 pub(crate) struct ExDataRegistration;
 
 pub(crate) trait ExData<T: Default> {
@@ -239,6 +264,7 @@
 }
 
 register_ex_data!(Option<Waker>);
+register_ex_data!(Option<Box<dyn VerifyCertificateTask>>);
 
 pub(crate) trait HasTlsConnectionMethod {
     fn registration() -> c_int;
diff --git a/rust/bssl-tls/src/connection/transport.rs b/rust/bssl-tls/src/connection/transport.rs
index 71abcb5..67bc9ca 100644
--- a/rust/bssl-tls/src/connection/transport.rs
+++ b/rust/bssl-tls/src/connection/transport.rs
@@ -52,8 +52,7 @@
             // Safety: the `bio` pointer has been sanitised and `self.0` is still valid.
             bssl_sys::SSL_set_bio(self.ptr(), bio.ptr(), bio.ptr());
         }
-        let methods = self.get_connection_methods();
-        methods.bio = Some(bio);
+        self.get_connection_methods().bio = Some(bio);
         Ok(self)
     }
 
@@ -74,8 +73,7 @@
             // Safety: the `bio` pointer has been sanitised and `self.0` is still valid.
             bssl_sys::SSL_set_bio(self.ptr(), bio.ptr(), bio.ptr());
         }
-        let methods = self.get_connection_methods();
-        methods.bio = Some(bio);
+        self.get_connection_methods().bio = Some(bio);
         Ok(self)
     }
 
diff --git a/rust/bssl-tls/src/context.rs b/rust/bssl-tls/src/context.rs
index f75ec37..0ddc865 100644
--- a/rust/bssl-tls/src/context.rs
+++ b/rust/bssl-tls/src/context.rs
@@ -153,7 +153,6 @@
         }
     }
 
-    #[allow(unused)]
     fn get_context_methods(&mut self) -> &mut methods::RustContextMethods<M> {
         let methods = unsafe {
             // Safety: the validity of the handle `self.0` is witnessed by `self`.
diff --git a/rust/bssl-tls/src/context/credentials.rs b/rust/bssl-tls/src/context/credentials.rs
index 073dd59..6c27400 100644
--- a/rust/bssl-tls/src/context/credentials.rs
+++ b/rust/bssl-tls/src/context/credentials.rs
@@ -30,7 +30,9 @@
     credentials::{
         CertificateVerificationMode,
         SignatureAlgorithm,
-        TlsCredential, //
+        TlsCredential,
+        VerifyCertificate,
+        cert_cb, //
     },
     errors::Error,
     ffi::slice_into_ffi_raw_parts,
@@ -79,9 +81,54 @@
     ) -> &mut Self {
         let conn = self.ptr();
         unsafe {
+            // Safety: we only install our own vtable.
+            bssl_sys::SSL_CTX_set_verify(conn, mode as _, None);
+        }
+        self
+    }
+
+    /// Set certificate verification mode and custom verifier.
+    ///
+    /// # Setting custom certificate verifier
+    ///
+    /// See [`VerifyCertificate`] for how to implement a custom verifier.
+    ///
+    /// # Client certificate verification for servers, mutual TLS
+    ///
+    /// Server can choose to request a certificate from the client by setting `mode` to
+    /// - [`CertificateVerificationMode::PeerCertRequested`] which may still let handshake complete
+    ///   if the certificate request by the server is not fulfilled.
+    /// - [`CertificateVerificationMode::PeerCertMandatory`] which will abort handshake if
+    ///   the request is not fulfilled.
+    pub fn with_certificate_verifier<V>(
+        &mut self,
+        mode: CertificateVerificationMode,
+        verifier: V,
+    ) -> &mut Self
+    where
+        V: VerifyCertificate + 'static,
+    {
+        let conn = self.ptr();
+        unsafe {
+            // Safety: we only install our own vtable.
+            bssl_sys::SSL_CTX_set_custom_verify(
+                conn,
+                mode as _,
+                Some(cert_cb::<super::methods::RustContextMethods<M>>),
+            );
+        }
+        self.get_context_methods().verify_certificate_methods = Some(Box::new(verifier) as _);
+        self
+    }
+
+    /// Remove custom certificate verifier.
+    pub fn without_certificate_verifier(&mut self, mode: CertificateVerificationMode) -> &mut Self {
+        let conn = self.ptr();
+        unsafe {
             // Safety: we only uninstall the vtable.
             bssl_sys::SSL_CTX_set_custom_verify(conn, mode as _, None);
         }
+        self.get_context_methods().verify_certificate_methods = None;
         self
     }
 
diff --git a/rust/bssl-tls/src/context/methods.rs b/rust/bssl-tls/src/context/methods.rs
index ae2d319..e701810 100644
--- a/rust/bssl-tls/src/context/methods.rs
+++ b/rust/bssl-tls/src/context/methods.rs
@@ -22,21 +22,29 @@
 
 use crate::{
     Methods,
+    VerifyCertificateMethods,
     context::{
         DtlsMode,
         QuicMode,
         TlsMode, //
     },
+    credentials::VerifyCertificate,
     methods::drop_box_rust_methods, //
 };
 
-pub(crate) struct RustContextMethods<M>(PhantomData<fn() -> M>);
+pub(crate) struct RustContextMethods<M> {
+    pub(crate) verify_certificate_methods: Option<Box<dyn VerifyCertificate>>,
+    _p: PhantomData<fn() -> M>,
+}
 
 // NOTE(@xfding): the reason we do not use `register_ex_data` for this type is because we need to
 // look up the associated SSL_CTX first.
 impl<M> RustContextMethods<M> {
     pub fn new() -> Self {
-        Self(PhantomData)
+        Self {
+            verify_certificate_methods: None,
+            _p: PhantomData,
+        }
     }
 }
 
@@ -56,6 +64,12 @@
     }
 }
 
+impl<M: HasTlsContextMethod> VerifyCertificateMethods for RustContextMethods<M> {
+    fn verify_certificate_methods(&self) -> Option<&dyn VerifyCertificate> {
+        self.verify_certificate_methods.as_deref()
+    }
+}
+
 fn register_tls_context_vtable<M: HasTlsContextMethod>() -> c_int {
     unsafe {
         // Safety: this a one-time registration uses only valid function pointers.
diff --git a/rust/bssl-tls/src/credentials.rs b/rust/bssl-tls/src/credentials.rs
index 5f2e860..b7610ef 100644
--- a/rust/bssl-tls/src/credentials.rs
+++ b/rust/bssl-tls/src/credentials.rs
@@ -25,11 +25,18 @@
         c_int, //
     },
     fmt::Debug,
+    future::Future,
+    iter::FusedIterator,
     marker::PhantomData,
     mem::forget,
+    pin::Pin,
     ptr::{
         NonNull,
         null_mut, //
+    },
+    task::{
+        Context,
+        Poll, //
     }, //
 };
 
@@ -39,8 +46,13 @@
 };
 
 use crate::{
+    VerifyCertificateMethods,
+    abort_on_panic,
+    alerts::AlertDescription,
+    call_slice_getter,
     check_lib_error,
     config::ConfigurationError,
+    connection::methods::{verify_cert_task_from_ssl, waker_data_from_ssl},
     context::CertificateCache,
     crypto_buffer_wrapper,
     errors::{
@@ -517,6 +529,307 @@
     }
 }
 
+// NOTE: this context does not own the connection.
+/// A verification context handle.
+#[repr(transparent)]
+pub struct VerifyCertificateContext(NonNull<bssl_sys::SSL>);
+
+impl VerifyCertificateContext {
+    fn ptr(&self) -> *mut bssl_sys::SSL {
+        self.0.as_ptr()
+    }
+
+    /// Get Encrypted `ClientHello` name override, specifically a DNS name per
+    /// [RFC 5280], which a character set stipulated by [RFC 1034] §3.5.
+    ///
+    /// The returned name should be interpreted first as an opaque byte string.
+    ///
+    /// # Interaction with custom certificate verification
+    ///
+    /// If the return value is [`Some`], the end-entity certificate must be
+    /// verified against the name reported by this call.
+    ///
+    /// [RFC 5280]: <https://datatracker.ietf.org/doc/html/rfc5280>
+    /// [RFC 1034]: <https://datatracker.ietf.org/doc/html/rfc1034#section-3.5>
+    pub fn get_ech_name_override(&self) -> Option<&str> {
+        let name: &[u8] = unsafe {
+            // Safety:
+            // - `self` outlives the slice.
+            // - transmuting i8 to u8 preserve the character value.
+            core::mem::transmute(call_slice_getter!(
+                bssl_sys::SSL_get0_ech_name_override,
+                self.ptr()
+            )?)
+        };
+        if name.is_empty() || !name.is_ascii() {
+            return None;
+        }
+        // A DNS name has to be an IA5String, specifically ASCII first.
+        str::from_utf8(name).ok()
+    }
+
+    /// Get the stapled OCSP response, if any.
+    ///
+    /// The response may not be a valid OCSPResponse from the server as per
+    /// [RFC 2560].
+    ///
+    /// [RFC 2560]: <https://datatracker.ietf.org/doc/html/rfc6960>
+    pub fn get_ocsp_response(&self) -> Option<&[u8]> {
+        // Safety: response, when it exists, is outlived by the connection.
+        let response = call_slice_getter!(bssl_sys::SSL_get0_ocsp_response, self.ptr())?;
+        (!response.is_empty()).then_some(response)
+    }
+
+    /// Get the Signed Certificate Timestamp list, if any, as per [RFC 6962] §3.2.
+    ///
+    /// [RFC 6962]: <https://datatracker.ietf.org/doc/html/rfc6962#section-3.2>
+    pub fn get_signed_cert_timestamp_list(&self) -> Option<&[u8]> {
+        // Safety: list, when it exists, is outlived by the connection.
+        let list = call_slice_getter!(bssl_sys::SSL_get0_signed_cert_timestamp_list, self.ptr())?;
+        (!list.is_empty()).then_some(list)
+    }
+}
+
+/// Custom certificate verification callback.
+///
+/// It is recommended to avoid panicking in the trait implementation.
+/// A panic in this callback will lead to abort.
+pub trait VerifyCertificate: Send + Sync {
+    /// Decide whether a certificate chain is acceptable.
+    ///
+    /// The peer certificate chain is supplied in `certs`, in which the first certificate is
+    /// the End Entity certificate, if any.
+    ///
+    /// This method may be called more than once if the verification is asynchronous.
+    /// To signal suspension, this method should return [`VerifyResult::Pending`].
+    fn verify<'a>(
+        &self,
+        ctx: &'a VerifyCertificateContext,
+        certs: CertificateChainIterator<'a>,
+    ) -> Box<dyn VerifyCertificateTask>;
+}
+
+/// An outstanding certificate verification task.
+pub trait VerifyCertificateTask: Send {
+    /// Try to complete the verification task.
+    fn complete(&mut self, async_ctx: Option<&mut Context<'_>>) -> VerifyResult;
+}
+
+/// Custom certificate verification result.
+pub enum VerifyResult {
+    /// The certificate chain is accepted.
+    Accept,
+    /// The certification chain is pending asynchronous result.
+    Pending,
+    /// The certificate chain is rejected possibly with an alert.
+    Reject(Option<AlertDescription>),
+}
+
+/// Asynchronous custom certificate verification.
+///
+/// This is the `async` analogue of [`VerifyCertificate`].
+pub trait AsyncVerifyCertificate: Send + Sync + Unpin {
+    /// The future type of the verification process.
+    type VerifyFuture: 'static + Unpin + Send + Sync + Future<Output = bool>;
+
+    /// Decide whether a certificate chain is acceptable.
+    fn verify(
+        &self,
+        ctx: &VerifyCertificateContext,
+        certs: CertificateChainIterator<'_>,
+    ) -> Self::VerifyFuture;
+}
+
+/// Adapter to run certificate verification asynchronously.
+pub struct AsyncVerifyCertificateAdapter<T>(pub T);
+
+impl<T, Fut> VerifyCertificate for AsyncVerifyCertificateAdapter<T>
+where
+    T: AsyncVerifyCertificate<VerifyFuture = Fut>,
+    Fut: 'static + Unpin + Send + Sync + Future<Output = bool>,
+{
+    fn verify<'a>(
+        &self,
+        ctx: &'a VerifyCertificateContext,
+        certs: CertificateChainIterator<'a>,
+    ) -> Box<dyn VerifyCertificateTask> {
+        Box::new(AsyncVerifyCertificateTask(self.0.verify(ctx, certs)))
+    }
+}
+
+struct AsyncVerifyCertificateTask<Fut>(Fut);
+
+impl<Fut> VerifyCertificateTask for AsyncVerifyCertificateTask<Fut>
+where
+    Fut: 'static + Unpin + Send + Sync + Future<Output = bool>,
+{
+    fn complete(&mut self, async_ctx: Option<&mut Context<'_>>) -> VerifyResult {
+        let Some(cx) = async_ctx else {
+            return VerifyResult::Reject(Some(AlertDescription::InternalError));
+        };
+        let outstanding_task = Pin::new(&mut self.0);
+        match outstanding_task.poll(cx) {
+            Poll::Ready(accept) => {
+                if accept {
+                    VerifyResult::Accept
+                } else {
+                    VerifyResult::Reject(Some(AlertDescription::BadCertificate))
+                }
+            }
+            Poll::Pending => VerifyResult::Pending,
+        }
+    }
+}
+
+/// Certificate chain iterator.
+///
+/// This iterator will supply the peer leaf certificate as the first element in the chain, if any.
+#[derive(Clone, Copy)]
+pub struct CertificateChainIterator<'a> {
+    certs: *const bssl_sys::stack_st_CRYPTO_BUFFER,
+    len: usize,
+    curr: usize,
+    _p: PhantomData<&'a ()>,
+}
+
+impl<'a> CertificateChainIterator<'a> {
+    /// Safety: caller must ensure that `certs` is outlived by,
+    /// or in other words stays alive as long as, `'a`.
+    pub(crate) unsafe fn new(certs: *const bssl_sys::stack_st_CRYPTO_BUFFER) -> Self {
+        let len = if certs.is_null() {
+            0
+        } else {
+            unsafe {
+                // Safety: `certs` is valid now.
+                bssl_sys::sk_CRYPTO_BUFFER_num(certs)
+            }
+        };
+        Self {
+            certs,
+            len,
+            curr: 0,
+            _p: PhantomData,
+        }
+    }
+}
+
+impl<'a> Iterator for CertificateChainIterator<'a> {
+    type Item = Certificate;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if self.curr >= self.len {
+            return None;
+        }
+        let cert = unsafe {
+            // Safety: `self.certs` is still valid now and `self.curr` is within the bound.
+            bssl_sys::sk_CRYPTO_BUFFER_value(self.certs, self.curr)
+        };
+        self.curr += 1;
+        let Some(cert) = NonNull::new(cert) else {
+            // Fuse the iterator.
+            self.curr = self.len;
+            return None;
+        };
+        unsafe {
+            // Safety: `cert` is valid here.
+            bssl_sys::CRYPTO_BUFFER_up_ref(cert.as_ptr());
+        }
+        Some(Certificate(cert))
+    }
+}
+
+impl ExactSizeIterator for CertificateChainIterator<'_> {
+    fn len(&self) -> usize {
+        self.len
+    }
+}
+
+impl FusedIterator for CertificateChainIterator<'_> {}
+
+/// Safety: this callback stub must be installed with a context object allocated
+/// as a `Box<dyn VerifyCertificate>`.
+pub(crate) unsafe extern "C" fn cert_cb<M: VerifyCertificateMethods>(
+    ssl: *mut bssl_sys::SSL,
+    alert: *mut u8,
+) -> bssl_sys::ssl_verify_result_t {
+    let Some(ssl) = NonNull::new(ssl) else {
+        return bssl_sys::ssl_verify_result_t_ssl_verify_invalid;
+    };
+    let Some(methods) = (unsafe {
+        // Safety: `ssl` outlives `methods`
+        M::from_ssl(ssl.as_ptr())
+    }) else {
+        return bssl_sys::ssl_verify_result_t_ssl_verify_invalid;
+    };
+    let waker = unsafe {
+        // Safety:
+        // - this callback must be installed by `TlsContextBuilder` or `TlsConnection`,
+        //   so the associated data must have been set up correctly.
+        // - the caller of this callback must own the connection exclusively.
+        waker_data_from_ssl(ssl)
+    };
+    let mut context = waker.as_ref().map(Context::from_waker);
+    let Some(verify) = methods.verify_certificate_methods() else {
+        return bssl_sys::ssl_verify_result_t_ssl_verify_invalid;
+    };
+    let cert_chain = unsafe {
+        // Safety: `ssl` is still alive in handshake mode and will outlive `cert_chain`.
+        bssl_sys::SSL_get0_peer_certificates(ssl.as_ptr())
+    };
+    let certs = unsafe {
+        // Safety: `cert_chain` is outlived by `ssl` whose lifetime is annotated as `'a`.
+        CertificateChainIterator::new(cert_chain)
+    };
+    let ctx = &VerifyCertificateContext(ssl);
+    let async_ctx = context.as_mut();
+
+    let outstanding_task = unsafe {
+        // Safety: `ssl` outlives the in-flight task and exclusively owned by the caller of
+        // this callback.
+        verify_cert_task_from_ssl(ssl)
+    };
+
+    abort_on_panic(move || {
+        if let Some(task) = outstanding_task {
+            match task.complete(async_ctx) {
+                VerifyResult::Pending => bssl_sys::ssl_verify_result_t_ssl_verify_retry,
+                VerifyResult::Accept => {
+                    let _ = outstanding_task.take();
+                    bssl_sys::ssl_verify_result_t_ssl_verify_ok
+                }
+                VerifyResult::Reject(ad) => {
+                    let _ = outstanding_task.take();
+                    if let Some(ad) = ad {
+                        unsafe {
+                            // Safety: `alert` is valid per BoringSSL invariants.
+                            alert.write(ad as _);
+                        }
+                    }
+                    bssl_sys::ssl_verify_result_t_ssl_verify_invalid
+                }
+            }
+        } else {
+            let mut task = verify.verify(ctx, certs);
+            match task.complete(async_ctx) {
+                VerifyResult::Pending => {
+                    *outstanding_task = Some(task);
+                    bssl_sys::ssl_verify_result_t_ssl_verify_retry
+                }
+                VerifyResult::Accept => bssl_sys::ssl_verify_result_t_ssl_verify_ok,
+                VerifyResult::Reject(ad) => {
+                    if let Some(ad) = ad {
+                        unsafe {
+                            // Safety: `alert` is valid per BoringSSL invariants.
+                            alert.write(ad as _);
+                        }
+                    }
+                    bssl_sys::ssl_verify_result_t_ssl_verify_invalid
+                }
+            }
+        }
+    })
+}
+
 bssl_macros::bssl_enum! {
     /// Certificate verification mode
     pub enum CertificateVerificationMode: i8 {
diff --git a/rust/bssl-tls/src/lib.rs b/rust/bssl-tls/src/lib.rs
index e993416..88dd1ef 100644
--- a/rust/bssl-tls/src/lib.rs
+++ b/rust/bssl-tls/src/lib.rs
@@ -59,13 +59,16 @@
     list.iter().any(|elem| !seen.insert(elem))
 }
 
-#[allow(unused)]
 pub(crate) trait Methods {
     /// Safety: `ssl` must outlive `'a` and it must be passed in from BoringSSL
     /// through vtable calls.
     unsafe extern "C" fn from_ssl<'a>(ssl: *mut bssl_sys::SSL) -> Option<&'a Self>;
 }
 
+pub(crate) trait VerifyCertificateMethods: Methods {
+    fn verify_certificate_methods(&self) -> Option<&dyn credentials::VerifyCertificate>;
+}
+
 #[inline]
 fn abort_on_panic<T>(work: impl FnOnce() -> T) -> T {
     let assert_unwind_safe = AssertUnwindSafe(work);
diff --git a/rust/bssl-tls/src/macros.rs b/rust/bssl-tls/src/macros.rs
index eea95e3..eac9622 100644
--- a/rust/bssl-tls/src/macros.rs
+++ b/rust/bssl-tls/src/macros.rs
@@ -132,3 +132,23 @@
         }
     };
 }
+
+// Safety: `$obj` must outlive the returned slice.
+#[doc(hidden)]
+#[macro_export]
+macro_rules! call_slice_getter {
+    ($fn:path, $obj:expr) => {{
+        let mut data = ::core::ptr::null();
+        let mut len = 0;
+        #[allow(unused_unsafe)]
+        unsafe {
+            // Safety: `obj`, `data` and `len` are all valid.
+            $fn($obj, &raw mut data, &raw mut len);
+        }
+        #[allow(unused_unsafe)]
+        unsafe {
+            // Safety: data and len are returned by BoringSSL and are valid.
+            $crate::ffi::sanitize_slice(data, len)
+        }
+    }};
+}
diff --git a/rust/bssl-tls/src/sessions.rs b/rust/bssl-tls/src/sessions.rs
index 5a6676c..84cb7cc 100644
--- a/rust/bssl-tls/src/sessions.rs
+++ b/rust/bssl-tls/src/sessions.rs
@@ -17,11 +17,11 @@
 use alloc::vec::Vec;
 use core::ptr::{
     NonNull,
-    null,
     null_mut, //
 };
 
 use crate::{
+    call_slice_getter,
     config::ProtocolVersion,
     context::TlsContext,
     errors::Error,
@@ -78,21 +78,6 @@
     }
 }
 
-macro_rules! call_slice_getter {
-    ($fn:path, $obj:expr) => {{
-        let mut data = null();
-        let mut len = 0;
-        unsafe {
-            // Safety: `obj`, `data` and `len` are all valid.
-            $fn($obj, &raw mut data, &raw mut len);
-        }
-        unsafe {
-            // Safety: data and len are returned by BoringSSL and are valid.
-            sanitize_slice(data, len)
-        }
-    }};
-}
-
 impl TlsSession {
     pub(crate) fn ptr(&self) -> *mut bssl_sys::SSL_SESSION {
         self.0.as_ptr()