feat(transport): Add support client mTLS (#77)

This commit adds a simple API for specifying the TLS certificate a GRPC
client will present (via the same `Identity` wrapper as a server cert is
configured). It also adds an API to specify which CA certificate client
TLS certificates will be validated against for servers.
This commit is contained in:
James Nugent
2019-10-22 17:00:08 -04:00
committed by Lucio Franco
parent 9079e0f66b
commit 335a373a40
11 changed files with 354 additions and 51 deletions
+22 -3
View File
@@ -2,7 +2,7 @@ use super::channel::Channel;
#[cfg(feature = "tls")]
use super::{
service::TlsConnector,
tls::{Certificate, TlsProvider},
tls::{Certificate, Identity, TlsProvider},
};
use bytes::Bytes;
use http::uri::{InvalidUriBytes, Uri};
@@ -212,6 +212,7 @@ pub struct ClientTlsConfig {
provider: TlsProvider,
domain: Option<String>,
cert: Option<Certificate>,
identity: Option<Identity>,
#[cfg(feature = "openssl")]
openssl_raw: Option<openssl1::ssl::SslConnector>,
#[cfg(feature = "rustls")]
@@ -223,6 +224,9 @@ impl fmt::Debug for ClientTlsConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ClientTlsConfig")
.field("provider", &self.provider)
.field("domain", &self.domain)
.field("cert", &self.cert)
.field("identity", &self.identity)
.finish()
}
}
@@ -246,6 +250,7 @@ impl ClientTlsConfig {
provider,
domain: None,
cert: None,
identity: None,
#[cfg(feature = "openssl")]
openssl_raw: None,
#[cfg(feature = "rustls")]
@@ -265,6 +270,12 @@ impl ClientTlsConfig {
self
}
/// Sets the client identity to present to the server.
pub fn identity(&mut self, identity: Identity) -> &mut Self {
self.identity = Some(identity);
self
}
/// Use options specified by the given `SslConnector` to configure TLS.
///
/// This overrides all other TLS options set via other means.
@@ -294,12 +305,20 @@ impl ClientTlsConfig {
match self.provider {
#[cfg(feature = "openssl")]
TlsProvider::OpenSsl => match &self.openssl_raw {
None => TlsConnector::new_with_openssl_cert(self.cert.clone(), domain),
None => TlsConnector::new_with_openssl_cert(
self.cert.clone(),
self.identity.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),
None => TlsConnector::new_with_rustls_cert(
self.cert.clone(),
self.identity.clone(),
domain,
),
Some(c) => TlsConnector::new_with_rustls_raw(c.clone(), domain),
},
}
+17 -2
View File
@@ -5,6 +5,7 @@ use super::service::{layer_fn, BoxedIo, ServiceBuilderExt};
use super::{
service::TlsAcceptor,
tls::{Identity, TlsProvider},
Certificate,
};
use crate::body::BoxBody;
use futures_core::Stream;
@@ -225,6 +226,7 @@ impl fmt::Debug for Server {
pub struct ServerTlsConfig {
provider: TlsProvider,
identity: Option<Identity>,
client_ca_root: Option<Certificate>,
#[cfg(feature = "openssl")]
openssl_raw: Option<openssl1::ssl::SslAcceptor>,
#[cfg(feature = "rustls")]
@@ -260,6 +262,7 @@ impl ServerTlsConfig {
ServerTlsConfig {
provider,
identity: None,
client_ca_root: None,
#[cfg(feature = "openssl")]
openssl_raw: None,
#[cfg(feature = "rustls")]
@@ -273,6 +276,12 @@ impl ServerTlsConfig {
self
}
/// Sets a certificate against which to validate client TLS certificates.
pub fn client_ca_root(&mut self, cert: Certificate) -> &mut Self {
self.client_ca_root = Some(cert);
self
}
/// Use options specified by the given `SslAcceptor` to configure TLS.
///
/// This overrides all other TLS options set via other means.
@@ -298,12 +307,18 @@ impl ServerTlsConfig {
match self.provider {
#[cfg(feature = "openssl")]
TlsProvider::OpenSsl => match &self.openssl_raw {
None => TlsAcceptor::new_with_openssl_identity(self.identity.clone().unwrap()),
None => TlsAcceptor::new_with_openssl_identity(
self.identity.clone().unwrap(),
self.client_ca_root.clone(),
),
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()),
None => TlsAcceptor::new_with_rustls_identity(
self.identity.clone().unwrap(),
self.client_ca_root.clone(),
),
Some(config) => TlsAcceptor::new_with_rustls_raw(config.clone()),
},
}
+104 -46
View File
@@ -3,14 +3,14 @@ use crate::transport::{Certificate, Identity};
#[cfg(feature = "openssl")]
use openssl1::{
pkey::PKey,
ssl::{select_next_proto, AlpnError, SslAcceptor, SslConnector, SslMethod},
x509::X509,
ssl::{select_next_proto, AlpnError, SslAcceptor, SslConnector, SslMethod, SslVerifyMode},
x509::{store::X509StoreBuilder, X509},
};
use std::{fmt, sync::Arc};
use tokio::net::TcpStream;
#[cfg(feature = "rustls")]
use tokio_rustls::{
rustls::{internal::pemfile, ClientConfig, NoClientAuth, PrivateKey, ServerConfig, Session},
rustls::{ClientConfig, NoClientAuth, ServerConfig, Session},
webpki::DNSNameRef,
TlsAcceptor as RustlsAcceptor, TlsConnector as RustlsConnector,
};
@@ -58,6 +58,7 @@ impl TlsConnector {
#[cfg(feature = "openssl")]
pub(crate) fn new_with_openssl_cert(
cert: Option<Certificate>,
identity: Option<Identity>,
domain: String,
) -> Result<Self, crate::Error> {
let mut config = SslConnector::builder(SslMethod::tls())?;
@@ -68,6 +69,13 @@ impl TlsConnector {
config.cert_store_mut().add_cert(ca)?;
}
if let Some(identity) = identity {
let key = PKey::private_key_from_pem(&identity.key[..])?;
let cert = X509::from_pem(&identity.cert.pem[..])?;
config.set_certificate(&cert)?;
config.set_private_key(&key)?;
}
Ok(Self {
inner: Connector::Openssl(config.build()),
domain: Arc::new(domain),
@@ -87,14 +95,19 @@ impl TlsConnector {
#[cfg(feature = "rustls")]
pub(crate) fn new_with_rustls_cert(
cert: Option<Certificate>,
ca_cert: Option<Certificate>,
identity: Option<Identity>,
domain: String,
) -> Result<Self, crate::Error> {
let mut config = ClientConfig::new();
config.set_protocols(&[Vec::from(&ALPN_H2[..])]);
if cert.is_some() {
let cert = cert.unwrap();
if let Some(identity) = identity {
let (client_cert, client_key) = rustls_keys::load_identity(identity)?;
config.set_single_client_cert(client_cert, client_key);
}
if let Some(cert) = ca_cert {
let mut buf = std::io::Cursor::new(&cert.pem[..]);
config.root_store.add_pem_file(&mut buf).unwrap();
}
@@ -192,7 +205,10 @@ enum Acceptor {
impl TlsAcceptor {
#[cfg(feature = "openssl")]
pub(crate) fn new_with_openssl_identity(identity: Identity) -> Result<Self, crate::Error> {
pub(crate) fn new_with_openssl_identity(
identity: Identity,
client_ca_root: Option<Certificate>,
) -> Result<Self, crate::Error> {
let key = PKey::private_key_from_pem(&identity.key[..])?;
let cert = X509::from_pem(&identity.cert.pem[..])?;
@@ -205,6 +221,16 @@ impl TlsAcceptor {
select_next_proto(ALPN_H2_WIRE, alpn).ok_or(AlpnError::NOACK)
});
if let Some(cert) = client_ca_root {
let ca_cert = X509::from_pem(&cert.pem[..])?;
let mut store = X509StoreBuilder::new()?;
store.add_cert(ca_cert.clone())?;
config.add_client_ca(&ca_cert)?;
config.set_verify_cert_store(store.build())?;
config.set_verify(SslVerifyMode::PEER | SslVerifyMode::FAIL_IF_NO_PEER_CERT);
}
Ok(Self {
inner: Acceptor::Openssl(config.build()),
})
@@ -220,50 +246,28 @@ impl TlsAcceptor {
}
#[cfg(feature = "rustls")]
fn load_rustls_private_key(
mut cursor: std::io::Cursor<&[u8]>,
) -> Result<PrivateKey, crate::Error> {
// First attempt to load the private key assuming it is PKCS8-encoded
if let Ok(mut keys) = pemfile::pkcs8_private_keys(&mut cursor) {
if keys.len() > 0 {
return Ok(keys.remove(0));
}
}
pub(crate) fn new_with_rustls_identity(
identity: Identity,
client_ca_root: Option<Certificate>,
) -> Result<Self, crate::Error> {
let (cert, key) = rustls_keys::load_identity(identity)?;
// If it not, try loading the private key as an RSA key
cursor.set_position(0);
if let Ok(mut keys) = pemfile::rsa_private_keys(&mut cursor) {
if keys.len() > 0 {
return Ok(keys.remove(0));
}
}
let mut config = match client_ca_root {
None => ServerConfig::new(NoClientAuth::new()),
Some(cert) => {
let mut cert = std::io::Cursor::new(&cert.pem[..]);
// Otherwise we have a Private Key parsing problem
Err(Box::new(TlsError::PrivateKeyParseError))
}
let mut client_root_cert_store = tokio_rustls::rustls::RootCertStore::empty();
match client_root_cert_store.add_pem_file(&mut cert) {
Err(_) => return Err(Box::new(TlsError::CertificateParseError)),
_ => (),
};
#[cfg(feature = "rustls")]
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) {
Ok(certs) => certs,
Err(_) => return Err(Box::new(TlsError::CertificateParseError)),
let client_auth =
tokio_rustls::rustls::AllowAnyAuthenticatedClient::new(client_root_cert_store);
ServerConfig::new(client_auth)
}
};
let key = {
let key = std::io::Cursor::new(&identity.key[..]);
match Self::load_rustls_private_key(key) {
Ok(key) => key,
Err(e) => {
return Err(e);
}
}
};
let mut config = ServerConfig::new(NoClientAuth::new());
config.set_single_cert(cert, key)?;
config.set_protocols(&[Vec::from(&ALPN_H2[..])]);
@@ -338,3 +342,57 @@ impl fmt::Display for TlsError {
}
impl std::error::Error for TlsError {}
#[cfg(feature = "rustls")]
mod rustls_keys {
use tokio_rustls::rustls::{internal::pemfile, Certificate, PrivateKey};
use crate::transport::service::tls::TlsError;
use crate::transport::Identity;
fn load_rustls_private_key(
mut cursor: std::io::Cursor<&[u8]>,
) -> Result<PrivateKey, crate::Error> {
// First attempt to load the private key assuming it is PKCS8-encoded
if let Ok(mut keys) = pemfile::pkcs8_private_keys(&mut cursor) {
if keys.len() > 0 {
return Ok(keys.remove(0));
}
}
// If it not, try loading the private key as an RSA key
cursor.set_position(0);
if let Ok(mut keys) = pemfile::rsa_private_keys(&mut cursor) {
if keys.len() > 0 {
return Ok(keys.remove(0));
}
}
// Otherwise we have a Private Key parsing problem
Err(Box::new(TlsError::PrivateKeyParseError))
}
pub(crate) fn load_identity(
identity: Identity,
) -> Result<(Vec<Certificate>, PrivateKey), crate::Error> {
let cert = {
let mut cert = std::io::Cursor::new(&identity.cert.pem[..]);
match pemfile::certs(&mut cert) {
Ok(certs) => certs,
Err(_) => return Err(Box::new(TlsError::CertificateParseError)),
}
};
let key = {
let key = std::io::Cursor::new(&identity.key[..]);
match load_rustls_private_key(key) {
Ok(key) => key,
Err(e) => {
return Err(e);
}
}
};
Ok((cert, key))
}
}