mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-03 13:14:41 +00:00
Enable TLS native root toggling at runtime (#2362)
## Summary
It turns out that on macOS, reading the native certificates can add
hundreds of milliseconds to client initialization. This PR makes
`--native-tls` a command-line flag, to toggle (at runtime) the choice of
the `webpki` roots or the native system roots.
You can't accomplish this kind of configuration with the `reqwest`
builder API, so instead, I pulled out the heart of that logic from the
crate
(e319263851/src/async_impl/client.rs (L498)),
and modified it to allow toggling a choice of root.
Note that there's an open PR for this in reqwest
(https://github.com/seanmonstar/reqwest/pull/1848), along with an issue
(https://github.com/seanmonstar/reqwest/issues/1843), which I may ping,
but it's been around for a while and I believe reqwest is focused on its
next major release.
Closes https://github.com/astral-sh/uv/issues/2346.
This commit is contained in:
parent
1d21e65fbc
commit
e9c16e9aa2
11 changed files with 170 additions and 9 deletions
10
Cargo.lock
generated
10
Cargo.lock
generated
|
|
@ -2880,6 +2880,7 @@ dependencies = [
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
"wasm-streams",
|
"wasm-streams",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
|
"webpki-roots",
|
||||||
"winreg",
|
"winreg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -4360,6 +4361,8 @@ dependencies = [
|
||||||
"rmp-serde",
|
"rmp-serde",
|
||||||
"rust-netrc",
|
"rust-netrc",
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
|
"rustls",
|
||||||
|
"rustls-native-certs",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
|
|
@ -4378,6 +4381,7 @@ dependencies = [
|
||||||
"uv-normalize",
|
"uv-normalize",
|
||||||
"uv-version",
|
"uv-version",
|
||||||
"uv-warnings",
|
"uv-warnings",
|
||||||
|
"webpki-roots",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -4935,6 +4939,12 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webpki-roots"
|
||||||
|
version = "0.25.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "weezl"
|
name = "weezl"
|
||||||
version = "0.1.8"
|
version = "0.1.8"
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ rand = { version = "0.8.5" }
|
||||||
rayon = { version = "1.8.0" }
|
rayon = { version = "1.8.0" }
|
||||||
reflink-copy = { version = "0.1.15" }
|
reflink-copy = { version = "0.1.15" }
|
||||||
regex = { version = "1.10.2" }
|
regex = { version = "1.10.2" }
|
||||||
reqwest = { version = "0.11.23", default-features = false, features = ["json", "gzip", "brotli", "stream", "rustls-tls-native-roots"] }
|
reqwest = { version = "0.11.23", default-features = false, features = ["json", "gzip", "brotli", "stream", "rustls-tls", "rustls-tls-native-roots"] }
|
||||||
reqwest-middleware = { version = "0.2.4" }
|
reqwest-middleware = { version = "0.2.4" }
|
||||||
reqwest-retry = { version = "0.3.0" }
|
reqwest-retry = { version = "0.3.0" }
|
||||||
rkyv = { version = "0.7.43", features = ["strict", "validation"] }
|
rkyv = { version = "0.7.43", features = ["strict", "validation"] }
|
||||||
|
|
|
||||||
15
README.md
15
README.md
|
|
@ -425,13 +425,18 @@ In addition, uv respects the following environment variables:
|
||||||
|
|
||||||
## Custom CA Certificates
|
## Custom CA Certificates
|
||||||
|
|
||||||
uv supports custom CA certificates (such as those needed by corporate proxies) by utilizing the
|
By default, uv loads certificates from the bundled `webpki-roots` crate. The `webpki-roots` are a
|
||||||
system's trust store. To ensure this works out of the box, ensure your certificates are added to the
|
reliable set of trust roots from Mozilla, and including them in uv improves portability and
|
||||||
system's trust store.
|
performance (especially on macOS, where reading the system trust store incurs a significant delay).
|
||||||
|
|
||||||
|
However, in some cases, you may want to use the platform's native certificate store, especially if
|
||||||
|
you're relying on a corporate trust root (e.g., for a mandatory proxy) that's included in your
|
||||||
|
system's certificate store. To instruct uv to use the system's trust store, run uv with the
|
||||||
|
`--native-tls` command-line flag.
|
||||||
|
|
||||||
If a direct path to the certificate is required (e.g., in CI), set the `SSL_CERT_FILE` environment
|
If a direct path to the certificate is required (e.g., in CI), set the `SSL_CERT_FILE` environment
|
||||||
variable to the path of the certificate bundle, to instruct uv to use that file instead of the
|
variable to the path of the certificate bundle (alongside the `--native-tls` flag), to instruct uv
|
||||||
system's trust store.
|
to use that file instead of the system's trust store.
|
||||||
|
|
||||||
## Acknowledgements
|
## Acknowledgements
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,11 @@ tracing = { workspace = true }
|
||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
urlencoding = { workspace = true }
|
urlencoding = { workspace = true }
|
||||||
|
|
||||||
|
# These must be kept in-sync with those used by `reqwest`.
|
||||||
|
rustls = { version = "0.21.10" }
|
||||||
|
rustls-native-certs = { version = "0.6.3" }
|
||||||
|
webpki-roots = { version = "0.25.4" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
hyper = { version = "0.14.28", features = ["server", "http1"] }
|
hyper = { version = "0.14.28", features = ["server", "http1"] }
|
||||||
|
|
|
||||||
|
|
@ -16,3 +16,4 @@ mod middleware;
|
||||||
mod registry_client;
|
mod registry_client;
|
||||||
mod remote_metadata;
|
mod remote_metadata;
|
||||||
mod rkyvutil;
|
mod rkyvutil;
|
||||||
|
mod tls;
|
||||||
|
|
|
||||||
|
|
@ -32,12 +32,14 @@ use crate::html::SimpleHtml;
|
||||||
use crate::middleware::{NetrcMiddleware, OfflineMiddleware};
|
use crate::middleware::{NetrcMiddleware, OfflineMiddleware};
|
||||||
use crate::remote_metadata::wheel_metadata_from_remote_zip;
|
use crate::remote_metadata::wheel_metadata_from_remote_zip;
|
||||||
use crate::rkyvutil::OwnedArchive;
|
use crate::rkyvutil::OwnedArchive;
|
||||||
use crate::{CachedClient, CachedClientError, Error, ErrorKind};
|
use crate::tls::Roots;
|
||||||
|
use crate::{tls, CachedClient, CachedClientError, Error, ErrorKind};
|
||||||
|
|
||||||
/// A builder for an [`RegistryClient`].
|
/// A builder for an [`RegistryClient`].
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct RegistryClientBuilder {
|
pub struct RegistryClientBuilder {
|
||||||
index_urls: IndexUrls,
|
index_urls: IndexUrls,
|
||||||
|
native_tls: bool,
|
||||||
retries: u32,
|
retries: u32,
|
||||||
connectivity: Connectivity,
|
connectivity: Connectivity,
|
||||||
cache: Cache,
|
cache: Cache,
|
||||||
|
|
@ -48,6 +50,7 @@ impl RegistryClientBuilder {
|
||||||
pub fn new(cache: Cache) -> Self {
|
pub fn new(cache: Cache) -> Self {
|
||||||
Self {
|
Self {
|
||||||
index_urls: IndexUrls::default(),
|
index_urls: IndexUrls::default(),
|
||||||
|
native_tls: false,
|
||||||
cache,
|
cache,
|
||||||
connectivity: Connectivity::Online,
|
connectivity: Connectivity::Online,
|
||||||
retries: 3,
|
retries: 3,
|
||||||
|
|
@ -75,6 +78,12 @@ impl RegistryClientBuilder {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn native_tls(mut self, native_tls: bool) -> Self {
|
||||||
|
self.native_tls = native_tls;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn cache<T>(mut self, cache: Cache) -> Self {
|
pub fn cache<T>(mut self, cache: Cache) -> Self {
|
||||||
self.cache = cache;
|
self.cache = cache;
|
||||||
|
|
@ -110,11 +119,19 @@ impl RegistryClientBuilder {
|
||||||
|
|
||||||
// Initialize the base client.
|
// Initialize the base client.
|
||||||
let client = self.client.unwrap_or_else(|| {
|
let client = self.client.unwrap_or_else(|| {
|
||||||
// Disallow any connections.
|
// Load the TLS configuration.
|
||||||
|
let tls = tls::load(if self.native_tls {
|
||||||
|
Roots::Native
|
||||||
|
} else {
|
||||||
|
Roots::Webpki
|
||||||
|
})
|
||||||
|
.expect("Failed to load TLS configuration.");
|
||||||
|
|
||||||
let client_core = ClientBuilder::new()
|
let client_core = ClientBuilder::new()
|
||||||
.user_agent(user_agent_string)
|
.user_agent(user_agent_string)
|
||||||
.pool_max_idle_per_host(20)
|
.pool_max_idle_per_host(20)
|
||||||
.timeout(std::time::Duration::from_secs(timeout));
|
.timeout(std::time::Duration::from_secs(timeout))
|
||||||
|
.use_preconfigured_tls(tls);
|
||||||
|
|
||||||
client_core.build().expect("Failed to build HTTP client.")
|
client_core.build().expect("Failed to build HTTP client.")
|
||||||
});
|
});
|
||||||
|
|
|
||||||
102
crates/uv-client/src/tls.rs
Normal file
102
crates/uv-client/src/tls.rs
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
use rustls::ClientConfig;
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub(crate) enum TlsError {
|
||||||
|
#[error(transparent)]
|
||||||
|
Rustls(#[from] rustls::Error),
|
||||||
|
#[error("zero valid certificates found in native root store")]
|
||||||
|
ZeroCertificates,
|
||||||
|
#[error("failed to load native root certificates")]
|
||||||
|
NativeCertificates(#[source] std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub(crate) enum Roots {
|
||||||
|
/// Use reqwest's `rustls-tls-webpki-roots` behavior for loading root certificates.
|
||||||
|
Webpki,
|
||||||
|
/// Use reqwest's `rustls-tls-native-roots` behavior for loading root certificates.
|
||||||
|
Native,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize a TLS configuration for the client.
|
||||||
|
///
|
||||||
|
/// This is equivalent to the TLS initialization `reqwest` when `rustls-tls` is enabled,
|
||||||
|
/// with two notable changes:
|
||||||
|
///
|
||||||
|
/// 1. It enables _either_ the `webpki-roots` or the `native-certs` feature, but not both.
|
||||||
|
/// 2. It assumes the following builder settings (which match the defaults):
|
||||||
|
/// - `root_certs: vec![]`
|
||||||
|
/// - `min_tls_version: None`
|
||||||
|
/// - `max_tls_version: None`
|
||||||
|
/// - `identity: None`
|
||||||
|
/// - `certs_verification: false`
|
||||||
|
/// - `tls_sni: true`
|
||||||
|
/// - `http_version_pref: HttpVersionPref::All`
|
||||||
|
///
|
||||||
|
/// See: <https://github.com/seanmonstar/reqwest/blob/e3192638518d577759dd89da489175b8f992b12f/src/async_impl/client.rs#L498>
|
||||||
|
pub(crate) fn load(roots: Roots) -> Result<ClientConfig, TlsError> {
|
||||||
|
// Set root certificates.
|
||||||
|
let mut root_cert_store = rustls::RootCertStore::empty();
|
||||||
|
|
||||||
|
match roots {
|
||||||
|
Roots::Webpki => {
|
||||||
|
// Use `rustls-tls-webpki-roots`
|
||||||
|
use rustls::OwnedTrustAnchor;
|
||||||
|
|
||||||
|
let trust_anchors = webpki_roots::TLS_SERVER_ROOTS.iter().map(|trust_anchor| {
|
||||||
|
OwnedTrustAnchor::from_subject_spki_name_constraints(
|
||||||
|
trust_anchor.subject,
|
||||||
|
trust_anchor.spki,
|
||||||
|
trust_anchor.name_constraints,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
root_cert_store.add_trust_anchors(trust_anchors);
|
||||||
|
}
|
||||||
|
Roots::Native => {
|
||||||
|
// Use: `rustls-tls-native-roots`
|
||||||
|
let mut valid_count = 0;
|
||||||
|
let mut invalid_count = 0;
|
||||||
|
for cert in
|
||||||
|
rustls_native_certs::load_native_certs().map_err(TlsError::NativeCertificates)?
|
||||||
|
{
|
||||||
|
let cert = rustls::Certificate(cert.0);
|
||||||
|
// Continue on parsing errors, as native stores often include ancient or syntactically
|
||||||
|
// invalid certificates, like root certificates without any X509 extensions.
|
||||||
|
// Inspiration: https://github.com/rustls/rustls/blob/633bf4ba9d9521a95f68766d04c22e2b01e68318/rustls/src/anchors.rs#L105-L112
|
||||||
|
match root_cert_store.add(&cert) {
|
||||||
|
Ok(_) => valid_count += 1,
|
||||||
|
Err(err) => {
|
||||||
|
invalid_count += 1;
|
||||||
|
warn!(
|
||||||
|
"rustls failed to parse DER certificate {:?} {:?}",
|
||||||
|
&err, &cert
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if valid_count == 0 && invalid_count > 0 {
|
||||||
|
return Err(TlsError::ZeroCertificates);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build TLS config
|
||||||
|
let config_builder = ClientConfig::builder()
|
||||||
|
.with_safe_default_cipher_suites()
|
||||||
|
.with_safe_default_kx_groups()
|
||||||
|
.with_protocol_versions(rustls::ALL_VERSIONS)?
|
||||||
|
.with_root_certificates(root_cert_store);
|
||||||
|
|
||||||
|
// Finalize TLS config
|
||||||
|
let mut tls = config_builder.with_no_client_auth();
|
||||||
|
|
||||||
|
// Enable SNI
|
||||||
|
tls.enable_sni = true;
|
||||||
|
|
||||||
|
// ALPN protocol
|
||||||
|
tls.alpn_protocols = vec!["h2".into(), "http/1.1".into()];
|
||||||
|
|
||||||
|
Ok(tls)
|
||||||
|
}
|
||||||
|
|
@ -67,6 +67,7 @@ pub(crate) async fn pip_compile(
|
||||||
python_version: Option<PythonVersion>,
|
python_version: Option<PythonVersion>,
|
||||||
exclude_newer: Option<DateTime<Utc>>,
|
exclude_newer: Option<DateTime<Utc>>,
|
||||||
annotation_style: AnnotationStyle,
|
annotation_style: AnnotationStyle,
|
||||||
|
native_tls: bool,
|
||||||
quiet: bool,
|
quiet: bool,
|
||||||
cache: Cache,
|
cache: Cache,
|
||||||
printer: Printer,
|
printer: Printer,
|
||||||
|
|
@ -188,6 +189,7 @@ pub(crate) async fn pip_compile(
|
||||||
|
|
||||||
// Initialize the registry client.
|
// Initialize the registry client.
|
||||||
let client = RegistryClientBuilder::new(cache.clone())
|
let client = RegistryClientBuilder::new(cache.clone())
|
||||||
|
.native_tls(native_tls)
|
||||||
.connectivity(connectivity)
|
.connectivity(connectivity)
|
||||||
.index_urls(index_locations.index_urls())
|
.index_urls(index_locations.index_urls())
|
||||||
.build();
|
.build();
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ pub(crate) async fn pip_install(
|
||||||
python: Option<String>,
|
python: Option<String>,
|
||||||
system: bool,
|
system: bool,
|
||||||
break_system_packages: bool,
|
break_system_packages: bool,
|
||||||
|
native_tls: bool,
|
||||||
cache: Cache,
|
cache: Cache,
|
||||||
printer: Printer,
|
printer: Printer,
|
||||||
) -> Result<ExitStatus> {
|
) -> Result<ExitStatus> {
|
||||||
|
|
@ -177,6 +178,7 @@ pub(crate) async fn pip_install(
|
||||||
|
|
||||||
// Initialize the registry client.
|
// Initialize the registry client.
|
||||||
let client = RegistryClientBuilder::new(cache.clone())
|
let client = RegistryClientBuilder::new(cache.clone())
|
||||||
|
.native_tls(native_tls)
|
||||||
.connectivity(connectivity)
|
.connectivity(connectivity)
|
||||||
.index_urls(index_locations.index_urls())
|
.index_urls(index_locations.index_urls())
|
||||||
.build();
|
.build();
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ pub(crate) async fn pip_sync(
|
||||||
python: Option<String>,
|
python: Option<String>,
|
||||||
system: bool,
|
system: bool,
|
||||||
break_system_packages: bool,
|
break_system_packages: bool,
|
||||||
|
native_tls: bool,
|
||||||
cache: Cache,
|
cache: Cache,
|
||||||
printer: Printer,
|
printer: Printer,
|
||||||
) -> Result<ExitStatus> {
|
) -> Result<ExitStatus> {
|
||||||
|
|
@ -116,6 +117,7 @@ pub(crate) async fn pip_sync(
|
||||||
|
|
||||||
// Initialize the registry client.
|
// Initialize the registry client.
|
||||||
let client = RegistryClientBuilder::new(cache.clone())
|
let client = RegistryClientBuilder::new(cache.clone())
|
||||||
|
.native_tls(native_tls)
|
||||||
.connectivity(connectivity)
|
.connectivity(connectivity)
|
||||||
.index_urls(index_locations.index_urls())
|
.index_urls(index_locations.index_urls())
|
||||||
.build();
|
.build();
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,18 @@ struct Cli {
|
||||||
)]
|
)]
|
||||||
color: ColorChoice,
|
color: ColorChoice,
|
||||||
|
|
||||||
|
/// Whether to load TLS certificates from the platform's native certificate store.
|
||||||
|
///
|
||||||
|
/// By default, `uv` loads certificates from the bundled `webpki-roots` crate. The
|
||||||
|
/// `webpki-roots` are a reliable set of trust roots from Mozilla, and including them in `uv`
|
||||||
|
/// improves portability and performance (especially on macOS).
|
||||||
|
///
|
||||||
|
/// However, in some cases, you may want to use the platform's native certificate store,
|
||||||
|
/// especially if you're relying on a corporate trust root (e.g., for a mandatory proxy) that's
|
||||||
|
/// included in your system's certificate store.
|
||||||
|
#[arg(global = true, long)]
|
||||||
|
native_tls: bool,
|
||||||
|
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
cache_args: CacheArgs,
|
cache_args: CacheArgs,
|
||||||
}
|
}
|
||||||
|
|
@ -1419,6 +1431,7 @@ async fn run() -> Result<ExitStatus> {
|
||||||
args.python_version,
|
args.python_version,
|
||||||
args.exclude_newer,
|
args.exclude_newer,
|
||||||
args.annotation_style,
|
args.annotation_style,
|
||||||
|
cli.native_tls,
|
||||||
cli.quiet,
|
cli.quiet,
|
||||||
cache,
|
cache,
|
||||||
printer,
|
printer,
|
||||||
|
|
@ -1475,6 +1488,7 @@ async fn run() -> Result<ExitStatus> {
|
||||||
args.python,
|
args.python,
|
||||||
args.system,
|
args.system,
|
||||||
args.break_system_packages,
|
args.break_system_packages,
|
||||||
|
cli.native_tls,
|
||||||
cache,
|
cache,
|
||||||
printer,
|
printer,
|
||||||
)
|
)
|
||||||
|
|
@ -1570,6 +1584,7 @@ async fn run() -> Result<ExitStatus> {
|
||||||
args.python,
|
args.python,
|
||||||
args.system,
|
args.system,
|
||||||
args.break_system_packages,
|
args.break_system_packages,
|
||||||
|
cli.native_tls,
|
||||||
cache,
|
cache,
|
||||||
printer,
|
printer,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue