fix(packaging): verify the release download against a CA bundle that exists

python-build-standalone's OpenSSL has default trust paths that need not
exist on the host, so urlopen failed with "unable to get local issuer
certificate" while browsers and curl accepted the same chain.  Fall back
to certifi when OpenSSL's default cafile is missing, and name the bundle
in use when verification still fails so the next occurrence is one line
to diagnose.
This commit is contained in:
beefcarver committed 2026-11-15 09:51:01 -05:00
1 parent 512c9a663f
commit 4fd688e673
1 file changed
+43 -1
+43 -1
View File
@@ -33,11 +33,14 @@ from __future__ import annotations
import argparse
import hashlib
import os
import re
import ssl
import subprocess
import sys
import tarfile
import tempfile
import urllib.error
import urllib.request
from pathlib import Path
@@ -193,6 +196,30 @@ def sha256(path: Path) -> str:
return digest.hexdigest()
def ca_file() -> str | None:
"""The CA bundle to verify with, or None to let OpenSSL use its own default.
`uv python install` fetches a python-build-standalone interpreter whose
OpenSSL is compiled with default trust paths that need not exist on the
host (on macOS, and on distros that keep certs only in
/etc/ssl/certs/ca-certificates.crt, they often do not). Verification then
fails with `unable to get local issuer certificate` for every site, even
though curl and browsers are happy. Fall back to certifi, which `httpx`
already puts in the environment.
"""
if os.environ.get("SSL_CERT_FILE"):
return None # ssl honours the variable itself; do not second-guess it
paths = ssl.get_default_verify_paths()
for candidate in (paths.cafile, paths.openssl_cafile):
if candidate and Path(candidate).is_file():
return None
try:
import certifi
except ModuleNotFoundError:
return None
return certifi.where()
def fetch_asset(version: str, override: Path | None) -> Path:
if override is not None:
if not override.is_file():
@@ -200,10 +227,25 @@ def fetch_asset(version: str, override: Path | None) -> Path:
return override
url = RELEASE_ASSET.format(version=version, name=ASSET_NAME)
say(f"downloading {url}")
cafile = ca_file()
context = ssl.create_default_context(cafile=cafile)
target = Path(tempfile.mkdtemp()) / f"{ASSET_NAME}-{version}.tar.gz"
try:
with urllib.request.urlopen(url) as response, target.open("wb") as handle:
with urllib.request.urlopen(url, context=context) as response, target.open(
"wb"
) as handle:
handle.write(response.read())
except urllib.error.URLError as error:
# urlopen wraps socket/SSL failures; the cause is in `.reason`.
if isinstance(error.reason, ssl.SSLCertVerificationError):
bundle = cafile or ssl.get_default_verify_paths().cafile or "no bundle found"
raise fail(
f"cannot fetch the release asset ({error.reason.verify_message or error.reason}); "
f"verified against {bundle}. If that bundle is stale, point SSL_CERT_FILE "
"at a current one (on Arch and Debian that is "
"/etc/ssl/certs/ca-certificates.crt)"
) from error
raise fail(f"cannot fetch the release asset ({error.reason}); is the tag published?") from error
except OSError as error:
raise fail(f"cannot fetch the release asset ({error}); is the tag published?") from error
return target