From 8ece9c450804e2ae0f062cfd8573219933b5364a Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Tue, 19 Jun 2018 14:26:42 -1000 Subject: [PATCH] Proxy: Add TLS client infrastructure. (#1158) Move TLS cipher suite configuration to tls::config. Use the same configuration to act as a client and a server. Signed-off-by: Brian Smith --- proxy/benches/record.rs | 19 ++-- proxy/src/bind.rs | 16 +++- proxy/src/conditional.rs | 55 +++++++++++ proxy/src/connection.rs | 71 +++++++++----- proxy/src/ctx/mod.rs | 27 ++++-- proxy/src/ctx/transport.rs | 32 ++++--- proxy/src/inbound.rs | 13 ++- proxy/src/lib.rs | 9 +- proxy/src/telemetry/metrics/labels.rs | 11 ++- proxy/src/telemetry/metrics/mod.rs | 9 +- proxy/src/telemetry/metrics/record.rs | 21 ++-- proxy/src/transparency/tcp.rs | 16 ++-- proxy/src/transport/connect.rs | 12 ++- proxy/src/transport/tls/cert_resolver.rs | 53 ++++++---- proxy/src/transport/tls/config.rs | 117 +++++++++++++++++++++-- proxy/src/transport/tls/connection.rs | 63 +++++++++--- proxy/src/transport/tls/mod.rs | 15 ++- 17 files changed, 416 insertions(+), 143 deletions(-) create mode 100644 proxy/src/conditional.rs diff --git a/proxy/benches/record.rs b/proxy/benches/record.rs index 457fe3e0f..38a99eeca 100644 --- a/proxy/benches/record.rs +++ b/proxy/benches/record.rs @@ -7,6 +7,7 @@ extern crate test; use conduit_proxy::{ ctx, + conditional::Conditional, control::destination, telemetry::{ event, @@ -18,28 +19,26 @@ use std::{ fmt, net::SocketAddr, sync::Arc, - time::{Duration, Instant, SystemTime}, + time::{Duration, Instant}, }; use test::Bencher; +use conduit_proxy::tls; const REQUESTS: usize = 100; +const TLS_DISABLED: Conditional<(), tls::ReasonForNoTls> = + Conditional::None(tls::ReasonForNoTls::Disabled); + fn addr() -> SocketAddr { ([1, 2, 3, 4], 5678).into() } fn process() -> Arc { - Arc::new(ctx::Process { - scheduled_namespace: "test".into(), - start_time: SystemTime::now(), - }) + ctx::Process::test("test") } fn server(proxy: &Arc) -> Arc { - ctx::transport::Server::new( - &proxy, &addr(), &addr(), &Some(addr()), - ctx::transport::TlsStatus::Disabled, - ) + ctx::transport::Server::new(&proxy, &addr(), &addr(), &Some(addr()), TLS_DISABLED) } fn client(proxy: &Arc, labels: L) -> Arc @@ -51,7 +50,7 @@ where &proxy, &addr(), destination::Metadata::new(metrics::DstLabels::new(labels), None), - ctx::transport::TlsStatus::Disabled, + TLS_DISABLED, ) } diff --git a/proxy/src/bind.rs b/proxy/src/bind.rs index 2cfc214ea..2120bf69d 100644 --- a/proxy/src/bind.rs +++ b/proxy/src/bind.rs @@ -16,6 +16,8 @@ use ctx; use telemetry::{self, sensor}; use transparency::{self, HttpBody, h1}; use transport; +use tls; +use ctx::transport::TlsStatus; /// Binds a `Service` from a `SocketAddr`. /// @@ -205,18 +207,20 @@ where fn bind_stack(&self, ep: &Endpoint, protocol: &Protocol) -> Stack { debug!("bind_stack endpoint={:?}, protocol={:?}", ep, protocol); let addr = ep.address(); + + let tls = tls::current_connection_config(ep.tls_identity(), + &self.ctx.tls_client_config_watch()); + let client_ctx = ctx::transport::Client::new( &self.ctx, &addr, ep.metadata().clone(), - // TODO: when we can use TLS for client connections, indicate - // whether or not the connection was TLS here. - ctx::transport::TlsStatus::Disabled, + TlsStatus::from(&tls), ); // Map a socket address to a connection. let connect = self.sensors.connect( - transport::Connect::new(addr, ep.tls_identity().cloned()), + transport::Connect::new(addr, tls), &client_ctx, ); @@ -266,7 +270,9 @@ where impl Bind { - pub fn with_protocol(self, protocol: Protocol) -> BindProtocol { + pub fn with_protocol(self, protocol: Protocol) + -> BindProtocol + { BindProtocol { bind: self, protocol, diff --git a/proxy/src/conditional.rs b/proxy/src/conditional.rs new file mode 100644 index 000000000..d2715dbd9 --- /dev/null +++ b/proxy/src/conditional.rs @@ -0,0 +1,55 @@ +use std; + +/// Like `std::option::Option` but `None` carries a reason why the value +/// isn't available. +#[derive(Clone, Debug)] +pub enum Conditional +where + C: Clone + std::fmt::Debug, + R: Clone + std::fmt::Debug, +{ + Some(C), + None(R), +} + +impl Copy for Conditional +where + C: Copy + Clone + std::fmt::Debug, + R: Copy + Clone + std::fmt::Debug, +{ +} + +impl Eq for Conditional +where + C: Eq + Clone + std::fmt::Debug, + R: Eq + Clone + std::fmt::Debug, +{ +} + +impl PartialEq for Conditional +where + C: PartialEq + Clone + std::fmt::Debug, + R: PartialEq + Clone + std::fmt::Debug, +{ + fn eq(&self, other: &Conditional) -> bool { + use self::Conditional::*; + match (self, other) { + (Some(a), Some(b)) => a.eq(b), + (None(a), None(b)) => a.eq(b), + _ => false, + } + } +} + +impl std::hash::Hash for Conditional +where + C: std::hash::Hash + Clone + std::fmt::Debug, + R: std::hash::Hash + Clone + std::fmt::Debug, +{ + fn hash(&self, state: &mut H) { + match self { + Conditional::Some(c) => c.hash(state), + Conditional::None(r) => r.hash(state), + } + } +} diff --git a/proxy/src/connection.rs b/proxy/src/connection.rs index 2aae77895..698006177 100644 --- a/proxy/src/connection.rs +++ b/proxy/src/connection.rs @@ -9,32 +9,34 @@ use tokio::{ net::{TcpListener, TcpStream, ConnectFuture}, reactor::Handle, }; - +use conditional::Conditional; use ctx::transport::TlsStatus; use config::Addr; use transport::{GetOriginalDst, Io, tls}; -pub type PlaintextSocket = TcpStream; - pub struct BoundPort { inner: std::net::TcpListener, local_addr: SocketAddr, } /// Initiates a client connection to the given address. -pub fn connect(addr: &SocketAddr) -> Connecting { - Connecting { - inner: PlaintextSocket::connect(addr), - // TODO: when we can open TLS client connections, this is where we will - // indicate that for telemetry. - tls_status: TlsStatus::Disabled, +pub fn connect(addr: &SocketAddr, + tls: tls::ConditionalConnectionConfig) + -> Connecting +{ + Connecting::Plaintext { + connect: TcpStream::connect(addr), + tls: Some(tls), } } /// A socket that is in the process of connecting. -pub struct Connecting { - inner: ConnectFuture, - tls_status: TlsStatus, +pub enum Connecting { + Plaintext { + connect: ConnectFuture, + tls: Option>, + }, + UpgradeToTls(tls::UpgradeClientToTls), } /// Abstracts a plaintext socket vs. a TLS decorated one. @@ -138,7 +140,7 @@ impl BoundPort { // libraries don't have the necessary API for that, so just // do it here. set_nodelay_or_warn(&socket); - let tls_status = if let Some((_identity, config_watch)) = &tls { + let why_no_tls = if let Some((_identity, config_watch)) = &tls { // TODO: use `identity` to differentiate between TLS // that the proxy should terminate vs. TLS that should // be passed through. @@ -150,12 +152,12 @@ impl BoundPort { return Either::A(f); } else { // No valid TLS configuration. - TlsStatus::NoConfig + tls::ReasonForNoTls::NoConfig } } else { - TlsStatus::Disabled + tls::ReasonForNoTls::Disabled }; - let conn = Connection::new(socket, tls_status); + let conn = Connection::plain(socket, why_no_tls); Either::B(future::ok((conn, remote_addr))) }) .then(|r| { @@ -181,28 +183,47 @@ impl Future for Connecting { type Error = io::Error; fn poll(&mut self) -> Poll { - let socket = try_ready!(self.inner.poll()); - set_nodelay_or_warn(&socket); - Ok(Async::Ready(Connection::new(socket, self.tls_status))) + loop { + *self = match self { + Connecting::Plaintext { connect, tls } => { + let plaintext_stream = try_ready!(connect.poll()); + set_nodelay_or_warn(&plaintext_stream); + match tls.take().expect("Polled after ready") { + Conditional::Some(config) => { + let upgrade_to_tls = tls::Connection::connect( + plaintext_stream, &config.identity, config.config); + Connecting::UpgradeToTls(upgrade_to_tls) + }, + Conditional::None(why) => { + return Ok(Async::Ready(Connection::plain(plaintext_stream, why))); + }, + } + }, + Connecting::UpgradeToTls(upgrading) => { + let tls_stream = try_ready!(upgrading.poll()); + return Ok(Async::Ready(Connection::tls(tls_stream))); + }, + }; + } } } // ===== impl Connection ===== impl Connection { - fn new(io: I, tls_status: TlsStatus) -> Self { + fn plain(io: TcpStream, why_no_tls: tls::ReasonForNoTls) -> Self { Connection { io: Box::new(io), peek_buf: BytesMut::new(), - tls_status, + tls_status: Conditional::None(why_no_tls), } } - fn tls(tls: tls::Connection) -> Self { - Connection { + fn tls(tls: tls::Connection) -> Self { + Connection { io: Box::new(tls), peek_buf: BytesMut::new(), - tls_status: TlsStatus::Success, + tls_status: Conditional::Some(()), } } @@ -312,7 +333,7 @@ impl Future for PeekFuture { // Misc. -fn set_nodelay_or_warn(socket: &PlaintextSocket) { +fn set_nodelay_or_warn(socket: &TcpStream) { if let Err(e) = socket.set_nodelay(true) { warn!( "could not set TCP_NODELAY on {:?}/{:?}: {}", diff --git a/proxy/src/ctx/mod.rs b/proxy/src/ctx/mod.rs index 35033eca3..8dde49599 100644 --- a/proxy/src/ctx/mod.rs +++ b/proxy/src/ctx/mod.rs @@ -10,16 +10,20 @@ use config; use std::time::SystemTime; use std::sync::Arc; +use transport::tls; + pub mod http; pub mod transport; /// Describes a single running proxy instance. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug)] pub struct Process { /// Identifies the Kubernetes namespace in which this proxy is process. pub scheduled_namespace: String, pub start_time: SystemTime, + + tls_client_config: tls::ClientConfigWatch, } /// Indicates the orientation of traffic, relative to a sidecar proxy. @@ -29,27 +33,30 @@ pub struct Process { /// local instance. /// - The _outbound_ proxy receives traffic from the local instance and forwards it to a /// remove service. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug)] pub enum Proxy { Inbound(Arc), Outbound(Arc), } impl Process { - #[cfg(test)] + // Test-only, but we can't use `#[cfg(test)]` because it is used by the + // benchmarks pub fn test(ns: &str) -> Arc { Arc::new(Self { scheduled_namespace: ns.into(), start_time: SystemTime::now(), + tls_client_config: tls::ClientConfig::no_tls(), }) } /// Construct a new `Process` from environment variables. - pub fn new(config: &config::Config) -> Arc { + pub fn new(config: &config::Config, tls_client_config: tls::ClientConfigWatch) -> Arc { let start_time = SystemTime::now(); Arc::new(Self { scheduled_namespace: config.namespaces.pod.clone(), start_time, + tls_client_config, }) } } @@ -73,6 +80,12 @@ impl Proxy { pub fn is_outbound(&self) -> bool { !self.is_inbound() } + + pub fn tls_client_config_watch(&self) -> &tls::ClientConfigWatch { + match self { + Proxy::Inbound(process) | Proxy::Outbound(process) => &process.tls_client_config + } + } } #[cfg(test)] @@ -82,7 +95,6 @@ pub mod test_util { fmt, net::SocketAddr, sync::Arc, - time::SystemTime, }; use ctx; @@ -94,10 +106,7 @@ pub mod test_util { } pub fn process() -> Arc { - Arc::new(ctx::Process { - scheduled_namespace: "test".into(), - start_time: SystemTime::now(), - }) + ctx::Process::test("test") } pub fn server( diff --git a/proxy/src/ctx/transport.rs b/proxy/src/ctx/transport.rs index 719edc2e6..b3b0be16b 100644 --- a/proxy/src/ctx/transport.rs +++ b/proxy/src/ctx/transport.rs @@ -1,10 +1,13 @@ -use std::net::{IpAddr, SocketAddr}; -use std::sync::Arc; - +use std::{ + self, + net::{IpAddr, SocketAddr}, + sync::Arc, +}; use ctx; use control::destination; use telemetry::metrics::DstLabels; use transport::tls; +use conditional::Conditional; #[derive(Debug)] pub enum Ctx { @@ -33,19 +36,20 @@ pub struct Client { /// Identifies whether or not a connection was secured with TLS, /// and, if it was not, the reason why. -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] -pub enum TlsStatus { - /// The TLS handshake was successful. - Success, - /// TLS was not enabled for this connection. - Disabled, - /// TLS was enabled for this connection, but we have no valid - /// config. - NoConfig, - // TODO: When the proxy falls back to plaintext on handshake - // failures, we'll want to add a variant for that here as well. +pub type TlsStatus = Conditional<(), tls::ReasonForNoTls>; + +impl TlsStatus { + pub fn from(c: &Conditional) -> Self + where C: Clone + std::fmt::Debug + { + match c { + Conditional::Some(_) => Conditional::Some(()), + Conditional::None(r) => Conditional::None(*r), + } + } } + impl Ctx { pub fn proxy(&self) -> &Arc { match *self { diff --git a/proxy/src/inbound.rs b/proxy/src/inbound.rs index c116c33bc..49b1c0a90 100644 --- a/proxy/src/inbound.rs +++ b/proxy/src/inbound.rs @@ -109,6 +109,8 @@ mod tests { use super::Inbound; use bind::{self, Bind, Host}; use ctx; + use conditional::Conditional; + use tls; fn new_inbound(default: Option, ctx: &Arc) -> Inbound<()> { let bind = Bind::new().with_ctx(ctx.clone()); @@ -123,6 +125,9 @@ mod tests { (addr, protocol) } + const TLS_DISABLED: Conditional<(), tls::ReasonForNoTls> = + Conditional::None(tls::ReasonForNoTls::Disabled); + quickcheck! { fn recognize_orig_dst( orig_dst: net::SocketAddr, @@ -134,9 +139,7 @@ mod tests { let inbound = new_inbound(None, &ctx); let srv_ctx = ctx::transport::Server::new( - &ctx, &local, &remote, &Some(orig_dst), - ctx::transport::TlsStatus::Disabled, - ); + &ctx, &local, &remote, &Some(orig_dst), TLS_DISABLED); let rec = srv_ctx.orig_dst_if_not_local().map(make_key_http1); @@ -163,7 +166,7 @@ mod tests { &local, &remote, &None, - ctx::transport::TlsStatus::Disabled, + TLS_DISABLED, )); inbound.recognize(&req) == default.map(make_key_http1) @@ -195,7 +198,7 @@ mod tests { &local, &remote, &Some(local), - ctx::transport::TlsStatus::Disabled, + TLS_DISABLED, )); inbound.recognize(&req) == default.map(make_key_http1) diff --git a/proxy/src/lib.rs b/proxy/src/lib.rs index e8a5e225e..04b891edf 100644 --- a/proxy/src/lib.rs +++ b/proxy/src/lib.rs @@ -70,6 +70,7 @@ pub mod app; mod bind; pub mod config; mod connection; +pub mod conditional; pub mod control; pub mod convert; pub mod ctx; @@ -179,7 +180,10 @@ where where F: Future + Send + 'static, { - let process_ctx = ctx::Process::new(&self.config); + let (tls_client_config, tls_server_config, tls_cfg_bg) = + tls::watch_for_config_changes(self.config.tls_settings.as_ref()); + + let process_ctx = ctx::Process::new(&self.config, tls_client_config); let Main { config, @@ -237,9 +241,6 @@ where let bind = Bind::new().with_sensors(sensors.clone()); - let (_tls_client_config, tls_server_config, tls_cfg_bg) = - tls::watch_for_config_changes(config.tls_settings.as_ref()); - // Setup the public listener. This will listen on a publicly accessible // address and listen for inbound connections that should be forwarded // to the managed application (private destination). diff --git a/proxy/src/telemetry/metrics/labels.rs b/proxy/src/telemetry/metrics/labels.rs index cf5967efe..83610b664 100644 --- a/proxy/src/telemetry/metrics/labels.rs +++ b/proxy/src/telemetry/metrics/labels.rs @@ -7,6 +7,8 @@ use http; use ctx; use telemetry::event; +use tls; +use conditional::Conditional; #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub struct RequestLabels { @@ -376,11 +378,12 @@ impl fmt::Display for TransportCloseLabels { // TODO: There's got to be a nicer way to handle this. impl fmt::Display for ctx::transport::TlsStatus { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use ctx::transport::TlsStatus; match *self { - TlsStatus::Disabled => Ok(()), - TlsStatus::NoConfig => f.pad(",tls=\"no_config\""), - TlsStatus::Success => f.pad(",tls=\"true\""), + Conditional::Some(()) => f.pad(",tls=\"true\""), + Conditional::None(tls::ReasonForNoTls::NoConfig) => f.pad(",tls=\"no_config\""), + Conditional::None(tls::ReasonForNoTls::Disabled) | + Conditional::None(tls::ReasonForNoTls::NotImplementedForNonHttp) | + Conditional::None(tls::ReasonForNoTls::NotImplementedForControlPlane) => Ok(()), } } } diff --git a/proxy/src/telemetry/metrics/mod.rs b/proxy/src/telemetry/metrics/mod.rs index bbd8a56e1..f66fabb5b 100644 --- a/proxy/src/telemetry/metrics/mod.rs +++ b/proxy/src/telemetry/metrics/mod.rs @@ -278,6 +278,11 @@ mod tests { use ctx::test_util::*; use telemetry::event; use super::*; + use conditional::Conditional; + use tls; + + const TLS_DISABLED: Conditional<(), tls::ReasonForNoTls> = + Conditional::None(tls::ReasonForNoTls::Disabled); fn mock_route( root: &mut Root, @@ -285,7 +290,7 @@ mod tests { server: &Arc, team: &str ) { - let client = client(&proxy, vec![("team", team)], ctx::transport::TlsStatus::Disabled); + let client = client(&proxy, vec![("team", team)], TLS_DISABLED); let (req, rsp) = request("http://nba.com", &server, &client); let client_transport = Arc::new(ctx::transport::Ctx::Client(client)); @@ -310,7 +315,7 @@ mod tests { let process = process(); let proxy = ctx::Proxy::outbound(&process); - let server = server(&proxy, ctx::transport::TlsStatus::Disabled); + let server = server(&proxy, TLS_DISABLED); let server_transport = Arc::new(ctx::transport::Ctx::Server(server.clone())); let mut root = Root::default(); diff --git a/proxy/src/telemetry/metrics/record.rs b/proxy/src/telemetry/metrics/record.rs index 07834bdc7..8b1edeb1e 100644 --- a/proxy/src/telemetry/metrics/record.rs +++ b/proxy/src/telemetry/metrics/record.rs @@ -95,7 +95,12 @@ mod test { }; use ctx::{self, test_util::*, transport::TlsStatus}; use std::time::{Duration, Instant}; + use conditional::Conditional; + use tls; + const TLS_ENABLED: Conditional<(), tls::ReasonForNoTls> = Conditional::Some(()); + const TLS_DISABLED: Conditional<(), tls::ReasonForNoTls> = + Conditional::None(tls::ReasonForNoTls::Disabled); fn test_record_response_end_outbound(client_tls: TlsStatus, server_tls: TlsStatus) { let process = process(); @@ -325,41 +330,41 @@ mod test { #[test] fn record_one_conn_request_outbound_client_tls() { - test_record_one_conn_request_outbound(TlsStatus::Success, TlsStatus::Disabled) + test_record_one_conn_request_outbound(TLS_ENABLED, TLS_DISABLED) } #[test] fn record_one_conn_request_outbound_server_tls() { - test_record_one_conn_request_outbound(TlsStatus::Disabled, TlsStatus::Success) + test_record_one_conn_request_outbound(TLS_DISABLED, TLS_ENABLED) } #[test] fn record_one_conn_request_outbound_both_tls() { - test_record_one_conn_request_outbound(TlsStatus::Success, TlsStatus::Success) + test_record_one_conn_request_outbound(TLS_ENABLED, TLS_ENABLED) } #[test] fn record_one_conn_request_outbound_no_tls() { - test_record_one_conn_request_outbound(TlsStatus::Disabled, TlsStatus::Disabled) + test_record_one_conn_request_outbound(TLS_DISABLED, TLS_DISABLED) } #[test] fn record_response_end_outbound_client_tls() { - test_record_response_end_outbound(TlsStatus::Success, TlsStatus::Disabled) + test_record_response_end_outbound(TLS_ENABLED, TLS_DISABLED) } #[test] fn record_response_end_outbound_server_tls() { - test_record_response_end_outbound(TlsStatus::Disabled, TlsStatus::Success) + test_record_response_end_outbound(TLS_DISABLED, TLS_ENABLED) } #[test] fn record_response_end_outbound_both_tls() { - test_record_response_end_outbound(TlsStatus::Success, TlsStatus::Success) + test_record_response_end_outbound(TLS_ENABLED, TLS_ENABLED) } #[test] fn record_response_end_outbound_no_tls() { - test_record_response_end_outbound(TlsStatus::Disabled, TlsStatus::Disabled) + test_record_response_end_outbound(TLS_DISABLED, TLS_DISABLED) } } diff --git a/proxy/src/transparency/tcp.rs b/proxy/src/transparency/tcp.rs index f6aea0efd..e9dc785d0 100644 --- a/proxy/src/transparency/tcp.rs +++ b/proxy/src/transparency/tcp.rs @@ -7,15 +7,16 @@ use futures::{future, Async, Future, Poll}; use tokio_connect::Connect; use tokio::io::{AsyncRead, AsyncWrite}; +use conditional::Conditional; use control::destination; use ctx::transport::{ Client as ClientCtx, Server as ServerCtx, - TlsStatus, }; use telemetry::Sensors; use timeout::Timeout; -use transport; +use transport::{self, tls}; +use ctx::transport::TlsStatus; /// TCP Server Proxy #[derive(Debug, Clone)] @@ -59,19 +60,16 @@ impl Proxy { return future::Either::B(future::ok(())); }; + let tls = Conditional::None(tls::ReasonForNoTls::NotImplementedForNonHttp); // TODO + let client_ctx = ClientCtx::new( &srv_ctx.proxy, &orig_dst, destination::Metadata::no_metadata(), - // A raw TCP client connection may be or may not be TLS traffic, - // but the `TlsStatus` field indicates whether _the proxy_ is - // responsible for the encryption, so set this to "Disabled". - // XXX: Should raw TCP connections have a different TLS status - // from HTTP connections for which TLS is disabled? - TlsStatus::Disabled, + TlsStatus::from(&tls), ); let c = Timeout::new( - transport::Connect::new(orig_dst, None), // No TLS. + transport::Connect::new(orig_dst, tls), self.connect_timeout, ); let connect = self.sensors.connect(c, &client_ctx); diff --git a/proxy/src/transport/connect.rs b/proxy/src/transport/connect.rs index c5583c01e..875029d40 100644 --- a/proxy/src/transport/connect.rs +++ b/proxy/src/transport/connect.rs @@ -11,10 +11,12 @@ use connection; use convert::TryFrom; use dns; use transport::tls; +use conditional::Conditional; #[derive(Debug, Clone)] pub struct Connect { addr: SocketAddr, + tls: tls::ConditionalConnectionConfig, } #[derive(Clone, Debug)] @@ -102,12 +104,11 @@ impl Connect { /// Returns a `Connect` to `addr`. pub fn new( addr: SocketAddr, - tls_identity: Option, + tls: tls::ConditionalConnectionConfig, ) -> Self { - // TODO: this is currently unused. - let _ = tls_identity; Self { addr, + tls, } } } @@ -118,7 +119,7 @@ impl tokio_connect::Connect for Connect { type Future = connection::Connecting; fn connect(&self) -> Self::Future { - connection::connect(&self.addr) + connection::connect(&self.addr, self.tls.clone()) } } @@ -144,6 +145,7 @@ impl tokio_connect::Connect for LookupAddressAndConnect { fn connect(&self) -> Self::Future { let port = self.host_and_port.port; let host = self.host_and_port.host.clone(); + let tls = Conditional::None(tls::ReasonForNoTls::NotImplementedForControlPlane); // TODO let c = self.dns_resolver .resolve_one_ip(&self.host_and_port.host) .map_err(|_| { @@ -153,7 +155,7 @@ impl tokio_connect::Connect for LookupAddressAndConnect { info!("DNS resolved {:?} to {}", host, ip_addr); let addr = SocketAddr::from((ip_addr, port)); trace!("connect {}", addr); - connection::connect(&addr) + connection::connect(&addr, tls) }); Box::new(c) } diff --git a/proxy/src/transport/tls/cert_resolver.rs b/proxy/src/transport/tls/cert_resolver.rs index 52d56bbd6..16c6d5c24 100755 --- a/proxy/src/transport/tls/cert_resolver.rs +++ b/proxy/src/transport/tls/cert_resolver.rs @@ -12,6 +12,10 @@ use super::{ }; use ring::{self, rand, signature}; +/// Manages the use of the private key and certificate. +/// +/// Authentication is symmetric with respect to the client/server roles, so the +/// same certificate and private key is used for both roles. pub struct CertResolver { certified_key: rustls::sign::CertifiedKey, } @@ -48,7 +52,8 @@ impl CertResolver { private_key: untrusted::Input) -> Result { - let private_key = signature::key_pair_from_pkcs8(SIGNATURE_ALG_RING_SIGNING, private_key) + let private_key = + signature::key_pair_from_pkcs8(config::SIGNATURE_ALG_RING_SIGNING, private_key) .map_err(|ring::error::Unspecified| config::Error::InvalidPrivateKey)?; let signer = Signer { private_key: Arc::new(private_key) }; @@ -57,6 +62,17 @@ impl CertResolver { cert_chain, Arc::new(Box::new(signing_key))); Ok(Self { certified_key }) } + + fn resolve_(&self, sigschemes: &[rustls::SignatureScheme]) -> Option + { + if !sigschemes.contains(&config::SIGNATURE_ALG_RUSTLS_SCHEME) { + debug!("signature scheme not supported -> no certificate"); + return None; + } + + Some(self.certified_key.clone()) + } + } fn parse_end_entity_cert<'a>(cert_chain: &'a[rustls::Certificate]) @@ -68,6 +84,20 @@ fn parse_end_entity_cert<'a>(cert_chain: &'a[rustls::Certificate]) webpki::EndEntityCert::from(untrusted::Input::from(cert)) } +impl rustls::ResolvesClientCert for CertResolver { + fn resolve(&self, _acceptable_issuers: &[&[u8]], sigschemes: &[rustls::SignatureScheme]) + -> Option + { + // Conduit's server side doesn't send the list of acceptable issuers so + // don't bother looking at `_acceptable_issuers`. + self.resolve_(sigschemes) + } + + fn has_certs(&self) -> bool { + true + } +} + impl rustls::ResolvesServerCert for CertResolver { fn resolve(&self, server_name: Option, sigschemes: &[rustls::SignatureScheme]) -> Option { @@ -78,11 +108,6 @@ impl rustls::ResolvesServerCert for CertResolver { return None; }; - if !sigschemes.contains(&SIGNATURE_ALG_RUSTLS_SCHEME) { - debug!("signature scheme not supported -> no certificate"); - return None; - } - // Verify that our certificate is valid for the given SNI name. if let Err(err) = parse_end_entity_cert(&self.certified_key.cert) .and_then(|cert| cert.verify_is_valid_for_dns_name(server_name)) { @@ -90,7 +115,7 @@ impl rustls::ResolvesServerCert for CertResolver { return None; } - Some(self.certified_key.clone()) + self.resolve_(sigschemes) } } @@ -98,7 +123,7 @@ impl rustls::sign::SigningKey for SigningKey { fn choose_scheme(&self, offered: &[rustls::SignatureScheme]) -> Option> { - if offered.contains(&SIGNATURE_ALG_RUSTLS_SCHEME) { + if offered.contains(&config::SIGNATURE_ALG_RUSTLS_SCHEME) { Some(Box::new(self.signer.clone())) } else { None @@ -106,7 +131,7 @@ impl rustls::sign::SigningKey for SigningKey { } fn algorithm(&self) -> rustls::internal::msgs::enums::SignatureAlgorithm { - SIGNATURE_ALG_RUSTLS_ALGORITHM + config::SIGNATURE_ALG_RUSTLS_ALGORITHM } } @@ -120,14 +145,6 @@ impl rustls::sign::Signer for Signer { } fn get_scheme(&self) -> rustls::SignatureScheme { - SIGNATURE_ALG_RUSTLS_SCHEME + config::SIGNATURE_ALG_RUSTLS_SCHEME } } - -// Keep these in sync. -static SIGNATURE_ALG_RING_SIGNING: &signature::SigningAlgorithm = - &signature::ECDSA_P256_SHA256_ASN1_SIGNING; -const SIGNATURE_ALG_RUSTLS_SCHEME: rustls::SignatureScheme = - rustls::SignatureScheme::ECDSA_NISTP256_SHA256; -const SIGNATURE_ALG_RUSTLS_ALGORITHM: rustls::internal::msgs::enums::SignatureAlgorithm = - rustls::internal::msgs::enums::SignatureAlgorithm::ECDSA; diff --git a/proxy/src/transport/tls/config.rs b/proxy/src/transport/tls/config.rs index fd7ecc097..acfbbf9ef 100644 --- a/proxy/src/transport/tls/config.rs +++ b/proxy/src/transport/tls/config.rs @@ -1,4 +1,5 @@ use std::{ + self, fs::File, io::{self, Cursor, Read}, path::PathBuf, @@ -14,9 +15,10 @@ use super::{ untrusted, webpki, }; - +use conditional::Conditional; use futures::{future, stream, Future, Stream}; use futures_watch::Watch; +use ring::signature; /// Not-yet-validated settings that are used for both TLS clients and TLS /// servers. @@ -53,11 +55,18 @@ struct CommonConfig { cert_resolver: Arc, } -/// Validated configuration for TLS clients. -/// -/// TODO: Fill this in with the actual configuration. -#[derive(Clone, Debug)] -pub struct ClientConfig(Arc<()>); + +/// Validated configuration for TLS servers. +#[derive(Clone)] +pub struct ClientConfig(pub(super) Arc); + +/// XXX: `rustls::ClientConfig` doesn't implement `Debug` yet. +impl std::fmt::Debug for ClientConfig { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + f.debug_struct("ClientConfig") + .finish() + } +} /// Validated configuration for TLS servers. #[derive(Clone)] @@ -66,6 +75,33 @@ pub struct ServerConfig(pub(super) Arc); pub type ClientConfigWatch = Watch>; pub type ServerConfigWatch = Watch>; +/// The configuration in effect for a client (`ClientConfig`) or server +/// (`ServerConfig`) TLS connection. +#[derive(Clone, Debug)] +pub struct ConnectionConfig where C: Clone + std::fmt::Debug { + pub identity: Identity, + pub config: C, +} + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum ReasonForNoTls { + /// TLS is disabled. + Disabled, + + /// TLS was enabled but the configuration isn't available (yet). + NoConfig, + + /// TLS isn't implemented for the connection between the proxy and the + /// control plane yet. + NotImplementedForControlPlane, + + /// TLS is only enabled for HTTP (HTTPS) right now. + NotImplementedForNonHttp, + +} + +pub type ConditionalConnectionConfig = Conditional, ReasonForNoTls>; + #[derive(Debug)] pub enum Error { Io(PathBuf, io::Error), @@ -206,7 +242,7 @@ pub fn watch_for_config_changes(settings: Option<&CommonSettings>) (client_store, server_store), |(mut client_store, mut server_store), ref config| { client_store - .store(Some(ClientConfig(Arc::new(())))) + .store(Some(ClientConfig::from(config))) .map_err(|_| trace!("all client config watchers dropped"))?; server_store .store(Some(ServerConfig::from(config))) @@ -225,6 +261,38 @@ pub fn watch_for_config_changes(settings: Option<&CommonSettings>) (client_watch, server_watch, Box::new(f)) } +impl ClientConfig { + fn from(common: &CommonConfig) -> Self { + let mut config = rustls::ClientConfig::new(); + set_common_settings(&mut config.versions); + + // XXX: Rustls's built-in verifiers don't let us tweak things as fully + // as we'd like (e.g. controlling the set of trusted signature + // algorithms), but they provide good enough defaults for now. + // TODO: lock down the verification further. + // TODO: Change Rustls's API to Avoid needing to clone `root_cert_store`. + config.root_store = common.root_cert_store.clone(); + + // Disable session resumption for the time-being until resumption is + // more tested. + config.enable_tickets = false; + + // Enable client authentication if and only if we were configured for + // it. + config.client_auth_cert_resolver = common.cert_resolver.clone(); + + ClientConfig(Arc::new(config)) + } + + /// Some tests aren't set up to do TLS yet, but we require a + /// `ClientConfigWatch`. We can't use `#[cfg(test)]` here because the + /// benchmarks use this. + pub fn no_tls() -> ClientConfigWatch { + let (watch, _) = Watch::new(None); + watch + } +} + impl ServerConfig { fn from(common: &CommonConfig) -> Self { // Ask TLS clients for a certificate and accept any certificate issued @@ -250,6 +318,24 @@ impl ServerConfig { } } +pub fn current_connection_config(identity: Option<&Identity>, watch: &Watch>) + -> ConditionalConnectionConfig where C: Clone + std::fmt::Debug +{ + match identity { + Some(identity) => { + match *watch.borrow() { + Some(ref config) => + Conditional::Some(ConnectionConfig { + identity: identity.clone(), + config: config.clone() + }), + None => Conditional::None(ReasonForNoTls::NoConfig), + } + }, + None => Conditional::None(ReasonForNoTls::Disabled), + } +} + fn load_file_contents(path: &PathBuf) -> Result, Error> { fn load_file(path: &PathBuf) -> Result, io::Error> { let mut result = Vec::new(); @@ -279,12 +365,24 @@ fn load_file_contents(path: &PathBuf) -> Result, Error> { fn set_common_settings(versions: &mut Vec) { // Only enable TLS 1.2 until TLS 1.3 is stable. *versions = vec![rustls::ProtocolVersion::TLSv1_2] + + // XXX: Rustls doesn't provide a good way to customize the cipher suite + // support, so just use its defaults, which are still pretty good. + // TODO: Expand Rustls's API to allow us to clearly whitelist the cipher + // suites we want to enable. } +// Keep these in sync. +pub(super) static SIGNATURE_ALG_RING_SIGNING: &signature::SigningAlgorithm = + &signature::ECDSA_P256_SHA256_ASN1_SIGNING; +pub(super) const SIGNATURE_ALG_RUSTLS_SCHEME: rustls::SignatureScheme = + rustls::SignatureScheme::ECDSA_NISTP256_SHA256; +pub(super) const SIGNATURE_ALG_RUSTLS_ALGORITHM: rustls::internal::msgs::enums::SignatureAlgorithm = + rustls::internal::msgs::enums::SignatureAlgorithm::ECDSA; #[cfg(test)] mod tests { - use tls::{CommonSettings, Identity, ServerConfig}; + use tls::{ClientConfig, CommonSettings, Identity, ServerConfig}; use super::{CommonConfig, Error}; use config::Namespaces; use std::path::PathBuf; @@ -314,7 +412,7 @@ mod tests { } #[test] - fn can_construct_server_config_from_valid_settings() { + fn can_construct_client_and_server_config_from_valid_settings() { let settings = settings(&Strings { pod_name: "foo", pod_ns: "ns1", @@ -324,6 +422,7 @@ mod tests { private_key: "foo-ns1-ca1.p8", }); let config = CommonConfig::load_from_disk(&settings).unwrap(); + let _: ClientConfig = ClientConfig::from(&config); // Infallible. let _: ServerConfig = ServerConfig::from(&config); // Infallible. } diff --git a/proxy/src/transport/tls/connection.rs b/proxy/src/transport/tls/connection.rs index 2fc38b057..d38b760f4 100644 --- a/proxy/src/transport/tls/connection.rs +++ b/proxy/src/transport/tls/connection.rs @@ -9,39 +9,74 @@ use tokio::net::TcpStream; use transport::{AddrInfo, Io}; use super::{ - rustls::ServerSession, - tokio_rustls::{ServerConfigExt, TlsStream}, + identity::Identity, + rustls, + tokio_rustls::{self, ClientConfigExt, ServerConfigExt, TlsStream}, + ClientConfig, ServerConfig, }; +use std::fmt::Debug; + +pub use self::rustls::Session; // In theory we could replace `TcpStream` with `Io`. However, it is likely that // in the future we'll need to do things specific to `TcpStream`, so optimize // for that unless/until there is some benefit to doing otherwise. #[derive(Debug)] -pub struct Connection(TlsStream); +pub struct Connection(TlsStream); -impl Connection { - pub fn accept(socket: TcpStream, ServerConfig(config): ServerConfig) - -> impl Future - { - config.accept_async(socket).map(Connection) +pub struct UpgradeToTls(F) + where S: Session, + F: Future, Error = io::Error>; + +impl Future for UpgradeToTls + where S: Session, + F: Future, Error = io::Error> +{ + type Item = Connection; + type Error = io::Error; + + fn poll(&mut self) -> Result, Self::Error> { + let tls_stream = try_ready!(self.0.poll()); + return Ok(Async::Ready(Connection(tls_stream))); } } -impl io::Read for Connection { +pub type UpgradeClientToTls = + UpgradeToTls>; + +pub type UpgradeServerToTls = + UpgradeToTls>; + +impl Connection { + pub fn connect(socket: TcpStream, identity: &Identity, ClientConfig(config): ClientConfig) + -> UpgradeClientToTls + { + UpgradeToTls(config.connect_async(identity.as_dns_name_ref(), socket)) + } +} + +impl Connection { + pub fn accept(socket: TcpStream, ServerConfig(config): ServerConfig) -> UpgradeServerToTls + { + UpgradeToTls(config.accept_async(socket)) + } +} + +impl io::Read for Connection { fn read(&mut self, buf: &mut [u8]) -> io::Result { self.0.read(buf) } } -impl AsyncRead for Connection { +impl AsyncRead for Connection { unsafe fn prepare_uninitialized_buffer(&self, buf: &mut [u8]) -> bool { self.0.prepare_uninitialized_buffer(buf) } } -impl io::Write for Connection { +impl io::Write for Connection { fn write(&mut self, buf: &[u8]) -> io::Result { self.0.write(buf) } @@ -51,7 +86,7 @@ impl io::Write for Connection { } } -impl AsyncWrite for Connection { +impl AsyncWrite for Connection { fn shutdown(&mut self) -> Poll<(), io::Error> { self.0.shutdown() } @@ -61,7 +96,7 @@ impl AsyncWrite for Connection { } } -impl AddrInfo for Connection { +impl AddrInfo for Connection { fn local_addr(&self) -> Result { self.0.get_ref().0.local_addr() } @@ -71,7 +106,7 @@ impl AddrInfo for Connection { } } -impl Io for Connection { +impl Io for Connection { fn shutdown_write(&mut self) -> Result<(), io::Error> { self.0.get_mut().0.shutdown_write() } diff --git a/proxy/src/transport/tls/mod.rs b/proxy/src/transport/tls/mod.rs index 7ac1136d8..411948877 100755 --- a/proxy/src/transport/tls/mod.rs +++ b/proxy/src/transport/tls/mod.rs @@ -11,8 +11,19 @@ mod dns_name; mod identity; pub use self::{ - config::{CommonSettings, ServerConfig, ServerConfigWatch, watch_for_config_changes}, - connection::Connection, + config::{ + ClientConfig, + ClientConfigWatch, + CommonSettings, + ConditionalConnectionConfig, + ConnectionConfig, + ReasonForNoTls, + ServerConfig, + ServerConfigWatch, + current_connection_config, + watch_for_config_changes, + }, + connection::{Connection, Session, UpgradeClientToTls}, dns_name::{DnsName, InvalidDnsName}, identity::Identity, };