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:
1 parent
512c9a663f
commit
4fd688e673
1 file changed
+43
-1
+43
-1
@@ -33,11 +33,14 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
|
import ssl
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tarfile
|
import tarfile
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import urllib.error
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -193,6 +196,30 @@ def sha256(path: Path) -> str:
|
|||||||
return digest.hexdigest()
|
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:
|
def fetch_asset(version: str, override: Path | None) -> Path:
|
||||||
if override is not None:
|
if override is not None:
|
||||||
if not override.is_file():
|
if not override.is_file():
|
||||||
@@ -200,10 +227,25 @@ def fetch_asset(version: str, override: Path | None) -> Path:
|
|||||||
return override
|
return override
|
||||||
url = RELEASE_ASSET.format(version=version, name=ASSET_NAME)
|
url = RELEASE_ASSET.format(version=version, name=ASSET_NAME)
|
||||||
say(f"downloading {url}")
|
say(f"downloading {url}")
|
||||||
|
cafile = ca_file()
|
||||||
|
context = ssl.create_default_context(cafile=cafile)
|
||||||
target = Path(tempfile.mkdtemp()) / f"{ASSET_NAME}-{version}.tar.gz"
|
target = Path(tempfile.mkdtemp()) / f"{ASSET_NAME}-{version}.tar.gz"
|
||||||
try:
|
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())
|
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:
|
except OSError as error:
|
||||||
raise fail(f"cannot fetch the release asset ({error}); is the tag published?") from error
|
raise fail(f"cannot fetch the release asset ({error}); is the tag published?") from error
|
||||||
return target
|
return target
|
||||||
|
|||||||
Reference in new issue
Block a user