rust: bssl-tls: Introduce hyper support

Signed-off-by: Xiangfei Ding <xfding@google.com>
Change-Id: I1987731cefdf073ea08fb9c698cfa71d6a6a6964
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/100047
Reviewed-by: Adam Langley <agl@google.com>
Reviewed-by: Rudolf Polzer <rpolzer@google.com>
diff --git a/rust/Cargo.lock b/rust/Cargo.lock
index f24e291..c5e4524 100644
--- a/rust/Cargo.lock
+++ b/rust/Cargo.lock
@@ -3,6 +3,12 @@
 version = 4
 
 [[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
 name = "bitflags"
 version = "2.12.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -58,7 +64,10 @@
  "bssl-tls",
  "bssl-x509",
  "futures",
+ "hyper",
+ "hyper-util",
  "tokio",
+ "tower",
 ]
 
 [[package]]
@@ -100,6 +109,12 @@
 checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
 
 [[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
 name = "errno"
 version = "0.3.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -116,6 +131,12 @@
 checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
 
 [[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
 name = "futures"
 version = "0.3.32"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -215,6 +236,115 @@
 ]
 
 [[package]]
+name = "h2"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
+
+[[package]]
+name = "http"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "hyper"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
+dependencies = [
+ "bytes",
+ "http",
+ "http-body",
+ "hyper",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
 name = "lazy_static"
 version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -495,11 +625,48 @@
 ]
 
 [[package]]
+name = "tokio-util"
+version = "0.7.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tower"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
+dependencies = [
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
 name = "tracing"
 version = "0.1.44"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
 dependencies = [
+ "log",
  "pin-project-lite",
  "tracing-attributes",
  "tracing-core",
@@ -552,6 +719,12 @@
 ]
 
 [[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
 name = "unicode-ident"
 version = "1.0.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -570,6 +743,15 @@
 checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
 
 [[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
 name = "wasi"
 version = "0.11.1+wasi-snapshot-preview1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/rust/bssl-tls-tokio/Cargo.toml b/rust/bssl-tls-tokio/Cargo.toml
index 33af238..93dfac3 100644
--- a/rust/bssl-tls-tokio/Cargo.toml
+++ b/rust/bssl-tls-tokio/Cargo.toml
@@ -5,11 +5,31 @@
 publish = false
 license = "Apache-2.0"
 
-[dependencies]
-tokio = { version = "1.0", default-features = false, features = ["net"] }
-bssl-tls = { path = "../bssl-tls" }
+[dependencies.tokio]
+version = "1.0"
+default-features = false
+features = ["net"]
+
+[dependencies.bssl-tls]
+path = "../bssl-tls"
+features = ["std"]
+
+[dependencies.hyper]
+version = "1.0"
+optional = true
+features = ["client", "http1", "http2"]
+
+[dependencies.tower]
+version = "0.4"
+optional = true
 
 [dev-dependencies]
 tokio = { version = "1.0", features = ["full"] }
 futures = "0.3"
 bssl-x509 = { path = "../bssl-x509" }
+hyper = { version = "1.0", features = ["client", "server", "http1", "http2"] }
+hyper-util = { version = "0.1", features = ["tokio"] }
+
+[features]
+# `hyper` enables interop with `hyper` 1.0 and `tower`
+hyper = ["dep:hyper", "dep:tower"]
diff --git a/rust/bssl-tls-tokio/src/hyper.rs b/rust/bssl-tls-tokio/src/hyper.rs
new file mode 100644
index 0000000..850d155
--- /dev/null
+++ b/rust/bssl-tls-tokio/src/hyper.rs
@@ -0,0 +1,92 @@
+// Copyright 2026 The BoringSSL Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Hyper support
+
+use crate::TlsConnector;
+
+use std::{
+    error::Error,
+    fmt::Debug,
+    future::Future,
+    pin::Pin,
+    sync::Arc,
+    task::{
+        Context,
+        Poll, //
+    }, //
+};
+
+use hyper::http;
+use tower::Service;
+
+/// A connector for `hyper` using `bssl-tls`.
+#[derive(Clone)]
+pub struct HyperBsslConnector<Inner> {
+    inner: Inner,
+    connector: Arc<TlsConnector>,
+}
+
+impl<Inner: Debug> std::fmt::Debug for HyperBsslConnector<Inner> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("HyperBsslConnector")
+            .field("inner", &self.inner)
+            .finish()
+    }
+}
+
+impl<Inner> HyperBsslConnector<Inner> {
+    /// Construct a new `HyperBsslConnector`.
+    pub fn new(inner: Inner, connector: TlsConnector) -> Self {
+        Self {
+            inner,
+            connector: Arc::new(connector),
+        }
+    }
+}
+
+impl<Inner> Service<http::Uri> for HyperBsslConnector<Inner>
+where
+    Inner: Service<http::Uri>,
+    Inner::Response: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + Sync + 'static,
+    Inner::Future: Send + 'static,
+    Inner::Error: Into<Box<dyn Error + Send + Sync>>,
+{
+    type Response = crate::TlsStream<bssl_tls::connection::Client, Inner::Response>;
+    type Error = Box<dyn Error + Send + Sync>;
+    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
+
+    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
+        self.inner.poll_ready(cx).map_err(Into::into)
+    }
+
+    fn call(&mut self, uri: http::Uri) -> Self::Future {
+        let domain = uri
+            .host()
+            .unwrap_or("")
+            .trim_start_matches('[')
+            .trim_end_matches(']')
+            .to_string();
+        if domain.is_empty() {
+            return Box::pin(std::future::ready(Err("empty domain".into())));
+        }
+        let fut = self.inner.call(uri);
+        let connector = self.connector.clone();
+
+        Box::pin(async move {
+            let stream = fut.await.map_err(Into::into)?;
+            Ok(connector.connect(&domain, stream).await?)
+        })
+    }
+}
diff --git a/rust/bssl-tls-tokio/src/lib.rs b/rust/bssl-tls-tokio/src/lib.rs
index 9656a83..c1fe4c2 100644
--- a/rust/bssl-tls-tokio/src/lib.rs
+++ b/rust/bssl-tls-tokio/src/lib.rs
@@ -629,3 +629,6 @@
         TlsAcceptor::new(self.build())
     }
 }
+
+#[cfg(feature = "hyper")]
+pub mod hyper;
diff --git a/rust/bssl-tls-tokio/src/tests.rs b/rust/bssl-tls-tokio/src/tests.rs
index 0aa204e..408c67c 100644
--- a/rust/bssl-tls-tokio/src/tests.rs
+++ b/rust/bssl-tls-tokio/src/tests.rs
@@ -19,3 +19,48 @@
 const CA: &[u8] = include_bytes!("../../test-data/BoringSSLCATest.crt");
 const RSA_SERVER_CERT: &[u8] = include_bytes!("../../test-data/BoringSSLServerTest-RSA.crt");
 const RSA_SERVER_KEY: &[u8] = include_bytes!("../../test-data/BoringSSLServerTest-RSA.key");
+
+use bssl_tls::{
+    context::TlsContextBuilder,
+    credentials::{
+        Certificate,
+        TlsCredentialBuilder, //
+    },
+};
+use bssl_x509::{
+    certificates::X509Certificate,
+    keys::PrivateKey,
+    params::Trust,
+    store::X509StoreBuilder, //
+};
+
+/// Returns a `(server_ctx_builder, client_ctx_builder)` pair with certs/keys
+/// pre-configured for TLS testing.
+fn tls_ctx_builders() -> (TlsContextBuilder, TlsContextBuilder) {
+    let ca = Certificate::parse_one_from_pem(CA, None).unwrap();
+    let server_cert = Certificate::parse_one_from_pem(RSA_SERVER_CERT, None).unwrap();
+    let server_key = PrivateKey::from_pem(RSA_SERVER_KEY, || unreachable!()).unwrap();
+
+    let mut server_builder = TlsContextBuilder::new_tls();
+    let server_cred = {
+        let mut builder = TlsCredentialBuilder::new();
+        builder
+            .with_certificate_chain(&[server_cert, ca])
+            .unwrap()
+            .with_private_key(server_key)
+            .unwrap();
+        builder.build().unwrap()
+    };
+    server_builder.with_credential(server_cred).unwrap();
+
+    let mut client_builder = TlsContextBuilder::new_tls();
+    let mut cert_store = X509StoreBuilder::new();
+    cert_store
+        .set_trust(Trust::SslServer)
+        .unwrap()
+        .add_cert(X509Certificate::parse_one_from_pem(CA).unwrap())
+        .unwrap();
+    client_builder.with_certificate_store(&cert_store.build());
+
+    (server_builder, client_builder)
+}
diff --git a/rust/bssl-tls-tokio/src/tests/convenience.rs b/rust/bssl-tls-tokio/src/tests/convenience.rs
index 12a080f..7d459b0 100644
--- a/rust/bssl-tls-tokio/src/tests/convenience.rs
+++ b/rust/bssl-tls-tokio/src/tests/convenience.rs
@@ -12,56 +12,18 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-use bssl_tls::{
-    context::TlsContextBuilder,
-    credentials::{
-        Certificate,
-        TlsCredentialBuilder, //
-    }, //
-};
-use bssl_x509::{
-    certificates::X509Certificate,
-    keys::PrivateKey,
-    params::Trust,
-    store::X509StoreBuilder, //
-};
 use tokio::io::{
     AsyncReadExt,
     AsyncWriteExt, //
 };
 
-use super::{
-    CA,
-    RSA_SERVER_CERT,
-    RSA_SERVER_KEY, //
-};
 use crate::TokioTlsExt;
 
 #[tokio::test]
 async fn high_level_tokio() -> Result<(), bssl_tls::errors::Error> {
-    let ca = Certificate::parse_one_from_pem(CA, None)?;
-    let server_cert = Certificate::parse_one_from_pem(RSA_SERVER_CERT, None)?;
-    let server_key = PrivateKey::from_pem(RSA_SERVER_KEY, || unreachable!())?;
-
-    let mut server_ctx_builder = TlsContextBuilder::new_tls();
-    let server_cred = {
-        let mut builder = TlsCredentialBuilder::new();
-        builder
-            .with_certificate_chain(&[server_cert, ca])?
-            .with_private_key(server_key)?;
-        builder.build()
-    };
-    server_ctx_builder.with_credential(server_cred.unwrap())?;
-    let mut builder = TlsContextBuilder::new_tls();
-    let ca = X509Certificate::parse_one_from_pem(CA)?;
-    let store = {
-        let mut store = X509StoreBuilder::new();
-        store.set_trust(Trust::SslServer)?.add_cert(ca)?;
-        store.build()
-    };
-    builder.with_certificate_store(&store);
-    let connector = builder.build_tokio_connector();
-    let acceptor = server_ctx_builder.build_tokio_acceptor();
+    let (server_builder, client_builder) = super::tls_ctx_builders();
+    let connector = client_builder.build_tokio_connector();
+    let acceptor = server_builder.build_tokio_acceptor();
 
     let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
     let addr = listener.local_addr().unwrap();
diff --git a/rust/bssl-tls-tokio/src/tests/datagram.rs b/rust/bssl-tls-tokio/src/tests/datagram.rs
index 77a18e3..e88ea33 100644
--- a/rust/bssl-tls-tokio/src/tests/datagram.rs
+++ b/rust/bssl-tls-tokio/src/tests/datagram.rs
@@ -47,39 +47,42 @@
     new_std_datagram_with_tokio, //
 };
 
-fn dumb_dtls_server_client() -> Result<
-    (
-        TlsConnection<Server, DtlsMode>,
-        TlsConnection<Client, DtlsMode>,
-    ),
-    Error,
-> {
-    let ca = Certificate::parse_one_from_pem(CA, None)?;
-    let server_cert = Certificate::parse_one_from_pem(RSA_SERVER_CERT, None)?;
-    let server_key = PrivateKey::from_pem(RSA_SERVER_KEY, || unreachable!())?;
+fn dumb_dtls_server_client() -> (
+    TlsConnection<Server, DtlsMode>,
+    TlsConnection<Client, DtlsMode>,
+) {
+    let ca = Certificate::parse_one_from_pem(CA, None).unwrap();
+    let server_cert = Certificate::parse_one_from_pem(RSA_SERVER_CERT, None).unwrap();
+    let server_key = PrivateKey::from_pem(RSA_SERVER_KEY, || unreachable!()).unwrap();
 
     let mut server_ctx_builder = TlsContextBuilder::new_dtls();
     let server_cred = {
         let mut builder = TlsCredentialBuilder::new();
         builder
-            .with_certificate_chain(&[server_cert, ca])?
-            .with_private_key(server_key)?;
-        builder.build()
+            .with_certificate_chain(&[server_cert, ca])
+            .unwrap()
+            .with_private_key(server_key)
+            .unwrap();
+        builder.build().unwrap()
     };
-    server_ctx_builder.with_credential(server_cred.unwrap())?;
+    server_ctx_builder.with_credential(server_cred).unwrap();
     let server_ctx = server_ctx_builder.build();
     let server_conn = server_ctx.new_server_connection().build();
 
     let mut client_ctx_builder = TlsContextBuilder::new_dtls();
-    let ca = X509Certificate::parse_one_from_pem(CA)?;
+    let ca = X509Certificate::parse_one_from_pem(CA).unwrap();
     let mut cert_store = X509StoreBuilder::new();
-    cert_store.set_trust(Trust::SslServer)?.add_cert(ca)?;
+    cert_store
+        .set_trust(Trust::SslServer)
+        .unwrap()
+        .add_cert(ca)
+        .unwrap();
     let cert_store = cert_store.build();
     client_ctx_builder.with_certificate_store(&cert_store);
     let client_ctx = client_ctx_builder.build();
     let client_conn = client_ctx.new_client_connection().build();
 
-    Ok((server_conn, client_conn))
+    (server_conn, client_conn)
 }
 
 async fn async_ping_pong(
@@ -146,7 +149,7 @@
 #[tokio::test]
 #[ignore = "https://crbug.com/532601068"]
 async fn async_dtls() -> Result<(), Error> {
-    let (mut server_conn, mut client_conn) = dumb_dtls_server_client().unwrap();
+    let (mut server_conn, mut client_conn) = dumb_dtls_server_client();
     let (server_sock, client_sock) = tokio::net::UnixDatagram::pair().unwrap();
     server_conn.set_io(TokioDatagramIo(server_sock)).unwrap();
     client_conn.set_io(TokioDatagramIo(client_sock)).unwrap();
@@ -158,7 +161,7 @@
 #[tokio::test]
 #[ignore = "https://crbug.com/532601068"]
 async fn async_dtls_over_fd() -> Result<(), Error> {
-    let (mut server_conn, mut client_conn) = dumb_dtls_server_client().unwrap();
+    let (mut server_conn, mut client_conn) = dumb_dtls_server_client();
     let (server_sock, client_sock) = std::os::unix::net::UnixDatagram::pair().unwrap();
     server_sock.set_nonblocking(true).unwrap();
     client_sock.set_nonblocking(true).unwrap();
diff --git a/rust/bssl-tls-tokio/src/tests/transport.rs b/rust/bssl-tls-tokio/src/tests/transport.rs
index b094c55..5301825 100644
--- a/rust/bssl-tls-tokio/src/tests/transport.rs
+++ b/rust/bssl-tls-tokio/src/tests/transport.rs
@@ -18,69 +18,30 @@
         Server,
         TlsConnection, //
     },
-    context::TlsContextBuilder,
-    credentials::{
-        Certificate,
-        TlsCredentialBuilder, //
-    },
     errors::Error, //
 };
-use bssl_x509::{
-    certificates::X509Certificate,
-    keys::PrivateKey,
-    params::Trust,
-    store::X509StoreBuilder, //
-};
 use futures::future::FutureExt;
 use tokio::io::{
     AsyncReadExt,
     AsyncWriteExt, //
 };
 
-use super::{
-    CA,
-    RSA_SERVER_CERT,
-    RSA_SERVER_KEY, //
-};
 use crate::{
     TokioIo,
     TokioTlsConnection, //
 };
 
-fn dumb_server_client() -> Result<(TlsConnection<Server>, TlsConnection<Client>), Error> {
-    let ca = Certificate::parse_one_from_pem(CA, None)?;
-    let server_cert = Certificate::parse_one_from_pem(RSA_SERVER_CERT, None)?;
-    let server_key = PrivateKey::from_pem(RSA_SERVER_KEY, || unreachable!())?;
-
-    let mut server_ctx_builder = TlsContextBuilder::new_tls();
-    let server_cred = {
-        let mut builder = TlsCredentialBuilder::new();
-        builder
-            .with_certificate_chain(&[server_cert, ca])?
-            .with_private_key(server_key)?;
-        builder.build()
-    };
-    server_ctx_builder.with_credential(server_cred.unwrap())?;
-    let server_ctx = server_ctx_builder.build();
-    let server_conn = server_ctx.new_server_connection().build();
-
-    let mut client_ctx_builder = TlsContextBuilder::new_tls();
-    let mut cert_store = X509StoreBuilder::new();
-    cert_store
-        .set_trust(Trust::SslServer)?
-        .add_cert(X509Certificate::parse_one_from_pem(&CA)?)?;
-    let cert_store = cert_store.build();
-    client_ctx_builder.with_certificate_store(&cert_store);
-    let client_ctx = client_ctx_builder.build();
-    let client_conn = client_ctx.new_client_connection().build();
-
-    Ok((server_conn, client_conn))
+fn dumb_server_client() -> (TlsConnection<Server>, TlsConnection<Client>) {
+    let (server_builder, client_builder) = super::tls_ctx_builders();
+    let server_conn = server_builder.build().new_server_connection().build();
+    let client_conn = client_builder.build().new_client_connection().build();
+    (server_conn, client_conn)
 }
 
 #[cfg(unix)]
 #[tokio::test]
 async fn tokio_io() -> Result<(), Error> {
-    let (server_conn, client_conn) = dumb_server_client()?;
+    let (server_conn, client_conn) = dumb_server_client();
 
     let (server_tx, server_rx) = tokio::net::unix::pipe::pipe().unwrap();
     let (client_tx, client_rx) = tokio::net::unix::pipe::pipe().unwrap();
diff --git a/rust/bssl-tls-tokio/tests/hyper.rs b/rust/bssl-tls-tokio/tests/hyper.rs
new file mode 100644
index 0000000..27b52a7
--- /dev/null
+++ b/rust/bssl-tls-tokio/tests/hyper.rs
@@ -0,0 +1,184 @@
+// Copyright 2026 The BoringSSL Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#![cfg(feature = "hyper")]
+
+use bssl_tls::alpn::H2;
+use bssl_tls::context::TlsContextBuilder;
+use bssl_tls::credentials::{Certificate, TlsCredentialBuilder};
+use bssl_tls_tokio::TokioTlsExt;
+use bssl_tls_tokio::hyper::HyperBsslConnector;
+use bssl_x509::{
+    certificates::X509Certificate, keys::PrivateKey, params::Trust, store::X509StoreBuilder,
+};
+use hyper::body::{Body, Bytes, Frame};
+use hyper::service::service_fn;
+use hyper_util::rt::TokioIo as HyperTokioIo;
+use std::convert::Infallible;
+use std::future::Future;
+use std::pin::Pin;
+use std::task::{Context, Poll};
+use tokio::net::{TcpListener, TcpStream};
+use tower::Service;
+
+const CA: &[u8] = include_bytes!("../../test-data/BoringSSLCATest.crt");
+const RSA_SERVER_CERT: &[u8] = include_bytes!("../../test-data/BoringSSLServerTest-RSA.crt");
+const RSA_SERVER_KEY: &[u8] = include_bytes!("../../test-data/BoringSSLServerTest-RSA.key");
+
+/// A body that yields a single data frame, or is empty.
+struct SimpleBody(Option<Bytes>);
+
+impl SimpleBody {
+    fn new(data: &'static str) -> Self {
+        Self(Some(Bytes::from(data)))
+    }
+
+    fn empty() -> Self {
+        Self(None)
+    }
+}
+
+impl Body for SimpleBody {
+    type Data = Bytes;
+    type Error = Infallible;
+
+    fn poll_frame(
+        mut self: Pin<&mut Self>,
+        _cx: &mut Context<'_>,
+    ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
+        match self.0.take() {
+            Some(data) => Poll::Ready(Some(Ok(Frame::data(data)))),
+            None => Poll::Ready(None),
+        }
+    }
+}
+
+/// A mock TCP connector that connects to a fixed address, implementing
+/// `tower::Service<Uri>` so it can be wrapped in [`HyperBsslConnector`].
+struct MockTcpConnector {
+    addr: std::net::SocketAddr,
+}
+
+impl Service<hyper::http::Uri> for MockTcpConnector {
+    type Response = TcpStream;
+    type Error = std::io::Error;
+    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
+
+    fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
+        Poll::Ready(Ok(()))
+    }
+
+    fn call(&mut self, _: hyper::http::Uri) -> Self::Future {
+        let addr = self.addr;
+        Box::pin(TcpStream::connect(addr))
+    }
+}
+
+/// Sends an HTTP/2 request over TLS using `HyperBsslConnector` and verifies
+/// that a hyper HTTP/2 server receives and responds correctly.
+#[tokio::test]
+async fn test_hyper_h2_roundtrip() {
+    // Bind to an ephemeral port.
+    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
+    let addr = listener.local_addr().unwrap();
+
+    // Set up the TLS server context.
+    let ca = Certificate::parse_one_from_pem(CA, None).unwrap();
+    let server_cert = Certificate::parse_one_from_pem(RSA_SERVER_CERT, None).unwrap();
+    let server_key = PrivateKey::from_pem(RSA_SERVER_KEY, || unreachable!()).unwrap();
+    let mut server_ctx_builder = TlsContextBuilder::new_tls();
+    let server_cred = {
+        let mut builder = TlsCredentialBuilder::new();
+        builder
+            .with_certificate_chain(&[server_cert, ca])
+            .unwrap()
+            .with_private_key(server_key)
+            .unwrap();
+        builder.build().unwrap()
+    };
+    server_ctx_builder.with_credential(server_cred).unwrap();
+    server_ctx_builder.set_alpn_protocols([H2]).unwrap();
+    let acceptor = server_ctx_builder.build_tokio_acceptor();
+
+    // Server: accept a connection, do TLS, then serve one HTTP/2 request.
+    let server_fut = async move {
+        let (stream, _) = listener.accept().await.unwrap();
+        let tls_stream = acceptor.accept(stream).await.unwrap();
+
+        hyper::server::conn::http2::Builder::new(hyper_util::rt::TokioExecutor::new())
+            .serve_connection(
+                HyperTokioIo::new(tls_stream),
+                service_fn(|_req| async {
+                    Ok::<_, hyper::Error>(hyper::Response::new(SimpleBody::new(
+                        "hello from h2 server",
+                    )))
+                }),
+            )
+            .await
+            .unwrap();
+    };
+
+    // Client: use HyperBsslConnector to establish TLS, then do HTTP/2.
+    let client_fut = async move {
+        let mock_connector = MockTcpConnector { addr };
+
+        let mut client_ctx_builder = TlsContextBuilder::new_tls();
+        let mut cert_store = X509StoreBuilder::new();
+        cert_store
+            .set_trust(Trust::SslServer)
+            .unwrap()
+            .add_cert(X509Certificate::parse_one_from_pem(CA).unwrap())
+            .unwrap();
+        client_ctx_builder.with_certificate_store(&cert_store.build());
+        client_ctx_builder.set_alpn_protocols([H2]).unwrap();
+        let connector = client_ctx_builder.build_tokio_connector();
+
+        let mut hyper_connector = HyperBsslConnector::new(mock_connector, connector);
+        let tls_stream = hyper_connector
+            .call("https://localhost/".parse().unwrap())
+            .await
+            .unwrap();
+
+        let (mut sender, conn) = hyper::client::conn::http2::handshake(
+            hyper_util::rt::TokioExecutor::new(),
+            HyperTokioIo::new(tls_stream),
+        )
+        .await
+        .unwrap();
+
+        // Drive the connection in the background.
+        tokio::spawn(async move {
+            conn.await.unwrap();
+        });
+
+        let req = hyper::Request::get("/").body(SimpleBody::empty()).unwrap();
+        let resp = sender.send_request(req).await.unwrap();
+        assert_eq!(resp.status(), 200);
+
+        // Read the response body frame by frame.
+        let body = resp.into_body();
+        let mut body = std::pin::pin!(body);
+        let mut result = Vec::new();
+        while let Some(frame) = futures::future::poll_fn(|cx| body.as_mut().poll_frame(cx)).await {
+            if let Ok(frame) = frame {
+                if let Some(data) = frame.data_ref() {
+                    result.extend_from_slice(data);
+                }
+            }
+        }
+        assert_eq!(result, b"hello from h2 server");
+    };
+
+    tokio::join!(server_fut, client_fut);
+}