[fuchsia-hyper] Add RFC8446 Appendix C.4 session cache

TLS 1.3 makes some large changes around how sessions are resumed. In
particular, tickets are no longer sent from the server in plaintext.
While the client does send these to the server in plaintext, section
4.6.1 of the RFC suggests servers send multiple tickets to the client.

One reason for this is to prevent tracking of client sessions. Because
client session tickets are sent in plain text, it is possible to know
which sessions are related. Clients that never reuse a session ticket
cannot be tracked in this fashion.

Although the default session storage implementation doesn't reuse
tickets (it uses the newest ticket supplied), it has some undesirable
properties:

 * It evicts tickets in an arbitrary order. When the capacity of the
 cache is full, the newest item may be the one evicted. This may return
 the original item, which diminishes privacy.
 * It only stores a single ticket, which prevents effective racing of
 TLS connections in a Happy Eyeballs implementation.
 * When a negotiation attempt fails, the ticket will be retried.

This change adds a `StoresClientSessions` implementation that solves
these issues. The cache is sized and LRU-managed. Because keyspace is
shared between TLS 1.3 and TLS 1.2 resumption information, insertion of
one where the other is present evicts all the rest. It is expected to be
unlikely to run into cases where a SN is served by native 1.3 and 1.2
servers, and that this will continue to be unlikelier.

The TLS 1.3 cache stores 6 tickets per subject name; this value is
derived from Appendix C.4, which suggests that this is an expected
concurrency level for HTTP/1.1 clients. This is also a reasonable number
of tickets to store from a Happy Eyeballs perspective, allowing
consumption of all 6 tickets within the "magical" 2 second window
defined-in and oft-cited-from [Miller68].

This change makes the new C.4 compliant cache the default for
fuchsia_hyper clients, and updates the httpsdate client library to use
this cache as well.

[Miller68]: https://dl.acm.org/doi/10.1145/1476589.1476628

Test: New unit tests, OTA tests, CQ, bogo test in upstream.
Fixed: 68871

Change-Id: Iffa78459c14a2e66f8be38f5c229bfce71afe8a9
diff --git a/src/lib/fuchsia-hyper/BUILD.gn b/src/lib/fuchsia-hyper/BUILD.gn
index 91da568..093d3e7 100644
--- a/src/lib/fuchsia-hyper/BUILD.gn
+++ b/src/lib/fuchsia-hyper/BUILD.gn
@@ -14,22 +14,29 @@
   deps = [
     "//src/lib/fuchsia-async",
     "//third_party/rust_crates:futures",
+    "//third_party/rust_crates:hex",
     "//third_party/rust_crates:http",
     "//third_party/rust_crates:hyper",
     "//third_party/rust_crates:hyper-rustls",
     "//third_party/rust_crates:log",
+    "//third_party/rust_crates:lru-cache-v0_1_2",
+    "//third_party/rust_crates:parking_lot",
     "//third_party/rust_crates:pin-project",
     "//third_party/rust_crates:rustls",
     "//third_party/rust_crates:tokio",
   ]
+  test_deps = [
+    "//third_party/rust_crates:anyhow",
+    "//third_party/rust_crates:matches",
+    "//third_party/rust_crates:rand",
+    "//third_party/rust_crates:webpki",
+  ]
   if (is_host) {
     deps += [
       "//third_party/rust_crates:async-std",
       "//third_party/rust_crates:rustls-native-certs-v0_3_0",
       "//third_party/rust_crates:tower-service-v0_3_0",
-      "//third_party/rust_crates:webpki",
     ]
-    test_deps = [ "//third_party/rust_crates:anyhow" ]
   } else {
     deps += [
       "//garnet/lib/rust/webpki-roots-fuchsia",
@@ -42,19 +49,19 @@
       "//third_party/rust_crates:libc",
       "//third_party/rust_crates:socket2",
     ]
-    test_deps = [
+    test_deps += [
       "//sdk/fidl/fuchsia.net.interfaces:fuchsia.net.interfaces-rustc",
       "//sdk/fidl/fuchsia.netstack:fuchsia.netstack-rustc",
       "//src/lib/fidl/rust/fidl",
       "//src/lib/fuchsia",
-      "//third_party/rust_crates:anyhow",
-      "//third_party/rust_crates:matches",
-      "//third_party/rust_crates:parking_lot",
       "//third_party/rust_crates:test-case",
     ]
   }
 
-  sources = [ "src/lib.rs" ]
+  sources = [
+    "src/lib.rs",
+    "src/session_cache.rs",
+  ]
   if (is_host) {
     sources += [ "src/not_fuchsia.rs" ]
   } else {
diff --git a/src/lib/fuchsia-hyper/OWNERS b/src/lib/fuchsia-hyper/OWNERS
index 2944119..890abad 100644
--- a/src/lib/fuchsia-hyper/OWNERS
+++ b/src/lib/fuchsia-hyper/OWNERS
@@ -1,5 +1,6 @@
 adamperry@google.com
 brunodalbo@google.com
+dhobsd@google.com
 etryzelaar@google.com
 raggi@google.com
 tamird@google.com
diff --git a/src/lib/fuchsia-hyper/src/lib.rs b/src/lib/fuchsia-hyper/src/lib.rs
index 7d36007..6d52901 100644
--- a/src/lib/fuchsia-hyper/src/lib.rs
+++ b/src/lib/fuchsia-hyper/src/lib.rs
@@ -34,6 +34,9 @@
 #[cfg(target_os = "fuchsia")]
 pub use crate::fuchsia::*;
 
+mod session_cache;
+pub use session_cache::C4CapableSessionCache;
+
 #[cfg(target_os = "fuchsia")]
 mod happy_eyeballs;
 
@@ -138,7 +141,7 @@
 
 /// Returns a new Fuchsia-compatible hyper client for making HTTP and HTTPS requests.
 pub fn new_https_client_from_tcp_options(tcp_options: TcpOptions) -> HttpsClient {
-    let mut tls = rustls::ClientConfig::new();
+    let mut tls = new_rustls_client_config();
     configure_cert_store(&mut tls);
     new_https_client_dangerous(tls, tcp_options)
 }
@@ -147,3 +150,13 @@
 pub fn new_https_client() -> HttpsClient {
     new_https_client_from_tcp_options(std::default::Default::default())
 }
+
+/// Returns a rustls::ClientConfig for further construction with improved session cache and without
+/// a configured certificate store.
+pub fn new_rustls_client_config() -> rustls::ClientConfig {
+    let mut config = rustls::ClientConfig::new();
+    // The default depth for the ClientSessionMemoryCache in the default ClientConfig is 32; this
+    // value is assumed to be a sufficient default here as well.
+    config.set_persistence(session_cache::C4CapableSessionCache::new(32));
+    config
+}
diff --git a/src/lib/fuchsia-hyper/src/session_cache.rs b/src/lib/fuchsia-hyper/src/session_cache.rs
new file mode 100644
index 0000000..5ee8c9b
--- /dev/null
+++ b/src/lib/fuchsia-hyper/src/session_cache.rs
@@ -0,0 +1,309 @@
+// 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 _ = 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 _ = 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 _ = 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);
+    }
+}
diff --git a/src/sys/time/lib/httpdate-hyper/src/lib.rs b/src/sys/time/lib/httpdate-hyper/src/lib.rs
index 9d22558..978682d 100644
--- a/src/sys/time/lib/httpdate-hyper/src/lib.rs
+++ b/src/sys/time/lib/httpdate-hyper/src/lib.rs
@@ -123,7 +123,7 @@
         // we need to use a non-standard verifier, `RecordingVerifier`, to allow
         // us to defer trust evaluation until after we've parsed the response.
         let verifier = Arc::new(RecordingVerifier::default());
-        let mut config = rustls::ClientConfig::new();
+        let mut config = fuchsia_hyper::new_rustls_client_config();
         config.root_store.add_server_trust_anchors(trust_anchors);
         config
             .dangerous()