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:
committed by
Lucio Franco
parent
9079e0f66b
commit
335a373a40
@@ -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),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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()),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user