diff --git a/rsworkspace/Cargo.lock b/rsworkspace/Cargo.lock index 1df6615828..7c2e9ac9d4 100644 --- a/rsworkspace/Cargo.lock +++ b/rsworkspace/Cargo.lock @@ -7136,6 +7136,7 @@ dependencies = [ "hex", "hmac 0.13.0", "reqwest 0.12.28", + "rustls", "serde", "serde_json", "sha2 0.11.0", @@ -7302,6 +7303,7 @@ dependencies = [ "bytesize", "clap", "opentelemetry", + "rustls", "serde", "serde_json", "thiserror 2.0.19", diff --git a/rsworkspace/Cargo.toml b/rsworkspace/Cargo.toml index 060a02e97c..3a5b1c08fe 100644 --- a/rsworkspace/Cargo.toml +++ b/rsworkspace/Cargo.toml @@ -168,6 +168,7 @@ hmac = "=0.13.0" jsonwebtoken = { version = "=11.0.0", features = ["rust_crypto"] } nats-jwt-rs = "=0.1.1" nkeys = { version = "=0.4.5", features = ["xkeys"] } +rustls = { version = "=0.23.37", default-features = false, features = ["aws-lc-rs", "std"] } rustls-pemfile = "=2.2.0" rustls-pki-types = { version = "=1.15.1", default-features = false, features = ["std"] } rustls-webpki = { version = "=0.103.13", default-features = false, features = ["std", "alloc", "ring"] } diff --git a/rsworkspace/crates/platform/trogon-gateway/Cargo.toml b/rsworkspace/crates/platform/trogon-gateway/Cargo.toml index 683606c1bc..a72dc64be1 100644 --- a/rsworkspace/crates/platform/trogon-gateway/Cargo.toml +++ b/rsworkspace/crates/platform/trogon-gateway/Cargo.toml @@ -39,10 +39,11 @@ twilight-model = { workspace = true } trogon-nats = { workspace = true } trogon-semconv = { workspace = true } trogon-service-config = { workspace = true } -trogon-std = { workspace = true, features = ["clap", "signal", "telemetry-http"] } +trogon-std = { workspace = true, features = ["clap", "signal", "telemetry-http", "tls"] } url = "2.5" [dev-dependencies] +rustls = { workspace = true } tempfile = { workspace = true } time = { workspace = true } tower = "0.5" diff --git a/rsworkspace/crates/platform/trogon-gateway/src/main.rs b/rsworkspace/crates/platform/trogon-gateway/src/main.rs index 12800fcad0..2906e097d9 100644 --- a/rsworkspace/crates/platform/trogon-gateway/src/main.rs +++ b/rsworkspace/crates/platform/trogon-gateway/src/main.rs @@ -50,6 +50,8 @@ type SourceResult = (&'static str, anyhow::Result<()>); #[cfg(not(coverage))] #[tokio::main] async fn main() -> anyhow::Result<()> { + trogon_std::tls::install_default_crypto_provider()?; + let cli = CliArgs::::new().parse_args(); let resolved = config::load_with_overrides(cli.runtime.config.as_deref(), &cli.runtime.nats)?; @@ -219,12 +221,14 @@ async fn serve(resolved: config::ResolvedConfig) -> anyhow::Result<()> { Ok(()) } +#[cfg_attr(coverage, allow(dead_code))] #[derive(Debug, thiserror::Error)] enum NotionVerificationTokenCommandError { #[error("notion integration '{0}' is not configured")] IntegrationNotConfigured(source_integration_id::SourceIntegrationId), } +#[cfg_attr(coverage, allow(dead_code))] async fn notion_verification_token( resolved: &config::ResolvedConfig, integration: &source_integration_id::SourceIntegrationId, diff --git a/rsworkspace/crates/platform/trogon-gateway/src/source/slack/socket_mode.rs b/rsworkspace/crates/platform/trogon-gateway/src/source/slack/socket_mode.rs index dfc1079b78..1dd8a152c7 100644 --- a/rsworkspace/crates/platform/trogon-gateway/src/source/slack/socket_mode.rs +++ b/rsworkspace/crates/platform/trogon-gateway/src/source/slack/socket_mode.rs @@ -5,8 +5,10 @@ use futures_util::{Sink, SinkExt, Stream, StreamExt}; use serde::Deserialize; use tokio_tungstenite::tungstenite::{Error as WebSocketError, Message}; use tracing::{info, warn}; +#[cfg_attr(coverage, allow(unused_imports))] use trogon_nats::jetstream::{ClaimCheckPublisher, JetStreamPublisher, ObjectStorePut}; +#[cfg_attr(coverage, allow(unused_imports))] use super::config::{SlackConfig, SlackSocketModeConfig}; use super::constants::RECONNECT_MAX_DELAY; #[cfg(not(coverage))] diff --git a/rsworkspace/crates/platform/trogon-gateway/tests/crypto_provider.rs b/rsworkspace/crates/platform/trogon-gateway/tests/crypto_provider.rs new file mode 100644 index 0000000000..f1c45dfd69 --- /dev/null +++ b/rsworkspace/crates/platform/trogon-gateway/tests/crypto_provider.rs @@ -0,0 +1,14 @@ +//! Guards the dependency graph that made every TLS handshake panic at runtime. +//! +//! `trogon-gateway` reaches rustls through both provider features: `aws-lc-rs` +//! via the OTLP exporter and `ring` via async-nats and twilight. rustls refuses +//! to pick one, so anything building a TLS client panicked instead of +//! connecting. This fails if the explicit provider install stops happening or a +//! dependency change reintroduces the ambiguity. + +#[test] +fn tls_clients_build_once_the_provider_is_installed() { + assert!(trogon_std::tls::install_default_crypto_provider().is_ok()); + + let _ = rustls::ClientConfig::builder(); +} diff --git a/rsworkspace/crates/platform/trogon-std/Cargo.toml b/rsworkspace/crates/platform/trogon-std/Cargo.toml index 4d551ddf25..ad0136716c 100644 --- a/rsworkspace/crates/platform/trogon-std/Cargo.toml +++ b/rsworkspace/crates/platform/trogon-std/Cargo.toml @@ -12,6 +12,7 @@ test-support = ["dep:tracing", "dep:tracing-subscriber"] clap = ["dep:clap"] uuid = ["dep:uuid"] signal = ["dep:tokio", "dep:tracing"] +tls = ["dep:rustls"] telemetry-http = [ "dep:axum", "dep:opentelemetry", @@ -27,6 +28,7 @@ axum = { workspace = true, optional = true } bytesize = "2.3.1" clap = { workspace = true, optional = true } opentelemetry = { workspace = true, optional = true } +rustls = { workspace = true, optional = true } trogon-semconv = { workspace = true, optional = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/rsworkspace/crates/platform/trogon-std/src/lib.rs b/rsworkspace/crates/platform/trogon-std/src/lib.rs index 8847ebdab1..8c4e52b211 100644 --- a/rsworkspace/crates/platform/trogon-std/src/lib.rs +++ b/rsworkspace/crates/platform/trogon-std/src/lib.rs @@ -51,6 +51,8 @@ pub mod signal; #[cfg(feature = "telemetry-http")] pub mod telemetry; pub mod time; +#[cfg(feature = "tls")] +pub mod tls; #[cfg(feature = "uuid")] pub mod uuid; @@ -71,5 +73,7 @@ pub use json::{JsonSerialize, StdJsonSerialize}; pub use log_capture::{CapturedEvent, CapturedEvents}; pub use secret_string::{EmptySecretError, SecretString}; pub use time::{EpochClock, GetElapsed, GetNow, SystemClock}; +#[cfg(feature = "tls")] +pub use tls::{CryptoProviderAlreadyInstalledError, install_default_crypto_provider}; #[cfg(feature = "uuid")] pub use uuid::{NowV7, UuidV7Generator}; diff --git a/rsworkspace/crates/platform/trogon-std/src/tls.rs b/rsworkspace/crates/platform/trogon-std/src/tls.rs new file mode 100644 index 0000000000..e44c58304c --- /dev/null +++ b/rsworkspace/crates/platform/trogon-std/src/tls.rs @@ -0,0 +1,31 @@ +//! Process-wide TLS provider selection for rustls. +//! +//! rustls only derives its process-level [`rustls::crypto::CryptoProvider`] from +//! crate features when exactly one provider feature is enabled. A dependency +//! graph that reaches both `aws-lc-rs` and `ring` leaves the choice ambiguous, +//! and rustls panics on the first handshake rather than guessing. Installing the +//! provider explicitly removes the dependency on feature unification, so adding +//! a dependency cannot change which cryptography a binary uses. +//! +//! Call [`install_default_crypto_provider`] before the first TLS handshake, +//! ahead of any client construction or task spawn. + +/// A rustls [`rustls::crypto::CryptoProvider`] was already installed for this +/// process, so the caller has no guarantee which cryptography it now uses. +#[derive(Debug, thiserror::Error)] +#[error("a rustls CryptoProvider was already installed for this process")] +pub struct CryptoProviderAlreadyInstalledError; + +/// Installs aws-lc-rs as the process-wide rustls +/// [`rustls::crypto::CryptoProvider`]. +/// +/// Fails rather than accepting whichever provider got there first, so a second +/// installer cannot silently downgrade the process to different cryptography. +pub fn install_default_crypto_provider() -> Result<(), CryptoProviderAlreadyInstalledError> { + rustls::crypto::aws_lc_rs::default_provider() + .install_default() + .map_err(|_| CryptoProviderAlreadyInstalledError) +} + +#[cfg(test)] +mod tests; diff --git a/rsworkspace/crates/platform/trogon-std/src/tls/tests.rs b/rsworkspace/crates/platform/trogon-std/src/tls/tests.rs new file mode 100644 index 0000000000..1d90152c62 --- /dev/null +++ b/rsworkspace/crates/platform/trogon-std/src/tls/tests.rs @@ -0,0 +1,16 @@ +use super::install_default_crypto_provider; +use rustls::crypto::CryptoProvider; + +#[test] +fn installs_a_process_wide_provider() { + let _ = install_default_crypto_provider(); + + assert!(CryptoProvider::get_default().is_some()); +} + +#[test] +fn refuses_to_replace_an_installed_provider() { + let _ = install_default_crypto_provider(); + + assert!(install_default_crypto_provider().is_err()); +}