Skip to content

Commit

Permalink
Support mTLS for dynamic backends.
Browse files Browse the repository at this point in the history
  • Loading branch information
acw committed Aug 3, 2023
1 parent bef5e5e commit 7705f67
Show file tree
Hide file tree
Showing 12 changed files with 156 additions and 36 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,14 @@ opt-level = 1

[workspace.dependencies]
anyhow = "1.0.31"
base64 = "0.21.2"
hyper = { version = "=0.14.26", features = ["full"] }
itertools = "0.10.5"
rustls = { version = "0.21.5", features = ["dangerous_configuration"] }
rustls-pemfile = "1.0.3"
serde_json = "1.0.59"
tokio = { version = "1.21.2", features = ["full"] }
tokio-rustls = "0.24.1"
tracing = "0.1.37"
tracing-futures = "0.2.5"
futures = "0.3.24"
Expand Down
1 change: 1 addition & 0 deletions cli/tests/integration/common/backends.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ impl TestBackends {
override_host: backend.override_host.clone(),
cert_host: backend.cert_host.clone(),
use_sni: backend.use_sni,
client_cert: None,
};
backends.insert(name.to_string(), Arc::new(backend_config));
}
Expand Down
1 change: 1 addition & 0 deletions cli/tests/trap-test/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ lazy_static = "^1.4.0"
regex = "^1.3.9"
rustls = "^0.21.1"
rustls-native-certs = "^0.6.3"
rustls-pemfile = "^1.0.3"
semver = "^0.10.0"
serde = "^1.0.145"
serde_derive = "^1.0.114"
Expand Down
2 changes: 1 addition & 1 deletion lib/compute-at-edge-abi/typenames.witx
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@
$ciphers
$sni_hostname
$dont_pool
$client_cer
$client_cert
))

(typename $dynamic_backend_config
Expand Down
2 changes: 1 addition & 1 deletion lib/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ pub type Dictionaries = HashMap<DictionaryName, Dictionary>;
/// Types and deserializers for backend configuration settings.
mod backends;

pub use self::backends::Backend;
pub use self::backends::{Backend, ClientCertError, ClientCertInfo};

pub type Backends = HashMap<String, Arc<Backend>>;

Expand Down
7 changes: 7 additions & 0 deletions lib/src/config/backends.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
mod client_cert_info;

use {
hyper::{header::HeaderValue, Uri},
std::{collections::HashMap, sync::Arc},
};

pub use self::client_cert_info::{ClientCertError, ClientCertInfo};

/// A single backend definition.
#[derive(Clone, Debug)]
pub struct Backend {
pub uri: Uri,
pub override_host: Option<HeaderValue>,
pub cert_host: Option<String>,
pub use_sni: bool,
pub client_cert: Option<ClientCertInfo>,
}

/// A map of [`Backend`] definitions, keyed by their name.
Expand Down Expand Up @@ -128,6 +133,8 @@ mod deserialization {
override_host,
cert_host,
use_sni,
// NOTE: Update when we support client certs in static backends
client_cert: None,
})
}
}
Expand Down
65 changes: 65 additions & 0 deletions lib/src/config/backends/client_cert_info.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use rustls::{Certificate, PrivateKey};
use std::fmt;
use std::io::Cursor;

#[derive(Clone)]
pub struct ClientCertInfo {
certificates: Vec<Certificate>,
key: PrivateKey,
}

impl fmt::Debug for ClientCertInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.certs().fmt(f)
}
}

#[derive(Debug, thiserror::Error)]
pub enum ClientCertError {
#[error("Certificate/key read error: {0}")]
CertificateRead(#[from] std::io::Error),
#[error("No keys found for client certificate")]
NoKeysFound,
#[error("Too many keys found for client certificate (found {0})")]
TooManyKeys(usize),
}

impl ClientCertInfo {
pub fn new(certificate_bytes: &[u8], certificate_key: &[u8]) -> Result<Self, ClientCertError> {
let mut certificate_bytes_reader = Cursor::new(certificate_bytes);
let mut key_bytes_reader = Cursor::new(certificate_key);
let cert_info = rustls_pemfile::read_all(&mut certificate_bytes_reader)?;
let key_info = rustls_pemfile::read_all(&mut key_bytes_reader)?;

let mut certificates = Vec::new();
let mut keys = Vec::new();

for item in cert_info.into_iter().chain(key_info) {
match item {
rustls_pemfile::Item::X509Certificate(x) => certificates.push(Certificate(x)),
rustls_pemfile::Item::RSAKey(x) => keys.push(PrivateKey(x)),
rustls_pemfile::Item::PKCS8Key(x) => keys.push(PrivateKey(x)),
rustls_pemfile::Item::ECKey(x) => keys.push(PrivateKey(x)),
_ => {}
}
}

let key = if keys.is_empty() {
return Err(ClientCertError::NoKeysFound);
} else if keys.len() > 1 {
return Err(ClientCertError::TooManyKeys(keys.len()));
} else {
keys.remove(0)
};

Ok(ClientCertInfo { certificates, key })
}

pub fn certs(&self) -> Vec<Certificate> {
self.certificates.clone()
}

pub fn key(&self) -> PrivateKey {
self.key.clone()
}
}
5 changes: 4 additions & 1 deletion lib/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ pub enum Error {

#[error("String conversion error")]
ToStr(#[from] http::header::ToStrError),

#[error("invalid client certificate")]
InvalidClientCert(#[from] crate::config::ClientCertError),
}

impl Error {
Expand All @@ -152,7 +155,7 @@ impl Error {
Error::Unsupported { .. } => FastlyStatus::Unsupported,
Error::HandleError { .. } => FastlyStatus::Badf,
Error::InvalidStatusCode { .. } => FastlyStatus::Inval,
Error::UnknownBackend(_) => FastlyStatus::Inval,
Error::UnknownBackend(_) | Error::InvalidClientCert(_) => FastlyStatus::Inval,
// Map specific kinds of `hyper::Error` into their respective error codes.
Error::HyperError(e) if e.is_parse() => FastlyStatus::Httpinvalid,
Error::HyperError(e) if e.is_user() => FastlyStatus::Httpuser,
Expand Down
63 changes: 30 additions & 33 deletions lib/src/upstream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::{
use futures::Future;
use http::{uri, HeaderValue};
use hyper::{client::HttpConnector, header, Client, HeaderMap, Request, Response, Uri};
use rustls::client::ServerName;
use rustls::client::{ServerName, WantsTransparencyPolicyOrClientCert};
use std::{
io,
pin::Pin,
Expand All @@ -31,43 +31,36 @@ static GZIP_VALUES: [HeaderValue; 2] = [

/// Viceroy's preloaded TLS configuration.
///
/// Setting up client configuration is meant to be done once per process. However, we need
/// two distinct configurations, because backends may choose whether to employ SNI, and that
/// setting is baked into the configuration data.
/// We now have too many options to fully precompute this value, so what this actually
/// holds is a partially-complete TLS config builder, waiting for the point at which
/// we decide whether or not to provide a client certificate and whether or not to use
/// SNI.
#[derive(Clone)]
pub struct TlsConfig {
with_sni: Arc<rustls::ClientConfig>,
without_sni: Arc<rustls::ClientConfig>,
partial_config:
rustls::ConfigBuilder<rustls::ClientConfig, WantsTransparencyPolicyOrClientCert>,
}

fn setup_rustls(with_sni: bool) -> Result<rustls::ClientConfig, Error> {
let mut roots = rustls::RootCertStore::empty();
match rustls_native_certs::load_native_certs() {
Ok(certs) => {
for cert in certs {
roots.add(&rustls::Certificate(cert.0)).unwrap();
impl TlsConfig {
pub fn new() -> Result<TlsConfig, Error> {
let mut roots = rustls::RootCertStore::empty();
match rustls_native_certs::load_native_certs() {
Ok(certs) => {
for cert in certs {
roots.add(&rustls::Certificate(cert.0)).unwrap();
}
}
Err(err) => return Err(Error::BadCerts(err)),
}
if roots.is_empty() {
warn!("no CA certificates available");
}
Err(err) => return Err(Error::BadCerts(err)),
}
if roots.is_empty() {
warn!("no CA certificates available");
}

let mut config = rustls::ClientConfig::builder()
.with_safe_defaults()
.with_root_certificates(roots)
.with_no_client_auth();
config.enable_sni = with_sni;
Ok(config)
}
let partial_config = rustls::ClientConfig::builder()
.with_safe_defaults()
.with_root_certificates(roots);

impl TlsConfig {
pub fn new() -> Result<TlsConfig, Error> {
Ok(TlsConfig {
with_sni: Arc::new(setup_rustls(true)?),
without_sni: Arc::new(setup_rustls(false)?),
})
Ok(TlsConfig { partial_config })
}
}

Expand Down Expand Up @@ -127,11 +120,15 @@ impl hyper::service::Service<Uri> for BackendConnector {
let tcp = connect_fut.await.map_err(Box::new)?;

if backend.uri.scheme_str() == Some("https") {
let connector = if backend.use_sni {
TlsConnector::from(config.with_sni.clone())
let mut config = if let Some(certed_key) = &backend.client_cert {
config
.partial_config
.with_client_auth_cert(certed_key.certs(), certed_key.key())?
} else {
TlsConnector::from(config.without_sni.clone())
config.partial_config.with_no_client_auth()
};
config.enable_sni = backend.use_sni;
let connector = TlsConnector::from(Arc::new(config));

let cert_host = backend
.cert_host
Expand Down
40 changes: 40 additions & 0 deletions lib/src/wiggle_abi/req_impl.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
//! fastly_req` hostcall implementations.

use super::SecretStoreError;
use crate::config::ClientCertInfo;
use crate::secret_store::SecretLookup;

use {
crate::{
config::Backend,
Expand Down Expand Up @@ -310,6 +314,41 @@ impl FastlyHttpReq for Session {
true
};

let client_cert = if backend_info_mask.contains(BackendConfigOptions::CLIENT_CERT) {
let cert_slice = config
.client_certificate
.as_array(config.client_certificate_len)
.as_slice()?
.ok_or(Error::SharedMemory)?;
let key_lookup =
self.secret_lookup(config.client_key)
.ok_or(Error::SecretStoreError(
SecretStoreError::InvalidSecretHandle(config.client_key),
))?;
let key = match &key_lookup {
SecretLookup::Standard {
store_name,
secret_name,
} => self
.secret_stores()
.get_store(store_name)
.ok_or(Error::SecretStoreError(
SecretStoreError::InvalidSecretHandle(config.client_key),
))?
.get_secret(secret_name)
.ok_or(Error::SecretStoreError(
SecretStoreError::InvalidSecretHandle(config.client_key),
))?
.plaintext(),

SecretLookup::Injected { plaintext } => plaintext,
};

Some(ClientCertInfo::new(&cert_slice, key)?)
} else {
None
};

let new_backend = Backend {
uri: Uri::builder()
.scheme(scheme)
Expand All @@ -319,6 +358,7 @@ impl FastlyHttpReq for Session {
override_host,
cert_host,
use_sni,
client_cert,
};

if !self.add_backend(name, new_backend) {
Expand Down

0 comments on commit 7705f67

Please sign in to comment.