Skip to content

Commit

Permalink
Assert that outbound TLS works before identity is certified (linkerd#251
Browse files Browse the repository at this point in the history
)

This commit introduces TLS capabilities to the support server as well as
tests to ensure that outbound TLS works even when there is no verified
certificate for the proxy yet. 

Fixes linkerd/linkerd2#2599

Signed-off-by: Zahari Dichev <zaharidichev@gmail.com>
  • Loading branch information
zaharidichev authored and hawkw committed May 15, 2019
1 parent 6525b06 commit 91f32db
Show file tree
Hide file tree
Showing 5 changed files with 266 additions and 115 deletions.
58 changes: 56 additions & 2 deletions tests/identity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ fn nonblocking_identity_detection() {
let identity::Identity {
env,
mut certify_rsp,
client_config: _,
..
} = identity::Identity::new("foo-ns1", id.to_string());
certify_rsp.valid_until = Some((SystemTime::now() + Duration::from_secs(666)).into());

Expand Down Expand Up @@ -87,6 +87,7 @@ macro_rules! generate_tls_reject_test {
env,
mut certify_rsp,
client_config,
..
} = identity::Identity::new("foo-ns1", id.to_string());

certify_rsp.valid_until = Some((SystemTime::now() + Duration::from_secs(666)).into());
Expand Down Expand Up @@ -137,14 +138,66 @@ fn http2_rejects_tls_before_identity_is_certified() {
generate_tls_reject_test! {client: client::http2_tls}
}

macro_rules! generate_outbound_tls_accept_not_cert_identity_test {
(server: $make_server:path, client: $make_client:path) => {
let _ = env_logger_init();
let proxy_id = "foo.ns1.serviceaccount.identity.linkerd.cluster.local";
let app_id = "bar.ns1.serviceaccount.identity.linkerd.cluster.local";

let proxy_identity = identity::Identity::new("foo-ns1", proxy_id.to_string());

let (_tx, rx) = oneshot::channel();
let id_svc = controller::identity().certify_async(move |_| rx).run();

let app_identity = identity::Identity::new("bar-ns1", app_id.to_string());
let srv = $make_server(app_identity.server_config)
.route("/", "hello")
.run();

let proxy = proxy::new()
.outbound(srv)
.identity(id_svc)
.run_with_test_env(proxy_identity.env);

let client = $make_client(
proxy.outbound,
app_id,
client::TlsConfig::new(app_identity.client_config, app_id),
);

assert_eventually!(
client
.request(client.request_builder("/").method("GET"))
.status()
== http::StatusCode::OK
);
};
}

#[test]
fn http1_outbound_tls_works_before_identity_is_certified() {
generate_outbound_tls_accept_not_cert_identity_test! {
server: server::http1_tls,
client: client::http1_tls
}
}

#[test]
fn http2_outbound_tls_works_before_identity_is_certified() {
generate_outbound_tls_accept_not_cert_identity_test! {
server: server::http2_tls,
client: client::http2_tls
}
}

#[test]
fn ready() {
let _ = env_logger_init();
let id = "foo.ns1.serviceaccount.identity.linkerd.cluster.local";
let identity::Identity {
env,
mut certify_rsp,
client_config: _,
..
} = identity::Identity::new("foo-ns1", id.to_string());

certify_rsp.valid_until = Some((SystemTime::now() + Duration::from_secs(666)).into());
Expand Down Expand Up @@ -178,6 +231,7 @@ fn refresh() {
mut env,
certify_rsp,
client_config: _,
server_config: _,
} = identity::Identity::new("foo-ns1", id.to_string());

let (expiry_tx, expiry_rx) = oneshot::channel();
Expand Down
68 changes: 15 additions & 53 deletions tests/support/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,9 @@ use std::io;
use std::sync::Mutex;

use self::futures::sync::{mpsc, oneshot};
use self::tokio::{
io::{AsyncRead, AsyncWrite},
net::TcpStream,
};
use self::tokio_rustls::TlsStream;
use self::tokio::net::TcpStream;
use self::webpki::{DNSName, DNSNameRef};
use rustls::{ClientConfig, ClientSession};
use std::io::{Read, Write};
use std::sync::Arc;
use support::bytes::IntoBuf;
use support::hyper::body::Payload;
Expand Down Expand Up @@ -262,7 +257,9 @@ struct Conn {
}

impl Conn {
fn connect_(&self) -> Box<Future<Item = RunningIo, Error = ::std::io::Error> + Send> {
fn connect_(
&self,
) -> Box<Future<Item = RunningIo<ClientSession>, Error = ::std::io::Error> + Send> {
Box::new(ConnectorFuture::Init {
future: TcpStream::connect(&self.addr),
tls: self.tls.clone(),
Expand All @@ -272,7 +269,7 @@ impl Conn {
}

impl Connect for Conn {
type Connected = RunningIo;
type Connected = RunningIo<ClientSession>;
type Error = ::std::io::Error;
type Future = Box<Future<Item = Self::Connected, Error = ::std::io::Error>>;

Expand All @@ -282,7 +279,7 @@ impl Connect for Conn {
}

impl hyper::client::connect::Connect for Conn {
type Transport = RunningIo;
type Transport = RunningIo<ClientSession>;
type Future = Box<
Future<
Item = (Self::Transport, hyper::client::connect::Connected),
Expand All @@ -309,7 +306,7 @@ enum ConnectorFuture {
}

impl Future for ConnectorFuture {
type Item = RunningIo;
type Item = RunningIo<ClientSession>;
type Error = io::Error;

fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
Expand All @@ -331,7 +328,10 @@ impl Future for ConnectorFuture {

match tls {
None => {
return Ok(Async::Ready(RunningIo::Http(io, take_running(running))));
return Ok(Async::Ready(RunningIo::Plain(
io,
Some(take_running(running)),
)));
}

Some(TlsConfig {
Expand All @@ -350,54 +350,16 @@ impl Future for ConnectorFuture {

ConnectorFuture::Handshake { future, running } => {
let io = try_ready!(future.poll());
return Ok(Async::Ready(RunningIo::Https(io, take_running(running))));
return Ok(Async::Ready(RunningIo::Tls(
io,
Some(take_running(running)),
)));
}
}
}
}
}

enum RunningIo {
Http(TcpStream, oneshot::Sender<()>),
Https(TlsStream<TcpStream, ClientSession>, oneshot::Sender<()>),
}

impl Read for RunningIo {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match *self {
RunningIo::Http(ref mut s, _) => s.read(buf),
RunningIo::Https(ref mut s, _) => s.read(buf),
}
}
}

impl Write for RunningIo {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match *self {
RunningIo::Http(ref mut s, _) => s.write(buf),
RunningIo::Https(ref mut s, _) => s.write(buf),
}
}

fn flush(&mut self) -> io::Result<()> {
match *self {
RunningIo::Http(ref mut s, _) => s.flush(),
RunningIo::Https(ref mut s, _) => s.flush(),
}
}
}

impl AsyncRead for RunningIo {}

impl AsyncWrite for RunningIo {
fn shutdown(&mut self) -> Poll<(), io::Error> {
match *self {
RunningIo::Http(ref mut s, _) => s.shutdown(),
RunningIo::Https(ref mut s, _) => s.shutdown(),
}
}
}

impl HttpBody for BytesBody {
type Data = <Bytes as IntoBuf>::Buf;
type Error = hyper::Error;
Expand Down
110 changes: 80 additions & 30 deletions tests/support/identity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub struct Identity {
pub env: app::config::TestEnv,
pub certify_rsp: pb::CertifyResponse,
pub client_config: Arc<rustls::ClientConfig>,
pub server_config: Arc<rustls::ServerConfig>,
}

#[derive(Clone)]
Expand All @@ -29,44 +30,88 @@ type Certify = Box<
+ Send,
>;

pub fn rsp_from_cert<P>(p: P) -> Result<pb::CertifyResponse, io::Error>
where
P: AsRef<Path>,
{
let f = fs::File::open(p)?;
let mut r = io::BufReader::new(f);
let certs = rustls::internal::pemfile::certs(&mut r)
.map_err(|_| io::Error::new(io::ErrorKind::Other, "rustls error reading certs"))?;
let leaf_certificate = certs
.get(0)
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "no certs in pemfile"))?
.as_ref()
.into();
let intermediate_certificates = certs[1..].iter().map(|i| i.as_ref().into()).collect();
let rsp = pb::CertifyResponse {
leaf_certificate,
intermediate_certificates,
..Default::default()
};
Ok(rsp)
const TLS_VERSIONS: &[rustls::ProtocolVersion] = &[rustls::ProtocolVersion::TLSv1_2];

struct Certificates {
pub leaf: Vec<u8>,
pub intermediates: Vec<Vec<u8>>,
}

impl Certificates {
pub fn load<P>(p: P) -> Result<Certificates, io::Error>
where
P: AsRef<Path>,
{
let f = fs::File::open(p)?;
let mut r = io::BufReader::new(f);
let certs = rustls::internal::pemfile::certs(&mut r)
.map_err(|_| io::Error::new(io::ErrorKind::Other, "rustls error reading certs"))?;
let leaf = certs
.get(0)
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "no certs in pemfile"))?
.as_ref()
.into();
let intermediates = certs[1..].iter().map(|i| i.as_ref().into()).collect();

Ok(Certificates {
leaf,
intermediates,
})
}

pub fn chain(&self) -> Vec<rustls::Certificate> {
let mut chain = Vec::with_capacity(self.intermediates.len() + 1);
chain.push(self.leaf.clone());
chain.extend(self.intermediates.clone());
chain.into_iter().map(rustls::Certificate).collect()
}

pub fn response(&self) -> pb::CertifyResponse {
pb::CertifyResponse {
leaf_certificate: self.leaf.clone(),
intermediate_certificates: self.intermediates.clone(),
..Default::default()
}
}
}

impl Identity {
pub fn from_pem(s: &str) -> Arc<rustls::ClientConfig> {
use std::io::Cursor;
fn load_key<P>(p: P) -> rustls::PrivateKey
where
P: AsRef<Path>,
{
let p8 = fs::read(&p).expect("read key");
rustls::PrivateKey(p8)
}

fn configs(
trust_anchors: &str,
certs: &Certificates,
key: rustls::PrivateKey,
) -> (Arc<rustls::ClientConfig>, Arc<rustls::ServerConfig>) {
use std::io::Cursor;
let mut roots = rustls::RootCertStore::empty();
roots
.add_pem_file(&mut Cursor::new(s))
.add_pem_file(&mut Cursor::new(trust_anchors))
.expect("add pem file");

let mut c = rustls::ClientConfig::new();
c.root_store = roots;
Arc::new(c)
let mut client_config = rustls::ClientConfig::new();
client_config.root_store = roots;

let mut server_config = rustls::ServerConfig::new(
rustls::AllowAnyAnonymousOrAuthenticatedClient::new(client_config.root_store.clone()),
);

server_config.versions = TLS_VERSIONS.to_vec();
server_config
.set_single_cert(certs.chain(), key)
.expect("set server resover");

(Arc::new(client_config), Arc::new(server_config))
}

pub fn new(dir: &'static str, local_name: String) -> Self {
let (id_dir, token, trust_anchors, certify_rsp) = {
let (id_dir, token, trust_anchors, certs, key) = {
let path_to_string = |path: &PathBuf| {
path.as_path()
.to_owned()
Expand All @@ -89,12 +134,16 @@ impl Identity {
let token = path_to_string(&id);

id.set_file_name("ca1-cert.pem");
let rsp = rsp_from_cert(&id).expect("read cert");
let certs = Certificates::load(&id).expect("read cert");

id.set_file_name("key.p8");
let key = Identity::load_key(&id);

(id_dir, token, trust_anchors, rsp)
(id_dir, token, trust_anchors, certs, key)
};

let client_config = Identity::from_pem(&trust_anchors);
let certify_rsp = certs.response();
let (client_config, server_config) = Identity::configs(&trust_anchors, &certs, key);
let mut env = app::config::TestEnv::new();

env.put(app::config::ENV_IDENTITY_DIR, id_dir);
Expand All @@ -106,6 +155,7 @@ impl Identity {
env,
certify_rsp,
client_config,
server_config,
}
}

Expand Down
Loading

0 comments on commit 91f32db

Please sign in to comment.