blob: 84cb7ccfaad927ecf173edd08feb7ca6e6c1a940 [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 Session support for BoringSSL.
use alloc::vec::Vec;
use core::ptr::{
NonNull,
null_mut, //
};
use crate::{
call_slice_getter,
config::ProtocolVersion,
context::TlsContext,
errors::Error,
ffi::{
Alloc,
sanitize_slice,
slice_into_ffi_raw_parts, //
}, //
};
/// A TLS session.
///
/// See [RFC 8446 Section 2.2](https://datatracker.ietf.org/doc/html/rfc8446#section-2.2).
///
/// # Example
///
/// ```rust,no_run
/// # use bssl_tls::{context::TlsContext, sessions::TlsSession};
/// # use bssl_tls::context::{Client, TlsMode};
/// # use bssl_tls::connection::lifecycle::EstablishedTlsConnection;
/// // Assuming `conn` is an `EstablishedTlsConnection`
/// # let conn: EstablishedTlsConnection<'_, Client, TlsMode> = todo!();
/// # let ctx: TlsContext = todo!();
/// if let Some(session) = conn.get_session() {
/// // Serialize the session to store it
/// let session_bytes = session.to_bytes().unwrap();
///
/// // Deserialize the session to resume it later
/// let recovered_session = TlsSession::from_bytes(&session_bytes, &ctx).unwrap();
/// }
/// ```
pub struct TlsSession(pub(crate) NonNull<bssl_sys::SSL_SESSION>);
// Safety: once constructed an `SSL_SESSION` is immutable and has no thread-local data.
unsafe impl Send for TlsSession {}
unsafe impl Sync for TlsSession {}
impl Drop for TlsSession {
fn drop(&mut self) {
unsafe {
// Safety: self.ptr() is valid and we own a reference.
bssl_sys::SSL_SESSION_free(self.ptr());
}
}
}
impl Clone for TlsSession {
fn clone(&self) -> Self {
unsafe {
// Safety: self.ptr() is valid.
bssl_sys::SSL_SESSION_up_ref(self.ptr());
}
Self(self.0)
}
}
impl TlsSession {
pub(crate) fn ptr(&self) -> *mut bssl_sys::SSL_SESSION {
self.0.as_ptr()
}
/// Serializes the session into a newly allocated buffer.
pub fn to_bytes(&self) -> Result<Vec<u8>, Error> {
let mut out_data: *mut u8 = core::ptr::null_mut();
let mut out_len: usize = 0;
let rc = unsafe {
// Safety: `self.ptr()` is still valid.
bssl_sys::SSL_SESSION_to_bytes(self.ptr(), &raw mut out_data, &raw mut out_len)
};
if rc != 1 {
return Err(Error::extract_lib_err());
}
let out_data = Alloc(out_data);
let slice = unsafe {
// Safety: out_data.0 and out_len are returned by BoringSSL and are valid.
sanitize_slice(out_data.0, out_len).unwrap()
};
Ok(slice.to_vec())
}
/// Serializes the session for a ticket, excluding the session ID.
pub fn to_bytes_for_ticket(&self) -> Result<Vec<u8>, Error> {
let mut out_data: *mut u8 = core::ptr::null_mut();
let mut out_len: usize = 0;
let rc = unsafe {
// Safety: `self.ptr()` is still valid.
bssl_sys::SSL_SESSION_to_bytes_for_ticket(self.ptr(), &mut out_data, &mut out_len)
};
if rc != 1 {
return Err(Error::extract_lib_err());
}
let out_data = Alloc(out_data);
let slice = unsafe {
// Safety: out_data.0 and out_len are returned by BoringSSL and are valid.
sanitize_slice(out_data.0, out_len).unwrap()
};
Ok(slice.to_vec())
}
/// Parses a serialized session from bytes.
pub fn from_bytes<M>(bytes: &[u8], ctx: &TlsContext<M>) -> Result<Self, Error> {
let (ptr, len) = slice_into_ffi_raw_parts(bytes);
let ptr = unsafe {
// Safety: bytes is a valid slice and the context is still valid.
bssl_sys::SSL_SESSION_from_bytes(ptr, len, ctx.ptr())
};
let ptr = NonNull::new(ptr).ok_or_else(|| Error::extract_lib_err())?;
Ok(Self(ptr))
}
/// Get the protocol version of the session.
pub fn get_protocol_version(&self) -> Option<ProtocolVersion> {
let version = unsafe {
// Safety: self.ptr() is valid.
bssl_sys::SSL_SESSION_get_protocol_version(self.ptr())
};
version.try_into().ok()
}
/// Get the session creation time in seconds since the epoch.
pub fn get_time(&self) -> u64 {
unsafe {
// Safety: self.ptr() is valid.
bssl_sys::SSL_SESSION_get_time(self.ptr())
}
}
/// Get the session timeout in seconds.
pub fn get_timeout(&self) -> u64 {
unsafe {
// Safety: self.ptr() is valid.
bssl_sys::SSL_SESSION_get_timeout(self.ptr()).into()
}
}
/// Get the peer certificates as a list of DER-encoded certificates.
pub fn get_peer_certificates(&self) -> Result<Vec<Vec<u8>>, Error> {
let sk = unsafe {
// Safety: self.ptr() is valid.
bssl_sys::SSL_SESSION_get0_peer_certificates(self.ptr())
};
if sk.is_null() {
return Ok(Vec::new());
}
let len = unsafe {
// Safety: `sk` is valid.
bssl_sys::sk_CRYPTO_BUFFER_num(sk)
};
let mut res = Vec::new();
for i in 0..len {
let buf = unsafe {
// Safety: `sk` is valid and `i` is in bounds.
bssl_sys::sk_CRYPTO_BUFFER_value(sk, i)
};
if buf.is_null() {
continue;
}
let (data, len) = unsafe {
// Safety: `buf` is valid.
(
bssl_sys::CRYPTO_BUFFER_data(buf),
bssl_sys::CRYPTO_BUFFER_len(buf),
)
};
let Some(slice) = (unsafe {
// Safety: data and len are valid.
sanitize_slice(data, len)
}) else {
continue;
};
res.push(slice.to_vec());
}
Ok(res)
}
// TODO(@xfding): Implement SSL_SESSION_get0_peer_rpk when needed.
/// Get the signed certificate timestamp list, if any.
pub fn get0_signed_cert_timestamp_list(&self) -> Option<&[u8]> {
call_slice_getter!(
bssl_sys::SSL_SESSION_get0_signed_cert_timestamp_list,
self.ptr()
)
}
/// Get the OCSP response, if any.
///
/// See [RFC 8446 §4.4.2.1](https://datatracker.ietf.org/doc/html/rfc8446#section-4.4.2.1).
pub fn get_ocsp_response(&self) -> Option<&[u8]> {
call_slice_getter!(bssl_sys::SSL_SESSION_get0_ocsp_response, self.ptr())
}
/// Get the master key.
///
/// In TLS 1.3, this returns the **Resumption Master Secret**.
/// See [RFC 8446 §7.1](https://datatracker.ietf.org/doc/html/rfc8446#section-7.1).
///
/// BoringSSL uses this secret to automatically derive the Pre-Shared Key (PSK) for
/// session resumption. Users should not attempt to manually expand this secret or
/// perform manual cryptography; BoringSSL handles the key expansion internally when
/// a session is configured for resumption.
///
/// # Example
///
/// If you need to derive a PSK for external use (e.g. for external PSK resumption),
/// you can use `bssl_crypto::hkdf`:
///
/// ```rust,no_run
/// # use bssl_tls::sessions::TlsSession;
/// # // Assuming `session` is a `TlsSession`
/// # let session: TlsSession = todo!();
/// let master_key = session.get_master_key();
///
/// // Treat the master key as a PRK in HKDF
/// let prk = bssl_crypto::hkdf::Prk::new::<bssl_crypto::digest::Sha256>(&master_key)
/// .expect("Invalid master key length");
///
/// // Expand it to derive a PSK
/// let mut psk = vec![0u8; 32];
/// prk.expand_into(b"resumption psk", &mut psk)
/// .expect("HKDF expansion failed");
/// ```
pub fn get_master_key(&self) -> Vec<u8> {
let len = unsafe {
// Safety: self.ptr() is valid.
bssl_sys::SSL_SESSION_get_master_key(self.ptr(), null_mut(), 0)
};
let mut out = vec![0u8; len];
let len = unsafe {
// Safety: self.ptr() is valid.
bssl_sys::SSL_SESSION_get_master_key(self.ptr(), out.as_mut_ptr(), out.len())
};
out.truncate(len);
out
}
/// Check if the session should be single use.
pub fn should_be_single_use(&self) -> bool {
unsafe {
// Safety: self.ptr() is valid.
bssl_sys::SSL_SESSION_should_be_single_use(self.ptr()) == 1
}
}
/// Check if the session is resumable.
pub fn is_resumable(&self) -> bool {
unsafe {
// Safety: self.ptr() is valid.
bssl_sys::SSL_SESSION_is_resumable(self.ptr()) == 1
}
}
/// Check if the session has a ticket.
pub fn has_ticket(&self) -> bool {
unsafe {
// Safety: self.ptr() is valid.
bssl_sys::SSL_SESSION_has_ticket(self.ptr()) == 1
}
}
/// Get the ticket, if any.
pub fn get_ticket(&self) -> Option<&[u8]> {
call_slice_getter!(bssl_sys::SSL_SESSION_get0_ticket, self.ptr())
}
/// Check if the session has a peer SHA256.
pub fn has_peer_sha256(&self) -> bool {
unsafe {
// Safety: self.ptr() is valid.
bssl_sys::SSL_SESSION_has_peer_sha256(self.ptr()) == 1
}
}
/// Get the peer SHA256, if any.
pub fn get_peer_sha256(&self) -> Option<&[u8]> {
call_slice_getter!(bssl_sys::SSL_SESSION_get0_peer_sha256, self.ptr())
}
/// Check if the session is resumable across names.
pub fn is_resumable_across_names(&self) -> bool {
unsafe {
// Safety: self.ptr() is valid.
bssl_sys::SSL_SESSION_is_resumable_across_names(self.ptr()) == 1
}
}
/// Check if the session is early data capable.
pub fn early_data_capable(&self) -> bool {
unsafe {
// Safety: self.ptr() is valid.
bssl_sys::SSL_SESSION_early_data_capable(self.ptr()) == 1
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::{collections::VecDeque, sync::Arc};
use core::task::Context;
use std::sync::Mutex;
use crate::{
connection::{Client, Server, TlsConnection},
context::{TlsContextBuilder, TlsMode},
credentials::{PskHash, TlsCredential},
io::{AbstractReader, AbstractSocket, AbstractSocketResult, AbstractWriter},
};
const TEST_KEY: &[u8; 32] = b"0123456789abcdef0123456789abcdef";
const TEST_IDENTITY: &[u8] = b"test-identity";
const TEST_CONTEXT: &[u8] = b"test-context";
const TEST_DATA: &[u8] = b"BoringSSL is awesome!";
struct SharedPipe {
buf: VecDeque<u8>,
}
struct ConnectedSocket {
read_pipe: Arc<Mutex<SharedPipe>>,
write_pipe: Arc<Mutex<SharedPipe>>,
}
impl AbstractReader for ConnectedSocket {
fn read(&mut self, _: Option<&mut Context<'_>>, buf: &mut [u8]) -> AbstractSocketResult {
let mut pipe = self.read_pipe.lock().unwrap();
let len = pipe.buf.len().min(buf.len());
if len == 0 {
return AbstractSocketResult::Retry;
}
for i in 0..len {
buf[i] = pipe.buf.pop_front().unwrap();
}
AbstractSocketResult::Ok(len)
}
}
impl AbstractWriter for ConnectedSocket {
fn write(&mut self, _: Option<&mut Context<'_>>, buf: &[u8]) -> AbstractSocketResult {
let mut pipe = self.write_pipe.lock().unwrap();
pipe.buf.extend(buf.iter().copied());
AbstractSocketResult::Ok(buf.len())
}
fn flush(&mut self, _: Option<&mut Context<'_>>) -> AbstractSocketResult {
AbstractSocketResult::Ok(0)
}
}
impl AbstractSocket for ConnectedSocket {}
fn create_connected_pair() -> (ConnectedSocket, ConnectedSocket) {
let pipe1 = Arc::new(Mutex::new(SharedPipe {
buf: VecDeque::new(),
}));
let pipe2 = Arc::new(Mutex::new(SharedPipe {
buf: VecDeque::new(),
}));
(
ConnectedSocket {
read_pipe: pipe1.clone(),
write_pipe: pipe2.clone(),
},
ConnectedSocket {
read_pipe: pipe2,
write_pipe: pipe1,
},
)
}
fn check_connection(
conn_client: &mut TlsConnection<Client, TlsMode>,
conn_server: &mut TlsConnection<Server, TlsMode>,
) {
let mut client_established = false;
let mut server_established = false;
for _ in 0..100 {
if !client_established {
if let Some(mut handshake) = conn_client.in_handshake() {
match handshake.do_handshake() {
Ok(None) => client_established = true,
Ok(Some(_)) => {}
Err(e) => panic!("Client handshake failed: {:?}", e),
}
} else {
client_established = true;
}
}
if !server_established {
if let Some(mut handshake) = conn_server.in_handshake() {
match handshake.do_handshake() {
Ok(None) => server_established = true,
Ok(Some(_)) => {}
Err(e) => panic!("Server handshake failed: {:?}", e),
}
} else {
server_established = true;
}
}
if client_established && server_established {
break;
}
}
assert!(client_established);
assert!(server_established);
// Send application data to verify connection works
let test_data = TEST_DATA;
let mut written = 0;
while written < test_data.len() {
match conn_client.sync_write(&test_data[written..]) {
Ok(crate::io::IoStatus::Ok(n)) => written += n,
_ => panic!("Write failed"),
}
}
let mut read_buf = [0u8; TEST_DATA.len()];
let mut recv_buf = crate::ffi::ReceiveBuffer::new(&mut read_buf);
while recv_buf.remaining() > 0 {
match conn_server.sync_read(&mut recv_buf) {
Ok(crate::io::IoStatus::Ok(_)) => {}
_ => panic!("Read failed"),
}
}
assert_eq!(&read_buf, test_data);
}
#[test]
fn test_session_ops() {
let ctx = TlsContextBuilder::new_tls().build();
let dummy_bytes = vec![0u8; 32];
let res = TlsSession::from_bytes(&dummy_bytes, &ctx);
assert!(res.is_err());
}
#[test]
fn test_psk_resumption() {
let cred_client = TlsCredential::new_pre_shared_key(
TEST_KEY,
TEST_IDENTITY,
PskHash::Sha256,
TEST_CONTEXT,
)
.unwrap();
let cred_server = TlsCredential::new_pre_shared_key(
TEST_KEY,
TEST_IDENTITY,
PskHash::Sha256,
TEST_CONTEXT,
)
.unwrap();
let mut ctx_client = TlsContextBuilder::new_tls();
ctx_client.with_credential(cred_client).unwrap();
let ctx_client = ctx_client.build();
let mut ctx_server = TlsContextBuilder::new_tls();
ctx_server.with_credential(cred_server).unwrap();
let ctx_server = ctx_server.build();
let (sock_client, sock_server) = create_connected_pair();
let builder_client = ctx_client.new_client_connection(None).unwrap();
let mut conn_client = builder_client.build();
conn_client.set_io(sock_client).unwrap();
let builder_server = ctx_server.new_server_connection(None).unwrap();
let mut conn_server = builder_server.build();
conn_server.set_io(sock_server).unwrap();
check_connection(&mut conn_client, &mut conn_server);
let est_client = conn_client.established().unwrap();
let session = est_client.get_session().unwrap();
let session_bytes = session.to_bytes().unwrap();
assert!(!session_bytes.is_empty());
let session_recovered = TlsSession::from_bytes(&session_bytes, &ctx_client).unwrap();
let session_bytes_2 = session_recovered.to_bytes().unwrap();
assert_eq!(session_bytes, session_bytes_2);
}
#[test]
fn test_ticket_based_resumption() {
let cred_client = TlsCredential::new_pre_shared_key(
TEST_KEY,
TEST_IDENTITY,
PskHash::Sha256,
TEST_CONTEXT,
)
.unwrap();
let cred_server = TlsCredential::new_pre_shared_key(
TEST_KEY,
TEST_IDENTITY,
PskHash::Sha256,
TEST_CONTEXT,
)
.unwrap();
let mut ctx_client = TlsContextBuilder::new_tls();
ctx_client.with_credential(cred_client).unwrap();
let ctx_client = ctx_client.build();
let mut ctx_server = TlsContextBuilder::new_tls();
ctx_server.with_credential(cred_server).unwrap();
let ctx_server = ctx_server.build();
let (sock_client, sock_server) = create_connected_pair();
let builder_client = ctx_client.new_client_connection(None).unwrap();
let mut conn_client = builder_client.build();
conn_client.set_io(sock_client).unwrap();
let builder_server = ctx_server.new_server_connection(None).unwrap();
let mut conn_server = builder_server.build();
conn_server.set_io(sock_server).unwrap();
check_connection(&mut conn_client, &mut conn_server);
let est_client = conn_client.established().unwrap();
let session = est_client.get_session().unwrap();
// === SESSION RESUMPTION ===
// Use the session for a new connection
let (sock_client_2, sock_server_2) = create_connected_pair();
let mut builder_client_2 = ctx_client.new_client_connection(None).unwrap();
builder_client_2.with_session(&session);
let mut conn_client_2 = builder_client_2.build();
conn_client_2.set_io(sock_client_2).unwrap();
let builder_server_2 = ctx_server.new_server_connection(None).unwrap();
let mut conn_server_2 = builder_server_2.build();
conn_server_2.set_io(sock_server_2).unwrap();
check_connection(&mut conn_client_2, &mut conn_server_2);
}
}