diff --git a/Cargo.lock b/Cargo.lock index 21b3cd8b..35c9cc36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4987,6 +4987,7 @@ version = "0.1.0" dependencies = [ "async-nats", "reqwest", + "rustls", "serde", "serde_json", "strum", diff --git a/Cargo.toml b/Cargo.toml index d01321e2..2dd60d00 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -254,6 +254,13 @@ async-nats = { version = "0.50", default-features = false, features = [ "nkeys", "ring", ] } +# Named only to install the process-wide provider (`swarm-queue-client`'s +# `install_crypto_provider`). `aws-lc-rs` is the provider reqwest already +# falls back to, so installing it leaves every HTTPS client unchanged. +rustls = { version = "0.23", default-features = false, features = [ + "aws-lc-rs", + "std", +] } data-encoding = "2" # The nkey *format* - ed25519 + base32 + CRC16. The primitives are already in # the tree; the format is not, and hand-rolling a key format on an auth path diff --git a/hive-agent/src/main.rs b/hive-agent/src/main.rs index 6756ff90..ac1e0ca2 100644 --- a/hive-agent/src/main.rs +++ b/hive-agent/src/main.rs @@ -104,6 +104,7 @@ struct Cli { #[tokio::main] async fn main() -> Result<()> { hive_log::init(); + swarm_queue_client::install_crypto_provider(); let cli = Cli::parse(); serve_main::(&cli.socket, cli.poll_ms).await diff --git a/hive-c0re/src/main.rs b/hive-c0re/src/main.rs index 13c29b58..8855946a 100644 --- a/hive-c0re/src/main.rs +++ b/hive-c0re/src/main.rs @@ -144,6 +144,7 @@ enum Cmd { #[tokio::main] async fn main() -> Result<()> { hive_log::init(); + swarm_queue_client::install_crypto_provider(); let cli = Cli::parse(); match cli.cmd { diff --git a/swarm-controller/src/main.rs b/swarm-controller/src/main.rs index cadeee0f..b7edb801 100644 --- a/swarm-controller/src/main.rs +++ b/swarm-controller/src/main.rs @@ -2242,6 +2242,7 @@ fn keep_forge_for_state( #[tokio::main] async fn main() -> Result<()> { hive_log::init(); + swarm_queue_client::install_crypto_provider(); let path = socket_path(); diff --git a/swarm-nats-auth/src/main.rs b/swarm-nats-auth/src/main.rs index 58dc5c8b..bfee3d5e 100644 --- a/swarm-nats-auth/src/main.rs +++ b/swarm-nats-auth/src/main.rs @@ -147,6 +147,7 @@ fn read_secret(path: &std::path::Path) -> anyhow::Result { #[tokio::main] async fn main() -> anyhow::Result<()> { + swarm_queue_client::install_crypto_provider(); tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::try_from_default_env() diff --git a/swarm-queue-client/Cargo.toml b/swarm-queue-client/Cargo.toml index 6b973d1a..b4dd258a 100644 --- a/swarm-queue-client/Cargo.toml +++ b/swarm-queue-client/Cargo.toml @@ -45,6 +45,7 @@ async-nats.workspace = true # feature's comment above), and `cargo check -p swarm-queue-client` alone # must not depend on what else is in the build. reqwest = { workspace = true, features = ["blocking"] } +rustls.workspace = true serde.workspace = true serde_json.workspace = true strum.workspace = true diff --git a/swarm-queue-client/src/lib.rs b/swarm-queue-client/src/lib.rs index a144b634..077eca8b 100644 --- a/swarm-queue-client/src/lib.rs +++ b/swarm-queue-client/src/lib.rs @@ -656,6 +656,18 @@ pub fn max_payload(client: &async_nats::Client) -> usize { client.server_info().max_payload } +/// Install `aws-lc-rs` as the process-wide rustls crypto provider. Call it +/// first thing in `main` of every binary that dials the queue. +/// +/// rustls is built with both `ring` and `aws-lc-rs`; without an installed +/// default, the first TLS handshake panics (async-nats builds its config with +/// `ClientConfig::builder()`) and the client never connects. `aws-lc-rs` +/// because it is what reqwest uses when nothing is installed. +pub fn install_crypto_provider() { + // `Err` only means a provider is already installed, which is all this needs. + let _already_installed = rustls::crypto::aws_lc_rs::default_provider().install_default(); +} + /// Connect to the swarm queue, presenting a token on each connection attempt /// and minting a fresh one only when the cached one is near expiry. /// @@ -664,6 +676,7 @@ pub fn max_payload(client: &async_nats::Client) -> usize { /// token expires, the controller keeps serving, its status data quietly stops /// updating, and nothing says so until someone reads a dashboard. pub async fn connect(cfg: QueueConfig) -> Result { + install_crypto_provider(); // Trusts `cfg.ca_file` when set (the swarm's own CA, when the token // endpoint is signed by it) — see `build_http_client`. A timeout too, // because this client runs INSIDE the auth callback: a token endpoint @@ -771,6 +784,14 @@ pub async fn connect(cfg: QueueConfig) -> Result { mod tests { use super::*; + #[test] + fn a_tls_client_config_builds_once_the_provider_is_installed() { + install_crypto_provider(); + let _config = rustls::ClientConfig::builder() + .with_root_certificates(rustls::RootCertStore::empty()) + .with_no_client_auth(); + } + /// The all-unset case is the common one — most hosts do not run the queue. /// /// Uses a prefix no deployment sets, so it cannot pass vacuously by