From fe291b617aa43cde36847e35b1610d032978c73e Mon Sep 17 00:00:00 2001 From: Lann Martin Date: Mon, 28 Feb 2022 10:47:52 -0500 Subject: [PATCH] DO NOT COMMIT: Add server support for mTLS auth Fixes #312 --- Cargo.lock | 196 ++++++++++++++++++------- Cargo.toml | 5 +- bin/server.rs | 84 +++++++++-- src/authn/{always/mod.rs => always.rs} | 4 +- src/authn/http_basic.rs | 17 +-- src/authn/mod.rs | 11 +- src/authn/oidc.rs | 15 +- src/authn/tls.rs | 58 ++++++++ src/authz/always.rs | 8 +- src/authz/mod.rs | 6 +- src/server/filters.rs | 19 ++- src/server/mod.rs | 12 +- 12 files changed, 327 insertions(+), 108 deletions(-) rename src/authn/{always/mod.rs => always.rs} (69%) create mode 100644 src/authn/tls.rs diff --git a/Cargo.lock b/Cargo.lock index 9f4b782..8db63dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,6 +43,45 @@ version = "1.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84450d0b4a8bd1ba4144ce8ce718fbc5d071358b1e5384bace6536b3d1f2d5b3" +[[package]] +name = "asn1-rs" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ff05a702273012438132f449575dbc804e27b2f3cbe3069aa237d26c98fa33" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror", + "time 0.3.7", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8b7511298d5b7784b40b092d9e9dcd3a627a5707e4b5e507931ab0d44eeebf" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-compression" version = "0.3.11" @@ -136,7 +175,7 @@ dependencies = [ "sled", "tempfile", "thiserror", - "time 0.3.5", + "time 0.3.7", "tokio", "tokio-stream", "tokio-tar", @@ -147,6 +186,7 @@ dependencies = [ "tracing-subscriber", "url", "warp", + "x509-parser", ] [[package]] @@ -377,6 +417,20 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" +[[package]] +name = "der-parser" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe398ac75057914d7d07307bf67dc7f3f574a26783b4fc7805a20ffa9f506e82" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint 0.4.3", + "num-traits", + "rusticata-macros", +] + [[package]] name = "digest" version = "0.9.0" @@ -417,6 +471,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "displaydoc" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bf95dc3f046b9da4f2d51833c0d3547d8564ef6910f5c1ed130306a75b92886" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "ed25519" version = "1.3.0" @@ -807,9 +872,9 @@ checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac" dependencies = [ "http", "hyper", - "rustls 0.20.2", + "rustls", "tokio", - "tokio-rustls 0.23.2", + "tokio-rustls", ] [[package]] @@ -987,6 +1052,12 @@ dependencies = [ "unicase", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.4.4" @@ -1055,6 +1126,17 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nom" +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d11e1ef389c76fe5b81bcaf2ea32cf88b62bc494e19f493d0b30e7a930109" +dependencies = [ + "memchr", + "minimal-lexical", + "version_check", +] + [[package]] name = "ntapi" version = "0.3.6" @@ -1161,6 +1243,15 @@ dependencies = [ "libc", ] +[[package]] +name = "num_threads" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ba99ba6393e2c3734791401b66902d981cb03bf190af674ca69949b6d5fb15" +dependencies = [ + "libc", +] + [[package]] name = "oauth2" version = "4.1.0" @@ -1181,6 +1272,15 @@ dependencies = [ "url", ] +[[package]] +name = "oid-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e20717fa0541f39bd146692035c37bedfa532b3e5071b35761082407546b2a" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.9.0" @@ -1554,14 +1654,14 @@ dependencies = [ "native-tls", "percent-encoding", "pin-project-lite", - "rustls 0.20.2", + "rustls", "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", "tokio", "tokio-native-tls", - "tokio-rustls 0.23.2", + "tokio-rustls", "tokio-util", "url", "wasm-bindgen", @@ -1609,16 +1709,12 @@ dependencies = [ ] [[package]] -name = "rustls" -version = "0.19.1" +name = "rusticata-macros" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" dependencies = [ - "base64", - "log", - "ring", - "sct 0.6.1", - "webpki 0.21.4", + "nom", ] [[package]] @@ -1629,8 +1725,8 @@ checksum = "d37e5e2290f3e040b594b1a9e04377c2c671f1a1cfd9bfdef82106ac1c113f84" dependencies = [ "log", "ring", - "sct 0.7.0", - "webpki 0.22.0", + "sct", + "webpki", ] [[package]] @@ -1676,16 +1772,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" -[[package]] -name = "sct" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "sct" version = "0.7.0" @@ -1861,7 +1947,7 @@ dependencies = [ "num-bigint 0.4.3", "num-traits", "thiserror", - "time 0.3.5", + "time 0.3.7", ] [[package]] @@ -2014,12 +2100,13 @@ dependencies = [ [[package]] name = "time" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41effe7cfa8af36f439fac33861b66b049edc6f9a32331e2312660529c1c24ad" +checksum = "004cbc98f30fa233c61a38bc77e96a9106e65c88f2d3bef182ae952027e5753d" dependencies = [ - "itoa 0.4.8", + "itoa 1.0.1", "libc", + "num_threads", "quickcheck", "serde", "time-macros", @@ -2086,26 +2173,15 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-rustls" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6" -dependencies = [ - "rustls 0.19.1", - "tokio", - "webpki 0.21.4", -] - [[package]] name = "tokio-rustls" version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a27d5f2b839802bd8267fa19b0530f5a08b9c08cd417976be2a65d130fe1c11b" dependencies = [ - "rustls 0.20.2", + "rustls", "tokio", - "webpki 0.22.0", + "webpki", ] [[package]] @@ -2407,8 +2483,7 @@ dependencies = [ [[package]] name = "warp" version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cef4e1e9114a4b7f1ac799f16ce71c14de5778500c5450ec6b7b920c55b587e" +source = "git+https://github.com/lann/warp?rev=ce63dc437c2a177a551ae9bebf40222f24052946#ce63dc437c2a177a551ae9bebf40222f24052946" dependencies = [ "bytes", "futures-channel", @@ -2422,12 +2497,13 @@ dependencies = [ "multipart", "percent-encoding", "pin-project", + "rustls-pemfile", "scoped-tls", "serde", "serde_json", "serde_urlencoded", "tokio", - "tokio-rustls 0.22.0", + "tokio-rustls", "tokio-stream", "tokio-tungstenite", "tokio-util", @@ -2523,16 +2599,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webpki" -version = "0.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "webpki" version = "0.22.0" @@ -2549,7 +2615,7 @@ version = "0.22.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "552ceb903e957524388c4d3475725ff2c8b7960922063af6ce53c9a43da07449" dependencies = [ - "webpki 0.22.0", + "webpki", ] [[package]] @@ -2592,6 +2658,24 @@ dependencies = [ "winapi", ] +[[package]] +name = "x509-parser" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5f14bdbacc48cea8d2a3112fa141949ffb707d724b51a8a1e6a6091f6c26e38" +dependencies = [ + "asn1-rs", + "base64", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror", + "time 0.3.7", +] + [[package]] name = "xattr" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index 5b371e1..640aee2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ maintenance = { status = "actively-developed" } [features] default = ["server", "client", "caching", "test-tools"] -server = ["warp", "openid"] +server = ["warp", "openid", "x509-parser"] client = ["mime_guess", "dirs", "time"] caching = [] test-tools = [] @@ -48,7 +48,7 @@ semver = { version = "1.0.4", features = ["serde"] } tokio = { version = "1.11.0", features = ["full"] } tokio-util = { version = "0.6.8", features = ["io"] } tokio-stream = { version = "0.1.7", features = ["fs"] } -warp = { version = "0.3", features = ["tls"], optional = true } +warp = { git = "https://github.com/lann/warp", rev = "ce63dc437c2a177a551ae9bebf40222f24052946", features = ["tls"], optional = true } bytes = "1.1.0" async-trait = "0.1.51" futures = "0.3.17" @@ -78,6 +78,7 @@ time = { version = "0.3", features = ["serde"], optional = true } atty = {version = "0.2", optional = true} async-compression = { version = "0.3", default-features = false, features = ["tokio", "gzip"]} tokio-tar = "0.3" +x509-parser = { version = "0.13", optional = true } # NOTE: This is a workaround due to a dependency issue in oauth2: https://github.com/tkaitchuck/ahash/issues/95#issuecomment-903560879 indexmap = "~1.6.2" diff --git a/bin/server.rs b/bin/server.rs index b80d403..a9d3c80 100644 --- a/bin/server.rs +++ b/bin/server.rs @@ -17,6 +17,8 @@ enum AuthType { Oidc(String, String, String), /// Use an HTPassword file at the given path HttpBasic(PathBuf), + /// Use mutual TLS authentication with the given CA certificate file + Tls, /// Do not perform auth. None, } @@ -142,6 +144,15 @@ struct Opts { )] oidc_issuer_url: Option, + #[clap( + name = "tls-auth-ca", + long = "tls-auth-ca", + env = "BINDLE_TLS_AUTH_CA", + requires = "cert_path", + help = "If set, enable client certificate TLS auth, with certificates validated by the given CA certificate path." + )] + tls_auth_ca_path: Option, + #[clap( name = "unauthenticated", long = "unauthenticated", @@ -220,6 +231,7 @@ async fn main() -> anyhow::Result<()> { key_path: config .key_path .expect("--key-path should be set if --cert-path was set"), + auth_ca_path: config.tls_auth_ca_path.clone(), }), }; @@ -242,22 +254,32 @@ async fn main() -> anyhow::Result<()> { bindle_directory.display() ); - let auth_method = if config.oidc_client_id.is_some() { + let mut auth_methods = vec![]; + if config.oidc_client_id.is_some() { // We can unwrap safely here because Clap checks that all args exist and we already // checked that one of them exists - AuthType::Oidc( + auth_methods.push(AuthType::Oidc( config.oidc_client_id.unwrap(), config.oidc_issuer_url.unwrap(), config.oidc_device_url.unwrap(), - ) - } else if let Some(htpasswd) = config.htpasswd_file { - AuthType::HttpBasic(htpasswd) - } else if config.unauthenticated { - AuthType::None - } else { - anyhow::bail!( - "An authentication method must be specified. Use --unauthenticated to run server without authentication" - ); + )); + } + if let Some(htpasswd) = config.htpasswd_file { + auth_methods.push(AuthType::HttpBasic(htpasswd)); + } + if config.tls_auth_ca_path.is_some() { + auth_methods.push(AuthType::Tls); + } + if config.unauthenticated { + auth_methods.push(AuthType::None); + } + let auth_method = match auth_methods.len() { + 1 => auth_methods.into_iter().next().unwrap(), + 0 => anyhow::bail!( + "An authentication method must be specified. Use --unauthenticated to run server without authentication" + ), + // TODO: support multiple simultaneous auth methods + _ => anyhow::bail!("Only one authentication method may be specified."), }; // TODO: This is really gnarly, but the associated type on `Authenticator` makes turning it into @@ -385,6 +407,45 @@ async fn main() -> anyhow::Result<()> { ) .await } + // DB with TLS auth + (true, AuthType::Tls) => { + warn!("Using EmbeddedProvider. This is currently experimental"); + info!("Auth mode: TLS client certificates"); + let store = + provider::embedded::EmbeddedProvider::new(&bindle_directory, index.clone()).await?; + let authn = bindle::authn::tls::TlsAuthenticator::new(); + server( + store, + index, + authn, + bindle::authz::anonymous_get::AnonymousGet, + addr, + tls, + secret_store, + strategy, + keyring, + ) + .await + } + // File system with TLS auth + (false, AuthType::Tls) => { + info!("Using FileProvider"); + info!("Auth mode: TLS client certificates"); + let store = provider::file::FileProvider::new(&bindle_directory, index.clone()).await; + let authn = bindle::authn::tls::TlsAuthenticator::new(); + server( + store, + index, + authn, + bindle::authz::anonymous_get::AnonymousGet, + addr, + tls, + secret_store, + strategy, + keyring, + ) + .await + } } } @@ -472,6 +533,7 @@ async fn merged_opts() -> anyhow::Result { signing_file: opts.signing_file.or(config.signing_file), use_embedded_db: opts.use_embedded_db || config.use_embedded_db, verification_strategy: opts.verification_strategy.or(config.verification_strategy), + tls_auth_ca_path: opts.tls_auth_ca_path.or(config.tls_auth_ca_path), }) } diff --git a/src/authn/always/mod.rs b/src/authn/always.rs similarity index 69% rename from src/authn/always/mod.rs rename to src/authn/always.rs index 6475779..7f88b3f 100644 --- a/src/authn/always/mod.rs +++ b/src/authn/always.rs @@ -1,4 +1,4 @@ -use super::Authenticator; +use super::{Authenticator, AuthData}; use crate::authz::always::Anonymous; /// An authenticator that simply returns an anonymous user @@ -9,7 +9,7 @@ pub struct AlwaysAuthenticate; impl Authenticator for AlwaysAuthenticate { type Item = Anonymous; - async fn authenticate(&self, _auth_data: &str) -> anyhow::Result { + async fn authenticate(&self, _auth_data: &AuthData) -> anyhow::Result { Ok(Anonymous) } } diff --git a/src/authn/http_basic.rs b/src/authn/http_basic.rs index 0ac47ce..5c65806 100644 --- a/src/authn/http_basic.rs +++ b/src/authn/http_basic.rs @@ -1,6 +1,6 @@ use std::{collections::HashMap, path::Path}; -use super::Authenticator; +use super::{Authenticator, AuthData}; use crate::authz::Authorizable; /// HTTP header prefix @@ -81,12 +81,13 @@ impl HttpBasic { impl Authenticator for HttpBasic { type Item = HttpUser; - async fn authenticate(&self, auth_data: &str) -> anyhow::Result { - if auth_data.is_empty() { + async fn authenticate(&self, auth_data: &AuthData) -> anyhow::Result { + let auth_header = auth_data.auth_header.as_deref().unwrap_or_default(); + if auth_header.is_empty() { anyhow::bail!("Username and password are required") } - let (username, password) = parse_basic(auth_data)?; + let (username, password) = parse_basic(auth_header)?; match self.check_credentials(&username, &password) { true => Ok(HttpUser { username }), false => anyhow::bail!("Authentication failed"), @@ -117,12 +118,8 @@ pub struct HttpUser { } impl Authorizable for HttpUser { - fn principal(&self) -> String { - self.username.clone() - } - - fn groups(&self) -> Vec { - Vec::with_capacity(0) + fn principal(&self) -> &str { + self.username.as_ref() } } diff --git a/src/authn/mod.rs b/src/authn/mod.rs index a66151d..3063f2d 100644 --- a/src/authn/mod.rs +++ b/src/authn/mod.rs @@ -4,9 +4,18 @@ pub mod always; pub mod http_basic; pub mod oidc; +pub mod tls; + +use warp::mtls::Certificates; use crate::authz::Authorizable; +#[non_exhaustive] +pub struct AuthData { + pub auth_header: Option, + pub peer_certs: Option, +} + /// A trait that can be implemented by any system able to authenticate a request #[async_trait::async_trait] pub trait Authenticator { @@ -16,7 +25,7 @@ pub trait Authenticator { /// Authenticate the request given the arbitrary `auth_data`, returning an arbitrary error in /// case of a failure. This data will likely be the value of the Authorization header. Anonymous /// auth will be indicated by an empty auth_data string - async fn authenticate(&self, auth_data: &str) -> anyhow::Result; + async fn authenticate(&self, auth_data: &AuthData) -> anyhow::Result; // TODO(thomastaylor312): Perhaps we should create a single method that returns another trait // implementing type for actually authenticating with a service. That way we can encapsulate all diff --git a/src/authn/oidc.rs b/src/authn/oidc.rs index c0a5ca1..7f2cb29 100644 --- a/src/authn/oidc.rs +++ b/src/authn/oidc.rs @@ -11,7 +11,7 @@ use std::{ sync::Arc, }; -use super::Authenticator; +use super::{Authenticator, AuthData}; use crate::authz::Authorizable; const ONE_HOUR: Duration = Duration::from_secs(3600); @@ -202,11 +202,12 @@ struct Claims { impl Authenticator for OidcAuthenticator { type Item = OidcUser; - async fn authenticate(&self, auth_data: &str) -> anyhow::Result { + async fn authenticate(&self, auth_data: &AuthData) -> anyhow::Result { + let auth_header = auth_data.auth_header.as_deref().unwrap_or_default(); // This is the raw auth data, so we need to chop off the "Bearer" part of the header data // with any starting whitespace. I am not using to_lowercase to avoid an extra string // allocation - let raw_token = auth_data + let raw_token = auth_header .trim_start_matches("Bearer") .trim_start_matches("bearer") .trim(); @@ -249,12 +250,12 @@ pub struct OidcUser { } impl Authorizable for OidcUser { - fn principal(&self) -> String { - self.principal.clone() + fn principal(&self) -> &str { + self.principal.as_ref() } - fn groups(&self) -> Vec { - self.groups.clone() + fn groups(&self) -> &[String] { + self.groups.as_ref() } } diff --git a/src/authn/tls.rs b/src/authn/tls.rs new file mode 100644 index 0000000..c8d27c7 --- /dev/null +++ b/src/authn/tls.rs @@ -0,0 +1,58 @@ +use anyhow::bail; +use x509_parser::parse_x509_certificate; + +use super::{AuthData, Authenticator}; +use crate::authz::Authorizable; + +/// An authenticator that checks for a (preauthenticated) client certificate +#[derive(Clone, Debug, Default)] +pub struct TlsAuthenticator(()); + +impl TlsAuthenticator { + pub fn new() -> Self { + Self(()) + } +} + +#[async_trait::async_trait] +impl Authenticator for TlsAuthenticator { + type Item = ClientCertInfo; + + async fn authenticate(&self, auth_data: &AuthData) -> anyhow::Result { + let peer_cert = match auth_data + .peer_certs + .as_ref() + .and_then(|certs| certs.as_ref().last()) + .map(|cert| parse_x509_certificate(cert.as_ref())) + .transpose()? + { + // Sanity check: should have been None instead of Some() + None => bail!("no certificate in peer_certificates!"), + // Sanity check: valid Certificate should have just enough data + Some((extra_data, _)) if !extra_data.is_empty() => { + bail!("extra data in peer certificate!") + } + Some((_, cert)) => cert, + }; + + let common_name = match peer_cert.subject().iter_common_name().collect::>().as_slice() { + &[name] => name.as_str()?.to_string(), + names => bail!("peer certificate has {} common names; expected 1", names.len()) + }; + + // TODO(lann): Could populate groups from other subject parts + + Ok(ClientCertInfo { common_name }) + } +} + +/// Represents authenticated client certificate info. +pub struct ClientCertInfo { + common_name: String, +} + +impl Authorizable for ClientCertInfo { + fn principal(&self) -> &str { + self.common_name.as_ref() + } +} \ No newline at end of file diff --git a/src/authz/always.rs b/src/authz/always.rs index 8ebf638..4f5c489 100644 --- a/src/authz/always.rs +++ b/src/authz/always.rs @@ -7,12 +7,8 @@ use super::{Authorizable, Authorizer}; pub struct Anonymous; impl Authorizable for Anonymous { - fn principal(&self) -> String { - String::new() - } - - fn groups(&self) -> Vec { - Vec::with_capacity(0) + fn principal(&self) -> &str { + "" } } diff --git a/src/authz/mod.rs b/src/authz/mod.rs index 8b46cbb..4037afc 100644 --- a/src/authz/mod.rs +++ b/src/authz/mod.rs @@ -8,11 +8,13 @@ pub mod anonymous_get; /// can be authorized by an [`Authorizer`](Authorizer) pub trait Authorizable { /// Returns the identity or username of the authenticated user - fn principal(&self) -> String; + fn principal(&self) -> &str; /// Returns the groups the authenticated user is a member of, generally embedded on something /// like a JWT or fetched from an upstream server - fn groups(&self) -> Vec; + fn groups(&self) -> &[String] { + &[] + } } /// A trait for any system that can authorize any [`Authorizable`](Authorizable) type diff --git a/src/server/filters.rs b/src/server/filters.rs index d5fbd47..91aa080 100644 --- a/src/server/filters.rs +++ b/src/server/filters.rs @@ -7,11 +7,12 @@ use serde::de::DeserializeOwned; use serde::Deserialize; use tracing::{debug, instrument, trace, warn}; use tracing_futures::Instrument; +use warp::mtls::Certificates; use warp::reject::{custom, Reject, Rejection}; use warp::Filter; use super::TOML_MIME_TYPE; -use crate::authn::Authenticator; +use crate::authn::{Authenticator, AuthData}; use crate::authz::always::Anonymous; use crate::authz::Authorizer; @@ -102,20 +103,22 @@ fn authenticate( warp::any() .map(move || authn.clone()) .and(warp::header::optional::("Authorization")) + .and(warp::mtls::peer_certificates()) .and_then(_authenticate) } -#[instrument(level = "trace", skip(authn, auth_data), name = "authentication")] +#[instrument(level = "trace", skip(authn, auth_header, peer_certs), name = "authentication")] async fn _authenticate( authn: A, - auth_data: Option, + auth_header: Option, + peer_certs: Option ) -> Result, Rejection> { - let data = match auth_data { - Some(s) => s, + if let (None, None) = (&auth_header, &peer_certs) { // If we had no auth data, that means this is anonymous - None => return Ok(Either::Left(Anonymous)), - }; - match authn.authenticate(&data).await { + return Ok(Either::Left(Anonymous)); + } + let auth_data = AuthData{ auth_header, peer_certs }; + match authn.authenticate(&auth_data).await { Ok(a) => Ok(Either::Right(a)), Err(e) => { debug!(error = %e, "Authentication error"); diff --git a/src/server/mod.rs b/src/server/mod.rs index bd2da17..cecd3fe 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -24,6 +24,7 @@ pub(crate) const JSON_MIME_TYPE: &str = "application/json"; pub struct TlsConfig { pub cert_path: PathBuf, pub key_path: PathBuf, + pub auth_ca_path: Option, } /// Returns a future that runs a server until it receives a SIGINT to stop. If optional TLS @@ -71,12 +72,17 @@ where Some(config) => { debug!( ?config.key_path, - ?config.cert_path, "Got TLS config, starting server in HTTPS mode", + ?config.cert_path, + ?config.auth_ca_path, "Got TLS config, starting server in HTTPS mode", ); - server + let mut tls_server = server .tls() .key_path(config.key_path) - .cert_path(config.cert_path) + .cert_path(config.cert_path); + if let Some(auth_ca_path) = config.auth_ca_path { + tls_server = tls_server.client_auth_optional_path(auth_ca_path); + } + tls_server .bind_with_graceful_shutdown(addr, shutdown_signal()) .1 .await