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:
Rodolfo Olivieri
2026-03-25 17:46:50 -03:00
committed by GitHub
parent 3a9805e255
commit caef9f6466
17 changed files with 786 additions and 100 deletions
+32 -12
View File
@@ -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?;
+7
View File
@@ -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;
+70 -21
View File
@@ -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();