diff --git a/boring/src/ssl/mod.rs b/boring/src/ssl/mod.rs
index 29254449..f212644e 100644
--- a/boring/src/ssl/mod.rs
+++ b/boring/src/ssl/mod.rs
@@ -478,6 +478,9 @@ pub struct SelectCertError(ffi::ssl_select_cert_result_t);
 impl SelectCertError {
     /// A fatal error occured and the handshake should be terminated.
     pub const ERROR: Self = Self(ffi::ssl_select_cert_result_t::ssl_select_cert_error);
+
+    /// The operation could not be completed and should be retried later.
+    pub const RETRY: Self = Self(ffi::ssl_select_cert_result_t::ssl_select_cert_retry);
 }
 
 /// Extension types, to be used with `ClientHello::get_extension`.
@@ -3197,6 +3200,11 @@ impl<S> MidHandshakeSslStream<S> {
         self.stream.ssl()
     }
 
+    /// Returns a mutable reference to the `Ssl` of the stream.
+    pub fn ssl_mut(&mut self) -> &mut SslRef {
+        self.stream.ssl_mut()
+    }
+
     /// Returns the underlying error which interrupted this handshake.
     pub fn error(&self) -> &Error {
         &self.error
@@ -3451,6 +3459,11 @@ impl<S> SslStream<S> {
     pub fn ssl(&self) -> &SslRef {
         &self.ssl
     }
+
+    /// Returns a mutable reference to the `Ssl` object associated with this stream.
+    pub fn ssl_mut(&mut self) -> &mut SslRef {
+        &mut self.ssl
+    }
 }
 
 impl<S: Read + Write> Read for SslStream<S> {
diff --git a/tokio-boring/Cargo.toml b/tokio-boring/Cargo.toml
index 009dd580..88ba52d0 100644
--- a/tokio-boring/Cargo.toml
+++ b/tokio-boring/Cargo.toml
@@ -31,6 +31,7 @@ pq-experimental = ["boring/pq-experimental"]
 [dependencies]
 boring = { workspace = true }
 boring-sys = { workspace = true }
+once_cell = { workspace = true }
 tokio = { workspace = true }
 
 [dev-dependencies]
diff --git a/tokio-boring/src/async_callbacks.rs b/tokio-boring/src/async_callbacks.rs
new file mode 100644
index 00000000..6bd2cf5e
--- /dev/null
+++ b/tokio-boring/src/async_callbacks.rs
@@ -0,0 +1,260 @@
+use boring::ex_data::Index;
+use boring::ssl::{self, ClientHello, PrivateKeyMethod, Ssl, SslContextBuilder};
+use once_cell::sync::Lazy;
+use std::future::Future;
+use std::pin::Pin;
+use std::task::{ready, Context, Poll, Waker};
+
+type BoxSelectCertFuture = ExDataFuture<Result<BoxSelectCertFinish, AsyncSelectCertError>>;
+
+type BoxSelectCertFinish = Box<dyn FnOnce(ClientHello<'_>) -> Result<(), AsyncSelectCertError>>;
+
+/// The type of futures returned by [`AsyncPrivateKeyMethod`] methods.
+pub type BoxPrivateKeyMethodFuture =
+    ExDataFuture<Result<BoxPrivateKeyMethodFinish, AsyncPrivateKeyMethodError>>;
+
+/// The type of callbacks returned by [`BoxPrivateKeyMethodFuture`].
+pub type BoxPrivateKeyMethodFinish =
+    Box<dyn FnOnce(&mut ssl::SslRef, &mut [u8]) -> Result<usize, AsyncPrivateKeyMethodError>>;
+
+type ExDataFuture<T> = Pin<Box<dyn Future<Output = T> + Send + Sync>>;
+
+pub(crate) static TASK_WAKER_INDEX: Lazy<Index<Ssl, Option<Waker>>> =
+    Lazy::new(|| Ssl::new_ex_index().unwrap());
+pub(crate) static SELECT_CERT_FUTURE_INDEX: Lazy<Index<Ssl, BoxSelectCertFuture>> =
+    Lazy::new(|| Ssl::new_ex_index().unwrap());
+pub(crate) static SELECT_PRIVATE_KEY_METHOD_FUTURE_INDEX: Lazy<
+    Index<Ssl, BoxPrivateKeyMethodFuture>,
+> = Lazy::new(|| Ssl::new_ex_index().unwrap());
+
+/// Extensions to [`SslContextBuilder`].
+///
+/// This trait provides additional methods to use async callbacks with boring.
+pub trait SslContextBuilderExt: private::Sealed {
+    /// Sets a callback that is called before most [`ClientHello`] processing
+    /// and before the decision whether to resume a session is made. The
+    /// callback may inspect the [`ClientHello`] and configure the connection.
+    ///
+    /// This method uses a function that returns a future whose output is
+    /// itself a closure that will be passed [`ClientHello`] to configure
+    /// the connection based on the computations done in the future.
+    ///
+    /// See [`SslContextBuilder::set_select_certificate_callback`] for the sync
+    /// setter of this callback.
+    fn set_async_select_certificate_callback<Init, Fut, Finish>(&mut self, callback: Init)
+    where
+        Init: Fn(&mut ClientHello<'_>) -> Result<Fut, AsyncSelectCertError> + Send + Sync + 'static,
+        Fut: Future<Output = Result<Finish, AsyncSelectCertError>> + Send + Sync + 'static,
+        Finish: FnOnce(ClientHello<'_>) -> Result<(), AsyncSelectCertError> + 'static;
+
+    /// Configures a custom private key method on the context.
+    ///
+    /// See [`AsyncPrivateKeyMethod`] for more details.
+    fn set_async_private_key_method(&mut self, method: impl AsyncPrivateKeyMethod);
+}
+
+impl SslContextBuilderExt for SslContextBuilder {
+    fn set_async_select_certificate_callback<Init, Fut, Finish>(&mut self, callback: Init)
+    where
+        Init: Fn(&mut ClientHello<'_>) -> Result<Fut, AsyncSelectCertError> + Send + Sync + 'static,
+        Fut: Future<Output = Result<Finish, AsyncSelectCertError>> + Send + Sync + 'static,
+        Finish: FnOnce(ClientHello<'_>) -> Result<(), AsyncSelectCertError> + 'static,
+    {
+        self.set_select_certificate_callback(move |mut client_hello| {
+            let fut_poll_result = with_ex_data_future(
+                &mut client_hello,
+                *SELECT_CERT_FUTURE_INDEX,
+                ClientHello::ssl_mut,
+                |client_hello| {
+                    let fut = callback(client_hello)?;
+
+                    Ok(Box::pin(async move {
+                        Ok(Box::new(fut.await?) as BoxSelectCertFinish)
+                    }))
+                },
+            );
+
+            let fut_result = match fut_poll_result {
+                Poll::Ready(fut_result) => fut_result,
+                Poll::Pending => return Err(ssl::SelectCertError::RETRY),
+            };
+
+            let finish = fut_result.or(Err(ssl::SelectCertError::ERROR))?;
+
+            finish(client_hello).or(Err(ssl::SelectCertError::ERROR))
+        })
+    }
+
+    fn set_async_private_key_method(&mut self, method: impl AsyncPrivateKeyMethod) {
+        self.set_private_key_method(Box::new(method) as Box<dyn AsyncPrivateKeyMethod>);
+    }
+}
+
+/// A fatal error to be returned from async select certificate callbacks.
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub struct AsyncSelectCertError;
+
+/// Describes async private key hooks. This is used to off-load signing
+/// operations to a custom, potentially asynchronous, backend. Metadata about the
+/// key such as the type and size are parsed out of the certificate.
+///
+/// See [`PrivateKeyMethod`] for the sync version of those hooks.
+///
+/// [`ssl_private_key_method_st`]: https://commondatastorage.googleapis.com/chromium-boringssl-docs/ssl.h.html#ssl_private_key_method_st
+pub trait AsyncPrivateKeyMethod: Send + Sync + 'static {
+    /// Signs the message `input` using the specified signature algorithm.
+    ///
+    /// This method uses a function that returns a future whose output is
+    /// itself a closure that will be passed `ssl` and `output`
+    /// to finish writing the signature.
+    ///
+    /// See [`PrivateKeyMethod::sign`] for the sync version of this method.
+    fn sign(
+        &self,
+        ssl: &mut ssl::SslRef,
+        input: &[u8],
+        signature_algorithm: ssl::SslSignatureAlgorithm,
+        output: &mut [u8],
+    ) -> Result<BoxPrivateKeyMethodFuture, AsyncPrivateKeyMethodError>;
+
+    /// Decrypts `input`.
+    ///
+    /// This method uses a function that returns a future whose output is
+    /// itself a closure that will be passed `ssl` and `output`
+    /// to finish decrypting the input.
+    ///
+    /// See [`PrivateKeyMethod::decrypt`] for the sync version of this method.
+    fn decrypt(
+        &self,
+        ssl: &mut ssl::SslRef,
+        input: &[u8],
+        output: &mut [u8],
+    ) -> Result<BoxPrivateKeyMethodFuture, AsyncPrivateKeyMethodError>;
+}
+
+/// A fatal error to be returned from async private key methods.
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub struct AsyncPrivateKeyMethodError;
+
+impl PrivateKeyMethod for Box<dyn AsyncPrivateKeyMethod> {
+    fn sign(
+        &self,
+        ssl: &mut ssl::SslRef,
+        input: &[u8],
+        signature_algorithm: ssl::SslSignatureAlgorithm,
+        output: &mut [u8],
+    ) -> Result<usize, ssl::PrivateKeyError> {
+        with_private_key_method(ssl, output, |ssl, output| {
+            <dyn AsyncPrivateKeyMethod>::sign(&**self, ssl, input, signature_algorithm, output)
+        })
+    }
+
+    fn decrypt(
+        &self,
+        ssl: &mut ssl::SslRef,
+        input: &[u8],
+        output: &mut [u8],
+    ) -> Result<usize, ssl::PrivateKeyError> {
+        with_private_key_method(ssl, output, |ssl, output| {
+            <dyn AsyncPrivateKeyMethod>::decrypt(&**self, ssl, input, output)
+        })
+    }
+
+    fn complete(
+        &self,
+        ssl: &mut ssl::SslRef,
+        output: &mut [u8],
+    ) -> Result<usize, ssl::PrivateKeyError> {
+        with_private_key_method(ssl, output, |_, _| {
+            // This should never be reached, if it does, that's a bug on boring's side,
+            // which called `complete` without having been returned to with a pending
+            // future from `sign` or `decrypt`.
+
+            if cfg!(debug_assertions) {
+                panic!("BUG: boring called complete without a pending operation");
+            }
+
+            Err(AsyncPrivateKeyMethodError)
+        })
+    }
+}
+
+/// Creates and drives a private key method future.
+///
+/// This is a convenience function for the three methods of impl `PrivateKeyMethod``
+/// for `dyn AsyncPrivateKeyMethod`. It relies on [`with_ex_data_future`] to
+/// drive the future and then immediately calls the final [`BoxPrivateKeyMethodFinish`]
+/// when the future is ready.
+fn with_private_key_method(
+    ssl: &mut ssl::SslRef,
+    output: &mut [u8],
+    create_fut: impl FnOnce(
+        &mut ssl::SslRef,
+        &mut [u8],
+    ) -> Result<BoxPrivateKeyMethodFuture, AsyncPrivateKeyMethodError>,
+) -> Result<usize, ssl::PrivateKeyError> {
+    let fut_poll_result = with_ex_data_future(
+        ssl,
+        *SELECT_PRIVATE_KEY_METHOD_FUTURE_INDEX,
+        |ssl| ssl,
+        |ssl| create_fut(ssl, output),
+    );
+
+    let fut_result = match fut_poll_result {
+        Poll::Ready(fut_result) => fut_result,
+        Poll::Pending => return Err(ssl::PrivateKeyError::RETRY),
+    };
+
+    let finish = fut_result.or(Err(ssl::PrivateKeyError::FAILURE))?;
+
+    finish(ssl, output).or(Err(ssl::PrivateKeyError::FAILURE))
+}
+
+/// Creates and drives a future stored in `ssl_handle`'s `Ssl` at ex data index `index`.
+///
+/// This function won't even bother storing the future in `index` if the future
+/// created by `create_fut` returns `Poll::Ready(_)` on the first poll call.
+fn with_ex_data_future<H, T, E>(
+    ssl_handle: &mut H,
+    index: Index<ssl::Ssl, ExDataFuture<Result<T, E>>>,
+    get_ssl_mut: impl Fn(&mut H) -> &mut ssl::SslRef,
+    create_fut: impl FnOnce(&mut H) -> Result<ExDataFuture<Result<T, E>>, E>,
+) -> Poll<Result<T, E>> {
+    let ssl = get_ssl_mut(ssl_handle);
+    let waker = ssl
+        .ex_data(*TASK_WAKER_INDEX)
+        .cloned()
+        .flatten()
+        .expect("task waker should be set");
+
+    let mut ctx = Context::from_waker(&waker);
+
+    match ssl.ex_data_mut(index) {
+        Some(fut) => {
+            let fut_result = ready!(fut.as_mut().poll(&mut ctx));
+
+            // NOTE(nox): For memory usage concerns, maybe we should implement
+            // a way to remove the stored future from the `Ssl` value here?
+
+            Poll::Ready(fut_result)
+        }
+        None => {
+            let mut fut = create_fut(ssl_handle)?;
+
+            match fut.as_mut().poll(&mut ctx) {
+                Poll::Ready(fut_result) => Poll::Ready(fut_result),
+                Poll::Pending => {
+                    get_ssl_mut(ssl_handle).set_ex_data(index, fut);
+
+                    Poll::Pending
+                }
+            }
+        }
+    }
+}
+
+mod private {
+    pub trait Sealed {}
+}
+
+impl private::Sealed for SslContextBuilder {}
diff --git a/tokio-boring/src/lib.rs b/tokio-boring/src/lib.rs
index 7d84c250..3015ee0d 100644
--- a/tokio-boring/src/lib.rs
+++ b/tokio-boring/src/lib.rs
@@ -27,8 +27,14 @@ use std::pin::Pin;
 use std::task::{Context, Poll};
 use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
 
+mod async_callbacks;
 mod bridge;
 
+use self::async_callbacks::TASK_WAKER_INDEX;
+pub use self::async_callbacks::{
+    AsyncPrivateKeyMethod, AsyncPrivateKeyMethodError, AsyncSelectCertError,
+    BoxPrivateKeyMethodFinish, BoxPrivateKeyMethodFuture, SslContextBuilderExt,
+};
 use self::bridge::AsyncStreamBridge;
 
 /// Asynchronously performs a client-side TLS handshake over the provided stream.
@@ -90,6 +96,11 @@ impl<S> SslStream<S> {
         self.0.ssl()
     }
 
+    /// Returns a mutable reference to the `Ssl` object associated with this stream.
+    pub fn ssl_mut(&mut self) -> &mut SslRef {
+        self.0.ssl_mut()
+    }
+
     /// Returns a shared reference to the underlying stream.
     pub fn get_ref(&self) -> &S {
         &self.0.get_ref().stream
@@ -285,15 +296,20 @@ where
         let mut mid_handshake = self.0.take().expect("future polled after completion");
 
         mid_handshake.get_mut().set_waker(Some(ctx));
+        mid_handshake
+            .ssl_mut()
+            .set_ex_data(*TASK_WAKER_INDEX, Some(ctx.waker().clone()));
 
         match mid_handshake.handshake() {
             Ok(mut stream) => {
                 stream.get_mut().set_waker(None);
+                stream.ssl_mut().set_ex_data(*TASK_WAKER_INDEX, None);
 
                 Poll::Ready(Ok(SslStream(stream)))
             }
             Err(ssl::HandshakeError::WouldBlock(mut mid_handshake)) => {
                 mid_handshake.get_mut().set_waker(None);
+                mid_handshake.ssl_mut().set_ex_data(*TASK_WAKER_INDEX, None);
 
                 self.0 = Some(mid_handshake);
 
diff --git a/tokio-boring/tests/client_server.rs b/tokio-boring/tests/client_server.rs
index 00d789b6..719ffbf5 100644
--- a/tokio-boring/tests/client_server.rs
+++ b/tokio-boring/tests/client_server.rs
@@ -1,12 +1,16 @@
 use boring::error::ErrorStack;
-use boring::ssl::{SslAcceptor, SslConnector, SslConnectorBuilder, SslFiletype, SslMethod};
+use boring::ssl::{
+    ClientHello, SslAcceptor, SslAcceptorBuilder, SslConnector, SslConnectorBuilder, SslFiletype,
+    SslMethod,
+};
 use futures::future;
-use std::future::Future;
+use std::future::{Future, Pending};
 use std::net::{SocketAddr, ToSocketAddrs};
 use std::pin::Pin;
 use tokio::io::{AsyncReadExt, AsyncWrite, AsyncWriteExt};
 use tokio::net::{TcpListener, TcpStream};
-use tokio_boring::{HandshakeError, SslStream};
+use tokio::task::yield_now;
+use tokio_boring::{AsyncSelectCertError, HandshakeError, SslContextBuilderExt, SslStream};
 
 #[tokio::test]
 async fn google() {
@@ -34,7 +38,9 @@ async fn google() {
     assert!(response.ends_with("</html>") || response.ends_with("</HTML>"));
 }
 
-fn create_server() -> (
+fn create_server(
+    setup: impl FnOnce(&mut SslAcceptorBuilder),
+) -> (
     impl Future<Output = Result<SslStream<TcpStream>, HandshakeError<TcpStream>>>,
     SocketAddr,
 ) {
@@ -56,8 +62,9 @@ fn create_server() -> (
             .set_certificate_chain_file("tests/cert.pem")
             .unwrap();
 
-        let acceptor = acceptor.build();
+        setup(&mut acceptor);
 
+        let acceptor = acceptor.build();
         let stream = listener.accept().await.unwrap().0;
 
         tokio_boring::accept(&acceptor, stream).await
@@ -75,7 +82,6 @@ async fn connect(
     setup(&mut connector).unwrap();
 
     let config = connector.build().configure().unwrap();
-
     let stream = TcpStream::connect(&addr).await.unwrap();
 
     tokio_boring::connect(config, "localhost", stream).await
@@ -83,7 +89,98 @@ async fn connect(
 
 #[tokio::test]
 async fn server() {
-    let (stream, addr) = create_server();
+    with_trivial_client_server_exchange(|_| ()).await;
+}
+
+#[tokio::test]
+async fn test_async_select_certificate_callback_trivial() {
+    with_trivial_client_server_exchange(|builder| {
+        builder.set_async_select_certificate_callback(|_| {
+            Ok(async move { Ok(|_: ClientHello<'_>| Ok(())) })
+        });
+    })
+    .await;
+}
+
+#[tokio::test]
+async fn test_async_select_certificate_callback_yield() {
+    with_trivial_client_server_exchange(|builder| {
+        builder.set_async_select_certificate_callback(|_| {
+            Ok(async move {
+                yield_now().await;
+
+                Ok(|_: ClientHello<'_>| Ok(()))
+            })
+        });
+    })
+    .await;
+}
+
+#[tokio::test]
+async fn test_async_select_certificate_callback_return_error() {
+    with_async_select_certificate_callback_error::<_, Pending<_>, fn(_: ClientHello<'_>) -> _>(
+        |_| Err(AsyncSelectCertError),
+    )
+    .await;
+}
+
+#[tokio::test]
+async fn test_async_select_certificate_callback_future_error() {
+    with_async_select_certificate_callback_error::<_, _, fn(_: ClientHello<'_>) -> _>(|_| {
+        Ok(async move { Err(AsyncSelectCertError) })
+    })
+    .await;
+}
+
+#[tokio::test]
+async fn test_async_select_certificate_callback_future_yield_error() {
+    with_async_select_certificate_callback_error::<_, _, fn(_: ClientHello<'_>) -> _>(|_| {
+        Ok(async move {
+            yield_now().await;
+
+            Err(AsyncSelectCertError)
+        })
+    })
+    .await;
+}
+
+#[tokio::test]
+async fn test_async_select_certificate_callback_finish_error() {
+    with_async_select_certificate_callback_error(|_| {
+        Ok(async move {
+            yield_now().await;
+
+            Ok(|_: ClientHello<'_>| Err(AsyncSelectCertError))
+        })
+    })
+    .await;
+}
+
+async fn with_async_select_certificate_callback_error<Init, Fut, Finish>(callback: Init)
+where
+    Init: Fn(&mut ClientHello<'_>) -> Result<Fut, AsyncSelectCertError> + Send + Sync + 'static,
+    Fut: Future<Output = Result<Finish, AsyncSelectCertError>> + Send + Sync + 'static,
+    Finish: FnOnce(ClientHello<'_>) -> Result<(), AsyncSelectCertError> + 'static,
+{
+    let (stream, addr) = create_server(|builder| {
+        builder.set_async_select_certificate_callback(callback);
+    });
+
+    let server = async {
+        let _err = stream.await.unwrap_err();
+    };
+
+    let client = async {
+        let _err = connect(addr, |builder| builder.set_ca_file("tests/cert.pem"))
+            .await
+            .unwrap_err();
+    };
+
+    future::join(server, client).await;
+}
+
+async fn with_trivial_client_server_exchange(server_setup: impl FnOnce(&mut SslAcceptorBuilder)) {
+    let (stream, addr) = create_server(server_setup);
 
     let server = async {
         let mut stream = stream.await.unwrap();
@@ -115,7 +212,7 @@ async fn server() {
 
 #[tokio::test]
 async fn handshake_error() {
-    let (stream, addr) = create_server();
+    let (stream, addr) = create_server(|_| ());
 
     let server = async {
         let err = stream.await.unwrap_err();