Add set_ticket_key_callback (SSL_CTX_set_tlsext_ticket_key_cb)

Add a wrapper for the `SSL_CTX_set_tlsext_ticket_key_cb`, which allows
consumers to configure the EVP_CIPHER_CTX and HMAC_CTX used for
encrypting/decrypting session tickets.

See https://docs.openssl.org/1.0.2/man3/SSL_CTX_set_tlsext_ticket_key_cb/
for more details.
This commit is contained in:
Apoorv Kothari 2025-03-08 10:35:46 -08:00 committed by Kornel
parent b3521e5523
commit c49282f112
4 changed files with 282 additions and 0 deletions

View File

@ -8,6 +8,7 @@ use super::{
};
use crate::error::ErrorStack;
use crate::ffi;
use crate::ssl::TicketKeyCallbackResult;
use crate::x509::{X509StoreContext, X509StoreContextRef};
use foreign_types::ForeignType;
use foreign_types::ForeignTypeRef;
@ -269,6 +270,46 @@ where
}
}
pub(super) unsafe extern "C" fn raw_ticket_key<F>(
ssl: *mut ffi::SSL,
key_name: *mut u8,
iv: *mut u8,
evp_ctx: *mut ffi::EVP_CIPHER_CTX,
hmac_ctx: *mut ffi::HMAC_CTX,
encrypt: c_int,
) -> c_int
where
F: Fn(
&SslRef,
&mut [u8; 16],
*mut u8,
*mut ffi::EVP_CIPHER_CTX,
*mut ffi::HMAC_CTX,
bool,
) -> TicketKeyCallbackResult
+ 'static
+ Sync
+ Send,
{
// SAFETY: boring provides valid inputs.
let ssl = unsafe { SslRef::from_ptr_mut(ssl) };
let ssl_context = ssl.ssl_context().to_owned();
let callback = ssl_context
.ex_data::<F>(SslContext::cached_ex_index::<F>())
.expect("expected session resumption callback");
// Safety: the callback guarantees that key_name is 16 bytes
let key_name =
unsafe { slice::from_raw_parts_mut(key_name, ffi::SSL_TICKET_KEY_NAME_LEN as usize) };
let key_name = <&mut [u8; 16]>::try_from(key_name).expect("boring provides a 16-byte key name");
// When encrypting a new ticket, encrypt will be one.
let encrypt = encrypt == 1;
callback(ssl, key_name, iv, evp_ctx, hmac_ctx, encrypt).into()
}
pub(super) unsafe extern "C" fn raw_alpn_select<F>(
ssl: *mut ffi::SSL,
out: *mut *const c_uchar,

View File

@ -804,6 +804,50 @@ pub enum SslInfoCallbackValue {
Alert(SslInfoCallbackAlert),
}
/// Ticket key callback status.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum TicketKeyCallbackResult {
/// Abort the handshake.
Error,
/// The peer supplied session ticket was not recognized. Continue with a full handshake.
///
/// # Note
///
/// This is a decryption specific status code.
DecryptTicketUnrecognized,
/// Resumption callback was successful.
///
/// When in decryption mode, attempt an abbreviated handshake via session resumption. When in
/// encryption mode, provide a new ticket to the client.
Success,
/// Resumption callback was successful. Attempt an abbreviated handshake, and additionally
/// provide new session tickets to the peer.
///
/// Session resumption short-circuits some security checks of a full-handshake, in exchange for
/// potential performance gains. For this reason, a session ticket should only be valid for a
/// limited time. Providing the peer with renewed session tickets allows them to continue
/// session resumption with the new tickets.
///
/// # Note
///
/// This is a decryption specific status code.
DecryptSuccessRenew,
}
impl From<TicketKeyCallbackResult> for c_int {
fn from(value: TicketKeyCallbackResult) -> Self {
match value {
TicketKeyCallbackResult::Error => -1,
TicketKeyCallbackResult::DecryptTicketUnrecognized => 0,
TicketKeyCallbackResult::Success => 1,
TicketKeyCallbackResult::DecryptSuccessRenew => 2,
}
}
}
#[derive(Hash, Copy, Clone, PartialOrd, Ord, Eq, PartialEq, Debug)]
pub struct SslInfoCallbackAlert(c_int);
@ -1080,6 +1124,43 @@ impl SslContextBuilder {
}
}
/// Configures a custom session ticket key callback for session resumption.
///
/// Session Resumption uses the security context (aka. session tickets) of a previous
/// connection to establish a new connection via an abbreviated handshake. Skipping portions of
/// a handshake can potentially yield performance gains.
///
/// An attacker that compromises a server's session ticket key can impersonate the server and,
/// prior to TLS 1.3, retroactively decrypt all application traffic from sessions using that
/// ticket key. Thus ticket keys must be regularly rotated for forward secrecy.
///
/// # Panics
///
/// This method panics if this `Ssl` is associated with a RPK context.
#[corresponds(SSL_CTX_set_tlsext_ticket_key_cb)]
pub fn set_ticket_key_callback<F>(&mut self, callback: F)
where
F: Fn(
&SslRef,
&mut [u8; 16],
*mut u8,
*mut ffi::EVP_CIPHER_CTX,
*mut ffi::HMAC_CTX,
bool,
) -> TicketKeyCallbackResult
+ 'static
+ Sync
+ Send,
{
#[cfg(feature = "rpk")]
assert!(!self.is_rpk, "This API is not supported for RPK");
unsafe {
self.replace_ex_data(SslContext::cached_ex_index::<F>(), callback);
ffi::SSL_CTX_set_tlsext_ticket_key_cb(self.as_ptr(), Some(raw_ticket_key::<F>))
};
}
/// Sets the certificate verification depth.
///
/// If the peer's certificate chain is longer than this value, verification will fail.

View File

@ -30,6 +30,7 @@ mod ech;
mod private_key_method;
mod server;
mod session;
mod session_resumption;
mod verify;
static ROOT_CERT: &[u8] = include_bytes!("../../../test/root-ca.pem");

View File

@ -0,0 +1,159 @@
use super::server::Server;
use crate::ssl::test::MessageDigest;
use crate::ssl::SslRef;
use crate::ssl::SslSession;
use crate::ssl::SslSessionCacheMode;
use crate::ssl::TicketKeyCallbackResult;
use crate::symm::Cipher;
use std::ffi::c_void;
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::OnceLock;
static CUSTOM_ENCRYPTION_CALLED_BACK: AtomicU8 = AtomicU8::new(0);
static CUSTOM_DECRYPTION_CALLED_BACK: AtomicU8 = AtomicU8::new(0);
#[test]
fn resume_session() {
static SESSION_TICKET: OnceLock<Vec<u8>> = OnceLock::new();
let mut server = Server::builder();
server.expected_connections_count(2);
let server = server.build();
let mut client = server.client();
client
.ctx()
.set_session_cache_mode(SslSessionCacheMode::CLIENT);
client.ctx().set_new_session_callback(|_, session| {
let _can_receive_multiple_tickets = SESSION_TICKET.set(session.to_der().unwrap());
});
let ssl_stream = client.connect();
assert!(!ssl_stream.ssl().session_reused());
assert!(SESSION_TICKET.get().is_some());
// Retrieve the session ticket
let session_ticket = SslSession::from_der(SESSION_TICKET.get().unwrap()).unwrap();
// Attempt to resume the connection using the session ticket
let client_2 = server.client();
let mut ssl_builder = client_2.build().builder();
unsafe { ssl_builder.ssl().set_session(&session_ticket).unwrap() };
let ssl_stream_2 = ssl_builder.connect();
assert!(ssl_stream_2.ssl().session_reused());
}
#[test]
fn custom_callback() {
static SESSION_TICKET: OnceLock<Vec<u8>> = OnceLock::new();
let mut server = Server::builder();
server.expected_connections_count(2);
server
.ctx()
.set_ticket_key_callback(test_tickey_key_callback);
let server = server.build();
let mut client = server.client();
client
.ctx()
.set_session_cache_mode(SslSessionCacheMode::CLIENT);
client.ctx().set_new_session_callback(|_, session| {
let _can_receive_multiple_tickets = SESSION_TICKET.set(session.to_der().unwrap());
});
let ssl_stream = client.connect();
assert!(!ssl_stream.ssl().session_reused());
assert!(SESSION_TICKET.get().is_some());
assert_eq!(CUSTOM_ENCRYPTION_CALLED_BACK.load(Ordering::SeqCst), 2);
assert_eq!(CUSTOM_DECRYPTION_CALLED_BACK.load(Ordering::SeqCst), 0);
// Retrieve the session ticket
let session_ticket = SslSession::from_der(SESSION_TICKET.get().unwrap()).unwrap();
// Attempt to resume the connection using the session ticket
let client_2 = server.client();
let mut ssl_builder = client_2.build().builder();
unsafe { ssl_builder.ssl().set_session(&session_ticket).unwrap() };
let ssl_stream_2 = ssl_builder.connect();
assert!(ssl_stream_2.ssl().session_reused());
assert_eq!(CUSTOM_ENCRYPTION_CALLED_BACK.load(Ordering::SeqCst), 4);
assert_eq!(CUSTOM_DECRYPTION_CALLED_BACK.load(Ordering::SeqCst), 1);
}
// Custom callback to encrypt and decrypt session tickets
fn test_tickey_key_callback(
_ssl: &SslRef,
_key_name: &mut [u8; 16],
_iv: *mut u8,
evp_ctx: *mut ffi::EVP_CIPHER_CTX,
hmac_ctx: *mut ffi::HMAC_CTX,
encrypt: bool,
) -> TicketKeyCallbackResult {
// These should only be used for testing purposes.
const TEST_CBC_IV: [u8; 16] = [1; 16];
const TEST_AES_128_CBC_KEY: [u8; 16] = [2; 16];
const TEST_HMAC_KEY: [u8; 32] = [3; 32];
let digest = MessageDigest::sha256();
let cipher = Cipher::aes_128_cbc();
if encrypt {
CUSTOM_ENCRYPTION_CALLED_BACK.fetch_add(1, Ordering::SeqCst);
// Set the encryption context.
let ret = unsafe {
ffi::EVP_EncryptInit_ex(
evp_ctx,
cipher.as_ptr(),
// ENGINE api is deprecated
core::ptr::null_mut(),
TEST_AES_128_CBC_KEY.as_ptr(),
TEST_CBC_IV.as_ptr(),
)
};
assert!(ret == 1);
// Set the hmac context.
let ret = unsafe {
ffi::HMAC_Init_ex(
hmac_ctx,
TEST_HMAC_KEY.as_ptr() as *const c_void,
TEST_HMAC_KEY.len(),
digest.as_ptr(),
// ENGINE api is deprecated
core::ptr::null_mut(),
)
};
assert!(ret == 1);
} else {
CUSTOM_DECRYPTION_CALLED_BACK.fetch_add(1, Ordering::SeqCst);
let ret = unsafe {
ffi::EVP_DecryptInit_ex(
evp_ctx,
cipher.as_ptr(),
// ENGINE api is deprecated
core::ptr::null_mut(),
TEST_AES_128_CBC_KEY.as_ptr(),
TEST_CBC_IV.as_ptr(),
)
};
assert!(ret == 1);
// Set the hmac context.
let ret = unsafe {
ffi::HMAC_Init_ex(
hmac_ctx,
TEST_HMAC_KEY.as_ptr() as *const c_void,
TEST_HMAC_KEY.len(),
digest.as_ptr(),
// ENGINE api is deprecated
core::ptr::null_mut(),
)
};
assert!(ret == 1);
}
TicketKeyCallbackResult::Success
}