feat: add optional native-tls support as alternative to rustls (#8037)
Signed-off-by: Rodolfo Olivieri <rolivier@redhat.com> Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -11,11 +11,28 @@ description.workspace = true
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
default = ["code-mode", "local-inference", "aws-providers"]
|
||||
default = ["code-mode", "local-inference", "aws-providers", "rustls-tls"]
|
||||
code-mode = ["goose/code-mode"]
|
||||
local-inference = ["goose/local-inference"]
|
||||
aws-providers = ["goose/aws-providers"]
|
||||
cuda = ["goose/cuda", "local-inference"]
|
||||
rustls-tls = [
|
||||
"reqwest/rustls",
|
||||
"tokio-tungstenite/rustls-tls-native-roots",
|
||||
"axum-server/tls-rustls",
|
||||
"dep:rustls",
|
||||
"dep:aws-lc-rs",
|
||||
"goose/rustls-tls",
|
||||
"goose-mcp/rustls-tls",
|
||||
]
|
||||
native-tls = [
|
||||
"reqwest/native-tls",
|
||||
"tokio-tungstenite/native-tls",
|
||||
"axum-server/tls-openssl",
|
||||
"dep:openssl",
|
||||
"goose/native-tls",
|
||||
"goose-mcp/native-tls",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
goose = { path = "../goose", default-features = false }
|
||||
@@ -41,21 +58,22 @@ thiserror = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
serde_yaml = { workspace = true }
|
||||
utoipa = { workspace = true, features = ["axum_extras", "chrono"] }
|
||||
reqwest = { workspace = true, features = ["json", "rustls", "blocking", "multipart", "system-proxy"], default-features = false }
|
||||
reqwest = { workspace = true, features = ["json", "blocking", "multipart", "system-proxy"], default-features = false }
|
||||
tokio-util = { workspace = true }
|
||||
serde_path_to_error = "0.1.20"
|
||||
tokio-tungstenite = { version = "0.28.0", features = ["rustls-tls-native-roots"] }
|
||||
tokio-tungstenite = { version = "0.28.0" }
|
||||
url = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
hex = "0.4.3"
|
||||
subtle = "2.6"
|
||||
socket2 = "0.6.1"
|
||||
fs2 = { workspace = true }
|
||||
rustls = { version = "0.23", features = ["aws_lc_rs"] }
|
||||
rustls = { version = "0.23", features = ["aws_lc_rs"], optional = true }
|
||||
uuid = { workspace = true }
|
||||
rcgen = "0.13"
|
||||
axum-server = { version = "0.8.0", features = ["tls-rustls"] }
|
||||
aws-lc-rs = "1.16.0"
|
||||
axum-server = { version = "0.8.0" }
|
||||
aws-lc-rs = { version = "1.16.0", optional = true }
|
||||
openssl = { version = "0.10", optional = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winreg = { version = "0.55.0" }
|
||||
|
||||
@@ -4,6 +4,7 @@ use anyhow::Result;
|
||||
use axum::middleware;
|
||||
use axum_server::Handle;
|
||||
use goose_server::auth::check_token;
|
||||
#[cfg(any(feature = "rustls-tls", feature = "native-tls"))]
|
||||
use goose_server::tls::self_signed_config;
|
||||
use tower_http::cors::{Any, CorsLayer};
|
||||
use tracing::info;
|
||||
@@ -31,6 +32,7 @@ pub async fn run() -> Result<()> {
|
||||
// gateways, etc.) try to open TLS connections. Both `ring` and `aws-lc-rs`
|
||||
// features are enabled on rustls (via different transitive deps), so rustls
|
||||
// cannot auto-detect a provider — we must pick one explicitly.
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
|
||||
crate::logging::setup_logging(Some("goosed"))?;
|
||||
@@ -74,21 +76,39 @@ pub async fn run() -> Result<()> {
|
||||
});
|
||||
|
||||
if settings.tls {
|
||||
let tls_setup = self_signed_config().await?;
|
||||
#[cfg(any(feature = "rustls-tls", feature = "native-tls"))]
|
||||
{
|
||||
let tls_setup = self_signed_config().await?;
|
||||
|
||||
let handle = Handle::new();
|
||||
let shutdown_handle = handle.clone();
|
||||
tokio::spawn(async move {
|
||||
shutdown_signal().await;
|
||||
shutdown_handle.graceful_shutdown(None);
|
||||
});
|
||||
let handle = Handle::new();
|
||||
let shutdown_handle = handle.clone();
|
||||
tokio::spawn(async move {
|
||||
shutdown_signal().await;
|
||||
shutdown_handle.graceful_shutdown(None);
|
||||
});
|
||||
|
||||
info!("listening on https://{}", addr);
|
||||
info!("listening on https://{}", addr);
|
||||
|
||||
axum_server::bind_rustls(addr, tls_setup.config)
|
||||
.handle(handle)
|
||||
.serve(app.into_make_service())
|
||||
.await?;
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
axum_server::bind_rustls(addr, tls_setup.config)
|
||||
.handle(handle)
|
||||
.serve(app.into_make_service())
|
||||
.await?;
|
||||
|
||||
#[cfg(feature = "native-tls")]
|
||||
axum_server::bind_openssl(addr, tls_setup.config)
|
||||
.handle(handle)
|
||||
.serve(app.into_make_service())
|
||||
.await?;
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "rustls-tls", feature = "native-tls")))]
|
||||
{
|
||||
anyhow::bail!(
|
||||
"TLS was requested but no TLS backend is enabled. \
|
||||
Enable the `rustls-tls` or `native-tls` feature."
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let listener = tokio::net::TcpListener::bind(addr).await?;
|
||||
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
#[cfg(not(any(feature = "rustls-tls", feature = "native-tls")))]
|
||||
compile_error!("At least one of `rustls-tls` or `native-tls` features must be enabled");
|
||||
|
||||
#[cfg(all(feature = "rustls-tls", feature = "native-tls"))]
|
||||
compile_error!("Features `rustls-tls` and `native-tls` are mutually exclusive");
|
||||
|
||||
pub mod auth;
|
||||
pub mod configuration;
|
||||
pub mod error;
|
||||
@@ -5,6 +11,7 @@ pub mod openapi;
|
||||
pub mod routes;
|
||||
pub mod session_event_bus;
|
||||
pub mod state;
|
||||
#[cfg(any(feature = "rustls-tls", feature = "native-tls"))]
|
||||
pub mod tls;
|
||||
pub mod tunnel;
|
||||
|
||||
|
||||
@@ -1,22 +1,31 @@
|
||||
//! TLS configuration for the goose server.
|
||||
//!
|
||||
//! Two TLS backends are supported for the HTTPS listener via `axum-server`:
|
||||
//!
|
||||
//! - **`rustls-tls`** (enabled by default) – uses `axum-server/tls-rustls` with
|
||||
//! the `aws-lc-rs` crypto provider.
|
||||
//! - **`native-tls`** – uses `axum-server/tls-openssl`, which links against the
|
||||
//! platform's OpenSSL (or a compatible fork such as LibreSSL / BoringSSL).
|
||||
//! On Linux this *is* the platform-native TLS stack; on macOS/Windows the
|
||||
//! `native-tls` crate used by the HTTP *client* delegates to Security.framework
|
||||
//! / SChannel respectively, but `axum-server` does not offer those backends so
|
||||
//! the server listener always uses OpenSSL when this feature is active.
|
||||
|
||||
use anyhow::Result;
|
||||
use aws_lc_rs::digest;
|
||||
use axum_server::tls_rustls::RustlsConfig;
|
||||
use rcgen::{CertificateParams, DnType, KeyPair, SanType};
|
||||
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
pub type TlsConfig = axum_server::tls_rustls::RustlsConfig;
|
||||
|
||||
#[cfg(feature = "native-tls")]
|
||||
pub type TlsConfig = axum_server::tls_openssl::OpenSSLConfig;
|
||||
|
||||
pub struct TlsSetup {
|
||||
pub config: RustlsConfig,
|
||||
pub config: TlsConfig,
|
||||
pub fingerprint: String,
|
||||
}
|
||||
|
||||
/// Generate a self-signed TLS certificate for localhost (127.0.0.1) and
|
||||
/// return a [`TlsSetup`] containing the rustls config and the SHA-256
|
||||
/// fingerprint of the generated certificate (colon-separated hex).
|
||||
///
|
||||
/// The fingerprint is printed to stdout so the parent process (e.g. Electron)
|
||||
/// can pin it and reject connections from any other certificate.
|
||||
pub async fn self_signed_config() -> Result<TlsSetup> {
|
||||
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
|
||||
|
||||
fn generate_self_signed_cert() -> Result<(rcgen::Certificate, KeyPair)> {
|
||||
let mut params = CertificateParams::default();
|
||||
params
|
||||
.distinguished_name
|
||||
@@ -28,22 +37,62 @@ pub async fn self_signed_config() -> Result<TlsSetup> {
|
||||
|
||||
let key_pair = KeyPair::generate()?;
|
||||
let cert = params.self_signed(&key_pair)?;
|
||||
Ok((cert, key_pair))
|
||||
}
|
||||
|
||||
let cert_der = cert.der();
|
||||
let sha256 = digest::digest(&digest::SHA256, cert_der);
|
||||
let fingerprint = sha256
|
||||
.as_ref()
|
||||
.iter()
|
||||
.map(|b| format!("{b:02X}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(":");
|
||||
fn sha256_fingerprint(der: &[u8]) -> String {
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
{
|
||||
let sha256 = aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA256, der);
|
||||
sha256
|
||||
.as_ref()
|
||||
.iter()
|
||||
.map(|b| format!("{b:02X}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(":")
|
||||
}
|
||||
|
||||
#[cfg(feature = "native-tls")]
|
||||
{
|
||||
use openssl::hash::MessageDigest;
|
||||
let digest =
|
||||
openssl::hash::hash(MessageDigest::sha256(), der).expect("SHA-256 hash failed");
|
||||
digest
|
||||
.iter()
|
||||
.map(|b| format!("{b:02X}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(":")
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a self-signed TLS certificate for localhost (127.0.0.1) and
|
||||
/// return a [`TlsSetup`] containing the server config and the SHA-256
|
||||
/// fingerprint of the generated certificate (colon-separated hex).
|
||||
///
|
||||
/// The fingerprint is printed to stdout so the parent process (e.g. Electron)
|
||||
/// can pin it and reject connections from any other certificate.
|
||||
pub async fn self_signed_config() -> Result<TlsSetup> {
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
|
||||
|
||||
let (cert, key_pair) = generate_self_signed_cert()?;
|
||||
|
||||
let fingerprint = sha256_fingerprint(cert.der());
|
||||
println!("GOOSED_CERT_FINGERPRINT={fingerprint}");
|
||||
|
||||
let cert_pem = cert.pem();
|
||||
let key_pem = key_pair.serialize_pem();
|
||||
|
||||
let config = RustlsConfig::from_pem(cert_pem.into_bytes(), key_pem.into_bytes()).await?;
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
let config = axum_server::tls_rustls::RustlsConfig::from_pem(
|
||||
cert_pem.into_bytes(),
|
||||
key_pem.into_bytes(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
#[cfg(feature = "native-tls")]
|
||||
let config =
|
||||
axum_server::tls_openssl::OpenSSLConfig::from_pem(cert_pem.as_bytes(), key_pem.as_bytes())?;
|
||||
|
||||
Ok(TlsSetup {
|
||||
config,
|
||||
|
||||
@@ -485,6 +485,7 @@ async fn run_single_connection(
|
||||
scheme: String,
|
||||
restart_tx: mpsc::Sender<()>,
|
||||
) {
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
|
||||
|
||||
let worker_url = get_worker_url();
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
use goose_server::tls::{self_signed_config, TlsConfig};
|
||||
|
||||
#[cfg(not(feature = "native-tls"))]
|
||||
#[test]
|
||||
fn default_tls_config_is_rustls() {
|
||||
fn assert_type<T>(_: &T) {}
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let setup = rt.block_on(self_signed_config()).unwrap();
|
||||
// Proves TlsConfig resolves to RustlsConfig when native-tls is disabled.
|
||||
let _: &axum_server::tls_rustls::RustlsConfig = &setup.config;
|
||||
assert_type::<TlsConfig>(&setup.config);
|
||||
}
|
||||
|
||||
#[cfg(feature = "native-tls")]
|
||||
#[test]
|
||||
fn native_tls_config_is_openssl() {
|
||||
fn assert_type<T>(_: &T) {}
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let setup = rt.block_on(self_signed_config()).unwrap();
|
||||
// Proves TlsConfig resolves to OpenSSLConfig when native-tls is enabled.
|
||||
let _: &axum_server::tls_openssl::OpenSSLConfig = &setup.config;
|
||||
assert_type::<TlsConfig>(&setup.config);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn self_signed_config_produces_valid_fingerprint() {
|
||||
let setup = self_signed_config().await.unwrap();
|
||||
|
||||
assert!(
|
||||
!setup.fingerprint.is_empty(),
|
||||
"fingerprint must not be empty"
|
||||
);
|
||||
|
||||
let parts: Vec<&str> = setup.fingerprint.split(':').collect();
|
||||
assert_eq!(
|
||||
parts.len(),
|
||||
32,
|
||||
"SHA-256 fingerprint must have 32 hex pairs"
|
||||
);
|
||||
|
||||
for part in &parts {
|
||||
assert_eq!(
|
||||
part.len(),
|
||||
2,
|
||||
"each fingerprint segment must be 2 hex chars"
|
||||
);
|
||||
assert!(
|
||||
part.chars().all(|c| c.is_ascii_hexdigit()),
|
||||
"fingerprint segment '{}' must be valid hex",
|
||||
part
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn self_signed_config_returns_usable_tls_config() {
|
||||
use axum::routing::get;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
let setup = self_signed_config().await.unwrap();
|
||||
|
||||
let app = axum::Router::new().route("/health", get(|| async { "ok" }));
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], 0));
|
||||
|
||||
#[cfg(not(feature = "native-tls"))]
|
||||
let server = axum_server::bind_rustls(addr, setup.config);
|
||||
|
||||
#[cfg(feature = "native-tls")]
|
||||
let server = axum_server::bind_openssl(addr, setup.config);
|
||||
|
||||
let handle = axum_server::Handle::new();
|
||||
let shutdown_handle = handle.clone();
|
||||
|
||||
let server_handle = tokio::spawn({
|
||||
let handle = handle.clone();
|
||||
async move {
|
||||
server
|
||||
.handle(handle)
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for the server to start listening.
|
||||
let listening_addr = loop {
|
||||
if let Some(addr) = handle.listening().await {
|
||||
break addr;
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||
};
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let resp = client
|
||||
.get(format!("https://{}/health", listening_addr))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(resp.status(), 200);
|
||||
assert_eq!(resp.text().await.unwrap(), "ok");
|
||||
|
||||
shutdown_handle.graceful_shutdown(None);
|
||||
let _ = server_handle.await;
|
||||
}
|
||||
Reference in New Issue
Block a user