Skip to content

Commit 6c10dd8

Browse files
committed
install: meaningful TLS cert errors; --allow-insecure accepts bad certs
Surface a clear error instead of a raw [SSL: CERTIFICATE_VERIFY_FAILED] when an HTTPS endpoint presents an untrusted/expired/self-signed cert (or a hostname mismatch), for both Docker registry pulls and plain URL (tarball) downloads. The download path now fails fast on such errors rather than looping through every retry first. Extend --allow-insecure to also skip TLS certificate verification on HTTPS endpoints. For a custom registry the scheme is now resolved by probing HTTPS (with verification disabled) first and falling back to plain HTTP, so one flag covers both bad-cert HTTPS registries and HTTP-only registries (Docker's --insecure-registry model). To carry the resolved scheme, get_auth_token now returns (token, base_url) threaded through the pull path. The generic TLS classifiers and the unverified SSL context live in helpers/download.py and are shared by the registry transport. push is unchanged (verified HTTPS only).
1 parent 7544c1e commit 6c10dd8

9 files changed

Lines changed: 426 additions & 176 deletions

File tree

proot_distro/commands/help/pages.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -340,9 +340,11 @@
340340
"platform strings (linux/arm64, linux/amd64, linux/arm/v7, "
341341
"linux/386, linux/riscv64)."),
342342
("--allow-insecure",
343-
"Permit pulling from a custom registry served over plain "
344-
"HTTP instead of HTTPS. Use this only for trusted registries "
345-
"on a network path you control."),
343+
"Permit insecure transport: a custom registry served over "
344+
"plain HTTP, or an HTTPS endpoint (registry or download URL) "
345+
"whose TLS certificate is untrusted, expired, self-signed, or "
346+
"for a different hostname. Use only for trusted sources on a "
347+
"network path you control."),
346348
("-q, --quiet", "Suppress non-error output."),
347349
],
348350
"examples": [

proot_distro/commands/install.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ def _cleanup() -> None:
240240
)
241241
os.close(fd)
242242
log_info("Downloading archive...")
243-
download_file(url, tmp_archive)
243+
download_file(url, tmp_archive, insecure=allow_insecure)
244244
log_info("Extracting rootfs from archive...")
245245
metadata = install_from_local_file(tmp_archive, rootfs_dir, dist_arch)
246246
else:

proot_distro/helpers/docker/layers.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,13 @@
3232
from proot_distro.progress import clear_bar, draw_bytes_bar
3333
from proot_distro.helpers.docker.cache import layer_cache_path
3434
from proot_distro.helpers.docker.transport import (
35-
auth_opener, registry_base_url, _ua,
35+
opener, _ua,
3636
)
3737
from proot_distro.helpers.tar_extract import extract_tar_to_rootfs
3838

3939

4040
def download_blob(
41-
repo: str, digest: str, token: str, registry: str = "",
41+
repo: str, digest: str, token: str, base: str,
4242
insecure: bool = False,
4343
) -> str:
4444
"""Download a blob to the layer cache; return the local file path.
@@ -60,7 +60,6 @@ def download_blob(
6060
f"is supported)."
6161
)
6262

63-
base = registry_base_url(registry, insecure)
6463
url = f"{base}/v2/{repo}/blobs/{digest}"
6564
headers = {**_ua()}
6665
if token:
@@ -70,7 +69,7 @@ def download_blob(
7069

7170
try:
7271
with atomic_replace(dest) as tmp:
73-
with auth_opener().open(req) as resp, open(tmp, "wb") as fh:
72+
with opener(insecure).open(req) as resp, open(tmp, "wb") as fh:
7473
total = int(resp.headers.get("Content-Length", 0))
7574
downloaded = 0
7675
while True:

proot_distro/helpers/docker/pull.py

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,8 @@
5454
from proot_distro.helpers.docker.transport import (
5555
auth_denied_msg,
5656
auth_note,
57-
auth_opener,
5857
get_auth_token,
59-
registry_base_url,
58+
opener,
6059
_ua,
6160
)
6261

@@ -76,16 +75,15 @@
7675

7776

7877
def _get_manifest(
79-
repo: str, ref: str, token: str, registry: str = "",
78+
repo: str, ref: str, token: str, base: str,
8079
insecure: bool = False,
8180
) -> dict:
82-
base = registry_base_url(registry, insecure)
8381
url = f"{base}/v2/{repo}/manifests/{ref}"
8482
headers = {**_ua(), "Accept": _ACCEPT_HEADER}
8583
if token:
8684
headers["Authorization"] = f"Bearer {token}"
8785
req = urllib.request.Request(url, headers=headers)
88-
with urllib.request.urlopen(req) as resp:
86+
with opener(insecure).open(req) as resp:
8987
body = resp.read()
9088
ct = resp.headers.get("Content-Type", "")
9189
data = json.loads(body)
@@ -134,14 +132,14 @@ def _pick_platform(
134132
def _resolve_single_manifest(
135133
image_ref: str, arch: str, insecure: bool = False
136134
) -> tuple:
137-
"""Return (single_image_manifest, token, repo, registry) for the arch."""
135+
"""Return (single_image_manifest, token, repo, base) for the arch."""
138136
registry, repo, tag = parse_image_ref(image_ref)
139137

140138
log_info(f"Authenticating with registry{auth_note()}...")
141-
token = get_auth_token(repo, registry, insecure=insecure)
139+
token, base = get_auth_token(repo, registry, insecure=insecure)
142140

143141
log_info(f"Fetching manifest for '{image_ref}'...")
144-
manifest = _get_manifest(repo, tag, token, registry, insecure)
142+
manifest = _get_manifest(repo, tag, token, base, insecure)
145143

146144
if manifest["_ct"] in _MANIFEST_LIST_TYPES or "manifests" in manifest:
147145
docker_arch, docker_variant = ARCH_TO_DOCKER.get(arch, (arch, ""))
@@ -153,27 +151,26 @@ def _resolve_single_manifest(
153151
)
154152
log_info(f"Fetching {arch} manifest...")
155153
manifest = _get_manifest(
156-
repo, target["digest"], token, registry, insecure
154+
repo, target["digest"], token, base, insecure
157155
)
158156

159-
return manifest, token, repo, registry
157+
return manifest, token, repo, base
160158

161159

162160
def _fetch_config_blob(
163-
repo: str, cfg_digest: str, token: str, registry: str = "",
161+
repo: str, cfg_digest: str, token: str, base: str,
164162
insecure: bool = False,
165163
) -> dict:
166164
"""Fetch the image config blob; return parsed dict (empty on error)."""
167165
if not cfg_digest:
168166
return {}
169167
try:
170-
base = registry_base_url(registry, insecure)
171168
url = f"{base}/v2/{repo}/blobs/{cfg_digest}"
172169
headers = {**_ua()}
173170
if token:
174171
headers["Authorization"] = f"Bearer {token}"
175172
req = urllib.request.Request(url, headers=headers)
176-
with auth_opener().open(req) as resp:
173+
with opener(insecure).open(req) as resp:
177174
return json.loads(resp.read())
178175
except Exception:
179176
return {}
@@ -189,16 +186,19 @@ def pull_image(
189186
access. If the manifest is cached but some layers are missing, only
190187
an auth token is fetched before downloading the missing layers.
191188
192-
Registry traffic uses HTTPS unless *insecure* is set, in which case a
193-
custom registry is contacted over plain HTTP (Docker Hub stays HTTPS
194-
regardless). With HTTPS enforced, an HTTP-only registry surfaces a
195-
RuntimeError that points the user at ``--allow-insecure``.
189+
Registry traffic uses verified HTTPS unless *insecure* is set. With
190+
*insecure* a custom registry is reached over HTTPS with certificate
191+
verification disabled, falling back to plain HTTP when the registry only
192+
speaks HTTP (Docker Hub stays verified-HTTPS regardless). When enforcing
193+
HTTPS, an untrusted certificate or an HTTP-only registry surfaces a
194+
RuntimeError pointing the user at ``--allow-insecure``.
196195
197196
Returns ``{"manifest": ..., "image_config": ...}``. The caller is
198197
expected to persist these into ``containers/<name>/manifest.json``
199198
so `run`, `reset`, and `login` can later read image_config.
200199
"""
201200
token = None
201+
base = None
202202

203203
manifest, repo, image_config = load_manifest_cache(image_ref, arch)
204204
registry = parse_image_ref(image_ref)[0]
@@ -216,7 +216,9 @@ def pull_image(
216216
f"layer(s) for '{image_ref}' ({arch})...")
217217
try:
218218
log_info(f"Authenticating with registry{auth_note()}...")
219-
token = get_auth_token(repo, registry, insecure=insecure)
219+
token, base = get_auth_token(
220+
repo, registry, insecure=insecure
221+
)
220222
except (urllib.error.URLError, OSError) as net_err:
221223
if isinstance(net_err, urllib.error.HTTPError):
222224
if net_err.code in (401, 403):
@@ -234,7 +236,7 @@ def pull_image(
234236
raise RuntimeError(f"Network error: {net_err}") from net_err
235237
else:
236238
try:
237-
manifest, token, repo, registry = _resolve_single_manifest(
239+
manifest, token, repo, base = _resolve_single_manifest(
238240
image_ref, arch, insecure
239241
)
240242
except (urllib.error.URLError, OSError) as net_err:
@@ -252,7 +254,7 @@ def pull_image(
252254
raise RuntimeError(f"Network error: {net_err}") from net_err
253255
cfg_digest = manifest.get("config", {}).get("digest", "")
254256
image_config = _fetch_config_blob(
255-
repo, cfg_digest, token, registry, insecure
257+
repo, cfg_digest, token, base, insecure
256258
)
257259
save_manifest_cache(image_ref, arch, manifest, repo, image_config)
258260

@@ -286,7 +288,7 @@ def pull_image(
286288
f"{i + 1}/{n_layers}{size_str}...")
287289
try:
288290
layer_path = download_blob(
289-
repo, digest, token or "", registry, insecure
291+
repo, digest, token or "", base, insecure
290292
)
291293
except urllib.error.HTTPError as dl_err:
292294
if dl_err.code in (401, 403):

proot_distro/helpers/docker/push.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,8 @@ def push_image(image_ref: str, arch: str) -> dict:
301301

302302
log_info(f"Authenticating with registry{auth_note()}...")
303303
try:
304-
token = get_auth_token(repo, registry, actions="pull,push")
304+
# push always uses verified HTTPS; the resolved base is ignored.
305+
token, _base = get_auth_token(repo, registry, actions="pull,push")
305306
except urllib.error.HTTPError as exc:
306307
if exc.code in (401, 403):
307308
raise RuntimeError(push_denied_msg(image_ref, exc.code)) from exc

0 commit comments

Comments
 (0)