From 19c0bf2fee75daf1a66d8a5b78456b290569e740 Mon Sep 17 00:00:00 2001 From: Zsolt Dollenstein Date: Tue, 28 Jul 2026 13:16:06 +0100 Subject: [PATCH 1/2] Honor certificate overrides when running remote scripts --- crates/uv/src/commands/project/run.rs | 16 ++----- crates/uv/src/lib.rs | 62 +++++++++++++-------------- crates/uv/src/settings.rs | 16 +++++-- crates/uv/tests/project/run.rs | 21 +++++++++ crates/uv/tests/sync/show_settings.rs | 8 +++- 5 files changed, 75 insertions(+), 48 deletions(-) diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index d915ec1b83d20..0d2a6ff828bac 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -49,6 +49,7 @@ use uv_types::SourceTreeEditablePolicy; use uv_warnings::warn_user; use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceCache, WorkspaceErrorKind}; +use crate::base_client_builder; use crate::child::run_to_completion; /// GitHub Gist API response structure @@ -1472,19 +1473,8 @@ impl ParsedRunCommand { Ok((script, run_command)) } Self::PendingRemote(remote_command) => { - let settings = GlobalSettings::resolve(global_args, filesystem, environment)?; - let client_builder = BaseClientBuilder::new( - settings.network_settings.connectivity, - settings.network_settings.system_certs, - settings.network_settings.allow_insecure_host, - settings.preview, - settings.network_settings.read_timeout, - settings.network_settings.connect_timeout, - settings.network_settings.retries, - ) - .http_proxy(settings.network_settings.http_proxy) - .https_proxy(settings.network_settings.https_proxy) - .no_proxy(settings.network_settings.no_proxy); + let settings = GlobalSettings::resolve(global_args, filesystem, environment, None)?; + let client_builder = base_client_builder(&settings); let (url, downloaded_script, args) = remote_command.download(&client_builder).await?; diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index ae019b9aff14f..0e2e34f62da2f 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -32,7 +32,7 @@ use uv_cli::{ PythonNamespace, SelfCommand, SelfNamespace, ToolCommand, ToolNamespace, TopLevelArgs, WorkspaceCommand, WorkspaceNamespace, compat::CompatArgs, options::ArgumentError, }; -use uv_client::{BaseClientBuilder, Certificates}; +use uv_client::BaseClientBuilder; use uv_configuration::min_stack_size; use uv_flags::EnvironmentFlags; use uv_fs::{CWD, Simplified, normalize_path}; @@ -68,6 +68,29 @@ mod logging; pub(crate) mod printer; pub(crate) mod settings; +/// Construct the shared HTTP client builder from the resolved global settings. +pub(crate) fn base_client_builder<'a>(globals: &GlobalSettings) -> BaseClientBuilder<'a> { + let client_builder = BaseClientBuilder::new( + globals.network_settings.connectivity, + globals.network_settings.system_certs, + globals.network_settings.allow_insecure_host.clone(), + globals.preview, + globals.network_settings.read_timeout, + globals.network_settings.connect_timeout, + globals.network_settings.retries, + ) + .cache_read_concurrency(globals.concurrency.cache_reads) + .http_proxy(globals.network_settings.http_proxy.clone()) + .https_proxy(globals.network_settings.https_proxy.clone()) + .no_proxy(globals.network_settings.no_proxy.clone()); + + if let Some(certificates) = &globals.network_settings.custom_certificates { + client_builder.custom_certificates(certificates.clone()) + } else { + client_builder + } +} + /// Whether to initialize process-global state. #[derive(Debug, Copy, Clone, Eq, PartialEq)] #[doc(hidden)] @@ -461,11 +484,17 @@ pub async fn run(cli: Cli, global_initialization: GlobalInitialization) -> Resul .map(FilesystemOptions::from) .combine(filesystem); + let custom_certificate_file = match &*cli.command { + Commands::Pip(PipNamespace { cert, .. }) => cert.as_deref(), + _ => None, + }; + // Resolve the global settings. let globals = GlobalSettings::resolve( &cli.top_level.global_args, filesystem.as_ref(), &environment, + custom_certificate_file, )?; if global_initialization.needs_initialization() { @@ -607,37 +636,8 @@ pub async fn run(cli: Cli, global_initialization: GlobalInitialization) -> Resul WorkspaceCache::default() }; - // Configure custom CA certificates from `--cert` or from the environment (`SSL_CERT_FILE` and - // `SSL_CERT_DIR`). - // Like pip, an explicit certificate bundle overrides the environment certificate sources. - let custom_certificates = if let Commands::Pip(PipNamespace { - cert: Some(cert), .. - }) = &*cli.command - { - Some(Certificates::from_file(cert)?) - } else { - Certificates::from_env() - }; - // Configure the global network settings. - let client_builder = BaseClientBuilder::new( - globals.network_settings.connectivity, - globals.network_settings.system_certs, - globals.network_settings.allow_insecure_host.clone(), - globals.preview, - globals.network_settings.read_timeout, - globals.network_settings.connect_timeout, - globals.network_settings.retries, - ) - .cache_read_concurrency(globals.concurrency.cache_reads) - .http_proxy(globals.network_settings.http_proxy.clone()) - .https_proxy(globals.network_settings.https_proxy.clone()) - .no_proxy(globals.network_settings.no_proxy.clone()); - let client_builder = if let Some(certificates) = custom_certificates { - client_builder.custom_certificates(certificates) - } else { - client_builder - }; + let client_builder = base_client_builder(&globals); match *cli.command { Commands::Auth(AuthNamespace { diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 97ca6b6794065..6bd6ec6558d9a 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -1,7 +1,7 @@ use std::env::VarError; use std::fmt; use std::num::NonZeroUsize; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::process; use std::str::FromStr; use std::time::Duration; @@ -32,7 +32,7 @@ use uv_cli::{ resolver_installer_options, resolver_options, }, }; -use uv_client::Connectivity; +use uv_client::{Certificates, Connectivity}; use uv_configuration::{ BuildIsolation, BuildOptions, Concurrency, DependencyGroups, DevMode, DryRun, EditableMode, EnvFile, ExcludeDependency, ExportFormat, ExtrasSpecification, GitLfsSetting, HashCheckingMode, @@ -98,8 +98,10 @@ impl GlobalSettings { args: &GlobalArgs, workspace: Option<&FilesystemOptions>, environment: &EnvironmentOptions, + custom_certificate_file: Option<&Path>, ) -> anyhow::Result { - let network_settings = NetworkSettings::resolve(args, workspace, environment)?; + let network_settings = + NetworkSettings::resolve(args, workspace, environment, custom_certificate_file)?; let python_preference = resolve_python_preference(args, workspace, environment)?; let color = resolve_color(args); Ok(Self { @@ -273,6 +275,7 @@ pub(crate) struct NetworkSettings { pub(super) connectivity: Connectivity, pub(super) offline: Flag, pub(super) system_certs: bool, + pub(super) custom_certificates: Option, pub(super) http_proxy: Option, pub(super) https_proxy: Option, pub(super) no_proxy: Option>, @@ -288,6 +291,7 @@ impl NetworkSettings { args: &GlobalArgs, workspace: Option<&FilesystemOptions>, environment: &EnvironmentOptions, + custom_certificate_file: Option<&Path>, ) -> anyhow::Result { // Resolve offline flag from CLI, environment variable, and workspace config. // Precedence: CLI > Env var > Workspace config > default (false). @@ -388,10 +392,16 @@ impl NetworkSettings { let https_proxy = workspace.and_then(|workspace| workspace.globals.https_proxy.clone()); let no_proxy = workspace.and_then(|workspace| workspace.globals.no_proxy.clone()); + let custom_certificates = custom_certificate_file + .map(Certificates::from_file) + .transpose()? + .or_else(Certificates::from_env); + Ok(Self { connectivity, offline, system_certs, + custom_certificates, http_proxy, https_proxy, no_proxy, diff --git a/crates/uv/tests/project/run.rs b/crates/uv/tests/project/run.rs index 0138ca462468e..5bd6465210c28 100644 --- a/crates/uv/tests/project/run.rs +++ b/crates/uv/tests/project/run.rs @@ -4641,6 +4641,27 @@ fn run_remote_pep723_script() { "); } +#[test] +fn run_remote_pep723_script_with_nonexistent_ssl_cert_file() { + let context = uv_test::test_context!("3.12"); + + uv_snapshot!(context.filters(), context.run() + .arg("https://raw.githubusercontent.com/astral-sh/uv/df45b9ac2584824309ff29a6a09421055ad730f6/scripts/uv-run-remote-script-test.py") + .arg(EnvVars::CI) + .env(EnvVars::SSL_CERT_FILE, context.temp_dir.join("missing.pem")) + .env(EnvVars::UV_HTTP_RETRIES, "0") + .env_remove(EnvVars::SSL_CERT_DIR) + .env_remove(EnvVars::UV_NATIVE_TLS) + .env_remove(EnvVars::UV_SYSTEM_CERTS), @" + exit_code: 2 (failure) + ----- stderr ----- + warning: Invalid `SSL_CERT_FILE`. Path does not exist: [TEMP_DIR]/missing.pem. No default certificates will be trusted. + error: error sending request for url (https://raw.githubusercontent.com/astral-sh/uv/df45b9ac2584824309ff29a6a09421055ad730f6/scripts/uv-run-remote-script-test.py) + Caused by: client error (Connect) + Caused by: invalid peer certificate: UnknownIssuer + "); +} + #[test] fn run_remote_requirements_offline_redacts_credentials() -> Result<()> { let context = uv_test::test_context!("3.12"); diff --git a/crates/uv/tests/sync/show_settings.rs b/crates/uv/tests/sync/show_settings.rs index 5579d7b7f4302..59581285eeaeb 100644 --- a/crates/uv/tests/sync/show_settings.rs +++ b/crates/uv/tests/sync/show_settings.rs @@ -49,6 +49,7 @@ fn pip_compile_baseline() { connectivity: Online, offline: Disabled, system_certs: false, + custom_certificates: None, http_proxy: None, https_proxy: None, no_proxy: None, @@ -247,6 +248,7 @@ fn publish_resolved_settings() -> anyhow::Result<()> { connectivity: Online, offline: Disabled, system_certs: false, + custom_certificates: None, http_proxy: None, https_proxy: None, no_proxy: None, @@ -413,6 +415,7 @@ fn pip_install_baseline() { connectivity: Online, offline: Disabled, system_certs: false, + custom_certificates: None, http_proxy: None, https_proxy: None, no_proxy: None, @@ -594,6 +597,7 @@ fn lock_baseline() { connectivity: Online, offline: Disabled, system_certs: false, + custom_certificates: None, http_proxy: None, https_proxy: None, no_proxy: None, @@ -714,6 +718,7 @@ fn version_baseline() { connectivity: Online, offline: Disabled, system_certs: false, + custom_certificates: None, http_proxy: None, https_proxy: None, no_proxy: None, @@ -849,6 +854,7 @@ fn tool_install_baseline() { connectivity: Online, offline: Disabled, system_certs: false, + custom_certificates: None, http_proxy: None, https_proxy: None, no_proxy: None, @@ -4307,9 +4313,9 @@ fn system_certs_config_aliases() -> anyhow::Result<()> { offline: Disabled, - system_certs: false, + system_certs: true, + custom_certificates: None, http_proxy: None, https_proxy: None, - no_proxy: None, ... " ); From 0746cc07ad5b3e2d8cd3dae1c88d748e42de5ed2 Mon Sep 17 00:00:00 2001 From: Zsolt Dollenstein Date: Tue, 28 Jul 2026 18:04:26 +0100 Subject: [PATCH 2/2] Filter certificate overrides from settings snapshots --- crates/uv-test/src/lib.rs | 5 +++++ crates/uv/tests/sync/show_settings.rs | 14 +++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/crates/uv-test/src/lib.rs b/crates/uv-test/src/lib.rs index 6c55d6964d08d..680698b36bde4 100755 --- a/crates/uv-test/src/lib.rs +++ b/crates/uv-test/src/lib.rs @@ -114,6 +114,11 @@ pub const INSTA_FILTERS: &[(&str, &str)] = &[ ), // Trim end-of-line whitespaces, to allow removing them on save. (r"([^\s])[ \t]+(\r?\n)", "$1$2"), + // Certificate overrides and their contents depend on the host environment. + ( + r"(?ms)^([ \t]*custom_certificates: )(?:None|Some\(\n.*?^[ \t]*\),\n[ \t]*\)),", + "${1}[CERTIFICATES],", + ), // Filter SSL certificate loading debug messages (environment-dependent) (r"DEBUG Loaded \d+ certificate\(s\) from [^\n]+\n", ""), ]; diff --git a/crates/uv/tests/sync/show_settings.rs b/crates/uv/tests/sync/show_settings.rs index 59581285eeaeb..dadaf37811e4a 100644 --- a/crates/uv/tests/sync/show_settings.rs +++ b/crates/uv/tests/sync/show_settings.rs @@ -49,7 +49,7 @@ fn pip_compile_baseline() { connectivity: Online, offline: Disabled, system_certs: false, - custom_certificates: None, + custom_certificates: [CERTIFICATES], http_proxy: None, https_proxy: None, no_proxy: None, @@ -248,7 +248,7 @@ fn publish_resolved_settings() -> anyhow::Result<()> { connectivity: Online, offline: Disabled, system_certs: false, - custom_certificates: None, + custom_certificates: [CERTIFICATES], http_proxy: None, https_proxy: None, no_proxy: None, @@ -415,7 +415,7 @@ fn pip_install_baseline() { connectivity: Online, offline: Disabled, system_certs: false, - custom_certificates: None, + custom_certificates: [CERTIFICATES], http_proxy: None, https_proxy: None, no_proxy: None, @@ -597,7 +597,7 @@ fn lock_baseline() { connectivity: Online, offline: Disabled, system_certs: false, - custom_certificates: None, + custom_certificates: [CERTIFICATES], http_proxy: None, https_proxy: None, no_proxy: None, @@ -718,7 +718,7 @@ fn version_baseline() { connectivity: Online, offline: Disabled, system_certs: false, - custom_certificates: None, + custom_certificates: [CERTIFICATES], http_proxy: None, https_proxy: None, no_proxy: None, @@ -854,7 +854,7 @@ fn tool_install_baseline() { connectivity: Online, offline: Disabled, system_certs: false, - custom_certificates: None, + custom_certificates: [CERTIFICATES], http_proxy: None, https_proxy: None, no_proxy: None, @@ -4313,7 +4313,7 @@ fn system_certs_config_aliases() -> anyhow::Result<()> { offline: Disabled, - system_certs: false, + system_certs: true, - custom_certificates: None, + custom_certificates: [CERTIFICATES], http_proxy: None, https_proxy: None, ...