Upgrade reqwest to 0.13 (#18550)

The following user-facing changes are included here:

- `aws-lc` is used instead of `ring` for a cryptography backend
- Expands our certificate signature algorithm support to include
ECDSA_P256_SHA512, ECDSA_P384_SHA512, ECDSA_P521_SHA256,
ECDSA_P521_SHA384, and ECDSA_P521_SHA512
- `--native-tls` is deprecated in favor of a new `--system-certs` flag,
avoiding confusion with the TLS implementation used (we use `rustls` not
`native-tls`, see prior confusion at
https://github.com/astral-sh/uv/issues/11595)
- NASM is a new build requirement on Windows, it is required by `aws-lc`
on x86-64 and i386
- `rustls-platform-verifier` is used instead of `rustls-native-certs`
for system certificate verification
- On macOS, certificate validation is now delegated to
`Security.framework` (`SecTrust`). Performance when using
`--system-certs` is improved by avoiding exporting and parsing all the
certificates from the keychain at startup.
- On Windows, certificate validation is now delegated to
`CertGetCertificateChain` and `CertVerifyCertificateChainPolicy`
    - On Linux, certificate validation should be approximately unchanged
- Some previously failing chains may succeed, and some previously
accepted chains may fail; generally, this should result in behavior
closer matching browsers and other native applications
- macOS and Windows may now perform live OCSP fetches for early
revocation, which could add latency to some requests
- Empty `SSL_CERT_FILE` values are ignored (for consistency with
`SSL_CERT_DIR`)

The following internal changes are included here:

- Certificate loading has been refactored to use a newtype with helper
methods
- The certificate tests have been rewritten
- We use `webpki-root-certs` instead of `webpki-roots`, see
https://github.com/astral-sh/uv/pull/17543#discussion_r2820187691
- We request `identity` encoding for range requests, see
https://github.com/astral-sh/async_http_range_reader/pull/3#discussion_r2700194798
- Various dependencies (including forks) updates to versions which use
reqwest 0.13+

This is a replacement of #17543 with an updated description. See that
pull request for prior discussion. I've made the following changes from
the initial approach there:

- Previously, the `native-tls` TLS implementation was added which
included an OpenSSL build. We don't currently use the `native-tls`
implementation, but the `--native-tls` flag there was erroneously
updated to enable it.
- Previously, there was a `--tls-backend` flag to toggle between
`native-tls` and `rustls`. Since we currently always use `rustls`, this
is deferred to future work (if we need it at all).
- Previously, there were unintentional breaking changes to
`SSL_CERT_FILE` and `SSL_CERT_DIR` handling, including merging with the
base certificates instead of replacing them, dropping support for
OpenSSL hash-named certificate files, skipping deduplication of
certificates. Here, we retain use of `rustls-native-certs` for loading
certificates from the system as it handles these edge cases.


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

---------

Co-authored-by: salmonsd <22984014+salmonsd@users.noreply.github.com>
This commit is contained in:
Zanie Blue
2026-03-23 13:22:19 -05:00
committed by GitHub
parent c43c0d0e8b
commit b6854d77bf
43 changed files with 2201 additions and 773 deletions
+2
View File
@@ -307,7 +307,9 @@ jobs:
echo "CC_aarch64_linux_android=${TOOLCHAIN}/bin/aarch64-linux-android24-clang" >> "$GITHUB_ENV"
echo "CXX_aarch64_linux_android=${TOOLCHAIN}/bin/aarch64-linux-android24-clang++" >> "$GITHUB_ENV"
echo "AR_aarch64_linux_android=${TOOLCHAIN}/bin/llvm-ar" >> "$GITHUB_ENV"
echo "RANLIB_aarch64_linux_android=${TOOLCHAIN}/bin/llvm-ranlib" >> "$GITHUB_ENV"
echo "CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=${TOOLCHAIN}/bin/aarch64-linux-android24-clang" >> "$GITHUB_ENV"
echo "CARGO_TARGET_AARCH64_LINUX_ANDROID_RANLIB=${TOOLCHAIN}/bin/llvm-ranlib" >> "$GITHUB_ENV"
# NDK 23+ removed libgcc, provide a stub that redirects to libunwind
LIBDIR=$(echo "${TOOLCHAIN}"/lib/clang/*/lib/linux/aarch64)
+23 -10
View File
@@ -256,7 +256,14 @@ jobs:
- name: "Install cargo extensions"
shell: bash
run: scripts/install-cargo-extensions.sh
- name: "Install NASM"
# NASM is required for x86/x86-64 Windows targets by aws-lc-sys.
# On aarch64-pc-windows-msvc, it uses clang-cl instead.
# See: https://aws.github.io/aws-lc-rs/requirements/windows.html#build-requirements
if: contains(matrix.platform.target, 'x86') || contains(matrix.platform.target, 'i686')
run: |
winget install NASM.NASM --accept-source-agreements --accept-package-agreements
echo "C:\Program Files\NASM" | Out-File -FilePath $env:GITHUB_PATH -Append
# uv
- name: "Build wheels"
uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.50.1
@@ -266,6 +273,8 @@ jobs:
args: --release --locked --out dist --features self-update,windows-gui-bin --compatibility pypi
env:
CARGO: ${{ github.workspace }}/scripts/cargo.cmd
# Disable prebuilt NASM objects so we always compile assembly from source.
AWS_LC_SYS_PREBUILT_NASM: "0"
- name: "Test wheel"
shell: bash
run: |
@@ -351,25 +360,25 @@ jobs:
manylinux: 2_17
docker-options: -e CARGO
args: --release --locked --out dist --features self-update --compatibility pypi
# See: https://github.com/sfackler/rust-openssl/issues/2036#issuecomment-1724324145
before-script-linux: |
# Install the 32-bit cross target on 64-bit (noop if we're already on 64-bit)
rustup target add ${{ matrix.target }}
# If we're running on rhel centos, install needed packages.
if command -v yum &> /dev/null; then
yum update -y && yum install -y perl-core openssl openssl-devel pkgconfig libatomic
yum update -y && yum install -y pkgconfig libatomic
# If we're running on i686 we need to symlink libatomic
# in order to build openssl with -latomic flag.
if [[ ! -d "/usr/lib64" ]]; then
# Install cross build requirements
if [[ "${{ matrix.target }}" == "i686-unknown-linux-gnu" ]]; then
yum install -y glibc-devel.i686 libstdc++-devel.i686 libatomic.i686
fi
# Symlink libatomic so the linker can find it with -latomic.
if [[ -f "/usr/lib/libatomic.so.1" && ! -f "/usr/lib/libatomic.so" ]]; then
ln -s /usr/lib/libatomic.so.1 /usr/lib/libatomic.so
else
# Support cross-compiling from 64-bit to 32-bit
yum install -y glibc-devel.i686 libstdc++-devel.i686
fi
else
# If we're running on debian-based system.
apt update -y && apt-get install -y libssl-dev openssl pkg-config
apt update -y && apt-get install -y pkg-config
fi
# Install cargo extensions as a static musl binary so it runs in any container.
scripts/install-cargo-extensions.sh
@@ -595,8 +604,12 @@ jobs:
rust-toolchain: ${{ matrix.platform.toolchain || null }}
before-script-linux: |
scripts/install-cargo-extensions.sh
# Install the s390x cross target on x86_64
rustup target add ${{ matrix.platform.target }}
apt-get update && apt-get install -y gcc-s390x-linux-gnu binutils-s390x-linux-gnu
env:
CARGO: ${{ github.workspace }}/scripts/cargo.sh
- uses: uraimo/run-on-arch-action@d94c13912ea685de38fccc1109385b83fd79427d # v3.0.1
name: "Test wheel"
with:
+11
View File
@@ -53,6 +53,17 @@ On Fedora-based distributions, you can install a C compiler with:
sudo dnf install gcc
```
On Windows, [NASM](https://www.nasm.us/) is required for building the TLS backend (`aws-lc-sys`). If
it is not present, a prebuilt blob provided by `aws-lc-sys` will be used instead. WinGet can be used
to install NASM:
```shell
winget install NASM.NASM
```
After installation, add `C:\Program Files\NASM` to your `PATH`. While the prebuilt blob will not be
used when NASM is found, you can guarantee this behavior by setting `AWS_LC_SYS_PREBUILT_NASM=0`.
## Testing
For running tests, we recommend [nextest](https://nexte.st/).
Generated
+246 -57
View File
@@ -36,9 +36,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "ambient-id"
version = "0.0.10"
version = "0.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e61320f0a8ca54a235b0e49307b16dcade6eecd441b1f8a8c7ca9204056cb17c"
checksum = "c1daa54020e05aa0b163ee10434fff35a0f18d28a1cafa142bd1290e1abe630e"
dependencies = [
"astral-reqwest-middleware",
"reqwest",
@@ -114,7 +114,7 @@ version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -125,7 +125,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -256,9 +256,9 @@ dependencies = [
[[package]]
name = "astral-reqwest-middleware"
version = "0.4.2"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "638d02e24aeb92f9537897cd1ff82e2bc98fd9ac9575a503e27bb07cdf64d4d7"
checksum = "98e1c6be25cfbf1bb4fea1a9da51bc05d3259a9062df4e53f54e5607895e33c9"
dependencies = [
"anyhow",
"async-trait",
@@ -271,9 +271,9 @@ dependencies = [
[[package]]
name = "astral-reqwest-retry"
version = "0.8.0"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ab210f6cdf8fd3254d47e5ee27ce60ed34a428ff71b4ae9477b1c84b49498c"
checksum = "48c76a42c052d7a95249b90b83d44e8f1bbde7c8e08dbed50d49c58321815da3"
dependencies = [
"anyhow",
"astral-reqwest-middleware",
@@ -326,18 +326,18 @@ dependencies = [
[[package]]
name = "astral_async_http_range_reader"
version = "0.9.1"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ddaca0fbbf0d91103cca7c7611790c65f6eff1d456f7fe6bf565d436dc9b8f3"
checksum = "7ab9a05148c7b3e85c17806fef4b2889c44f1a60575c5da9e9be80ce13227185"
dependencies = [
"astral-reqwest-middleware",
"bisection",
"futures",
"http-content-range",
"itertools 0.13.0",
"itertools 0.14.0",
"memmap2",
"reqwest",
"thiserror 1.0.69",
"thiserror 2.0.18",
"tokio",
"tokio-stream",
"tokio-util",
@@ -436,10 +436,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "axoasset"
version = "1.5.0"
name = "aws-lc-rs"
version = "1.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "448205969ebf1eec16088881b9d309e90e52ceed1bfa92e7efbfb00f3b10982a"
checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc"
dependencies = [
"aws-lc-sys",
"zeroize",
]
[[package]]
name = "aws-lc-sys"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa7e52a4c5c547c741610a2c6f123f3881e409b714cd27e6798ef020c514f0a"
dependencies = [
"cc",
"cmake",
"dunce",
"fs_extra",
]
[[package]]
name = "axoasset"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1be1b9c2739b635e04c7bbcde9e89dd5e874b9e86e28f1b41c44eb830635d83e"
dependencies = [
"camino",
"image",
@@ -478,9 +500,9 @@ dependencies = [
[[package]]
name = "axoupdater"
version = "0.9.1"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc482a1926df098f4e3806b834f3fe73a1ab54b24ab0ac481f72de479af5e982"
checksum = "0ab66f118bab79524a27139b7341cdf1c4f839c6274ef89a6d8fb4365cb218cf"
dependencies = [
"axoasset",
"axoprocess",
@@ -736,6 +758,12 @@ dependencies = [
"shlex",
]
[[package]]
name = "cesu8"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
[[package]]
name = "cfg-if"
version = "1.0.4"
@@ -866,6 +894,15 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
name = "cmake"
version = "0.1.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d"
dependencies = [
"cc",
]
[[package]]
name = "codspeed"
version = "4.4.1"
@@ -945,7 +982,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
name = "combine"
version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
dependencies = [
"bytes",
"memchr",
]
[[package]]
@@ -1313,7 +1360,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -1492,7 +1539,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -1668,6 +1715,12 @@ dependencies = [
"tokio",
]
[[package]]
name = "fs_extra"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]]
name = "futures"
version = "0.3.32"
@@ -2079,12 +2132,10 @@ dependencies = [
"hyper",
"hyper-util",
"rustls",
"rustls-native-certs",
"rustls-pki-types",
"tokio",
"tokio-rustls",
"tower-service",
"webpki-roots",
]
[[package]]
@@ -2357,7 +2408,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
dependencies = [
"hermit-abi",
"libc",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -2391,15 +2442,6 @@ dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.14.0"
@@ -2427,7 +2469,7 @@ dependencies = [
"portable-atomic",
"portable-atomic-util",
"serde_core",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -2456,6 +2498,50 @@ dependencies = [
"jiff-tzdb",
]
[[package]]
name = "jni"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
dependencies = [
"cesu8",
"cfg-if",
"combine",
"jni-sys 0.3.1",
"log",
"thiserror 1.0.69",
"walkdir",
"windows-sys 0.45.0",
]
[[package]]
name = "jni-sys"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258"
dependencies = [
"jni-sys 0.4.1",
]
[[package]]
name = "jni-sys"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
dependencies = [
"jni-sys-macros",
]
[[package]]
name = "jni-sys-macros"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
dependencies = [
"quote",
"syn",
]
[[package]]
name = "jobserver"
version = "0.1.34"
@@ -2498,7 +2584,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cfc352a66ba903c23239ef51e809508b6fc2b0f90e3476ac7a9ff47e863ae95"
dependencies = [
"scopeguard",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -2769,7 +2855,7 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "536bfad37a309d62069485248eeaba1e8d9853aaf951caaeaed0585a95346f08"
dependencies = [
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -2857,7 +2943,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -3553,6 +3639,7 @@ version = "0.11.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
dependencies = [
"aws-lc-rs",
"bytes",
"getrandom 0.3.3",
"lru-slab",
@@ -3579,7 +3666,7 @@ dependencies = [
"once_cell",
"socket2",
"tracing",
"windows-sys 0.52.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -3806,9 +3893,9 @@ dependencies = [
[[package]]
name = "reqsign"
version = "0.18.1"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea386ba750000b6e59f760a08bdcca9461809b95e6f8f209ce5724056802824f"
checksum = "af8fa9a6948938319944b0f5399b1de4324767ac35c8bdbf991b3e71197b1087"
dependencies = [
"reqsign-aws-v4",
"reqsign-command-execute-tokio",
@@ -3907,9 +3994,9 @@ dependencies = [
[[package]]
name = "reqsign-http-send-reqwest"
version = "2.0.1"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46186bce769674f9200ad01af6f2ca42de3e819ddc002fff1edae135bfb6cd9c"
checksum = "c76a1aafda3c789146d5df362fb2e760051c38e0957f863c0bec8713d3a35833"
dependencies = [
"anyhow",
"async-trait",
@@ -3924,11 +4011,10 @@ dependencies = [
[[package]]
name = "reqwest"
version = "0.12.22"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531"
checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801"
dependencies = [
"async-compression",
"base64 0.22.1",
"bytes",
"futures-channel",
@@ -3948,8 +4034,8 @@ dependencies = [
"pin-project-lite",
"quinn",
"rustls",
"rustls-native-certs",
"rustls-pki-types",
"rustls-platform-verifier",
"serde",
"serde_json",
"serde_urlencoded",
@@ -3965,7 +4051,6 @@ dependencies = [
"wasm-bindgen-futures",
"wasm-streams",
"web-sys",
"webpki-roots",
]
[[package]]
@@ -4148,7 +4233,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -4157,8 +4242,8 @@ version = "0.23.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
dependencies = [
"aws-lc-rs",
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki",
"subtle",
@@ -4187,12 +4272,40 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rustls-platform-verifier"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
dependencies = [
"core-foundation 0.10.1",
"core-foundation-sys",
"jni",
"log",
"once_cell",
"rustls",
"rustls-native-certs",
"rustls-platform-verifier-android",
"rustls-webpki",
"security-framework",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.61.2",
]
[[package]]
name = "rustls-platform-verifier-android"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]]
name = "rustls-webpki"
version = "0.103.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53"
dependencies = [
"aws-lc-rs",
"ring",
"rustls-pki-types",
"untrusted",
@@ -4656,7 +4769,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
dependencies = [
"libc",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -4870,6 +4983,7 @@ version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96374855068f47402c3121c6eed88d29cb1de8f3ab27090e273e420bdabcf050"
dependencies = [
"futures",
"parking_lot",
]
@@ -4880,10 +4994,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom 0.3.3",
"getrandom 0.4.1",
"once_cell",
"rustix",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -5299,13 +5413,18 @@ version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
dependencies = [
"async-compression",
"bitflags 2.11.0",
"bytes",
"futures-core",
"futures-util",
"http",
"http-body",
"http-body-util",
"iri-string",
"pin-project-lite",
"tokio",
"tokio-util",
"tower",
"tower-layer",
"tower-service",
@@ -5495,7 +5614,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
dependencies = [
"memoffset",
"tempfile",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -6131,8 +6250,11 @@ dependencies = [
"rmp-serde",
"rustc-hash",
"rustls",
"rustls-native-certs",
"rustls-pki-types",
"serde",
"serde_json",
"temp-env",
"tempfile",
"thiserror 2.0.18",
"tokio",
@@ -6161,6 +6283,7 @@ dependencies = [
"uv-torch",
"uv-version",
"uv-warnings",
"webpki-root-certs",
"wiremock",
]
@@ -7544,9 +7667,9 @@ dependencies = [
[[package]]
name = "wasm-streams"
version = "0.4.2"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb"
dependencies = [
"futures-util",
"js-sys",
@@ -7602,10 +7725,10 @@ dependencies = [
]
[[package]]
name = "webpki-roots"
name = "webpki-root-certs"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed"
checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca"
dependencies = [
"rustls-pki-types",
]
@@ -7651,7 +7774,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -7773,6 +7896,15 @@ dependencies = [
"windows-link 0.1.3",
]
[[package]]
name = "windows-sys"
version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
dependencies = [
"windows-targets 0.42.2",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
@@ -7809,6 +7941,21 @@ dependencies = [
"windows-link 0.2.1",
]
[[package]]
name = "windows-targets"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
dependencies = [
"windows_aarch64_gnullvm 0.42.2",
"windows_aarch64_msvc 0.42.2",
"windows_i686_gnu 0.42.2",
"windows_i686_msvc 0.42.2",
"windows_x86_64_gnu 0.42.2",
"windows_x86_64_gnullvm 0.42.2",
"windows_x86_64_msvc 0.42.2",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
@@ -7860,6 +8007,12 @@ dependencies = [
"windows-link 0.2.1",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
@@ -7872,6 +8025,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
@@ -7884,6 +8043,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
@@ -7908,6 +8073,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
@@ -7920,6 +8091,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
@@ -7932,6 +8109,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
@@ -7944,6 +8127,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
+17 -12
View File
@@ -88,8 +88,8 @@ uv-warnings = { version = "0.0.32", path = "crates/uv-warnings" }
uv-windows = { version = "0.0.32", path = "crates/uv-windows" }
uv-workspace = { version = "0.0.32", path = "crates/uv-workspace" }
ambient-id = { version = "0.0.10", default-features = false, features = [
"astral-reqwest-middleware",
ambient-id = { version = "0.0.11", default-features = false, features = [
"reqwest-middleware",
] }
anstream = { version = "1.0.0" }
anyhow = { version = "1.0.89" }
@@ -104,7 +104,7 @@ async-compression = { version = "0.4.12", features = [
"zstd",
] }
async-trait = { version = "0.1.82" }
async_http_range_reader = { version = "0.9.1", package = "astral_async_http_range_reader" }
async_http_range_reader = { version = "0.10.0", package = "astral_async_http_range_reader" }
async_zip = { version = "0.0.17", package = "astral_async_zip", features = [
"bzip2",
"deflate",
@@ -113,7 +113,7 @@ async_zip = { version = "0.0.17", package = "astral_async_zip", features = [
"xz",
"zstd",
] }
axoupdater = { version = "0.9.0", default-features = false }
axoupdater = { version = "0.10.0", default-features = false }
backon = { version = "1.3.0" }
base64 = { version = "0.22.1" }
bitflags = { version = "2.6.0" }
@@ -196,35 +196,39 @@ regex-automata = { version = "0.4.8", default-features = false, features = [
"std",
"syntax",
] }
reqsign = { version = "0.18.1", features = [
reqsign = { version = "0.19.0", features = [
"aws",
"google",
"default-context",
], default-features = false }
reqwest = { version = "0.12.22", default-features = false, features = [
reqwest = { version = "0.13.1", default-features = false, features = [
"json",
"gzip",
"deflate",
"zstd",
"stream",
"system-proxy",
"rustls-tls",
"rustls-tls-native-roots",
"rustls",
"socks",
"multipart",
"http2",
"blocking",
"query",
"form",
] }
reqwest-middleware = { version = "0.4.2", package = "astral-reqwest-middleware", features = [
reqwest-middleware = { version = "0.5.1", package = "astral-reqwest-middleware", features = [
"multipart",
"query",
] }
reqwest-retry = { version = "0.8.0", package = "astral-reqwest-retry", features = [
reqwest-retry = { version = "0.9.1", package = "astral-reqwest-retry", features = [
"tracing",
] }
rkyv = { version = "0.8.14", features = ["bytecheck"] }
rmp-serde = { version = "1.3.0" }
rust-netrc = { version = "0.1.2" }
rustc-hash = { version = "2.0.0" }
rustls-native-certs = { version = "0.8.3" }
rustls-pki-types = { version = "1.14.0" }
rustix = { version = "1.0.0", default-features = false, features = [
"fs",
"std",
@@ -274,6 +278,7 @@ url = { version = "2.5.2", features = ["serde"] }
uuid = { version = "1.16.0" }
version-ranges = { version = "0.1.3", package = "astral-version-ranges" }
walkdir = { version = "2.5.0" }
webpki-root-certs = { version = "1" }
which = { version = "8.0.0", features = ["regex"] }
windows = { version = "0.61.0", features = [
"std",
@@ -325,9 +330,9 @@ rcgen = { version = "0.14.5", features = [
"pem",
"ring",
], default-features = false }
rustls = { version = "0.23.29", default-features = false }
rustls = { version = "0.23.36", default-features = false }
similar = { version = "2.6.0" }
temp-env = { version = "0.3.6" }
temp-env = { version = "0.3.6", features = ["async_closure"] }
test-case = { version = "3.3.1" }
test-log = { version = "0.2.16", features = [
"trace",
+1
View File
@@ -18,6 +18,7 @@ doc-valid-idents = [
"UV_LOCKED",
"UV_MANAGED_PYTHON",
"UV_NATIVE_TLS",
"UV_SYSTEM_CERTS",
"UV_NO_DEV",
"UV_NO_EDITABLE",
"UV_NO_ENV_FILE",
+1 -2
View File
@@ -34,8 +34,7 @@ tracing = { workspace = true }
[dev-dependencies]
insta = { workspace = true }
reqwest = { workspace = true, default-features = false, features = [
"rustls-tls",
"rustls-tls-native-roots",
"rustls",
] }
serde_json = { workspace = true }
tokio = { workspace = true }
+20 -8
View File
@@ -236,20 +236,32 @@ pub struct GlobalArgs {
)]
pub color: Option<ColorChoice>,
/// Whether to load TLS certificates from the platform's native store [env: UV_NATIVE_TLS=]
/// Whether to load TLS certificates from the platform's native certificate store [env:
/// UV_NATIVE_TLS=]
///
/// 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).
/// By default, uv uses bundled Mozilla root certificates. When enabled, this flag loads
/// certificates from the platform's native certificate store instead.
///
/// This is equivalent to `--system-certs`.
#[arg(global = true, long, value_parser = clap::builder::BoolishValueParser::new(), overrides_with_all = ["no_native_tls", "system_certs", "no_system_certs"], hide = true)]
pub native_tls: bool,
#[arg(global = true, long, overrides_with_all = ["native_tls", "system_certs", "no_system_certs"], hide = true)]
pub no_native_tls: bool,
/// Whether to load TLS certificates from the platform's native certificate store [env: UV_SYSTEM_CERTS=]
///
/// By default, uv uses bundled Mozilla root certificates, which 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, value_parser = clap::builder::BoolishValueParser::new(), overrides_with("no_native_tls"))]
pub native_tls: bool,
#[arg(global = true, long, value_parser = clap::builder::BoolishValueParser::new(), overrides_with_all = ["no_system_certs", "native_tls", "no_native_tls"])]
pub system_certs: bool,
#[arg(global = true, long, overrides_with("native_tls"), hide = true)]
pub no_native_tls: bool,
#[arg(global = true, long, overrides_with_all = ["system_certs", "native_tls", "no_native_tls"], hide = true)]
pub no_system_certs: bool,
/// Disable network access [env: UV_OFFLINE=]
///
+7
View File
@@ -9,6 +9,9 @@ repository = { workspace = true }
authors = { workspace = true }
license = { workspace = true }
[features]
test-pypi = []
[lib]
doctest = false
@@ -57,6 +60,8 @@ reqwest-retry = { workspace = true }
rkyv = { workspace = true }
rmp-serde = { workspace = true }
rustc-hash = { workspace = true }
rustls-native-certs = { workspace = true }
rustls-pki-types = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
uv-platform = { workspace = true }
@@ -65,6 +70,7 @@ tokio = { workspace = true }
tokio-util = { workspace = true }
tracing = { workspace = true }
url = { workspace = true }
webpki-root-certs = { workspace = true }
[dev-dependencies]
anyhow = { workspace = true }
@@ -75,6 +81,7 @@ insta = { workspace = true }
rcgen = { workspace = true }
regex = { workspace = true }
rustls = { workspace = true }
temp-env = { workspace = true }
tempfile = { workspace = true }
tokio = { workspace = true }
tokio-rustls = { workspace = true }
+30 -108
View File
@@ -2,12 +2,12 @@ use std::error::Error;
use std::fmt::Debug;
use std::fmt::Write;
use std::num::ParseIntError;
use std::path::Path;
use std::sync::Arc;
use std::time::{Duration, SystemTime, SystemTimeError};
use std::{env, io, iter};
use anyhow::anyhow;
use http::{
HeaderMap, HeaderName, HeaderValue, Method, StatusCode,
header::{
@@ -16,7 +16,9 @@ use http::{
},
};
use itertools::Itertools;
use reqwest::{Client, ClientBuilder, IntoUrl, NoProxy, Proxy, Request, Response, multipart};
use reqwest::{
Certificate, Client, ClientBuilder, IntoUrl, NoProxy, Proxy, Request, Response, multipart,
};
use reqwest_middleware::{ClientWithMiddleware, Middleware};
use reqwest_retry::policies::ExponentialBackoff;
use reqwest_retry::{
@@ -33,7 +35,7 @@ use url::Url;
use uv_auth::{AuthMiddleware, Credentials, CredentialsCache, Indexes, PyxTokenStore};
use uv_configuration::ProxyUrlKind;
use uv_configuration::{KeyringProviderType, ProxyUrl, TrustedHost};
use uv_fs::Simplified;
use uv_pep508::MarkerEnvironment;
use uv_platform_tags::Platform;
use uv_preview::Preview;
@@ -45,7 +47,7 @@ use uv_warnings::warn_user_once;
use crate::linehaul::LineHaul;
use crate::middleware::OfflineMiddleware;
use crate::tls::read_identity;
use crate::tls::{Certificates, read_identity};
use crate::{Connectivity, WrappedReqwestError};
pub const DEFAULT_RETRIES: u32 = 3;
@@ -88,8 +90,7 @@ pub struct BaseClientBuilder<'a> {
keyring: KeyringProviderType,
preview: Preview,
allow_insecure_host: Vec<TrustedHost>,
native_tls: bool,
built_in_root_certs: bool,
system_certs: bool,
retries: u32,
pub connectivity: Connectivity,
markers: Option<&'a MarkerEnvironment>,
@@ -161,8 +162,7 @@ impl Default for BaseClientBuilder<'_> {
keyring: KeyringProviderType::default(),
preview: Preview::default(),
allow_insecure_host: vec![],
native_tls: false,
built_in_root_certs: false,
system_certs: false,
connectivity: Connectivity::Online,
retries: DEFAULT_RETRIES,
markers: None,
@@ -190,7 +190,7 @@ impl Default for BaseClientBuilder<'_> {
impl<'a> BaseClientBuilder<'a> {
pub fn new(
connectivity: Connectivity,
native_tls: bool,
system_certs: bool,
allow_insecure_host: Vec<TrustedHost>,
preview: Preview,
read_timeout: Duration,
@@ -200,7 +200,7 @@ impl<'a> BaseClientBuilder<'a> {
Self {
preview,
allow_insecure_host,
native_tls,
system_certs,
retries,
connectivity,
read_timeout,
@@ -251,14 +251,13 @@ impl<'a> BaseClientBuilder<'a> {
}
#[must_use]
pub fn native_tls(mut self, native_tls: bool) -> Self {
self.native_tls = native_tls;
self
pub fn system_certs(&self) -> bool {
self.system_certs
}
#[must_use]
pub fn built_in_root_certs(mut self, built_in_root_certs: bool) -> Self {
self.built_in_root_certs = built_in_root_certs;
pub fn with_system_certs(mut self, system_certs: bool) -> Self {
self.system_certs = system_certs;
self
}
@@ -372,10 +371,6 @@ impl<'a> BaseClientBuilder<'a> {
self.credentials_cache.store_credentials(url, credentials);
}
pub fn is_native_tls(&self) -> bool {
self.native_tls
}
pub fn is_offline(&self) -> bool {
matches!(self.connectivity, Connectivity::Offline)
}
@@ -479,93 +474,15 @@ impl<'a> BaseClientBuilder<'a> {
let _ = write!(user_agent_string, " {output}");
}
// Checks for the presence of `SSL_CERT_FILE`.
// Certificate loading support is delegated to `rustls-native-certs`.
// See https://github.com/rustls/rustls-native-certs/blob/813790a297ad4399efe70a8e5264ca1b420acbec/src/lib.rs#L118-L125
let ssl_cert_file_exists = env::var_os(EnvVars::SSL_CERT_FILE).is_some_and(|path| {
let path = Path::new(&path);
match path.metadata() {
Ok(metadata) if metadata.is_file() => true,
Ok(_) => {
warn_user_once!(
"Ignoring invalid `SSL_CERT_FILE`. Path is not a file: {}.",
path.simplified_display().cyan()
);
false
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
warn_user_once!(
"Ignoring invalid `SSL_CERT_FILE`. Path does not exist: {}.",
path.simplified_display().cyan()
);
false
}
Err(err) => {
warn_user_once!(
"Ignoring invalid `SSL_CERT_FILE`. Path is not accessible: {} ({err}).",
path.simplified_display().cyan()
);
false
}
}
});
// Checks for the presence of `SSL_CERT_DIR`.
// Certificate loading support is delegated to `rustls-native-certs`.
// See https://github.com/rustls/rustls-native-certs/blob/813790a297ad4399efe70a8e5264ca1b420acbec/src/lib.rs#L118-L125
let ssl_cert_dir_exists = env::var_os(EnvVars::SSL_CERT_DIR)
.filter(|v| !v.is_empty())
.is_some_and(|dirs| {
// Parse `SSL_CERT_DIR`, with support for multiple entries using
// a platform-specific delimiter (`:` on Unix, `;` on Windows)
let (existing, missing): (Vec<_>, Vec<_>) =
env::split_paths(&dirs).partition(|p| p.exists());
if existing.is_empty() {
let end_note = if missing.len() == 1 {
"The directory does not exist."
} else {
"The entries do not exist."
};
warn_user_once!(
"Ignoring invalid `SSL_CERT_DIR`. {end_note}: {}.",
missing
.iter()
.map(Simplified::simplified_display)
.join(", ")
.cyan()
);
return false;
}
// Warn on any missing entries
if !missing.is_empty() {
let end_note = if missing.len() == 1 {
"The following directory does not exist:"
} else {
"The following entries do not exist:"
};
warn_user_once!(
"Invalid entries in `SSL_CERT_DIR`. {end_note}: {}.",
missing
.iter()
.map(Simplified::simplified_display)
.join(", ")
.cyan()
);
}
// Proceed while ignoring missing entries
true
});
// Load custom CA certificates from `SSL_CERT_FILE` and `SSL_CERT_DIR`.
let custom_certs = Certificates::from_env().map(|certs| certs.to_reqwest_certs());
// Create a secure client that validates certificates.
let raw_client = self.create_client(
&user_agent_string,
read_timeout,
connect_timeout,
ssl_cert_file_exists,
ssl_cert_dir_exists,
custom_certs.clone(),
Security::Secure,
self.redirect_policy,
);
@@ -575,8 +492,7 @@ impl<'a> BaseClientBuilder<'a> {
&user_agent_string,
read_timeout,
connect_timeout,
ssl_cert_file_exists,
ssl_cert_dir_exists,
custom_certs,
Security::Insecure,
self.redirect_policy,
);
@@ -589,8 +505,7 @@ impl<'a> BaseClientBuilder<'a> {
user_agent: &str,
read_timeout: Duration,
connect_timeout: Duration,
ssl_cert_file_exists: bool,
ssl_cert_dir_exists: bool,
custom_certs: Option<Vec<Certificate>>,
security: Security,
redirect_policy: RedirectPolicy,
) -> Client {
@@ -601,7 +516,6 @@ impl<'a> BaseClientBuilder<'a> {
.pool_max_idle_per_host(20)
.read_timeout(read_timeout)
.connect_timeout(connect_timeout)
.tls_built_in_root_certs(self.built_in_root_certs)
.redirect(redirect_policy.reqwest_policy());
// If necessary, accept invalid certificates.
@@ -610,10 +524,18 @@ impl<'a> BaseClientBuilder<'a> {
Security::Insecure => client_builder.danger_accept_invalid_certs(true),
};
let client_builder = if self.native_tls || ssl_cert_file_exists || ssl_cert_dir_exists {
client_builder.tls_built_in_native_certs(true)
let client_builder = client_builder.tls_backend_rustls();
// Configure the certificate source.
//
// `SSL_CERT_FILE` and `SSL_CERT_DIR` override the default certificate source when they
// contain valid certificates.
let client_builder = if let Some(custom_certs) = custom_certs {
client_builder.tls_certs_only(custom_certs)
} else if self.system_certs {
client_builder
} else {
client_builder.tls_built_in_webpki_certs(true)
client_builder.tls_certs_only(Certificates::webpki_roots().to_reqwest_certs())
};
// Configure mTLS.
+12 -9
View File
@@ -99,14 +99,6 @@ impl<'a> RegistryClientBuilder<'a> {
self
}
#[must_use]
pub fn built_in_root_certs(mut self, built_in_root_certs: bool) -> Self {
self.base_client_builder = self
.base_client_builder
.built_in_root_certs(built_in_root_certs);
self
}
#[must_use]
pub fn cache(mut self, cache: Cache) -> Self {
self.cache = cache;
@@ -1147,7 +1139,18 @@ impl RegistryClient {
if let Some(authorization) = req.headers().get("authorization") {
headers.append("authorization", authorization.clone());
}
// These range requests need the bytes from the wheel archive itself.
// After `reqwest` moved decompression to tower-http[1], this path could receive
// transparently decompressed responses. That breaks the byte offsets used by
// `AsyncHttpRangeReader` and results in us incorrectly trying to double-decompress gzip streams[2].
// We request with `Accept: identity` so that the range reader always sees the compressed wheel bytes.
//
// [1]: https://github.com/seanmonstar/reqwest/pull/2840
// [2]: https://github.com/astral-sh/async_http_range_reader/pull/3#discussion_r2700194798
headers.insert(
reqwest::header::ACCEPT_ENCODING,
reqwest::header::HeaderValue::from_static("identity"),
);
// This response callback is special, we actually make a number of subsequent requests to
// fetch the file from the remote zip.
let read_metadata_range_request = |response: Response| {
+324 -5
View File
@@ -1,17 +1,253 @@
use reqwest::Identity;
use std::ffi::OsStr;
use std::io::Read;
use std::env;
use std::io::{self, Read};
use std::path::{Path, PathBuf};
use itertools::Itertools;
use reqwest::{Certificate, Identity};
use rustls_native_certs::{CertificateResult, load_certs_from_paths};
use rustls_pki_types::CertificateDer;
use tracing::debug;
use uv_fs::Simplified;
use uv_static::EnvVars;
use uv_warnings::warn_user_once;
/// A collection of TLS certificates in DER form.
#[derive(Debug, Clone, Default)]
pub(crate) struct Certificates(Vec<CertificateDer<'static>>);
impl Certificates {
/// Load the bundled Mozilla root certificates.
///
/// We use `webpki-root-certs` (which gives us [`CertificateDer`] values) rather than the more
/// space-efficient `webpki-roots` (pre-parsed [`TrustAnchor`] values) because reqwest's
/// [`ClientBuilder::tls_certs_only`] accepts [`Certificate`] values built from DER bytes. Using
/// `webpki-roots` would require constructing a [`rustls::ClientConfig`] manually and passing it
/// via the semver-unstable [`ClientBuilder::tls_backend_preconfigured`], which also means
/// taking ownership of ALPN, SNI, certificate verification, and mTLS configuration that reqwest
/// otherwise handles for us.
pub(crate) fn webpki_roots() -> Self {
// Each [`CertificateDer`] in [`webpki_root_certs::TLS_SERVER_ROOT_CERTS`] borrows from static
// data, so cloning into the [`Vec`] only copies the fat pointer, not the certificate bytes.
Self(webpki_root_certs::TLS_SERVER_ROOT_CERTS.to_vec())
}
/// Load custom CA certificates from `SSL_CERT_FILE` and `SSL_CERT_DIR` environment variables.
///
/// Returns `None` if neither variable is set, if the referenced files or directories are
/// missing or inaccessible, or if no valid certificates are found (with a warning in each
/// case). Delegates path loading to [`rustls_native_certs::load_certs_from_paths`].
pub(crate) fn from_env() -> Option<Self> {
let mut certs = Self::default();
let mut has_source = false;
if let Some(ssl_cert_file) = env::var_os(EnvVars::SSL_CERT_FILE)
&& let Some(file_certs) = Self::from_ssl_cert_file(&ssl_cert_file)
{
has_source = true;
certs.merge(file_certs);
}
if let Some(ssl_cert_dir) = env::var_os(EnvVars::SSL_CERT_DIR)
&& let Some(dir_certs) = Self::from_ssl_cert_dir(&ssl_cert_dir)
{
has_source = true;
certs.merge(dir_certs);
}
if has_source { Some(certs) } else { None }
}
/// Load certificates from the value of `SSL_CERT_FILE`.
///
/// Returns `None` if the value is empty, the path does not refer to an accessible file,
/// or the file contains no valid certificates.
fn from_ssl_cert_file(ssl_cert_file: &std::ffi::OsStr) -> Option<Self> {
if ssl_cert_file.is_empty() {
return None;
}
let file = PathBuf::from(ssl_cert_file);
match file.metadata() {
Ok(metadata) if metadata.is_file() => {
let result = Self::from_paths(Some(&file), None);
for err in &result.errors {
warn_user_once!(
"Failed to load `SSL_CERT_FILE` ({}): {err}",
file.simplified_display().cyan()
);
}
let certs = Self::from(result);
if certs.0.is_empty() {
warn_user_once!(
"Ignoring `SSL_CERT_FILE`. No certificates found in: {}.",
file.simplified_display().cyan()
);
return None;
}
Some(certs)
}
Ok(_) => {
warn_user_once!(
"Ignoring invalid `SSL_CERT_FILE`. Path is not a file: {}.",
file.simplified_display().cyan()
);
None
}
Err(err) if err.kind() == io::ErrorKind::NotFound => {
warn_user_once!(
"Ignoring invalid `SSL_CERT_FILE`. Path does not exist: {}.",
file.simplified_display().cyan()
);
None
}
Err(err) => {
warn_user_once!(
"Ignoring invalid `SSL_CERT_FILE`. Path is not accessible: {} ({err}).",
file.simplified_display().cyan()
);
None
}
}
}
/// Load certificates from the value of `SSL_CERT_DIR`.
///
/// The value may include multiple entries, separated by a platform-specific delimiter (`:` on
/// Unix, `;` on Windows).
///
/// Returns `None` if the value is empty, no listed directories exist, or no valid
/// certificates are found.
fn from_ssl_cert_dir(ssl_cert_dir: &std::ffi::OsStr) -> Option<Self> {
if ssl_cert_dir.is_empty() {
return None;
}
let (existing, missing): (Vec<_>, Vec<_>) =
env::split_paths(ssl_cert_dir).partition(|path| path.exists());
if existing.is_empty() {
let end_note = if missing.len() == 1 {
"The directory does not exist."
} else {
"The entries do not exist."
};
warn_user_once!(
"Ignoring invalid `SSL_CERT_DIR`. {end_note}: {}.",
missing
.iter()
.map(Simplified::simplified_display)
.join(", ")
.cyan()
);
return None;
}
if !missing.is_empty() {
let end_note = if missing.len() == 1 {
"The following directory does not exist:"
} else {
"The following entries do not exist:"
};
warn_user_once!(
"Invalid entries in `SSL_CERT_DIR`. {end_note}: {}.",
missing
.iter()
.map(Simplified::simplified_display)
.join(", ")
.cyan()
);
}
let mut certs = Self::default();
for dir in &existing {
let result = Self::from_paths(None, Some(dir));
for err in &result.errors {
warn_user_once!(
"Failed to load `SSL_CERT_DIR` ({}): {err}",
dir.simplified_display().cyan()
);
}
certs.merge(Self::from(result));
}
if certs.0.is_empty() {
warn_user_once!(
"Ignoring `SSL_CERT_DIR`. No certificates found in: {}.",
existing
.iter()
.map(Simplified::simplified_display)
.join(", ")
.cyan()
);
return None;
}
Some(certs)
}
/// Load certificates from explicit file and directory paths.
fn from_paths(file: Option<&Path>, dir: Option<&Path>) -> CertificateResult {
load_certs_from_paths(file, dir)
}
/// Remove duplicate certificates, sorting by DER bytes.
fn dedup(&mut self) {
self.0
.sort_unstable_by(|left, right| left.as_ref().cmp(right.as_ref()));
self.0.dedup();
}
/// Merge another set of certificates into this one.
///
/// After merging, duplicates are removed.
fn merge(&mut self, other: Self) {
self.0.extend(other.0);
self.dedup();
}
/// Convert certificates to reqwest [`Certificate`] objects.
pub(crate) fn to_reqwest_certs(&self) -> Vec<Certificate> {
self.0
.iter()
// `Certificate::from_der` returns a `Result` for backend compatibility, but these
// certificates come from `rustls-native-certs` and are already validated DER certs.
// In our rustls-based client configuration this conversion is expected to succeed.
.filter_map(|cert| match Certificate::from_der(cert) {
Ok(certificate) => Some(certificate),
Err(err) => {
debug!("Failed to convert DER certificate to reqwest certificate: {err}");
None
}
})
.collect()
}
/// Iterate over raw DER certificates.
#[cfg(test)]
fn iter(&self) -> impl Iterator<Item = &CertificateDer<'static>> {
self.0.iter()
}
}
impl From<CertificateResult> for Certificates {
fn from(result: CertificateResult) -> Self {
Self(result.certs)
}
}
#[derive(thiserror::Error, Debug)]
pub(crate) enum CertificateError {
#[error(transparent)]
Io(#[from] std::io::Error),
Io(#[from] io::Error),
#[error(transparent)]
Reqwest(reqwest::Error),
}
/// Return the `Identity` from the provided file.
pub(crate) fn read_identity(ssl_client_cert: &OsStr) -> Result<Identity, CertificateError> {
pub(crate) fn read_identity(
ssl_client_cert: &std::ffi::OsStr,
) -> Result<Identity, CertificateError> {
let mut buf = Vec::new();
fs_err::File::open(ssl_client_cert)?.read_to_end(&mut buf)?;
Identity::from_pem(&buf).map_err(|tls_err| {
@@ -19,3 +255,86 @@ pub(crate) fn read_identity(ssl_client_cert: &OsStr) -> Result<Identity, Certifi
CertificateError::Reqwest(tls_err)
})
}
#[cfg(test)]
mod tests {
use std::ffi::OsString;
use super::*;
fn generate_cert_pem() -> String {
let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_string()]).unwrap();
cert.cert.pem()
}
#[test]
fn test_from_ssl_cert_file_nonexistent_returns_none() {
let dir = tempfile::tempdir().unwrap();
let missing_file = dir.path().join("missing.pem");
let certs = Certificates::from_ssl_cert_file(missing_file.as_os_str());
assert!(certs.is_none());
}
#[test]
fn test_from_ssl_cert_file_empty_value_returns_none() {
let certs = Certificates::from_ssl_cert_file(OsString::new().as_os_str());
assert!(certs.is_none());
}
#[test]
fn test_from_ssl_cert_file_no_valid_certs_returns_none() {
let dir = tempfile::tempdir().unwrap();
let cert_path = dir.path().join("empty.pem");
fs_err::write(&cert_path, "not a certificate").unwrap();
let certs = Certificates::from_ssl_cert_file(cert_path.as_os_str());
assert!(certs.is_none());
}
#[test]
fn test_from_ssl_cert_dir_empty_value_returns_none() {
let certs = Certificates::from_ssl_cert_dir(OsString::new().as_os_str());
assert!(certs.is_none());
}
#[test]
fn test_from_ssl_cert_dir_nonexistent_returns_none() {
let dir = tempfile::tempdir().unwrap();
let missing_dir = dir.path().join("missing-dir");
let cert_dirs = std::env::join_paths([&missing_dir]).unwrap();
let certs = Certificates::from_ssl_cert_dir(cert_dirs.as_os_str());
assert!(certs.is_none());
}
#[test]
fn test_from_ssl_cert_dir_empty_existing_returns_none() {
let dir = tempfile::tempdir().unwrap();
let cert_dirs = std::env::join_paths([dir.path()]).unwrap();
let certs = Certificates::from_ssl_cert_dir(cert_dirs.as_os_str());
assert!(certs.is_none());
}
#[test]
fn test_merge_deduplicates() {
let dir = tempfile::tempdir().unwrap();
let cert_path = dir.path().join("cert.pem");
fs_err::write(&cert_path, generate_cert_pem()).unwrap();
let first = Certificates::from(Certificates::from_paths(Some(&cert_path), None));
let second = Certificates::from(Certificates::from_paths(Some(&cert_path), None));
let mut merged = first;
merged.merge(second);
assert_eq!(merged.iter().count(), 1);
}
#[test]
fn test_webpki_roots_not_empty() {
let certs = Certificates::webpki_roots();
assert!(certs.iter().count() > 0);
}
}
-35
View File
@@ -46,41 +46,6 @@ pub(crate) fn test_cert_dir() -> PathBuf {
.join("certs")
}
/// Generates a self-signed server certificate for `uv-test-server`, `localhost` and `127.0.0.1`.
/// This certificate is standalone and not issued by a self-signed Root CA.
///
/// Use sparingly as generation of certs is a slow operation.
pub(crate) fn generate_self_signed_certs() -> Result<SelfSigned> {
let mut params = CertificateParams::default();
params.is_ca = IsCa::NoCa;
params.not_before = date_time_ymd(1975, 1, 1);
params.not_after = date_time_ymd(4096, 1, 1);
params.key_usages.push(KeyUsagePurpose::DigitalSignature);
params.key_usages.push(KeyUsagePurpose::KeyEncipherment);
params
.extended_key_usages
.push(ExtendedKeyUsagePurpose::ServerAuth);
params
.distinguished_name
.push(DnType::OrganizationName, "Astral Software Inc.");
params
.distinguished_name
.push(DnType::CommonName, "uv-test-server");
params
.subject_alt_names
.push(SanType::DnsName("uv-test-server".try_into()?));
params
.subject_alt_names
.push(SanType::DnsName("localhost".try_into()?));
params
.subject_alt_names
.push(SanType::IpAddress("127.0.0.1".parse()?));
let private = KeyPair::generate()?;
let public = params.self_signed(&private)?;
Ok(SelfSigned { public, private })
}
/// Generates a self-signed root CA, server certificate, and client certificate.
/// There are no intermediate certs generated as part of this function.
/// The server certificate is for `uv-test-server`, `localhost` and `127.0.0.1` issued by this CA.
+636 -312
View File
@@ -1,7 +1,11 @@
use std::io::Write;
use std::net::SocketAddr;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use anyhow::Result;
use rustls::AlertDescription;
use temp_env::async_with_vars;
use tempfile::{NamedTempFile, TempDir};
use url::Url;
use uv_cache::Cache;
@@ -11,335 +15,655 @@ use uv_redacted::DisplaySafeUrl;
use uv_static::EnvVars;
use crate::http_util::{
generate_self_signed_certs, generate_self_signed_certs_with_ca,
start_https_mtls_user_agent_server, start_https_user_agent_server, test_cert_dir,
SelfSigned, generate_self_signed_certs_with_ca, start_https_mtls_user_agent_server,
start_https_user_agent_server, test_cert_dir,
};
// SAFETY: This test is meant to run with single thread configuration
#[tokio::test]
#[allow(unsafe_code)]
async fn ssl_env_vars() -> Result<()> {
// Ensure our environment is not polluted with anything that may affect `rustls-native-certs`
unsafe {
std::env::remove_var(EnvVars::UV_NATIVE_TLS);
std::env::remove_var(EnvVars::SSL_CERT_FILE);
std::env::remove_var(EnvVars::SSL_CERT_DIR);
std::env::remove_var(EnvVars::SSL_CLIENT_CERT);
/// A self-signed CA together with a server certificate and a client certificate
/// it has issued. Every [`TestCertificate`] is an independent trust domain.
struct TestCertificate {
_temp_dir: TempDir,
/// The CA certificate (root of trust).
ca: SelfSigned,
/// A server certificate signed by [`ca`](Self::ca).
server: SelfSigned,
/// Path to the CA public cert PEM — the file you put in `SSL_CERT_FILE` to
/// trust this certificate family.
trust_path: PathBuf,
/// Path to the combined client cert + key PEM — the file you put in
/// `SSL_CLIENT_CERT` for mTLS.
client_cert_path: PathBuf,
}
impl TestCertificate {
/// Generate a fresh CA, server cert, and client cert, persisting the
/// relevant PEM files to a temporary directory.
fn new() -> Result<Self> {
let cert_dir = test_cert_dir();
fs_err::create_dir_all(&cert_dir)?;
let temp_dir = TempDir::new_in(cert_dir)?;
let (ca, server, client) = generate_self_signed_certs_with_ca()?;
let trust_path = temp_dir.path().join("ca.pem");
fs_err::write(&trust_path, ca.public.pem())?;
let client_cert_path = temp_dir.path().join("client.pem");
fs_err::write(
&client_cert_path,
format!(
"{}\n{}",
client.public.pem(),
client.private.serialize_pem()
),
)?;
Ok(Self {
_temp_dir: temp_dir,
ca,
server,
trust_path,
client_cert_path,
})
}
// Create temporary cert dirs
let cert_dir = test_cert_dir();
fs_err::create_dir_all(&cert_dir).expect("Failed to create test cert bucket");
let cert_dir =
tempfile::TempDir::new_in(cert_dir).expect("Failed to create test cert directory");
let does_not_exist_cert_dir = cert_dir.path().join("does_not_exist");
// Generate self-signed standalone cert
let standalone_server_cert = generate_self_signed_certs()?;
let standalone_public_pem_path = cert_dir.path().join("standalone_public.pem");
let standalone_private_pem_path = cert_dir.path().join("standalone_private.pem");
// Generate self-signed CA, server, and client certs
let (ca_cert, server_cert, client_cert) = generate_self_signed_certs_with_ca()?;
let ca_public_pem_path = cert_dir.path().join("ca_public.pem");
let ca_private_pem_path = cert_dir.path().join("ca_private.pem");
let server_public_pem_path = cert_dir.path().join("server_public.pem");
let server_private_pem_path = cert_dir.path().join("server_private.pem");
let client_combined_pem_path = cert_dir.path().join("client_combined.pem");
// Persist the certs in PKCS8 format as the env vars expect a path on disk
fs_err::write(
standalone_public_pem_path.as_path(),
standalone_server_cert.public.pem(),
)?;
fs_err::write(
standalone_private_pem_path.as_path(),
standalone_server_cert.private.serialize_pem(),
)?;
fs_err::write(ca_public_pem_path.as_path(), ca_cert.public.pem())?;
fs_err::write(
ca_private_pem_path.as_path(),
ca_cert.private.serialize_pem(),
)?;
fs_err::write(server_public_pem_path.as_path(), server_cert.public.pem())?;
fs_err::write(
server_private_pem_path.as_path(),
server_cert.private.serialize_pem(),
)?;
fs_err::write(
client_combined_pem_path.as_path(),
// SSL_CLIENT_CERT expects a "combined" cert with the public and private key.
format!(
/// Write a CA + server PEM bundle to a [`NamedTempFile`].
fn write_bundle_pem(&self) -> NamedTempFile {
let mut file = NamedTempFile::new().unwrap();
write!(
file,
"{}\n{}",
client_cert.public.pem(),
client_cert.private.serialize_pem()
),
)?;
// ** Set SSL_CERT_FILE to non-existent location
// ** Then verify our request fails to establish a connection
unsafe {
std::env::set_var(EnvVars::SSL_CERT_FILE, does_not_exist_cert_dir.as_os_str());
}
let (server_task, addr) = start_https_user_agent_server(&standalone_server_cert).await?;
let url = DisplaySafeUrl::from_str(&format!("https://{addr}"))?;
let cache = Cache::temp()?.init().await?;
let client =
RegistryClientBuilder::new(BaseClientBuilder::default().no_retry_delay(true), cache)
.build();
let res = client
.cached_client()
.uncached()
.for_host(&url)
.get(Url::from(url))
.send()
.await;
unsafe {
std::env::remove_var(EnvVars::SSL_CERT_FILE);
self.ca.public.pem(),
self.server.public.pem()
)
.unwrap();
file
}
// Validate the client error
let Some(reqwest_middleware::Error::Middleware(middleware_error)) = res.err() else {
panic!("expected middleware error");
};
let reqwest_error = middleware_error
.chain()
.find_map(|err| {
err.downcast_ref::<reqwest_middleware::Error>().map(|err| {
if let reqwest_middleware::Error::Reqwest(inner) = err {
inner
} else {
panic!("expected reqwest error")
}
})
/// Write the CA public PEM into a fresh temporary directory, returning it.
fn ca_pem_dir(&self) -> TempDir {
self.ca_pem_dir_as("ca.pem")
}
/// Write the CA public PEM with a custom filename into a fresh temporary
/// directory, returning it.
fn ca_pem_dir_as(&self, filename: &str) -> TempDir {
let dir = TempDir::new().unwrap();
fs_err::write(dir.path().join(filename), self.ca.public.pem()).unwrap();
dir
}
/// Write a CA + server PEM bundle into a fresh temporary directory,
/// returning it.
fn bundle_pem_dir(&self) -> TempDir {
let dir = TempDir::new().unwrap();
fs_err::write(
dir.path().join("bundle.pem"),
format!("{}\n{}", self.ca.public.pem(), self.server.public.pem()),
)
.unwrap();
dir
}
}
/// Client-side configuration builder. Collects environment variable overrides
/// and provides terminal assertion methods that start a server, send a request,
/// and verify the outcome.
struct TestClient {
overrides: Vec<(&'static str, String)>,
system_certs: bool,
}
/// Create a [`TestClient`] with no environment overrides.
fn client() -> TestClient {
TestClient {
overrides: Vec::new(),
system_certs: false,
}
}
impl TestClient {
/// Enable or disable system certificate loading.
fn system_certs(mut self, enabled: bool) -> Self {
self.system_certs = enabled;
self
}
/// Set `SSL_CERT_FILE` to `path`.
fn ssl_cert_file(self, path: &Path) -> Self {
self.with_env(EnvVars::SSL_CERT_FILE, path.to_str().unwrap())
}
/// Set `SSL_CERT_DIR` to a single directory.
fn ssl_cert_dir(self, path: &Path) -> Self {
self.with_env(EnvVars::SSL_CERT_DIR, path.to_str().unwrap())
}
/// Set `SSL_CERT_DIR` to multiple directories joined with the
/// platform-specific path separator.
fn ssl_cert_dirs(self, paths: &[&Path]) -> Self {
let joined = std::env::join_paths(paths).unwrap();
self.with_env(EnvVars::SSL_CERT_DIR, joined.to_str().unwrap())
}
/// Set `SSL_CLIENT_CERT` to `path`.
fn ssl_client_cert(self, path: &Path) -> Self {
self.with_env(EnvVars::SSL_CLIENT_CERT, path.to_str().unwrap())
}
/// Set an arbitrary environment variable.
fn with_env(mut self, key: &'static str, value: &str) -> Self {
self.overrides.push((key, value.to_string()));
self
}
/// Assert that an HTTPS connection to `cert`'s server succeeds.
async fn expect_https_connect_succeeds(&self, cert: &TestCertificate) {
self.run_https(cert, |response, server_task| async move {
assert!(
response.is_ok(),
"expected successful response, got: {:?}",
response.err()
);
server_task.await.unwrap().unwrap();
})
.expect("expected reqwest error");
assert!(reqwest_error.is_connect());
// Validate the server error
let server_res = server_task.await?;
let expected_err = if let Err(anyhow_err) = server_res
&& let Some(io_err) = anyhow_err.downcast_ref::<std::io::Error>()
&& let Some(wrapped_err) = io_err.get_ref()
&& let Some(tls_err) = wrapped_err.downcast_ref::<rustls::Error>()
&& matches!(
tls_err,
rustls::Error::AlertReceived(AlertDescription::UnknownCA)
) {
true
} else {
false
};
assert!(expected_err);
// ** Set SSL_CERT_FILE to our public certificate
// ** Then verify our request successfully establishes a connection
unsafe {
std::env::set_var(
EnvVars::SSL_CERT_FILE,
standalone_public_pem_path.as_os_str(),
);
}
let (server_task, addr) = start_https_user_agent_server(&standalone_server_cert).await?;
let url = DisplaySafeUrl::from_str(&format!("https://{addr}"))?;
let cache = Cache::temp()?.init().await?;
let client =
RegistryClientBuilder::new(BaseClientBuilder::default().no_retry_delay(true), cache)
.build();
let res = client
.cached_client()
.uncached()
.for_host(&url)
.get(Url::from(url))
.send()
.await;
assert!(res.is_ok());
let _ = server_task.await?; // wait for server shutdown
unsafe {
std::env::remove_var(EnvVars::SSL_CERT_FILE);
}
// ** Set SSL_CERT_DIR to our cert dir as well as some other dir that does not exist
// ** Then verify our request still successfully establishes a connection
unsafe {
std::env::set_var(
EnvVars::SSL_CERT_DIR,
std::env::join_paths(vec![
cert_dir.path().as_os_str(),
does_not_exist_cert_dir.as_os_str(),
])?,
);
}
let (server_task, addr) = start_https_user_agent_server(&standalone_server_cert).await?;
let url = DisplaySafeUrl::from_str(&format!("https://{addr}"))?;
let cache = Cache::temp()?.init().await?;
let client =
RegistryClientBuilder::new(BaseClientBuilder::default().no_retry_delay(true), cache)
.build();
let res = client
.cached_client()
.uncached()
.for_host(&url)
.get(Url::from(url))
.send()
.await;
assert!(res.is_ok());
let _ = server_task.await?; // wait for server shutdown
unsafe {
std::env::remove_var(EnvVars::SSL_CERT_DIR);
}
// ** Set SSL_CERT_DIR to only the dir that does not exist
// ** Then verify our request fails to establish a connection
unsafe {
std::env::set_var(EnvVars::SSL_CERT_DIR, does_not_exist_cert_dir.as_os_str());
}
let (server_task, addr) = start_https_user_agent_server(&standalone_server_cert).await?;
let url = DisplaySafeUrl::from_str(&format!("https://{addr}"))?;
let cache = Cache::temp()?.init().await?;
let client =
RegistryClientBuilder::new(BaseClientBuilder::default().no_retry_delay(true), cache)
.build();
let res = client
.cached_client()
.uncached()
.for_host(&url)
.get(Url::from(url))
.send()
.await;
unsafe {
std::env::remove_var(EnvVars::SSL_CERT_DIR);
}
// Validate the client error
let Some(reqwest_middleware::Error::Middleware(middleware_error)) = res.err() else {
panic!("expected middleware error");
};
let reqwest_error = middleware_error
.chain()
.find_map(|err| {
err.downcast_ref::<reqwest_middleware::Error>().map(|err| {
if let reqwest_middleware::Error::Reqwest(inner) = err {
inner
} else {
panic!("expected reqwest error")
}
})
/// Assert that an HTTPS connection to `cert`'s server fails with a TLS
/// error on the client side.
async fn expect_https_connect_fails(&self, cert: &TestCertificate) {
self.run_https(cert, |response, server_task| async move {
assert_connection_error(&response);
// Server may or may not have errored — just ensure no panic.
let _ = server_task.await;
})
.expect("expected reqwest error");
assert!(reqwest_error.is_connect());
// Validate the server error
let server_res = server_task.await?;
let expected_err = if let Err(anyhow_err) = server_res
&& let Some(io_err) = anyhow_err.downcast_ref::<std::io::Error>()
&& let Some(wrapped_err) = io_err.get_ref()
&& let Some(tls_err) = wrapped_err.downcast_ref::<rustls::Error>()
&& matches!(
tls_err,
rustls::Error::AlertReceived(AlertDescription::UnknownCA)
) {
true
} else {
false
};
assert!(expected_err);
// *** mTLS Tests
// ** Set SSL_CERT_FILE to our CA and SSL_CLIENT_CERT to our client cert
// ** Then verify our request still successfully establishes a connection
// We need to set SSL_CERT_FILE or SSL_CERT_DIR to our CA as we need to tell
// our HTTP client that we trust certificates issued by our self-signed CA.
// This inherently also tests that our server cert is also validated as part
// of the certificate path validation algorithm.
unsafe {
std::env::set_var(EnvVars::SSL_CERT_FILE, ca_public_pem_path.as_os_str());
std::env::set_var(
EnvVars::SSL_CLIENT_CERT,
client_combined_pem_path.as_os_str(),
);
}
let (server_task, addr) = start_https_mtls_user_agent_server(&ca_cert, &server_cert).await?;
let url = DisplaySafeUrl::from_str(&format!("https://{addr}"))?;
let cache = Cache::temp()?.init().await?;
let client =
RegistryClientBuilder::new(BaseClientBuilder::default().no_retry_delay(true), cache)
.build();
let res = client
.cached_client()
.uncached()
.for_host(&url)
.get(Url::from(url))
.send()
.await;
assert!(res.is_ok());
let _ = server_task.await?; // wait for server shutdown
unsafe {
std::env::remove_var(EnvVars::SSL_CERT_FILE);
std::env::remove_var(EnvVars::SSL_CLIENT_CERT);
}
// ** Set SSL_CERT_FILE to our CA and unset SSL_CLIENT_CERT
// ** Then verify our request fails to establish a connection
unsafe {
std::env::set_var(EnvVars::SSL_CERT_FILE, ca_public_pem_path.as_os_str());
}
let (server_task, addr) = start_https_mtls_user_agent_server(&ca_cert, &server_cert).await?;
let url = DisplaySafeUrl::from_str(&format!("https://{addr}"))?;
let cache = Cache::temp()?.init().await?;
let client =
RegistryClientBuilder::new(BaseClientBuilder::default().no_retry_delay(true), cache)
.build();
let res = client
.cached_client()
.uncached()
.for_host(&url)
.get(Url::from(url))
.send()
.await;
unsafe {
std::env::remove_var(EnvVars::SSL_CERT_FILE);
}
// Validate the client error
let Some(reqwest_middleware::Error::Middleware(middleware_error)) = res.err() else {
panic!("expected middleware error");
};
let reqwest_error = middleware_error
.chain()
.find_map(|err| {
err.downcast_ref::<reqwest_middleware::Error>().map(|err| {
if let reqwest_middleware::Error::Reqwest(inner) = err {
inner
} else {
panic!("expected reqwest error")
}
})
/// Assert that an mTLS connection to `cert`'s server succeeds.
async fn expect_mtls_connect_succeeds(&self, cert: &TestCertificate) {
self.run_mtls(cert, |response, server_task| async move {
assert!(
response.is_ok(),
"expected successful response, got: {:?}",
response.err()
);
server_task.await.unwrap().unwrap();
})
.expect("expected reqwest error");
assert!(reqwest_error.is_connect());
.await;
}
// Validate the server error
let server_res = server_task.await?;
let expected_err = if let Err(anyhow_err) = server_res
&& let Some(io_err) = anyhow_err.downcast_ref::<std::io::Error>()
&& let Some(wrapped_err) = io_err.get_ref()
&& let Some(tls_err) = wrapped_err.downcast_ref::<rustls::Error>()
&& matches!(tls_err, rustls::Error::NoCertificatesPresented)
/// Assert that an mTLS connection to `cert`'s server fails and the server
/// reports a specific TLS error.
async fn expect_mtls_connect_fails_with_server_tls_error<F>(
&self,
cert: &TestCertificate,
assert_tls_error: F,
) where
F: FnOnce(&rustls::Error),
{
true
} else {
false
};
assert!(expected_err);
self.run_mtls(cert, |response, server_task| async move {
assert_connection_error(&response);
// Fin.
let server_res = server_task.await.expect("server task panicked");
let Err(anyhow_err) = server_res else {
panic!("expected server error, got Ok");
};
let Some(io_err) = anyhow_err.downcast_ref::<std::io::Error>() else {
panic!("expected io::Error, got: {anyhow_err}");
};
let Some(wrapped_err) = io_err.get_ref() else {
panic!("expected wrapped error in io::Error, got: {io_err}");
};
let Some(tls_err) = wrapped_err.downcast_ref::<rustls::Error>() else {
panic!("expected rustls::Error, got: {wrapped_err}");
};
assert_tls_error(tls_err);
})
.await;
}
/// Assert that an mTLS connection to `cert`'s server fails because no
/// valid client certificate was presented.
async fn expect_mtls_connect_fails(&self, cert: &TestCertificate) {
self.expect_mtls_connect_fails_with_server_tls_error(cert, |tls_err| {
assert!(
matches!(tls_err, rustls::Error::NoCertificatesPresented),
"expected NoCertificatesPresented, got: {tls_err}"
);
})
.await;
}
/// Build the full environment variable list: clear all SSL-related
/// variables, then apply the accumulated overrides.
fn ssl_vars(&self) -> Vec<(&'static str, Option<&str>)> {
let mut vars: Vec<(&'static str, Option<&str>)> = vec![
(EnvVars::UV_NATIVE_TLS, None),
(EnvVars::UV_SYSTEM_CERTS, None),
(EnvVars::SSL_CERT_FILE, None),
(EnvVars::SSL_CERT_DIR, None),
(EnvVars::SSL_CLIENT_CERT, None),
];
vars.extend(self.overrides.iter().map(|(k, v)| (*k, Some(v.as_str()))));
vars
}
/// Assert that an HTTPS connection to a public host succeeds.
#[cfg(feature = "test-pypi")]
async fn expect_https_connect_succeeds_for_host(&self, host: &str) {
let url = DisplaySafeUrl::from_str(&format!("https://{host}/")).unwrap();
let vars = self.ssl_vars();
let system_certs = self.system_certs;
async_with_vars(vars, async {
let response = send_request_to(&url, system_certs).await;
assert!(
response.is_ok(),
"expected successful response to {host}, got: {:?}",
response.err()
);
})
.await;
}
/// Assert that an HTTPS connection to a public host fails with a TLS
/// error on the client side.
#[cfg(feature = "test-pypi")]
async fn expect_https_connect_fails_for_host(&self, host: &str) {
let url = DisplaySafeUrl::from_str(&format!("https://{host}/")).unwrap();
let vars = self.ssl_vars();
let system_certs = self.system_certs;
async_with_vars(vars, async {
let response = send_request_to(&url, system_certs).await;
assert_connection_error(&response);
})
.await;
}
/// Start an HTTPS server, send a request inside `async_with_vars`, and
/// hand the response + server task to `check`.
async fn run_https<F, Fut>(&self, cert: &TestCertificate, check: F)
where
F: FnOnce(
Result<reqwest::Response, reqwest_middleware::Error>,
tokio::task::JoinHandle<Result<()>>,
) -> Fut,
Fut: std::future::Future<Output = ()>,
{
let vars = self.ssl_vars();
let system_certs = self.system_certs;
async_with_vars(vars, async {
let (server_task, addr) = start_https_user_agent_server(&cert.server).await.unwrap();
let response = send_request(addr, system_certs).await;
check(response, server_task).await;
})
.await;
}
/// Start an mTLS server, send a request inside `async_with_vars`, and
/// hand the response + server task to `check`.
async fn run_mtls<F, Fut>(&self, cert: &TestCertificate, check: F)
where
F: FnOnce(
Result<reqwest::Response, reqwest_middleware::Error>,
tokio::task::JoinHandle<Result<()>>,
) -> Fut,
Fut: std::future::Future<Output = ()>,
{
let vars = self.ssl_vars();
let system_certs = self.system_certs;
async_with_vars(vars, async {
let (server_task, addr) = start_https_mtls_user_agent_server(&cert.ca, &cert.server)
.await
.unwrap();
let response = send_request(addr, system_certs).await;
check(response, server_task).await;
})
.await;
}
}
/// Send a GET request to the given server address using a fresh registry client.
async fn send_request(
addr: SocketAddr,
system_certs: bool,
) -> Result<reqwest::Response, reqwest_middleware::Error> {
let url = DisplaySafeUrl::from_str(&format!("https://{addr}")).unwrap();
send_request_to(&url, system_certs).await
}
/// Send a GET request to an arbitrary URL using a fresh registry client.
async fn send_request_to(
url: &DisplaySafeUrl,
system_certs: bool,
) -> Result<reqwest::Response, reqwest_middleware::Error> {
let cache = Cache::temp().unwrap().init().await.unwrap();
let base = BaseClientBuilder::default()
.no_retry_delay(true)
.with_system_certs(system_certs);
let client = RegistryClientBuilder::new(base, cache).build();
client
.cached_client()
.uncached()
.for_host(url)
.get(Url::from(url.clone()))
.send()
.await
}
/// Assert that a request result is a TLS connection error.
fn assert_connection_error(res: &Result<reqwest::Response, reqwest_middleware::Error>) {
let Some(reqwest_middleware::Error::Middleware(middleware_error)) = res.as_ref().err() else {
panic!("expected middleware error, got: {res:?}");
};
let reqwest_error = middleware_error
.chain()
.find_map(|err| {
err.downcast_ref::<reqwest_middleware::Error>().map(|err| {
if let reqwest_middleware::Error::Reqwest(inner) = err {
inner
} else {
panic!("expected reqwest error, got: {err}")
}
})
})
.expect("expected reqwest error");
assert!(reqwest_error.is_connect());
}
/// A self-signed server certificate is rejected when no custom certs are
/// configured — the bundled webpki roots don't include our test CA.
#[tokio::test]
async fn test_no_custom_certs_rejects_self_signed() -> Result<()> {
let cert = TestCertificate::new()?;
client().expect_https_connect_fails(&cert).await;
Ok(())
}
/// Trusting cert A does not let you connect to a server presenting cert B.
#[tokio::test]
async fn test_ssl_cert_file_wrong_cert_rejected() -> Result<()> {
let cert_a = TestCertificate::new()?;
let cert_b = TestCertificate::new()?;
client()
.ssl_cert_file(&cert_a.trust_path)
.expect_https_connect_fails(&cert_b)
.await;
Ok(())
}
/// A nonexistent `SSL_CERT_FILE` is ignored; the client falls back to webpki
/// roots which don't include our test CA.
#[tokio::test]
async fn test_ssl_cert_file_nonexistent_falls_back() -> Result<()> {
let cert = TestCertificate::new()?;
let dir = TempDir::new()?;
let missing = dir.path().join("missing.pem");
client()
.ssl_cert_file(&missing)
.expect_https_connect_fails(&cert)
.await;
Ok(())
}
/// A nonexistent `SSL_CERT_DIR` is ignored; the client falls back to webpki
/// roots which don't include our test CA.
#[tokio::test]
async fn test_ssl_cert_dir_nonexistent_falls_back() -> Result<()> {
let cert = TestCertificate::new()?;
let dir = TempDir::new()?;
let missing = dir.path().join("missing-certs");
client()
.ssl_cert_dir(&missing)
.expect_https_connect_fails(&cert)
.await;
Ok(())
}
/// A valid `SSL_CERT_FILE` pointing to the server's CA cert is trusted.
#[tokio::test]
async fn test_ssl_cert_file_valid() -> Result<()> {
let cert = TestCertificate::new()?;
client()
.ssl_cert_file(&cert.trust_path)
.expect_https_connect_succeeds(&cert)
.await;
Ok(())
}
/// A PEM bundle containing multiple certificates in `SSL_CERT_FILE` is loaded.
#[tokio::test]
async fn test_ssl_cert_file_bundle() -> Result<()> {
let cert = TestCertificate::new()?;
let bundle = cert.write_bundle_pem();
client()
.ssl_cert_file(bundle.path())
.expect_https_connect_succeeds(&cert)
.await;
Ok(())
}
/// Certificates from both `SSL_CERT_FILE` and `SSL_CERT_DIR` are trusted.
#[tokio::test]
async fn test_ssl_cert_file_and_dir_combined() -> Result<()> {
let cert_a = TestCertificate::new()?;
let cert_b = TestCertificate::new()?;
let dir = cert_b.ca_pem_dir();
let c = client()
.ssl_cert_file(&cert_a.trust_path)
.ssl_cert_dir(dir.path());
c.expect_https_connect_succeeds(&cert_a).await;
c.expect_https_connect_succeeds(&cert_b).await;
Ok(())
}
/// PEM bundles inside `SSL_CERT_DIR` are loaded correctly.
#[tokio::test]
async fn test_ssl_cert_dir_bundle_files() -> Result<()> {
let cert = TestCertificate::new()?;
let dir = cert.bundle_pem_dir();
client()
.ssl_cert_dir(dir.path())
.expect_https_connect_succeeds(&cert)
.await;
Ok(())
}
/// OpenSSL hash-based filenames in `SSL_CERT_DIR` are loaded correctly.
///
/// The filename `5d30f3c5.3` is not the actual OpenSSL hash of the CA cert —
/// it's an arbitrary name matching the `[hex].[digit]` pattern to verify that
/// such files are loaded from the directory.
#[tokio::test]
async fn test_ssl_cert_dir_hash_named_files() -> Result<()> {
let cert = TestCertificate::new()?;
let dir = cert.ca_pem_dir_as("5d30f3c5.3");
client()
.ssl_cert_dir(dir.path())
.expect_https_connect_succeeds(&cert)
.await;
Ok(())
}
/// `SSL_CERT_DIR` supports multiple platform-separated directories. Certs are
/// split across two directories; each only has one cert, but both are trusted.
#[tokio::test]
async fn test_ssl_cert_dir_multiple_directories() -> Result<()> {
let cert_a = TestCertificate::new()?;
let cert_b = TestCertificate::new()?;
let dir_a = cert_a.ca_pem_dir();
let dir_b = cert_b.ca_pem_dir();
let c = client().ssl_cert_dirs(&[dir_a.path(), dir_b.path()]);
c.expect_https_connect_succeeds(&cert_a).await;
c.expect_https_connect_succeeds(&cert_b).await;
Ok(())
}
/// Missing entries in `SSL_CERT_DIR` do not prevent valid directories from
/// being loaded.
#[tokio::test]
async fn test_ssl_cert_dir_multiple_directories_with_missing_entry() -> Result<()> {
let cert = TestCertificate::new()?;
let dir = cert.ca_pem_dir();
let scratch = TempDir::new()?;
let missing = scratch.path().join("missing-certs");
client()
.ssl_cert_dirs(&[&missing, dir.path()])
.expect_https_connect_succeeds(&cert)
.await;
Ok(())
}
/// `SSL_CLIENT_CERT` with invalid content is ignored and the mTLS server
/// rejects the connection.
#[tokio::test]
async fn test_mtls_with_invalid_client_cert() -> Result<()> {
let cert = TestCertificate::new()?;
let mut invalid = NamedTempFile::new()?;
write!(invalid, "not a valid certificate or key")?;
client()
.ssl_cert_file(&cert.trust_path)
.ssl_client_cert(invalid.path())
.expect_mtls_connect_fails(&cert)
.await;
Ok(())
}
/// mTLS succeeds when `SSL_CLIENT_CERT` contains a valid client certificate
/// and key.
#[tokio::test]
async fn test_mtls_with_client_cert() -> Result<()> {
let cert = TestCertificate::new()?;
client()
.ssl_cert_file(&cert.trust_path)
.ssl_client_cert(&cert.client_cert_path)
.expect_mtls_connect_succeeds(&cert)
.await;
Ok(())
}
/// mTLS rejects a syntactically valid client certificate from the wrong trust
/// domain.
#[tokio::test]
async fn test_mtls_with_wrong_client_cert() -> Result<()> {
let server_cert = TestCertificate::new()?;
let other_cert = TestCertificate::new()?;
client()
.ssl_cert_file(&server_cert.trust_path)
.ssl_client_cert(&other_cert.client_cert_path)
.expect_mtls_connect_fails_with_server_tls_error(&server_cert, |tls_err| {
assert!(
matches!(
tls_err,
rustls::Error::InvalidCertificate(
rustls::CertificateError::BadSignature
| rustls::CertificateError::UnknownIssuer
)
),
"expected InvalidCertificate(BadSignature | UnknownIssuer), got: {tls_err}"
);
})
.await;
Ok(())
}
/// mTLS rejects connections when no client certificate is presented.
#[tokio::test]
async fn test_mtls_without_client_cert() -> Result<()> {
let cert = TestCertificate::new()?;
client()
.ssl_cert_file(&cert.trust_path)
.expect_mtls_connect_fails(&cert)
.await;
Ok(())
}
/// When `system_certs` is enabled, `SSL_CERT_FILE` still overrides the
/// certificate source — a valid cert connects successfully.
#[tokio::test]
async fn test_system_certs_with_ssl_cert_file_valid() -> Result<()> {
let cert = TestCertificate::new()?;
client()
.system_certs(true)
.ssl_cert_file(&cert.trust_path)
.expect_https_connect_succeeds(&cert)
.await;
Ok(())
}
/// When `system_certs` is enabled, `SSL_CERT_DIR` still overrides the
/// certificate source.
#[tokio::test]
async fn test_system_certs_with_ssl_cert_dir_valid() -> Result<()> {
let cert = TestCertificate::new()?;
let dir = cert.ca_pem_dir();
client()
.system_certs(true)
.ssl_cert_dir(dir.path())
.expect_https_connect_succeeds(&cert)
.await;
Ok(())
}
/// Webpki roots include the CA for pypi.org, so a connection succeeds without
/// any custom configuration.
#[cfg(feature = "test-pypi")]
#[tokio::test]
async fn test_webpki_roots_trusts_pypi() -> Result<()> {
client()
.expect_https_connect_succeeds_for_host("pypi.org")
.await;
Ok(())
}
/// System certificate roots include the CA for pypi.org, so a connection
/// succeeds when `system_certs` is enabled.
#[cfg(feature = "test-pypi")]
#[tokio::test]
async fn test_system_certs_trusts_pypi() -> Result<()> {
client()
.system_certs(true)
.expect_https_connect_succeeds_for_host("pypi.org")
.await;
Ok(())
}
/// When `system_certs` is enabled and `SSL_CERT_FILE` is set to a self-signed
/// CA, a public host (whose CA is in the system store but not in the override
/// file) is rejected — proving that `SSL_CERT_FILE` replaces rather than
/// supplements the system roots.
#[cfg(feature = "test-pypi")]
#[tokio::test]
async fn test_system_certs_with_ssl_cert_file_replaces_system_roots() -> Result<()> {
let cert = TestCertificate::new()?;
client()
.system_certs(true)
.ssl_cert_file(&cert.trust_path)
.expect_https_connect_fails_for_host("pypi.org")
.await;
Ok(())
}
/// When `system_certs` is enabled and `SSL_CERT_DIR` points to a directory
/// with only a self-signed CA, a public host (whose CA is in the system store
/// but not in the override directory) is rejected — proving that `SSL_CERT_DIR`
/// replaces rather than supplements the system roots.
#[cfg(feature = "test-pypi")]
#[tokio::test]
async fn test_system_certs_with_ssl_cert_dir_replaces_system_roots() -> Result<()> {
let cert = TestCertificate::new()?;
let dir = cert.ca_pem_dir();
client()
.system_certs(true)
.ssl_cert_dir(dir.path())
.expect_https_connect_fails_for_host("pypi.org")
.await;
Ok(())
}
+2
View File
@@ -20,6 +20,7 @@ pub use required_version::*;
pub use sources::*;
pub use target_triple::*;
pub use threading::*;
pub use trusted_host::*;
pub use trusted_publishing::*;
pub use vcs::*;
@@ -46,6 +47,7 @@ mod required_version;
mod sources;
mod target_triple;
mod threading;
mod trusted_host;
mod trusted_publishing;
mod vcs;
+7
View File
@@ -300,11 +300,13 @@ fn validate_uv_toml(path: &Path, options: &Options) -> Result<(), Error> {
/// Validate that an [`Options`] contains no fields that `uv.toml` would mask
///
/// This is essentially the inverse of [`validate_uv_toml`].
#[allow(deprecated)]
fn warn_uv_toml_masked_fields(options: &Options) {
let Options {
globals:
GlobalOptions {
required_version,
system_certs,
native_tls,
offline,
no_cache,
@@ -392,6 +394,9 @@ fn warn_uv_toml_masked_fields(options: &Options) {
if required_version.is_some() {
masked_fields.push("required-version");
}
if system_certs.is_some() {
masked_fields.push("system-certs");
}
if native_tls.is_some() {
masked_fields.push("native-tls");
}
@@ -652,6 +657,7 @@ pub struct EnvironmentOptions {
pub managed_python: EnvFlag,
pub no_managed_python: EnvFlag,
pub native_tls: EnvFlag,
pub system_certs: EnvFlag,
pub preview: EnvFlag,
pub isolated: EnvFlag,
pub no_progress: EnvFlag,
@@ -746,6 +752,7 @@ impl EnvironmentOptions {
managed_python: EnvFlag::new(EnvVars::UV_MANAGED_PYTHON)?,
no_managed_python: EnvFlag::new(EnvVars::UV_NO_MANAGED_PYTHON)?,
native_tls: EnvFlag::new(EnvVars::UV_NATIVE_TLS)?,
system_certs: EnvFlag::new(EnvVars::UV_SYSTEM_CERTS)?,
preview: EnvFlag::new(EnvVars::UV_PREVIEW)?,
isolated: EnvFlag::new(EnvVars::UV_ISOLATED)?,
no_progress: EnvFlag::new(EnvVars::UV_NO_PROGRESS)?,
+21 -6
View File
@@ -236,13 +236,24 @@ pub struct GlobalOptions {
pub required_version: Option<RequiredVersion>,
/// 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).
/// By default, uv uses bundled Mozilla root certificates. When enabled, this loads
/// certificates from the platform's native certificate store instead.
#[option(
default = "false",
value_type = "bool",
uv_toml_only = true,
example = r#"
system-certs = true
"#
)]
pub system_certs: Option<bool>,
/// Whether to load TLS certificates from the platform's native certificate store.
///
/// 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.
/// By default, uv uses bundled Mozilla root certificates. When enabled, this loads
/// certificates from the platform's native certificate store instead.
///
/// (Deprecated: use `system-certs` instead.)
#[deprecated(note = "use `system-certs` instead")]
#[option(
default = "false",
value_type = "bool",
@@ -2187,6 +2198,7 @@ pub struct OptionsWire {
// #[serde(flatten)]
// globals: GlobalOptions
required_version: Option<RequiredVersion>,
system_certs: Option<bool>,
native_tls: Option<bool>,
offline: Option<bool>,
no_cache: Option<bool>,
@@ -2283,9 +2295,11 @@ pub struct OptionsWire {
}
impl From<OptionsWire> for Options {
#[allow(deprecated)]
fn from(value: OptionsWire) -> Self {
let OptionsWire {
required_version,
system_certs,
native_tls,
offline,
no_cache,
@@ -2362,6 +2376,7 @@ impl From<OptionsWire> for Options {
Self {
globals: GlobalOptions {
required_version,
system_certs,
native_tls,
offline,
no_cache,
+28 -7
View File
@@ -108,10 +108,17 @@ impl EnvVars {
pub const UV_BREAK_SYSTEM_PACKAGES: &'static str = "UV_BREAK_SYSTEM_PACKAGES";
/// Equivalent to the `--native-tls` command-line argument. If set to `true`, uv will
/// use the system's trust store instead of the bundled `webpki-roots` crate.
/// load TLS certificates from the platform's native certificate store instead of the
/// bundled Mozilla root certificates.
#[attr_added_in("0.1.19")]
pub const UV_NATIVE_TLS: &'static str = "UV_NATIVE_TLS";
/// Equivalent to the `--system-certs` command-line argument. If set to `true`, uv will
/// load TLS certificates from the platform's native certificate store instead of the
/// bundled Mozilla root certificates.
#[attr_added_in("next release")]
pub const UV_SYSTEM_CERTS: &'static str = "UV_SYSTEM_CERTS";
/// Equivalent to the `--index-strategy` command-line argument.
///
/// For example, if set to `unsafe-best-match`, uv will consider versions of a given package
@@ -647,17 +654,31 @@ impl EnvVars {
#[attr_added_in("0.2.16")]
pub const XDG_BIN_HOME: &'static str = "XDG_BIN_HOME";
/// Custom certificate bundle file path for SSL connections.
/// Path to a CA certificate bundle file for TLS connections.
///
/// Takes precedence over `UV_NATIVE_TLS` when set.
/// Requires a PEM-encoded certificate file (e.g., `certs.pem`, `ca-bundle.crt`). DER-encoded
/// files are not supported.
///
/// When set, this overrides the default certificate source (bundled Mozilla roots or system
/// certificates). Only the certificates in this file will be trusted.
#[attr_added_in("0.1.14")]
pub const SSL_CERT_FILE: &'static str = "SSL_CERT_FILE";
/// Custom path for certificate bundles for SSL connections.
/// Multiple entries are supported separated using a platform-specific
/// delimiter (`:` on Unix, `;` on Windows).
/// Path to a directory containing PEM-encoded CA certificate files for TLS connections.
///
/// Takes precedence over `UV_NATIVE_TLS` when set.
/// Multiple entries are supported, separated using a platform-specific delimiter (`:` on Unix,
/// `;` on Windows).
///
/// Certificates are usually stored with `.pem`, `.crt`, or `.cer` extensions, but uv will
/// attempt to read a certificate from any regular file in the provided `SSL_CERT_DIR`.
///
/// Files that cannot be parsed as PEM certificates are ignored. uv resolves symlinks and
/// ignores dangling symlinks.
///
/// Only PEM-encoded files are supported, i.e., DER-encoded files are not supported.
///
/// When set, this overrides the default certificate source (bundled Mozilla roots or system
/// certificates). Only the certificates in this directory will be trusted.
#[attr_added_in("0.9.10")]
pub const SSL_CERT_DIR: &'static str = "SSL_CERT_DIR";
+3
View File
@@ -130,6 +130,8 @@ pub const INSTA_FILTERS: &[(&str, &str)] = &[
),
// Trim end-of-line whitespaces, to allow removing them on save.
(r"([^\s])[ \t]+(\r?\n)", "$1$2"),
// Filter SSL certificate loading debug messages (environment-dependent)
(r"DEBUG Loaded \d+ certificate\(s\) from [^\n]+\n", ""),
];
/// Create a context for tests which simplifies shared behavior across tests.
@@ -1954,6 +1956,7 @@ impl TestContext {
EnvVars::SSL_CERT_DIR,
EnvVars::SSL_CERT_FILE,
EnvVars::UV_NATIVE_TLS,
EnvVars::UV_SYSTEM_CERTS,
];
for env_var in EnvVars::all_names()
+1 -1
View File
@@ -193,7 +193,7 @@ test-git = ["uv-test?/git"]
# Introduces a testing dependency on Git LFS.
test-git-lfs = ["test-git"]
# Introduces a testing dependency on PyPI.
test-pypi = []
test-pypi = ["uv-client/test-pypi"]
# Introduces a testing dependency on R2.
test-r2 = []
# Introduces a testing dependency on a local Python installation.
+9 -9
View File
@@ -34,18 +34,18 @@ static SUGGESTIONS: LazyLock<FxHashMap<PackageName, PackageName>> = LazyLock::ne
pub(crate) struct OperationDiagnostic {
/// The hint to display to the user upon resolution failure.
pub(crate) hint: Option<String>,
/// Whether native TLS is enabled.
pub(crate) native_tls: bool,
/// Whether system certificates are being used.
pub(crate) system_certs: bool,
/// The context to display to the user upon resolution failure.
pub(crate) context: Option<&'static str>,
}
impl OperationDiagnostic {
/// Create an [`OperationDiagnostic`] with the given native TLS setting.
/// Create an [`OperationDiagnostic`] with the given system certificates setting.
#[must_use]
pub(crate) fn native_tls(native_tls: bool) -> Self {
pub(crate) fn with_system_certs(system_certs: bool) -> Self {
Self {
native_tls,
system_certs,
..Default::default()
}
}
@@ -131,9 +131,9 @@ impl OperationDiagnostic {
}
}
pip::operations::Error::Resolve(uv_resolver::ResolveError::Client(err))
if !self.native_tls && err.is_ssl() =>
if !self.system_certs && err.is_ssl() =>
{
native_tls_hint(err);
system_certs_hint(err);
None
}
pip::operations::Error::OutdatedEnvironment => {
@@ -335,7 +335,7 @@ pub(crate) fn no_solution_hint(err: Box<uv_resolver::NoSolutionError>, help: Str
/// Render a [`uv_resolver::NoSolutionError`] with a help message.
// https://github.com/rust-lang/rust/issues/147648
#[allow(unused_assignments)]
pub(crate) fn native_tls_hint(err: uv_client::Error) {
pub(crate) fn system_certs_hint(err: uv_client::Error) {
#[derive(Debug, miette::Diagnostic)]
#[diagnostic()]
struct Error {
@@ -363,7 +363,7 @@ pub(crate) fn native_tls_hint(err: uv_client::Error) {
err,
help: format!(
"Consider enabling use of system TLS certificates with the `{}` command-line flag",
"--native-tls".green()
"--system-certs".green()
),
});
anstream::eprint!("{report:?}");
+5 -3
View File
@@ -597,9 +597,11 @@ pub(crate) async fn pip_compile(
{
Ok(resolution) => resolution,
Err(err) => {
return diagnostics::OperationDiagnostic::native_tls(client_builder.is_native_tls())
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
return diagnostics::OperationDiagnostic::with_system_certs(
client_builder.system_certs(),
)
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
}
};
+7 -5
View File
@@ -607,8 +607,8 @@ pub(crate) async fn pip_install(
{
Ok(graph) => Resolution::from(graph),
Err(err) => {
return diagnostics::OperationDiagnostic::native_tls(
client_builder.is_native_tls(),
return diagnostics::OperationDiagnostic::with_system_certs(
client_builder.system_certs(),
)
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
@@ -675,9 +675,11 @@ pub(crate) async fn pip_install(
{
Ok(..) => {}
Err(err) => {
return diagnostics::OperationDiagnostic::native_tls(client_builder.is_native_tls())
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
return diagnostics::OperationDiagnostic::with_system_certs(
client_builder.system_certs(),
)
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
}
}
+7 -5
View File
@@ -498,8 +498,8 @@ pub(crate) async fn pip_sync(
{
Ok(resolution) => Resolution::from(resolution),
Err(err) => {
return diagnostics::OperationDiagnostic::native_tls(
client_builder.is_native_tls(),
return diagnostics::OperationDiagnostic::with_system_certs(
client_builder.system_certs(),
)
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
@@ -566,9 +566,11 @@ pub(crate) async fn pip_sync(
{
Ok(_) => {}
Err(err) => {
return diagnostics::OperationDiagnostic::native_tls(client_builder.is_native_tls())
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
return diagnostics::OperationDiagnostic::with_system_certs(
client_builder.system_certs(),
)
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
}
}
+1 -1
View File
@@ -764,7 +764,7 @@ pub(crate) async fn add(
let _ = snapshot.revert();
}
match err {
ProjectError::Operation(err) => diagnostics::OperationDiagnostic::native_tls(client_builder.is_native_tls()).with_hint(format!("If you want to add the package regardless of the failed resolution, provide the `{}` flag to skip locking and syncing.", "--frozen".green()))
ProjectError::Operation(err) => diagnostics::OperationDiagnostic::with_system_certs(client_builder.system_certs()).with_hint(format!("If you want to add the package regardless of the failed resolution, provide the `{}` flag to skip locking and syncing.", "--frozen".green()))
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into())),
err => Err(err.into()),
+5 -3
View File
@@ -167,9 +167,11 @@ pub(crate) async fn audit(
{
Ok(result) => result.into_lock(),
Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::native_tls(client_builder.is_native_tls())
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
return diagnostics::OperationDiagnostic::with_system_certs(
client_builder.system_certs(),
)
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
}
Err(err) => return Err(err.into()),
};
+5 -3
View File
@@ -221,9 +221,11 @@ pub(crate) async fn export(
{
Ok(result) => result.into_lock(),
Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::native_tls(client_builder.is_native_tls())
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
return diagnostics::OperationDiagnostic::with_system_certs(
client_builder.system_certs(),
)
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
}
Err(err) => return Err(err.into()),
};
+1 -1
View File
@@ -260,7 +260,7 @@ pub(crate) async fn lock(
Ok(ExitStatus::Failure)
}
Err(ProjectError::Operation(err)) => {
diagnostics::OperationDiagnostic::native_tls(client_builder.is_native_tls())
diagnostics::OperationDiagnostic::with_system_certs(client_builder.system_certs())
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()))
}
+10 -6
View File
@@ -316,9 +316,11 @@ pub(crate) async fn remove(
{
Ok(result) => result.into_lock(),
Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::native_tls(client_builder.is_native_tls())
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
return diagnostics::OperationDiagnostic::with_system_certs(
client_builder.system_certs(),
)
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
}
Err(err) => return Err(err.into()),
};
@@ -373,9 +375,11 @@ pub(crate) async fn remove(
{
Ok(_) => {}
Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::native_tls(client_builder.is_native_tls())
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
return diagnostics::OperationDiagnostic::with_system_certs(
client_builder.system_certs(),
)
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
}
Err(err) => return Err(err.into()),
}
+12 -12
View File
@@ -294,8 +294,8 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
{
Ok(result) => result.into_lock(),
Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::native_tls(
client_builder.is_native_tls(),
return diagnostics::OperationDiagnostic::with_system_certs(
client_builder.system_certs(),
)
.with_context("script")
.report(err)
@@ -341,8 +341,8 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
{
Ok(_) => {}
Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::native_tls(
client_builder.is_native_tls(),
return diagnostics::OperationDiagnostic::with_system_certs(
client_builder.system_certs(),
)
.with_context("script")
.report(err)
@@ -458,8 +458,8 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
{
Ok(update) => Some(update.into_environment().into_interpreter()),
Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::native_tls(
client_builder.is_native_tls(),
return diagnostics::OperationDiagnostic::with_system_certs(
client_builder.system_certs(),
)
.with_context("script")
.report(err)
@@ -794,8 +794,8 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
{
Ok(result) => result,
Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::native_tls(
client_builder.is_native_tls(),
return diagnostics::OperationDiagnostic::with_system_certs(
client_builder.system_certs(),
)
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
@@ -882,8 +882,8 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
{
Ok(_) => {}
Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::native_tls(
client_builder.is_native_tls(),
return diagnostics::OperationDiagnostic::with_system_certs(
client_builder.system_certs(),
)
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
@@ -1037,8 +1037,8 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
let environment = match result {
Ok(resolution) => resolution,
Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::native_tls(
client_builder.is_native_tls(),
return diagnostics::OperationDiagnostic::with_system_certs(
client_builder.system_certs(),
)
.with_context("`--with`")
.report(err)
+12 -8
View File
@@ -305,8 +305,8 @@ pub(crate) async fn sync(
}
// TODO(zanieb): We should respect `--output-format json` for the error case
Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::native_tls(
client_builder.is_native_tls(),
return diagnostics::OperationDiagnostic::with_system_certs(
client_builder.system_certs(),
)
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
@@ -354,9 +354,11 @@ pub(crate) async fn sync(
{
Ok(result) => Outcome::Success(result),
Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::native_tls(client_builder.is_native_tls())
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
return diagnostics::OperationDiagnostic::with_system_certs(
client_builder.system_certs(),
)
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
}
Err(ProjectError::LockMismatch(prev, cur, lock_source)) => {
if dry_run.enabled() {
@@ -430,9 +432,11 @@ pub(crate) async fn sync(
{
Ok(changelog) => changelog,
Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::native_tls(client_builder.is_native_tls())
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
return diagnostics::OperationDiagnostic::with_system_certs(
client_builder.system_certs(),
)
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
}
Err(err) => return Err(err.into()),
};
+5 -3
View File
@@ -158,9 +158,11 @@ pub(crate) async fn tree(
{
Ok(result) => result.into_lock(),
Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::native_tls(client_builder.is_native_tls())
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
return diagnostics::OperationDiagnostic::with_system_certs(
client_builder.system_certs(),
)
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
}
Err(err) => return Err(err.into()),
};
+15 -9
View File
@@ -509,9 +509,11 @@ async fn print_frozen_version(
{
Ok(result) => result.into_lock(),
Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::native_tls(client_builder.is_native_tls())
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
return diagnostics::OperationDiagnostic::with_system_certs(
client_builder.system_certs(),
)
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
}
Err(err) => return Err(err.into()),
};
@@ -650,9 +652,11 @@ async fn lock_and_sync(
{
Ok(result) => result.into_lock(),
Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::native_tls(client_builder.is_native_tls())
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
return diagnostics::OperationDiagnostic::with_system_certs(
client_builder.system_certs(),
)
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
}
Err(err) => return Err(err.into()),
};
@@ -709,9 +713,11 @@ async fn lock_and_sync(
{
Ok(_) => {}
Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::native_tls(client_builder.is_native_tls())
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
return diagnostics::OperationDiagnostic::with_system_certs(
client_builder.system_certs(),
)
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
}
Err(err) => return Err(err.into()),
}
+2 -2
View File
@@ -5,7 +5,7 @@ use axoupdater::{AxoUpdater, AxoupdateError, UpdateRequest};
use owo_colors::OwoColorize;
use tracing::debug;
use uv_client::{BaseClientBuilder, WrappedReqwestError};
use uv_client::BaseClientBuilder;
use uv_fs::Simplified;
use crate::commands::ExitStatus;
@@ -210,7 +210,7 @@ pub(crate) async fn self_update(
)?;
Ok(ExitStatus::Error)
} else {
Err(WrappedReqwestError::from(err).into())
Err(err.into())
}
} else {
Err(err.into())
+8 -8
View File
@@ -594,8 +594,8 @@ pub(crate) async fn install(
{
Ok(update) => update.into_environment(),
Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::native_tls(
client_builder.is_native_tls(),
return diagnostics::OperationDiagnostic::with_system_certs(
client_builder.system_certs(),
)
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
@@ -658,8 +658,8 @@ pub(crate) async fn install(
.await
.ok()
.flatten() else {
return diagnostics::OperationDiagnostic::native_tls(
client_builder.is_native_tls(),
return diagnostics::OperationDiagnostic::with_system_certs(
client_builder.system_certs(),
)
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
@@ -690,8 +690,8 @@ pub(crate) async fn install(
{
Ok(resolution) => (resolution, interpreter),
Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::native_tls(
client_builder.is_native_tls(),
return diagnostics::OperationDiagnostic::with_system_certs(
client_builder.system_certs(),
)
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
@@ -735,8 +735,8 @@ pub(crate) async fn install(
}) {
Ok(environment) => environment,
Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::native_tls(
client_builder.is_native_tls(),
return diagnostics::OperationDiagnostic::with_system_certs(
client_builder.system_certs(),
)
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
+3 -3
View File
@@ -319,8 +319,8 @@ pub(crate) async fn run(
// If the user ran `uvx run ...`, the `run` is likely a mistake. Show a dedicated hint.
if from.is_none() && invocation_source == ToolRunCommand::Uvx && target == "run" {
let rest = args.iter().map(|s| s.to_string_lossy()).join(" ");
return diagnostics::OperationDiagnostic::native_tls(
client_builder.is_native_tls(),
return diagnostics::OperationDiagnostic::with_system_certs(
client_builder.system_certs(),
)
.with_hint(format!(
"`{}` invokes the `{}` package. Did you mean `{}`?",
@@ -334,7 +334,7 @@ pub(crate) async fn run(
}
let diagnostic =
diagnostics::OperationDiagnostic::native_tls(client_builder.is_native_tls());
diagnostics::OperationDiagnostic::with_system_certs(client_builder.system_certs());
let diagnostic = if let Some(verbose_flag) = find_verbose_flag(args) {
diagnostic.with_hint(format!(
"You provided `{}` to `{}`. Did you mean to provide it to `{}`? e.g., `{}`",
+2 -2
View File
@@ -254,7 +254,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
);
let client_builder = BaseClientBuilder::new(
settings.network_settings.connectivity,
settings.network_settings.native_tls,
settings.network_settings.system_certs,
settings.network_settings.allow_insecure_host,
settings.preview,
settings.network_settings.read_timeout,
@@ -556,7 +556,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
// Configure the global network settings.
let client_builder = BaseClientBuilder::new(
globals.network_settings.connectivity,
globals.network_settings.native_tls,
globals.network_settings.system_certs,
globals.network_settings.allow_insecure_host.clone(),
globals.preview,
globals.network_settings.read_timeout,
+30 -14
View File
@@ -241,7 +241,7 @@ pub(crate) fn resolve_preview(
pub(crate) struct NetworkSettings {
pub(crate) connectivity: Connectivity,
pub(crate) offline: Flag,
pub(crate) native_tls: bool,
pub(crate) system_certs: bool,
pub(crate) http_proxy: Option<ProxyUrl>,
pub(crate) https_proxy: Option<ProxyUrl>,
pub(crate) no_proxy: Option<Vec<String>>,
@@ -252,6 +252,7 @@ pub(crate) struct NetworkSettings {
}
impl NetworkSettings {
#[allow(deprecated)]
pub(crate) fn resolve(
args: &GlobalArgs,
workspace: Option<&FilesystemOptions>,
@@ -284,18 +285,33 @@ impl NetworkSettings {
} else {
Connectivity::Online
};
let native_tls = match flag(args.native_tls, args.no_native_tls, "native-tls") {
Some(value) => value,
None => {
if environment.native_tls.value == Some(true) {
true
} else {
workspace
.and_then(|workspace| workspace.globals.native_tls)
.unwrap_or(false)
}
}
};
// Resolve whether to use system certificates.
//
// `--native-tls` is a legacy alias for `--system-certs` — it enables system certificates
// but does NOT change the TLS backend. Any explicit CLI setting should take precedence
// over environment variables and workspace configuration, regardless of which spelling is
// used.
let system_certs =
if let Some(value) = flag(args.system_certs, args.no_system_certs, "system-certs") {
value
} else if let Some(value) = flag(args.native_tls, args.no_native_tls, "native-tls") {
value
} else if let Some(true) = environment.system_certs.value {
true
} else if let Some(true) = environment.native_tls.value {
true
} else {
workspace
.and_then(|workspace| {
workspace
.globals
.system_certs
.or(workspace.globals.native_tls)
})
.unwrap_or(false)
};
let allow_insecure_host = args
.allow_insecure_host
.as_ref()
@@ -320,7 +336,7 @@ impl NetworkSettings {
Self {
connectivity,
offline,
native_tls,
system_certs,
http_proxy,
https_proxy,
no_proxy,
+31 -28
View File
@@ -57,8 +57,9 @@ fn help() {
Use verbose output
--color <COLOR_CHOICE>
Control the use of color in output [possible values: auto, always, never]
--native-tls
Whether to load TLS certificates from the platform's native store [env: UV_NATIVE_TLS=]
--system-certs
Whether to load TLS certificates from the platform's native certificate store [env:
UV_SYSTEM_CERTS=]
--offline
Disable network access [env: UV_OFFLINE=]
--allow-insecure-host <ALLOW_INSECURE_HOST>
@@ -138,8 +139,9 @@ fn help_flag() {
Use verbose output
--color <COLOR_CHOICE>
Control the use of color in output [possible values: auto, always, never]
--native-tls
Whether to load TLS certificates from the platform's native store [env: UV_NATIVE_TLS=]
--system-certs
Whether to load TLS certificates from the platform's native certificate store [env:
UV_SYSTEM_CERTS=]
--offline
Disable network access [env: UV_OFFLINE=]
--allow-insecure-host <ALLOW_INSECURE_HOST>
@@ -218,8 +220,9 @@ fn help_short_flag() {
Use verbose output
--color <COLOR_CHOICE>
Control the use of color in output [possible values: auto, always, never]
--native-tls
Whether to load TLS certificates from the platform's native store [env: UV_NATIVE_TLS=]
--system-certs
Whether to load TLS certificates from the platform's native certificate store [env:
UV_SYSTEM_CERTS=]
--offline
Disable network access [env: UV_OFFLINE=]
--allow-insecure-host <ALLOW_INSECURE_HOST>
@@ -368,19 +371,17 @@ fn help_subcommand() {
- always: Enables colored output regardless of the detected environment
- never: Disables colored output
--native-tls
Whether to load TLS certificates from the platform's native store.
--system-certs
Whether to load TLS certificates from the platform's native certificate store [env:
UV_SYSTEM_CERTS=]
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).
By default, uv uses bundled Mozilla root certificates, which 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.
[env: UV_NATIVE_TLS=]
--offline
Disable network access.
@@ -654,19 +655,17 @@ fn help_subsubcommand() {
- always: Enables colored output regardless of the detected environment
- never: Disables colored output
--native-tls
Whether to load TLS certificates from the platform's native store.
--system-certs
Whether to load TLS certificates from the platform's native certificate store [env:
UV_SYSTEM_CERTS=]
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).
By default, uv uses bundled Mozilla root certificates, which 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.
[env: UV_NATIVE_TLS=]
--offline
Disable network access.
@@ -783,8 +782,9 @@ fn help_flag_subcommand() {
Use verbose output
--color <COLOR_CHOICE>
Control the use of color in output [possible values: auto, always, never]
--native-tls
Whether to load TLS certificates from the platform's native store [env: UV_NATIVE_TLS=]
--system-certs
Whether to load TLS certificates from the platform's native certificate store [env:
UV_SYSTEM_CERTS=]
--offline
Disable network access [env: UV_OFFLINE=]
--allow-insecure-host <ALLOW_INSECURE_HOST>
@@ -866,8 +866,9 @@ fn help_flag_subsubcommand() {
Use verbose output
--color <COLOR_CHOICE>
Control the use of color in output [possible values: auto, always, never]
--native-tls
Whether to load TLS certificates from the platform's native store [env: UV_NATIVE_TLS=]
--system-certs
Whether to load TLS certificates from the platform's native certificate store [env:
UV_SYSTEM_CERTS=]
--offline
Disable network access [env: UV_OFFLINE=]
--allow-insecure-host <ALLOW_INSECURE_HOST>
@@ -1030,8 +1031,9 @@ fn help_with_global_option() {
Use verbose output
--color <COLOR_CHOICE>
Control the use of color in output [possible values: auto, always, never]
--native-tls
Whether to load TLS certificates from the platform's native store [env: UV_NATIVE_TLS=]
--system-certs
Whether to load TLS certificates from the platform's native certificate store [env:
UV_SYSTEM_CERTS=]
--offline
Disable network access [env: UV_OFFLINE=]
--allow-insecure-host <ALLOW_INSECURE_HOST>
@@ -1153,8 +1155,9 @@ fn help_with_no_pager() {
Use verbose output
--color <COLOR_CHOICE>
Control the use of color in output [possible values: auto, always, never]
--native-tls
Whether to load TLS certificates from the platform's native store [env: UV_NATIVE_TLS=]
--system-certs
Whether to load TLS certificates from the platform's native certificate store [env:
UV_SYSTEM_CERTS=]
--offline
Disable network access [env: UV_OFFLINE=]
--allow-insecure-host <ALLOW_INSECURE_HOST>
+1 -1
View File
@@ -234,7 +234,7 @@ fn invalid_pyproject_toml_option_unknown_field() -> Result<()> {
|
2 | unknown = "field"
| ^^^^^^^
unknown field `unknown`, expected one of `required-version`, `native-tls`, [...]
unknown field `unknown`, expected one of `required-version`, `system-certs`, `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `http-proxy`, `https-proxy`, `no-proxy`, `allow-insecure-host`, `resolution`, `prerelease`, `fork-strategy`, `dependency-metadata`, `config-settings`, `config-settings-package`, `no-build-isolation`, `no-build-isolation-package`, `extra-build-dependencies`, `extra-build-variables`, `exclude-newer`, `exclude-newer-package`, `link-mode`, `compile-bytecode`, `no-sources`, `no-sources-package`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `torch-backend`, `python-install-mirror`, `pypy-install-mirror`, `python-downloads-json-url`, `publish-url`, `trusted-publishing`, `check-url`, `add-bounds`, `pip`, `cache-keys`, `override-dependencies`, `exclude-dependencies`, `constraint-dependencies`, `build-constraint-dependencies`, `environments`, `required-environments`, `conflicts`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dependency-groups`, `dev-dependencies`, `build-backend`
Resolved in [TIME]
Checked in [TIME]
+577 -57
View File
@@ -61,7 +61,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -272,7 +272,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -484,7 +484,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -728,7 +728,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -941,7 +941,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -1128,7 +1128,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -1364,7 +1364,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -1608,7 +1608,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -1910,7 +1910,7 @@ fn resolve_find_links() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -2143,7 +2143,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -2335,7 +2335,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -2577,7 +2577,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -2842,7 +2842,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -3024,7 +3024,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -3206,7 +3206,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -3390,7 +3390,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -3593,7 +3593,7 @@ fn resolve_tool() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -3791,7 +3791,7 @@ fn resolve_poetry_toml() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -4007,7 +4007,7 @@ fn resolve_both() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -4264,7 +4264,7 @@ fn resolve_both_special_fields() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -4600,7 +4600,7 @@ fn resolve_config_file() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -4821,7 +4821,7 @@ fn resolve_config_file() -> anyhow::Result<()> {
|
1 | [project]
| ^^^^^^^
unknown field `project`, expected one of `required-version`, `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `http-proxy`, `https-proxy`, `no-proxy`, `allow-insecure-host`, `resolution`, `prerelease`, `fork-strategy`, `dependency-metadata`, `config-settings`, `config-settings-package`, `no-build-isolation`, `no-build-isolation-package`, `extra-build-dependencies`, `extra-build-variables`, `exclude-newer`, `exclude-newer-package`, `link-mode`, `compile-bytecode`, `no-sources`, `no-sources-package`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `torch-backend`, `python-install-mirror`, `pypy-install-mirror`, `python-downloads-json-url`, `publish-url`, `trusted-publishing`, `check-url`, `add-bounds`, `pip`, `cache-keys`, `override-dependencies`, `exclude-dependencies`, `constraint-dependencies`, `build-constraint-dependencies`, `environments`, `required-environments`, `conflicts`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dependency-groups`, `dev-dependencies`, `build-backend`
unknown field `project`, expected one of `required-version`, `system-certs`, `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `http-proxy`, `https-proxy`, `no-proxy`, `allow-insecure-host`, `resolution`, `prerelease`, `fork-strategy`, `dependency-metadata`, `config-settings`, `config-settings-package`, `no-build-isolation`, `no-build-isolation-package`, `extra-build-dependencies`, `extra-build-variables`, `exclude-newer`, `exclude-newer-package`, `link-mode`, `compile-bytecode`, `no-sources`, `no-sources-package`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `torch-backend`, `python-install-mirror`, `pypy-install-mirror`, `python-downloads-json-url`, `publish-url`, `trusted-publishing`, `check-url`, `add-bounds`, `pip`, `cache-keys`, `override-dependencies`, `exclude-dependencies`, `constraint-dependencies`, `build-constraint-dependencies`, `environments`, `required-environments`, `conflicts`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dependency-groups`, `dev-dependencies`, `build-backend`
"
);
@@ -4909,7 +4909,7 @@ fn resolve_skip_empty() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -5094,7 +5094,7 @@ fn resolve_skip_empty() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -5287,7 +5287,7 @@ fn allow_insecure_host() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -5494,7 +5494,7 @@ fn index_priority() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -5740,7 +5740,7 @@ fn index_priority() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -5992,7 +5992,7 @@ fn index_priority() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -6239,7 +6239,7 @@ fn index_priority() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -6493,7 +6493,7 @@ fn index_priority() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -6740,7 +6740,7 @@ fn index_priority() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -7000,7 +7000,7 @@ fn verify_hashes() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -7175,7 +7175,7 @@ fn verify_hashes() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -7348,7 +7348,7 @@ fn verify_hashes() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -7523,7 +7523,7 @@ fn verify_hashes() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -7696,7 +7696,7 @@ fn verify_hashes() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -7870,7 +7870,7 @@ fn verify_hashes() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -8059,7 +8059,7 @@ fn preview_features() {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -8209,7 +8209,7 @@ fn preview_features() {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -8330,7 +8330,7 @@ fn preview_features() {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -8480,7 +8480,7 @@ fn preview_features() {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -8604,7 +8604,7 @@ fn preview_features() {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -8730,7 +8730,7 @@ fn preview_features() {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -8840,6 +8840,526 @@ fn preview_features() {
);
}
#[test]
#[cfg_attr(
windows,
ignore = "Configuration tests are not yet supported on Windows"
)]
fn system_certs_cli_aliases_override_env() {
let context = uv_test::test_context!("3.12");
uv_snapshot!(context.filters(), add_shared_args(context.version())
.arg("--show-settings")
.arg("--no-native-tls")
.env(EnvVars::UV_SYSTEM_CERTS, "1"), @r#"
success: true
exit_code: 0
----- stdout -----
GlobalSettings {
required_version: None,
quiet: 0,
verbose: 0,
color: Auto,
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
allow_insecure_host: [],
read_timeout: [TIME],
connect_timeout: [TIME],
retries: 3,
},
concurrency: Concurrency {
downloads: 50,
builds: 16,
installs: 8,
},
show_settings: true,
preview: Preview {
flags: [],
},
python_preference: Managed,
python_downloads: Automatic,
no_progress: false,
installer_metadata: true,
}
CacheSettings {
no_cache: false,
cache_dir: Some(
"[CACHE_DIR]/",
),
}
VersionSettings {
value: None,
bump: [],
short: false,
output_format: Text,
dry_run: false,
lock_check: Disabled,
frozen: None,
active: None,
no_sync: false,
package: None,
python: None,
install_mirrors: PythonInstallMirrors {
python_install_mirror: None,
pypy_install_mirror: None,
python_downloads_json_url: None,
},
refresh: None(
Timestamp(
SystemTime {
tv_sec: [TIME],
tv_nsec: [TIME],
},
),
),
settings: ResolverInstallerSettings {
resolver: ResolverSettings {
build_options: BuildOptions {
no_binary: None,
no_build: None,
},
config_setting: ConfigSettings(
{},
),
config_settings_package: PackageConfigSettings(
{},
),
dependency_metadata: DependencyMetadata(
{},
),
exclude_newer: ExcludeNewer {
global: None,
package: ExcludeNewerPackage(
{},
),
},
fork_strategy: RequiresPython,
index_locations: IndexLocations {
indexes: [],
flat_index: [],
no_index: false,
},
index_strategy: FirstIndex,
keyring_provider: Disabled,
link_mode: Clone,
build_isolation: Isolate,
extra_build_dependencies: ExtraBuildDependencies(
{},
),
extra_build_variables: ExtraBuildVariables(
{},
),
prerelease: IfNecessaryOrExplicit,
resolution: Highest,
sources: None,
torch_backend: None,
upgrade: Upgrade {
strategy: None,
constraints: {},
},
},
compile_bytecode: false,
reinstall: None,
},
}
----- stderr -----
"#
);
uv_snapshot!(context.filters(), add_shared_args(context.version())
.arg("--show-settings")
.arg("--no-system-certs")
.env(EnvVars::UV_NATIVE_TLS, "1"), @r#"
success: true
exit_code: 0
----- stdout -----
GlobalSettings {
required_version: None,
quiet: 0,
verbose: 0,
color: Auto,
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
allow_insecure_host: [],
read_timeout: [TIME],
connect_timeout: [TIME],
retries: 3,
},
concurrency: Concurrency {
downloads: 50,
builds: 16,
installs: 8,
},
show_settings: true,
preview: Preview {
flags: [],
},
python_preference: Managed,
python_downloads: Automatic,
no_progress: false,
installer_metadata: true,
}
CacheSettings {
no_cache: false,
cache_dir: Some(
"[CACHE_DIR]/",
),
}
VersionSettings {
value: None,
bump: [],
short: false,
output_format: Text,
dry_run: false,
lock_check: Disabled,
frozen: None,
active: None,
no_sync: false,
package: None,
python: None,
install_mirrors: PythonInstallMirrors {
python_install_mirror: None,
pypy_install_mirror: None,
python_downloads_json_url: None,
},
refresh: None(
Timestamp(
SystemTime {
tv_sec: [TIME],
tv_nsec: [TIME],
},
),
),
settings: ResolverInstallerSettings {
resolver: ResolverSettings {
build_options: BuildOptions {
no_binary: None,
no_build: None,
},
config_setting: ConfigSettings(
{},
),
config_settings_package: PackageConfigSettings(
{},
),
dependency_metadata: DependencyMetadata(
{},
),
exclude_newer: ExcludeNewer {
global: None,
package: ExcludeNewerPackage(
{},
),
},
fork_strategy: RequiresPython,
index_locations: IndexLocations {
indexes: [],
flat_index: [],
no_index: false,
},
index_strategy: FirstIndex,
keyring_provider: Disabled,
link_mode: Clone,
build_isolation: Isolate,
extra_build_dependencies: ExtraBuildDependencies(
{},
),
extra_build_variables: ExtraBuildVariables(
{},
),
prerelease: IfNecessaryOrExplicit,
resolution: Highest,
sources: None,
torch_backend: None,
upgrade: Upgrade {
strategy: None,
constraints: {},
},
},
compile_bytecode: false,
reinstall: None,
},
}
----- stderr -----
"#
);
}
#[test]
#[cfg_attr(
windows,
ignore = "Configuration tests are not yet supported on Windows"
)]
fn system_certs_config_aliases() -> anyhow::Result<()> {
let context = uv_test::test_context!("3.12");
let config = context.temp_dir.child("uv.toml");
config.write_str("system-certs = true\n")?;
uv_snapshot!(context.filters(), add_shared_args(context.version())
.arg("--show-settings"), @r#"
success: true
exit_code: 0
----- stdout -----
GlobalSettings {
required_version: None,
quiet: 0,
verbose: 0,
color: Auto,
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
system_certs: true,
http_proxy: None,
https_proxy: None,
no_proxy: None,
allow_insecure_host: [],
read_timeout: [TIME],
connect_timeout: [TIME],
retries: 3,
},
concurrency: Concurrency {
downloads: 50,
builds: 16,
installs: 8,
},
show_settings: true,
preview: Preview {
flags: [],
},
python_preference: Managed,
python_downloads: Automatic,
no_progress: false,
installer_metadata: true,
}
CacheSettings {
no_cache: false,
cache_dir: Some(
"[CACHE_DIR]/",
),
}
VersionSettings {
value: None,
bump: [],
short: false,
output_format: Text,
dry_run: false,
lock_check: Disabled,
frozen: None,
active: None,
no_sync: false,
package: None,
python: None,
install_mirrors: PythonInstallMirrors {
python_install_mirror: None,
pypy_install_mirror: None,
python_downloads_json_url: None,
},
refresh: None(
Timestamp(
SystemTime {
tv_sec: [TIME],
tv_nsec: [TIME],
},
),
),
settings: ResolverInstallerSettings {
resolver: ResolverSettings {
build_options: BuildOptions {
no_binary: None,
no_build: None,
},
config_setting: ConfigSettings(
{},
),
config_settings_package: PackageConfigSettings(
{},
),
dependency_metadata: DependencyMetadata(
{},
),
exclude_newer: ExcludeNewer {
global: None,
package: ExcludeNewerPackage(
{},
),
},
fork_strategy: RequiresPython,
index_locations: IndexLocations {
indexes: [],
flat_index: [],
no_index: false,
},
index_strategy: FirstIndex,
keyring_provider: Disabled,
link_mode: Clone,
build_isolation: Isolate,
extra_build_dependencies: ExtraBuildDependencies(
{},
),
extra_build_variables: ExtraBuildVariables(
{},
),
prerelease: IfNecessaryOrExplicit,
resolution: Highest,
sources: None,
torch_backend: None,
upgrade: Upgrade {
strategy: None,
constraints: {},
},
},
compile_bytecode: false,
reinstall: None,
},
}
----- stderr -----
"#
);
config.write_str(indoc::indoc! {r"
system-certs = false
native-tls = true
"})?;
uv_snapshot!(context.filters(), add_shared_args(context.version())
.arg("--show-settings"), @r#"
success: true
exit_code: 0
----- stdout -----
GlobalSettings {
required_version: None,
quiet: 0,
verbose: 0,
color: Auto,
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
allow_insecure_host: [],
read_timeout: [TIME],
connect_timeout: [TIME],
retries: 3,
},
concurrency: Concurrency {
downloads: 50,
builds: 16,
installs: 8,
},
show_settings: true,
preview: Preview {
flags: [],
},
python_preference: Managed,
python_downloads: Automatic,
no_progress: false,
installer_metadata: true,
}
CacheSettings {
no_cache: false,
cache_dir: Some(
"[CACHE_DIR]/",
),
}
VersionSettings {
value: None,
bump: [],
short: false,
output_format: Text,
dry_run: false,
lock_check: Disabled,
frozen: None,
active: None,
no_sync: false,
package: None,
python: None,
install_mirrors: PythonInstallMirrors {
python_install_mirror: None,
pypy_install_mirror: None,
python_downloads_json_url: None,
},
refresh: None(
Timestamp(
SystemTime {
tv_sec: [TIME],
tv_nsec: [TIME],
},
),
),
settings: ResolverInstallerSettings {
resolver: ResolverSettings {
build_options: BuildOptions {
no_binary: None,
no_build: None,
},
config_setting: ConfigSettings(
{},
),
config_settings_package: PackageConfigSettings(
{},
),
dependency_metadata: DependencyMetadata(
{},
),
exclude_newer: ExcludeNewer {
global: None,
package: ExcludeNewerPackage(
{},
),
},
fork_strategy: RequiresPython,
index_locations: IndexLocations {
indexes: [],
flat_index: [],
no_index: false,
},
index_strategy: FirstIndex,
keyring_provider: Disabled,
link_mode: Clone,
build_isolation: Isolate,
extra_build_dependencies: ExtraBuildDependencies(
{},
),
extra_build_variables: ExtraBuildVariables(
{},
),
prerelease: IfNecessaryOrExplicit,
resolution: Highest,
sources: None,
torch_backend: None,
upgrade: Upgrade {
strategy: None,
constraints: {},
},
},
compile_bytecode: false,
reinstall: None,
},
}
----- stderr -----
"#
);
Ok(())
}
/// Track the interactions between `upgrade` and `upgrade-package` across the `uv pip` CLI and a
/// configuration file.
#[test]
@@ -8872,7 +9392,7 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -9061,7 +9581,7 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -9244,7 +9764,7 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -9425,7 +9945,7 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -9606,7 +10126,7 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -9788,7 +10308,7 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -9995,7 +10515,7 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -10127,7 +10647,7 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -10253,7 +10773,7 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -10377,7 +10897,7 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -10497,7 +11017,7 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -10618,7 +11138,7 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -10763,7 +11283,7 @@ fn build_isolation_override() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
@@ -10941,7 +11461,7 @@ fn build_isolation_override() -> anyhow::Result<()> {
network_settings: NetworkSettings {
connectivity: Online,
offline: Disabled,
native_tls: false,
system_certs: false,
http_proxy: None,
https_proxy: None,
no_proxy: None,
+53 -15
View File
@@ -1,32 +1,70 @@
# TLS certificates
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).
uv uses TLS to securely communicate with package indexes and other HTTPS servers. TLS certificates
are used to verify the identity of these servers, ensuring that connections are not intercepted.
## TLS backend
uv uses [`rustls`](https://github.com/rustls/rustls), a memory-safe TLS implementation written in
Rust, with [`aws-lc-rs`](https://github.com/aws/aws-lc-rs) as the cryptography provider.
uv supports the following X.509 certificate signature algorithms:
- ECDSA (P-256, P-384, P-521) with SHA-256, SHA-384, or SHA-512
- Ed25519
- RSA PKCS#1 v1.5 (20488192 bit) with SHA-256, SHA-384, or SHA-512
- RSA-PSS (20488192 bit) with SHA-256, SHA-384, or SHA-512
## System certificates
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, or set the `UV_NATIVE_TLS` environment variable to `true`.
By default, uv uses bundled Mozilla root certificates for TLS verification. In some cases, you may
want to use the platform's native certificate store instead — for example, 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 use system certificates, pass the [`--system-certs`](../../reference/cli.md#uv) flag, set the
[`UV_SYSTEM_CERTS`](../../reference/environment.md#uv_system_certs) environment variable to `true`,
or set [`system-certs = true`](../../reference/settings.md#system-certs) in `uv.toml`.
When using system certificates, certificate verification is performed by
[`rustls-platform-verifier`](https://github.com/rustls/rustls-platform-verifier), which delegates to
the operating system's certificate verifier.
## Custom certificates
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.
To use custom CA certificates, set the
[`SSL_CERT_FILE`](../../reference/environment.md#ssl_cert_file) environment variable to the path of
a PEM-encoded certificate bundle (e.g., `certs.pem`, `ca-bundle.crt`), or set
[`SSL_CERT_DIR`](../../reference/environment.md#ssl_cert_dir) to one or more directories containing
PEM-encoded certificate files. Multiple entries are supported, separated using a platform-specific
delimiter (`:` on Unix, `;` on Windows).
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.
Certificates are usually stored with `.pem`, `.crt`, or `.cer` extensions, but uv will attempt to
read a certificate from any regular file in the provided `SSL_CERT_DIR`.
Files that cannot be parsed as PEM certificates are ignored. uv resolves symlinks and ignores
dangling symlinks.
DER-encoded files are not supported.
When set, these environment variables **override** the default certificate source entirely — only
the provided certificates will be trusted.
`SSL_CERT_FILE` can point to a single certificate or a bundle containing multiple certificates.
`SSL_CERT_DIR` can include multiple directory entries; uv will load all valid certificates from each
directory.
If client certificate authentication (mTLS) is desired, set the
[`SSL_CLIENT_CERT`](../../reference/environment.md#ssl_client_cert) environment variable to the path
of a PEM formatted file containing the certificate followed by the private key.
## Insecure hosts
If you're using a setup in which you want to trust a self-signed certificate or otherwise disable
certificate verification, you can instruct uv to allow insecure connections to dedicated hosts via
the `allow-insecure-host` configuration option. For example, adding the following to
`pyproject.toml` will allow insecure connections to `example.com`:
the [`allow-insecure-host`](../../reference/settings.md#allow-insecure-host) configuration option.
For example, adding the following to `pyproject.toml` will allow insecure connections to
`example.com`:
```toml
[tool.uv]
+8 -3
View File
@@ -322,8 +322,9 @@
"type": ["boolean", "null"]
},
"native-tls": {
"description": "Whether to load TLS certificates from the platform's native certificate store.\n\nBy default, uv loads certificates from the bundled `webpki-roots` crate. The\n`webpki-roots` are a reliable set of trust roots from Mozilla, and including them in uv\nimproves portability and performance (especially on macOS).\n\nHowever, in some cases, you may want to use the platform's native certificate store,\nespecially if you're relying on a corporate trust root (e.g., for a mandatory proxy) that's\nincluded in your system's certificate store.",
"type": ["boolean", "null"]
"description": "Whether to load TLS certificates from the platform's native certificate store.\n\nBy default, uv uses bundled Mozilla root certificates. When enabled, this loads\ncertificates from the platform's native certificate store instead.\n\n(Deprecated: use `system-certs` instead.)",
"type": ["boolean", "null"],
"deprecated": true
},
"no-binary": {
"description": "Don't install pre-built wheels.\n\nThe given packages will be built and installed from source. The resolver will still use\npre-built wheels to extract package metadata, if available.",
@@ -520,6 +521,10 @@
}
]
},
"system-certs": {
"description": "Whether to load TLS certificates from the platform's native certificate store.\n\nBy default, uv uses bundled Mozilla root certificates. When enabled, this loads\ncertificates from the platform's native certificate store instead.",
"type": ["boolean", "null"]
},
"torch-backend": {
"description": "The backend to use when fetching packages in the PyTorch ecosystem.\n\nWhen set, uv will ignore the configured index URLs for packages in the PyTorch ecosystem,\nand will instead use the defined backend.\n\nFor example, when set to `cpu`, uv will use the CPU-only PyTorch index; when set to `cu126`,\nuv will use the PyTorch index for CUDA 12.6.\n\nThe `auto` mode will attempt to detect the appropriate PyTorch index based on the currently\ninstalled CUDA drivers.\n\nThis setting is only respected by `uv pip` commands.\n\nThis option is in preview and may change in any future release.",
"anyOf": [
@@ -1051,7 +1056,7 @@
]
},
"LinkMode": {
"description": "The method to use when linking.\n\nDefaults to [`Clone`](LinkMode::Clone) on macOS and Linux (which support copy-on-write on\nAPFS and btrfs/xfs/bcachefs respectively), and [`Hardlink`](LinkMode::Hardlink) on other\nplatforms.",
"description": "The method to use when linking.\n\nDefaults to [`LinkMode::Clone`] on macOS and Linux (which support copy-on-write on\nAPFS and btrfs/xfs/bcachefs respectively), and [`LinkMode::Hardlink`] on other\nplatforms.",
"oneOf": [
{
"description": "Clone (i.e., copy-on-write) packages from the source into the destination.",