feat(transport): Expose more granular control of TLS configuration (#48)

This commit reworks TLS configuration of both servers and endpoints in
order to provide a more flexible API. We now add options to configure
the selected TLS library using the appropriate 'native' configuration
structures, as well as retaining the existing simplier interface which
is compatible with both.

The new API can also be easily extended to support simple interfaces for
configuring mTLS and a range of other options without creating sprawl
in the builders for `Server` and `Endpoint`.
This commit is contained in:
James Nugent
2019-10-08 11:49:05 -04:00
committed by Lucio Franco
parent 4628ff0258
commit 8db3961491
9 changed files with 311 additions and 132 deletions
+7 -3
View File
@@ -3,19 +3,23 @@ pub mod pb {
}
use pb::{client::EchoClient, EchoRequest};
use tonic::transport::{Certificate, Channel};
use tonic::transport::{Certificate, Channel, ClientTlsConfig};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let pem = tokio::fs::read("tonic-examples/data/tls/ca.pem").await?;
let ca = Certificate::from_pem(pem);
let tls = ClientTlsConfig::with_rustls()
.ca_certificate(ca)
.domain_name("example.com")
.clone();
let channel = Channel::from_static("http://[::1]:50051")
.rustls_tls(ca, Some("example.com".into()))
.tls_config(&tls)
.channel();
let mut client = EchoClient::new(channel);
let request = tonic::Request::new(EchoRequest {
message: "hello".into(),
});
+2 -2
View File
@@ -5,7 +5,7 @@ pub mod pb {
use pb::{EchoRequest, EchoResponse};
use std::collections::VecDeque;
use tonic::{
transport::{Identity, Server},
transport::{Identity, Server, ServerTlsConfig},
Request, Response, Status, Streaming,
};
@@ -59,7 +59,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let server = EchoServer::default();
Server::builder()
.rustls_tls(identity)
.tls_config(ServerTlsConfig::with_rustls().identity(identity))
.clone()
.serve(addr, pb::server::EchoServer::new(server))
.await?;
+7 -2
View File
@@ -1,6 +1,6 @@
use std::time::Duration;
use structopt::{clap::arg_enum, StructOpt};
use tonic::transport::{Certificate, Endpoint};
use tonic::transport::{Certificate, ClientTlsConfig, Endpoint};
use tonic_interop::client;
#[derive(StructOpt)]
@@ -33,7 +33,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
if matches.use_tls {
let pem = tokio::fs::read("tonic-interop/data/ca.pem").await?;
let ca = Certificate::from_pem(pem);
endpoint.openssl_tls(ca, Some("foo.test.google.fr".into()));
endpoint.tls_config(
ClientTlsConfig::with_openssl()
.ca_certificate(ca)
.domain_name("foo.test.google.fr"),
);
}
let channel = endpoint.channel();
+2 -2
View File
@@ -2,7 +2,7 @@ use http::header::HeaderName;
use structopt::StructOpt;
use tonic::body::BoxBody;
use tonic::client::GrpcService;
use tonic::transport::{Identity, Server};
use tonic::transport::{Identity, Server, ServerTlsConfig};
use tonic_interop::{server, MergeTrailers};
#[derive(StructOpt)]
@@ -26,7 +26,7 @@ async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
let key = tokio::fs::read("tonic-interop/data/server1.key").await?;
let identity = Identity::from_pem(cert, key);
builder.openssl_tls(identity);
builder.tls_config(ServerTlsConfig::with_openssl().identity(identity));
}
builder.interceptor_fn(|svc, req| {
+112 -59
View File
@@ -1,6 +1,9 @@
use super::channel::Channel;
#[cfg(feature = "tls")]
use super::{service::TlsConnector, tls::Certificate};
use super::{
service::TlsConnector,
tls::{Certificate, TlsProvider},
};
use bytes::Bytes;
use http::uri::{InvalidUriBytes, Uri};
use std::{
@@ -122,64 +125,6 @@ impl Endpoint {
self
}
/// Enable TLS and apply the CA as the root certificate.
///
/// Providing an optional domain to override. If `None` is passed to this
/// the TLS implementation will use the `Uri` that was used to create the
/// `Endpoint` builder.
///
/// ```no_run
/// # use tonic::transport::{Certificate, Endpoint};
/// # fn dothing() -> Result<(), Box<dyn std::error::Error>> {
/// # let mut builder = Endpoint::from_static("https://example.com");
/// let ca = std::fs::read_to_string("ca.pem")?;
///
/// let ca = Certificate::from_pem(ca);
///
/// builder.openssl_tls(ca, "example.com".to_string());
/// # Ok(())
/// # }
/// ```
#[cfg(feature = "openssl")]
#[cfg_attr(docsrs, doc(cfg(feature = "openssl")))]
pub fn openssl_tls(&mut self, ca: Certificate, domain: impl Into<Option<String>>) -> &mut Self {
let domain = domain
.into()
.unwrap_or_else(|| self.uri.clone().to_string());
let tls = TlsConnector::new_with_openssl(ca, domain).unwrap();
self.tls = Some(tls);
self
}
/// Enable TLS and apply the CA as the root certificate.
///
/// Providing an optional domain to override. If `None` is passed to this
/// the TLS implementation will use the `Uri` that was used to create the
/// `Endpoint` builder.
///
/// ```no_run
/// # use tonic::transport::{Certificate, Endpoint};
/// # fn dothing() -> Result<(), Box<dyn std::error::Error>> {
/// # let mut builder = Endpoint::from_static("https://example.com");
/// let ca = std::fs::read_to_string("ca.pem")?;
///
/// let ca = Certificate::from_pem(ca);
///
/// builder.rustls_tls(ca, "example.com".to_string());
/// # Ok(())
/// # }
/// ```
#[cfg(feature = "rustls")]
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
pub fn rustls_tls(&mut self, ca: Certificate, domain: impl Into<Option<String>>) -> &mut Self {
let domain = domain
.into()
.unwrap_or_else(|| self.uri.clone().to_string());
let tls = TlsConnector::new_with_rustls(ca, domain).unwrap();
self.tls = Some(tls);
self
}
/// Intercept outbound HTTP Request headers;
pub fn intercept_headers<F>(&mut self, f: F) -> &mut Self
where
@@ -189,6 +134,13 @@ impl Endpoint {
self
}
/// Configures TLS for the endpoint.
#[cfg(feature = "tls")]
pub fn tls_config(&mut self, tls_config: &ClientTlsConfig) -> &mut Self {
self.tls = Some(tls_config.tls_connector(self.uri.clone()).unwrap());
self
}
/// Create a channel from this config.
pub fn channel(&self) -> Channel {
Channel::connect(self.clone())
@@ -252,3 +204,104 @@ impl fmt::Debug for Endpoint {
f.debug_struct("Endpoint").finish()
}
}
/// Configures TLS settings for endpoints.
#[cfg(feature = "tls")]
#[derive(Clone)]
pub struct ClientTlsConfig {
provider: TlsProvider,
domain: Option<String>,
cert: Option<Certificate>,
#[cfg(feature = "openssl")]
openssl_raw: Option<openssl1::ssl::SslConnector>,
#[cfg(feature = "rustls")]
rustls_raw: Option<tokio_rustls::rustls::ClientConfig>,
}
#[cfg(feature = "tls")]
impl fmt::Debug for ClientTlsConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ClientTlsConfig")
.field("provider", &self.provider)
.finish()
}
}
#[cfg(feature = "tls")]
impl ClientTlsConfig {
/// Creates a new `ClientTlsConfig` using OpenSSL.
#[cfg(feature = "openssl")]
pub fn with_openssl() -> Self {
Self::new(TlsProvider::OpenSsl)
}
/// Creates a new `ClientTlsConfig` using Rustls.
#[cfg(feature = "rustls")]
pub fn with_rustls() -> Self {
Self::new(TlsProvider::Rustls)
}
fn new(provider: TlsProvider) -> Self {
ClientTlsConfig {
provider,
domain: None,
cert: None,
#[cfg(feature = "openssl")]
openssl_raw: None,
#[cfg(feature = "rustls")]
rustls_raw: None,
}
}
/// Sets the domain name against which to verify the server's TLS certificate.
pub fn domain_name(&mut self, domain_name: impl Into<String>) -> &mut Self {
self.domain = Some(domain_name.into());
self
}
/// Sets the CA Certificate against which to verify the server's TLS certificate.
pub fn ca_certificate(&mut self, ca_certificate: Certificate) -> &mut Self {
self.cert = Some(ca_certificate);
self
}
/// Use options specified by the given `SslConnector` to configure TLS.
///
/// This overrides all other TLS options set via other means.
#[cfg(feature = "openssl")]
pub fn openssl_connector(&mut self, connector: openssl1::ssl::SslConnector) -> &mut Self {
self.openssl_raw = Some(connector);
self
}
/// Use options specified by the given `ClientConfig` to configure TLS.
///
/// This overrides all other TLS options set via other means.
#[cfg(feature = "rustls")]
pub fn rustls_client_config(
&mut self,
config: tokio_rustls::rustls::ClientConfig,
) -> &mut Self {
self.rustls_raw = Some(config);
self
}
fn tls_connector(&self, uri: Uri) -> Result<TlsConnector, crate::Error> {
let domain = match &self.domain {
None => uri.to_string(),
Some(domain) => domain.clone(),
};
match self.provider {
#[cfg(feature = "openssl")]
TlsProvider::OpenSsl => match &self.openssl_raw {
None => TlsConnector::new_with_openssl_cert(self.cert.clone(), domain),
Some(r) => TlsConnector::new_with_openssl_raw(r.clone(), domain),
},
#[cfg(feature = "rustls")]
TlsProvider::Rustls => match &self.rustls_raw {
None => TlsConnector::new_with_rustls_cert(self.cert.clone(), domain),
Some(c) => TlsConnector::new_with_rustls_raw(c.clone(), domain),
},
}
}
}
+12 -4
View File
@@ -19,7 +19,7 @@
//! ## Client
//!
//! ```no_run
//! # use tonic::transport::{Channel, Certificate};
//! # use tonic::transport::{Channel, Certificate, ClientTlsConfig};
//! # use std::time::Duration;
//! # use tonic::body::BoxBody;
//! # use tonic::client::GrpcService;;
@@ -29,7 +29,9 @@
//! let cert = std::fs::read_to_string("ca.pem")?;
//!
//! let mut channel = Channel::from_static("https://example.com")
//! .rustls_tls(Certificate::from_pem(&cert), "example.com".to_string())
//! .tls_config(ClientTlsConfig::with_rustls()
//! .ca_certificate(Certificate::from_pem(&cert))
//! .domain_name("example.com".to_string()))
//! .timeout(Duration::from_secs(5))
//! .rate_limit(5, Duration::from_secs(1))
//! .concurrency_limit(256)
@@ -43,7 +45,7 @@
//! ## Server
//!
//! ```no_run
//! # use tonic::transport::{Server, Identity};
//! # use tonic::transport::{Server, Identity, ServerTlsConfig};
//! # use tower::{Service, service_fn};
//! # use futures_util::future::{err, ok};
//! # #[cfg(feature = "rustls")]
@@ -55,7 +57,8 @@
//! let addr = "[::1]:50051".parse()?;
//!
//! Server::builder()
//! .rustls_tls(Identity::from_pem(&cert, &key))
//! .tls_config(ServerTlsConfig::with_rustls()
//! .identity(Identity::from_pem(&cert, &key)))
//! .concurrency_limit_per_connection(256)
//! .interceptor_fn(|svc, req| {
//! println!("Request: {:?}", req);
@@ -89,4 +92,9 @@ pub use self::server::Server;
pub use self::tls::{Certificate, Identity};
pub use hyper::Body;
#[cfg(feature = "tls")]
pub use self::endpoint::ClientTlsConfig;
#[cfg(feature = "tls")]
pub use self::server::ServerTlsConfig;
pub(crate) use self::error::ErrorKind;
+99 -44
View File
@@ -2,7 +2,10 @@
use super::service::{layer_fn, BoxedIo, ServiceBuilderExt};
#[cfg(feature = "tls")]
use super::{service::TlsAcceptor, tls::Identity};
use super::{
service::TlsAcceptor,
tls::{Identity, TlsProvider},
};
use crate::body::BoxBody;
use futures_core::Stream;
use futures_util::{ready, try_future::MapErr, TryFutureExt, TryStreamExt};
@@ -62,49 +65,10 @@ impl Server {
}
impl Server {
/// Set the [`Identity`] of this server using `openssl`.
///
/// ```no_run
/// # use tonic::transport::{Identity, Server};
/// # fn dothing() -> Result<(), Box<dyn std::error::Error>> {
/// # let mut builder = Server::builder();
/// let cert = std::fs::read_to_string("server.pem")?;
/// let key = std::fs::read_to_string("server.key")?;
///
/// let identity = Identity::from_pem(&cert, &key);
///
/// builder.openssl_tls(identity);
/// # Ok(())
/// # }
/// ```
#[cfg(feature = "openssl")]
#[cfg_attr(docsrs, doc(cfg(feature = "openssl")))]
pub fn openssl_tls(&mut self, identity: Identity) -> &mut Self {
let acceptor = TlsAcceptor::new_with_openssl(identity).unwrap();
self.tls = Some(acceptor);
self
}
/// Set the [`Identity`] of this server using `rustls`.
///
/// ```no_run
/// # use tonic::transport::{Identity, Server};
/// # fn dothing() -> Result<(), Box<dyn std::error::Error>> {
/// # let mut builder = Server::builder();
/// let cert = std::fs::read_to_string("server.pem")?;
/// let key = std::fs::read_to_string("server.key")?;
///
/// let identity = Identity::from_pem(&cert, &key);
///
/// builder.rustls_tls(identity);
/// # Ok(())
/// # }
/// ```
#[cfg(feature = "rustls")]
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
pub fn rustls_tls(&mut self, identity: Identity) -> &mut Self {
let acceptor = TlsAcceptor::new_with_rustls(identity).unwrap();
self.tls = Some(acceptor);
/// Configure TLS for this server.
#[cfg(feature = "tls")]
pub fn tls_config(&mut self, tls_config: &ServerTlsConfig) -> &mut Self {
self.tls = Some(tls_config.tls_acceptor().unwrap());
self
}
@@ -255,6 +219,97 @@ impl fmt::Debug for Server {
}
}
/// Configures TLS settings for servers.
#[cfg(feature = "tls")]
#[derive(Clone)]
pub struct ServerTlsConfig {
provider: TlsProvider,
identity: Option<Identity>,
#[cfg(feature = "openssl")]
openssl_raw: Option<openssl1::ssl::SslAcceptor>,
#[cfg(feature = "rustls")]
rustls_raw: Option<tokio_rustls::rustls::ServerConfig>,
}
#[cfg(feature = "tls")]
impl fmt::Debug for ServerTlsConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ServerTlsConfig")
.field("provider", &self.provider)
.finish()
}
}
#[cfg(feature = "tls")]
impl ServerTlsConfig {
/// Creates a new `ServerTlsConfig` using OpenSSL.
#[cfg(feature = "openssl")]
pub fn with_openssl() -> Self {
Self::new(TlsProvider::OpenSsl)
}
/// Creates a new `ServerTlsConfig` using Rustls.
#[cfg(feature = "rustls")]
pub fn with_rustls() -> Self {
Self::new(TlsProvider::Rustls)
}
/// Creates a new `ServerTlsConfig` backed by the specified provider. Enable the `openssl` or
/// `rustls` features of the `tonic` crate to use OpenSSL or Rustls respectively.
fn new(provider: TlsProvider) -> Self {
ServerTlsConfig {
provider,
identity: None,
#[cfg(feature = "openssl")]
openssl_raw: None,
#[cfg(feature = "rustls")]
rustls_raw: None,
}
}
/// Sets the [`Identity`] of the server.
pub fn identity(&mut self, identity: Identity) -> &mut Self {
self.identity = Some(identity);
self
}
/// Use options specified by the given `SslAcceptor` to configure TLS.
///
/// This overrides all other TLS options set via other means.
#[cfg(feature = "openssl")]
pub fn openssl_connector(&mut self, acceptor: openssl1::ssl::SslAcceptor) -> &mut Self {
self.openssl_raw = Some(acceptor);
self
}
/// Use options specified by the given `ServerConfig` to configure TLS.
///
/// This overrides all other TLS options set via other means.
#[cfg(feature = "rustls")]
pub fn rustls_client_config(
&mut self,
config: tokio_rustls::rustls::ServerConfig,
) -> &mut Self {
self.rustls_raw = Some(config);
self
}
fn tls_acceptor(&self) -> Result<TlsAcceptor, crate::Error> {
match self.provider {
#[cfg(feature = "openssl")]
TlsProvider::OpenSsl => match &self.openssl_raw {
None => TlsAcceptor::new_with_openssl_identity(self.identity.clone().unwrap()),
Some(acceptor) => TlsAcceptor::new_with_openssl_raw(acceptor.clone()),
},
#[cfg(feature = "rustls")]
TlsProvider::Rustls => match &self.rustls_raw {
None => TlsAcceptor::new_with_rustls_identity(self.identity.clone().unwrap()),
Some(config) => TlsAcceptor::new_with_rustls_raw(config.clone()),
},
}
}
}
#[derive(Debug)]
struct TcpIncoming {
inner: conn::AddrIncoming,
+59 -16
View File
@@ -56,35 +56,60 @@ enum Connector {
impl TlsConnector {
#[cfg(feature = "openssl")]
pub(crate) fn new_with_openssl(
cert: Certificate,
pub(crate) fn new_with_openssl_cert(
cert: Option<Certificate>,
domain: String,
) -> Result<Self, crate::Error> {
let mut config = SslConnector::builder(SslMethod::tls())?;
config.set_alpn_protos(ALPN_H2_WIRE)?;
let ca = X509::from_pem(&cert.pem[..])?;
config.cert_store_mut().add_cert(ca)?;
let config = config.build();
if let Some(cert) = cert {
let ca = X509::from_pem(&cert.pem[..])?;
config.cert_store_mut().add_cert(ca)?;
}
Ok(Self {
inner: Connector::Openssl(config),
inner: Connector::Openssl(config.build()),
domain: Arc::new(domain),
})
}
#[cfg(feature = "openssl")]
pub(crate) fn new_with_openssl_raw(
ssl_connector: openssl1::ssl::SslConnector,
domain: String,
) -> Result<Self, crate::Error> {
Ok(Self {
inner: Connector::Openssl(ssl_connector),
domain: Arc::new(domain),
})
}
#[cfg(feature = "rustls")]
pub(crate) fn new_with_rustls(cert: Certificate, domain: String) -> Result<Self, crate::Error> {
let mut buf = std::io::Cursor::new(&cert.pem[..]);
pub(crate) fn new_with_rustls_cert(
cert: Option<Certificate>,
domain: String,
) -> Result<Self, crate::Error> {
let mut config = ClientConfig::new();
config.root_store.add_pem_file(&mut buf).unwrap();
config.set_protocols(&[Vec::from(&ALPN_H2[..])]);
if cert.is_some() {
let cert = cert.unwrap();
let mut buf = std::io::Cursor::new(&cert.pem[..]);
config.root_store.add_pem_file(&mut buf).unwrap();
}
Ok(Self {
inner: Connector::Rustls(Arc::new(config)),
domain: Arc::new(domain),
})
}
#[cfg(feature = "rustls")]
pub(crate) fn new_with_rustls_raw(
config: tokio_rustls::rustls::ClientConfig,
domain: String,
) -> Result<Self, crate::Error> {
Ok(Self {
inner: Connector::Rustls(Arc::new(config)),
domain: Arc::new(domain),
@@ -167,7 +192,7 @@ enum Acceptor {
impl TlsAcceptor {
#[cfg(feature = "openssl")]
pub(crate) fn new_with_openssl(identity: Identity) -> Result<Self, crate::Error> {
pub(crate) fn new_with_openssl_identity(identity: Identity) -> Result<Self, crate::Error> {
let key = PKey::private_key_from_pem(&identity.key[..])?;
let cert = X509::from_pem(&identity.cert.pem[..])?;
@@ -185,6 +210,15 @@ impl TlsAcceptor {
})
}
#[cfg(feature = "openssl")]
pub(crate) fn new_with_openssl_raw(
acceptor: openssl1::ssl::SslAcceptor,
) -> Result<Self, crate::Error> {
Ok(Self {
inner: Acceptor::Openssl(acceptor),
})
}
#[cfg(feature = "rustls")]
fn load_rustls_private_key(
mut cursor: std::io::Cursor<&[u8]>,
@@ -209,7 +243,7 @@ impl TlsAcceptor {
}
#[cfg(feature = "rustls")]
pub(crate) fn new_with_rustls(identity: Identity) -> Result<Self, crate::Error> {
pub(crate) fn new_with_rustls_identity(identity: Identity) -> Result<Self, crate::Error> {
let cert = {
let mut cert = std::io::Cursor::new(&identity.cert.pem[..]);
match pemfile::certs(&mut cert) {
@@ -238,6 +272,15 @@ impl TlsAcceptor {
})
}
#[cfg(feature = "rustls")]
pub(crate) fn new_with_rustls_raw(
config: tokio_rustls::rustls::ServerConfig,
) -> Result<Self, crate::Error> {
Ok(Self {
inner: Acceptor::Rustls(Arc::new(config)),
})
}
pub(crate) async fn connect(&self, io: TcpStream) -> Result<BoxedIo, crate::Error> {
let io = match &self.inner {
#[cfg(feature = "openssl")]
+11
View File
@@ -1,3 +1,14 @@
/// Selects a library to provide TLS.
#[derive(Clone, Debug)]
pub(crate) enum TlsProvider {
/// Use OpenSSL for TLS.
#[cfg(feature = "openssl")]
OpenSsl,
/// Use OpenSSL for TLS.
#[cfg(feature = "rustls")]
Rustls,
}
/// Represents a X509 certificate.
#[derive(Debug, Clone)]
pub struct Certificate {