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')