rust: bssl-tls: Add convenient adapters

This patch provies convenience to match rustls when the socket
configuration is very simple.

Bug: 479599893
Signed-off-by: Xiangfei Ding <xfding@google.com>
Change-Id: Ibf8f7fc7c6de5138eaabf34910213e3f6a6a6964
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/92469
Reviewed-by: Adam Langley <agl@google.com>
diff --git a/rust/bssl-tls-tokio/src/lib.rs b/rust/bssl-tls-tokio/src/lib.rs
index 87c345e..a439368 100644
--- a/rust/bssl-tls-tokio/src/lib.rs
+++ b/rust/bssl-tls-tokio/src/lib.rs
@@ -113,6 +113,7 @@
 };
 use std::{
     io,
+    marker::PhantomData,
     ops::{
         Deref,
         DerefMut, //
@@ -141,16 +142,27 @@
 };
 use bssl_tls::{
     connection::{
+        Client,
+        Server,
         TlsConnection,
         lifecycle::ShutdownStatus, //
     },
-    context::TlsMode,
+    context::{
+        DtlsMode,
+        TlsContext,
+        TlsContextBuilder,
+        TlsMode, //
+    },
+    errors::Error,
     io::{
         AbstractReader, AbstractSocket, AbstractSocketResult, AbstractWriter, IoStatus,
         NoAsyncContext, stdio::PollFor,
     }, //
 };
 
+#[cfg(test)]
+mod tests;
+
 /// Translates a `std::io::Error` into an `AbstractSocketResult`.
 fn translate_stdio_err(err: io::Error) -> AbstractSocketResult {
     match err.kind() {
@@ -495,3 +507,227 @@
         }
     }
 }
+
+/// A wrapper around `TlsContext` for creating Tokio-based client connections.
+pub struct TlsConnector {
+    ctx: TlsContext,
+}
+
+impl TlsConnector {
+    /// Construct a new `TlsConnector`.
+    pub(crate) fn new(ctx: TlsContext) -> Self {
+        Self { ctx }
+    }
+
+    /// Connect to the given domain using the provided stream.
+    pub async fn connect<S>(&self, domain: &str, stream: S) -> Result<TlsStream<Client, S>, Error>
+    where
+        S: AsyncRead + AsyncWrite + Send + Unpin + 'static,
+    {
+        let mut conn = self.ctx.new_client_connection(None)?.build();
+        conn.in_handshake().unwrap().set_host(domain)?;
+
+        conn.set_io(TokioIo(stream))?;
+
+        conn.async_handshake().await?;
+
+        Ok(TlsStream {
+            conn: TokioTlsConnection::new(conn),
+            _marker: PhantomData,
+        })
+    }
+}
+
+/// A wrapper around `TlsContext` for creating Tokio-based server connections.
+pub struct TlsAcceptor {
+    ctx: TlsContext,
+}
+
+impl TlsAcceptor {
+    /// Construct a new `TlsAcceptor`.
+    pub(crate) fn new(ctx: TlsContext) -> Self {
+        Self { ctx }
+    }
+
+    /// Accept a new connection using the provided stream.
+    pub async fn accept<S>(&self, stream: S) -> Result<TlsStream<Server, S>, Error>
+    where
+        S: AsyncRead + AsyncWrite + Send + Unpin + 'static,
+    {
+        let mut conn = self.ctx.new_server_connection(None)?.build();
+
+        conn.set_io(TokioIo(stream))?;
+
+        conn.async_handshake().await?;
+
+        Ok(TlsStream {
+            conn: TokioTlsConnection::new(conn),
+            _marker: PhantomData,
+        })
+    }
+}
+
+/// A TLS stream driven by Tokio I/O.
+pub struct TlsStream<Role, Stream> {
+    conn: TokioTlsConnection<Role>,
+    _marker: PhantomData<Stream>,
+}
+
+impl<Role, S> TlsStream<Role, S> {
+    /// Get a reference to the underlying `TlsConnection`.
+    pub fn get_ref(&self) -> &TlsConnection<Role> {
+        &self.conn
+    }
+
+    /// Get a mutable reference to the underlying `TlsConnection`.
+    pub fn get_mut(&mut self) -> &mut TlsConnection<Role> {
+        &mut self.conn
+    }
+}
+
+impl<Role, S: Unpin> AsyncRead for TlsStream<Role, S> {
+    fn poll_read(
+        mut self: Pin<&mut Self>,
+        cx: &mut Context<'_>,
+        buf: &mut ReadBuf<'_>,
+    ) -> Poll<io::Result<()>> {
+        Pin::new(&mut self.conn).poll_read(cx, buf)
+    }
+}
+
+impl<Role, S: Unpin> AsyncWrite for TlsStream<Role, S> {
+    fn poll_write(
+        mut self: Pin<&mut Self>,
+        cx: &mut Context<'_>,
+        buf: &[u8],
+    ) -> Poll<io::Result<usize>> {
+        Pin::new(&mut self.conn).poll_write(cx, buf)
+    }
+
+    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
+        Pin::new(&mut self.conn).poll_flush(cx)
+    }
+
+    fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
+        Pin::new(&mut self.conn).poll_shutdown(cx)
+    }
+}
+
+/// A wrapper around `TlsContext` for creating Tokio-based DTLS client connections.
+pub struct DtlsConnector {
+    ctx: TlsContext<DtlsMode>,
+}
+
+impl DtlsConnector {
+    /// Construct a new `DtlsConnector`.
+    pub(crate) fn new(ctx: TlsContext<DtlsMode>) -> Self {
+        Self { ctx }
+    }
+
+    /// Connect to the given domain using the provided datagram stream.
+    pub async fn connect<S>(&self, domain: &str, stream: S) -> Result<DtlsStream<Client, S>, Error>
+    where
+        S: AbstractSocket + Send + Unpin + 'static,
+    {
+        let mut conn = self.ctx.new_client_connection(None)?.build();
+        conn.in_handshake().unwrap().set_host(domain)?;
+        conn.set_io(stream)?;
+        conn.async_handshake().await?;
+
+        Ok(DtlsStream {
+            conn,
+            _marker: PhantomData,
+        })
+    }
+}
+
+/// A wrapper around `TlsContext` for creating Tokio-based DTLS server connections.
+pub struct DtlsAcceptor {
+    ctx: TlsContext<DtlsMode>,
+}
+
+impl DtlsAcceptor {
+    /// Construct a new `DtlsAcceptor`.
+    pub(crate) fn new(ctx: TlsContext<DtlsMode>) -> Self {
+        Self { ctx }
+    }
+
+    /// Accept a new connection using the provided datagram stream.
+    pub async fn accept<S>(&self, stream: S) -> Result<DtlsStream<Server, S>, Error>
+    where
+        S: AbstractSocket + Send + Unpin + 'static,
+    {
+        let mut conn = self.ctx.new_server_connection(None)?.build();
+        conn.set_io(stream)?;
+        conn.async_handshake().await?;
+
+        Ok(DtlsStream {
+            conn,
+            _marker: PhantomData,
+        })
+    }
+}
+
+/// A DTLS stream driven by Tokio.
+pub struct DtlsStream<Role, S> {
+    conn: TlsConnection<Role, DtlsMode>,
+    _marker: PhantomData<S>,
+}
+
+impl<Role, S> DtlsStream<Role, S> {
+    /// Get a reference to the underlying `TlsConnection`.
+    pub fn get_ref(&self) -> &TlsConnection<Role, DtlsMode> {
+        &self.conn
+    }
+
+    /// Get a mutable reference to the underlying `TlsConnection`.
+    pub fn get_mut(&mut self) -> &mut TlsConnection<Role, DtlsMode> {
+        &mut self.conn
+    }
+
+    /// Send application data over DTLS.
+    pub async fn send(&mut self, buf: &[u8]) -> Result<IoStatus, Error> {
+        self.conn.as_pin_mut().async_write(buf).await
+    }
+
+    /// Receive application data over DTLS.
+    pub async fn recv(&mut self, buf: &mut [u8]) -> Result<IoStatus, Error> {
+        self.conn.as_pin_mut().async_read(buf).await
+    }
+}
+
+/// Extension trait for `TlsContextBuilder` to support Tokio TLS.
+pub trait TokioTlsExt {
+    /// Build a `TlsConnector`.
+    fn build_tokio_connector(self) -> TlsConnector;
+    /// Build a `TlsAcceptor`.
+    fn build_tokio_acceptor(self) -> TlsAcceptor;
+}
+
+impl TokioTlsExt for TlsContextBuilder<TlsMode> {
+    fn build_tokio_connector(self) -> TlsConnector {
+        TlsConnector::new(self.build())
+    }
+
+    fn build_tokio_acceptor(self) -> TlsAcceptor {
+        TlsAcceptor::new(self.build())
+    }
+}
+
+/// Extension trait for `TlsContextBuilder` to support Tokio DTLS.
+pub trait TokioDtlsExt {
+    /// Build a `DtlsConnector`.
+    fn build_dtls_tokio_connector(self) -> DtlsConnector;
+    /// Build a `DtlsAcceptor`.
+    fn build_dtls_tokio_acceptor(self) -> DtlsAcceptor;
+}
+
+impl TokioDtlsExt for TlsContextBuilder<DtlsMode> {
+    fn build_dtls_tokio_connector(self) -> DtlsConnector {
+        DtlsConnector::new(self.build())
+    }
+
+    fn build_dtls_tokio_acceptor(self) -> DtlsAcceptor {
+        DtlsAcceptor::new(self.build())
+    }
+}
diff --git a/rust/bssl-tls-tokio/src/tests.rs b/rust/bssl-tls-tokio/src/tests.rs
new file mode 100644
index 0000000..0aa204e
--- /dev/null
+++ b/rust/bssl-tls-tokio/src/tests.rs
@@ -0,0 +1,21 @@
+// 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.
+
+mod convenience;
+mod datagram;
+mod transport;
+
+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");
diff --git a/rust/bssl-tls-tokio/src/tests/convenience.rs b/rust/bssl-tls-tokio/src/tests/convenience.rs
new file mode 100644
index 0000000..12a080f
--- /dev/null
+++ b/rust/bssl-tls-tokio/src/tests/convenience.rs
@@ -0,0 +1,94 @@
+// 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.
+
+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 listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
+    let addr = listener.local_addr().unwrap();
+
+    let server_task = tokio::spawn(async move {
+        let (stream, _) = listener.accept().await.unwrap();
+        let mut tls_stream = acceptor.accept(stream).await.unwrap();
+
+        let mut buf = [0; 5];
+        tls_stream.read_exact(&mut buf).await.unwrap();
+        assert_eq!(&buf, b"hello");
+
+        tls_stream.write_all(b"world").await.unwrap();
+        tls_stream.flush().await.unwrap();
+    });
+
+    let stream = tokio::net::TcpStream::connect(addr).await.unwrap();
+    let mut tls_stream = connector.connect("www.google.com", stream).await.unwrap();
+
+    tls_stream.write_all(b"hello").await.unwrap();
+    tls_stream.flush().await.unwrap();
+
+    let mut buf = [0; 5];
+    tls_stream.read_exact(&mut buf).await.unwrap();
+    assert_eq!(&buf, b"world");
+
+    server_task.await.unwrap();
+
+    Ok(())
+}
diff --git a/rust/bssl-tls-tokio/tests/datagram.rs b/rust/bssl-tls-tokio/src/tests/datagram.rs
similarity index 94%
rename from rust/bssl-tls-tokio/tests/datagram.rs
rename to rust/bssl-tls-tokio/src/tests/datagram.rs
index 4c39b69..4012d48 100644
--- a/rust/bssl-tls-tokio/tests/datagram.rs
+++ b/rust/bssl-tls-tokio/src/tests/datagram.rs
@@ -30,10 +30,6 @@
     },
     errors::Error, //
 };
-use bssl_tls_tokio::{
-    TokioDatagramIo,
-    new_std_datagram_with_tokio, //
-};
 use bssl_x509::{
     certificates::X509Certificate,
     keys::PrivateKey,
@@ -41,9 +37,15 @@
     store::X509StoreBuilder, //
 };
 
-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 super::{
+    CA,
+    RSA_SERVER_CERT,
+    RSA_SERVER_KEY, //
+};
+use crate::{
+    TokioDatagramIo,
+    new_std_datagram_with_tokio, //
+};
 
 fn dumb_dtls_server_client() -> Result<
     (
@@ -140,6 +142,7 @@
     Ok(())
 }
 
+#[cfg(unix)]
 #[tokio::test]
 #[ignore = "https://crbug.com/532601068"]
 async fn async_dtls() -> Result<(), Error> {
@@ -151,6 +154,7 @@
     async_ping_pong(server_conn, client_conn).await
 }
 
+#[cfg(unix)]
 #[tokio::test]
 #[ignore = "https://crbug.com/532601068"]
 async fn async_dtls_over_fd() -> Result<(), Error> {
diff --git a/rust/bssl-tls-tokio/tests/transport.rs b/rust/bssl-tls-tokio/src/tests/transport.rs
similarity index 92%
rename from rust/bssl-tls-tokio/tests/transport.rs
rename to rust/bssl-tls-tokio/src/tests/transport.rs
index 2be385d..16dcf01 100644
--- a/rust/bssl-tls-tokio/tests/transport.rs
+++ b/rust/bssl-tls-tokio/src/tests/transport.rs
@@ -12,8 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#![cfg(unix)]
-
 use bssl_tls::{
     connection::{
         Client,
@@ -27,26 +25,27 @@
     },
     errors::Error, //
 };
-use bssl_tls_tokio::{
-    TokioIo,
-    TokioTlsConnection, //
-};
 use bssl_x509::{
     certificates::X509Certificate,
     keys::PrivateKey,
     params::Trust,
     store::X509StoreBuilder, //
 };
+use futures::future::FutureExt;
 use tokio::io::{
     AsyncReadExt,
     AsyncWriteExt, //
 };
 
-use futures::future::FutureExt;
-
-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 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)?;
@@ -78,6 +77,7 @@
     Ok((server_conn, client_conn))
 }
 
+#[cfg(unix)]
 #[tokio::test]
 async fn tokio_io() -> Result<(), Error> {
     let (server_conn, client_conn) = dumb_server_client()?;
diff --git a/rust/bssl-tls/src/context.rs b/rust/bssl-tls/src/context.rs
index 88c0774..285f722 100644
--- a/rust/bssl-tls/src/context.rs
+++ b/rust/bssl-tls/src/context.rs
@@ -416,6 +416,19 @@
     }
 }
 
+#[cfg(feature = "std")]
+impl TlsContextBuilder<TlsMode> {
+    /// Builds and returns a synchronous `TlsConnector` using the configured context.
+    pub fn build_connector(self) -> crate::sync_io::TlsConnector {
+        crate::sync_io::TlsConnector::new(self.build())
+    }
+
+    /// Builds and returns a synchronous `TlsAcceptor` using the configured context.
+    pub fn build_acceptor(self) -> crate::sync_io::TlsAcceptor {
+        crate::sync_io::TlsAcceptor::new(self.build())
+    }
+}
+
 impl<M> Drop for TlsContextBuilder<M> {
     fn drop(&mut self) {
         unsafe {
diff --git a/rust/bssl-tls/src/lib.rs b/rust/bssl-tls/src/lib.rs
index 54fd6bd..bf949c7 100644
--- a/rust/bssl-tls/src/lib.rs
+++ b/rust/bssl-tls/src/lib.rs
@@ -45,6 +45,10 @@
 pub mod io;
 mod methods;
 pub mod sessions;
+#[cfg(feature = "std")]
+/// Synchronous I/O high-level APIs.
+pub mod sync_io;
+
 #[macro_use]
 #[doc(hidden)]
 mod macros;
diff --git a/rust/bssl-tls/src/sync_io.rs b/rust/bssl-tls/src/sync_io.rs
new file mode 100644
index 0000000..b61cf80
--- /dev/null
+++ b/rust/bssl-tls/src/sync_io.rs
@@ -0,0 +1,128 @@
+// 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.
+
+use crate::{
+    connection::{
+        Client,
+        Server,
+        TlsConnection, //
+    },
+    context::TlsContext,
+    io::sync_io::{NoAsync, StdIoWithReactor}, //
+};
+
+use std::{
+    io::{
+        Read,
+        Write, //
+    },
+    marker::PhantomData, //
+};
+
+/// A convenient wrapper around `TlsContext` for creating synchronous client connections.
+pub struct TlsConnector {
+    ctx: TlsContext,
+}
+
+impl TlsConnector {
+    /// Construct a new `TlsConnector`.
+    pub(crate) fn new(ctx: TlsContext) -> Self {
+        Self { ctx }
+    }
+
+    /// Connect to the given domain using the provided stream.
+    pub fn connect<S>(
+        &self,
+        domain: &str,
+        stream: S,
+    ) -> Result<TlsStream<Client, S>, crate::errors::Error>
+    where
+        S: Read + Write + Send + 'static,
+    {
+        let mut conn = self.ctx.new_client_connection(None)?.build();
+        {
+            conn.in_handshake()
+                .expect("connection is freshly constructed and it cannot already be established")
+                .set_host(domain)?;
+            conn.set_io(StdIoWithReactor::new(stream, NoAsync))?
+                .do_handshake()?;
+        }
+
+        Ok(TlsStream {
+            conn,
+            _marker: PhantomData,
+        })
+    }
+}
+
+/// A wrapper around `TlsContext` for creating synchronous server connections.
+pub struct TlsAcceptor {
+    ctx: TlsContext,
+}
+
+impl TlsAcceptor {
+    /// Construct a new `TlsAcceptor`.
+    pub(crate) fn new(ctx: TlsContext) -> Self {
+        Self { ctx }
+    }
+
+    /// Accept a new connection using the provided stream.
+    pub fn accept<S>(&self, stream: S) -> Result<TlsStream<Server, S>, crate::errors::Error>
+    where
+        S: Read + Write + Send + 'static,
+    {
+        let mut conn = self.ctx.new_server_connection(None)?.build();
+        conn.set_io(StdIoWithReactor::new(stream, NoAsync))?;
+        conn.do_handshake()?;
+
+        Ok(TlsStream {
+            conn,
+            _marker: PhantomData,
+        })
+    }
+}
+
+/// A TLS stream driven by synchronous I/O.
+pub struct TlsStream<Role, S> {
+    conn: TlsConnection<Role>,
+    _marker: PhantomData<S>,
+}
+
+impl<Role, S> TlsStream<Role, S> {
+    /// Get a reference to the underlying `TlsConnection`.
+    pub fn get_ref(&self) -> &TlsConnection<Role> {
+        &self.conn
+    }
+
+    /// Get a mutable reference to the underlying `TlsConnection`.
+    pub fn get_mut(&mut self) -> &mut TlsConnection<Role> {
+        &mut self.conn
+    }
+}
+
+impl<Role, S> Read for TlsStream<Role, S> {
+    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
+        self.conn.read(buf)
+    }
+}
+
+impl<Role, S> Write for TlsStream<Role, S> {
+    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
+        self.conn.write(buf)
+    }
+
+    fn flush(&mut self) -> std::io::Result<()> {
+        Write::flush(&mut self.conn)
+    }
+}
diff --git a/rust/bssl-tls/src/tests/transport.rs b/rust/bssl-tls/src/tests/transport.rs
index fc09f9e..1f24c8f 100644
--- a/rust/bssl-tls/src/tests/transport.rs
+++ b/rust/bssl-tls/src/tests/transport.rs
@@ -12,6 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+use bssl_x509::params::Trust;
+
 use crate::tests::dumb_server_client;
 
 #[cfg(unix)]
@@ -54,3 +56,68 @@
     client_conn.established().unwrap().sync_shutdown().unwrap();
     thread.join().unwrap();
 }
+
+#[cfg(feature = "std")]
+#[test]
+fn high_level_sync() -> Result<(), crate::errors::Error> {
+    use crate::context::TlsContextBuilder;
+    use crate::credentials::{Certificate, TlsCredentialBuilder};
+    use crate::tests::{CA, RSA_SERVER_CERT, RSA_SERVER_KEY};
+    use bssl_x509::certificates::X509Certificate;
+    use bssl_x509::keys::PrivateKey;
+    use bssl_x509::store::X509StoreBuilder;
+    use std::io::{Read, Write};
+
+    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_connector();
+    let acceptor = server_ctx_builder.build_acceptor();
+
+    let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+    let addr = listener.local_addr().unwrap();
+
+    let server_thread = std::thread::spawn(move || {
+        let (stream, _) = listener.accept().unwrap();
+        let mut tls_stream = acceptor.accept(stream).unwrap();
+
+        let mut buf = [0; 5];
+        tls_stream.read_exact(&mut buf).unwrap();
+        assert_eq!(&buf, b"hello");
+
+        tls_stream.write_all(b"world").unwrap();
+        tls_stream.flush().unwrap();
+    });
+
+    let stream = std::net::TcpStream::connect(addr).unwrap();
+    let mut tls_stream = connector.connect("www.google.com", stream).unwrap();
+
+    tls_stream.write_all(b"hello").unwrap();
+    tls_stream.flush().unwrap();
+
+    let mut buf = [0; 5];
+    tls_stream.read_exact(&mut buf).unwrap();
+    assert_eq!(&buf, b"world");
+
+    server_thread.join().unwrap();
+
+    Ok(())
+}