rust: bssl-tls: General functional tests

Bug: 479599893

Signed-off-by: Xiangfei Ding <xfding@google.com>
Change-Id: I7bc900c38a43ba00c936e679bfe927396a6a6964
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/92347
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/lib.rs b/rust/bssl-tls/src/lib.rs
index c52d705..1c43067 100644
--- a/rust/bssl-tls/src/lib.rs
+++ b/rust/bssl-tls/src/lib.rs
@@ -47,6 +47,9 @@
 #[macro_use]
 #[doc(hidden)]
 mod macros;
+#[cfg(test)]
+#[cfg(feature = "std")]
+mod tests;
 
 pub use ffi::ReceiveBuffer;
 
diff --git a/rust/bssl-tls/src/tests.rs b/rust/bssl-tls/src/tests.rs
new file mode 100644
index 0000000..166a95a
--- /dev/null
+++ b/rust/bssl-tls/src/tests.rs
@@ -0,0 +1,177 @@
+// 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 std::mem::MaybeUninit;
+
+use crate::{
+    ReceiveBuffer,
+    connection::{
+        Client,
+        Server,
+        TlsConnection, //
+    },
+    context::TlsContextBuilder,
+    credentials::{
+        Certificate,
+        TlsCredentialBuilder, //
+    },
+    errors::Error,
+    io::IoStatus, //
+};
+
+use bssl_x509::{
+    certificates::X509Certificate,
+    keys::PrivateKey,
+    params::Trust,
+    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");
+
+mod datagram;
+mod handshake;
+mod transport;
+
+/// Dumb server-client pair that does no certificate verification.
+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(None)?.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(None)?.build();
+
+    Ok((server_conn, client_conn))
+}
+
+fn sync_ping_pong<
+    M: crate::connection::methods::HasTlsConnectionMethod
+        + crate::context::SupportedMode
+        + crate::context::HasBasicIo
+        + 'static,
+>(
+    mut server_conn: TlsConnection<Server, M>,
+    mut client_conn: TlsConnection<Client, M>,
+) -> Result<(), Error> {
+    let thread = std::thread::spawn(move || {
+        server_conn.in_handshake().unwrap().accept()?;
+        assert!(!server_conn.is_in_handshake());
+        // TODO: switch to `From` impls when Rust compiler is bumped to 1.95.0.
+        let mut message = [MaybeUninit::uninit(); 21];
+        let mut message = ReceiveBuffer::new_uninit(&mut message);
+        assert!(matches!(
+            server_conn.sync_read(&mut message)?,
+            IoStatus::Ok(21)
+        ));
+        assert_eq!(*message, *b"BoringSSL is awesome!");
+        server_conn.sync_write(b"Oh yeah definitely!")?;
+        server_conn.established().unwrap().sync_shutdown()?;
+        // Second shutdown poll.
+        server_conn.established().unwrap().sync_shutdown()?;
+        Ok::<_, Error>(())
+    });
+
+    client_conn.in_handshake().unwrap().connect()?;
+    assert!(!client_conn.is_in_handshake());
+    client_conn.sync_write(b"BoringSSL is awesome!")?;
+    let mut message = [MaybeUninit::uninit(); 19];
+    let mut message = ReceiveBuffer::new_uninit(&mut message);
+    assert!(matches!(
+        client_conn.sync_read(&mut message)?,
+        IoStatus::Ok(19)
+    ));
+    assert_eq!(*message, *b"Oh yeah definitely!");
+    client_conn.established().unwrap().sync_shutdown()?;
+    thread.join().unwrap()?;
+
+    Ok(())
+}
+
+#[cfg(feature = "tokio_io")]
+async fn async_ping_pong<
+    M: crate::connection::methods::HasTlsConnectionMethod
+        + crate::context::SupportedMode
+        + crate::context::HasBasicIo
+        + 'static,
+>(
+    mut server_conn: TlsConnection<Server, M>,
+    mut client_conn: TlsConnection<Client, M>,
+) -> Result<(), Error> {
+    use std::time::Duration;
+
+    let task = tokio::spawn(async move {
+        server_conn
+            .in_handshake()
+            .unwrap()
+            .async_handshake()
+            .await?;
+
+        let mut message = [0; 21];
+        assert!(matches!(
+            server_conn.as_pin_mut().async_read(&mut message).await?,
+            IoStatus::Ok(21)
+        ));
+        assert_eq!(message, *b"BoringSSL is awesome!");
+        tokio::time::sleep(Duration::from_secs(2)).await;
+        server_conn
+            .as_pin_mut()
+            .async_write(b"Oh yeah definitely!")
+            .await?;
+        server_conn.as_pin_mut().async_shutdown().await?;
+        Ok::<_, Error>(())
+    });
+
+    client_conn
+        .in_handshake()
+        .unwrap()
+        .async_handshake()
+        .await?;
+    client_conn
+        .as_pin_mut()
+        .async_write(b"BoringSSL is awesome!")
+        .await?;
+    let mut message = [0; 19];
+    assert!(matches!(
+        client_conn.as_pin_mut().async_read(&mut message).await?,
+        IoStatus::Ok(19)
+    ));
+    assert_eq!(message, *b"Oh yeah definitely!");
+    assert!(matches!(
+        client_conn.as_pin_mut().async_shutdown().await,
+        Ok(_) | Err(Error::Io(crate::errors::IoError::EndOfStream))
+    ));
+    task.await.unwrap()?;
+    Ok(())
+}
diff --git a/rust/bssl-tls/src/tests/datagram.rs b/rust/bssl-tls/src/tests/datagram.rs
new file mode 100644
index 0000000..0b22229
--- /dev/null
+++ b/rust/bssl-tls/src/tests/datagram.rs
@@ -0,0 +1,103 @@
+// 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_x509::{
+    certificates::X509Certificate, keys::PrivateKey, params::Trust, store::X509StoreBuilder,
+};
+
+use crate::{
+    connection::{Client, Server, TlsConnection},
+    context::{DtlsMode, TlsContextBuilder},
+    credentials::{Certificate, TlsCredentialBuilder},
+    errors::Error,
+};
+
+fn dumb_dtls_server_client() -> Result<
+    (
+        TlsConnection<Server, DtlsMode>,
+        TlsConnection<Client, DtlsMode>,
+    ),
+    Error,
+> {
+    let ca = Certificate::parse_one_from_pem(super::CA, None)?;
+    let server_cert = Certificate::parse_one_from_pem(super::RSA_SERVER_CERT, None)?;
+    let server_key = PrivateKey::from_pem(super::RSA_SERVER_KEY, || unreachable!())?;
+
+    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()
+    };
+    server_ctx_builder.with_credential(server_cred.unwrap())?;
+    let server_ctx = server_ctx_builder.build();
+    let server_conn = server_ctx.new_server_connection(None)?.build();
+
+    let mut client_ctx_builder = TlsContextBuilder::new_dtls();
+    let ca = X509Certificate::parse_one_from_pem(super::CA)?;
+    let mut cert_store = X509StoreBuilder::new();
+    cert_store.set_trust(Trust::SslServer)?.add_cert(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(None)?.build();
+
+    Ok((server_conn, client_conn))
+}
+
+#[cfg(unix)]
+#[test]
+fn dtls() {
+    use crate::{io::sync_io::NoAsync, io::unix::StdDatagram, tests::sync_ping_pong};
+
+    let (mut server_conn, mut client_conn) = dumb_dtls_server_client().unwrap();
+    let (server_sock, client_sock) = std::os::unix::net::UnixDatagram::pair().unwrap();
+    let server_sock = StdDatagram::new(server_sock, NoAsync);
+    let client_sock = StdDatagram::new(client_sock, NoAsync);
+    server_conn.set_io(server_sock).unwrap();
+    client_conn.set_io(client_sock).unwrap();
+    sync_ping_pong(server_conn, client_conn).unwrap();
+}
+
+#[cfg(all(unix, feature = "tokio_net"))]
+#[tokio::test]
+async fn async_dtls() -> Result<(), Error> {
+    use crate::tests::async_ping_pong;
+
+    let (mut server_conn, mut client_conn) = dumb_dtls_server_client().unwrap();
+    let (server_sock, client_sock) = tokio::net::UnixDatagram::pair().unwrap();
+    server_conn.set_io(server_sock).unwrap();
+    client_conn.set_io(client_sock).unwrap();
+
+    async_ping_pong(server_conn, client_conn).await
+}
+
+#[cfg(all(feature = "tokio_net", unix))]
+#[tokio::test]
+async fn async_dtls_over_fd() -> Result<(), Error> {
+    use crate::{io::unix::StdDatagram, tests::async_ping_pong};
+
+    let (mut server_conn, mut client_conn) = dumb_dtls_server_client().unwrap();
+    let (server_sock, client_sock) = std::os::unix::net::UnixDatagram::pair().unwrap();
+    server_sock.set_nonblocking(true).unwrap();
+    client_sock.set_nonblocking(true).unwrap();
+    let server_sock = StdDatagram::new_with_tokio(server_sock).unwrap();
+    let client_sock = StdDatagram::new_with_tokio(client_sock).unwrap();
+    server_conn.set_io(server_sock).unwrap();
+    client_conn.set_io(client_sock).unwrap();
+
+    async_ping_pong(server_conn, client_conn).await
+}
diff --git a/rust/bssl-tls/src/tests/handshake.rs b/rust/bssl-tls/src/tests/handshake.rs
new file mode 100644
index 0000000..0425ae5
--- /dev/null
+++ b/rust/bssl-tls/src/tests/handshake.rs
@@ -0,0 +1,80 @@
+// 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::errors::Error;
+
+#[cfg(unix)]
+#[test]
+fn ping_pong() -> Result<(), Error> {
+    use std::sync::Arc;
+
+    use bssl_x509::{
+        certificates::X509Certificate, keys::PrivateKey, params::Trust, store::X509StoreBuilder,
+    };
+
+    use super::{CA, RSA_SERVER_CERT, RSA_SERVER_KEY};
+    use crate::{
+        context::{CertificateCache, TlsContextBuilder},
+        credentials::{Certificate, CertificateVerificationMode, TlsCredentialBuilder},
+        io::sync_io::{NoAsync, StdIoWithReactor},
+        tests::sync_ping_pong,
+    };
+
+    let cache = Arc::new(CertificateCache::new());
+    let ca = Certificate::parse_one_from_pem(CA, Some(&cache))?;
+    let server_cert = Certificate::parse_one_from_pem(RSA_SERVER_CERT, Some(&cache))?;
+    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_certificate_cache(Some(Arc::clone(&cache)))
+        .with_credential(server_cred.unwrap())?;
+    let server_ctx = server_ctx_builder.build();
+    let mut server_conn = server_ctx.new_server_connection(None)?.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_cache(Some(Arc::clone(&cache)))
+        .with_certificate_store(&cert_store);
+    let client_ctx = client_ctx_builder.build();
+    let mut client_conn = client_ctx.new_client_connection(None)?;
+    client_conn.with_certificate_verification_mode(CertificateVerificationMode::PeerCertMandatory);
+    let mut client_conn = client_conn.build();
+    client_conn
+        .in_handshake()
+        .unwrap()
+        .set_host("www.google.com")?;
+
+    let (server_rx, server_tx) = std::io::pipe().unwrap();
+    let (client_rx, client_tx) = std::io::pipe().unwrap();
+    let server_rx = StdIoWithReactor::new(server_rx, NoAsync);
+    let server_tx = StdIoWithReactor::new(server_tx, NoAsync);
+    let client_rx = StdIoWithReactor::new(client_rx, NoAsync);
+    let client_tx = StdIoWithReactor::new(client_tx, NoAsync);
+    server_conn.set_split_io(client_rx, server_tx)?;
+    client_conn.set_split_io(server_rx, client_tx)?;
+    sync_ping_pong(server_conn, client_conn)
+}
diff --git a/rust/bssl-tls/src/tests/transport.rs b/rust/bssl-tls/src/tests/transport.rs
new file mode 100644
index 0000000..a50a9db
--- /dev/null
+++ b/rust/bssl-tls/src/tests/transport.rs
@@ -0,0 +1,105 @@
+// 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::tests::dumb_server_client;
+
+#[cfg(unix)]
+#[test]
+fn stdio() {
+    use std::io::{Read, Write};
+
+    use crate::io::sync_io::{NoAsync, StdIoWithReactor};
+
+    let (mut server_conn, mut client_conn) = dumb_server_client().unwrap();
+
+    let (server_rx, server_tx) = std::io::pipe().unwrap();
+    let (client_rx, client_tx) = std::io::pipe().unwrap();
+    let server_rx = StdIoWithReactor::new(server_rx, NoAsync);
+    let server_tx = StdIoWithReactor::new(server_tx, NoAsync);
+    let client_rx = StdIoWithReactor::new(client_rx, NoAsync);
+    let client_tx = StdIoWithReactor::new(client_tx, NoAsync);
+
+    server_conn.set_split_io(client_rx, server_tx).unwrap();
+    client_conn.set_split_io(server_rx, client_tx).unwrap();
+
+    let thread = std::thread::spawn(move || {
+        let mut message = [0; 21];
+        // Use `std::io::Read::read_exact`
+        server_conn.read_exact(&mut message).unwrap();
+        assert_eq!(message, *b"BoringSSL is awesome!");
+        // Use `std::io::Write::write_all`
+        server_conn.write_all(b"Oh yeah definitely!").unwrap();
+        server_conn.established().unwrap().sync_shutdown().unwrap();
+        // Second shutdown poll.
+        server_conn.established().unwrap().sync_shutdown().unwrap();
+    });
+
+    // Use `std::io::Write::write_all`
+    client_conn.write_all(b"BoringSSL is awesome!").unwrap();
+    let mut message = [0; 19];
+    // Use `std::io::Read::read_exact`
+    client_conn.read_exact(&mut message).unwrap();
+    assert_eq!(message, *b"Oh yeah definitely!");
+    client_conn.established().unwrap().sync_shutdown().unwrap();
+    thread.join().unwrap();
+}
+
+#[cfg(all(unix, feature = "tokio_net"))]
+#[tokio::test]
+async fn tokio_io() -> Result<(), crate::errors::Error> {
+    use crate::io::unix::TokioIo;
+
+    use futures::prelude::*;
+    use tokio::io::{AsyncReadExt, AsyncWriteExt};
+
+    let (mut server_conn, mut 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();
+    let (send_signal, recv_signal) = tokio::sync::oneshot::channel();
+    let server_rx = TokioIo(server_rx);
+    let server_tx = TokioIo(server_tx);
+    let client_rx = TokioIo(client_rx);
+    let client_tx = TokioIo(client_tx);
+
+    server_conn.set_split_io(client_rx, server_tx)?;
+    client_conn.set_split_io(server_rx, client_tx)?;
+
+    let thread = tokio::spawn(async move {
+        let mut message = [0; 21];
+        server_conn.read_exact(&mut message).await.unwrap();
+        assert_eq!(message, *b"BoringSSL is awesome!");
+        server_conn.write_all(b"Oh yeah definitely!").await.unwrap();
+        server_conn.established().unwrap().sync_shutdown().unwrap();
+
+        // The following shutdown call should not make progress.
+        let res = server_conn.established().unwrap().shutdown().now_or_never();
+        assert!(res.is_none(), "{res:?}");
+        // Make the client progress.
+        send_signal.send(()).unwrap();
+        server_conn.established().unwrap().shutdown().await.unwrap();
+    });
+    client_conn
+        .write_all(b"BoringSSL is awesome!")
+        .await
+        .unwrap();
+    recv_signal.await.unwrap();
+    let mut message = [0; 19];
+    client_conn.read_exact(&mut message).await.unwrap();
+    assert_eq!(message, *b"Oh yeah definitely!");
+    client_conn.established().unwrap().sync_shutdown().unwrap();
+    thread.await.unwrap();
+
+    Ok(())
+}