Remove ticketer from CryptoProvider

Instead, the ring-based `rustls::Ticketer` is exported directly,
as is the `TicketSwitcher` which is a useful building block for
downstream users.
This commit is contained in:
Joseph Birr-Pixton 2023-07-12 12:28:34 +01:00 committed by ctz
parent 7d6a84ba0c
commit 3d5c93aa0b
8 changed files with 173 additions and 172 deletions

View File

@ -633,7 +633,7 @@ fn make_config(args: &Args) -> Arc<rustls::ServerConfig<Ring>> {
}
if args.flag_tickets {
config.ticketer = rustls::Ticketer::new::<Ring>().unwrap();
config.ticketer = rustls::Ticketer::new().unwrap();
}
config.alpn_protocols = args

View File

@ -322,7 +322,7 @@ fn make_server_config(
if resume == ResumptionParam::SessionID {
cfg.session_storage = ServerSessionMemoryCache::new(128);
} else if resume == ResumptionParam::Tickets {
cfg.ticketer = Ticketer::new::<Ring>().unwrap();
cfg.ticketer = Ticketer::new().unwrap();
} else {
cfg.session_storage = Arc::new(NoServerSessionStorage {});
}

View File

@ -425,7 +425,7 @@ fn make_server_cfg(opts: &Options) -> Arc<ServerConfig<Ring>> {
}
if opts.tickets {
cfg.ticketer = Ticketer::new::<Ring>().unwrap();
cfg.ticketer = Ticketer::new().unwrap();
} else if opts.resumes == 0 {
cfg.session_storage = Arc::new(server::NoServerSessionStorage {});
}

View File

@ -1,5 +1,4 @@
use crate::rand::GetRandomFailed;
use crate::server::ProducesTickets;
use crate::{Error, NamedGroup};
use std::fmt::Debug;
@ -12,9 +11,6 @@ pub trait CryptoProvider: Send + Sync + 'static {
/// KeyExchange operations that are supported by the provider.
type KeyExchange: KeyExchange;
/// Build a ticket generator.
fn ticket_generator() -> Result<Box<dyn ProducesTickets>, GetRandomFailed>;
/// Fill the given buffer with random bytes.
fn fill_random(buf: &mut [u8]) -> Result<(), GetRandomFailed>;
}

View File

@ -9,6 +9,7 @@ use ring::agreement::{agree_ephemeral, EphemeralPrivateKey, UnparsedPublicKey};
use ring::rand::{SecureRandom, SystemRandom};
use std::fmt;
use std::sync::Arc;
/// Default crypto provider.
#[derive(Debug)]
@ -17,20 +18,6 @@ pub struct Ring;
impl CryptoProvider for Ring {
type KeyExchange = KeyExchange;
fn ticket_generator() -> Result<Box<dyn ProducesTickets>, GetRandomFailed> {
let mut key = [0u8; 32];
Self::fill_random(&mut key)?;
let alg = &aead::CHACHA20_POLY1305;
let key = aead::UnboundKey::new(alg, &key).unwrap();
Ok(Box::new(AeadTicketer {
alg,
key: aead::LessSafeKey::new(key),
lifetime: 60 * 60 * 12,
}))
}
fn fill_random(buf: &mut [u8]) -> Result<(), GetRandomFailed> {
SystemRandom::new()
.fill(buf)
@ -38,67 +25,6 @@ impl CryptoProvider for Ring {
}
}
/// This is a `ProducesTickets` implementation which uses
/// any *ring* `aead::Algorithm` to encrypt and authentication
/// the ticket payload. It does not enforce any lifetime
/// constraint.
struct AeadTicketer {
alg: &'static aead::Algorithm,
key: aead::LessSafeKey,
lifetime: u32,
}
impl ProducesTickets for AeadTicketer {
fn enabled(&self) -> bool {
true
}
fn lifetime(&self) -> u32 {
self.lifetime
}
/// Encrypt `message` and return the ciphertext.
fn encrypt(&self, message: &[u8]) -> Option<Vec<u8>> {
// Random nonce, because a counter is a privacy leak.
let mut nonce_buf = [0u8; 12];
Ring::fill_random(&mut nonce_buf).ok()?;
let nonce = aead::Nonce::assume_unique_for_key(nonce_buf);
let aad = ring::aead::Aad::empty();
let mut ciphertext =
Vec::with_capacity(nonce_buf.len() + message.len() + self.key.algorithm().tag_len());
ciphertext.extend(nonce_buf);
ciphertext.extend(message);
self.key
.seal_in_place_separate_tag(nonce, aad, &mut ciphertext[nonce_buf.len()..])
.map(|tag| {
ciphertext.extend(tag.as_ref());
ciphertext
})
.ok()
}
/// Decrypt `ciphertext` and recover the original message.
fn decrypt(&self, ciphertext: &[u8]) -> Option<Vec<u8>> {
// Non-panicking `let (nonce, ciphertext) = ciphertext.split_at(...)`.
let nonce = ciphertext.get(..self.alg.nonce_len())?;
let ciphertext = ciphertext.get(nonce.len()..)?;
// This won't fail since `nonce` has the required length.
let nonce = aead::Nonce::try_assume_unique_for_key(nonce).ok()?;
let mut out = Vec::from(ciphertext);
let plain_len = self
.key
.open_in_place(nonce, aead::Aad::empty(), &mut out)
.ok()?
.len();
out.truncate(plain_len);
Some(out)
}
}
/// An in-progress key exchange. This has the algorithm,
/// our private key, and our public key.
#[derive(Debug)]
@ -224,3 +150,159 @@ pub mod kx_group {
pub use crate::crypto::ring::SECP384R1;
pub use crate::crypto::ring::X25519;
}
/// A concrete, safe ticket creation mechanism.
pub struct Ticketer {}
impl Ticketer {
/// Make the recommended Ticketer. This produces tickets
/// with a 12 hour life and randomly generated keys.
///
/// The encryption mechanism used is Chacha20Poly1305.
pub fn new() -> Result<Arc<dyn ProducesTickets>, Error> {
Ok(Arc::new(crate::ticketer::TicketSwitcher::new(
6 * 60 * 60,
make_ticket_generator,
)?))
}
}
fn make_ticket_generator() -> Result<Box<dyn ProducesTickets>, GetRandomFailed> {
let mut key = [0u8; 32];
Ring::fill_random(&mut key)?;
let alg = &aead::CHACHA20_POLY1305;
let key = aead::UnboundKey::new(alg, &key).unwrap();
Ok(Box::new(AeadTicketer {
alg,
key: aead::LessSafeKey::new(key),
lifetime: 60 * 60 * 12,
}))
}
/// This is a `ProducesTickets` implementation which uses
/// any *ring* `aead::Algorithm` to encrypt and authentication
/// the ticket payload. It does not enforce any lifetime
/// constraint.
struct AeadTicketer {
alg: &'static aead::Algorithm,
key: aead::LessSafeKey,
lifetime: u32,
}
impl ProducesTickets for AeadTicketer {
fn enabled(&self) -> bool {
true
}
fn lifetime(&self) -> u32 {
self.lifetime
}
/// Encrypt `message` and return the ciphertext.
fn encrypt(&self, message: &[u8]) -> Option<Vec<u8>> {
// Random nonce, because a counter is a privacy leak.
let mut nonce_buf = [0u8; 12];
Ring::fill_random(&mut nonce_buf).ok()?;
let nonce = aead::Nonce::assume_unique_for_key(nonce_buf);
let aad = ring::aead::Aad::empty();
let mut ciphertext =
Vec::with_capacity(nonce_buf.len() + message.len() + self.key.algorithm().tag_len());
ciphertext.extend(nonce_buf);
ciphertext.extend(message);
self.key
.seal_in_place_separate_tag(nonce, aad, &mut ciphertext[nonce_buf.len()..])
.map(|tag| {
ciphertext.extend(tag.as_ref());
ciphertext
})
.ok()
}
/// Decrypt `ciphertext` and recover the original message.
fn decrypt(&self, ciphertext: &[u8]) -> Option<Vec<u8>> {
// Non-panicking `let (nonce, ciphertext) = ciphertext.split_at(...)`.
let nonce = ciphertext.get(..self.alg.nonce_len())?;
let ciphertext = ciphertext.get(nonce.len()..)?;
// This won't fail since `nonce` has the required length.
let nonce = aead::Nonce::try_assume_unique_for_key(nonce).ok()?;
let mut out = Vec::from(ciphertext);
let plain_len = self
.key
.open_in_place(nonce, aead::Aad::empty(), &mut out)
.ok()?
.len();
out.truncate(plain_len);
Some(out)
}
}
#[cfg(test)]
use crate::ticketer::TimeBase;
#[test]
fn basic_pairwise_test() {
let t = Ticketer::new().unwrap();
assert!(t.enabled());
let cipher = t.encrypt(b"hello world").unwrap();
let plain = t.decrypt(&cipher).unwrap();
assert_eq!(plain, b"hello world");
}
#[test]
fn ticketswitcher_switching_test() {
let t = Arc::new(crate::ticketer::TicketSwitcher::new(1, make_ticket_generator).unwrap());
let now = TimeBase::now().unwrap();
let cipher1 = t.encrypt(b"ticket 1").unwrap();
assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
{
// Trigger new ticketer
t.maybe_roll(TimeBase(now.0 + std::time::Duration::from_secs(10)));
}
let cipher2 = t.encrypt(b"ticket 2").unwrap();
assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2");
{
// Trigger new ticketer
t.maybe_roll(TimeBase(now.0 + std::time::Duration::from_secs(20)));
}
let cipher3 = t.encrypt(b"ticket 3").unwrap();
assert!(t.decrypt(&cipher1).is_none());
assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2");
assert_eq!(t.decrypt(&cipher3).unwrap(), b"ticket 3");
}
#[cfg(test)]
fn fail_generator() -> Result<Box<dyn ProducesTickets>, GetRandomFailed> {
Err(GetRandomFailed)
}
#[test]
fn ticketswitcher_recover_test() {
let mut t = crate::ticketer::TicketSwitcher::new(1, make_ticket_generator).unwrap();
let now = TimeBase::now().unwrap();
let cipher1 = t.encrypt(b"ticket 1").unwrap();
assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
t.generator = fail_generator;
{
// Failed new ticketer
t.maybe_roll(TimeBase(now.0 + std::time::Duration::from_secs(10)));
}
t.generator = make_ticket_generator;
let cipher2 = t.encrypt(b"ticket 2").unwrap();
assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2");
{
// recover
t.maybe_roll(TimeBase(now.0 + std::time::Duration::from_secs(20)));
}
let cipher3 = t.encrypt(b"ticket 3").unwrap();
assert!(t.decrypt(&cipher1).is_none());
assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2");
assert_eq!(t.decrypt(&cipher3).unwrap(), b"ticket 3");
}

View File

@ -379,6 +379,7 @@ pub use crate::builder::{
};
pub use crate::common_state::{CommonState, IoState, Side};
pub use crate::conn::{Connection, ConnectionCommon, Reader, SideData, Writer};
pub use crate::crypto::ring::Ticketer;
pub use crate::crypto::ring::{SupportedKxGroup, ALL_KX_GROUPS};
pub use crate::enums::{
AlertDescription, CipherSuite, ContentType, HandshakeType, ProtocolVersion, SignatureAlgorithm,
@ -400,7 +401,7 @@ pub use crate::suites::{
#[cfg(feature = "secret_extraction")]
#[cfg_attr(docsrs, doc(cfg(feature = "secret_extraction")))]
pub use crate::suites::{ConnectionTrafficSecrets, ExtractedSecrets};
pub use crate::ticketer::Ticketer;
pub use crate::ticketer::TicketSwitcher;
#[cfg(feature = "tls12")]
pub use crate::tls12::Tls12CipherSuite;
pub use crate::tls13::Tls13CipherSuite;

View File

@ -1,12 +1,9 @@
#[cfg(test)]
use crate::crypto::ring::Ring;
use crate::crypto::CryptoProvider;
use crate::rand;
use crate::server::ProducesTickets;
use crate::Error;
use std::mem;
use std::sync::{Arc, Mutex, MutexGuard};
use std::sync::{Mutex, MutexGuard};
use std::time;
/// The timebase for expiring and rolling tickets and ticketing
@ -14,7 +11,7 @@ use std::time;
///
/// This is guaranteed to be on or after the UNIX epoch.
#[derive(Clone, Copy, Debug)]
pub struct TimeBase(time::Duration);
pub struct TimeBase(pub(crate) time::Duration);
impl TimeBase {
#[inline]
@ -30,7 +27,7 @@ impl TimeBase {
}
}
struct TicketSwitcherState {
pub(crate) struct TicketSwitcherState {
next: Option<Box<dyn ProducesTickets>>,
current: Box<dyn ProducesTickets>,
previous: Option<Box<dyn ProducesTickets>>,
@ -40,18 +37,21 @@ struct TicketSwitcherState {
/// A ticketer that has a 'current' sub-ticketer and a single
/// 'previous' ticketer. It creates a new ticketer every so
/// often, demoting the current ticketer.
struct TicketSwitcher {
generator: fn() -> Result<Box<dyn ProducesTickets>, rand::GetRandomFailed>,
pub struct TicketSwitcher {
pub(crate) generator: fn() -> Result<Box<dyn ProducesTickets>, rand::GetRandomFailed>,
lifetime: u32,
state: Mutex<TicketSwitcherState>,
}
impl TicketSwitcher {
/// Creates a new `TicketSwitcher`, which rotates through sub-ticketers
/// based on the passage of time.
///
/// `lifetime` is in seconds, and is how long the current ticketer
/// is used to generate new tickets. Tickets are accepted for no
/// longer than twice this duration. `generator` produces a new
/// `ProducesTickets` implementation.
fn new(
pub fn new(
lifetime: u32,
generator: fn() -> Result<Box<dyn ProducesTickets>, rand::GetRandomFailed>,
) -> Result<Self, Error> {
@ -79,7 +79,7 @@ impl TicketSwitcher {
///
/// For efficiency, this is also responsible for locking the state mutex
/// and returning the mutexguard.
fn maybe_roll(&self, now: TimeBase) -> Option<MutexGuard<TicketSwitcherState>> {
pub(crate) fn maybe_roll(&self, now: TimeBase) -> Option<MutexGuard<TicketSwitcherState>> {
// The code below aims to make switching as efficient as possible
// in the common case that the generator never fails. To achieve this
// we run the following steps:
@ -181,81 +181,3 @@ impl ProducesTickets for TicketSwitcher {
})
}
}
/// A concrete, safe ticket creation mechanism.
pub struct Ticketer {}
impl Ticketer {
/// Make the recommended Ticketer. This produces tickets
/// with a 12 hour life and randomly generated keys.
///
/// The encryption mechanism used in Chacha20Poly1305.
pub fn new<C: CryptoProvider>() -> Result<Arc<dyn ProducesTickets>, Error> {
Ok(Arc::new(TicketSwitcher::new(
6 * 60 * 60,
C::ticket_generator,
)?))
}
}
#[test]
fn basic_pairwise_test() {
let t = Ticketer::new::<Ring>().unwrap();
assert!(t.enabled());
let cipher = t.encrypt(b"hello world").unwrap();
let plain = t.decrypt(&cipher).unwrap();
assert_eq!(plain, b"hello world");
}
#[test]
fn ticketswitcher_switching_test() {
let t = Arc::new(TicketSwitcher::new(1, Ring::ticket_generator).unwrap());
let now = TimeBase::now().unwrap();
let cipher1 = t.encrypt(b"ticket 1").unwrap();
assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
{
// Trigger new ticketer
t.maybe_roll(TimeBase(now.0 + std::time::Duration::from_secs(10)));
}
let cipher2 = t.encrypt(b"ticket 2").unwrap();
assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2");
{
// Trigger new ticketer
t.maybe_roll(TimeBase(now.0 + std::time::Duration::from_secs(20)));
}
let cipher3 = t.encrypt(b"ticket 3").unwrap();
assert!(t.decrypt(&cipher1).is_none());
assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2");
assert_eq!(t.decrypt(&cipher3).unwrap(), b"ticket 3");
}
#[cfg(test)]
fn fail_generator() -> Result<Box<dyn ProducesTickets>, rand::GetRandomFailed> {
Err(rand::GetRandomFailed)
}
#[test]
fn ticketswitcher_recover_test() {
let mut t = TicketSwitcher::new(1, Ring::ticket_generator).unwrap();
let now = TimeBase::now().unwrap();
let cipher1 = t.encrypt(b"ticket 1").unwrap();
assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
t.generator = fail_generator;
{
// Failed new ticketer
t.maybe_roll(TimeBase(now.0 + std::time::Duration::from_secs(10)));
}
t.generator = Ring::ticket_generator;
let cipher2 = t.encrypt(b"ticket 2").unwrap();
assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2");
{
// recover
t.maybe_roll(TimeBase(now.0 + std::time::Duration::from_secs(20)));
}
let cipher3 = t.encrypt(b"ticket 3").unwrap();
assert!(t.decrypt(&cipher1).is_none());
assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2");
assert_eq!(t.decrypt(&cipher3).unwrap(), b"ticket 3");
}

View File

@ -3058,7 +3058,7 @@ fn tls13_stateless_resumption() {
let client_config = Arc::new(client_config);
let mut server_config = make_server_config(kt);
server_config.ticketer = rustls::Ticketer::new::<Ring>().unwrap();
server_config.ticketer = rustls::Ticketer::new().unwrap();
let storage = Arc::new(ServerStorage::new());
server_config.session_storage = storage.clone();
let server_config = Arc::new(server_config);