Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Include/patchlevel.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions Lib/ensurepip/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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():
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added Lib/test/distrusted.zip
Binary file not shown.
48 changes: 48 additions & 0 deletions Lib/test/test_ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
import errno
import pprint
import shutil
import tempfile
import urllib2
import zipfile
import traceback
import weakref
import platform
Expand Down Expand Up @@ -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())

Expand Down Expand Up @@ -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")
Expand Down
38 changes: 38 additions & 0 deletions Misc/NEWS.d/2.7.18.15.rst
Original file line number Diff line number Diff line change
@@ -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.
75 changes: 71 additions & 4 deletions Modules/_ssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}

Expand Down
75 changes: 75 additions & 0 deletions Modules/_ssl_distrusted_spki.h
Original file line number Diff line number Diff line change
@@ -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 */
3 changes: 3 additions & 0 deletions PCbuild/_ssl.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@
<AdditionalDependencies>ws2_32.lib;crypt32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="..\Modules\_ssl_distrusted_spki.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\Modules\_ssl.c" />
<ClCompile Include="$(opensslIncludeDir)\applink.c">
Expand Down
8 changes: 8 additions & 0 deletions PCbuild/_ssl.vcxproj.filters
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,18 @@
<Filter Include="Source Files">
<UniqueIdentifier>{695348f7-e9f6-4fe1-bc03-5f08ffc8095b}</UniqueIdentifier>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{7c1bd5da-8912-4107-b6ac-f3930f2d90c7}</UniqueIdentifier>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{1b18a2e6-040d-46c7-a9ac-ac2ec64fb5d6}</UniqueIdentifier>
</Filter>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\Modules\_ssl_distrusted_spki.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\Modules\_ssl.c">
<Filter>Source Files</Filter>
Expand Down
1 change: 1 addition & 0 deletions Tools/msi/msi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down