Skip to content
Merged
32 changes: 32 additions & 0 deletions boring/src/hmac.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
use crate::cvt;
use crate::error::ErrorStack;
use crate::foreign_types::ForeignTypeRef;
use crate::hash::MessageDigest;

foreign_type_and_impl_send_sync! {
type CType = ffi::HMAC_CTX;
fn drop = ffi::HMAC_CTX_free;

pub struct HmacCtx;
}

impl HmacCtxRef {
/// Configures HmacCtx to use `md` as the hash function and `key` as the key.
///
/// https://commondatastorage.googleapis.com/chromium-boringssl-docs/hmac.h.html#HMAC_Init_ex
pub fn init(&mut self, key: &[u8], md: &MessageDigest) -> Result<(), ErrorStack> {
ffi::init();

unsafe {
cvt(ffi::HMAC_Init_ex(
self.as_ptr(),
key.as_ptr().cast(),
key.len(),
md.as_ptr(),
// ENGINE api is deprecated
core::ptr::null_mut(),
))
.map(|_| ())
}
}
}
1 change: 1 addition & 0 deletions boring/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ pub mod error;
pub mod ex_data;
pub mod fips;
pub mod hash;
pub mod hmac;
pub mod hpke;
pub mod memcmp;
pub mod nid;
Expand Down
69 changes: 67 additions & 2 deletions boring/src/ssl/callbacks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ use super::{
};
use crate::error::ErrorStack;
use crate::ffi;
use crate::hmac::HmacCtxRef;
use crate::ssl::TicketKeyCallbackResult;
use crate::symm::CipherCtxRef;
use crate::x509::{X509StoreContext, X509StoreContextRef};
use foreign_types::ForeignType;
use foreign_types::ForeignTypeRef;
use libc::c_char;
use libc::{c_int, c_uchar, c_uint, c_void};
use libc::{c_char, c_int, c_uchar, c_uint, c_void};
use std::ffi::CStr;
use std::mem::MaybeUninit;
use std::ptr;
use std::slice;
use std::str;
Expand Down Expand Up @@ -269,6 +272,68 @@ where
}
}

unsafe fn to_uninit<'a, T: 'a>(ptr: *mut T) -> &'a mut MaybeUninit<T> {
assert!(!ptr.is_null());
unsafe { &mut *ptr.cast::<MaybeUninit<T>>() }
}
Comment on lines +276 to +278
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirming my understanding: panic on assert seems appropriate because otherwise we have UB.

Copy link
Collaborator

@kornelski kornelski Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

References must never be null, so they can't be made from a null pointer.

This assert isn't strictly necessary as long as the function is always given non-null pointers.

I thought it's better to be extra careful about this, I don't feel strongly about this choice. It could have been a Result (assuming null is a real possibility), or downgraded to debug_assert if we trust that we'll never get a null pointer in these functions.


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; ffi::EVP_MAX_IV_LENGTH as usize],
&mut CipherCtxRef,
&mut HmacCtxRef,
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 { to_uninit(key_name.cast::<[u8; ffi::SSL_TICKET_KEY_NAME_LEN as usize]>()) };

// SAFETY: the callback provides 16 bytes iv
//
// https://github.com/google/boringssl/blob/main/ssl/ssl_session.cc#L331
let iv = unsafe { to_uninit(iv.cast::<[u8; ffi::EVP_MAX_IV_LENGTH as usize]>()) };

// When encrypting a new ticket, encrypt will be one.
let encrypt = encrypt == 1;

// Zero-initialize the key_name and iv, since the application is expected to populate these
// fields in the encrypt mode.
if encrypt {
*key_name = MaybeUninit::zeroed();
*iv = MaybeUninit::zeroed();
}
let key_name = unsafe { key_name.assume_init_mut() };
let iv = unsafe { iv.assume_init_mut() };

// The EVP_CIPHER_CTX and HMAC_CTX are owned by boringSSL.
let evp_ctx = unsafe { CipherCtxRef::from_ptr_mut(evp_ctx) };
let hmac_ctx = unsafe { HmacCtxRef::from_ptr_mut(hmac_ctx) };

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,
Expand Down
95 changes: 95 additions & 0 deletions boring/src/ssl/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,15 @@ use crate::dh::DhRef;
use crate::ec::EcKeyRef;
use crate::error::ErrorStack;
use crate::ex_data::Index;
use crate::hmac::HmacCtxRef;
use crate::nid::Nid;
use crate::pkey::{HasPrivate, PKeyRef, Params, Private};
use crate::srtp::{SrtpProtectionProfile, SrtpProtectionProfileRef};
use crate::ssl::bio::BioMethod;
use crate::ssl::callbacks::*;
use crate::ssl::error::InnerError;
use crate::stack::{Stack, StackRef, Stackable};
use crate::symm::CipherCtxRef;
use crate::x509::store::{X509Store, X509StoreBuilder, X509StoreBuilderRef, X509StoreRef};
use crate::x509::verify::X509VerifyParamRef;
use crate::x509::{
Expand Down Expand Up @@ -888,6 +890,53 @@ pub enum SslInfoCallbackValue {
Alert(SslInfoCallbackAlert),
}

/// Ticket key callback status.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum TicketKeyCallbackResult {
/// Abort the handshake.
Error,

/// Continue with a full handshake.
///
/// When in decryption mode, this indicates that the peer supplied session ticket was not
/// recognized. When in encryption mode, this instructs boring to not send a session ticket.
///
/// # Note
///
/// This is a decryption specific status code when using the submoduled BoringSSL.
Noop,

/// 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::Noop => 0,
TicketKeyCallbackResult::Success => 1,
TicketKeyCallbackResult::DecryptSuccessRenew => 2,
}
}
}

#[derive(Hash, Copy, Clone, PartialOrd, Ord, Eq, PartialEq, Debug)]
pub struct SslInfoCallbackAlert(c_int);

Expand Down Expand Up @@ -1180,6 +1229,52 @@ 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.
///
/// CipherCtx and HmacCtx are guaranteed to be initialized.
///
/// # Panics
///
/// This method panics if this `Ssl` is associated with a RPK context.
///
/// # Safety
///
/// The application is responsible for correctly setting the key_name, iv, encryption context
/// and hmac context. See the [`SSL_CTX_set_tlsext_ticket_key_cb`] docs for additional info.
///
/// [`SSL_CTX_set_tlsext_ticket_key_cb`]: https://commondatastorage.googleapis.com/chromium-boringssl-docs/ssl.h.html#SSL_CTX_set_tlsext_ticket_key_cb
#[corresponds(SSL_CTX_set_tlsext_ticket_key_cb)]
pub unsafe fn set_ticket_key_callback<F>(&mut self, callback: F)
where
F: Fn(
&SslRef,
&mut [u8; 16],
&mut [u8; ffi::EVP_MAX_IV_LENGTH as usize],
&mut CipherCtxRef,
&mut HmacCtxRef,
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.
Expand Down
1 change: 1 addition & 0 deletions boring/src/ssl/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,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");
Expand Down
Loading
Loading