blob: f75ec3792b2b7849f9253644538e1fa40bdd3038 [file]
// 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.
//! TLS context builder and context type
use alloc::boxed::Box;
use core::{
marker::PhantomData,
mem::forget,
ptr::NonNull, //
};
use bssl_crypto::FfiSlice;
use crate::{
check_lib_error,
config::{
CompliancePolicy,
ConfigurationError,
KeyExchangeGroupFlag,
KeyExchangeGroups,
ProtocolVersion, //
},
connection::{
TlsConnectionBuilder,
methods::HasTlsConnectionMethod, //
},
context::methods::HasTlsContextMethod,
errors::Error,
has_duplicates, //
};
pub use crate::connection::{
Client,
Server, //
};
mod credentials;
mod methods;
mod sessions;
/// TLS or DTLS mode
pub enum TlsMode {}
/// DTLS mode
pub enum DtlsMode {}
/// QUIC mode
pub enum QuicMode {}
pub(crate) trait HasBasicIo {}
/// A collection of supported mode of operations.
pub trait SupportedMode: HasTlsContextMethod + HasTlsConnectionMethod {}
impl SupportedMode for TlsMode {}
impl SupportedMode for DtlsMode {}
impl SupportedMode for QuicMode {}
impl HasBasicIo for TlsMode {}
impl HasBasicIo for DtlsMode {}
/// General TLS configuration
///
/// The `Mode` generic can be either [`TlsMode`] or [`QuicMode`].
/// This generic governs the kind of [`TlsConnection`] that can be constructed.
pub struct TlsContextBuilder<Mode = TlsMode> {
ptr: NonNull<bssl_sys::SSL_CTX>,
_p: PhantomData<fn() -> Mode>,
}
impl<M> TlsContextBuilder<M> {
fn ptr(&self) -> *mut bssl_sys::SSL_CTX {
self.ptr.as_ptr()
}
}
impl<M> TlsContextBuilder<M>
where
M: HasTlsContextMethod,
{
fn new_inner(method: *const bssl_sys::SSL_METHOD) -> Self {
let Some(ptr) = NonNull::new(unsafe {
// Safety: this call only makes allocations
bssl_sys::SSL_CTX_new(method)
}) else {
panic!("allocation failure")
};
let this = TlsContextBuilder {
ptr,
_p: PhantomData,
};
let rc = unsafe {
// Safety: `ctx` is still valid
bssl_sys::SSL_CTX_set_ex_data(
ptr.as_ptr(),
M::registration(),
Box::into_raw(Box::new(methods::RustContextMethods::<M>::new())) as _,
)
};
assert!(rc == 1);
this
}
}
/// # Make a TLS context builder
impl TlsContextBuilder<TlsMode> {
/// Creates a new TLS context builder.
pub fn new_tls() -> Self {
Self::new_inner(unsafe {
// Safety: this call returns a static immutable data
bssl_sys::TLS_method()
})
}
}
/// # Make a DTLS context builder operating in pure DTLS mode
impl TlsContextBuilder<DtlsMode> {
/// Creates a new DTLS context builder.
pub fn new_dtls() -> Self {
Self::new_inner(unsafe {
// Safety: this call returns a static immutable data
bssl_sys::DTLS_method()
})
}
}
/// # Configure the context through a context builder
impl<M> TlsContextBuilder<M>
where
M: HasTlsContextMethod,
{
/// Builds and returns the configured TLS context.
pub fn build(self) -> TlsContext<M> {
let ptr = self.ptr;
// We must disarm the drop activated by the builder and pass on the ownership.
// Now `self` has no destructors to call.
forget(self);
TlsContext {
ptr,
_p: PhantomData,
}
}
#[allow(unused)]
fn get_context_methods(&mut self) -> &mut methods::RustContextMethods<M> {
let methods = unsafe {
// Safety: the validity of the handle `self.0` is witnessed by `self`.
bssl_sys::SSL_CTX_get_ex_data(self.ptr(), M::registration())
};
if methods.is_null() {
panic!("context method goes missing")
}
unsafe {
// Safety: `methods` must be constructed by `new_inner`
&mut *(methods as *mut methods::RustContextMethods<M>)
}
}
/// Set the minimum acceptable protocol.
///
/// If `version` is set to `None`, the library will choose a default minimum version.
/// - For TLS, it is TLS 1.3
/// - For DTLS, it is DTLS 1.2
///
/// This configuration fails if the protocol version is incompatible with the selected TLS
/// flavour, such as setting TLS 1.3 for DTLS flavour.
pub fn with_min_protocol(
&mut self,
version: Option<ProtocolVersion>,
) -> Result<&mut Self, Error> {
let version = version.map_or(0, |ver| ver as u16);
check_lib_error!(unsafe {
// Safety: the validity of the handle `self.0` is witnessed by `self`
bssl_sys::SSL_CTX_set_min_proto_version(self.ptr(), version)
});
Ok(self)
}
/// Set the maximum acceptable protocol.
///
/// If `version` is set to `None`, the library will choose a default maximum version.
/// - For TLS, it is TLS 1.3
/// - For DTLS, it is DTLS 1.2
///
/// This configuration fails if the protocol version is incompatible with the selected TLS
/// flavour, such as setting TLS 1.3 for DTLS flavour.
pub fn with_max_protocol(
&mut self,
version: Option<ProtocolVersion>,
) -> Result<&mut Self, Error> {
let version = version.map_or(0, |ver| ver as u16);
check_lib_error!(unsafe {
// Safety: the validity of the handle `self.0` is witnessed by `self`
bssl_sys::SSL_CTX_set_max_proto_version(self.ptr(), version)
});
Ok(self)
}
/// Set acceptable key exchange groups.
///
/// This method sets up the acceptable key exchange groups and the list of groups advertised by
/// the client.
///
/// If the supplied list is empty, the context will revert back to a default list of groups.
///
/// If the `flags` list is not empty, its length must match that of `groups`.
///
/// This method returns [`ConfigurationError::InvalidParameters`] if the key exchange groups are not unique in the list.
pub fn with_key_exchange_groups(
&mut self,
groups: &[KeyExchangeGroups],
flags: &[KeyExchangeGroupFlag],
) -> Result<&mut Self, Error> {
// TODO(@xfding): maybe we should use zerocopy here when v0.9 is finalised
let groups: &[u16] = unsafe {
// Safety: `KeyExchangeGroups` has the same layout as a `u16`,
// so `&[KeyExchangeGroups]` and `&[u16]` has the same layout.
core::mem::transmute(groups)
};
let flags: &[u32] = unsafe {
// Safety: `KeyExchangeGroupFlag` has the same layout as a `u32` through
// `repr(transparent)`.
core::mem::transmute(flags)
};
if !flags.is_empty() && groups.len() != flags.len() {
return Err(Error::Configuration(ConfigurationError::InvalidParameters));
}
if has_duplicates(groups) {
return Err(Error::Configuration(
ConfigurationError::DuplicatedParameters,
));
}
check_lib_error!(unsafe {
// Safety: the validity of the handle `self.0` is witnessed by `self`
if flags.is_empty() {
bssl_sys::SSL_CTX_set1_group_ids(self.ptr(), groups.as_ffi_ptr(), groups.len())
} else {
bssl_sys::SSL_CTX_set1_group_ids_with_flags(
self.ptr(),
groups.as_ffi_ptr(),
flags.as_ffi_ptr(),
groups.len(),
)
}
});
Ok(self)
}
/// Set a list of cipher suites, in the order of descending preference.
///
/// This method accepts a string that conforms to OpenSSL [cipher suite configuration] language
/// specification.
///
/// # Available cipher rules are:
///
/// - `ALL` matches all ciphers, except for deprecated ciphers which must be
/// named explicitly.
///
/// - `kRSA`, `kDHE`, `kECDHE`, and `kPSK` match ciphers using plain RSA, DHE,
/// ECDHE, and plain PSK key exchanges, respectively. Note that ECDHE_PSK is
/// matched by `kECDHE` and not `kPSK`.
///
/// - `aRSA`, `aECDSA`, and `aPSK` match ciphers authenticated by RSA, ECDSA, and
/// a pre-shared key, respectively.
///
/// - `RSA`, `DHE`, `ECDHE`, `PSK`, `ECDSA`, and `PSK` are aliases for the
/// corresponding `k*` or `a*` cipher rule. `RSA` is an alias for `kRSA`, not
/// `aRSA`.
///
/// - `3DES`, `AES128`, `AES256`, `AES`, `AESGCM`, `CHACHA20` match ciphers
/// whose bulk cipher use the corresponding encryption scheme. Note that
/// `AES`, `AES128`, and `AES256` match both CBC and GCM ciphers.
///
/// - `SHA1`, and its alias `SHA`, match legacy cipher suites using HMAC-SHA1.
///
/// # Deprecated cipher rules:
///
/// - `kEDH`, `EDH`, `kEECDH`, and `EECDH` are legacy aliases for `kDHE`, `DHE`,
/// `kECDHE`, and `ECDHE`, respectively.
///
/// - `HIGH` is an alias for `ALL`.
///
/// - `FIPS` is an alias for `HIGH`.
///
/// - `SSLv3` and `TLSv1` match ciphers available in TLS 1.1 or earlier.
/// `TLSv1_2` matches ciphers new in TLS 1.2. This is confusing and should not
/// be used.
///
/// [cipher suite configuration]: <https://docs.openssl.org/3.0/man1/openssl-ciphers/#cipher-list-format>
pub fn with_ciphers(&mut self, cipher_ops: &str) -> Result<&mut Self, Error> {
let Ok(cipher_ops) = alloc::ffi::CString::new(cipher_ops) else {
return Err(Error::Configuration(ConfigurationError::InvalidString));
};
check_lib_error!(unsafe {
// Safety:
// - the validity of the handle `self.0` is witnessed by `self`;
// - the string has been checked for internal NUL bytes and NUL-terminated.
bssl_sys::SSL_CTX_set_strict_cipher_list(self.ptr(), cipher_ops.as_ptr())
});
Ok(self)
}
/// Set TLS 1.2 session lifetime in seconds.
///
/// If 0 seconds are specified, BoringSSL will pick a default timeout for the connections.
pub fn with_tls12_timeout(&mut self, seconds: u32) -> &mut Self {
unsafe {
// Safety: the validity of the handle `self.0` is witnessed by `self`.
bssl_sys::SSL_CTX_set_timeout(self.ptr(), seconds)
};
self
}
/// Set TLS 1.3 session lifetime in seconds.
///
/// If 0 seconds are specified, BoringSSL will pick a default timeout for the connections.
pub fn with_tls13_timeout(&mut self, seconds: u32) -> &mut Self {
unsafe {
// Safety: the validity of the handle `self.0` is witnessed by `self`.
bssl_sys::SSL_CTX_set_session_psk_dhe_timeout(self.ptr(), seconds)
};
self
}
/// Set whether shutting down the connection sends out a `close_notify` alert.
pub fn with_quiet_shutdown(&mut self, quiet: bool) -> &mut Self {
unsafe {
// Safety: the validity of the handle `self.0` is witnessed by `self`.
bssl_sys::SSL_CTX_set_quiet_shutdown(self.ptr(), if quiet { 1 } else { 0 });
}
self
}
/// Set whether only the SHA-256 hash of the peer's certificate should be saved in the session.
///
/// This can save memory, ticket size and session cache space.
pub fn with_retain_only_sha256_of_client_certs(&mut self, enable: bool) -> &mut Self {
unsafe {
// Safety: the validity of the handle `self.ptr` is witnessed by `self`.
bssl_sys::SSL_CTX_set_retain_only_sha256_of_client_certs(
self.ptr(),
if enable { 1 } else { 0 },
);
}
self
}
}
impl<M> Drop for TlsContextBuilder<M> {
fn drop(&mut self) {
unsafe {
// Safety: self.0 is created from SSL_CTX_new so it is a valid pointer
bssl_sys::SSL_CTX_free(self.ptr());
}
}
}
/// A TLS context that is finalised and can be shared across connections
#[repr(transparent)]
pub struct TlsContext<M = TlsMode> {
ptr: NonNull<bssl_sys::SSL_CTX>,
_p: PhantomData<fn() -> M>,
}
impl<M> TlsContext<M> {
pub(crate) fn ptr(&self) -> *mut bssl_sys::SSL_CTX {
self.ptr.as_ptr()
}
}
/// # Create new connections associated to a context
impl<M> TlsContext<M>
where
M: HasTlsContextMethod + HasTlsConnectionMethod,
{
fn new_connection(
&self,
compliance_policy: Option<CompliancePolicy>,
) -> NonNull<bssl_sys::SSL> {
let conn = unsafe {
// Safety: in this type-state, our SSL_CTX is effectively immutable,
// so we can freely alias.
bssl_sys::SSL_new(self.ptr())
};
if let Some(policy) = compliance_policy {
unsafe {
// Safety: `policy` is a valid enum value per construction.
bssl_sys::SSL_set_compliance_policy(conn, policy as _);
}
}
NonNull::new(conn).expect("allocation failure")
}
/// Make a new client-half connection inheriting the configuration of this context
///
/// By default, this method will configure the connection to request certificates
/// from the server.
/// To override this default, use
/// [`TlsConnectionBuilder::with_certificate_verification_mode`].
pub fn new_client_connection(
&self,
compliance_policy: Option<CompliancePolicy>,
) -> Result<TlsConnectionBuilder<Client, M>, Error> {
let conn = self.new_connection(compliance_policy);
unsafe {
// Safety: the connection is still valid here
bssl_sys::SSL_set_connect_state(conn.as_ptr());
}
let mut builder = TlsConnectionBuilder::from_ssl(conn);
// The safe default is that the client should perform at least
// some certification verification.
builder.with_certificate_verification_mode(
crate::credentials::CertificateVerificationMode::PeerCertRequested,
);
Ok(builder)
}
/// Make a new server-half connection inheriting the configuration of this context
pub fn new_server_connection(
&self,
compliance_policy: Option<CompliancePolicy>,
) -> Result<TlsConnectionBuilder<Server, M>, Error> {
let conn = self.new_connection(compliance_policy);
unsafe {
// Safety: the connection is still valid here
bssl_sys::SSL_set_accept_state(conn.as_ptr());
}
Ok(TlsConnectionBuilder::from_ssl(conn))
}
/// Expose the fully built BoringSSL's `SSL_CTX` pointer.
///
/// # Safety
/// - `SSL_CTX` is **not fully thread-safe**; do not invoke any BoringSSL mutating functions
/// on the returned handle; otherwise, it is **undefined behaviour**.
/// - interacting with the `ex_data` of the `SSL_CTX` instance that this method returns is
/// **undefined behaviour**.
/// - `self` must outlive all uses of the returned handle;
/// - this handle must be used with functions from the BoringSSL library this crate is linked
/// to; otherwise, it is **undefined behaviour**.
///
/// # Recommendation
/// If the handle is used to interface BoringSSL library functions, please consider using a
/// corresponding Rust safe binding or contact BoringSSL Authors for support.
pub unsafe fn as_mut_ptr(&mut self) -> *mut bssl_sys::SSL_CTX {
self.ptr()
}
}
/// Safety: at this type state, most of the underlying context is immutable,
/// while methods on shared accesses are protected behind a mutex, so they are thread-safe.
unsafe impl<M> Send for TlsContext<M> {}
unsafe impl<M> Sync for TlsContext<M> {}
impl<M> Drop for TlsContext<M> {
fn drop(&mut self) {
unsafe {
// Safety: this handle is taken from the builder, so it must be
// live and valid.
bssl_sys::SSL_CTX_free(self.ptr());
}
}
}
impl<M> Clone for TlsContext<M> {
fn clone(&self) -> Self {
unsafe {
// Safety: this handle is already valid by the witness of self.
bssl_sys::SSL_CTX_up_ref(self.ptr());
// BoringSSL will always return success on bumping reference.
}
TlsContext {
ptr: self.ptr,
_p: PhantomData,
}
}
}
/// A reusable certificate cache shared across [`TlsContext`]
pub struct CertificateCache(pub(crate) NonNull<bssl_sys::CRYPTO_BUFFER_POOL>);
// Safety: `CRYPTO_BUFFER_POOL` is mutex-protected
unsafe impl Send for CertificateCache {}
unsafe impl Sync for CertificateCache {}
impl CertificateCache {
/// Construct a new certificate cache.
pub fn new() -> Self {
let pool = unsafe {
// Safety: this call does not have side-effect other than allocation
bssl_sys::CRYPTO_BUFFER_POOL_new()
};
Self(NonNull::new(pool).expect("allocation failure"))
}
pub(crate) fn ptr(&self) -> *mut bssl_sys::CRYPTO_BUFFER_POOL {
self.0.as_ptr()
}
}
impl Drop for CertificateCache {
fn drop(&mut self) {
unsafe {
// Safety: the validity of `self.0` is witnessed by `self`
bssl_sys::CRYPTO_BUFFER_POOL_free(self.ptr());
}
}
}
impl Clone for CertificateCache {
fn clone(&self) -> Self {
unsafe {
// Safety: the validity of `self.0` is witnessed by `self`
bssl_sys::CRYPTO_BUFFER_POOL_up_ref(self.ptr());
}
Self(self.0)
}
}