diff --git a/Cargo.lock b/Cargo.lock index f6970c53..b8383b3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4984,7 +4984,6 @@ dependencies = [ "clap", "serde", "subtle", - "swarm-queue-client", "swarm-secret-client", "tokio", "tracing", @@ -5026,10 +5025,10 @@ dependencies = [ "futures-util", "hive-sh4re", "reqwest", - "rustls", "serde", "serde_json", "strum", + "swarm-secret-client", "thiserror 2.0.18", "tokio", "tracing", @@ -5066,6 +5065,7 @@ dependencies = [ "reqwest", "rustify", "rustify_derive", + "rustls", "serde", "serde_json", "strum", diff --git a/Cargo.toml b/Cargo.toml index d194aaf6..ed69dd19 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -256,7 +256,7 @@ async-nats = { version = "0.50", default-features = false, features = [ "nkeys", "ring", ] } -# Named only to install the process-wide provider (`swarm-queue-client`'s +# Named only to install the process-wide provider (`swarm-secret-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 = [ diff --git a/hive-agent/src/main.rs b/hive-agent/src/main.rs index 05d4cc94..93024155 100644 --- a/hive-agent/src/main.rs +++ b/hive-agent/src/main.rs @@ -121,7 +121,7 @@ struct Cli { #[tokio::main] async fn main() -> Result<()> { hive_log::init(); - swarm_queue_client::install_crypto_provider(); + swarm_secret_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 34c12090..ddf6878d 100644 --- a/hive-c0re/src/main.rs +++ b/hive-c0re/src/main.rs @@ -129,7 +129,7 @@ enum Cmd { #[tokio::main] async fn main() -> Result<()> { hive_log::init(); - swarm_queue_client::install_crypto_provider(); + swarm_secret_client::install_crypto_provider(); let cli = Cli::parse(); match cli.cmd { diff --git a/hive-subagent-mcp/src/swarm_models.rs b/hive-subagent-mcp/src/swarm_models.rs index 46f35aa8..a71f4d78 100644 --- a/hive-subagent-mcp/src/swarm_models.rs +++ b/hive-subagent-mcp/src/swarm_models.rs @@ -106,7 +106,7 @@ impl Reader { return None; } }; - swarm_queue_client::install_crypto_provider(); + swarm_secret_client::install_crypto_provider(); let agent = target.agent.clone(); let client = Arc::new(OnceLock::new()); tokio::spawn(connect_until_up(Arc::new(target), Arc::clone(&client))); diff --git a/hive-subagent-mcp/src/swarm_term.rs b/hive-subagent-mcp/src/swarm_term.rs index 4d92c67f..1d17f479 100644 --- a/hive-subagent-mcp/src/swarm_term.rs +++ b/hive-subagent-mcp/src/swarm_term.rs @@ -111,7 +111,7 @@ impl Publisher { return None; } }; - swarm_queue_client::install_crypto_provider(); + swarm_secret_client::install_crypto_provider(); tracing::info!( url = %target.url, stream = %subagent_term::stream_name(&target.agent), diff --git a/swarm-builder/src/main.rs b/swarm-builder/src/main.rs index 8f409d12..cf325d8f 100644 --- a/swarm-builder/src/main.rs +++ b/swarm-builder/src/main.rs @@ -57,7 +57,7 @@ const MAX_SLOT_WAIT: Duration = REPLY_DEADLINE #[tokio::main] async fn main() -> Result<()> { hive_log::init(); - swarm_queue_client::install_crypto_provider(); + swarm_secret_client::install_crypto_provider(); let var = |name: &str| { let full = format!("{PREFIX}_{name}"); std::env::var(&full).with_context(|| format!("{full} is unset")) diff --git a/swarm-ci-metrics/Cargo.toml b/swarm-ci-metrics/Cargo.toml index 9b4926c2..81b0ab82 100644 --- a/swarm-ci-metrics/Cargo.toml +++ b/swarm-ci-metrics/Cargo.toml @@ -17,7 +17,7 @@ hive-log.workspace = true reqwest.workspace = true serde.workspace = true serde_json.workspace = true -# The OIDC client-credentials mint, and the rustls provider. +# The OIDC client-credentials mint. swarm-queue-client.workspace = true # The forge token and client secret paths, and the store login. swarm-secret-client.workspace = true diff --git a/swarm-ci-metrics/src/main.rs b/swarm-ci-metrics/src/main.rs index feb4b574..a106689d 100644 --- a/swarm-ci-metrics/src/main.rs +++ b/swarm-ci-metrics/src/main.rs @@ -53,7 +53,7 @@ struct Config { #[tokio::main] async fn main() -> Result<()> { hive_log::init(); - swarm_queue_client::install_crypto_provider(); + swarm_secret_client::install_crypto_provider(); let var = |name: &str| { let full = format!("{PREFIX}_{name}"); std::env::var(&full).with_context(|| format!("{full} is unset")) diff --git a/swarm-claude-login/src/main.rs b/swarm-claude-login/src/main.rs index da3732f7..9ad56f03 100644 --- a/swarm-claude-login/src/main.rs +++ b/swarm-claude-login/src/main.rs @@ -51,7 +51,7 @@ impl login::CredentialSink for AgentLeaf { #[tokio::main] async fn main() -> Result<()> { hive_log::init(); - swarm_queue_client::install_crypto_provider(); + swarm_secret_client::install_crypto_provider(); let var = |name: &str| { let full = format!("{PREFIX}_{name}"); std::env::var(&full).with_context(|| format!("{full} is unset")) diff --git a/swarm-controller/src/main.rs b/swarm-controller/src/main.rs index 43c709a3..60fd4d6f 100644 --- a/swarm-controller/src/main.rs +++ b/swarm-controller/src/main.rs @@ -3475,7 +3475,7 @@ async fn start_forge( #[tokio::main] async fn main() -> Result<()> { hive_log::init(); - swarm_queue_client::install_crypto_provider(); + swarm_secret_client::install_crypto_provider(); let path = socket_path(); diff --git a/swarm-logs-auth/Cargo.toml b/swarm-logs-auth/Cargo.toml index fdb25ae8..2e5fca46 100644 --- a/swarm-logs-auth/Cargo.toml +++ b/swarm-logs-auth/Cargo.toml @@ -14,7 +14,7 @@ clap.workspace = true # The verdict cache's key: a digest of the bearer, never the bearer. sha2.workspace = true swarm-agent-verify.workspace = true -# The agent-token spelling, and the rustls provider the store client needs. +# The agent-token spelling. swarm-queue-client.workspace = true swarm-secret-client.workspace = true tokio.workspace = true diff --git a/swarm-logs-auth/src/main.rs b/swarm-logs-auth/src/main.rs index df2deabd..b57484a8 100644 --- a/swarm-logs-auth/src/main.rs +++ b/swarm-logs-auth/src/main.rs @@ -114,7 +114,7 @@ async fn handle( #[tokio::main] async fn main() -> anyhow::Result<()> { - swarm_queue_client::install_crypto_provider(); + swarm_secret_client::install_crypto_provider(); tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::try_from_default_env() diff --git a/swarm-logs/Cargo.toml b/swarm-logs/Cargo.toml index 7dd4a491..647d8505 100644 --- a/swarm-logs/Cargo.toml +++ b/swarm-logs/Cargo.toml @@ -12,8 +12,7 @@ path = "src/main.rs" anyhow.workspace = true clap.workspace = true clap-markdown = "0.1" -# The agent-token spelling the gateway's responder parses, and the rustls -# provider the store client needs. +# The agent-token spelling the gateway's responder parses. swarm-queue-client.workspace = true # The read of this agent's own queue secret. swarm-secret-client.workspace = true diff --git a/swarm-logs/src/main.rs b/swarm-logs/src/main.rs index 1fb0189e..81302186 100644 --- a/swarm-logs/src/main.rs +++ b/swarm-logs/src/main.rs @@ -85,7 +85,7 @@ fn main() -> Result<()> { // keep working on a machine that has no log store wired up at // all — `markdown-docs` in particular runs in a nix check, where // no credential exists and none should. - swarm_queue_client::install_crypto_provider(); + swarm_secret_client::install_crypto_provider(); let cfg = auth::Config::from_env()?; let stdout = std::io::stdout(); query::run(&cfg, &logsql, limit, format, &mut stdout.lock()) diff --git a/swarm-metrics-auth/Cargo.toml b/swarm-metrics-auth/Cargo.toml index 33891503..29476439 100644 --- a/swarm-metrics-auth/Cargo.toml +++ b/swarm-metrics-auth/Cargo.toml @@ -14,8 +14,6 @@ clap.workspace = true serde.workspace = true # Comparing the presented bearer with the stored one. subtle.workspace = true -# Only for the rustls provider the store client needs. -swarm-queue-client.workspace = true swarm-secret-client.workspace = true tokio.workspace = true tracing.workspace = true diff --git a/swarm-metrics-auth/src/main.rs b/swarm-metrics-auth/src/main.rs index be478d21..cb7bfc77 100644 --- a/swarm-metrics-auth/src/main.rs +++ b/swarm-metrics-auth/src/main.rs @@ -190,7 +190,7 @@ async fn handle(State(responder): State>>, headers: HeaderM #[tokio::main] async fn main() -> anyhow::Result<()> { - swarm_queue_client::install_crypto_provider(); + swarm_secret_client::install_crypto_provider(); tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::try_from_default_env() diff --git a/swarm-metrics/Cargo.toml b/swarm-metrics/Cargo.toml index c2dd3dc1..efae1dd2 100644 --- a/swarm-metrics/Cargo.toml +++ b/swarm-metrics/Cargo.toml @@ -12,8 +12,7 @@ path = "src/main.rs" anyhow.workspace = true clap.workspace = true clap-markdown = "0.1" -# The agent-token spelling the gateway's responder parses, and the rustls -# provider the store client needs. +# The agent-token spelling the gateway's responder parses. swarm-queue-client.workspace = true # The read of this agent's own queue secret. swarm-secret-client.workspace = true diff --git a/swarm-metrics/src/main.rs b/swarm-metrics/src/main.rs index 6c53047a..c2569bcf 100644 --- a/swarm-metrics/src/main.rs +++ b/swarm-metrics/src/main.rs @@ -169,7 +169,7 @@ fn main() -> Result<()> { }; // Config read only for a store verb: `markdown-docs` runs in a nix check, // where no credential exists. - swarm_queue_client::install_crypto_provider(); + swarm_secret_client::install_crypto_provider(); let cfg = auth::Config::from_env()?; let stdout = std::io::stdout(); query::run(&cfg, &request, &mut stdout.lock()) diff --git a/swarm-nats-auth/src/main.rs b/swarm-nats-auth/src/main.rs index 1f6b3ae6..a1ed9178 100644 --- a/swarm-nats-auth/src/main.rs +++ b/swarm-nats-auth/src/main.rs @@ -192,7 +192,7 @@ struct Args { #[tokio::main] async fn main() -> anyhow::Result<()> { - swarm_queue_client::install_crypto_provider(); + swarm_secret_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 7bb3086e..e43623e7 100644 --- a/swarm-queue-client/Cargo.toml +++ b/swarm-queue-client/Cargo.toml @@ -82,10 +82,11 @@ hive-sh4re = { workspace = true, optional = 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 +# The rustls provider `connect` installs. +swarm-secret-client.workspace = true # A library, so its errors are a matchable enum rather than an opaque # `anyhow::Error`. The binaries that consume this keep anyhow; `?` converts. thiserror.workspace = true diff --git a/swarm-queue-client/src/lib.rs b/swarm-queue-client/src/lib.rs index f7b9e650..f74be01d 100644 --- a/swarm-queue-client/src/lib.rs +++ b/swarm-queue-client/src/lib.rs @@ -941,18 +941,6 @@ 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. Reply /// inboxes are under [`client_inbox_prefix`] of `cfg.client_id`. @@ -962,7 +950,7 @@ pub fn install_crypto_provider() { /// 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(); + swarm_secret_client::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 @@ -1134,14 +1122,6 @@ mod tests { } } - #[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(); - } - #[tokio::test] async fn a_debug_print_of_a_config_does_not_carry_a_held_secret() { let cfg = QueueConfig { diff --git a/swarm-review-classifier/src/main.rs b/swarm-review-classifier/src/main.rs index 04d6bc71..6b6c6940 100644 --- a/swarm-review-classifier/src/main.rs +++ b/swarm-review-classifier/src/main.rs @@ -46,7 +46,7 @@ fn main() -> Result<()> { async fn serve() -> Result<()> { hive_log::init(); - swarm_queue_client::install_crypto_provider(); + swarm_secret_client::install_crypto_provider(); let var = |name: &str| { let full = format!("{PREFIX}_{name}"); std::env::var(&full).with_context(|| format!("{full} is unset")) diff --git a/swarm-secret-client/Cargo.toml b/swarm-secret-client/Cargo.toml index 05b5cf9b..629355d6 100644 --- a/swarm-secret-client/Cargo.toml +++ b/swarm-secret-client/Cargo.toml @@ -12,6 +12,9 @@ reqwest.workspace = true # implement — see `client::WriteAclPolicy`. rustify.workspace = true rustify_derive.workspace = true +# `install_crypto_provider`, which the store client (reqwest, via vaultrs) +# needs before its first TLS handshake. +rustls.workspace = true serde.workspace = true # `read_versioned` decodes the data half of the response vaultrs hands back as # a `serde_json::Value`. @@ -22,3 +25,9 @@ vaultrs.workspace = true [lints] workspace = true + +# `ring` as well, as every binary that links async-nats builds it: with one +# provider compiled in rustls picks it without an install, so the provider +# test would pass with `install_crypto_provider` doing nothing. +[dev-dependencies] +rustls = { workspace = true, features = ["ring"] } diff --git a/swarm-secret-client/src/lib.rs b/swarm-secret-client/src/lib.rs index 3e7e9438..9449d446 100644 --- a/swarm-secret-client/src/lib.rs +++ b/swarm-secret-client/src/lib.rs @@ -24,6 +24,10 @@ //! //! [`mtls`] is the one module about reaching the store rather than about a //! value inside it, and its doc explains why that is not circular. +//! +//! [`install_crypto_provider`] is process setup, not an agreement. It lives +//! here because this crate depends on no other swarm crate, so a binary that +//! needs only the provider and the store links nothing else. pub mod acp; pub mod builder; @@ -105,3 +109,30 @@ impl From for Error { Self::Vault(Box::new(e)) } } + +/// Install `aws-lc-rs` as the process-wide rustls crypto provider. Call it +/// first thing in `main` of every binary that dials the queue or reaches the +/// secret store over TLS. +/// +/// rustls is built with both `ring` and `aws-lc-rs`; without an installed +/// default, the first TLS handshake panics. On the queue side async-nats +/// builds its config with `ClientConfig::builder()`, so the client never +/// connects. On the store side the client is reqwest, via vaultrs. +/// `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(); +} + +#[cfg(test)] +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(); + } +}