-
Notifications
You must be signed in to change notification settings - Fork 56
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2474 from jarhodes314/feat/file-transfer-tls
Add HTTPS/certificate authentication support to file transfer service
- Loading branch information
Showing
47 changed files
with
1,319 additions
and
419 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
use crate::load_cert; | ||
use crate::load_pkey; | ||
use crate::read_trust_store; | ||
use crate::ssl_config; | ||
use anyhow::anyhow; | ||
use anyhow::Context; | ||
use camino::Utf8Path; | ||
use rustls::RootCertStore; | ||
use std::fmt::Debug; | ||
use std::fs::File; | ||
use std::io; | ||
use std::io::stderr; | ||
use std::io::Cursor; | ||
use std::io::IsTerminal; | ||
use std::path::Path; | ||
use tedge_config::OptionalConfig; | ||
use tracing::info; | ||
use yansi::Paint; | ||
|
||
/// Loads the relevant [rustls::ServerConfig] from configured values for `cert_path`, `key_path` and `ca_path` | ||
/// | ||
/// In production use, all the paths should be passed in as [OptionalConfig]s from [TEdgeConfig] | ||
/// | ||
/// ```no_run | ||
/// # fn main() -> anyhow::Result<()> { | ||
/// use axum_tls::config::load_ssl_config; | ||
/// use tedge_config::TEdgeConfig; | ||
/// | ||
/// let config: TEdgeConfig = unimplemented!("read config"); | ||
/// | ||
/// let config = load_ssl_config( | ||
/// config.http.cert_path.as_ref(), | ||
/// config.http.key_path.as_ref(), | ||
/// config.http.ca_path.as_ref(), | ||
/// "File transfer service", | ||
/// )?; | ||
/// # Ok(()) | ||
/// # } | ||
/// ``` | ||
/// | ||
/// In a test, we can instead use [InjectedValue] | ||
/// | ||
/// ``` | ||
/// # fn main() -> anyhow::Result<()> { | ||
/// use rustls::RootCertStore; | ||
/// use axum_tls::config::{InjectedValue, load_ssl_config}; | ||
/// use tedge_config::{OptionalConfig, TEdgeConfig}; | ||
/// | ||
/// let cert = rcgen::generate_simple_self_signed(["localhost".to_owned()]).unwrap(); | ||
/// let cert_pem = cert.serialize_pem().unwrap(); | ||
/// let key_pem = cert.serialize_private_key_pem(); | ||
/// | ||
/// let config = load_ssl_config( | ||
/// OptionalConfig::present(InjectedValue(cert_pem), "http.cert_path"), | ||
/// OptionalConfig::present(InjectedValue(key_pem), "http.key_path"), | ||
/// OptionalConfig::<InjectedValue<RootCertStore>>::empty("http.ca_path"), | ||
/// "File transfer service", | ||
/// )?; | ||
/// # Ok(()) | ||
/// # } | ||
/// ``` | ||
/// | ||
pub fn load_ssl_config( | ||
cert_path: OptionalConfig<impl PemReader>, | ||
key_path: OptionalConfig<impl PemReader>, | ||
ca_path: OptionalConfig<impl TrustStoreLoader>, | ||
service_name: &'static str, | ||
) -> anyhow::Result<Option<rustls::ServerConfig>> { | ||
// TODO this could be moved somewhere more generic (e.g. where we initialize tracing subscriber) | ||
if !stderr().is_terminal() { | ||
yansi::Paint::disable(); | ||
} | ||
|
||
let enabled = Paint::green("enabled").bold(); | ||
let disabled = Paint::red("disabled").bold(); | ||
let service_name = Paint::default(service_name).bold(); | ||
let cert_key = cert_path.key(); | ||
let key_key = key_path.key(); | ||
let ca_key = ca_path.key(); | ||
if let Some((cert, key)) = load_certificate_and_key(cert_path, key_path)? { | ||
let trust_store = match ca_path.or_none() { | ||
Some(path) => path | ||
.load_trust_store() | ||
.map(Some) | ||
.with_context(|| format!("reading root certificates configured in `{ca_key}`",))?, | ||
None => None, | ||
}; | ||
let ca_state = if let Some(store) = &trust_store { | ||
let count = store.len(); | ||
format!("{enabled} ({count} certificates found)") | ||
} else { | ||
format!("{disabled}") | ||
}; | ||
|
||
info!(target: "HTTP Server", "{service_name} has HTTPS {enabled} (configured in `{cert_key}`/`{key_key}`) and certificate authentication {ca_state} (configured in `{ca_key}`)", ); | ||
Ok(Some(ssl_config(cert, key, trust_store)?)) | ||
} else { | ||
info!(target: "HTTP Server", "{service_name} has HTTPS {disabled} (configured in `{cert_key}`/`{key_key}`) and certificate authentication {disabled} (configured in `{ca_key}`)"); | ||
Ok(None) | ||
} | ||
} | ||
|
||
type CertKeyPair = (Vec<Vec<u8>>, Vec<u8>); | ||
|
||
fn load_certificate_and_key( | ||
cert_path: OptionalConfig<impl PemReader>, | ||
key_path: OptionalConfig<impl PemReader>, | ||
) -> anyhow::Result<Option<CertKeyPair>> { | ||
let paths = tedge_config::all_or_nothing((cert_path.as_ref(), key_path.as_ref())) | ||
.map_err(|e| anyhow!("{e}"))?; | ||
|
||
if let Some((cert_file, key_file)) = paths { | ||
Ok(Some(( | ||
load_cert(cert_file).with_context(|| { | ||
format!("reading certificate configured in `{}`", cert_path.key()) | ||
})?, | ||
load_pkey(key_file).with_context(|| { | ||
format!("reading private key configured in `{}`", key_path.key()) | ||
})?, | ||
))) | ||
} else { | ||
Ok(None) | ||
} | ||
} | ||
|
||
pub trait PemReader: Debug { | ||
type Read<'a>: io::Read | ||
where | ||
Self: 'a; | ||
|
||
fn open(&self) -> io::Result<Self::Read<'_>>; | ||
} | ||
|
||
pub trait TrustStoreLoader { | ||
fn load_trust_store(&self) -> anyhow::Result<RootCertStore>; | ||
} | ||
|
||
#[derive(Debug)] | ||
/// An injected value, used to avoid reading from the file system in unit tests | ||
/// | ||
/// For example, a certificate path can be replaced with an [InjectedValue<String>] | ||
/// where the [String] inside is a PEM-encoded certificate | ||
/// | ||
/// ``` | ||
/// use axum_tls::config::InjectedValue; | ||
/// use axum_tls::load_cert; | ||
/// let cert = rcgen::generate_simple_self_signed(["localhost".to_owned()]).unwrap(); | ||
/// let pem_data = cert.serialize_pem().unwrap(); | ||
/// | ||
/// let loaded_chain = load_cert(&InjectedValue(pem_data)).unwrap(); | ||
/// | ||
/// assert_eq!(loaded_chain.len(), 1); | ||
/// ``` | ||
pub struct InjectedValue<S>(pub S); | ||
|
||
impl PemReader for InjectedValue<String> { | ||
type Read<'a> = Cursor<&'a [u8]>; | ||
fn open(&self) -> io::Result<Self::Read<'_>> { | ||
Ok(Cursor::new(self.0.as_bytes())) | ||
} | ||
} | ||
|
||
impl<P: AsRef<Path> + Debug + ?Sized> PemReader for P { | ||
type Read<'a> = File where Self: 'a; | ||
fn open(&self) -> io::Result<File> { | ||
File::open(self) | ||
} | ||
} | ||
|
||
impl<P: AsRef<Utf8Path> + 'static> TrustStoreLoader for P { | ||
fn load_trust_store(&self) -> anyhow::Result<RootCertStore> { | ||
read_trust_store(self.as_ref()) | ||
} | ||
} | ||
|
||
impl TrustStoreLoader for InjectedValue<RootCertStore> { | ||
fn load_trust_store(&self) -> anyhow::Result<RootCertStore> { | ||
Ok(self.0.clone()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
use std::error::Error; | ||
|
||
#[cfg(any(test, feature = "test-helpers"))] | ||
pub fn assert_error_matches(err: reqwest::Error, alert_description: rustls::AlertDescription) { | ||
let rustls_err = match rustls_error_from_reqwest(&err) { | ||
Some(err) => err, | ||
None => panic!("{:?}", anyhow::Error::from(err)), | ||
}; | ||
assert_matches::assert_matches!( | ||
rustls_err, | ||
rustls::Error::AlertReceived(des) if des == &alert_description | ||
); | ||
} | ||
|
||
pub fn rustls_error_from_reqwest(err: &reqwest::Error) -> Option<&rustls::Error> { | ||
err.source()? | ||
.downcast_ref::<hyper::Error>()? | ||
.source()? | ||
.downcast_ref::<std::io::Error>()? | ||
.get_ref()? | ||
.downcast_ref::<rustls::Error>() | ||
} |
Oops, something went wrong.
ea254bf
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Robot Results