blob: f1aeb9a1924bb9c37b364b1a852a4ce50a6e21fe [file] [log] [blame]
// Copyright 2021 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use {
lru_cache::LruCache,
parking_lot::Mutex,
rustls::{
internal::msgs::{
codec::{Codec, Reader},
enums::{NamedGroup, ProtocolVersion},
persist::ClientSessionValue,
},
StoresClientSessions,
},
std::{collections::VecDeque, fmt::Debug, sync::Arc},
};
// BoundedStack implements a stack that produces and consumes from the back, and evicts from the
// front when full.
#[derive(Debug)]
struct BoundedStack {
jar: VecDeque<Vec<u8>>,
size: usize,
}
impl BoundedStack {
fn new(size: usize) -> Self {
BoundedStack { jar: VecDeque::with_capacity(size), size }
}
fn push(&mut self, val: Vec<u8>) {
if self.jar.len() == self.size {
let _ = self.jar.pop_front();
}
self.jar.push_back(val)
}
fn pop(&mut self) -> Option<Vec<u8>> {
self.jar.pop_back()
}
}
enum CacheEntry {
C4(BoundedStack),
RFC5077(Vec<u8>),
KX(Vec<u8>),
}
/// `C4CapableSessionCache` implements `StoresClientSessions` while supporting TLS 1.3 Client
/// Tracking Prevention as described in [RFC8446 Appendix C.4].
///
/// The `StoresClientSessions` API is overloaded in that it stores TLS 1.3 tickets, TLS 1.2
/// sessions, and TLS 1.3 key exchange hints. While the key exchange hints have guaranteed
/// different keys than tickets and sessions, the latter two share keyspace.
///
/// This is worked around by removing any overlapping keys. This is safe because this API provides
/// ownership of all values resolved through it, and they also must be refreshed via this API.
///
/// This cache is sized and items are evicted per LRU policy.
///
/// [RFC8446 Appendix C.4]: https://tools.ietf.org/html/rfc8446#appendix-C.4
pub struct C4CapableSessionCache {
cache: Mutex<LruCache<Vec<u8>, CacheEntry>>,
}
impl C4CapableSessionCache {
// Value chosen based on suggestion in C.4 that an HTTP/1.1 client might open 6 concurrent
// connections. This value is also amenable to exhaustion in Happy Eyeballs races within 2
// seconds (using default connection intervals).
const TLS13_TICKET_COUNT: usize = 6;
/// Create a new `C4CapableSessionCache` constrained by the supplied size. Note that this size
/// includes TLS 1.3 tickets, TLS 1.2 sessions, and TLS 1.3 key exchange hints.
pub fn new(size: usize) -> Arc<C4CapableSessionCache> {
debug_assert!(size > 0);
Arc::new(C4CapableSessionCache { cache: Mutex::new(LruCache::new(size)) })
}
}
impl StoresClientSessions for C4CapableSessionCache {
// Our `put` implementation evicts occupants on collision. This removes the need to have any
// logic in `get` other than unwrapping `CacheEntry` types.
fn put(&self, key: Vec<u8>, value: Vec<u8>) -> bool {
let csv = ClientSessionValue::read(&mut Reader::init(&value));
let mut locked_cache = self.cache.lock();
match csv.map(|v| v.version) {
Some(ProtocolVersion::TLSv1_3) => {
// We need to do a lookup for TLS 1.3 tickets to determine whether to insert to an
// existing C.4-compatible storage or whether we need to create a new one and evict
// the current occupant.
match locked_cache.get_mut(&key) {
Some(CacheEntry::C4(entry)) => {
let () = entry.push(value);
}
// Create a new LIFO for TLS 1.3 tickets at this cache entry, dropping whatever
// was there before.
Some(CacheEntry::RFC5077(_)) | Some(CacheEntry::KX(_)) | None => {
let mut c4 = BoundedStack::new(C4CapableSessionCache::TLS13_TICKET_COUNT);
let () = c4.push(value);
let _: Option<CacheEntry> = locked_cache.insert(key, CacheEntry::C4(c4));
}
}
true
}
// TLS versions prior to TLS 1.3 may resume with RFC5077 semantics.
Some(ProtocolVersion::TLSv1_2)
| Some(ProtocolVersion::TLSv1_1)
| Some(ProtocolVersion::TLSv1_0) => {
let _: Option<CacheEntry> = locked_cache.insert(key, CacheEntry::RFC5077(value));
true
}
None => {
// This didn't match a ProtocolVersion, probably because it isn't actually a
// ClientSessionValue. Try to read it as a NamedGroup and, if successful, insert a
// kx hint.
match NamedGroup::read_bytes(&value) {
Some(_) => {
let _: Option<CacheEntry> = locked_cache.insert(key, CacheEntry::KX(value));
true
}
None => false,
}
}
// These should never happen, but there's no need to panic about it.
Some(ProtocolVersion::SSLv2)
| Some(ProtocolVersion::SSLv3)
| Some(ProtocolVersion::Unknown(_)) => false,
}
}
// Because `put` evicts on key collision, `get` may directly return whatever occupant it finds.
fn get(&self, key: &[u8]) -> Option<Vec<u8>> {
let mut locked_cache = self.cache.lock();
return locked_cache.get_mut(key).and_then(|entry| match entry {
CacheEntry::C4(entry) => entry.pop(),
CacheEntry::RFC5077(entry) | CacheEntry::KX(entry) => Some(entry.clone()),
});
}
}
#[cfg(test)]
mod test {
use {
crate::C4CapableSessionCache,
matches::assert_matches,
rand::{thread_rng, Rng},
rustls::{
internal::msgs::{
codec::{Codec, Reader},
enums::{CipherSuite, ProtocolVersion},
handshake::SessionID,
persist::{ClientSessionKey, ClientSessionValue},
},
StoresClientSessions,
},
webpki::DNSNameRef,
};
enum KeyType {
ForKX,
ForTLS12,
ForTLS13,
}
// Helper function to make a key/value pair to store in the session cache.
fn make_kv_pair(dns_name: &str, key_type: KeyType) -> (Vec<u8>, Vec<u8>) {
let sn = DNSNameRef::try_from_ascii_str(dns_name).expect("to parse SN");
let csk = match key_type {
KeyType::ForKX => ClientSessionKey::hint_for_dns_name(sn).get_encoding(),
_ => ClientSessionKey::session_for_dns_name(sn).get_encoding(),
};
let mut session_id = [0u8; 32];
thread_rng().fill(&mut session_id[..]);
let csv = match key_type {
KeyType::ForKX => vec![0u8, 29u8],
KeyType::ForTLS12 => ClientSessionValue::new(
ProtocolVersion::TLSv1_2,
CipherSuite::TLS_NULL_WITH_NULL_NULL,
&SessionID::new(&session_id),
Vec::new(),
Vec::new(),
)
.get_encoding(),
KeyType::ForTLS13 => ClientSessionValue::new(
ProtocolVersion::TLSv1_3,
CipherSuite::TLS_NULL_WITH_NULL_NULL,
&SessionID::new(&session_id),
Vec::new(),
Vec::new(),
)
.get_encoding(),
};
(csk, csv)
}
// This test checks that we can insert a session key for TLS 1.3, read the same value out, and
// that this causes the cache to be empty for that key.
#[test]
fn test_tls13key_session_roundtrip() {
let (csk, csv) = make_kv_pair("example.com", KeyType::ForTLS13);
let csv_in = ClientSessionValue::read(&mut Reader::init(&csv))
.expect("to parse initial ClientSessionValue");
let cache = C4CapableSessionCache::new(1);
assert_eq!(cache.put(csk.clone(), csv), true);
let v0 = cache.get(&csk).expect("to get a value for the key");
let csv_out = ClientSessionValue::read(&mut Reader::init(&v0))
.expect("to parse retrieved ClientSessionValue");
assert_eq!(csv_in.get_encoding(), csv_out.get_encoding());
assert_eq!(true, cache.get(&csk).is_none());
}
// This test checks the various eviction semantics of the session cache. In particular, we're
// looking to see that a full cache evicts with LRU semantics, we can round-trip items we
// insert, and that key collision is resolved via eviction.
#[test]
fn test_eviction() {
let (tls13_key, tls13_val) = make_kv_pair("example.com", KeyType::ForTLS13);
let tls13_csv_in = ClientSessionValue::read(&mut Reader::init(&tls13_val))
.expect("to parse initial TLS 1.3 ClientSessionValue");
let (tls12_key, tls12_val) = make_kv_pair("example.com", KeyType::ForTLS12);
let tls12_csv_in = ClientSessionValue::read(&mut Reader::init(&tls12_val))
.expect("to parse initial TLS 1.2 ClientSessionValue");
let (kx_key, kx_val) = make_kv_pair("example.com", KeyType::ForKX);
let cache = C4CapableSessionCache::new(3);
// These keys should be equivalent.
assert_eq!(tls13_key, tls12_key);
// Test we can read out the TLS 1.2 value we put in.
assert_eq!(cache.put(tls12_key.clone(), tls12_val), true);
let v1 = cache.get(&tls12_key).expect("to get a TLS 1.2 value for the key");
let tls12_csv_out = ClientSessionValue::read(&mut Reader::init(&v1))
.expect("to parse retrieved TLS 1.2 ClientSessionValue");
assert_eq!(tls12_csv_in.get_encoding(), tls12_csv_out.get_encoding());
// Test that inserting the TLS 1.3 value round-trips, and that we read it out even if we
// use the key supplied for the TLS 1.2 value.
assert_eq!(cache.put(tls13_key.clone(), tls13_val), true);
let v0 = cache.get(&tls12_key).expect("to get a TLS 1.3 value for the key");
let tls13_csv_out = ClientSessionValue::read(&mut Reader::init(&v0))
.expect("to parse retrieved TLS 1.3 ClientSessionValue");
assert_eq!(tls13_csv_in.get_encoding(), tls13_csv_out.get_encoding());
// Test that insertion of the KX hint doesn't evict the TLS 1.3 key.
assert_eq!(cache.put(kx_key.clone(), kx_val.clone()), true);
let tls13_csv_out = ClientSessionValue::read(&mut Reader::init(&v0))
.expect("to parse retrieved TLS 1.3 ClientSessionValue");
assert_eq!(tls13_csv_in.get_encoding(), tls13_csv_out.get_encoding());
let v3 = cache.get(&kx_key).expect("to get KX hint");
assert_eq!(kx_val, v3);
// Test that inserting more than three elements starts to evict the least-recently-used
// ones.
let (tls12p_key, tls12p_val) = make_kv_pair("example.net", KeyType::ForTLS12);
let (kxp_key, kxp_val) = make_kv_pair("example.net", KeyType::ForKX);
assert_eq!(cache.put(tls12p_key.clone(), tls12p_val), true);
assert_eq!(cache.put(kxp_key.clone(), kxp_val.clone()), true);
// The TLS 1.3 entry was the fourth last thing used, so it's gone.
assert_matches!(cache.get(&tls12_key), None);
// The KX hint, and KX' and TLS12' are all in the cache.
assert_matches!(cache.get(&kx_key), Some(_));
assert_matches!(cache.get(&tls12p_key), Some(_));
assert_matches!(cache.get(&kxp_key), Some(_));
}
// Ensure that the BoundedStack returns values in LIFO order, and that it stores exactly
// C4CapableSessionCache::TLS13_TICKET_COUNT items. This is tested via the
// C4CapableSessionCache API to ensure those semantics are visible to rustls.
#[test]
fn test_tls13_cache_depth() {
let mut tickets: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
let cache = C4CapableSessionCache::new(1);
// First, overfill the container.
for _i in 0..=C4CapableSessionCache::TLS13_TICKET_COUNT {
let (k, v) = make_kv_pair("example.com", KeyType::ForTLS13);
assert_eq!(cache.put(k.clone(), v.clone()), true);
tickets.push((k.clone(), v.clone()));
}
// Read out the first TLS13_TICKET_COUNT values.
for _i in 0..C4CapableSessionCache::TLS13_TICKET_COUNT {
let (k_in, v_in) = tickets.pop().expect("to retrieve a previously-enqueued ticket");
let v_out =
cache.get(&k_in.clone()).expect("to read a previously-enqueued ticket from cache");
assert_eq!(v_in, v_out);
}
// Ensure that we didn't consume all initially inserted items, but there's also nothing
// left in the cache.
let (k, _) = tickets.pop().expect("one value still in ticket vec");
assert_matches!(cache.get(&k.clone()), None);
}
}