rust: Add `rustls` CryptoProvider adapters

For now we target `rustls 0.23.0` and its other semver compatible
versions.

The support is not complete in the following way.
- We lack DTLS safe binding, for which we do not plan as of writing.

Change-Id: I2ebcd62cc690cd331d7e4338b49d3a3adbbb0f4a
Signed-off-by: Xiangfei Ding <xfding@google.com>
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/88127
Reviewed-by: Adam Langley <agl@google.com>
diff --git a/rust/Cargo.lock b/rust/Cargo.lock
index d1c658f..d67ec88 100644
--- a/rust/Cargo.lock
+++ b/rust/Cargo.lock
@@ -12,3 +12,374 @@
 [[package]]
 name = "bssl-sys"
 version = "0.1.0"
+
+[[package]]
+name = "bssl-tls"
+version = "0.1.0"
+dependencies = [
+ "bssl-crypto",
+ "bssl-sys",
+ "rustls",
+ "tracing",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "cc"
+version = "1.2.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
+
+[[package]]
+name = "getrandom"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "libc"
+version = "0.2.177"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
+
+[[package]]
+name = "log"
+version = "0.4.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.50.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b"
+dependencies = [
+ "once_cell",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a"
+dependencies = [
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "2.0.110"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
+dependencies = [
+ "nu-ansi-term",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing-core",
+ "tracing-log",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "valuable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "zeroize"
+version = "1.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
diff --git a/rust/Cargo.toml b/rust/Cargo.toml
index a0689b3..7a2db1d 100644
--- a/rust/Cargo.toml
+++ b/rust/Cargo.toml
@@ -2,5 +2,6 @@
 members = [
   "bssl-crypto",
   "bssl-sys",
+  "bssl-tls",
 ]
 resolver = "3"
diff --git a/rust/bssl-crypto/src/lib.rs b/rust/bssl-crypto/src/lib.rs
index dfa7ef6..a11ce40 100644
--- a/rust/bssl-crypto/src/lib.rs
+++ b/rust/bssl-crypto/src/lib.rs
@@ -81,8 +81,10 @@
 /// the pointer. When passing pointers into C/C++ code, that is not a valid
 /// pointer. Thus this method should be used whenever passing a pointer to a
 /// slice into BoringSSL code.
-trait FfiSlice<T> {
+pub trait FfiSlice<T> {
+    /// Cast the slice into a valid raw pointer for FFI.
     fn as_ffi_ptr(&self) -> *const T;
+    /// Cast the slice into a valid `const void *` pointer for FFI.
     fn as_ffi_void_ptr(&self) -> *const c_void {
         self.as_ffi_ptr() as *const c_void
     }
@@ -109,7 +111,8 @@
 }
 
 /// See the comment [`FfiSlice`].
-trait FfiMutSlice {
+pub trait FfiMutSlice {
+    /// Cast the mutable slice as a valid `uint8_t*` pointer for FFI.
     fn as_mut_ffi_ptr(&mut self) -> *mut u8;
 }
 
diff --git a/rust/bssl-tls/Cargo.toml b/rust/bssl-tls/Cargo.toml
new file mode 100644
index 0000000..c86145f
--- /dev/null
+++ b/rust/bssl-tls/Cargo.toml
@@ -0,0 +1,36 @@
+[package]
+name = "bssl-tls"
+version = "0.1.0"
+edition = "2024"
+publish = false
+license = "Apache-2.0"
+
+[dependencies.bssl-crypto]
+path = "../bssl-crypto"
+
+[dependencies.bssl-sys]
+path = "../bssl-sys"
+
+[dependencies.rustls]
+version = "0.23.0"
+default-features = false
+optional = true
+
+[dev-dependencies]
+tracing = "0.1"
+tracing-subscriber = "0.3"
+
+[dev-dependencies.rustls]
+version = "0.23.0"
+default-features = false
+features = ["ring"]
+
+[features]
+default = []
+# `std` depends on the Rust `std` crate, but adds some useful trait impls if
+# available.
+std = ["bssl-crypto/std"]
+# `mlalgs` enables ML-KEM and ML-DSA support. This requires Rust 1.82.
+mlalgs = ["bssl-crypto/mlalgs"]
+# `rustls` enables the adapters to key traits for inter-op with `rustls` crate
+rustls-adapters = ["rustls/tls12", "rustls/std"]
diff --git a/rust/bssl-tls/deny.toml b/rust/bssl-tls/deny.toml
new file mode 100644
index 0000000..a4dedf9
--- /dev/null
+++ b/rust/bssl-tls/deny.toml
@@ -0,0 +1,28 @@
+# Configuration file used for `cargo deny check`, which checks for licensing
+# issues and security advisories.
+#
+# For a list of possible sections and their default values, see
+# https://github.com/EmbarkStudios/cargo-deny/blob/main/deny.template.toml
+#
+# For further documentation, see https://embarkstudios.github.io/cargo-deny/.
+
+# This section is considered when running `cargo deny check licenses`
+# More documentation for the licenses section can be found here:
+# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
+[licenses]
+# List of explicitly allowed licenses
+# See https://spdx.org/licenses/ for list of possible licenses
+# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
+allow = ["Apache-2.0"]
+
+# This section is considered when running `cargo deny check bans`.
+# More documentation about the 'bans' section can be found here:
+# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html
+[bans]
+# Lint level for when multiple versions of the same crate are detected
+multiple-versions = "deny"
+# List of crates that are allowed. Use with care!
+# This is meant to control any external dependencies. This is effectively
+# a minimalist binding library and we try to have none, so you are strongly
+# encouraged not to add dependencies here.
+allow = []
diff --git a/rust/bssl-tls/src/lib.rs b/rust/bssl-tls/src/lib.rs
new file mode 100644
index 0000000..99111e4
--- /dev/null
+++ b/rust/bssl-tls/src/lib.rs
@@ -0,0 +1,32 @@
+// 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.
+
+#![deny(
+    missing_docs,
+    unsafe_op_in_unsafe_fn,
+    clippy::indexing_slicing,
+    clippy::unwrap_used,
+    clippy::panic,
+    clippy::expect_used
+)]
+#![allow(private_bounds)]
+#![cfg_attr(not(any(feature = "std", test)), no_std)]
+
+//! Rust BoringSSL bindings
+
+extern crate alloc;
+extern crate core;
+
+#[cfg(feature = "rustls-adapters")]
+pub mod rustls_provider;
diff --git a/rust/bssl-tls/src/rustls_provider.rs b/rust/bssl-tls/src/rustls_provider.rs
new file mode 100644
index 0000000..389a948
--- /dev/null
+++ b/rust/bssl-tls/src/rustls_provider.rs
@@ -0,0 +1,335 @@
+// 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.
+
+//! `rustls` Adapters
+//!
+//! This module provides a provider builder `CryptoProviderBuilder`,
+//! which constructs a [`rustls::crypto::CryptoProvider`] for interop with `rustls` TLS stack.
+//!
+//! # Supported signature schemes
+//!
+//! ## [RFC 8446] IANA assignments for TLS supported signature scheme
+//!
+//! - `TLS_AES_128_GCM_SHA256`
+//! - `TLS_AES_256_GCM_SHA384`
+//! - `TLS_CHACHA20_POLY1305_SHA256`
+//!
+//! # Supported key exchange groups
+//!
+//! - [`secp256r1`] backed by [`key_exchange::ECDH_P256`]
+//! - [`secp384r1`] backed by [`key_exchange::ECDH_P384`]
+//! - [`X25519`] backed by [`key_exchange::X25519`]
+//!
+//! [RFC 8446]: https://datatracker.ietf.org/doc/html/rfc8446
+//! [`X25519`]: https://datatracker.ietf.org/doc/html/rfc7748
+
+use alloc::{boxed::Box, sync::Arc, vec, vec::Vec};
+use bssl_sys::RAND_bytes;
+use core::{
+    fmt::{Debug, Formatter, Result as FmtResult},
+    marker::PhantomData,
+};
+
+use rustls::{
+    Error, SignatureScheme, SupportedCipherSuite,
+    crypto::{
+        self, CryptoProvider, KeyProvider as RustlsKeyProvider, SecureRandom, SupportedKxGroup,
+        WebPkiSupportedAlgorithms,
+    },
+    pki_types::PrivateKeyDer,
+    sign::SigningKey,
+};
+
+use bssl_crypto::{FfiMutSlice, digest, ec, pkcs8};
+
+mod aead;
+pub mod cipher_suites;
+pub mod key_exchange;
+pub mod pki;
+mod prf;
+mod sign;
+
+// A sealed trait only for supported digests
+pub(crate) trait RsaSignatureDigest: digest::Algorithm {
+    /// A description of the digest algorithm.
+    const ALGORITHM: pki::DigestAlgorithm;
+}
+
+impl RsaSignatureDigest for digest::Sha256 {
+    const ALGORITHM: pki::DigestAlgorithm = pki::DigestAlgorithm::Sha256;
+}
+
+impl RsaSignatureDigest for digest::Sha384 {
+    const ALGORITHM: pki::DigestAlgorithm = pki::DigestAlgorithm::Sha384;
+}
+
+impl RsaSignatureDigest for digest::Sha512 {
+    const ALGORITHM: pki::DigestAlgorithm = pki::DigestAlgorithm::Sha512;
+}
+
+/// This random source delegates to [`RAND_bytes`] which aborts on insufficient
+/// entropy.
+struct Rand;
+
+impl Debug for Rand {
+    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
+        f.debug_struct("Rand").finish()
+    }
+}
+
+impl SecureRandom for Rand {
+    fn fill(&self, buf: &mut [u8]) -> Result<(), crypto::GetRandomFailed> {
+        // Safety: `RAND_bytes` guarantees that the writes are in-bound
+        unsafe {
+            RAND_bytes(buf.as_mut_ffi_ptr(), buf.len());
+        }
+        Ok(())
+    }
+}
+
+const ALL_SIGNATURE_ALGORITHMS: WebPkiSupportedAlgorithms = WebPkiSupportedAlgorithms {
+    all: &[
+        pki::ECDSA_NISTP256_SHA256,
+        pki::ECDSA_NISTP384_SHA384,
+        pki::ED25519,
+        pki::RSA_PKCS1_SHA256,
+        pki::RSA_PKCS1_SHA384,
+        pki::RSA_PKCS1_SHA512,
+        pki::RSA_PSS_SHA256,
+        pki::RSA_PSS_SHA384,
+        pki::RSA_PSS_SHA512,
+    ],
+    mapping: &[
+        (
+            SignatureScheme::ECDSA_NISTP256_SHA256,
+            &[pki::ECDSA_NISTP256_SHA256],
+        ),
+        (
+            SignatureScheme::ECDSA_NISTP384_SHA384,
+            &[pki::ECDSA_NISTP384_SHA384],
+        ),
+        (SignatureScheme::ED25519, &[pki::ED25519]),
+        (SignatureScheme::RSA_PKCS1_SHA256, &[pki::RSA_PKCS1_SHA256]),
+        (SignatureScheme::RSA_PKCS1_SHA384, &[pki::RSA_PKCS1_SHA384]),
+        (SignatureScheme::RSA_PKCS1_SHA512, &[pki::RSA_PKCS1_SHA512]),
+        (SignatureScheme::RSA_PSS_SHA256, &[pki::RSA_PSS_SHA256]),
+        (SignatureScheme::RSA_PSS_SHA384, &[pki::RSA_PSS_SHA384]),
+        (SignatureScheme::RSA_PSS_SHA512, &[pki::RSA_PSS_SHA512]),
+    ],
+};
+
+struct KeyProvider;
+
+impl Debug for KeyProvider {
+    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
+        f.debug_struct("FipsKeyProvider").finish()
+    }
+}
+
+impl RustlsKeyProvider for KeyProvider {
+    fn load_private_key(
+        &self,
+        key_der: PrivateKeyDer<'static>,
+    ) -> Result<Arc<dyn SigningKey>, Error> {
+        match key_der {
+            PrivateKeyDer::Pkcs1(der) => {
+                sign::RsaPrivateKey::try_from(der).map(|key| Arc::new(key) as _)
+            }
+            PrivateKeyDer::Sec1(ref der) => sign::EcdsaPrivateKey::<ec::P256>::try_from(der)
+                .map(|key| Arc::new(key) as _)
+                .or_else(|_| {
+                    sign::EcdsaPrivateKey::<ec::P384>::try_from(der).map(|key| Arc::new(key) as _)
+                }),
+            PrivateKeyDer::Pkcs8(ref der) => {
+                pkcs8::SigningKey::from_der_private_key_info(der.secret_pkcs8_der())
+                    .map(|key| match key {
+                        pkcs8::SigningKey::Rsa(rsa) => Arc::new(sign::RsaPrivateKey(rsa)) as _,
+                        pkcs8::SigningKey::EcP256(key) => Arc::new(sign::EcdsaPrivateKey(key)) as _,
+                        pkcs8::SigningKey::EcP384(key) => Arc::new(sign::EcdsaPrivateKey(key)) as _,
+                        pkcs8::SigningKey::Ed25519(key) => {
+                            Arc::new(sign::EddsaPrivateKey(key)) as _
+                        }
+                    })
+                    .ok_or(Error::General("unsupported PKCS #8 private key".into()))
+            }
+            _ => Err(Error::General("type of key to load is unrecognised".into())),
+        }
+    }
+
+    fn fips(&self) -> bool {
+        false
+    }
+}
+
+#[derive(Clone)]
+struct HashContext<A>(A);
+impl<A> Debug for HashContext<A> {
+    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
+        f.debug_tuple("HashContext").finish()
+    }
+}
+
+impl<A: digest::Algorithm + Send + Sync + Clone + 'static> crypto::hash::Context
+    for HashContext<A>
+{
+    fn fork_finish(&self) -> crypto::hash::Output {
+        let digest = self.0.clone().digest_to_vec();
+        assert!(digest.len() <= crypto::hash::Output::MAX_LEN);
+        crypto::hash::Output::new(&digest)
+    }
+
+    fn fork(&self) -> Box<dyn crypto::hash::Context> {
+        Box::new(self.clone())
+    }
+
+    fn finish(self: Box<Self>) -> crypto::hash::Output {
+        let digest = self.0.digest_to_vec();
+        assert!(digest.len() <= crypto::hash::Output::MAX_LEN);
+        crypto::hash::Output::new(&digest)
+    }
+
+    fn update(&mut self, data: &[u8]) {
+        self.0.update(data);
+    }
+}
+
+struct HashAlgorithm<A>(PhantomData<fn() -> A>);
+
+impl<A> HashAlgorithm<A> {
+    const fn new() -> Self {
+        Self(PhantomData)
+    }
+}
+
+macro_rules! impl_crypto_hash {
+    ($algo:path, $id:path) => {
+        impl crypto::hash::Hash for HashAlgorithm<$algo> {
+            fn start(&self) -> Box<dyn crypto::hash::Context> {
+                Box::new(HashContext(<$algo as digest::Algorithm>::new()))
+            }
+
+            fn hash(&self, data: &[u8]) -> crypto::hash::Output {
+                let mut ctx = <$algo as digest::Algorithm>::new();
+                ctx.update(data);
+                let digest = digest::Algorithm::digest_to_vec(ctx);
+                assert!(digest.len() <= crypto::hash::Output::MAX_LEN);
+                crypto::hash::Output::new(&digest)
+            }
+
+            fn output_len(&self) -> usize {
+                <$algo as digest::Algorithm>::OUTPUT_LEN
+            }
+
+            fn algorithm(&self) -> crypto::hash::HashAlgorithm {
+                $id
+            }
+        }
+    };
+}
+
+impl_crypto_hash!(digest::Sha256, crypto::hash::HashAlgorithm::SHA256);
+impl_crypto_hash!(digest::Sha384, crypto::hash::HashAlgorithm::SHA384);
+impl_crypto_hash!(digest::Sha512, crypto::hash::HashAlgorithm::SHA512);
+
+/// The main provider builder.
+pub struct CryptoProviderBuilder {
+    kx_groups: Vec<&'static dyn SupportedKxGroup>,
+    cipher_suites: Vec<SupportedCipherSuite>,
+}
+
+impl CryptoProviderBuilder {
+    /// Make a new provider builder.
+    pub fn new() -> Self {
+        Self {
+            kx_groups: vec![],
+            cipher_suites: vec![],
+        }
+    }
+
+    /// Include all possible cipher suites and key agreement groups.
+    pub fn full() -> CryptoProvider {
+        Self::new()
+            .with_default_key_exchange_groups()
+            .with_full_cipher_suites()
+            .build()
+    }
+
+    /// Include a key exchange group, with a lower priority than previously registered groups.
+    pub fn with_key_exchange_group(mut self, group: &'static dyn SupportedKxGroup) -> Self {
+        self.kx_groups.push(group);
+        self
+    }
+
+    /// Use the default key exchange groups, with a lower priority than previously registered
+    /// groups.
+    pub fn with_default_key_exchange_groups(mut self) -> Self {
+        self.kx_groups.extend_from_slice(&[
+            key_exchange::ECDH_P256,
+            key_exchange::ECDH_P384,
+            key_exchange::X25519,
+        ]);
+        self
+    }
+
+    #[cfg(feature = "mlalgs")]
+    /// Include post-quantum MLKEM hybrid key exchange groups, with a lower priority than previously
+    /// registered groups.
+    pub fn with_mlkem_groups(mut self) -> Self {
+        self.kx_groups
+            .extend_from_slice(&[key_exchange::X25519MLKEM768]);
+        self
+    }
+
+    /// Use all the provided cipher suites
+    #[inline]
+    pub fn with_full_cipher_suites(mut self) -> Self {
+        self.cipher_suites.extend([
+            SupportedCipherSuite::Tls12(
+                cipher_suites::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
+            ),
+            SupportedCipherSuite::Tls12(cipher_suites::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256),
+            SupportedCipherSuite::Tls12(cipher_suites::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256),
+            SupportedCipherSuite::Tls12(cipher_suites::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384),
+            SupportedCipherSuite::Tls12(cipher_suites::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256),
+            SupportedCipherSuite::Tls12(cipher_suites::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384),
+            SupportedCipherSuite::Tls13(cipher_suites::TLS13_AES_128_GCM_SHA256),
+            SupportedCipherSuite::Tls13(cipher_suites::TLS13_AES_256_GCM_SHA384),
+            SupportedCipherSuite::Tls13(cipher_suites::TLS13_CHACHA20_POLY1305_SHA256),
+        ]);
+        self
+    }
+
+    /// Add a [`SupportedCipherSuite`].
+    /// More cipher suites are available in [`cipher_suites`].
+    #[inline]
+    pub fn with_cipher_suite(mut self, cipher_suite: SupportedCipherSuite) -> Self {
+        self.cipher_suites.push(cipher_suite);
+        self
+    }
+
+    #[inline]
+    /// Finalise and build the [`CryptoProvider`] for `rustls`.
+    pub fn build(self) -> CryptoProvider {
+        CryptoProvider {
+            cipher_suites: self.cipher_suites,
+            kx_groups: self.kx_groups,
+            signature_verification_algorithms: ALL_SIGNATURE_ALGORITHMS,
+            secure_random: &Rand,
+            key_provider: &KeyProvider,
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests;
diff --git a/rust/bssl-tls/src/rustls_provider/aead.rs b/rust/bssl-tls/src/rustls_provider/aead.rs
new file mode 100644
index 0000000..fad045d
--- /dev/null
+++ b/rust/bssl-tls/src/rustls_provider/aead.rs
@@ -0,0 +1,587 @@
+// 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 alloc::boxed::Box;
+use core::marker::PhantomData;
+
+use rustls::{
+    ConnectionTrafficSecrets, ContentType, Error, ProtocolVersion,
+    crypto::cipher::{
+        AeadKey, InboundOpaqueMessage, InboundPlainMessage, Iv, KeyBlockShape, MessageDecrypter,
+        MessageEncrypter, OutboundOpaqueMessage, OutboundPlainMessage, PrefixedPayload,
+        Tls12AeadAlgorithm, Tls13AeadAlgorithm, UnsupportedOperationError, make_tls12_aad,
+        make_tls13_aad,
+    },
+};
+
+use bssl_crypto::aead;
+
+struct Tls12AdditionalData;
+
+impl TlsAdditionalData for Tls12AdditionalData {
+    #[inline]
+    fn build_additional_data(
+        seq: u64,
+        typ: ContentType,
+        version: ProtocolVersion,
+        len: usize,
+        ad_buf: &mut [u8],
+    ) -> Option<&[u8]> {
+        let ad_buf: &mut [u8; 13] = ad_buf.try_into().ok()?;
+        ad_buf.copy_from_slice(&make_tls12_aad(seq, typ, version, len));
+        Some(ad_buf)
+    }
+}
+
+const NONCE_LEN: usize = 12;
+
+enum AeadKind {
+    Aes128Gcm,
+    Aes256Gcm,
+    Chacha20Poly1305,
+}
+
+trait AeadConstructible: aead::Aead + Sized {
+    const KEY_LEN: usize;
+    const TAG_LEN: usize;
+    const KIND: AeadKind;
+    fn new_with_key(key: &AeadKey) -> Option<Self>;
+    fn nonce_from_buf(buf: &[u8]) -> Option<&Self::Nonce>;
+    fn tag_from_buf(buf: &[u8]) -> Option<&Self::Tag>;
+    fn tag_to_buf(tag: &Self::Tag) -> &[u8];
+}
+
+fn make_nonce_from_iv_seq(iv: [u8; NONCE_LEN], seq: u64) -> [u8; NONCE_LEN] {
+    let mut buf = [0; NONCE_LEN];
+    buf[4..].copy_from_slice(&seq.to_be_bytes());
+    for i in 0..NONCE_LEN {
+        buf[i] ^= iv[i];
+    }
+    buf
+}
+
+macro_rules! aead_ctor {
+    ($($algo:ty : {
+        key = $key_len:literal,
+        tag = $tag_len:literal,
+        kind = $kind:expr
+    }),+) => { $(
+        impl AeadConstructible for $algo {
+            const KEY_LEN: usize = $key_len;
+            const TAG_LEN: usize = $tag_len;
+            const KIND: AeadKind = $kind;
+            fn new_with_key(key: &AeadKey) -> Option<Self> {
+                let key: [u8; $key_len] = key.as_ref().try_into().ok()?;
+                Some(Self::new(&key))
+            }
+            fn nonce_from_buf(buf: &[u8]) -> Option<&[u8; NONCE_LEN]> {
+                buf.try_into().ok()
+            }
+            fn tag_from_buf(buf: &[u8]) -> Option<&[u8; $tag_len]> {
+                buf.try_into().ok()
+            }
+            fn tag_to_buf(tag: &[u8; $tag_len]) -> &[u8] {
+                &*tag
+            }
+        }
+    )+ };
+    () => {};
+    ($($algo:ty : $tt:tt),+,) => { aead_ctor!($($algo : $tt),+); }
+}
+
+aead_ctor!(
+    aead::Aes128Gcm : {key = 16, tag = 16, kind = AeadKind::Aes128Gcm},
+    aead::Aes256Gcm : {key = 32, tag = 16, kind = AeadKind::Aes256Gcm},
+    aead::Chacha20Poly1305 : {key = 32, tag = 16, kind = AeadKind::Chacha20Poly1305},
+);
+
+trait Gcm: AeadConstructible {}
+impl Gcm for aead::Aes128Gcm {}
+impl Gcm for aead::Aes256Gcm {}
+
+trait Chacha20: AeadConstructible {}
+impl Chacha20 for aead::Chacha20Poly1305 {}
+
+// ===================================================
+// TLS 1.2 AES GCM cipher
+// ===================================================
+
+enum MaybeValidByteArray<const N: usize> {
+    Valid([u8; N]),
+    Invalid,
+}
+
+impl<const N: usize> From<&'_ [u8]> for MaybeValidByteArray<N> {
+    fn from(value: &[u8]) -> Self {
+        if let Ok(value) = value.try_into() {
+            Self::Valid(value)
+        } else {
+            Self::Invalid
+        }
+    }
+}
+
+struct Tls12GcmMessageEncrypter<A, AD> {
+    key: AeadKey,
+    iv: MaybeValidByteArray<4>,
+    explicit_nonce: MaybeValidByteArray<8>,
+    _p: PhantomData<fn() -> (A, AD)>,
+}
+
+trait TlsAdditionalData {
+    fn build_additional_data(
+        seq: u64,
+        typ: ContentType,
+        version: ProtocolVersion,
+        len: usize,
+        ad_buf: &mut [u8],
+    ) -> Option<&[u8]>;
+}
+
+const TLS12_AAD_SIZE: usize = 13;
+const TLS12_GCM_EXTRA_NONCE_SIZE: usize = 8;
+
+#[inline]
+fn gcm_iv(iv: &[u8; 4], extra: &[u8; TLS12_GCM_EXTRA_NONCE_SIZE]) -> [u8; NONCE_LEN] {
+    let mut gcm_iv = [0; NONCE_LEN];
+    gcm_iv[..4].copy_from_slice(iv);
+    gcm_iv[4..].copy_from_slice(extra);
+    gcm_iv
+}
+
+const GCM_EXPLICIT_NONCE_LEN: usize = 8;
+
+impl<A: Gcm, AD: TlsAdditionalData> MessageEncrypter for Tls12GcmMessageEncrypter<A, AD> {
+    fn encrypt(
+        &mut self,
+        msg: OutboundPlainMessage<'_>,
+        seq: u64,
+    ) -> Result<OutboundOpaqueMessage, Error> {
+        let payload_len = msg.payload.len();
+        let mut payload = PrefixedPayload::with_capacity(self.encrypted_payload_len(payload_len));
+        let aead = A::new_with_key(&self.key).ok_or(Error::EncryptError)?;
+        let (MaybeValidByteArray::Valid(iv), MaybeValidByteArray::Valid(explicit_nonce)) =
+            (&self.iv, &self.explicit_nonce)
+        else {
+            return Err(Error::DecryptError);
+        };
+        let write_iv = gcm_iv(iv, explicit_nonce);
+        let nonce = make_nonce_from_iv_seq(write_iv, seq);
+        let nonce = A::nonce_from_buf(&nonce).ok_or(Error::EncryptError)?;
+        let mut ad_buf = [0; TLS12_AAD_SIZE];
+        let ad = AD::build_additional_data(seq, msg.typ, msg.version, payload_len, &mut ad_buf)
+            .ok_or(Error::EncryptError)?;
+        payload.extend_from_slice(&nonce.as_ref()[4..]);
+        payload.extend_from_chunks(&msg.payload);
+        let tag = aead.seal_in_place(
+            &nonce,
+            &mut payload.as_mut()[GCM_EXPLICIT_NONCE_LEN..GCM_EXPLICIT_NONCE_LEN + payload_len],
+            ad,
+        );
+        payload.extend_from_slice(A::tag_to_buf(&tag));
+        Ok(OutboundOpaqueMessage::new(msg.typ, msg.version, payload))
+    }
+
+    fn encrypted_payload_len(&self, payload_len: usize) -> usize {
+        payload_len + GCM_EXPLICIT_NONCE_LEN + A::TAG_LEN
+    }
+}
+
+struct Tls12GcmMessageDecrypter<A, AD> {
+    key: AeadKey,
+    iv: MaybeValidByteArray<4>,
+    _p: PhantomData<fn() -> (A, AD)>,
+}
+
+impl<A: Gcm, AD: TlsAdditionalData> MessageDecrypter for Tls12GcmMessageDecrypter<A, AD> {
+    fn decrypt<'a>(
+        &mut self,
+        mut msg: InboundOpaqueMessage<'a>,
+        seq: u64,
+    ) -> Result<InboundPlainMessage<'a>, Error> {
+        let aead = A::new_with_key(&self.key).ok_or(Error::DecryptError)?;
+        let InboundOpaqueMessage {
+            typ,
+            version,
+            ref mut payload,
+        } = msg;
+        let Some((explicit_nonce, payload)) = payload.split_at_mut_checked(GCM_EXPLICIT_NONCE_LEN)
+        else {
+            return Err(Error::DecryptError);
+        };
+        let Ok(extra) = explicit_nonce.try_into() else {
+            unreachable!("length should be 8")
+        };
+        let MaybeValidByteArray::Valid(iv) = &self.iv else {
+            return Err(Error::DecryptError);
+        };
+        let nonce = gcm_iv(iv, &extra);
+        let Some(ctxt_len) = payload.len().checked_sub(A::TAG_LEN) else {
+            return Err(Error::DecryptError);
+        };
+        // TODO(@xfding) Should we check the fragment size?
+        let Some((ciphertext, tag)) = payload.split_at_mut_checked(ctxt_len) else {
+            return Err(Error::DecryptError);
+        };
+        let mut ad_buf = [0; TLS12_AAD_SIZE];
+        let ad = AD::build_additional_data(seq, typ, version, ctxt_len, &mut ad_buf)
+            .ok_or(Error::DecryptError)?;
+        let tag = A::tag_from_buf(tag).ok_or(Error::DecryptError)?;
+        let nonce = A::nonce_from_buf(&nonce).ok_or(Error::DecryptError)?;
+        aead.open_in_place(&nonce, ciphertext, &tag, ad)
+            .map_err(|_| Error::DecryptError)?;
+        Ok(msg.into_plain_message_range(GCM_EXPLICIT_NONCE_LEN..GCM_EXPLICIT_NONCE_LEN + ctxt_len))
+    }
+}
+
+struct Tls12GcmAeadAlgorithm<A>(PhantomData<fn() -> A>);
+
+impl<A> Tls12GcmAeadAlgorithm<A> {}
+
+impl<A: 'static + Gcm> Tls12AeadAlgorithm for Tls12GcmAeadAlgorithm<A> {
+    fn encrypter(&self, key: AeadKey, iv: &[u8], extra: &[u8]) -> Box<dyn MessageEncrypter> {
+        Box::new(Tls12GcmMessageEncrypter::<A, Tls12AdditionalData> {
+            key,
+            iv: iv.into(),
+            explicit_nonce: extra.into(),
+            _p: PhantomData,
+        })
+    }
+
+    fn decrypter(&self, key: AeadKey, iv: &[u8]) -> Box<dyn MessageDecrypter> {
+        Box::new(Tls12GcmMessageDecrypter::<A, Tls12AdditionalData> {
+            key,
+            iv: iv.into(),
+            _p: PhantomData,
+        })
+    }
+
+    fn key_block_shape(&self) -> KeyBlockShape {
+        KeyBlockShape {
+            enc_key_len: A::KEY_LEN,
+            fixed_iv_len: 4,
+            explicit_nonce_len: 8,
+        }
+    }
+
+    fn extract_keys(
+        &self,
+        key: AeadKey,
+        iv: &[u8],
+        explicit: &[u8],
+    ) -> Result<ConnectionTrafficSecrets, UnsupportedOperationError> {
+        let iv = Iv::new(gcm_iv(
+            iv.try_into().map_err(|_| UnsupportedOperationError)?,
+            explicit.try_into().map_err(|_| UnsupportedOperationError)?,
+        ));
+        Ok(match A::KEY_LEN {
+            16 => ConnectionTrafficSecrets::Aes128Gcm { key, iv },
+            32 => ConnectionTrafficSecrets::Aes256Gcm { key, iv },
+            _ => unreachable!(),
+        })
+    }
+}
+
+/// TLS 1.2 AEAD scheme AES 128 GCM
+pub(crate) const TLS12_AES_128_GCM_AEAD: &'static dyn Tls12AeadAlgorithm =
+    &Tls12GcmAeadAlgorithm::<aead::Aes128Gcm>(PhantomData);
+
+/// TLS 1.2 AEAD scheme AES 256 GCM
+pub(crate) const TLS12_AES_256_GCM_AEAD: &'static dyn Tls12AeadAlgorithm =
+    &Tls12GcmAeadAlgorithm::<aead::Aes256Gcm>(PhantomData);
+
+// ===================================================
+// TLS 1.2 ChaCha20 cipher
+// ===================================================
+
+struct Tls12Chacha20PolyMessageEncrypter<A, AD> {
+    key: AeadKey,
+    iv: MaybeValidByteArray<NONCE_LEN>,
+    _p: PhantomData<fn() -> (A, AD)>,
+}
+
+impl<A: Chacha20, AD: TlsAdditionalData> MessageEncrypter
+    for Tls12Chacha20PolyMessageEncrypter<A, AD>
+{
+    fn encrypt(
+        &mut self,
+        msg: OutboundPlainMessage<'_>,
+        seq: u64,
+    ) -> Result<OutboundOpaqueMessage, Error> {
+        let payload_len = msg.payload.len();
+        let mut payload = PrefixedPayload::with_capacity(self.encrypted_payload_len(payload_len));
+        let aead = A::new_with_key(&self.key).ok_or(Error::EncryptError)?;
+        let MaybeValidByteArray::Valid(iv) = self.iv else {
+            return Err(Error::EncryptError);
+        };
+        let nonce = make_nonce_from_iv_seq(iv, seq);
+        let nonce = A::nonce_from_buf(&nonce).ok_or(Error::EncryptError)?;
+        let mut ad_buf = [0; TLS12_AAD_SIZE];
+        let ad = AD::build_additional_data(seq, msg.typ, msg.version, payload_len, &mut ad_buf)
+            .ok_or(Error::EncryptError)?;
+        payload.extend_from_chunks(&msg.payload);
+        let tag = aead.seal_in_place(&nonce, &mut payload.as_mut()[..payload_len], ad);
+        payload.extend_from_slice(A::tag_to_buf(&tag));
+        Ok(OutboundOpaqueMessage::new(msg.typ, msg.version, payload))
+    }
+
+    fn encrypted_payload_len(&self, payload_len: usize) -> usize {
+        payload_len + A::TAG_LEN
+    }
+}
+
+struct Tls12Chacha20PolyMessageDecrypter<A, AD> {
+    key: AeadKey,
+    iv: MaybeValidByteArray<NONCE_LEN>,
+    _p: PhantomData<fn() -> (A, AD)>,
+}
+
+impl<A: Chacha20, AD: TlsAdditionalData> MessageDecrypter
+    for Tls12Chacha20PolyMessageDecrypter<A, AD>
+{
+    fn decrypt<'a>(
+        &mut self,
+        mut msg: InboundOpaqueMessage<'a>,
+        seq: u64,
+    ) -> Result<InboundPlainMessage<'a>, Error> {
+        let aead = A::new_with_key(&self.key).ok_or(Error::DecryptError)?;
+        let MaybeValidByteArray::Valid(iv) = self.iv else {
+            return Err(Error::DecryptError);
+        };
+        let nonce = make_nonce_from_iv_seq(iv, seq);
+        let nonce = A::nonce_from_buf(&nonce).ok_or(Error::DecryptError)?;
+        let mut ad_buf = [0; TLS12_AAD_SIZE];
+        let Some(ctxt_len) = msg.payload.len().checked_sub(A::TAG_LEN) else {
+            return Err(Error::DecryptError);
+        };
+        let ad = AD::build_additional_data(seq, msg.typ, msg.version, ctxt_len, &mut ad_buf)
+            .ok_or(Error::DecryptError)?;
+        // TODO(@xfding) Should we check the fragment size?
+        let Some((ciphertext, tag)) = msg.payload.split_at_mut_checked(ctxt_len) else {
+            return Err(Error::DecryptError);
+        };
+        let tag = A::tag_from_buf(tag).ok_or(Error::DecryptError)?;
+        aead.open_in_place(&nonce, ciphertext, tag, ad)
+            .map_err(|_| Error::DecryptError)?;
+        Ok(msg.into_plain_message_range(0..ctxt_len))
+    }
+}
+
+struct Tls12Chacha20AeadAlgorithm<A>(PhantomData<fn() -> A>);
+
+impl<A: 'static + Chacha20> Tls12AeadAlgorithm for Tls12Chacha20AeadAlgorithm<A> {
+    fn encrypter(&self, key: AeadKey, iv: &[u8], _extra: &[u8]) -> Box<dyn MessageEncrypter> {
+        Box::new(
+            Tls12Chacha20PolyMessageEncrypter::<A, Tls12AdditionalData> {
+                key,
+                iv: iv.into(),
+                _p: PhantomData,
+            },
+        )
+    }
+
+    fn decrypter(&self, key: AeadKey, iv: &[u8]) -> Box<dyn MessageDecrypter> {
+        Box::new(
+            Tls12Chacha20PolyMessageDecrypter::<A, Tls12AdditionalData> {
+                key,
+                iv: iv.into(),
+                _p: PhantomData,
+            },
+        )
+    }
+
+    fn key_block_shape(&self) -> KeyBlockShape {
+        KeyBlockShape {
+            enc_key_len: 32,
+            fixed_iv_len: 12,
+            explicit_nonce_len: 0,
+        }
+    }
+
+    fn extract_keys(
+        &self,
+        key: AeadKey,
+        iv: &[u8],
+        _explicit: &[u8],
+    ) -> Result<ConnectionTrafficSecrets, UnsupportedOperationError> {
+        let iv = Iv::new(iv.try_into().map_err(|_| UnsupportedOperationError)?);
+        Ok(ConnectionTrafficSecrets::Chacha20Poly1305 { key, iv })
+    }
+
+    fn fips(&self) -> bool {
+        false
+    }
+}
+
+/// TLS 1.2 AEAD cipher with Chacha202-Poly1305
+pub(crate) static TLS12_CHACHA20_POLY1305_AEAD: &'static dyn Tls12AeadAlgorithm =
+    &Tls12Chacha20AeadAlgorithm::<aead::Chacha20Poly1305>(PhantomData);
+
+// ===================================================
+// TLS 1.3 AEAD cipher family
+// ===================================================
+
+struct Tls13AdditionalData;
+
+impl TlsAdditionalData for Tls13AdditionalData {
+    #[inline]
+    fn build_additional_data<'a>(
+        _seq: u64,
+        _typ: ContentType,
+        _version: ProtocolVersion,
+        len: usize,
+        ad_buf: &'a mut [u8],
+    ) -> Option<&'a [u8]> {
+        let ad_buf: &mut [u8; 5] = ad_buf.try_into().ok()?;
+        ad_buf.copy_from_slice(&make_tls13_aad(len));
+        Some(ad_buf)
+    }
+}
+
+/// TLS 1.3 Cipher suite TLS13_AES_128_GCM
+pub(crate) const TLS13_AES_128_GCM: &'static dyn Tls13AeadAlgorithm =
+    &Tls13AeadAlgorithmImpl::<aead::Aes128Gcm>(PhantomData);
+
+/// TLS 1.3 Cipher suite TLS13_AES_256_GCM
+pub(crate) const TLS13_AES_256_GCM: &'static dyn Tls13AeadAlgorithm =
+    &Tls13AeadAlgorithmImpl::<aead::Aes256Gcm>(PhantomData);
+
+/// TLS 1.3 Cipher suite TLS13_CHACHA20_POLY1305
+pub(crate) const TLS13_CHACHA20_POLY1305: &'static dyn Tls13AeadAlgorithm =
+    &Tls13AeadAlgorithmImpl::<aead::Chacha20Poly1305>(PhantomData);
+
+const TLS13_AAD_SIZE: usize = 5;
+
+struct Tls13MessageEncrypter<A, AD> {
+    key: AeadKey,
+    iv: MaybeValidByteArray<NONCE_LEN>,
+    _p: PhantomData<fn() -> (A, AD)>,
+}
+
+impl<A: AeadConstructible, AD: TlsAdditionalData> MessageEncrypter
+    for Tls13MessageEncrypter<A, AD>
+{
+    fn encrypt(
+        &mut self,
+        msg: OutboundPlainMessage<'_>,
+        seq: u64,
+    ) -> Result<OutboundOpaqueMessage, Error> {
+        let plaintxt_len = msg.payload.len();
+        let payload_len = self.encrypted_payload_len(plaintxt_len);
+        let mut payload = PrefixedPayload::with_capacity(payload_len);
+        let aead = A::new_with_key(&self.key).ok_or(Error::EncryptError)?;
+        let MaybeValidByteArray::Valid(iv) = self.iv else {
+            return Err(Error::EncryptError);
+        };
+        let nonce = make_nonce_from_iv_seq(iv, seq);
+        let nonce = A::nonce_from_buf(&nonce).ok_or(Error::EncryptError)?;
+        let mut ad_buf = [0; TLS13_AAD_SIZE];
+        let ad = AD::build_additional_data(seq, msg.typ, msg.version, payload_len, &mut ad_buf)
+            .ok_or(Error::EncryptError)?;
+        // Layout: [<..PAYLOAD, TYPE (size 1)>, TAG (size TAG_LEN)]
+        payload.extend_from_chunks(&msg.payload);
+        payload.extend_from_slice(&msg.typ.to_array());
+        let tag = aead.seal_in_place(&nonce, &mut payload.as_mut()[0..plaintxt_len + 1], ad);
+        payload.extend_from_slice(A::tag_to_buf(&tag));
+        Ok(OutboundOpaqueMessage::new(msg.typ, msg.version, payload))
+    }
+
+    fn encrypted_payload_len(&self, payload_len: usize) -> usize {
+        payload_len + A::TAG_LEN + 1
+    }
+}
+
+struct Tls13MessageDecrypter<A, AD> {
+    key: AeadKey,
+    iv: MaybeValidByteArray<NONCE_LEN>,
+    _p: PhantomData<fn() -> (A, AD)>,
+}
+
+impl<A: AeadConstructible, AD: TlsAdditionalData> MessageDecrypter
+    for Tls13MessageDecrypter<A, AD>
+{
+    fn decrypt<'a>(
+        &mut self,
+        mut msg: InboundOpaqueMessage<'a>,
+        seq: u64,
+    ) -> Result<InboundPlainMessage<'a>, Error> {
+        let aead = A::new_with_key(&self.key).ok_or(Error::DecryptError)?;
+        let MaybeValidByteArray::Valid(iv) = self.iv else {
+            return Err(Error::DecryptError);
+        };
+        let nonce = make_nonce_from_iv_seq(iv, seq);
+        let nonce = A::nonce_from_buf(&nonce).ok_or(Error::DecryptError)?;
+        let InboundOpaqueMessage {
+            typ,
+            version,
+            ref mut payload,
+        } = msg;
+        // Layout: [<..PAYLOAD, TYPE (size 1)>, TAG (size TAG_LEN)]
+        let total_len = payload.len();
+        let Some(ctxt_len) = total_len.checked_sub(A::TAG_LEN) else {
+            return Err(Error::DecryptError);
+        };
+        let Some((ciphertext, tag)) = payload.split_at_mut_checked(ctxt_len) else {
+            return Err(Error::DecryptError);
+        };
+        let mut ad_buf = [0; TLS13_AAD_SIZE];
+        let ad = AD::build_additional_data(seq, typ, version, total_len, &mut ad_buf)
+            .ok_or(Error::DecryptError)?;
+        let tag = A::tag_from_buf(tag).ok_or(Error::DecryptError)?;
+        aead.open_in_place(&nonce, ciphertext, tag, ad)
+            .map_err(|_| Error::DecryptError)?;
+        payload.truncate(ctxt_len);
+        msg.into_tls13_unpadded_message()
+    }
+}
+
+struct Tls13AeadAlgorithmImpl<A>(PhantomData<fn() -> A>);
+
+impl<A: 'static + AeadConstructible> Tls13AeadAlgorithm for Tls13AeadAlgorithmImpl<A> {
+    fn encrypter(&self, key: AeadKey, iv: Iv) -> Box<dyn MessageEncrypter> {
+        Box::new(Tls13MessageEncrypter::<A, Tls13AdditionalData> {
+            key,
+            iv: iv.as_ref().into(),
+            _p: PhantomData,
+        })
+    }
+
+    fn decrypter(&self, key: AeadKey, iv: Iv) -> Box<dyn MessageDecrypter> {
+        Box::new(Tls13MessageDecrypter::<A, Tls13AdditionalData> {
+            key,
+            iv: iv.as_ref().into(),
+            _p: PhantomData,
+        })
+    }
+
+    fn key_len(&self) -> usize {
+        A::KEY_LEN
+    }
+
+    fn extract_keys(
+        &self,
+        key: AeadKey,
+        iv: Iv,
+    ) -> Result<ConnectionTrafficSecrets, UnsupportedOperationError> {
+        Ok(match A::KIND {
+            AeadKind::Aes128Gcm => ConnectionTrafficSecrets::Aes128Gcm { key, iv },
+            AeadKind::Aes256Gcm => ConnectionTrafficSecrets::Aes256Gcm { key, iv },
+            AeadKind::Chacha20Poly1305 => ConnectionTrafficSecrets::Chacha20Poly1305 { key, iv },
+        })
+    }
+
+    fn fips(&self) -> bool {
+        false
+    }
+}
diff --git a/rust/bssl-tls/src/rustls_provider/cipher_suites.rs b/rust/bssl-tls/src/rustls_provider/cipher_suites.rs
new file mode 100644
index 0000000..f8417cb
--- /dev/null
+++ b/rust/bssl-tls/src/rustls_provider/cipher_suites.rs
@@ -0,0 +1,148 @@
+// 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.
+
+//! Supported cipher suites
+
+use bssl_crypto::digest;
+use rustls::{
+    CipherSuite, CipherSuiteCommon, SignatureScheme, Tls12CipherSuite, Tls13CipherSuite,
+    crypto::KeyExchangeAlgorithm,
+};
+
+use super::HashAlgorithm;
+use super::prf::Tls12PrfImpl;
+
+macro_rules! tls12_suites {
+    ($($suite:ident {
+        $hash:ident,
+        $confid:expr,
+        $kx:ident,
+        $sign:expr,
+        $aead:ident
+    })*) => {
+        $(
+            #[doc = concat!("TLS 1.2 cipher suite `", stringify!($suite), "`")]
+            pub const $suite: &'static Tls12CipherSuite = &Tls12CipherSuite {
+                common: CipherSuiteCommon {
+                    suite: CipherSuite::$suite,
+                    hash_provider: &HashAlgorithm::<digest::$hash>::new(),
+                    confidentiality_limit: $confid,
+                },
+                prf_provider: &Tls12PrfImpl::<digest::$hash>::new(),
+                kx: KeyExchangeAlgorithm::$kx,
+                sign: $sign,
+                aead_alg: super::aead::$aead,
+            };
+        )*
+    };
+    ($($tt:tt)*) => {
+        tls12_suites!($($tt),*)
+    };
+}
+
+tls12_suites! {
+    TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 {
+        Sha256,
+        1 << 24,
+        ECDHE,
+        TLS12_ECDSA_SCHEMES,
+        TLS12_CHACHA20_POLY1305_AEAD
+    }
+    TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 {
+        Sha256,
+        u64::MAX,
+        ECDHE,
+        TLS12_RSA_SCHEMES,
+        TLS12_CHACHA20_POLY1305_AEAD
+    }
+    TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 {
+        Sha256,
+        1 << 24,
+        ECDHE,
+        TLS12_RSA_SCHEMES,
+        TLS12_AES_128_GCM_AEAD
+    }
+    TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 {
+        Sha384,
+        1 << 24,
+        ECDHE,
+        TLS12_RSA_SCHEMES,
+        TLS12_AES_256_GCM_AEAD
+    }
+    TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 {
+        Sha256,
+        1 << 24,
+        ECDHE,
+        TLS12_ECDSA_SCHEMES,
+        TLS12_AES_128_GCM_AEAD
+    }
+    TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 {
+        Sha384,
+        1 << 24,
+        ECDHE,
+        TLS12_ECDSA_SCHEMES,
+        TLS12_AES_256_GCM_AEAD
+    }
+}
+
+const TLS12_ECDSA_SCHEMES: &[SignatureScheme] = &[
+    SignatureScheme::ED25519,
+    SignatureScheme::ECDSA_NISTP384_SHA384,
+    SignatureScheme::ECDSA_NISTP256_SHA256,
+];
+
+const TLS12_RSA_SCHEMES: &[SignatureScheme] = &[
+    SignatureScheme::RSA_PKCS1_SHA256,
+    SignatureScheme::RSA_PKCS1_SHA384,
+    SignatureScheme::RSA_PKCS1_SHA512,
+    SignatureScheme::RSA_PSS_SHA256,
+    SignatureScheme::RSA_PSS_SHA384,
+    SignatureScheme::RSA_PSS_SHA512,
+];
+
+/// Cipher suite TLS13_AES_128_GCM_SHA256
+pub const TLS13_AES_128_GCM_SHA256: &'static Tls13CipherSuite = &Tls13CipherSuite {
+    common: CipherSuiteCommon {
+        suite: CipherSuite::TLS13_AES_128_GCM_SHA256,
+        hash_provider: &HashAlgorithm::<digest::Sha256>::new(),
+        confidentiality_limit: 1 << 24,
+    },
+    hkdf_provider: super::prf::TLS13_HKDF_SHA256,
+    aead_alg: super::aead::TLS13_AES_128_GCM,
+    quic: None,
+};
+
+/// Cipher suite TLS13_AES_256_GCM_SHA384
+pub const TLS13_AES_256_GCM_SHA384: &'static Tls13CipherSuite = &Tls13CipherSuite {
+    common: CipherSuiteCommon {
+        suite: CipherSuite::TLS13_AES_256_GCM_SHA384,
+        hash_provider: &HashAlgorithm::<digest::Sha384>::new(),
+        confidentiality_limit: 1 << 24,
+    },
+    hkdf_provider: super::prf::TLS13_HKDF_SHA384,
+    aead_alg: super::aead::TLS13_AES_256_GCM,
+    quic: None,
+};
+
+/// Cipher suite TLS13_CHACHA20_POLY1305_SHA256
+pub const TLS13_CHACHA20_POLY1305_SHA256: &'static Tls13CipherSuite = &Tls13CipherSuite {
+    common: CipherSuiteCommon {
+        suite: CipherSuite::TLS13_CHACHA20_POLY1305_SHA256,
+        hash_provider: &HashAlgorithm::<digest::Sha256>::new(),
+        confidentiality_limit: u64::MAX,
+    },
+    hkdf_provider: super::prf::TLS13_HKDF_SHA256,
+    aead_alg: super::aead::TLS13_CHACHA20_POLY1305,
+    quic: None,
+};
diff --git a/rust/bssl-tls/src/rustls_provider/key_exchange.rs b/rust/bssl-tls/src/rustls_provider/key_exchange.rs
new file mode 100644
index 0000000..e109171
--- /dev/null
+++ b/rust/bssl-tls/src/rustls_provider/key_exchange.rs
@@ -0,0 +1,146 @@
+// 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.
+
+//! Supported key exchange groups
+//!
+//! We support the following standard groups.
+//!
+//! - [ECDH_P256] for elliptic curve based Diffie-Hellman ECDH-P256
+//! - [ECDH_P384] for elliptic curve based Diffie-Hellman ECDH-P384
+//! - [X25519] for X25519
+//!
+//! If `mlalgs` feature is enabled, we also support the following post-quantum hybrid key exchange
+//! groups, for TLS 1.3.
+//!
+//! - [X25519MLKEM768] for X25519MLKEM768
+
+use alloc::{
+    boxed::Box,
+    fmt::{Debug, Formatter, Result as FmtResult},
+    vec::Vec,
+};
+use core::marker::PhantomData;
+
+use bssl_crypto::{ec, ecdh, x25519};
+use rustls::{
+    Error, NamedGroup, PeerMisbehaved,
+    crypto::{ActiveKeyExchange, SharedSecret, SupportedKxGroup},
+};
+
+#[cfg(feature = "mlalgs")]
+mod mlkem;
+#[cfg(feature = "mlalgs")]
+pub use mlkem::X25519MLKEM768;
+
+/// Elliptic Curve Diffie-Hellman key exchange group
+struct EcGroup<C: ec::Curve>(PhantomData<fn() -> C>);
+/// Elliptic Curve Diffie-Hellman key exchange group `ECDH-P256`
+pub const ECDH_P256: &'static dyn SupportedKxGroup = &EcGroup::<ec::P256>(PhantomData);
+/// Elliptic Curve Diffie-Hellman key exchange group `ECDH-P384`
+pub const ECDH_P384: &'static dyn SupportedKxGroup = &EcGroup::<ec::P384>(PhantomData);
+
+impl<C: ec::Curve> Debug for EcGroup<C> {
+    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
+        f.debug_struct("EcGroup").finish()
+    }
+}
+
+impl<C: ec::Curve + 'static> SupportedKxGroup for EcGroup<C> {
+    fn start(&self) -> Result<Box<dyn ActiveKeyExchange>, Error> {
+        let priv_key = ecdh::PrivateKey::<C>::generate();
+        let pub_key = priv_key.to_public_key();
+        let pub_key_x962_uncompressed = pub_key.to_x962_uncompressed().as_ref().into();
+        Ok(Box::new(EcActiveKeyExchange {
+            priv_key,
+            pub_key_x962_uncompressed,
+        }))
+    }
+
+    fn name(&self) -> NamedGroup {
+        match C::group() {
+            ec::Group::P256 => NamedGroup::secp256r1,
+            ec::Group::P384 => NamedGroup::secp384r1,
+        }
+    }
+}
+
+/// This type encodes the Diffie-Hellman key exchange state during TLS.
+struct EcActiveKeyExchange<C: ec::Curve> {
+    priv_key: ecdh::PrivateKey<C>,
+    pub_key_x962_uncompressed: Vec<u8>,
+}
+impl<C: ec::Curve> ActiveKeyExchange for EcActiveKeyExchange<C> {
+    fn complete(self: Box<Self>, peer_pub_key: &[u8]) -> Result<SharedSecret, Error> {
+        let peer_pub_key = ecdh::PublicKey::from_x962_uncompressed(peer_pub_key)
+            .ok_or(Error::PeerMisbehaved(PeerMisbehaved::InvalidKeyShare))?;
+        let shared_secret = self.priv_key.compute_shared_key(&peer_pub_key);
+        Ok(shared_secret.into())
+    }
+
+    fn pub_key(&self) -> &[u8] {
+        &self.pub_key_x962_uncompressed
+    }
+
+    fn group(&self) -> NamedGroup {
+        match C::group() {
+            ec::Group::P256 => NamedGroup::secp256r1,
+            ec::Group::P384 => NamedGroup::secp384r1,
+        }
+    }
+}
+
+/// X25519 Key exchange group
+#[derive(Debug)]
+struct X25519Group;
+/// X25519 key exchange group
+pub const X25519: &'static dyn SupportedKxGroup = &X25519Group;
+
+impl SupportedKxGroup for X25519Group {
+    fn start(&self) -> Result<Box<dyn ActiveKeyExchange>, Error> {
+        let (pub_key, priv_key) = x25519::PrivateKey::generate();
+        Ok(Box::new(X25519ActiveKeyExchange { pub_key, priv_key }))
+    }
+
+    fn name(&self) -> NamedGroup {
+        NamedGroup::X25519
+    }
+}
+
+struct X25519ActiveKeyExchange {
+    pub_key: x25519::PublicKey,
+    priv_key: x25519::PrivateKey,
+}
+
+impl ActiveKeyExchange for X25519ActiveKeyExchange {
+    fn complete(self: Box<Self>, peer_pub_key: &[u8]) -> Result<SharedSecret, Error> {
+        let secret = self
+            .priv_key
+            .compute_shared_key(
+                peer_pub_key
+                    .try_into()
+                    .map_err(|_| Error::PeerMisbehaved(PeerMisbehaved::InvalidKeyShare))?,
+            )
+            .ok_or(Error::PeerMisbehaved(PeerMisbehaved::InvalidKeyShare))?
+            .to_vec();
+        Ok(secret.into())
+    }
+
+    fn pub_key(&self) -> &[u8] {
+        &self.pub_key
+    }
+
+    fn group(&self) -> NamedGroup {
+        NamedGroup::X25519
+    }
+}
diff --git a/rust/bssl-tls/src/rustls_provider/key_exchange/mlkem.rs b/rust/bssl-tls/src/rustls_provider/key_exchange/mlkem.rs
new file mode 100644
index 0000000..45169a6
--- /dev/null
+++ b/rust/bssl-tls/src/rustls_provider/key_exchange/mlkem.rs
@@ -0,0 +1,144 @@
+// 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 alloc::{boxed::Box, vec::Vec};
+
+use bssl_crypto::{mlkem, x25519};
+use rustls::{
+    Error, NamedGroup, PeerMisbehaved, ProtocolVersion,
+    crypto::{ActiveKeyExchange, CompletedKeyExchange, SharedSecret, SupportedKxGroup},
+};
+
+/// X25519MLKEM768 key exchange group
+pub const X25519MLKEM768: &'static dyn SupportedKxGroup = &X25519Mlkem768Group;
+
+macro_rules! ok_or_invalid_share {
+    ($e:expr) => {
+        match $e {
+            Some(v) => v,
+            None => return Err(Error::PeerMisbehaved(PeerMisbehaved::InvalidKeyShare)),
+        }
+    };
+}
+
+#[derive(Debug)]
+struct X25519Mlkem768Group;
+
+impl X25519Mlkem768Group {
+    fn extract_keys(client_pub_key: &[u8]) -> Option<(mlkem::PublicKey768, x25519::PublicKey)> {
+        // X25519MLKEM768 transposed the order of concatenation in spite of the naming
+        let (client_encap_key, client_pub_key) =
+            client_pub_key.split_at_checked(mlkem::PUBLIC_KEY_BYTES_768)?;
+        let client_encap_key = mlkem::PublicKey768::parse(client_encap_key)?;
+        Some((client_encap_key, client_pub_key.try_into().ok()?))
+    }
+}
+
+impl SupportedKxGroup for X25519Mlkem768Group {
+    fn start(&self) -> Result<Box<dyn ActiveKeyExchange>, Error> {
+        let (encap_key, decap_key, _) = mlkem::PrivateKey768::generate();
+        let (pub_key, priv_key) = x25519::PrivateKey::generate();
+        // X25519MLKEM768 transposed the order of concatenation in spite of the naming
+        let mut client_share = encap_key;
+        client_share.extend_from_slice(&pub_key);
+        Ok(Box::new(X25519MlKem768ActiveKeyExchange {
+            decap_key,
+            priv_key,
+            pub_key,
+            client_share,
+        }))
+    }
+
+    // This override is for server side share
+    fn start_and_complete(&self, client_share: &[u8]) -> Result<CompletedKeyExchange, Error> {
+        // X25519MLKEM768 transposed the order of concatenation in spite of the naming
+        let (client_encap_key, client_pub_key) =
+            ok_or_invalid_share!(Self::extract_keys(client_share));
+        let (pub_key, priv_key) = x25519::PrivateKey::generate();
+        let dh_secret = ok_or_invalid_share!(priv_key.compute_shared_key(&client_pub_key));
+        let (mlkem_ctxt, quantum_secret) = client_encap_key.encapsulate();
+
+        let mut server_share = mlkem_ctxt;
+        server_share.extend(pub_key);
+
+        let mut secret = Vec::with_capacity(mlkem::SHARED_SECRET_BYTES + x25519::SHARED_KEY_LEN);
+        secret.extend(quantum_secret);
+        secret.extend(dh_secret);
+        Ok(CompletedKeyExchange {
+            group: NamedGroup::X25519MLKEM768,
+            pub_key: server_share,
+            secret: secret.into(),
+        })
+    }
+
+    fn name(&self) -> NamedGroup {
+        NamedGroup::X25519MLKEM768
+    }
+
+    fn usable_for_version(&self, version: ProtocolVersion) -> bool {
+        matches!(version, ProtocolVersion::TLSv1_3)
+    }
+}
+
+struct X25519MlKem768ActiveKeyExchange {
+    decap_key: mlkem::PrivateKey768,
+    priv_key: x25519::PrivateKey,
+    pub_key: x25519::PublicKey,
+    client_share: Vec<u8>,
+}
+
+impl X25519MlKem768ActiveKeyExchange {
+    fn compute_dh_share(&self, peer_pub_key: &[u8]) -> Result<[u8; x25519::SHARED_KEY_LEN], Error> {
+        peer_pub_key
+            .try_into()
+            .ok()
+            .and_then(|peer_pub_key| self.priv_key.compute_shared_key(&peer_pub_key))
+            .ok_or(Error::PeerMisbehaved(PeerMisbehaved::InvalidKeyShare))
+    }
+}
+
+impl ActiveKeyExchange for X25519MlKem768ActiveKeyExchange {
+    fn complete(self: Box<Self>, peer_pub_key: &[u8]) -> Result<SharedSecret, Error> {
+        // X25519MLKEM768 transposed the order of concatenation in spite of the naming
+        let (peer_mlkem_share, peer_x25519_share) =
+            ok_or_invalid_share!(peer_pub_key.split_at_checked(mlkem::CIPHERTEXT_BYTES_768));
+        let quantum_secret = ok_or_invalid_share!(self.decap_key.decapsulate(peer_mlkem_share));
+        let dh_secret = self.compute_dh_share(peer_x25519_share)?;
+        let mut shared_secret =
+            Vec::with_capacity(mlkem::SHARED_SECRET_BYTES + x25519::SHARED_KEY_LEN);
+        shared_secret.extend(quantum_secret);
+        shared_secret.extend(dh_secret);
+        Ok(shared_secret.into())
+    }
+
+    fn pub_key(&self) -> &[u8] {
+        &self.client_share
+    }
+
+    fn hybrid_component(&self) -> Option<(NamedGroup, &[u8])> {
+        Some((NamedGroup::X25519, &self.pub_key))
+    }
+
+    fn complete_hybrid_component(
+        self: Box<Self>,
+        peer_pub_key: &[u8],
+    ) -> Result<SharedSecret, Error> {
+        self.compute_dh_share(peer_pub_key)
+            .map(|secret| Vec::from(secret).into())
+    }
+
+    fn group(&self) -> NamedGroup {
+        NamedGroup::X25519MLKEM768
+    }
+}
diff --git a/rust/bssl-tls/src/rustls_provider/pki.rs b/rust/bssl-tls/src/rustls_provider/pki.rs
new file mode 100644
index 0000000..bb16e2d
--- /dev/null
+++ b/rust/bssl-tls/src/rustls_provider/pki.rs
@@ -0,0 +1,347 @@
+// 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.
+
+//! PKI definitions for TLS
+
+use core::{
+    fmt::{Debug, Formatter, Result as FmtResult},
+    marker::PhantomData,
+};
+
+use bssl_crypto::{digest, ec, ecdsa, ed25519, rsa};
+use rustls::pki_types::{
+    AlgorithmIdentifier, InvalidSignature, SignatureVerificationAlgorithm, alg_id,
+};
+
+use crate::rustls_provider::RsaSignatureDigest;
+
+/// A PKI object with assigned Object Identifier
+pub(crate) trait PkiIdentified {
+    /// The assigned identifier
+    const OBJECT_IDENTIFIER: AlgorithmIdentifier;
+}
+
+/// PKI Verification algorithm
+pub(crate) trait PkiPublicKeyAlgorithm: PkiIdentified {
+    /// Public key type that operates based on the identified algorithm.
+    type PublicKey;
+
+    /// Decode a signature public key from a `subjectPublicKey` field.
+    fn from_der_subject_public_key(spk: &[u8]) -> Option<Self::PublicKey>;
+}
+
+/// PKI Verification algorithm
+pub(crate) trait PkiSignatureAlgorithm: PkiIdentified {
+    /// Supported public key, which can be deserialised from a DER-serialised
+    /// `SubjectPublicKeyInfo` field.
+    type PublicKeyAlgorithm: PkiPublicKeyAlgorithm;
+
+    /// Perform verification of the signature against the deserialised public key
+    /// and the original message.
+    fn verify(
+        public_key: <Self::PublicKeyAlgorithm as PkiPublicKeyAlgorithm>::PublicKey,
+        message: &[u8],
+        signature: &[u8],
+    ) -> bool;
+}
+
+/// PKI verification as prescribed in [RFC 3279](https://datatracker.ietf.org/doc/html/rfc3279)
+struct PkiSignatureVerification<S>(PhantomData<fn() -> S>);
+
+impl<S> PkiSignatureVerification<S> {
+    pub(crate) const fn new() -> Self {
+        Self(PhantomData)
+    }
+}
+
+impl<S> Debug for PkiSignatureVerification<S> {
+    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
+        f.debug_struct("PkiVerification").finish()
+    }
+}
+
+impl<S: PkiSignatureAlgorithm> SignatureVerificationAlgorithm for PkiSignatureVerification<S> {
+    fn verify_signature(
+        &self,
+        subject_public_key: &[u8],
+        message: &[u8],
+        signature: &[u8],
+    ) -> Result<(), InvalidSignature> {
+        let public_key = S::PublicKeyAlgorithm::from_der_subject_public_key(subject_public_key)
+            .ok_or(InvalidSignature)?;
+        if S::verify(public_key, message, signature) {
+            Ok(())
+        } else {
+            Err(InvalidSignature)
+        }
+    }
+
+    fn public_key_alg_id(&self) -> AlgorithmIdentifier {
+        S::PublicKeyAlgorithm::OBJECT_IDENTIFIER
+    }
+
+    fn signature_alg_id(&self) -> AlgorithmIdentifier {
+        S::OBJECT_IDENTIFIER
+    }
+}
+
+/// RSA encryption
+///
+/// OID: 1.2.840.113549.1.1.1
+struct PkiRsaPublicKey;
+
+impl PkiIdentified for PkiRsaPublicKey {
+    const OBJECT_IDENTIFIER: AlgorithmIdentifier = alg_id::RSA_ENCRYPTION;
+}
+
+impl PkiPublicKeyAlgorithm for PkiRsaPublicKey {
+    type PublicKey = rsa::PublicKey;
+
+    fn from_der_subject_public_key(spk: &[u8]) -> Option<Self::PublicKey> {
+        rsa::PublicKey::from_der_rsa_public_key(spk)
+    }
+}
+
+/// Elliptic curve (EC) public key encoded in X9.62
+///
+/// OID: 1.2.840.10045.2.1
+struct PkiEcPublicKey<C> {
+    pub(crate) _p: PhantomData<fn() -> C>,
+}
+
+/// EC public key over curve P-256
+///
+/// OID: 1.2.840.10045.3.1.7
+impl PkiIdentified for PkiEcPublicKey<ec::P256> {
+    const OBJECT_IDENTIFIER: AlgorithmIdentifier = alg_id::ECDSA_P256;
+}
+
+/// EC public key over curve P-384
+///
+/// OID: 1.3.132.0.34
+impl PkiIdentified for PkiEcPublicKey<ec::P384> {
+    const OBJECT_IDENTIFIER: AlgorithmIdentifier = alg_id::ECDSA_P384;
+}
+
+impl<C: ec::Curve> PkiPublicKeyAlgorithm for PkiEcPublicKey<C>
+where
+    Self: PkiIdentified,
+{
+    type PublicKey = ecdsa::PublicKey<C>;
+
+    fn from_der_subject_public_key(spk: &[u8]) -> Option<Self::PublicKey> {
+        ecdsa::PublicKey::from_x962_uncompressed(spk)
+            .or_else(|| ecdsa::PublicKey::from_x962_compressed(spk))
+    }
+}
+
+/// Ed25519/EdDSA public key
+///
+/// OID: 1.3.101.112
+pub(crate) struct PkiEd25519PublicKey;
+
+impl PkiIdentified for PkiEd25519PublicKey {
+    const OBJECT_IDENTIFIER: AlgorithmIdentifier = alg_id::ED25519;
+}
+
+impl PkiPublicKeyAlgorithm for PkiEd25519PublicKey {
+    type PublicKey = ed25519::PublicKey;
+
+    fn from_der_subject_public_key(spk: &[u8]) -> Option<Self::PublicKey> {
+        Some(ed25519::PublicKey::from_bytes(spk.try_into().ok()?))
+    }
+}
+
+/// A family of RSA signature scheme
+struct PkiRsaPkcs1SignatureScheme<D> {
+    pub(crate) _p: PhantomData<fn() -> D>,
+}
+
+pub(crate) enum DigestAlgorithm {
+    Sha256,
+    Sha384,
+    Sha512,
+}
+
+/// RSA with SHA256 signature scheme
+///
+/// OID: 1.2.840.113549.1.1.11
+impl PkiIdentified for PkiRsaPkcs1SignatureScheme<digest::Sha256> {
+    const OBJECT_IDENTIFIER: AlgorithmIdentifier = alg_id::RSA_PKCS1_SHA256;
+}
+
+/// RSA with SHA384
+///
+/// OID: 1.2.840.113549.1.1.12
+impl PkiIdentified for PkiRsaPkcs1SignatureScheme<digest::Sha384> {
+    const OBJECT_IDENTIFIER: AlgorithmIdentifier = alg_id::RSA_PKCS1_SHA384;
+}
+
+/// RSA with SHA512
+///
+/// OID: 1.2.840.113549.1.1.13
+impl PkiIdentified for PkiRsaPkcs1SignatureScheme<digest::Sha512> {
+    const OBJECT_IDENTIFIER: AlgorithmIdentifier = alg_id::RSA_PKCS1_SHA512;
+}
+
+impl<D: RsaSignatureDigest> PkiSignatureAlgorithm for PkiRsaPkcs1SignatureScheme<D>
+where
+    Self: PkiIdentified,
+{
+    type PublicKeyAlgorithm = PkiRsaPublicKey;
+
+    fn verify(
+        public_key: <Self::PublicKeyAlgorithm as PkiPublicKeyAlgorithm>::PublicKey,
+        message: &[u8],
+        signature: &[u8],
+    ) -> bool {
+        public_key.verify_pkcs1::<D>(message, signature).is_ok()
+    }
+}
+
+/// A family of RSA signature scheme
+struct PkiRsaPssSignatureScheme<D> {
+    pub(crate) _p: PhantomData<fn() -> D>,
+}
+
+/// RSA PSS with SHA256 signature scheme
+///
+/// OID: 1.2.840.113549.1.1.10
+impl PkiIdentified for PkiRsaPssSignatureScheme<digest::Sha256> {
+    const OBJECT_IDENTIFIER: AlgorithmIdentifier = alg_id::RSA_PSS_SHA256;
+}
+
+/// RSA PSS with SHA384
+///
+/// OID: 1.2.840.113549.1.1.10
+impl PkiIdentified for PkiRsaPssSignatureScheme<digest::Sha384> {
+    const OBJECT_IDENTIFIER: AlgorithmIdentifier = alg_id::RSA_PSS_SHA384;
+}
+
+/// RSA PSS with SHA512
+///
+/// OID: 1.2.840.113549.1.1.10
+impl PkiIdentified for PkiRsaPssSignatureScheme<digest::Sha512> {
+    const OBJECT_IDENTIFIER: AlgorithmIdentifier = alg_id::RSA_PSS_SHA512;
+}
+
+impl<D: RsaSignatureDigest> PkiSignatureAlgorithm for PkiRsaPssSignatureScheme<D>
+where
+    Self: PkiIdentified,
+{
+    type PublicKeyAlgorithm = PkiRsaPublicKey;
+
+    fn verify(
+        public_key: <Self::PublicKeyAlgorithm as PkiPublicKeyAlgorithm>::PublicKey,
+        message: &[u8],
+        signature: &[u8],
+    ) -> bool {
+        public_key.verify_pss::<D>(message, signature).is_ok()
+    }
+}
+
+struct PkiEcdsaScheme<C> {
+    pub(crate) _p: PhantomData<fn() -> C>,
+}
+
+/// ECDSA with SHA-256
+///
+/// OID: 1.2.840.10045.4.3.2
+impl PkiIdentified for PkiEcdsaScheme<ec::P256> {
+    const OBJECT_IDENTIFIER: AlgorithmIdentifier = alg_id::ECDSA_SHA256;
+}
+
+/// ECDSA with SHA-384
+///
+/// OID: 1.2.840.10045.4.3.3
+impl PkiIdentified for PkiEcdsaScheme<ec::P384> {
+    const OBJECT_IDENTIFIER: AlgorithmIdentifier = alg_id::ECDSA_SHA384;
+}
+
+impl<C: ec::Curve> PkiSignatureAlgorithm for PkiEcdsaScheme<C>
+where
+    Self: PkiIdentified,
+    PkiEcPublicKey<C>: PkiIdentified,
+{
+    type PublicKeyAlgorithm = PkiEcPublicKey<C>;
+
+    fn verify(
+        public_key: <Self::PublicKeyAlgorithm as PkiPublicKeyAlgorithm>::PublicKey,
+        message: &[u8],
+        signature: &[u8],
+    ) -> bool {
+        // Safety: we would only allow SHA-256 hashing onto P-256 and SHA-386 hashing onto P-384
+        public_key.verify(message, signature).is_ok()
+    }
+}
+
+/// Signature algorithm EdDSA
+///
+/// OID: 1.3.101.112
+struct PkiEddsa;
+
+impl PkiIdentified for PkiEddsa {
+    const OBJECT_IDENTIFIER: AlgorithmIdentifier = alg_id::ED25519;
+}
+
+impl PkiSignatureAlgorithm for PkiEddsa {
+    type PublicKeyAlgorithm = PkiEd25519PublicKey;
+
+    fn verify(
+        public_key: <Self::PublicKeyAlgorithm as PkiPublicKeyAlgorithm>::PublicKey,
+        message: &[u8],
+        signature: &[u8],
+    ) -> bool {
+        let Ok(signature) = signature.try_into() else {
+            return false;
+        };
+        public_key.verify(message, signature).is_ok()
+    }
+}
+
+/// PKI Signature scheme `ECDSA_NISTP256_SHA256`
+pub const ECDSA_NISTP256_SHA256: &'static dyn SignatureVerificationAlgorithm =
+    &PkiSignatureVerification::<PkiEcdsaScheme<ec::P256>>::new();
+
+/// PKI Signature scheme `ECDSA_NISTP384_SHA384`
+pub const ECDSA_NISTP384_SHA384: &'static dyn SignatureVerificationAlgorithm =
+    &PkiSignatureVerification::<PkiEcdsaScheme<ec::P384>>::new();
+
+/// PKI Signature scheme `ED25519`
+pub const ED25519: &'static (dyn SignatureVerificationAlgorithm + 'static) =
+    &PkiSignatureVerification::<PkiEddsa>::new();
+
+/// PKI Signature scheme `RSA_PKCS1_SHA256`
+pub const RSA_PKCS1_SHA256: &'static (dyn SignatureVerificationAlgorithm + 'static) =
+    &PkiSignatureVerification::<PkiRsaPkcs1SignatureScheme<digest::Sha256>>::new();
+
+/// PKI Signature scheme `RSA_PKCS1_SHA384`
+pub const RSA_PKCS1_SHA384: &'static (dyn SignatureVerificationAlgorithm + 'static) =
+    &PkiSignatureVerification::<PkiRsaPkcs1SignatureScheme<digest::Sha384>>::new();
+
+/// PKI Signature scheme `RSA_PKCS1_SHA512`
+pub const RSA_PKCS1_SHA512: &'static (dyn SignatureVerificationAlgorithm + 'static) =
+    &PkiSignatureVerification::<PkiRsaPkcs1SignatureScheme<digest::Sha512>>::new();
+
+/// PKI Signature scheme `RSA_PSS_SHA256`
+pub const RSA_PSS_SHA256: &'static (dyn SignatureVerificationAlgorithm + 'static) =
+    &PkiSignatureVerification::<PkiRsaPssSignatureScheme<digest::Sha256>>::new();
+
+/// PKI Signature scheme `RSA_PSS_SHA384`
+pub const RSA_PSS_SHA384: &'static (dyn SignatureVerificationAlgorithm + 'static) =
+    &PkiSignatureVerification::<PkiRsaPssSignatureScheme<digest::Sha384>>::new();
+
+/// PKI Signature scheme `RSA_PSS_SHA512`
+pub const RSA_PSS_SHA512: &'static (dyn SignatureVerificationAlgorithm + 'static) =
+    &PkiSignatureVerification::<PkiRsaPssSignatureScheme<digest::Sha512>>::new();
diff --git a/rust/bssl-tls/src/rustls_provider/prf.rs b/rust/bssl-tls/src/rustls_provider/prf.rs
new file mode 100644
index 0000000..b41beb0
--- /dev/null
+++ b/rust/bssl-tls/src/rustls_provider/prf.rs
@@ -0,0 +1,174 @@
+// 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.
+
+//! Supported cipher suites
+
+use alloc::{boxed::Box, vec::Vec};
+use core::marker::PhantomData;
+
+use bssl_crypto::{
+    digest::{self, Algorithm},
+    hkdf, hmac,
+    tls12_prf::Tls12Prf,
+};
+use rustls::{
+    Error,
+    crypto::{
+        hmac::Tag,
+        tls12::Prf,
+        tls13::{Hkdf, HkdfExpander, OkmBlock, OutputLengthError},
+    },
+    version::TLS12,
+};
+
+pub(crate) struct Tls12PrfImpl<A>(PhantomData<fn() -> A>);
+
+impl<A> Tls12PrfImpl<A> {
+    pub(crate) const fn new() -> Self {
+        Self(PhantomData)
+    }
+}
+
+impl<A: digest::Algorithm> Prf for Tls12PrfImpl<A> {
+    fn for_key_exchange(
+        &self,
+        output: &mut [u8; 48],
+        kx: Box<dyn rustls::crypto::ActiveKeyExchange>,
+        peer_pub_key: &[u8],
+        label: &[u8],
+        seed: &[u8],
+    ) -> Result<(), Error> {
+        Tls12Prf::<A>::generate_secret(
+            kx.complete_for_tls_version(peer_pub_key, &TLS12)?
+                .secret_bytes(),
+            label,
+            seed,
+            None,
+            output,
+        )
+        .map_err(|_| Error::General("PRF failed".into()))
+    }
+
+    fn for_secret(&self, output: &mut [u8], secret: &[u8], label: &[u8], seed: &[u8]) {
+        Tls12Prf::<A>::generate_secret(secret, label, seed, None, output)
+            .expect("for_secret should be infallible");
+    }
+}
+
+/// TLS 1.3 HKDF over SHA-256
+pub(crate) const TLS13_HKDF_SHA256: &'static dyn Hkdf = &Tls13HkdfImpl::<digest::Sha256>::new();
+/// TLS 1.3 HKDF over SHA-384
+pub(crate) const TLS13_HKDF_SHA384: &'static dyn Hkdf = &Tls13HkdfImpl::<digest::Sha384>::new();
+
+pub(crate) struct Tls13HkdfImpl<A>(PhantomData<fn() -> A>);
+
+impl<A> Tls13HkdfImpl<A> {
+    pub(crate) const fn new() -> Self {
+        Self(PhantomData)
+    }
+}
+
+struct Tls13HkdfExpander<A> {
+    prk: hkdf::Prk,
+    _p: PhantomData<fn() -> A>,
+}
+
+impl<A: Tls13HkdfDigest> HkdfExpander for Tls13HkdfExpander<A> {
+    fn expand_slice(&self, info: &[&[u8]], output: &mut [u8]) -> Result<(), OutputLengthError> {
+        let info: Vec<_> = info.iter().copied().flatten().copied().collect();
+        self.prk
+            .expand_into(&info, output)
+            .map_err(|_| OutputLengthError)
+    }
+
+    fn expand_block(&self, info: &[&[u8]]) -> OkmBlock {
+        let mut buf = A::zero_secret().to_vec();
+        let info: Vec<_> = info.iter().copied().flatten().copied().collect();
+        self.prk
+            .expand_into(&info, &mut buf)
+            .expect("digest length should not be too long");
+        OkmBlock::new(&buf)
+    }
+
+    fn hash_len(&self) -> usize {
+        A::OUTPUT_LEN
+    }
+}
+
+trait Tls13HkdfDigest: digest::Algorithm {
+    fn hmac_sign(key: &[u8], data: &[u8]) -> Tag;
+    fn zero_secret() -> &'static [u8];
+}
+
+impl Tls13HkdfDigest for digest::Sha256 {
+    fn hmac_sign(key: &[u8], data: &[u8]) -> Tag {
+        Tag::new(&hmac::HmacSha256::mac(key, data))
+    }
+    fn zero_secret() -> &'static [u8] {
+        &[0; Self::OUTPUT_LEN]
+    }
+}
+
+impl Tls13HkdfDigest for digest::Sha384 {
+    fn hmac_sign(key: &[u8], data: &[u8]) -> Tag {
+        Tag::new(&hmac::HmacSha384::mac(key, data))
+    }
+    fn zero_secret() -> &'static [u8] {
+        &[0; Self::OUTPUT_LEN]
+    }
+}
+
+impl<A: 'static + Tls13HkdfDigest> Hkdf for Tls13HkdfImpl<A> {
+    fn extract_from_zero_ikm(&self, salt: Option<&[u8]>) -> Box<dyn HkdfExpander> {
+        let prk = hkdf::Hkdf::<A>::extract(
+            A::zero_secret(),
+            if let Some(salt) = salt {
+                hkdf::Salt::NonEmpty(salt)
+            } else {
+                hkdf::Salt::None
+            },
+        );
+        Box::new(Tls13HkdfExpander::<A> {
+            prk,
+            _p: PhantomData,
+        })
+    }
+
+    fn extract_from_secret(&self, salt: Option<&[u8]>, secret: &[u8]) -> Box<dyn HkdfExpander> {
+        let prk = hkdf::Hkdf::<A>::extract(
+            secret,
+            if let Some(salt) = salt {
+                hkdf::Salt::NonEmpty(salt)
+            } else {
+                hkdf::Salt::None
+            },
+        );
+        Box::new(Tls13HkdfExpander::<A> {
+            prk,
+            _p: PhantomData,
+        })
+    }
+
+    fn expander_for_okm(&self, okm: &OkmBlock) -> Box<dyn HkdfExpander> {
+        let okm = okm.as_ref();
+        Box::new(Tls13HkdfExpander::<A> {
+            prk: hkdf::Prk::new::<A>(okm).expect("OKM size mismatch"),
+            _p: PhantomData,
+        })
+    }
+
+    fn hmac_sign(&self, key: &OkmBlock, message: &[u8]) -> Tag {
+        A::hmac_sign(key.as_ref(), message)
+    }
+}
diff --git a/rust/bssl-tls/src/rustls_provider/sign.rs b/rust/bssl-tls/src/rustls_provider/sign.rs
new file mode 100644
index 0000000..9e48356
--- /dev/null
+++ b/rust/bssl-tls/src/rustls_provider/sign.rs
@@ -0,0 +1,269 @@
+// 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 alloc::{boxed::Box, vec::Vec};
+use core::{
+    fmt::{Debug, Formatter, Result as FmtResult},
+    marker::PhantomData,
+};
+
+use bssl_crypto::{digest, ec, ecdsa, ed25519, rsa};
+use rustls::{
+    Error, SignatureAlgorithm, SignatureScheme,
+    pki_types::{PrivatePkcs1KeyDer, PrivateSec1KeyDer},
+    sign::{Signer, SigningKey},
+};
+
+use crate::rustls_provider::{RsaSignatureDigest, pki};
+
+/// A generic `id-rsaEncryption` RSA key
+///
+/// This key type is only intended for signature
+pub(crate) struct RsaPrivateKey(pub rsa::PrivateKey);
+
+impl TryFrom<PrivatePkcs1KeyDer<'_>> for RsaPrivateKey {
+    type Error = Error;
+    fn try_from(der: PrivatePkcs1KeyDer) -> Result<Self, Self::Error> {
+        let der = der.secret_pkcs1_der();
+        let key = rsa::PrivateKey::from_der_rsa_private_key(der)
+            .ok_or(Error::General("Cannot parse PKCS#1 key from DER".into()))?;
+        Ok(Self(key))
+    }
+}
+
+impl Debug for RsaPrivateKey {
+    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
+        f.debug_tuple("RsaPrivateKey").finish()
+    }
+}
+
+struct RsaPkcs1Signer<D> {
+    key: rsa::PrivateKey,
+    _p: PhantomData<fn() -> D>,
+}
+
+impl<D> Debug for RsaPkcs1Signer<D> {
+    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
+        f.debug_struct("RsaPkcs1Signer").finish()
+    }
+}
+
+impl<D: RsaSignatureDigest> RsaPkcs1Signer<D> {
+    fn new(key: rsa::PrivateKey) -> Self {
+        Self {
+            key,
+            _p: PhantomData,
+        }
+    }
+}
+
+impl<D: RsaSignatureDigest> Signer for RsaPkcs1Signer<D> {
+    fn sign(&self, message: &[u8]) -> Result<Vec<u8>, Error> {
+        Ok(self.key.sign_pkcs1::<D>(message))
+    }
+
+    fn scheme(&self) -> SignatureScheme {
+        match D::ALGORITHM {
+            pki::DigestAlgorithm::Sha256 => SignatureScheme::RSA_PKCS1_SHA256,
+            pki::DigestAlgorithm::Sha384 => SignatureScheme::RSA_PKCS1_SHA384,
+            pki::DigestAlgorithm::Sha512 => SignatureScheme::RSA_PKCS1_SHA512,
+        }
+    }
+}
+
+struct RsaPssSigner<D> {
+    key: rsa::PrivateKey,
+    _p: PhantomData<fn() -> D>,
+}
+
+impl<D> Debug for RsaPssSigner<D> {
+    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
+        f.debug_struct("RsaPssSigner").finish()
+    }
+}
+
+impl<D: RsaSignatureDigest> RsaPssSigner<D> {
+    fn new(key: rsa::PrivateKey) -> Self {
+        Self {
+            key,
+            _p: PhantomData,
+        }
+    }
+}
+
+impl<D: RsaSignatureDigest> Signer for RsaPssSigner<D> {
+    fn sign(&self, message: &[u8]) -> Result<Vec<u8>, Error> {
+        Ok(self.key.sign_pss::<D>(message))
+    }
+
+    fn scheme(&self) -> SignatureScheme {
+        match D::ALGORITHM {
+            pki::DigestAlgorithm::Sha256 => SignatureScheme::RSA_PSS_SHA256,
+            pki::DigestAlgorithm::Sha384 => SignatureScheme::RSA_PSS_SHA384,
+            pki::DigestAlgorithm::Sha512 => SignatureScheme::RSA_PSS_SHA512,
+        }
+    }
+}
+
+impl SigningKey for RsaPrivateKey {
+    fn choose_scheme(&self, offered: &[SignatureScheme]) -> Option<Box<dyn Signer>> {
+        for offer in offered {
+            match offer {
+                SignatureScheme::RSA_PKCS1_SHA1 => continue,
+                SignatureScheme::RSA_PKCS1_SHA256 => {
+                    return Some(Box::new(RsaPkcs1Signer::<digest::Sha256>::new(
+                        self.0.clone(),
+                    )));
+                }
+                SignatureScheme::RSA_PKCS1_SHA384 => {
+                    return Some(Box::new(RsaPkcs1Signer::<digest::Sha384>::new(
+                        self.0.clone(),
+                    )));
+                }
+                SignatureScheme::RSA_PKCS1_SHA512 => {
+                    return Some(Box::new(RsaPkcs1Signer::<digest::Sha512>::new(
+                        self.0.clone(),
+                    )));
+                }
+                SignatureScheme::RSA_PSS_SHA256 => {
+                    return Some(Box::new(RsaPssSigner::<digest::Sha256>::new(
+                        self.0.clone(),
+                    )));
+                }
+                SignatureScheme::RSA_PSS_SHA384 => {
+                    return Some(Box::new(RsaPssSigner::<digest::Sha384>::new(
+                        self.0.clone(),
+                    )));
+                }
+                SignatureScheme::RSA_PSS_SHA512 => {
+                    return Some(Box::new(RsaPssSigner::<digest::Sha512>::new(
+                        self.0.clone(),
+                    )));
+                }
+                _ => continue,
+            }
+        }
+        None
+    }
+
+    fn algorithm(&self) -> SignatureAlgorithm {
+        SignatureAlgorithm::RSA
+    }
+}
+
+/// An ECDSA signing key
+#[derive(Clone)]
+pub(crate) struct EcdsaPrivateKey<C: ec::Curve>(pub ecdsa::PrivateKey<C>);
+
+impl<C: ec::Curve> Debug for EcdsaPrivateKey<C> {
+    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
+        f.debug_tuple("EcdsaPrivateKey").finish()
+    }
+}
+
+impl<C: ec::Curve> TryFrom<&'_ PrivateSec1KeyDer<'_>> for EcdsaPrivateKey<C> {
+    type Error = Error;
+
+    fn try_from(der: &PrivateSec1KeyDer) -> Result<Self, Self::Error> {
+        let der = der.secret_sec1_der();
+        let key = ecdsa::PrivateKey::from_der_ec_private_key(der)
+            .ok_or(Error::General("Cannot parse Sec1 key from DER".into()))?;
+        Ok(Self(key))
+    }
+}
+
+macro_rules! ecdsa_signer {
+    ($scheme:path, $curve:path) => {
+        impl SigningKey for EcdsaPrivateKey<$curve> {
+            fn choose_scheme(&self, offered: &[SignatureScheme]) -> Option<Box<dyn Signer>> {
+                for offer in offered {
+                    match offer {
+                        // We do not support any SHA1 scheme
+                        SignatureScheme::ECDSA_SHA1_Legacy => continue,
+                        $scheme => return Some(Box::new(EcdsaSigner(self.0.clone()))),
+                        _ => continue,
+                    }
+                }
+                None
+            }
+
+            fn algorithm(&self) -> SignatureAlgorithm {
+                SignatureAlgorithm::ECDSA
+            }
+        }
+
+        impl Signer for EcdsaSigner<$curve> {
+            fn sign(&self, message: &[u8]) -> Result<Vec<u8>, Error> {
+                Ok(self.0.sign(message))
+            }
+
+            fn scheme(&self) -> SignatureScheme {
+                $scheme
+            }
+        }
+    };
+}
+
+ecdsa_signer!(SignatureScheme::ECDSA_NISTP256_SHA256, ec::P256);
+ecdsa_signer!(SignatureScheme::ECDSA_NISTP384_SHA384, ec::P384);
+
+struct EcdsaSigner<C: ec::Curve>(ecdsa::PrivateKey<C>);
+
+impl<C: ec::Curve> Debug for EcdsaSigner<C> {
+    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
+        f.debug_tuple("EcdsaSigner").finish()
+    }
+}
+
+#[derive(Clone)]
+pub(crate) struct EddsaPrivateKey(pub ed25519::PrivateKey);
+
+impl Debug for EddsaPrivateKey {
+    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
+        f.debug_tuple("EddsaPrivateKey").finish()
+    }
+}
+
+impl SigningKey for EddsaPrivateKey {
+    fn choose_scheme(&self, offered: &[SignatureScheme]) -> Option<Box<dyn Signer>> {
+        for offer in offered {
+            if matches!(offer, SignatureScheme::ED25519) {
+                return Some(Box::new(EddsaSigner(self.0.clone())));
+            }
+        }
+        None
+    }
+
+    fn algorithm(&self) -> SignatureAlgorithm {
+        SignatureAlgorithm::ED25519
+    }
+}
+
+struct EddsaSigner(ed25519::PrivateKey);
+
+impl Debug for EddsaSigner {
+    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
+        f.debug_tuple("EddsaSigner").finish()
+    }
+}
+
+impl Signer for EddsaSigner {
+    fn sign(&self, message: &[u8]) -> Result<Vec<u8>, Error> {
+        Ok(self.0.sign(message).to_vec())
+    }
+
+    fn scheme(&self) -> SignatureScheme {
+        SignatureScheme::ED25519
+    }
+}
diff --git a/rust/bssl-tls/src/rustls_provider/tests.rs b/rust/bssl-tls/src/rustls_provider/tests.rs
new file mode 100644
index 0000000..88d523f
--- /dev/null
+++ b/rust/bssl-tls/src/rustls_provider/tests.rs
@@ -0,0 +1,442 @@
+// 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::io::{PipeReader, PipeWriter, Read, Write, pipe};
+use std::sync::Arc;
+
+use super::*;
+
+use rustls::crypto::ring;
+use rustls::{
+    RootCertStore, ServerConfig, ServerConnection, Stream, SupportedProtocolVersion,
+    client::{ClientConfig, ClientConnection},
+    pki_types::{CertificateDer, ServerName, pem::PemObject},
+    version::{TLS12, TLS13},
+};
+use tracing::{Level, debug};
+
+// ===================================================================================
+// All CSRs `server.csr` is generated from
+// openssl req -new -nodes -key <input key> -out server.csr -subj '/CN=www.google.com'
+// ===================================================================================
+
+const CA_CERT: &'static [u8] = include_bytes!("./tests/BoringSSLCATest.crt");
+
+const RSA_SVC_CERT: &'static [u8] = include_bytes!("./tests/BoringSSLServerTest-RSA.crt");
+
+const RSA_SVC_KEY: &'static [u8] = include_bytes!("./tests/BoringSSLServerTest-RSA.key");
+
+const ECDSA_P256_SVC_CERT: &'static [u8] =
+    include_bytes!("./tests/BoringSSLServerTest-ECDSA-P256.crt");
+
+const ECDSA_P256_SVC_KEY: &'static [u8] =
+    include_bytes!("./tests/BoringSSLServerTest-ECDSA-P256.key");
+
+const RSA_PSS_SVC_CERT: &'static [u8] =
+    include_bytes!("./tests/BoringSSLServerTest-RSA-PSS-SHA256.crt");
+
+struct PipeSocket {
+    tx: PipeWriter,
+    rx: PipeReader,
+}
+impl Read for PipeSocket {
+    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
+        self.rx.read(buf)
+    }
+}
+impl Write for PipeSocket {
+    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
+        self.tx.write(buf)
+    }
+
+    fn flush(&mut self) -> std::io::Result<()> {
+        self.tx.flush()
+    }
+}
+
+const EXPECTED_SERVER_MSG: &[u8; 18] = b"Awesome BoringSSL!";
+const EXPECTED_CLIENT_MSG: &[u8; 19] = b"Oh yeah definitely!";
+
+fn init_tracing() {
+    let _ = tracing_subscriber::fmt()
+        .with_max_level(Level::DEBUG)
+        .try_init();
+}
+
+fn test_cipher_suite(
+    ca: &[u8],
+    svc_cert: &[u8],
+    svc_key: &[u8],
+    client_protocols: &[&'static SupportedProtocolVersion],
+    server_crypto_provider: CryptoProvider,
+    client_crypto_provider: CryptoProvider,
+) {
+    init_tracing();
+    let ca_cert = CertificateDer::from_pem_slice(ca).unwrap();
+    let svc_cert = CertificateDer::from_pem_slice(svc_cert).unwrap();
+    let svc_key = PrivateKeyDer::from_pem_slice(svc_key).unwrap();
+    let mut root_ca = RootCertStore::empty();
+    assert_eq!(root_ca.add_parsable_certificates([ca_cert.clone()]), (1, 0));
+    let client_conf = ClientConfig::builder_with_provider(Arc::new(client_crypto_provider))
+        .with_protocol_versions(client_protocols)
+        .unwrap()
+        .with_root_certificates(Arc::new(root_ca))
+        .with_no_client_auth();
+    let server_conf = ServerConfig::builder_with_provider(Arc::new(server_crypto_provider))
+        .with_protocol_versions(&[&TLS12, &TLS13])
+        .unwrap()
+        .with_no_client_auth()
+        .with_single_cert(vec![svc_cert, ca_cert], svc_key)
+        .unwrap();
+    let (server_rx, server_tx) = pipe().unwrap();
+    let (client_rx, client_tx) = pipe().unwrap();
+    let mut server_sock = PipeSocket {
+        tx: client_tx,
+        rx: server_rx,
+    };
+    let mut client_sock = PipeSocket {
+        tx: server_tx,
+        rx: client_rx,
+    };
+    let mut client_conn = ClientConnection::new(
+        Arc::new(client_conf),
+        ServerName::try_from("www.google.com").unwrap(),
+    )
+    .unwrap();
+    let client_thread = std::thread::spawn(move || {
+        let mut client_stream = Stream::new(&mut client_conn, &mut client_sock);
+        let mut buf = [0; EXPECTED_SERVER_MSG.len()];
+        client_stream.read_exact(&mut buf).unwrap();
+        assert_eq!(&buf, EXPECTED_SERVER_MSG);
+        client_stream.write_all(EXPECTED_CLIENT_MSG).unwrap();
+    });
+
+    let mut server_conn = ServerConnection::new(Arc::new(server_conf)).unwrap();
+    let mut server_stream = Stream::new(&mut server_conn, &mut server_sock);
+    server_stream.write_all(EXPECTED_SERVER_MSG).unwrap();
+    debug!("scheduled server message, polling client message");
+    let mut buf = [0; EXPECTED_CLIENT_MSG.len()];
+    server_stream.read_exact(&mut buf).unwrap();
+    assert_eq!(&buf, EXPECTED_CLIENT_MSG);
+    debug!("received client message");
+    server_stream.conn.send_close_notify();
+    if server_stream.conn.write_tls(server_stream.sock).is_err() {
+        return;
+    }
+    let _ = server_stream;
+    let _ = server_conn;
+    let _ = server_sock;
+    client_thread.join().unwrap();
+}
+
+#[test]
+fn all_key_agreement_algorithms() {
+    for group in [
+        super::key_exchange::ECDH_P256,
+        super::key_exchange::ECDH_P384,
+        super::key_exchange::X25519,
+        #[cfg(feature = "mlalgs")]
+        super::key_exchange::X25519MLKEM768,
+    ] {
+        test_cipher_suite(
+            CA_CERT,
+            RSA_SVC_CERT,
+            RSA_SVC_KEY,
+            &[&TLS13],
+            CryptoProviderBuilder::new()
+                .with_key_exchange_group(group)
+                .with_cipher_suite(SupportedCipherSuite::Tls13(
+                    cipher_suites::TLS13_AES_256_GCM_SHA384,
+                ))
+                .build(),
+            CryptoProviderBuilder::new()
+                .with_key_exchange_group(group)
+                .with_full_cipher_suites()
+                .build(),
+        );
+    }
+    for group in [
+        super::key_exchange::ECDH_P256,
+        super::key_exchange::ECDH_P384,
+        super::key_exchange::X25519,
+    ] {
+        test_cipher_suite(
+            CA_CERT,
+            RSA_SVC_CERT,
+            RSA_SVC_KEY,
+            &[&TLS13],
+            CryptoProviderBuilder::new()
+                .with_key_exchange_group(group)
+                .with_cipher_suite(SupportedCipherSuite::Tls13(
+                    cipher_suites::TLS13_AES_256_GCM_SHA384,
+                ))
+                .build(),
+            ring::default_provider(),
+        );
+    }
+}
+
+fn test_half_connection(
+    providers: &[fn() -> CryptoProvider],
+    test_provider: fn() -> CryptoProvider,
+    run_test_suite: impl Fn(CryptoProvider, CryptoProvider),
+) {
+    for provider in providers {
+        run_test_suite(provider(), test_provider());
+        run_test_suite(test_provider(), provider());
+    }
+}
+
+#[test]
+fn tls12_ecdhe_rsa_aes_128_gcm() {
+    let providers = [CryptoProviderBuilder::full, ring::default_provider];
+    let test_provider = || {
+        CryptoProviderBuilder::new()
+            .with_default_key_exchange_groups()
+            .with_cipher_suite(SupportedCipherSuite::Tls12(
+                cipher_suites::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
+            ))
+            .build()
+    };
+    test_half_connection(
+        &providers,
+        test_provider,
+        |client_provider, server_provider| {
+            test_cipher_suite(
+                CA_CERT,
+                RSA_SVC_CERT,
+                RSA_SVC_KEY,
+                &[&TLS12],
+                server_provider,
+                client_provider,
+            );
+        },
+    );
+}
+
+#[test]
+fn tls12_ecdhe_rsa_pss_aes_128_gcm() {
+    let providers = [CryptoProviderBuilder::full, ring::default_provider];
+    let test_provider = || {
+        CryptoProviderBuilder::new()
+            .with_default_key_exchange_groups()
+            .with_cipher_suite(SupportedCipherSuite::Tls12(
+                cipher_suites::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
+            ))
+            .build()
+    };
+    test_half_connection(
+        &providers,
+        test_provider,
+        |client_provider, server_provider| {
+            test_cipher_suite(
+                CA_CERT,
+                RSA_PSS_SVC_CERT,
+                RSA_SVC_KEY,
+                &[&TLS12],
+                server_provider,
+                client_provider,
+            )
+        },
+    );
+}
+
+#[test]
+fn tls12_ecdhe_rsa_with_aes_256_gcm_sha384() {
+    let providers = [CryptoProviderBuilder::full, ring::default_provider];
+    let test_provider = || {
+        CryptoProviderBuilder::new()
+            .with_default_key_exchange_groups()
+            .with_cipher_suite(SupportedCipherSuite::Tls12(
+                cipher_suites::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
+            ))
+            .build()
+    };
+    test_half_connection(
+        &providers,
+        test_provider,
+        |client_provider, server_provider| {
+            test_cipher_suite(
+                CA_CERT,
+                RSA_SVC_CERT,
+                RSA_SVC_KEY,
+                &[&TLS12],
+                server_provider,
+                client_provider,
+            )
+        },
+    );
+}
+
+#[test]
+fn tls12_ecdhe_ecdsa_with_chacha20_poly1305_sha256() {
+    let providers = [CryptoProviderBuilder::full, ring::default_provider];
+    let test_provider = || {
+        CryptoProviderBuilder::new()
+            .with_default_key_exchange_groups()
+            .with_cipher_suite(SupportedCipherSuite::Tls12(
+                cipher_suites::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
+            ))
+            .build()
+    };
+    test_half_connection(
+        &providers,
+        test_provider,
+        |client_provider, server_provider| {
+            test_cipher_suite(
+                CA_CERT,
+                ECDSA_P256_SVC_CERT,
+                ECDSA_P256_SVC_KEY,
+                &[&TLS12],
+                server_provider,
+                client_provider,
+            )
+        },
+    );
+}
+
+#[test]
+fn tls12_ecdhe_ecdsa_with_aes_128_gcm_sha256() {
+    let providers = [CryptoProviderBuilder::full, ring::default_provider];
+    let test_provider = || {
+        CryptoProviderBuilder::new()
+            .with_default_key_exchange_groups()
+            .with_cipher_suite(SupportedCipherSuite::Tls12(
+                cipher_suites::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
+            ))
+            .build()
+    };
+    test_half_connection(
+        &providers,
+        test_provider,
+        |client_provider, server_provider| {
+            test_cipher_suite(
+                CA_CERT,
+                ECDSA_P256_SVC_CERT,
+                ECDSA_P256_SVC_KEY,
+                &[&TLS12],
+                server_provider,
+                client_provider,
+            )
+        },
+    );
+}
+
+#[test]
+fn tls12_ecdhe_ecdsa_with_aes_256_gcm_sha384() {
+    let providers = [CryptoProviderBuilder::full, ring::default_provider];
+    let test_provider = || {
+        CryptoProviderBuilder::new()
+            .with_default_key_exchange_groups()
+            .with_cipher_suite(SupportedCipherSuite::Tls12(
+                cipher_suites::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
+            ))
+            .build()
+    };
+    test_half_connection(
+        &providers,
+        test_provider,
+        |client_provider, server_provider| {
+            test_cipher_suite(
+                CA_CERT,
+                ECDSA_P256_SVC_CERT,
+                ECDSA_P256_SVC_KEY,
+                &[&TLS12],
+                server_provider,
+                client_provider,
+            )
+        },
+    );
+}
+
+#[test]
+fn tls13_aes_128_gcm_sha256() {
+    let providers = [CryptoProviderBuilder::full, ring::default_provider];
+    let test_provider = || {
+        CryptoProviderBuilder::new()
+            .with_default_key_exchange_groups()
+            .with_cipher_suite(SupportedCipherSuite::Tls13(
+                cipher_suites::TLS13_AES_128_GCM_SHA256,
+            ))
+            .build()
+    };
+    test_half_connection(
+        &providers,
+        test_provider,
+        |client_provider, server_provider| {
+            test_cipher_suite(
+                CA_CERT,
+                ECDSA_P256_SVC_CERT,
+                ECDSA_P256_SVC_KEY,
+                &[&TLS13],
+                server_provider,
+                client_provider,
+            )
+        },
+    );
+}
+
+#[test]
+fn tls13_aes_256_gcm_sha384() {
+    let providers = [CryptoProviderBuilder::full, ring::default_provider];
+    let test_provider = || {
+        CryptoProviderBuilder::new()
+            .with_default_key_exchange_groups()
+            .with_cipher_suite(SupportedCipherSuite::Tls13(
+                cipher_suites::TLS13_AES_256_GCM_SHA384,
+            ))
+            .build()
+    };
+    test_half_connection(
+        &providers,
+        test_provider,
+        |client_provider, server_provider| {
+            test_cipher_suite(
+                CA_CERT,
+                ECDSA_P256_SVC_CERT,
+                ECDSA_P256_SVC_KEY,
+                &[&TLS13],
+                server_provider,
+                client_provider,
+            )
+        },
+    );
+}
+
+#[test]
+fn tls13_chacha20_poly1305_sha256() {
+    let providers = [CryptoProviderBuilder::full, ring::default_provider];
+    let test_provider = || {
+        CryptoProviderBuilder::new()
+            .with_default_key_exchange_groups()
+            .with_cipher_suite(SupportedCipherSuite::Tls13(
+                cipher_suites::TLS13_CHACHA20_POLY1305_SHA256,
+            ))
+            .build()
+    };
+    test_half_connection(
+        &providers,
+        test_provider,
+        |client_provider, server_provider| {
+            test_cipher_suite(
+                CA_CERT,
+                ECDSA_P256_SVC_CERT,
+                ECDSA_P256_SVC_KEY,
+                &[&TLS13],
+                server_provider,
+                client_provider,
+            )
+        },
+    );
+}
diff --git a/rust/bssl-tls/src/rustls_provider/tests/BoringSSLCATest.crt b/rust/bssl-tls/src/rustls_provider/tests/BoringSSLCATest.crt
new file mode 100644
index 0000000..e5d28ce
--- /dev/null
+++ b/rust/bssl-tls/src/rustls_provider/tests/BoringSSLCATest.crt
@@ -0,0 +1,36 @@
+-----BEGIN CERTIFICATE-----
+MIIGUTCCBDmgAwIBAgIUHlETnZyBkDglMnJR8zMbcNCbalwwDQYJKoZIhvcNAQEL
+BQAwga8xCzAJBgNVBAYTAkRFMRkwFwYDVQQIDBBGcmVpc3RhYXQgQmF5ZXJuMREw
+DwYDVQQHDAhNdWVuY2hlbjEcMBoGA1UECgwTR29vZ2xlIEdlcm1hbnkgR21iSDET
+MBEGA1UECwwKSVNFIENyeXB0bzEaMBgGA1UEAwwRQm9yaW5nU1NMIEF1dGhvcnMx
+IzAhBgkqhkiG9w0BCQEWFGJvcmluZ3NzbEBnb29nbGUuY29tMB4XDTI2MDEyMjA5
+NTM0MVoXDTI2MDIyMTA5NTM0MVowga8xCzAJBgNVBAYTAkRFMRkwFwYDVQQIDBBG
+cmVpc3RhYXQgQmF5ZXJuMREwDwYDVQQHDAhNdWVuY2hlbjEcMBoGA1UECgwTR29v
+Z2xlIEdlcm1hbnkgR21iSDETMBEGA1UECwwKSVNFIENyeXB0bzEaMBgGA1UEAwwR
+Qm9yaW5nU1NMIEF1dGhvcnMxIzAhBgkqhkiG9w0BCQEWFGJvcmluZ3NzbEBnb29n
+bGUuY29tMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA2WxlWVEy+qVu
+q5Oj90XJdZdUC25f+28v1+1bvFcbqI6qvgNcOHhMkixfHLI5pKRTnbnX3XoAr01N
+k2pbBzwQv7wCPoW5qZY/UrrFBhWsrGZUJlV33IYRj6ace0nxFvc6YibU/qlpbCJ5
+wpJiLWweY14HQYo5J0E0wJlXIswwjJXnXD3yJulNKZ7JaTc9R/VeaTxDTrbTUuGX
+dw4aaS7qiv5QaBnyOOsFWFaKOSKDPK7jh0it/DBWQ11sdeTi/c0f06eBUh1/3TlX
+Ax/V7b7kI5QjbXC214TsgH3s9MpE4qdh6xZf3aJObZmPMJy3dXTag0FCrlZEIXtx
+z32otTKPkbMwofTqEM8+DTc8/qP68iZopRZAvrnnruDh62xlwgi91zJ+p1wJ89VA
+3sidxP0F4GoSk2A1tqR6a6c1oIaygPZRbEmhtCYfonsg/xT+NvEaPis8zz6fByTT
++q22iKY/mKEZAevke+nss1hWObvzcFjWh06DGt9Ir5b7cD5zk9+Eq21Uuwtt3WXS
+ml+i7bO0e1TuSf39RFQ6LdawyWKJ4u5x4WBzOt0cBfv1l52PgpusQjJ3Qtlgd1ks
+Ck7Nts3F4V3SADHI0qdXRmkab7ar85LiQjgvx1+zA9AEcAV/X21MGGmQlaicAkSs
+loj1/E2BSt7jXh5XwS26jvMrOa6rjQUCAwEAAaNjMGEwHQYDVR0OBBYEFOobWR5d
+4zMpQX/XWIhj1CooBngQMB8GA1UdIwQYMBaAFOobWR5d4zMpQX/XWIhj1CooBngQ
+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUA
+A4ICAQDYcmO6yY0mGBLQq1Hjasopg6/QsUKuNtYjktPH4/Qa2RYU2j+Xmv82X4Pt
++Njyj3oqc3qNeluRzrgX6EIp9BMe5S4/vw773iBpw0aeZ2Ym3FKOyFnY9R2t1ASs
+4CZxLqaGspc3RMyw++ZyIIMSepgH86YnuqIY/0CtyjO+pSPWLw7RddOhb4u7qX7g
+/RjhRDWEVlVyhnI1vOVQd2no8rbMPra/5FusF8SE/+jVvvR6AN5Zjipl2nFYTtAm
+gPBLhTDNw4wCL7Tcgsf38AJLZ13YoddNZJNrpOxc2VK9KLhqAqa8KXT1vU2iNqZX
+lUIv7h9dY2rSCqL3BEpsygoXOp6a/nIFWFmEd2Ggc40rWsfVmI+n3r1fJc/0AXx0
+DgRJHgorby9/5+PHzndDcxA98KT8CjshHeAOPk064CMKWepbFQfRLOQiTdoBXdKU
+iQt6PJ81vyw2tU2OfIhgDXRdtPDMJcfHk6T6JvpuE/YDWG+Xfs3jxM2qrmReKstw
+S98TPisHO9joqJHr+qgJYFyz2tMQbiOO15U0ARY8CPzxo0EbOkWiMpVYBfWzEgBg
+bkmlo8okXeuo6HNVZBwT9briumwMFWZVQGxV5k5gJ5FFI3zmju31zuQJnjJF2GMh
+6DqfkaVSwife/zUxPznM2MYOjL5YK/R3BqzX+3ZYFTAo356U4g==
+-----END CERTIFICATE-----
diff --git a/rust/bssl-tls/src/rustls_provider/tests/BoringSSLCATest.key b/rust/bssl-tls/src/rustls_provider/tests/BoringSSLCATest.key
new file mode 100644
index 0000000..7d037bb
--- /dev/null
+++ b/rust/bssl-tls/src/rustls_provider/tests/BoringSSLCATest.key
@@ -0,0 +1,52 @@
+-----BEGIN PRIVATE KEY-----
+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
+RxOVo4yLyYfsyBQUIsTZE1pciQfsXNM=
+-----END PRIVATE KEY-----
diff --git a/rust/bssl-tls/src/rustls_provider/tests/BoringSSLServerTest-ECDSA-P256.crt b/rust/bssl-tls/src/rustls_provider/tests/BoringSSLServerTest-ECDSA-P256.crt
new file mode 100644
index 0000000..b0282a5
--- /dev/null
+++ b/rust/bssl-tls/src/rustls_provider/tests/BoringSSLServerTest-ECDSA-P256.crt
@@ -0,0 +1,29 @@
+This is generated from
+    openssl x509 -req -in server.csr -CA BoringSSLCATest.crt \
+      -CAkey BoringSSLCATest.key -CAcreateserial \
+      -out BoringSSLServerTest-ECDSA-P256.crt -extfile svcconfig -days 730500
+
+-----BEGIN CERTIFICATE-----
+MIIEDzCCAfegAwIBAgIUUC0yUXu5UfDfbk7DBR8sS6/NUnQwDQYJKoZIhvcNAQEL
+BQAwga8xCzAJBgNVBAYTAkRFMRkwFwYDVQQIDBBGcmVpc3RhYXQgQmF5ZXJuMREw
+DwYDVQQHDAhNdWVuY2hlbjEcMBoGA1UECgwTR29vZ2xlIEdlcm1hbnkgR21iSDET
+MBEGA1UECwwKSVNFIENyeXB0bzEaMBgGA1UEAwwRQm9yaW5nU1NMIEF1dGhvcnMx
+IzAhBgkqhkiG9w0BCQEWFGJvcmluZ3NzbEBnb29nbGUuY29tMCAXDTI2MDEyMzEz
+NDA1MloYDzQwMjYwMjA3MTM0MDUyWjAZMRcwFQYDVQQDDA53d3cuZ29vZ2xlLmNv
+bTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABOvR/5V1SgsI/QDvkZjUbx73PjgD
+q3I7ovMEcZ/rpY0YFk9yyzkcKZgOcXIjPjxpreiEj3DjgOe2TfIjsOxdn3+jgYAw
+fjAfBgNVHSMEGDAWgBTqG1keXeMzKUF/11iIY9QqKAZ4EDAJBgNVHRMEAjAAMAsG
+A1UdDwQEAwIE8DAkBgNVHREEHTAbgg53d3cuZ29vZ2xlLmNvbYIJbG9jYWxob3N0
+MB0GA1UdDgQWBBTx2F/oNm4Eo+BUXY8sROyeuWchjDANBgkqhkiG9w0BAQsFAAOC
+AgEAq2anPF2MuHAJOP96bGGAhDFMBoq2URD5GY6GlGziKJtbW7GbrmNrA6tjXzLs
+/2uCaOnxu0MC3sWvAmeEonhoaANGBU6mNQkjmzSyxiwXWtdxaY/fQYQXOkGmNPou
+myonrElqi67ddy7sh2rRsWkfs4WWkWhELej8XGgpTsby+PtvyQTRtjvaTnEOppgk
+ySLN20b96X0smE6ENUfKqu1k5f6WQmIfC3TPh8dWlAbgpak7dG5AKYGxGQAyK+7R
+WoBf1ex7IkynuPLIDq9XxMHBFiq8tcx0fn9GGXJyDcxUgAYu75iSOU71f9gZgY1l
+kYhLYvjuSREoF6yhbVy9DPfzWy33xqIonqAOV2wSDVFpANES5Y9ojKF2eNAN5MVO
+abC+MCHovcShD3/l7kfoC2KHzJjXtaEgqQUShEbRuJ/ooPvBTX3fpj6QRCOL12Ly
+kIlMZLlW0a8GVQV3P5is5jF44TgCw+mTIs8A7B7wb7K27nY0XzPbg7ztKM/rmPol
+Muh1g7OekKBje6npVY1mZ6lwVKWSpOwKp3lMe2KEwAEny+Zdl9oAvFLF728qVk0u
+ND/h3LAERnb2zKEeobdoMYJurBLMWNrvnyvvCiK+MuBAnMowZS/wnC0T48eUJF+e
+60ddB3A1SF1HP1bY/VAfVVMEwWXJKUaD0HeGcIq8BIoZ160=
+-----END CERTIFICATE-----
diff --git a/rust/bssl-tls/src/rustls_provider/tests/BoringSSLServerTest-ECDSA-P256.key b/rust/bssl-tls/src/rustls_provider/tests/BoringSSLServerTest-ECDSA-P256.key
new file mode 100644
index 0000000..7a2dca2
--- /dev/null
+++ b/rust/bssl-tls/src/rustls_provider/tests/BoringSSLServerTest-ECDSA-P256.key
@@ -0,0 +1,9 @@
+This is generated from
+    openssl ecparam -genkey -name prime256v1 \
+    -out BoringSSLServerTest-ECDSA-P256.key -noout
+
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIAaBlGFkA3e1W1vgXB0XDNQh9b+NMiJWlOvd7G90RqiuoAoGCCqGSM49
+AwEHoUQDQgAE69H/lXVKCwj9AO+RmNRvHvc+OAOrcjui8wRxn+uljRgWT3LLORwp
+mA5xciM+PGmt6ISPcOOA57ZN8iOw7F2ffw==
+-----END EC PRIVATE KEY-----
diff --git a/rust/bssl-tls/src/rustls_provider/tests/BoringSSLServerTest-RSA-PSS-SHA256.crt b/rust/bssl-tls/src/rustls_provider/tests/BoringSSLServerTest-RSA-PSS-SHA256.crt
new file mode 100644
index 0000000..ad53a9c
--- /dev/null
+++ b/rust/bssl-tls/src/rustls_provider/tests/BoringSSLServerTest-RSA-PSS-SHA256.crt
@@ -0,0 +1,43 @@
+This is generated from
+    openssl x509 -req -in server.csr -CA BoringSSLCATest.crt \
+      -CAkey BoringSSLCATest.key -CAcreateserial \
+      -out BoringSSLServerTest-RSA-PSS-SHA256.crt -extfile svcconfig \
+      -days 730500 -sigopt rsa_padding_mode:pss -sigopt rsa_pss_saltlen:-1 \
+      -sigopt rsa_mgf1_md:sha256
+
+-----BEGIN CERTIFICATE-----
+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
+J1hof3HUwrQDu+GLkio9dR3rVD4T5w==
+-----END CERTIFICATE-----
diff --git a/rust/bssl-tls/src/rustls_provider/tests/BoringSSLServerTest-RSA.crt b/rust/bssl-tls/src/rustls_provider/tests/BoringSSLServerTest-RSA.crt
new file mode 100644
index 0000000..ca24a8f
--- /dev/null
+++ b/rust/bssl-tls/src/rustls_provider/tests/BoringSSLServerTest-RSA.crt
@@ -0,0 +1,39 @@
+This is generated from
+    openssl x509 -req -in server.csr -CA BoringSSLCATest.crt \
+      -CAkey BoringSSLCATest.key -CAcreateserial \
+      -out BoringSSLServerTest-RSA.crt -extfile svcconfig -days 730500
+
+-----BEGIN CERTIFICATE-----
+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
+1CFjnFjWbh1CedM8QPQ=
+-----END CERTIFICATE-----
diff --git a/rust/bssl-tls/src/rustls_provider/tests/BoringSSLServerTest-RSA.key b/rust/bssl-tls/src/rustls_provider/tests/BoringSSLServerTest-RSA.key
new file mode 100644
index 0000000..9053d6b
--- /dev/null
+++ b/rust/bssl-tls/src/rustls_provider/tests/BoringSSLServerTest-RSA.key
@@ -0,0 +1,55 @@
+This is generated from
+    openssl genrsa -out BoringSSLServerTest.key 4096
+
+-----BEGIN PRIVATE KEY-----
+MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQD3oj+OnqWa9+vF
+z9UxO8DKAoM6gZCajUfUPMHyYT2HESV/vLmk3H8+fTAeMo3s1eC1skezdz0tLmHd
+rQNiLeAZyPsfQ+/tzaKWjGEn/H2eQpAHeciH9Tr/+Q4ls7JnRT5rpr8Mjax4AIpd
+bN2XjPYfL3PI7a85c4GlOPztpgwcRkVJmBnNTDtjrGea2JU2RrZH7M29hMqLNG5e
+Z/AfT8d+V5c/5rl+sfZmQSYrPp8aiEhYRn5tanTwSm/0US3LIkUzwXdNx4oNrQ3c
+ao5chzqpp9zfRQvYNlrJP1wq3J2BHTC9DQNdy434XTGBdpfBHxQMVD0wQcR7lm+M
+Ki8q+/yWWV969khK64+mxlnh4QUnzH3AYHfdj6J8hsHXPgUYrVTEaI8Q1sfyY9gX
+vY/DKQibFa8Z66IWI2e4uLHu7jLTjzFLGcJO+T6av3fsuenZx5+CIbgtTe2cMLGy
+hsE4Ijr+CCH+LpTQuVrvj7AVdLLfNyhZ/UiS66JqF0+QD3i501ki9hC5NaXqIckL
+VT5WAPRqNxfZlFrX7HZtXSKb0L/xA7BmkY2uru5sAOgDrRrFGIAVsMuvLT60JfwB
+23AJ/kTQLfO+JVWCyaA29r4N1zmnUZ+n7meKqDTm9W2CVL+8B6Ym2g/WWPiTU/nz
+I3O1aSLP768KdwdTollZljaKEdCsLQIDAQABAoICAAeuC/uP1wH522GMowd+W2nI
+bypy1zm71PTzl249bsuQEBIol7dRsU6OUl41YipsraXk7A1YTtjmXdmion66fn8+
+OO+My1WcMYUqwF6dmYW9ebsJn1r8E4LZxgMMUiWaw6dSCg3JHQaxuZjRJgQrtnxc
+G+Ko4GzPNL+bh1iVdD2yPjbclTxFN3hNYf8u5V3EDqYnZXARvLhZfWzHG27VKhI0
+hDfSn4Ea4tHkBluD+yo2/MtkEEqzaQIExPkWRW3N18iVoO4UGKd47PufgF/FP+AA
+GT0BZq8jbGheYyzfH7Ff5uGOFEMl63a+6SijNWyjWptRR36GI6JTlY0Kx+C05O3z
+JIDPUciYqqvKUnoRy1wnySixKgm0WgWo6RNq5o8LRHj4ZWqxVq7oHVVBnQ99080Z
+woNFvDmeycshqSlFCjTnDSUfMJhkIBx+eWQPGE30DOVxverHOJ0XMxA9ZbZIxz2M
+AQhMTqRtClTozJ/xs0ZKGt9KUvcIf7EyhSdlS3TZJ/pP07bN6XeLsq1Kpp/BvQI0
+nll2c4eS6Aik0JiKbq1sxt0mSzZt/jG0QCMwvaVwu3XVGi01LCJ5BuKjvCTBgGAK
++wCFpncc+eXsfaEd3IyoYOVGZ8EROO34iA30ERvJ8o36l67q8qOsZm/kEB26Df4z
+upSnr15OlitpIJQ6YNHxAoIBAQD9hO0zM5ccW/OkN0M/mLYw9sqJJmILrB0Z81Vi
+vXsqItUqgsQJW1WFGx7CdjWxk8e8xQkDn+KvOCP0eBqosEoP99sYim5Cn+xjGA+T
+nGAQYEBs+cMFN3bu0z3YQvGyz0+E0AyNArwX0KMisUxrY3eTPOcdy4n8I+jMw9xw
+ueuIXw3KdwgeWaXrbRgMXjPhRjMn9+OZSJa2NDHuPnqCooox7Lr1hpuejKdLD/ht
+Kl1WtS84BcrWMcH3E8IF7TWmYtAhu84EfX1A2a+qqmL/RS2oOw3oYTugJuqF/dN2
+EjcpGmAAFxbSkvEtT21E+Nopvd18eDCOEW/6502XLjDhxtT1AoIBAQD6DpQVAG87
+7c0Un+TQulopb9nGmbuz2+vO9SIysbzlj/3elvaeRTizIM6KlWoWf5qI42Lu/9N5
+zTjDbu1ftg8p8rFlMqP54Ore4GUeMiM0Rk+LIjsIvanYvddvFPSRHCoK1ffCk/DG
+OSDENq2JoQYazJp16E1VLEu7/EBhHB6Cy8yFFj40rIAE9Rxd+7qLZm3FDrHtmGRM
+SXfYyaKXtQEeLnzsh3Sm4WWvgNQpGWFZQ44rsEB+bkokJ/XygfLugaPuXKcuoyaq
+2CywE7eAxj96+PSLyIg3mjXwGCXBIku+Ed0ALZWQ6Csu5vGfQCAvfxzubIWdIjnP
+0QQWNZV2MDdZAoIBAQCXPYqoRfm7EFwMNm+m6/qcwU3Yfg51uiruRU1GB5YHcBpN
+Lw+2KUeejaxPBGhJ1MiOo9kZ0XNRZqOEf3Yf9nNojUumm0bl9jP2de8s91gTzOgC
+Wwnt/cW0+k5lyqIYMzbUG62xHdWKO4xm8PCPDBrUuruB+eAKjH2gUqQal7+cbmBy
+zYoJWR/zj/SNxFEc7l0sVeTwl+5ZKlAzOhCqCD97QyfRu4jxECXpUNC6h1CBnrtZ
+p5L3L13wgVf5YybjaQWTak+gPCDR5Eu4+8btVJ7FQt2sKP2CMFUutFtHj9xaaAKn
+ax7RZpn8luqv/+leh4cvbyBAUMTGIOEX9JVyy8RVAoIBAHtluFPI3BuR1VNpOEx8
+ucObC7gC42r1ix+dPpwPs+0BKsGuc9NUy48yEFq5MxoZLFSDCa5xlpWT3YAr/H3v
+5PnJZxtOazcDdEQ6LgxBp7fDPrulT8aXefqYbHjHuYzmfiTMxDBEO1xGktHhPbAe
+Q1n0QAEReyAd9N22tLp3WuMm2S2P9XCe86n+n1oNwFfMWz0UbF+YhV5UHw1fK5p7
+2ypevI0opzs3HawHAiup961KNh1/I8SAfpvrEGb1E8H5PcGB/Yp5PrquZRcbE8I7
+ktYHhv54Hih6NEXgVLlDSGdqf0n4NMfGmpDRrMjupzNpIgjSivkpC6hvN/oRxUkG
+sDkCggEBAOeOIRtty6TU9VEJlEML2egUMFRP/cedi3lkW7ZscDtN7TGesB0cuogv
+Clzv93ICInaN0RRs8rt20LK6Cj5Q+vKFiTV1J3WLpu37pRgqGqYY36xIasvr1RaD
+sz+ux26P13xZcV0o/YNtAMIguV8+EHKqjyaZ8/w4z/CTQHY2F2JtdtXJWc+RfdZr
+4HN4CEYoUdKowyAsPp0anCQkJEAUyeFAdL/lCB1nY86o50QaaaUQ0+H+SAjrqQTY
+8LIbAyfvgcblseYUgqoydj+2LiJAw6JKNJCcL0yAmVjXgThW1hO9kaGK1c5bSqlm
+GDIXUzmOVt1IA81bYZAo78EH3G9yWZg=
+-----END PRIVATE KEY-----
diff --git a/rust/bssl-tls/src/rustls_provider/tests/caconfig b/rust/bssl-tls/src/rustls_provider/tests/caconfig
new file mode 100644
index 0000000..349b29d
--- /dev/null
+++ b/rust/bssl-tls/src/rustls_provider/tests/caconfig
@@ -0,0 +1,29 @@
+[ca]
+default_ca = CA_LOC
+
+[CA_LOC]
+prompt = no
+default_days = 730500
+
+[req]
+default_bits = 4096
+string_mask = utf8only
+default_md = sha256
+distinguished_name = req_distinguished_name
+x509_extensions = v3_ca
+prompt = no
+
+[req_distinguished_name]
+C = DE
+ST = Freistaat Bayern
+L = Muenchen
+O = Google Germany GmbH
+OU = ISE Crypto
+CN = BoringSSL Authors
+emailAddress = boringssl@google.com
+
+[v3_ca]
+subjectKeyIdentifier = hash
+authorityKeyIdentifier = keyid:always,issuer
+basicConstraints = critical,CA:true
+keyUsage = critical, digitalSignature, cRLSign, keyCertSign
\ No newline at end of file
diff --git a/rust/bssl-tls/src/rustls_provider/tests/svcconfig b/rust/bssl-tls/src/rustls_provider/tests/svcconfig
new file mode 100644
index 0000000..4d139ec
--- /dev/null
+++ b/rust/bssl-tls/src/rustls_provider/tests/svcconfig
@@ -0,0 +1,8 @@
+authorityKeyIdentifier=keyid,issuer
+basicConstraints=CA:FALSE
+keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
+subjectAltName = @alt_names
+
+[alt_names]
+DNS.1 = www.google.com
+DNS.2 = localhost