From f1e7bd4e54cb24e7030d99d92e2d9bfbf1834e44 Mon Sep 17 00:00:00 2001 From: Benjamin Saunders Date: Wed, 16 May 2018 00:23:30 -0700 Subject: [PATCH] session resumption --- examples/connect.rs | 4 +-- quicr/examples/client.rs | 44 ++++++++++++++++++++----- quicr/src/lib.rs | 8 ++--- src/endpoint.rs | 70 +++++++++++++++++++++++++++++++++------- src/lib.rs | 2 +- tests/tests.rs | 8 ++--- 6 files changed, 105 insertions(+), 31 deletions(-) diff --git a/examples/connect.rs b/examples/connect.rs index bdb56f82a..4e0d6ac54 100644 --- a/examples/connect.rs +++ b/examples/connect.rs @@ -12,7 +12,7 @@ use std::io::{self, Write}; use std::str; use failure::Error; -use quicr::{Endpoint, Config, Io, Timer, Event, Directionality, ReadError}; +use quicr::{Endpoint, Config, Io, Timer, Event, Directionality, ReadError, ClientConfig}; use slog::{Logger, Drain}; fn main() { @@ -77,7 +77,7 @@ impl Context { fn run(&mut self) -> Result<()> { let epoch = Instant::now(); - let c = self.client.connect(self.remote, Some(self.remote_host.as_bytes())); + let c = self.client.connect(self.remote, ClientConfig { server_name: Some(&self.remote_host), ..ClientConfig::default() }); let mut time = 0; let mut buf = Vec::new(); let mut sent = 0; diff --git a/quicr/examples/client.rs b/quicr/examples/client.rs index e9f8e68e7..934c8188e 100644 --- a/quicr/examples/client.rs +++ b/quicr/examples/client.rs @@ -14,6 +14,7 @@ use std::net::ToSocketAddrs; use std::io::{self, Write}; use std::time::{Instant, Duration}; use std::path::PathBuf; +use std::fs; use futures::Future; use tokio::runtime::current_thread::Runtime; @@ -31,10 +32,16 @@ struct Opt { /// file to log TLS keys to for debugging #[structopt(parse(from_os_str), long = "keylog")] keylog: Option, + url: Url, + /// whether to accept invalid (e.g. self-signed) TLS certificates #[structopt(long = "accept-insecure-certs")] accept_insecure_certs: bool, + + /// file to read/write session tickets to + #[structopt(long = "session-cache", parse(from_os_str))] + session_cache: Option, } fn main() { @@ -50,27 +57,48 @@ fn main() { ::std::process::exit(code); } -fn run(log: Logger, options: Opt) -> Result<()> { +fn run(log: Logger, mut options: Opt) -> Result<()> { let url = options.url; let remote = url.with_default_port(|_| Ok(4433))?.to_socket_addrs()?.next().ok_or(format_err!("couldn't resolve to an address"))?; let mut runtime = Runtime::new()?; + let mut config = quicr::Config { + protocols: vec![b"hq-11"[..].into()], + keylog: options.keylog, + ..quicr::Config::default() + }; + + let ticket; + if let Some(path) = options.session_cache.take() { + ticket = match fs::read(&path) { + Ok(x) => Some(x), + Err(ref e) if e.kind() == io::ErrorKind::NotFound => None, + Err(e) => { return Err(e.into()); } + }; + config.session_cache = Some(Box::new(move |_, _, data| { + fs::write(&path, data).unwrap(); + })); + } else { + ticket = None; + } + let mut builder = quicr::Endpoint::new(); builder.logger(log.clone()) - .config(quicr::Config { - protocols: vec![b"hq-11"[..].into()], - keylog: options.keylog, - accept_insecure_certs: options.accept_insecure_certs, - ..quicr::Config::default() - }); + .config(config); let (endpoint, driver, _) = builder.bind("[::]:0")?; runtime.spawn(driver.map_err(|e| eprintln!("IO error: {}", e))); let request = format!("GET {}\r\n", url.path()); let start = Instant::now(); runtime.block_on( - endpoint.connect(&remote, url.host_str().map(|x| x.as_bytes())) + endpoint.connect(&remote, + quicr::ClientConfig { + server_name: Some(url.host_str().ok_or(format_err!("URL missing host"))?), + accept_insecure_certs: options.accept_insecure_certs, + session_ticket: ticket.as_ref().map(|x| &x[..]), + ..quicr::ClientConfig::default() + }) .map_err(|e| format_err!("failed to connect: {}", e)) .and_then(move |(conn, _)| { eprintln!("connected at {}", duration_secs(&start.elapsed())); diff --git a/quicr/src/lib.rs b/quicr/src/lib.rs index 29d9bbe1c..a4e37746e 100644 --- a/quicr/src/lib.rs +++ b/quicr/src/lib.rs @@ -89,7 +89,7 @@ use bytes::Bytes; use quicr::{Directionality, StreamId, ConnectionHandle, Side, CertConfig}; -pub use quicr::{Config, ConnectionError, ConnectionId, ListenKeys}; +pub use quicr::{Config, ClientConfig, ConnectionError, ConnectionId, ListenKeys}; /// Errors that can occur during the construction of an `Endpoint`. #[derive(Debug, Fail)] @@ -307,13 +307,11 @@ impl Endpoint { }} /// Connect to a remote endpoint. - /// - /// `hostname` is used by the remote endpoint for disambiguation if `addr` hosts multiple services. - pub fn connect(&self, addr: &SocketAddr, hostname: Option<&[u8]>) -> impl Future { + pub fn connect(&self, addr: &SocketAddr, config: ClientConfig) -> impl Future { let (send, recv) = oneshot::channel(); let conn = { let mut endpoint = self.0.borrow_mut(); - let conn = endpoint.inner.connect(normalize(*addr), hostname); + let conn = endpoint.inner.connect(normalize(*addr), config); endpoint.pending.insert(conn, Pending::new(Some(send))); conn }; diff --git a/src/endpoint.rs b/src/endpoint.rs index 6cdf62c93..ede664ff4 100644 --- a/src/endpoint.rs +++ b/src/endpoint.rs @@ -10,9 +10,10 @@ use rand::distributions::Sample; use slab::Slab; use openssl::{self, ex_data}; use openssl::ssl::{self, SslContext, SslMethod, SslOptions, SslVersion, SslMode, Ssl, SslStream, HandshakeError, MidHandshakeSslStream, - SslStreamBuilder, SslAlert, SslRef}; + SslStreamBuilder, SslAlert, SslRef, SslSession}; use openssl::pkey::{PKeyRef, Private}; use openssl::x509::X509Ref; +use openssl::x509::verify::X509CheckFlags; use openssl::hash::MessageDigest; use openssl::symm::{Cipher, encrypt_aead, decrypt_aead}; use blake2::Blake2b; @@ -92,17 +93,14 @@ pub struct Config { /// If empty, application-layer protocol negotiation will not be preformed. pub protocols: Vec>, - /// Whether to accept inauthentic or unverifiable peer certificates. - /// - /// Turning this off exposes clients to man-in-the-middle attacks in the same manner as an unencrypted TCP - /// connection, but allows them to connect to servers that are using self-signed certificates. - pub accept_insecure_certs: bool, - /// Path to write NSS SSLKEYLOGFILE-compatible key log. /// /// Enabling this compromises security by committing secret information to disk. Useful for debugging communications /// when using tools like Wireshark. pub keylog: Option, + + /// Function to store session tickets transmitted by the server for fast resumption. + pub session_cache: Option, &SocketAddrV6, &[u8]) + Send + Sync + 'static>>, } pub struct CertConfig<'a> { @@ -141,12 +139,37 @@ impl Default for Config { loss_reduction_factor: 0x8000, // 1/2 protocols: Vec::new(), - accept_insecure_certs: false, keylog: None, + session_cache: None, } } } +pub struct ClientConfig<'a> { + /// The name of the server the client intends to connect to. + /// + /// Used for both certificate validation, and for disambiguating between multiple domains hosted by the same IP + /// address (using SNI). + pub server_name: Option<&'a str>, + + /// A ticket to resume a previous session faster than performing a full handshake. + pub session_ticket: Option<&'a [u8]>, + + /// Whether to accept inauthentic or unverifiable peer certificates. + /// + /// Turning this off exposes clients to man-in-the-middle attacks in the same manner as an unencrypted TCP + /// connection, but allows them to connect to servers that are using self-signed certificates. + pub accept_insecure_certs: bool, +} + +impl<'a> Default for ClientConfig<'a> { + fn default() -> Self { Self { + server_name: None, + session_ticket: None, + accept_insecure_certs: false, + }} +} + /// The main entry point to the library /// /// This object performs no I/O whatsoever. Instead, it generates a stream of I/O operations for a backend to perform @@ -338,6 +361,16 @@ impl Endpoint { }); } + if config.session_cache.is_some() { + let config = config.clone(); + tls.set_session_cache_mode(ssl::SslSessionCacheMode::CLIENT | ssl::SslSessionCacheMode::NO_INTERNAL_STORE); + tls.set_new_session_callback(move |tls, session| { + let conn = tls.ex_data(*CONNECTION_INFO_INDEX).expect("connection info unset"); + let name = tls.servername(ssl::NameType::HOST_NAME); + (config.session_cache.as_ref().unwrap())(name, &conn.remote, &session.to_der().expect("failed to serialize session ticket")); + }); + } + let tls = tls.build(); Ok(Self { @@ -482,15 +515,30 @@ impl Endpoint { } /// Initiate a connection - pub fn connect(&mut self, remote: SocketAddrV6, hostname: Option<&[u8]>) -> ConnectionHandle { + pub fn connect(&mut self, remote: SocketAddrV6, config: ClientConfig) -> ConnectionHandle { let local_id = ConnectionId::random(&mut self.rng, LOCAL_ID_LEN as u8); let remote_id = ConnectionId::random(&mut self.rng, MAX_CID_SIZE as u8); trace!(self.log, "initial dcid"; "value" => %remote_id); let conn = self.add_connection(remote_id.clone(), local_id, remote_id, remote, Side::Client); let mut tls = Ssl::new(&self.tls).unwrap(); // Is this fallible? - if !self.config.accept_insecure_certs { tls.set_verify(ssl::SslVerifyMode::PEER); } + if !config.accept_insecure_certs { + tls.set_verify(ssl::SslVerifyMode::PEER); + let param = tls.param_mut(); + if let Some(name) = config.server_name { + param.set_hostflags(X509CheckFlags::NO_PARTIAL_WILDCARDS); + match name.parse() { + Ok(ip) => { param.set_ip(ip).expect("failed to inform TLS of remote ip"); } + Err(_) => { param.set_host(name).expect("failed to inform TLS of remote hostname"); } + } + } + } tls.set_ex_data(*CONNECTION_INFO_INDEX, ConnectionInfo { id: self.connections[conn.0].local_id.clone(), remote }); - if let Some(hostname) = hostname { tls.set_hostname(str::from_utf8(hostname).expect("malformed hostname")).unwrap(); } + if let Some(name) = config.server_name { tls.set_hostname(name).unwrap(); } + if let Some(session) = config.session_ticket { + if let Err(e) = SslSession::from_der(session).and_then(|x| unsafe { tls.set_session(&x) }) { + error!(self.log, "failed to set TLS session"; "reason" => %e); + } + } let mut tls = match tls.connect(MemoryStream::new()) { Ok(_) => unreachable!(), Err(HandshakeError::WouldBlock(tls)) => tls, diff --git a/src/lib.rs b/src/lib.rs index db9e35d23..895854c6d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,7 +37,7 @@ pub use frame::{ApplicationClose, ConnectionClose}; mod endpoint; pub use endpoint::{Endpoint, Config, CertConfig, ListenKeys, ConnectionHandle, Event, Io, Timer, ConnectionError, ReadError, WriteError, - ConnectionId, EndpointError}; + ConnectionId, EndpointError, ClientConfig}; mod transport_error; pub use transport_error::Error as TransportError; diff --git a/tests/tests.rs b/tests/tests.rs index 7d4ddbf8c..f22b5b17b 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -79,7 +79,7 @@ struct Pair { impl Default for Pair { fn default() -> Self { Pair::new(Config { max_remote_uni_streams: 32, max_remote_bi_streams: 32, ..Config::default() }, - Config { accept_insecure_certs: true, ..Config::default() }) + Config::default()) } } @@ -145,7 +145,7 @@ impl Pair { fn connect(&mut self) -> (ConnectionHandle, ConnectionHandle) { info!(self.log, "connecting"); - let client_conn = self.client.connect(self.server.addr, None); + let client_conn = self.client.connect(self.server.addr, ClientConfig { accept_insecure_certs: true, ..ClientConfig::default() }); self.drive(); let server_conn = if let Some(c) = self.server.accept() { c } else { panic!("server didn't connect"); }; assert_matches!(self.client.poll(), Some((conn, Event::Connected { .. })) if conn == client_conn); @@ -367,7 +367,7 @@ fn stop_stream() { fn reject_self_signed_cert() { let mut pair = Pair::new(Config::default(), Config::default()); info!(pair.log, "connecting"); - let client_conn = pair.client.connect(pair.server.addr, None); + let client_conn = pair.client.connect(pair.server.addr, ClientConfig::default()); pair.drive(); assert_matches!(pair.client.poll(), Some((conn, Event::ConnectionLost { reason: ConnectionError::TransportError { @@ -398,7 +398,7 @@ fn congestion() { fn high_latency_handshake() { let mut pair = Pair::default(); pair.latency = 200 * 1000; - let client_conn = pair.client.connect(pair.server.addr, None); + let client_conn = pair.client.connect(pair.server.addr, ClientConfig { accept_insecure_certs: true, ..ClientConfig::default() }); pair.drive(); let server_conn = if let Some(c) = pair.server.accept() { c } else { panic!("server didn't connect"); }; assert_matches!(pair.client.poll(), Some((conn, Event::Connected { .. })) if conn == client_conn);