Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ rand = { version = "0.8.5" }
rayon = { version = "1.8.0" }
reflink-copy = { version = "0.1.15" }
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-retry = { version = "0.3.0" }
rkyv = { version = "0.7.43", features = ["strict", "validation"] }
Expand Down
15 changes: 10 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -425,13 +425,18 @@ In addition, uv respects the following environment variables:

## Custom CA Certificates

uv supports custom CA certificates (such as those needed by corporate proxies) by utilizing the
system's trust store. To ensure this works out of the box, ensure your certificates are added to the
system's trust 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, 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
variable to the path of the certificate bundle, to instruct uv to use that file instead of the
system's trust store.
variable to the path of the certificate bundle (alongside the `--native-tls` flag), to instruct uv
to use that file instead of the system's trust store.

## Acknowledgements

Expand Down
5 changes: 5 additions & 0 deletions crates/uv-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ tracing = { workspace = true }
url = { 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]
anyhow = { workspace = true }
hyper = { version = "0.14.28", features = ["server", "http1"] }
Expand Down
1 change: 1 addition & 0 deletions crates/uv-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ mod middleware;
mod registry_client;
mod remote_metadata;
mod rkyvutil;
mod tls;
23 changes: 20 additions & 3 deletions crates/uv-client/src/registry_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@ use crate::html::SimpleHtml;
use crate::middleware::{NetrcMiddleware, OfflineMiddleware};
use crate::remote_metadata::wheel_metadata_from_remote_zip;
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`].
#[derive(Debug, Clone)]
pub struct RegistryClientBuilder {
index_urls: IndexUrls,
native_tls: bool,
retries: u32,
connectivity: Connectivity,
cache: Cache,
Expand All @@ -48,6 +50,7 @@ impl RegistryClientBuilder {
pub fn new(cache: Cache) -> Self {
Self {
index_urls: IndexUrls::default(),
native_tls: false,
cache,
connectivity: Connectivity::Online,
retries: 3,
Expand Down Expand Up @@ -75,6 +78,12 @@ impl RegistryClientBuilder {
self
}

#[must_use]
pub fn native_tls(mut self, native_tls: bool) -> Self {
self.native_tls = native_tls;
self
}

#[must_use]
pub fn cache<T>(mut self, cache: Cache) -> Self {
self.cache = cache;
Expand Down Expand Up @@ -110,11 +119,19 @@ impl RegistryClientBuilder {

// Initialize the base client.
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()
.user_agent(user_agent_string)
.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.")
});
Expand Down
102 changes: 102 additions & 0 deletions crates/uv-client/src/tls.rs
Original file line number Diff line number Diff line change
@@ -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)
}
2 changes: 2 additions & 0 deletions crates/uv/src/commands/pip_compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ pub(crate) async fn pip_compile(
python_version: Option<PythonVersion>,
exclude_newer: Option<DateTime<Utc>>,
annotation_style: AnnotationStyle,
native_tls: bool,
quiet: bool,
cache: Cache,
printer: Printer,
Expand Down Expand Up @@ -188,6 +189,7 @@ pub(crate) async fn pip_compile(

// Initialize the registry client.
let client = RegistryClientBuilder::new(cache.clone())
.native_tls(native_tls)
.connectivity(connectivity)
.index_urls(index_locations.index_urls())
.build();
Expand Down
2 changes: 2 additions & 0 deletions crates/uv/src/commands/pip_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ pub(crate) async fn pip_install(
python: Option<String>,
system: bool,
break_system_packages: bool,
native_tls: bool,
cache: Cache,
printer: Printer,
) -> Result<ExitStatus> {
Expand Down Expand Up @@ -177,6 +178,7 @@ pub(crate) async fn pip_install(

// Initialize the registry client.
let client = RegistryClientBuilder::new(cache.clone())
.native_tls(native_tls)
.connectivity(connectivity)
.index_urls(index_locations.index_urls())
.build();
Expand Down
2 changes: 2 additions & 0 deletions crates/uv/src/commands/pip_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ pub(crate) async fn pip_sync(
python: Option<String>,
system: bool,
break_system_packages: bool,
native_tls: bool,
cache: Cache,
printer: Printer,
) -> Result<ExitStatus> {
Expand Down Expand Up @@ -116,6 +117,7 @@ pub(crate) async fn pip_sync(

// Initialize the registry client.
let client = RegistryClientBuilder::new(cache.clone())
.native_tls(native_tls)
.connectivity(connectivity)
.index_urls(index_locations.index_urls())
.build();
Expand Down
15 changes: 15 additions & 0 deletions crates/uv/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,18 @@ struct Cli {
)]
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)]
cache_args: CacheArgs,
}
Expand Down Expand Up @@ -1419,6 +1431,7 @@ async fn run() -> Result<ExitStatus> {
args.python_version,
args.exclude_newer,
args.annotation_style,
cli.native_tls,
cli.quiet,
cache,
printer,
Expand Down Expand Up @@ -1475,6 +1488,7 @@ async fn run() -> Result<ExitStatus> {
args.python,
args.system,
args.break_system_packages,
cli.native_tls,
cache,
printer,
)
Expand Down Expand Up @@ -1570,6 +1584,7 @@ async fn run() -> Result<ExitStatus> {
args.python,
args.system,
args.break_system_packages,
cli.native_tls,
cache,
printer,
)
Expand Down