rust: bssl-tls: Sessions for TLS 1.3 Bug: 479599893 Signed-off-by: Xiangfei Ding <xfding@google.com> Change-Id: I0a718492703d5ec2d358155efb6a4d5e6a6a6964 Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/94228 Reviewed-by: Adam Langley <agl@google.com> Presubmit-BoringSSL-Verified: boringssl-scoped@luci-project-accounts.iam.gserviceaccount.com <boringssl-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/rust/bssl-tls/src/connection.rs b/rust/bssl-tls/src/connection.rs index ded375d..e3b6d5f 100644 --- a/rust/bssl-tls/src/connection.rs +++ b/rust/bssl-tls/src/connection.rs
@@ -41,7 +41,8 @@ Error, TlsRetryReason, // }, - io::IoStatus, // + io::IoStatus, + sessions::TlsSession, // }; mod credentials; @@ -129,6 +130,15 @@ } self } + + /// Set the session for resumption. + pub fn with_session(&mut self, session: &TlsSession) -> &mut Self { + unsafe { + // Safety: self.ptr and session.0 are valid. + bssl_sys::SSL_set_session(self.ptr.as_ptr(), session.0.as_ptr()); + } + self + } } /// TLS Connection
diff --git a/rust/bssl-tls/src/connection/lifecycle.rs b/rust/bssl-tls/src/connection/lifecycle.rs index 62c621d..3b072f8 100644 --- a/rust/bssl-tls/src/connection/lifecycle.rs +++ b/rust/bssl-tls/src/connection/lifecycle.rs
@@ -252,6 +252,17 @@ } } +impl<'a, R, M> EstablishedTlsConnection<'a, R, M> { + /// Get the current session. + pub fn get_session(&self) -> Option<crate::sessions::TlsSession> { + let session = unsafe { + // Safety: self.ptr() is valid. + bssl_sys::SSL_get1_session(self.ptr()) + }; + core::ptr::NonNull::new(session).map(crate::sessions::TlsSession) + } +} + impl<R, M> EstablishedTlsConnection<'_, R, M> where M: HasTlsConnectionMethod + HasBasicIo,
diff --git a/rust/bssl-tls/src/context.rs b/rust/bssl-tls/src/context.rs index 96d4598..6253be3 100644 --- a/rust/bssl-tls/src/context.rs +++ b/rust/bssl-tls/src/context.rs
@@ -36,8 +36,6 @@ ProtocolVersion, // }, connection::{ - Client, - Server, TlsConnectionBuilder, methods::HasTlsConnectionMethod, // }, @@ -46,6 +44,11 @@ has_duplicates, // }; +pub use crate::connection::{ + Client, + Server, // +}; + mod credentials; mod methods; mod sessions; @@ -351,6 +354,20 @@ } 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> {
diff --git a/rust/bssl-tls/src/context/sessions.rs b/rust/bssl-tls/src/context/sessions.rs index 8d4e66b..006cfde 100644 --- a/rust/bssl-tls/src/context/sessions.rs +++ b/rust/bssl-tls/src/context/sessions.rs
@@ -21,9 +21,11 @@ config::ConfigurationError, context::{ SupportedMode, + TlsContext, TlsContextBuilder, // }, - errors::Error, // + errors::Error, + sessions::TlsSession, // }; /// # Sessions @@ -89,3 +91,65 @@ const NO_INTERNAL = bssl_sys::SSL_SESS_CACHE_NO_INTERNAL as c_int; } } + +/// # Sessions +impl<M> TlsContext<M> +where + M: SupportedMode, +{ + /// Add a [`TlsSession`] to the context. + /// + /// This method returns `true` if the session is freshly added; + /// otherwise, it is already present in the context by returning `false`. + pub fn add_session(&self, session: &TlsSession) -> bool { + let ret = unsafe { + // Safety: + // - the validity of the handle `self.0` is witnessed by `self`. + // - the method is synchronised with a mutex guard. + bssl_sys::SSL_CTX_add_session(self.ptr(), session.ptr()) == 1 + }; + unsafe { + // Safety: we are only clearing the error queue in the current thread. + bssl_sys::ERR_clear_error(); + } + ret + } + + /// Remove a [`TlsSession`] from the context. + /// + /// This method returns `true` if the session is present and removed from the context; + /// otherwise, it is missing by returning `false`. + pub fn remove_session(&self, session: &TlsSession) -> bool { + let ret = unsafe { + // Safety: + // - the validity of the handle `self.0` is witnessed by `self`. + // - the method is synchronised with a mutex guard. + bssl_sys::SSL_CTX_remove_session(self.ptr(), session.ptr()) == 1 + }; + unsafe { + // Safety: we are only clearing the error queue in the current thread. + bssl_sys::ERR_clear_error(); + } + ret + } + + /// Remove all sessions that have expired past the `deadline`, which is a POSIX timestamp. + pub fn flush_sessions(&self, deadline: u64) { + unsafe { + // Safety: + // - the validity of the handle `self.0` is witnessed by `self`. + // - the method is synchronised with a mutex guard. + bssl_sys::SSL_CTX_flush_sessions(self.ptr(), deadline); + } + } + + /// Get the number of sessions in the context. + pub fn count_sessions(&self) -> usize { + unsafe { + // Safety: + // - the validity of the handle `self.0` is witnessed by `self`. + // - the method is synchronised with a mutex guard. + bssl_sys::SSL_CTX_sess_number(self.ptr()) + } + } +}
diff --git a/rust/bssl-tls/src/lib.rs b/rust/bssl-tls/src/lib.rs index 1c43067..e993416 100644 --- a/rust/bssl-tls/src/lib.rs +++ b/rust/bssl-tls/src/lib.rs
@@ -44,6 +44,7 @@ mod ffi; pub mod io; mod methods; +pub mod sessions; #[macro_use] #[doc(hidden)] mod macros;
diff --git a/rust/bssl-tls/src/sessions.rs b/rust/bssl-tls/src/sessions.rs new file mode 100644 index 0000000..5a6676c --- /dev/null +++ b/rust/bssl-tls/src/sessions.rs
@@ -0,0 +1,582 @@ +// 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, + null_mut, // +}; + +use crate::{ + 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) + } +} + +macro_rules! call_slice_getter { + ($fn:path, $obj:expr) => {{ + let mut data = null(); + let mut len = 0; + unsafe { + // Safety: `obj`, `data` and `len` are all valid. + $fn($obj, &raw mut data, &raw mut len); + } + unsafe { + // Safety: data and len are returned by BoringSSL and are valid. + sanitize_slice(data, len) + } + }}; +} + +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); + } +}