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 <brian@briansmith.org>
This commit is contained in:
Brian Smith 2018-06-19 14:26:42 -10:00 committed by GitHub
parent 9bfd8898e0
commit 8ece9c4508
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 416 additions and 143 deletions

View File

@ -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<ctx::Process> {
Arc::new(ctx::Process {
scheduled_namespace: "test".into(),
start_time: SystemTime::now(),
})
ctx::Process::test("test")
}
fn server(proxy: &Arc<ctx::Proxy>) -> Arc<ctx::transport::Server> {
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<L, S>(proxy: &Arc<ctx::Proxy>, labels: L) -> Arc<ctx::transport::Client>
@ -51,7 +50,7 @@ where
&proxy,
&addr(),
destination::Metadata::new(metrics::DstLabels::new(labels), None),
ctx::transport::TlsStatus::Disabled,
TLS_DISABLED,
)
}

View File

@ -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<B> {
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<C, B> Bind<C, B> {
pub fn with_protocol(self, protocol: Protocol) -> BindProtocol<C, B> {
pub fn with_protocol(self, protocol: Protocol)
-> BindProtocol<C, B>
{
BindProtocol {
bind: self,
protocol,

55
proxy/src/conditional.rs Normal file
View File

@ -0,0 +1,55 @@
use std;
/// Like `std::option::Option<C>` but `None` carries a reason why the value
/// isn't available.
#[derive(Clone, Debug)]
pub enum Conditional<C, R>
where
C: Clone + std::fmt::Debug,
R: Clone + std::fmt::Debug,
{
Some(C),
None(R),
}
impl<C, R> Copy for Conditional<C, R>
where
C: Copy + Clone + std::fmt::Debug,
R: Copy + Clone + std::fmt::Debug,
{
}
impl<C, R> Eq for Conditional<C, R>
where
C: Eq + Clone + std::fmt::Debug,
R: Eq + Clone + std::fmt::Debug,
{
}
impl<C, R> PartialEq for Conditional<C, R>
where
C: PartialEq + Clone + std::fmt::Debug,
R: PartialEq + Clone + std::fmt::Debug,
{
fn eq(&self, other: &Conditional<C, R>) -> bool {
use self::Conditional::*;
match (self, other) {
(Some(a), Some(b)) => a.eq(b),
(None(a), None(b)) => a.eq(b),
_ => false,
}
}
}
impl<C, R> std::hash::Hash for Conditional<C, R>
where
C: std::hash::Hash + Clone + std::fmt::Debug,
R: std::hash::Hash + Clone + std::fmt::Debug,
{
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
match self {
Conditional::Some(c) => c.hash(state),
Conditional::None(r) => r.hash(state),
}
}
}

View File

@ -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<tls::ClientConfig>)
-> 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<tls::ConditionalConnectionConfig<tls::ClientConfig>>,
},
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<Self::Item, Self::Error> {
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<I: Io + 'static>(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<S: tls::Session + std::fmt::Debug + 'static>(tls: tls::Connection<S>) -> Self {
Connection {
io: Box::new(tls),
peek_buf: BytesMut::new(),
tls_status: TlsStatus::Success,
tls_status: Conditional::Some(()),
}
}
@ -312,7 +333,7 @@ impl<T: Peek> Future for PeekFuture<T> {
// 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 {:?}/{:?}: {}",

View File

@ -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<Process>),
Outbound(Arc<Process>),
}
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<Self> {
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<Self> {
pub fn new(config: &config::Config, tls_client_config: tls::ClientConfigWatch) -> Arc<Self> {
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<ctx::Process> {
Arc::new(ctx::Process {
scheduled_namespace: "test".into(),
start_time: SystemTime::now(),
})
ctx::Process::test("test")
}
pub fn server(

View File

@ -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>(c: &Conditional<C, tls::ReasonForNoTls>) -> 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<ctx::Proxy> {
match *self {

View File

@ -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<net::SocketAddr>, ctx: &Arc<ctx::Proxy>) -> 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)

View File

@ -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<Item = (), Error = ()> + 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).

View File

@ -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(()),
}
}
}

View File

@ -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<ctx::transport::Server>,
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();

View File

@ -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)
}
}

View File

@ -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);

View File

@ -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<tls::ClientConfig>,
}
#[derive(Clone, Debug)]
@ -102,12 +104,11 @@ impl Connect {
/// Returns a `Connect` to `addr`.
pub fn new(
addr: SocketAddr,
tls_identity: Option<tls::Identity>,
tls: tls::ConditionalConnectionConfig<tls::ClientConfig>,
) -> 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)
}

View File

@ -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<Self, config::Error>
{
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<rustls::sign::CertifiedKey>
{
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<rustls::sign::CertifiedKey>
{
// 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<webpki::DNSNameRef>,
sigschemes: &[rustls::SignatureScheme]) -> Option<rustls::sign::CertifiedKey> {
@ -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<Box<rustls::sign::Signer>>
{
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;

View File

@ -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<CertResolver>,
}
/// 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<rustls::ClientConfig>);
/// 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<rustls::ServerConfig>);
pub type ClientConfigWatch = Watch<Option<ClientConfig>>;
pub type ServerConfigWatch = Watch<Option<ServerConfig>>;
/// The configuration in effect for a client (`ClientConfig`) or server
/// (`ServerConfig`) TLS connection.
#[derive(Clone, Debug)]
pub struct ConnectionConfig<C> 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<C> = Conditional<ConnectionConfig<C>, 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<C>(identity: Option<&Identity>, watch: &Watch<Option<C>>)
-> ConditionalConnectionConfig<C> 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<Vec<u8>, Error> {
fn load_file(path: &PathBuf) -> Result<Vec<u8>, io::Error> {
let mut result = Vec::new();
@ -279,12 +365,24 @@ fn load_file_contents(path: &PathBuf) -> Result<Vec<u8>, Error> {
fn set_common_settings(versions: &mut Vec<rustls::ProtocolVersion>) {
// 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.
}

View File

@ -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<TcpStream, ServerSession>);
pub struct Connection<S: Session>(TlsStream<TcpStream, S>);
impl Connection {
pub fn accept(socket: TcpStream, ServerConfig(config): ServerConfig)
-> impl Future<Item = Connection, Error = io::Error>
{
config.accept_async(socket).map(Connection)
pub struct UpgradeToTls<S, F>(F)
where S: Session,
F: Future<Item = TlsStream<TcpStream, S>, Error = io::Error>;
impl<S, F> Future for UpgradeToTls<S, F>
where S: Session,
F: Future<Item = TlsStream<TcpStream, S>, Error = io::Error>
{
type Item = Connection<S>;
type Error = io::Error;
fn poll(&mut self) -> Result<Async<Self::Item>, 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<rustls::ClientSession, tokio_rustls::ConnectAsync<TcpStream>>;
pub type UpgradeServerToTls =
UpgradeToTls<rustls::ServerSession, tokio_rustls::AcceptAsync<TcpStream>>;
impl Connection<rustls::ClientSession> {
pub fn connect(socket: TcpStream, identity: &Identity, ClientConfig(config): ClientConfig)
-> UpgradeClientToTls
{
UpgradeToTls(config.connect_async(identity.as_dns_name_ref(), socket))
}
}
impl Connection<rustls::ServerSession> {
pub fn accept(socket: TcpStream, ServerConfig(config): ServerConfig) -> UpgradeServerToTls
{
UpgradeToTls(config.accept_async(socket))
}
}
impl<S: Session> io::Read for Connection<S> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.0.read(buf)
}
}
impl AsyncRead for Connection {
impl<S: Session> AsyncRead for Connection<S> {
unsafe fn prepare_uninitialized_buffer(&self, buf: &mut [u8]) -> bool {
self.0.prepare_uninitialized_buffer(buf)
}
}
impl io::Write for Connection {
impl<S: Session> io::Write for Connection<S> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.write(buf)
}
@ -51,7 +86,7 @@ impl io::Write for Connection {
}
}
impl AsyncWrite for Connection {
impl<S: Session> AsyncWrite for Connection<S> {
fn shutdown(&mut self) -> Poll<(), io::Error> {
self.0.shutdown()
}
@ -61,7 +96,7 @@ impl AsyncWrite for Connection {
}
}
impl AddrInfo for Connection {
impl<S: Session + Debug> AddrInfo for Connection<S> {
fn local_addr(&self) -> Result<SocketAddr, io::Error> {
self.0.get_ref().0.local_addr()
}
@ -71,7 +106,7 @@ impl AddrInfo for Connection {
}
}
impl Io for Connection {
impl<S: Session + Debug> Io for Connection<S> {
fn shutdown_write(&mut self) -> Result<(), io::Error> {
self.0.get_mut().0.shutdown_write()
}

View File

@ -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,
};