feat: mTLS support (#4171)

## Summary

Closes https://github.com/astral-sh/uv/issues/3626

This adds mTLS support to uv via the standard env var `SSL_CLIENT_CERT`.

## Test Plan

Tested locally using a [nginx proxy to
pypi](https://github.com/hauntsaninja/nginx_pypi_cache) using my own
self-signed ca + certs + client certs generated via
[mkcert](https://github.com/FiloSottile/mkcert). Used this proxy with
both uv and pip to make sure we have feature partity in mTLS
functionality.
This commit is contained in:
samypr100 2024-06-10 21:11:35 -04:00 committed by GitHub
parent 5f37395f45
commit 68abf85f0d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 38 additions and 0 deletions

View file

@ -502,6 +502,9 @@ If a direct path to the certificate is required (e.g., in CI), set the `SSL_CERT
variable to the path of the certificate bundle, to instruct uv to use that file instead of the
system's trust store.
If client certificate authentication (mTLS) is desired, set the `SSL_CLIENT_CERT` environment
variable to the path of the PEM formatted file containing the certificate followed by the private key.
## Platform support
uv has Tier 1 support for the following platforms:
@ -595,6 +598,8 @@ In addition, uv respects the following environment variables:
- `SSL_CERT_FILE`: If set, uv will use this file as the certificate bundle instead of the system's
trust store.
- `SSL_CLIENT_CERT`: If set, uv will use this file for mTLS authentication. This should be a single
file containing both the certificate and the private key in PEM format.
- `RUST_LOG`: If set, uv will use this value as the log level for its `--verbose` output. Accepts
any filter compatible with the `tracing_subscriber` crate. For example, `RUST_LOG=trace` will
enable trace-level logging. See the [tracing documentation](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#example-syntax)

View file

@ -23,6 +23,7 @@ use uv_warnings::warn_user_once;
use crate::linehaul::LineHaul;
use crate::middleware::OfflineMiddleware;
use crate::tls::read_identity;
use crate::Connectivity;
/// A builder for an [`BaseClient`].
@ -161,6 +162,19 @@ impl<'a> BaseClientBuilder<'a> {
client_core.tls_built_in_webpki_certs(true)
};
// Configure mTLS.
let client_core = if let Some(ssl_client_cert) = env::var_os("SSL_CLIENT_CERT") {
match read_identity(&ssl_client_cert) {
Ok(identity) => client_core.identity(identity),
Err(err) => {
warn_user_once!("Ignoring invalid `SSL_CLIENT_CERT`: {err}");
client_core
}
}
} else {
client_core
};
client_core.build().expect("Failed to build HTTP client.")
});

View file

@ -20,3 +20,4 @@ mod middleware;
mod registry_client;
mod remote_metadata;
mod rkyvutil;
mod tls;

View file

@ -0,0 +1,18 @@
use reqwest::Identity;
use std::ffi::OsStr;
use std::io::Read;
#[derive(thiserror::Error, Debug)]
pub(crate) enum CertificateError {
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Reqwest(#[from] reqwest::Error),
}
/// Return the `Identity` from the provided file.
pub(crate) fn read_identity(ssl_client_cert: &OsStr) -> Result<Identity, CertificateError> {
let mut buf = Vec::new();
fs_err::File::open(ssl_client_cert)?.read_to_end(&mut buf)?;
Ok(Identity::from_pem(&buf)?)
}