From 0ec4f54c9f39f01f806f47b42b577b164e7b3097 Mon Sep 17 00:00:00 2001 From: Chongkai Zhu Date: Tue, 10 Feb 2026 09:29:53 +0800 Subject: [PATCH 1/4] Support /etc/pki/tls cert store for older RHEL/Fedora systems Look for /etc/pki/tls/cert.pem when /etc/ssl/cert.pem is not available on RHEL 8 and Fedora 33 and below. Fixes #858, #259 --- cpython-unix/build-cpython.sh | 5 ++ .../patch-cpython-redhat-cert-file.patch | 52 +++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 cpython-unix/patch-cpython-redhat-cert-file.patch diff --git a/cpython-unix/build-cpython.sh b/cpython-unix/build-cpython.sh index a95e3adfb..e2bccf3e6 100755 --- a/cpython-unix/build-cpython.sh +++ b/cpython-unix/build-cpython.sh @@ -311,6 +311,11 @@ if [ "${PYTHON_MAJMIN_VERSION}" = 3.12 ] || [ "${PYTHON_MAJMIN_VERSION}" = 3.13 patch -p1 -i ${ROOT}/patch-test-embed-prevent-segfault.patch fi +# RHEL 8 (supported until 2029) and below, including Fedora 33 and below, do not +# ship an /etc/ssl/cert.pem or a hashed /etc/ssl/cert/ directory. Patch to look at +# /etc/pki/tls/cert.pem instead, if that file exists and /etc/ssl/cert.pem does not. +patch -p1 -i ${ROOT}/patch-cpython-redhat-cert-file.patch + # Cherry-pick an upstream change in Python 3.15 to build _asyncio as # static (which we do anyway in our own fashion) and more importantly to # take this into account when finding the AsyncioDebug section. diff --git a/cpython-unix/patch-cpython-redhat-cert-file.patch b/cpython-unix/patch-cpython-redhat-cert-file.patch new file mode 100644 index 000000000..1b0ff479e --- /dev/null +++ b/cpython-unix/patch-cpython-redhat-cert-file.patch @@ -0,0 +1,52 @@ +diff --git a/Lib/ssl.py b/Lib/ssl.py +index 42ebb8ed384..e35b056668b 100644 +--- a/Lib/ssl.py ++++ b/Lib/ssl.py +@@ -709,7 +709,24 @@ def create_default_context(purpose=Purpose.SERVER_AUTH, *, cafile=None, + # no explicit cafile, capath or cadata but the verify mode is + # CERT_OPTIONAL or CERT_REQUIRED. Let's try to load default system + # root CA certificates for the given purpose. This may fail silently. +- context.load_default_certs(purpose) ++ # ++ # First, try the paths reported by get_default_verify_paths(), ++ # which may include patched paths (e.g., /etc/pki/tls/cert.pem ++ # for RHEL/Fedora). OpenSSL's SSL_CTX_set_default_verify_paths() ++ # only uses its own compile-time defaults, so we must explicitly ++ # load these if they differ. ++ _def_paths = get_default_verify_paths() ++ _cafile = _def_paths.cafile # actual resolved file path ++ _capath = _def_paths.capath # actual resolved directory path ++ _loaded = False ++ if _cafile and os.path.isfile(_cafile): ++ context.load_verify_locations(cafile=_cafile) ++ _loaded = True ++ if _capath and os.path.isdir(_capath): ++ context.load_verify_locations(capath=_capath) ++ _loaded = True ++ if not _loaded: ++ context.load_default_certs(purpose) + # OpenSSL 1.1.1 keylog file + if hasattr(context, 'keylog_filename'): + keylogfile = os.environ.get('SSLKEYLOGFILE') +diff --git a/Modules/_ssl.c b/Modules/_ssl.c +index 0b8cf0b6df3..d2df96fff15 100644 +--- a/Modules/_ssl.c ++++ b/Modules/_ssl.c +@@ -5253,7 +5253,16 @@ _ssl_get_default_verify_paths_impl(PyObject *module) + } + + CONVERT(X509_get_default_cert_file_env(), ofile_env); +- CONVERT(X509_get_default_cert_file(), ofile); ++ ++ /* Check for certificate file path, with fallback for RHEL/Fedora */ ++ const char *cert_file = X509_get_default_cert_file(); ++ struct stat st; ++ ++ /* If the default cert file doesn't exist, try RHEL/Fedora location */ ++ if (!(cert_file != NULL && stat(cert_file, &st) == 0) && (stat("/etc/pki/tls/cert.pem", &st) == 0)) ++ cert_file = "/etc/pki/tls/cert.pem"; ++ ++ CONVERT(cert_file, ofile); + CONVERT(X509_get_default_cert_dir_env(), odir_env); + CONVERT(X509_get_default_cert_dir(), odir); + #undef CONVERT From 53f3a8af5dbe1effaf72aca51ba5f9435511cfc7 Mon Sep 17 00:00:00 2001 From: Chongkai Zhu Date: Tue, 24 Feb 2026 11:55:55 +0800 Subject: [PATCH 2/4] patch SSLContext.load_default_certs instead --- .../patch-cpython-redhat-cert-file.patch | 68 ++++++------------- 1 file changed, 20 insertions(+), 48 deletions(-) diff --git a/cpython-unix/patch-cpython-redhat-cert-file.patch b/cpython-unix/patch-cpython-redhat-cert-file.patch index 1b0ff479e..0ed5503ba 100644 --- a/cpython-unix/patch-cpython-redhat-cert-file.patch +++ b/cpython-unix/patch-cpython-redhat-cert-file.patch @@ -1,52 +1,24 @@ diff --git a/Lib/ssl.py b/Lib/ssl.py -index 42ebb8ed384..e35b056668b 100644 +index 42ebb8ed384..2cf7e64e18e 100644 --- a/Lib/ssl.py +++ b/Lib/ssl.py -@@ -709,7 +709,24 @@ def create_default_context(purpose=Purpose.SERVER_AUTH, *, cafile=None, - # no explicit cafile, capath or cadata but the verify mode is - # CERT_OPTIONAL or CERT_REQUIRED. Let's try to load default system - # root CA certificates for the given purpose. This may fail silently. -- context.load_default_certs(purpose) -+ # -+ # First, try the paths reported by get_default_verify_paths(), -+ # which may include patched paths (e.g., /etc/pki/tls/cert.pem -+ # for RHEL/Fedora). OpenSSL's SSL_CTX_set_default_verify_paths() -+ # only uses its own compile-time defaults, so we must explicitly -+ # load these if they differ. -+ _def_paths = get_default_verify_paths() -+ _cafile = _def_paths.cafile # actual resolved file path -+ _capath = _def_paths.capath # actual resolved directory path -+ _loaded = False -+ if _cafile and os.path.isfile(_cafile): -+ context.load_verify_locations(cafile=_cafile) -+ _loaded = True -+ if _capath and os.path.isdir(_capath): -+ context.load_verify_locations(capath=_capath) -+ _loaded = True -+ if not _loaded: -+ context.load_default_certs(purpose) - # OpenSSL 1.1.1 keylog file - if hasattr(context, 'keylog_filename'): - keylogfile = os.environ.get('SSLKEYLOGFILE') -diff --git a/Modules/_ssl.c b/Modules/_ssl.c -index 0b8cf0b6df3..d2df96fff15 100644 ---- a/Modules/_ssl.c -+++ b/Modules/_ssl.c -@@ -5253,7 +5253,16 @@ _ssl_get_default_verify_paths_impl(PyObject *module) - } +@@ -423,6 +423,7 @@ class SSLContext(_SSLContext): + """An SSLContext holds various SSL-related configuration options and + data, such as certificates and possibly a private key.""" + _windows_cert_stores = ("CA", "ROOT") ++ _FALLBACK_CERT_FILE = "/etc/pki/tls/cert.pem" # RHEL 8 and below, Fedora 33 and below - CONVERT(X509_get_default_cert_file_env(), ofile_env); -- CONVERT(X509_get_default_cert_file(), ofile); -+ -+ /* Check for certificate file path, with fallback for RHEL/Fedora */ -+ const char *cert_file = X509_get_default_cert_file(); -+ struct stat st; -+ -+ /* If the default cert file doesn't exist, try RHEL/Fedora location */ -+ if (!(cert_file != NULL && stat(cert_file, &st) == 0) && (stat("/etc/pki/tls/cert.pem", &st) == 0)) -+ cert_file = "/etc/pki/tls/cert.pem"; -+ -+ CONVERT(cert_file, ofile); - CONVERT(X509_get_default_cert_dir_env(), odir_env); - CONVERT(X509_get_default_cert_dir(), odir); - #undef CONVERT + sslsocket_class = None # SSLSocket is assigned later. + sslobject_class = None # SSLObject is assigned later. +@@ -531,6 +532,11 @@ def load_default_certs(self, purpose=Purpose.SERVER_AUTH): + if sys.platform == "win32": + for storename in self._windows_cert_stores: + self._load_windows_store_certs(storename, purpose) ++ elif sys.platform == "linux": ++ _def_paths = _ssl.get_default_verify_paths() ++ openssl_cafile = os.environ.get(_def_paths[0], _def_paths[1]) ++ if not os.path.isfile(openssl_cafile) and os.path.isfile(self._FALLBACK_CERT_FILE): ++ self.load_verify_locations(cafile=self._FALLBACK_CERT_FILE) + self.set_default_verify_paths() + + if hasattr(_SSLContext, 'minimum_version'): From bc437699afe3afc20703c7c7c23c3b381cf94432 Mon Sep 17 00:00:00 2001 From: Chongkai Zhu Date: Fri, 27 Feb 2026 06:06:29 +0800 Subject: [PATCH 3/4] deal with setting SSL_CERT_FILE to a path that does not exist --- cpython-unix/patch-cpython-redhat-cert-file.patch | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cpython-unix/patch-cpython-redhat-cert-file.patch b/cpython-unix/patch-cpython-redhat-cert-file.patch index 0ed5503ba..7d95a36c5 100644 --- a/cpython-unix/patch-cpython-redhat-cert-file.patch +++ b/cpython-unix/patch-cpython-redhat-cert-file.patch @@ -1,5 +1,5 @@ diff --git a/Lib/ssl.py b/Lib/ssl.py -index 42ebb8ed384..2cf7e64e18e 100644 +index 42ebb8ed384..5cf2575ecd8 100644 --- a/Lib/ssl.py +++ b/Lib/ssl.py @@ -423,6 +423,7 @@ class SSLContext(_SSLContext): @@ -10,14 +10,15 @@ index 42ebb8ed384..2cf7e64e18e 100644 sslsocket_class = None # SSLSocket is assigned later. sslobject_class = None # SSLObject is assigned later. -@@ -531,6 +532,11 @@ def load_default_certs(self, purpose=Purpose.SERVER_AUTH): +@@ -531,6 +532,12 @@ def load_default_certs(self, purpose=Purpose.SERVER_AUTH): if sys.platform == "win32": for storename in self._windows_cert_stores: self._load_windows_store_certs(storename, purpose) + elif sys.platform == "linux": + _def_paths = _ssl.get_default_verify_paths() -+ openssl_cafile = os.environ.get(_def_paths[0], _def_paths[1]) -+ if not os.path.isfile(openssl_cafile) and os.path.isfile(self._FALLBACK_CERT_FILE): ++ if (_def_paths[0] not in os.environ and ++ not os.path.isfile(_def_paths[1]) and ++ os.path.isfile(self._FALLBACK_CERT_FILE)): + self.load_verify_locations(cafile=self._FALLBACK_CERT_FILE) self.set_default_verify_paths() From 6cc9a48ba5603637a0ff0cda53b83254399c1b52 Mon Sep 17 00:00:00 2001 From: Chongkai Zhu Date: Sat, 7 Mar 2026 08:36:29 +0800 Subject: [PATCH 4/4] process SSL_CERT_DIR as well --- cpython-unix/patch-cpython-redhat-cert-file.patch | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/cpython-unix/patch-cpython-redhat-cert-file.patch b/cpython-unix/patch-cpython-redhat-cert-file.patch index 7d95a36c5..38e1126e4 100644 --- a/cpython-unix/patch-cpython-redhat-cert-file.patch +++ b/cpython-unix/patch-cpython-redhat-cert-file.patch @@ -1,16 +1,17 @@ diff --git a/Lib/ssl.py b/Lib/ssl.py -index 42ebb8ed384..5cf2575ecd8 100644 +index 42ebb8ed384..c15c0ec940f 100644 --- a/Lib/ssl.py +++ b/Lib/ssl.py -@@ -423,6 +423,7 @@ class SSLContext(_SSLContext): +@@ -423,6 +423,8 @@ class SSLContext(_SSLContext): """An SSLContext holds various SSL-related configuration options and data, such as certificates and possibly a private key.""" _windows_cert_stores = ("CA", "ROOT") + _FALLBACK_CERT_FILE = "/etc/pki/tls/cert.pem" # RHEL 8 and below, Fedora 33 and below ++ _FALLBACK_CERT_DIR = "/etc/pki/tls/certs" # RHEL 8 and below, Fedora 33 and below sslsocket_class = None # SSLSocket is assigned later. sslobject_class = None # SSLObject is assigned later. -@@ -531,6 +532,12 @@ def load_default_certs(self, purpose=Purpose.SERVER_AUTH): +@@ -531,6 +533,16 @@ def load_default_certs(self, purpose=Purpose.SERVER_AUTH): if sys.platform == "win32": for storename in self._windows_cert_stores: self._load_windows_store_certs(storename, purpose) @@ -20,6 +21,10 @@ index 42ebb8ed384..5cf2575ecd8 100644 + not os.path.isfile(_def_paths[1]) and + os.path.isfile(self._FALLBACK_CERT_FILE)): + self.load_verify_locations(cafile=self._FALLBACK_CERT_FILE) ++ if (_def_paths[2] not in os.environ and ++ not os.path.isdir(_def_paths[3]) and ++ os.path.isdir(self._FALLBACK_CERT_DIR)): ++ self.load_verify_locations(capath=self._FALLBACK_CERT_DIR) self.set_default_verify_paths() if hasattr(_SSLContext, 'minimum_version'):