diff --git a/Include/patchlevel.h b/Include/patchlevel.h index bd7ff936aa8a452..2a6d2f141b71328 100644 --- a/Include/patchlevel.h +++ b/Include/patchlevel.h @@ -27,7 +27,7 @@ #define PY_RELEASE_SERIAL 0 /* Version as a string */ -#define PY_VERSION "2.7.18.14" +#define PY_VERSION "2.7.18.15" /*--end constants--*/ /* Subversion Revision number of this file (not of the repository). Empty diff --git a/Lib/ensurepip/__init__.py b/Lib/ensurepip/__init__.py index 0955141184782fb..df740d0d2daf3c2 100644 --- a/Lib/ensurepip/__init__.py +++ b/Lib/ensurepip/__init__.py @@ -12,9 +12,9 @@ __all__ = ["version", "bootstrap"] -_SETUPTOOLS_VERSION = "41.2.0" +_SETUPTOOLS_VERSION = "44.1.1.post3" -_PIP_VERSION = "19.2.3" +_PIP_VERSION = "20.3.4.post4" _PROJECTS = [ ("setuptools", _SETUPTOOLS_VERSION), @@ -28,8 +28,8 @@ def _run_pip(args, additional_paths=None): sys.path = additional_paths + sys.path # Install the bundled software - import pip._internal - return pip._internal.main(args) + from pip._internal.cli.main import main as _main + return _main(args) def version(): diff --git a/Lib/ensurepip/_bundled/pip-19.2.3-py2.py3-none-any.whl b/Lib/ensurepip/_bundled/pip-19.2.3-py2.py3-none-any.whl deleted file mode 100644 index 8118df8ac1940f8..000000000000000 Binary files a/Lib/ensurepip/_bundled/pip-19.2.3-py2.py3-none-any.whl and /dev/null differ diff --git a/Lib/ensurepip/_bundled/pip-20.3.4.post4-py2.py3-none-any.whl b/Lib/ensurepip/_bundled/pip-20.3.4.post4-py2.py3-none-any.whl new file mode 100644 index 000000000000000..fc2943e9ec54635 Binary files /dev/null and b/Lib/ensurepip/_bundled/pip-20.3.4.post4-py2.py3-none-any.whl differ diff --git a/Lib/ensurepip/_bundled/setuptools-41.2.0-py2.py3-none-any.whl b/Lib/ensurepip/_bundled/setuptools-44.1.1.post3-py2.py3-none-any.whl similarity index 68% rename from Lib/ensurepip/_bundled/setuptools-41.2.0-py2.py3-none-any.whl rename to Lib/ensurepip/_bundled/setuptools-44.1.1.post3-py2.py3-none-any.whl index 82df6f63f4ee973..e19f0c3903ae06b 100644 Binary files a/Lib/ensurepip/_bundled/setuptools-41.2.0-py2.py3-none-any.whl and b/Lib/ensurepip/_bundled/setuptools-44.1.1.post3-py2.py3-none-any.whl differ diff --git a/Lib/test/distrusted.zip b/Lib/test/distrusted.zip new file mode 100644 index 000000000000000..83184329a5ab9e0 Binary files /dev/null and b/Lib/test/distrusted.zip differ diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index bab68808f7273bf..2381baa847cd7bd 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -20,7 +20,9 @@ import errno import pprint import shutil +import tempfile import urllib2 +import zipfile import traceback import weakref import platform @@ -80,6 +82,19 @@ def data_file(*name): NULLBYTECERT = data_file("nullbytecert.pem") TALOS_INVALID_CRLDP = data_file("talos-2019-0758.pem") +# gh-160040: a leaf signed by a compromised eMudhra intermediate whose public +# key is on the _ssl distrusted list. The chain to distrusted_root.pem is +# otherwise valid. +# The PEMs live in a password-protected zip so the compromised eMudhra certs +# don't trip security scanners that cannot tell internal test fixtures from +# production secrets and flag any certificate or private key they find. It is +# encrypted, not merely zipped, because such scanners read inside plain archives. +# The password is fixed, public, and not a secret; it only keeps the archive +# from storing the PEMs in the clear. +DISTRUSTED_SPKI_ZIP = data_file("distrusted.zip") +DISTRUSTED_SPKI_CHOSEN_KNIGHT = b"Brave Sir Robin" # not a secret, see comment +DISTRUSTED_SPKI_HOSTNAME = "localhost" + DHFILE = data_file("ffdh3072.pem") BYTES_DHFILE = DHFILE.encode(sys.getfilesystemencoding()) @@ -2279,6 +2294,39 @@ def test_crl_check(self): cert = s.getpeercert() self.assertTrue(cert, "Can't get peer certificate.") + def test_distrusted_spki(self): + # gh-160040: the server chain is otherwise valid, but the eMudhra + # intermediate's public key is on the _ssl distrusted list, so + # verification rejects it. + client_context = ssl.SSLContext(ssl.PROTOCOL_TLS) + client_context.verify_mode = ssl.CERT_REQUIRED + client_context.check_hostname = True + server_context = ssl.SSLContext(ssl.PROTOCOL_TLS) + # the contexts load the PEMs now, so the files are only needed here + tmpdir = tempfile.mkdtemp() + try: + with closing(zipfile.ZipFile(DISTRUSTED_SPKI_ZIP)) as z: + z.extractall(tmpdir, pwd=DISTRUSTED_SPKI_CHOSEN_KNIGHT) + certdir = os.path.join(tmpdir, "distrusted") + client_context.load_verify_locations( + os.path.join(certdir, "distrusted_root.pem")) + server_context.load_cert_chain( + os.path.join(certdir, "distrusted_keycert.pem")) + finally: + shutil.rmtree(tmpdir) + + server = ThreadedEchoServer(context=server_context, chatty=False) + with server: + with closing(client_context.wrap_socket( + socket.socket(), + server_hostname=DISTRUSTED_SPKI_HOSTNAME)) as s: + # 2.7 has no SSLCertVerificationError/verify_code; the + # X509_V_ERR_CERT_REJECTED reason only reaches the + # message on OpenSSL 1.1.0+. + with self.assertRaisesRegexp(ssl.SSLError, + "certificate verify failed"): + s.connect((HOST, server.port)) + def test_check_hostname(self): if support.verbose: sys.stdout.write("\n") diff --git a/Misc/NEWS.d/2.7.18.15.rst b/Misc/NEWS.d/2.7.18.15.rst new file mode 100644 index 000000000000000..851d0e9e1117527 --- /dev/null +++ b/Misc/NEWS.d/2.7.18.15.rst @@ -0,0 +1,38 @@ +.. bpo: 0 +.. date: 2026-10-27 +.. nonce: aS$mUd +.. release date: 2026-10-27 +.. original section: Library +.. section: Security + +Address CVE-2026-115557 (compromised eMudhra intermediate CA keys) in ssl + +The :mod:`ssl` module now rejects TLS certificate chains that contain any of +a set of compromised eMudhra intermediate CA keys (gh-160040, backported from +the upstream 3.11 security release). The denylist matches the SHA-256 digest +of each certificate's SubjectPublicKeyInfo, so it also covers cross-signed +certificates that reuse a distrusted key. A matching certificate fails +verification with ``X509_V_ERR_CERT_REJECTED`` ("certificate verify +failed"). The check runs whenever :attr:`SSLContext.verify_mode` is +``CERT_OPTIONAL`` or ``CERT_REQUIRED``. This is a last line of defense while +operating-system and bundled trust stores drop the eMudhra/emSign roots. + +.. bpo: 0 +.. date: 2026-10-27 +.. nonce: aS$mUd +.. release date: 2026-10-27 +.. original section: Library +.. section: Security + +Upgrade the pip and setuptools bundled with ensurepip + +:mod:`ensurepip` now bundles pip 20.3.4.post4 and setuptools 44.1.1.post3, +the ActiveState security releases of the last Python 2.7-compatible pip and +setuptools lines, in place of pip 19.2.3 and setuptools 41.2.0. pip's +vendored certifi is 2026.10.23.post1, ActiveState's Python 2.7 build of +certifi 2026.10.23, which removes the emSign ECC Root CA - G3, emSign Root +CA - C1 and emSign ECC Root CA - C3 roots; emSign Root CA - G1 is the only +emSign root left. pip's connections go through the :mod:`ssl` module, so the +eMudhra SPKI denylist above also applies to them (CVE-2026-115557). +setuptools 44.1.1.post3 includes the fixes for CVE-2022-40897, +CVE-2024-6345, CVE-2025-47273 and CVE-2026-59890. diff --git a/Modules/_ssl.c b/Modules/_ssl.c index e28f02d128d94a6..0df84021d3814a6 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -68,6 +68,7 @@ /* Include OpenSSL header files */ #include "openssl/rsa.h" #include "openssl/crypto.h" +#include "openssl/evp.h" #include "openssl/x509.h" #include "openssl/x509v3.h" #include "openssl/pem.h" @@ -2600,11 +2601,79 @@ get_verify_mode(PySSLContext *self, void *c) return NULL; } +#include "_ssl_distrusted_spki.h" + +/* Returns 1 if the SHA-256 of the cert's SPKI is on the distrusted list, + 0 if not (okay), -1 on an internal error. */ +static int +_cert_has_distrusted_spki(X509 *cert) +{ + unsigned char *spki = NULL; + unsigned char digest[EVP_MAX_MD_SIZE]; + unsigned int digest_len = 0; + int spki_len, ok; + size_t i; + + spki_len = i2d_X509_PUBKEY(X509_get_X509_PUBKEY(cert), &spki); + if (spki_len <= 0 || spki == NULL) { + return -1; + } + + ok = EVP_Digest(spki, (size_t)spki_len, digest, &digest_len, + EVP_sha256(), NULL); + OPENSSL_free(spki); + assert(sizeof(_distrusted_spki[0]) == SHA256_DIGEST_LENGTH); + if (!ok + || digest_len != SHA256_DIGEST_LENGTH + || sizeof(_distrusted_spki[0]) != SHA256_DIGEST_LENGTH) { + return -1; + } + + for (i = 0; i < sizeof(_distrusted_spki) / sizeof(_distrusted_spki[0]); i++) { + if (memcmp(digest, _distrusted_spki[i], SHA256_DIGEST_LENGTH) == 0) { + return 1; + } + } + return 0; +} + +/* Verify callback, called once per cert. Reject distrusted SPKIs. */ +static int +_ssl_verify_callback(int preverify_ok, X509_STORE_CTX *x509_ctx) +{ + X509 *cert; + + /* Don't override an error OpenSSL already reported for this certificate. */ + if (!preverify_ok) { + return preverify_ok; + } + cert = X509_STORE_CTX_get_current_cert(x509_ctx); + if (cert == NULL) { + return preverify_ok; + } + switch (_cert_has_distrusted_spki(cert)) { + case 0: + /* okay: not on the distrusted list */ + return preverify_ok; + case 1: + /* rejected: public key is on the distrusted list */ + X509_STORE_CTX_set_error(x509_ctx, X509_V_ERR_CERT_REJECTED); + return 0; + default: + /* internal error */ +#ifdef X509_V_ERR_UNSPECIFIED + X509_STORE_CTX_set_error(x509_ctx, X509_V_ERR_UNSPECIFIED); +#else + X509_STORE_CTX_set_error(x509_ctx, X509_V_ERR_APPLICATION_VERIFICATION); +#endif + return 0; + } +} + static int set_verify_mode(PySSLContext *self, PyObject *arg, void *c) { int n, mode; - int (*verify_cb)(int, X509_STORE_CTX *) = NULL; if (!PyArg_Parse(arg, "i", &n)) return -1; @@ -2630,9 +2699,7 @@ set_verify_mode(PySSLContext *self, PyObject *arg, void *c) /* bpo-37428: newPySSLSocket() sets SSL_VERIFY_POST_HANDSHAKE flag for * server sockets and SSL_set_post_handshake_auth() for client. */ - /* keep current verify cb */ - verify_cb = SSL_CTX_get_verify_callback(self->ctx); - SSL_CTX_set_verify(self->ctx, mode, verify_cb); + SSL_CTX_set_verify(self->ctx, mode, _ssl_verify_callback); return 0; } diff --git a/Modules/_ssl_distrusted_spki.h b/Modules/_ssl_distrusted_spki.h new file mode 100644 index 000000000000000..bbdb56dcfe671ed --- /dev/null +++ b/Modules/_ssl_distrusted_spki.h @@ -0,0 +1,75 @@ +/* Denylist of distrusted SubjectPublicKeyInfo (SPKI). + * + * Verification rejects any chain with a cert whose SPKI SHA-256 digest is in + * the _distrusted_spki table. We match the key, not a cert fingerprint, so + * cross-signs that reuse the key are blocked too. + * + * This is an extraordinary measure and last line of defense. Removing trust is + * primarily the job of the platform root store. This list only blocks keys while + * that update is still propagating to the OS, container images, or root CAs + * bundled in Python packages. The threshold for adding an entry is very high. + * + * See https://github.com/python/cpython/issues/160040 for the eMudhra discussion. + * + * To block another key, add the SHA-256 digest bytes of its SPKI: + * + * $ openssl x509 -in cert.pem -noout -pubkey | openssl pkey -pubin -outform DER | openssl dgst -sha256 -binary | xxd -i + */ +#ifndef Py_SSL_DISTRUSTED_SPKI_H +#define Py_SSL_DISTRUSTED_SPKI_H + +#include "openssl/sha.h" + +static const unsigned char _distrusted_spki[][SHA256_DIGEST_LENGTH] = { + /* Compromised eMudhra intermediates, https://emudhra.we-have-a-problem.com/ */ + /* CN=EM OV TLS CA - G3B,O=eMudhra BV,C=NL */ + { + 0x43, 0xbf, 0xfa, 0x19, 0xd6, 0xef, 0x65, 0x14, 0x51, 0xf6, 0x84, 0x1e, + 0x01, 0x99, 0x31, 0xe9, 0x6f, 0x6f, 0x2e, 0xae, 0xff, 0x28, 0xab, 0x4b, + 0x6f, 0x1c, 0xe4, 0x57, 0x6d, 0xe2, 0x71, 0x7b, + }, + /* CN=EM DV TLS CA - G3C,O=eMudhra Inc,C=US */ + { + 0x49, 0xdd, 0x35, 0x3d, 0xc2, 0xb9, 0x19, 0xa3, 0x77, 0x06, 0x45, 0x11, + 0x7d, 0x0e, 0x60, 0xc1, 0x61, 0x15, 0xe7, 0xd1, 0xbc, 0x25, 0x9e, 0xef, + 0xe7, 0x34, 0xb0, 0x0e, 0xa4, 0x34, 0xfe, 0x5f, + }, + /* CN=EM EV TLS CA - G3C,O=eMudhra Inc,C=US */ + { + 0x5c, 0x06, 0x60, 0xa2, 0xb1, 0x64, 0x91, 0x0d, 0x8f, 0xf5, 0x89, 0xc1, + 0x8e, 0x14, 0x55, 0x2e, 0x9e, 0x0e, 0xc2, 0x37, 0xdc, 0xef, 0xe2, 0xcb, + 0x73, 0x6d, 0x41, 0xa4, 0x81, 0x1f, 0x81, 0xa0, + }, + /* CN=EM OV TLS CA - G3C,O=eMudhra Inc,C=US */ + { + 0x63, 0xec, 0xef, 0xee, 0x43, 0x1d, 0x49, 0x52, 0x3b, 0xc1, 0x13, 0x6c, + 0x4e, 0x72, 0xd0, 0x20, 0x6a, 0x47, 0x70, 0x86, 0xca, 0xf6, 0x06, 0xcd, + 0x5c, 0xf0, 0x25, 0x68, 0x6c, 0x10, 0x9c, 0xf8, + }, + /* CN=EM OV CS CA - G3C,O=eMudhra Inc,C=US */ + { + 0x92, 0xc0, 0x63, 0xd6, 0x7e, 0x8f, 0x14, 0xdb, 0x71, 0xd2, 0x88, 0xe1, + 0x12, 0x66, 0xff, 0xda, 0xf6, 0x9c, 0xbf, 0x11, 0x5f, 0x7b, 0xab, 0x14, + 0xf3, 0xbc, 0x2f, 0x68, 0xd2, 0x5e, 0x0e, 0x30, + }, + /* CN=EM EV TLS CA - G3B,O=eMudhra BV,C=NL */ + { + 0xe0, 0x25, 0x4c, 0x7f, 0xcd, 0xc2, 0x65, 0xe2, 0xf0, 0xf8, 0xa4, 0x28, + 0xda, 0xbb, 0x75, 0xab, 0x4a, 0xe0, 0x39, 0xb4, 0x87, 0x2d, 0x62, 0xf5, + 0xee, 0x57, 0xf0, 0x87, 0x7d, 0x5a, 0x8a, 0x41, + }, + /* CN=EM DV TLS CA - G3B,O=eMudhra BV,C=NL */ + { + 0xeb, 0xed, 0x64, 0x75, 0xc2, 0xc2, 0xd8, 0x0f, 0xcb, 0x19, 0x9f, 0x3f, + 0xe8, 0xdd, 0x6e, 0x6b, 0x3c, 0xe6, 0x0b, 0x77, 0xeb, 0x15, 0xd4, 0xe4, + 0x2c, 0x3d, 0xec, 0x0c, 0x6f, 0x5b, 0x51, 0x05, + }, + /* CN=EM OV CS CA - G3B,O=eMudhra BV,C=NL */ + { + 0xec, 0x4f, 0xfa, 0x1a, 0xd0, 0xac, 0x88, 0x08, 0x13, 0x14, 0xb7, 0xaa, + 0xff, 0xef, 0x6a, 0x91, 0xf9, 0x7c, 0xd5, 0x72, 0x91, 0xbb, 0x5b, 0xa6, + 0x86, 0xc5, 0x41, 0x5f, 0x5e, 0xb4, 0xf6, 0xb6, + }, +}; + +#endif /* !Py_SSL_DISTRUSTED_SPKI_H */ diff --git a/PCbuild/_ssl.vcxproj b/PCbuild/_ssl.vcxproj index f2c030c3bf1790b..7a72c9df78bdc08 100644 --- a/PCbuild/_ssl.vcxproj +++ b/PCbuild/_ssl.vcxproj @@ -65,6 +65,9 @@ ws2_32.lib;crypt32.lib;%(AdditionalDependencies) + + + diff --git a/PCbuild/_ssl.vcxproj.filters b/PCbuild/_ssl.vcxproj.filters index 7afc386d25a10a2..626699a6530a864 100644 --- a/PCbuild/_ssl.vcxproj.filters +++ b/PCbuild/_ssl.vcxproj.filters @@ -4,10 +4,18 @@ {695348f7-e9f6-4fe1-bc03-5f08ffc8095b} + + {7c1bd5da-8912-4107-b6ac-f3930f2d90c7} + {1b18a2e6-040d-46c7-a9ac-ac2ec64fb5d6} + + + Header Files + + Source Files diff --git a/Tools/msi/msi.py b/Tools/msi/msi.py index da4a1c3d95981b3..4cc82338d4ec929 100644 --- a/Tools/msi/msi.py +++ b/Tools/msi/msi.py @@ -1060,6 +1060,7 @@ def add_files(db): lib.glob("*.pem") lib.glob("*.pck") lib.add_file("zipdir.zip") + lib.add_file("distrusted.zip") if dir=='tests' and parent.physical=='distutils': lib.add_file("Setup.sample") if dir=='audiodata': diff --git a/setup.py b/setup.py index 2e631bb0159e2c5..927572c949cbe78 100644 --- a/setup.py +++ b/setup.py @@ -879,7 +879,8 @@ def detect_modules(self): include_dirs = ssl_incs, library_dirs = ssl_libs, libraries = ['ssl', 'crypto'], - depends = ['socketmodule.h']), ) + depends = ['socketmodule.h', + '_ssl_distrusted_spki.h']), ) else: missing.append('_ssl')