From 96c286af10b477ebdb8fa112f965e9855d3063e7 Mon Sep 17 00:00:00 2001 From: Alan Morris Date: Thu, 22 Oct 2026 17:29:02 -0600 Subject: [PATCH] Trust certifi's roots when downloading use case data (#2676) A fresh Windows install lacks the root that www.sci.utah.edu chains to, and OpenSSL never triggers Windows' on-demand root fetch, so dataset downloads failed with CERTIFICATE_VERIFY_FAILED. Verify against certifi's bundle in addition to the system roots. --- Python/shapeworks/shapeworks/datasets.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/Python/shapeworks/shapeworks/datasets.py b/Python/shapeworks/shapeworks/datasets.py index bad22e363c..41adf1de8c 100644 --- a/Python/shapeworks/shapeworks/datasets.py +++ b/Python/shapeworks/shapeworks/datasets.py @@ -36,6 +36,11 @@ import urllib.request import zipfile +try: + import certifi +except ImportError: + certifi = None + INDEX_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "datasets.json") _CHUNK = 1024 * 256 @@ -51,6 +56,7 @@ _RETRY_STATUS = (408, 425, 429, 500, 502, 503, 504) _index_cache = None +_ssl_context = None _reported = [0.0] @@ -97,6 +103,22 @@ def _progress(name, done, total): sys.stdout.flush() +def _context(): + """TLS context trusting both the system roots and certifi's. + + A fresh Windows install holds only a few roots and fetches the rest on demand, + which happens only when Windows itself verifies a connection. OpenSSL reads the + store as it stands, so it can miss the root our server chains to. certifi + supplies it. + """ + global _ssl_context + if _ssl_context is None: + _ssl_context = ssl.create_default_context() + if certifi is not None: + _ssl_context.load_verify_locations(cafile=certifi.where()) + return _ssl_context + + def _size(path): return os.path.getsize(path) if os.path.exists(path) else 0 @@ -131,7 +153,7 @@ def _stream(url, destination, name, total, tag): # splice two different downloads together. request.add_header("If-Range", tag) - with urllib.request.urlopen(request, timeout=_TIMEOUT) as response: + with urllib.request.urlopen(request, timeout=_TIMEOUT, context=_context()) as response: tag = response.headers.get("ETag") or tag length = int(response.headers.get("Content-Length") or 0) if response.getcode() != 206: