diff --git a/Cargo.lock b/Cargo.lock index 4a220eaa92..62c28fe5cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4766,6 +4766,7 @@ dependencies = [ "pbjson-types", "penumbra-app", "penumbra-asset", + "penumbra-auto-https", "penumbra-compact-block", "penumbra-custody", "penumbra-dex", @@ -4790,8 +4791,6 @@ dependencies = [ "regex", "reqwest", "rocksdb", - "rustls 0.20.9", - "rustls-acme", "serde", "serde_json", "serde_with 1.14.0", @@ -4963,6 +4962,18 @@ dependencies = [ "tracing", ] +[[package]] +name = "penumbra-auto-https" +version = "0.65.0-alpha.1" +dependencies = [ + "anyhow", + "axum-server", + "futures", + "rustls 0.20.9", + "rustls-acme", + "tracing", +] + [[package]] name = "penumbra-bench" version = "0.65.0-alpha.1" diff --git a/Cargo.toml b/Cargo.toml index 4f2699fe54..19644ad80c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ members = [ "crates/custody", "crates/wallet", "crates/view", + "crates/util/auto-https", "crates/util/tendermint-proxy", "crates/util/tower-trace", "crates/bin/pd", diff --git a/crates/bin/pd/Cargo.toml b/crates/bin/pd/Cargo.toml index b4af915074..2e08780527 100644 --- a/crates/bin/pd/Cargo.toml +++ b/crates/bin/pd/Cargo.toml @@ -43,6 +43,7 @@ penumbra-app = { path = "../../core/app" } penumbra-custody = { path = "../../custody" } penumbra-tower-trace = { path = "../../util/tower-trace" } penumbra-tendermint-proxy = { path = "../../util/tendermint-proxy" } +penumbra-auto-https = { path = "../../util/auto-https" } # Penumbra dependencies decaf377 = { version = "0.5", features = ["parallel"] } @@ -125,8 +126,6 @@ atty = "0.2" fs_extra = "1.3.0" axum-server = { version = "0.4.7", features = ["tls-rustls"] } -rustls = "0.20.9" -rustls-acme = { version = "0.6.0", features = ["axum"] } [dev-dependencies] penumbra-proof-params = { path = "../../crypto/proof-params", features = [ diff --git a/crates/bin/pd/src/auto_https.rs b/crates/bin/pd/src/auto_https.rs index 6c98b8aaa1..e69de29bb2 100644 --- a/crates/bin/pd/src/auto_https.rs +++ b/crates/bin/pd/src/auto_https.rs @@ -1,88 +0,0 @@ -//! Automatic HTTPS certificate management facilities. -//! -//! See [`axum_acceptor`] for more information. - -use { - anyhow::Error, - futures::Future, - rustls::ServerConfig, - rustls_acme::{axum::AxumAcceptor, caches::DirCache, AcmeConfig, AcmeState}, - std::{fmt::Debug, path::PathBuf, sync::Arc}, -}; - -/// Protocols supported by this server, in order of preference. -/// -/// See [rfc7301] for more info on ALPN. -/// -/// [rfc7301]: https://datatracker.ietf.org/doc/html/rfc7301 -// -// We also permit HTTP1.1 for backwards-compatibility, specifically for grpc-web. -const ALPN_PROTOCOLS: [&[u8]; 2] = [b"h2", b"http/1.1"]; - -/// The location of the file-based certificate cache. -// NB: this must not be an absolute path see [Path::join]. -const CACHE_DIR: &str = "tokio_rustls_acme_cache"; - -/// Use ACME to resolve certificates and handle new connections. -/// -/// This returns a tuple containing an [`AxumAcceptor`] that may be used with [`axum_server`], and -/// a [`Future`] that represents the background task to poll and log for changes in the -/// certificate environment. -pub fn axum_acceptor( - home: PathBuf, - domain: String, - production_api: bool, -) -> (AxumAcceptor, impl Future>) { - // Use a file-based cache located within the home directory. - let cache = home.join(CACHE_DIR); - let cache = DirCache::new(cache); - - // Create an ACME client, which we will use to resolve certificates. - let state = AcmeConfig::new(vec![domain]) - .cache(cache) - .directory_lets_encrypt(production_api) - .state(); - - // Define our server configuration, using the ACME certificate resolver. - let mut rustls_config = ServerConfig::builder() - .with_safe_defaults() - .with_no_client_auth() - .with_cert_resolver(state.resolver()); - rustls_config.alpn_protocols = self::alpn_protocols(); - let rustls_config = Arc::new(rustls_config); - - // Return our connection acceptor and our background worker task. - let acceptor = state.axum_acceptor(rustls_config.clone()); - let worker = self::acme_worker(state); - (acceptor, worker) -} - -/// This function defines the task responsible for handling ACME events. -/// -/// This function will never return, unless an error is encountered. -#[tracing::instrument(level = "error", skip_all)] -async fn acme_worker(mut state: AcmeState) -> Result<(), anyhow::Error> -where - EC: Debug + 'static, - EA: Debug + 'static, -{ - use futures::StreamExt; - loop { - match state.next().await { - Some(Ok(ok)) => tracing::debug!("received acme event: {:?}", ok), - Some(Err(err)) => tracing::error!("acme error: {:?}", err), - None => { - debug_assert!(false, "acme worker unexpectedly reached end-of-stream"); - tracing::error!("acme worker unexpectedly reached end-of-stream"); - anyhow::bail!("unexpected end-of-stream"); - } - } - } -} - -/// Returns a vector of the protocols supported by this server. -/// -/// This is a convenience method to retrieve an owned copy of [`ALPN_PROTOCOLS`]. -fn alpn_protocols() -> Vec> { - ALPN_PROTOCOLS.into_iter().map(<[u8]>::to_vec).collect() -} diff --git a/crates/bin/pd/src/lib.rs b/crates/bin/pd/src/lib.rs index cd30c0ae3e..f6d3efd838 100644 --- a/crates/bin/pd/src/lib.rs +++ b/crates/bin/pd/src/lib.rs @@ -11,7 +11,6 @@ mod mempool; mod metrics; mod snapshot; -pub mod auto_https; pub mod cli; pub mod events; pub mod migrate; diff --git a/crates/bin/pd/src/main.rs b/crates/bin/pd/src/main.rs index f441f5b61a..5aabd2f616 100644 --- a/crates/bin/pd/src/main.rs +++ b/crates/bin/pd/src/main.rs @@ -293,7 +293,7 @@ async fn main() -> anyhow::Result<()> { let grpc_server = match grpc_auto_https { Some(domain) => { let (acceptor, acme_worker) = - pd::auto_https::axum_acceptor(pd_home, domain, !acme_staging); + penumbra_auto_https::axum_acceptor(pd_home, domain, !acme_staging); // TODO(kate): we should eventually propagate errors from the ACME worker task. tokio::spawn(acme_worker); spawn_grpc_server!(grpc_server.acceptor(acceptor)) diff --git a/crates/util/auto-https/Cargo.toml b/crates/util/auto-https/Cargo.toml new file mode 100644 index 0000000000..bd73e46dc5 --- /dev/null +++ b/crates/util/auto-https/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "penumbra-auto-https" +version = "0.65.0-alpha.1" +authors = ["Penumbra Labs "] +edition = "2021" +description = "Automatic HTTPS management for Penumbra" +repository = "https://github.com/penumbra-zone/penumbra/" +homepage = "https://penumbra.zone" +license = "MIT OR Apache-2.0" +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1" +futures = "0.3" +rustls = "0.20.9" +axum-server = { version = "0.4.7", features = [] } +rustls-acme = { version = "0.6.0", features = ["axum"] } +tracing = "0.1" diff --git a/crates/util/auto-https/src/lib.rs b/crates/util/auto-https/src/lib.rs new file mode 100644 index 0000000000..6c98b8aaa1 --- /dev/null +++ b/crates/util/auto-https/src/lib.rs @@ -0,0 +1,88 @@ +//! Automatic HTTPS certificate management facilities. +//! +//! See [`axum_acceptor`] for more information. + +use { + anyhow::Error, + futures::Future, + rustls::ServerConfig, + rustls_acme::{axum::AxumAcceptor, caches::DirCache, AcmeConfig, AcmeState}, + std::{fmt::Debug, path::PathBuf, sync::Arc}, +}; + +/// Protocols supported by this server, in order of preference. +/// +/// See [rfc7301] for more info on ALPN. +/// +/// [rfc7301]: https://datatracker.ietf.org/doc/html/rfc7301 +// +// We also permit HTTP1.1 for backwards-compatibility, specifically for grpc-web. +const ALPN_PROTOCOLS: [&[u8]; 2] = [b"h2", b"http/1.1"]; + +/// The location of the file-based certificate cache. +// NB: this must not be an absolute path see [Path::join]. +const CACHE_DIR: &str = "tokio_rustls_acme_cache"; + +/// Use ACME to resolve certificates and handle new connections. +/// +/// This returns a tuple containing an [`AxumAcceptor`] that may be used with [`axum_server`], and +/// a [`Future`] that represents the background task to poll and log for changes in the +/// certificate environment. +pub fn axum_acceptor( + home: PathBuf, + domain: String, + production_api: bool, +) -> (AxumAcceptor, impl Future>) { + // Use a file-based cache located within the home directory. + let cache = home.join(CACHE_DIR); + let cache = DirCache::new(cache); + + // Create an ACME client, which we will use to resolve certificates. + let state = AcmeConfig::new(vec![domain]) + .cache(cache) + .directory_lets_encrypt(production_api) + .state(); + + // Define our server configuration, using the ACME certificate resolver. + let mut rustls_config = ServerConfig::builder() + .with_safe_defaults() + .with_no_client_auth() + .with_cert_resolver(state.resolver()); + rustls_config.alpn_protocols = self::alpn_protocols(); + let rustls_config = Arc::new(rustls_config); + + // Return our connection acceptor and our background worker task. + let acceptor = state.axum_acceptor(rustls_config.clone()); + let worker = self::acme_worker(state); + (acceptor, worker) +} + +/// This function defines the task responsible for handling ACME events. +/// +/// This function will never return, unless an error is encountered. +#[tracing::instrument(level = "error", skip_all)] +async fn acme_worker(mut state: AcmeState) -> Result<(), anyhow::Error> +where + EC: Debug + 'static, + EA: Debug + 'static, +{ + use futures::StreamExt; + loop { + match state.next().await { + Some(Ok(ok)) => tracing::debug!("received acme event: {:?}", ok), + Some(Err(err)) => tracing::error!("acme error: {:?}", err), + None => { + debug_assert!(false, "acme worker unexpectedly reached end-of-stream"); + tracing::error!("acme worker unexpectedly reached end-of-stream"); + anyhow::bail!("unexpected end-of-stream"); + } + } + } +} + +/// Returns a vector of the protocols supported by this server. +/// +/// This is a convenience method to retrieve an owned copy of [`ALPN_PROTOCOLS`]. +fn alpn_protocols() -> Vec> { + ALPN_PROTOCOLS.into_iter().map(<[u8]>::to_vec).collect() +}