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);
+    }
+}