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:
parent
b3521e5523
commit
c49282f112
|
|
@ -8,6 +8,7 @@ use super::{
|
||||||
};
|
};
|
||||||
use crate::error::ErrorStack;
|
use crate::error::ErrorStack;
|
||||||
use crate::ffi;
|
use crate::ffi;
|
||||||
|
use crate::ssl::TicketKeyCallbackResult;
|
||||||
use crate::x509::{X509StoreContext, X509StoreContextRef};
|
use crate::x509::{X509StoreContext, X509StoreContextRef};
|
||||||
use foreign_types::ForeignType;
|
use foreign_types::ForeignType;
|
||||||
use foreign_types::ForeignTypeRef;
|
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>(
|
pub(super) unsafe extern "C" fn raw_alpn_select<F>(
|
||||||
ssl: *mut ffi::SSL,
|
ssl: *mut ffi::SSL,
|
||||||
out: *mut *const c_uchar,
|
out: *mut *const c_uchar,
|
||||||
|
|
|
||||||
|
|
@ -804,6 +804,50 @@ pub enum SslInfoCallbackValue {
|
||||||
Alert(SslInfoCallbackAlert),
|
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)]
|
#[derive(Hash, Copy, Clone, PartialOrd, Ord, Eq, PartialEq, Debug)]
|
||||||
pub struct SslInfoCallbackAlert(c_int);
|
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.
|
/// Sets the certificate verification depth.
|
||||||
///
|
///
|
||||||
/// If the peer's certificate chain is longer than this value, verification will fail.
|
/// If the peer's certificate chain is longer than this value, verification will fail.
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ mod ech;
|
||||||
mod private_key_method;
|
mod private_key_method;
|
||||||
mod server;
|
mod server;
|
||||||
mod session;
|
mod session;
|
||||||
|
mod session_resumption;
|
||||||
mod verify;
|
mod verify;
|
||||||
|
|
||||||
static ROOT_CERT: &[u8] = include_bytes!("../../../test/root-ca.pem");
|
static ROOT_CERT: &[u8] = include_bytes!("../../../test/root-ca.pem");
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue