If using the ssl
Python module (e.g. as part of makingconnections to https://
URLs), Python in its default configurationwill want to obtain a list of trusted X.509 / SSL certificates forverifying connections.
If a list of trusted certificates cannot be found, you may encountererrors like ssl.SSLCertVerificationError: [SSL:CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to getlocal issuer certificate
.
How Python Looks for Certificates¶
By default, Python will likely call ssl.SSLContext.load_default_certs()
to load the default certificates.
On Windows, Python automatically loads certificates from the Windowscertificate store. This should just work with PyOxidizer.
On all platforms, Python attempts to load certificates from the defaultlocations compiled into the OpenSSL library that is being used. WithPyOxidizer, the OpenSSL (or LibreSSL) library is part of the Pythondistribution used to produce a binary.
The OpenSSL library hard codes default certificate search paths.For PyOxidizer’s Python distributions, the paths are:
(Windows)
C:\Program Files\Common Files\SSL\cert.pem
(file) andC:\Program Files\Common Files\SSL\certs
(directory).(non-Windows)
/etc/ssl/cert.pem
(file) and/etc/ssl/certs
(directory).
In addition, OpenSSL (but not LibreSSL) will look for path overridesin the SSL_CERT_FILE
and SSL_CERT_DIR
environment variables.
You can verify all of this behavior by callingssl.get_default_verify_paths()
:
$ python3.9Python 3.9.5 (default, Apr 16 2021, 08:56:35)[GCC 10.2.0] on linuxType "help", "copyright", "credits" or "license" for more information.>>> import ssl>>> ssl.get_default_verify_paths()DefaultVerifyPaths(cafile=None, capath='/etc/ssl/certs', openssl_cafile_env='SSL_CERT_FILE', openssl_cafile='/etc/ssl/cert.pem', openssl_capath_env='SSL_CERT_DIR', openssl_capath='/etc/ssl/certs')
On macOS, /etc/ssl
should exist, as it is part of the standard macOSinstall. So OpenSSL / Python should find certificates automatically.
On Windows, the default certificate path won’t exist unless somethingthat isn’t PyOxidizer materializes the aforementioned files/directories.However, since Python loads certificates from the Windows certificatestore automatically, OpenSSL / Python should be able to load certificatesfrom PyOxidizer applications without issue.
On Linux, things are more complicated. The /etc/ssl
directory iscommon, but not ubiquitous. This directory likely exists on all Debianbased distributions, like Ubuntu. If the directory does not exist, OpenSSL /Python will likely fail to find certificates and summarily fail to verifyconnections against them.
Using Alternative Certificate Paths¶
PyOxidizer doesn’t yet have a built-in mechanism for automaticallyregistering additional certificates or certificate paths at run-time.Therefore, if OpenSSL / Python is unable to locate certificates, youwill need to add custom logic to your application to have it look foradditional certificates.
Certifi¶
The certifi Python package providesaccess to a copy of Mozilla’s trusted certificates list. Using certifi
enables you to have access to a known trusted certificates list withoutdependence on certificates present in the run-time environment / operatingsystem.
Because certifi
and its certificate list is distributed with yourapplication, it is guaranteed to be present and certificate loading shouldjust work.
To use certifi
with PyOxidizer, you can install it as an additionalpackage. From your Starlark configuration file:
def make_exe(): dist = default_python_distribution() exe = dist.to_python_executable(name="myapp") # Check for newer versions at https://pypi.org/project/certifi/. exe.add_python_resources(exe.pip_install(["certifi==2020.12.5"])) return exe
Then from your application’s Python code:
import certifiimport ssl# Obtain a default ssl.SSLContext but with certifi's certificate data loaded.ctx = ssl.create_default_context(cadata=certifi.contents())# Or if you already have an ssl.SSLContext instance and want to load# certifi's data in it:ctx.load_verify_locations(cadata=certifi.contents())# Various APIs that create connections also accept a `cadata` argument.# Under the hood they pass this argument to construct the ssl.SSLContext.# e.g. urllib.request.urlopen().import urllib.requesturllib.request.urlopen(url, cadata=certifi.contents())
Manually Specifying Paths to Certificates¶
If you know the paths to certificates to use, you can specify thosepaths via various ssl
APIs, often through the cafile
andcapath
arguments. e.g.
import sslctx = ssl.create_default_context(capath="/path/to/ssl/certs")import urllib.requesturllib.request.urlopen(url, capath="/path/to/ssl/certs")
Using Environment Variables¶
OpenSSL (but not LibreSSL) will look for the SSL_CERT_FILE
andSSL_CERT_DIR
environment variables to automatically set the CAfile and directory, respectively.
You can set these within your process to point to alternative paths. e.g.
import osos.environ["SSL_CERT_DIR"] = "/path/to/ssl/certs"