Skip to content

Commit d720f18

Browse files
authored
Merge branch '3.13' into backport-0071fb5-3.13
2 parents 25f598d + 8d4286a commit d720f18

16 files changed

Lines changed: 351 additions & 16 deletions

‎Doc/whatsnew/3.13.rst‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2867,3 +2867,18 @@ tarfile
28672867
:func:`~tarfile.TarFile.errorlevel` is zero.
28682868
(Contributed by Matt Prodani and Petr Viktorin in :gh:`112887`
28692869
and :cve:`2025-4435`.)
2870+
2871+
2872+
Notable changes in 3.13.18
2873+
==========================
2874+
2875+
ssl
2876+
---
2877+
2878+
* The :mod:`ssl` module now rejects TLS certificate chains that contain one
2879+
of a set of compromised eMudhra intermediate CA certificates. The affected
2880+
certificates are identified by their public key, so cross-signed copies that
2881+
reuse the same key are blocked as well. It is a defense-in-depth backstop
2882+
that applies even when an application supplies its own root CA bundle, and
2883+
regardless of whether the platform trust store has removed trust.
2884+
(Contributed by Christian Heimes in :gh:`160040` for :cve:`2026-115557`.)

‎Lib/mimetypes.py‎

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,17 +45,23 @@
4545
"suffix_map", "encodings_map", "types_map", "common_types"
4646
]
4747

48-
knownfiles = [
49-
"/etc/mime.types",
50-
"/etc/httpd/mime.types", # Mac OS X
51-
"/etc/httpd/conf/mime.types", # Apache
52-
"/etc/apache/mime.types", # Apache 1
53-
"/etc/apache2/mime.types", # Apache 2
54-
"/usr/local/etc/httpd/conf/mime.types",
55-
"/usr/local/lib/netscape/mime.types",
56-
"/usr/local/etc/httpd/conf/mime.types", # Apache 1.2
57-
"/usr/local/etc/mime.types", # Apache 1.3
58-
]
48+
if sys.platform == "win32":
49+
# The default paths would resolve to the root of the current drive on Windows
50+
# (e.g. C:\etc\mime.types), which unprivileged users can usually create.
51+
# Only the registry is used by default.
52+
knownfiles = []
53+
else:
54+
knownfiles = [
55+
"/etc/mime.types",
56+
"/etc/httpd/mime.types", # Mac OS X
57+
"/etc/httpd/conf/mime.types", # Apache
58+
"/etc/apache/mime.types", # Apache 1
59+
"/etc/apache2/mime.types", # Apache 2
60+
"/usr/local/etc/httpd/conf/mime.types",
61+
"/usr/local/lib/netscape/mime.types",
62+
"/usr/local/etc/httpd/conf/mime.types", # Apache 1.2
63+
"/usr/local/etc/mime.types", # Apache 1.3
64+
]
5965

6066
inited = False
6167
_db = None

‎Lib/test/certdata/distrusted.zip‎

4.94 KB
Binary file not shown.

‎Lib/test/certdata/make_ssl_certs.py‎

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import pprint
66
import shutil
77
import tempfile
8+
import zipfile
89
from subprocess import *
910

1011
startdate = "20180829142316Z"
@@ -224,6 +225,107 @@ def print_cert(path):
224225
pprint.pprint(_ssl._test_decode_cert(path))
225226

226227

228+
# The distrusted test material (compromised eMudhra CA certs and a leaf signed
229+
# by them) lives in a password-protected zip so it doesn't trip security
230+
# scanners that cannot tell internal test fixtures from production secrets and
231+
# flag any certificate or private key they find. It is encrypted, not merely
232+
# zipped, because such scanners read inside plain archives. The password is
233+
# fixed, public, and not a secret.
234+
DISTRUSTED_ZIP = 'distrusted.zip'
235+
DISTRUSTED_ZIP_DIR = 'distrusted'
236+
DISTRUSTED_ZIP_CHOSEN_KNIGHT = 'Brave Sir Robin'
237+
238+
# ASN.1 template for the intermediate's compromised key: secp384r1 scalar 4.
239+
# openssl derives the public key when the SEC1 structure omits it.
240+
distrusted_key_template = """
241+
asn1 = SEQUENCE:eckey
242+
[eckey]
243+
version = INTEGER:1
244+
priv = FORMAT:HEX,OCTETSTRING:{scalar}
245+
params = EXPLICIT:0,OID:secp384r1
246+
"""
247+
248+
249+
def make_distrusted_spki_cert():
250+
# leaf signed by the distrusted intermediate, chains to the distrusted root
251+
print("creating distrusted-SPKI test cert")
252+
# The standard library can read but not write encrypted zip files, so we
253+
# need the Info-ZIP "zip" command to repackage the archive.
254+
if shutil.which('zip') is None:
255+
raise SystemExit(
256+
f"the 'zip' command is required to write the encrypted {DISTRUSTED_ZIP} "
257+
"(the zipfile module cannot create encrypted archives); "
258+
"please install Info-ZIP's zip utility"
259+
)
260+
261+
# the root and intermediate are real certs we cannot re-mint; carry them
262+
# over from the existing archive
263+
pw = DISTRUSTED_ZIP_CHOSEN_KNIGHT.encode()
264+
with zipfile.ZipFile(DISTRUSTED_ZIP) as z:
265+
root = z.read(DISTRUSTED_ZIP_DIR + '/distrusted_root.pem', pw)
266+
intermediate = z.read(DISTRUSTED_ZIP_DIR + '/distrusted_intermediate.pem', pw)
267+
268+
with tempfile.TemporaryDirectory() as workdir:
269+
stage = os.path.join(workdir, DISTRUSTED_ZIP_DIR)
270+
os.mkdir(stage)
271+
req_file = os.path.join(workdir, 'req.cnf')
272+
key_file = os.path.join(workdir, 'leaf.key')
273+
csr_file = os.path.join(workdir, 'leaf.csr')
274+
cert_file = os.path.join(workdir, 'leaf.pem')
275+
cakey_conf = os.path.join(workdir, 'cakey.cnf')
276+
cakey_der = os.path.join(workdir, 'cakey.der')
277+
cakey_file = os.path.join(workdir, 'cakey.pem')
278+
279+
inter_file = os.path.join(stage, 'distrusted_intermediate.pem')
280+
with open(os.path.join(stage, 'distrusted_root.pem'), 'wb') as f:
281+
f.write(root)
282+
with open(inter_file, 'wb') as f:
283+
f.write(intermediate)
284+
285+
# recreate the intermediate's key (secp384r1 scalar 4) on the fly
286+
with open(cakey_conf, 'w') as f:
287+
f.write(distrusted_key_template.format(scalar='00' * 47 + '04'))
288+
check_call(['openssl', 'asn1parse', '-genconf', cakey_conf,
289+
'-out', cakey_der, '-noout'])
290+
check_call(['openssl', 'pkey', '-in', cakey_der,
291+
'-inform', 'DER', '-out', cakey_file])
292+
293+
req = req_template.format(
294+
hostname='localhost',
295+
extra_san='',
296+
startdate=startdate,
297+
enddate=enddate
298+
)
299+
with open(req_file, 'w') as f:
300+
f.write(req)
301+
# ECC leaf to match the ECC intermediate
302+
check_call(['openssl', 'req', '-new', '-nodes', '-days', '7000',
303+
'-newkey', 'param:secp384r1.pem', '-keyout', key_file,
304+
'-config', req_file, '-out', csr_file])
305+
# sign with the distrusted intermediate, not the test CA
306+
check_call(['openssl', 'ca', '-config', req_file,
307+
'-extensions', 'req_x509_extensions_full',
308+
'-cert', inter_file, '-keyfile', cakey_file,
309+
'-out', cert_file, '-outdir', 'cadir',
310+
'-policy', 'policy_anything', '-batch', '-infiles', csr_file])
311+
with open(key_file) as f:
312+
key = f.read()
313+
with open(cert_file) as f:
314+
cert = f.read()
315+
# server sends leaf + intermediate, client trusts the root
316+
with open(os.path.join(stage, 'distrusted_keycert.pem'), 'w') as f:
317+
f.write(key)
318+
f.write(cert)
319+
f.write(intermediate.decode())
320+
321+
# repackage the encrypted archive (zipfile cannot write encryption)
322+
zippath = os.path.join(here, DISTRUSTED_ZIP)
323+
if os.path.exists(zippath):
324+
os.remove(zippath)
325+
check_call(['zip', '-q', '-X', '-P', DISTRUSTED_ZIP_CHOSEN_KNIGHT, '-r',
326+
zippath, DISTRUSTED_ZIP_DIR], cwd=workdir)
327+
328+
227329
if __name__ == '__main__':
228330
os.chdir(here)
229331
cert, key = make_cert_key('localhost', ext='req_x509_extensions_simple')
@@ -309,6 +411,8 @@ def print_cert(path):
309411
f.write(key)
310412
f.write(cert)
311413

414+
make_distrusted_spki_cert()
415+
312416
unmake_ca()
313417
print("update Lib/test/test_ssl.py and Lib/test/test_asyncio/utils.py")
314418
print_cert('keycert.pem')

‎Lib/test/test_gdb/test_pretty_print.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ def test_strings(self):
115115
'python import gdb; print(gdb.host_charset())')
116116

117117
encoding = stdout.strip()
118-
if stderr or not encoding:
118+
if not encoding:
119119
raise RuntimeError(
120120
f'unable to determine the host charset of gdb\n'
121121
f'stdout={stdout!r}\n'

‎Lib/test/test_math.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2743,7 +2743,7 @@ def test_fma_infinities(self):
27432743
@unittest.skipIf(
27442744
sys.platform.startswith(("freebsd", "wasi", "netbsd", "emscripten"))
27452745
or (sys.platform == "android" and platform.machine() == "x86_64")
2746-
or (support.linked_to_musl() and support.linked_to_musl() < (1, 2, 6)), # gh-131032
2746+
or support.linked_to_musl(), # gh-131032
27472747
f"this platform doesn't implement IEE 754-2008 properly")
27482748
def test_fma_zero_result(self):
27492749
nonnegative_finites = [0.0, 1e-300, 2.3, 1e300]

‎Lib/test/test_mimetypes.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,10 @@ def test_init_knownfiles(self):
195195
mimetypes.init()
196196
self.assertEqual(guess_file_type("file.test2")[0], "testing/test2")
197197

198+
@unittest.skipIf(sys.platform == "win32", "non-Windows only")
199+
def test_default_knownfiles(self):
200+
self.assertIn("/etc/mime.types", knownfiles)
201+
198202
def test_added_types_are_used(self):
199203
mimetypes.add_type('testing/default-type', '')
200204
mime_type, _ = mimetypes.guess_type('')
@@ -431,6 +435,9 @@ def test_registry_parsing(self):
431435
eq(self.db.guess_type("image.jpg"), ("image/jpeg", None))
432436
eq(self.db.guess_type("image.png"), ("image/png", None))
433437

438+
def test_default_knownfiles(self):
439+
self.assertEqual(knownfiles, [])
440+
434441
@unittest.skipIf(not hasattr(_winapi, "_mimetypes_read_windows_registry"),
435442
"read_windows_registry accelerator unavailable")
436443
def test_registry_accelerator(self):

‎Lib/test/test_ssl.py‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
import socket
1919
import select
2020
import struct
21+
import tempfile
2122
import time
23+
import zipfile
2224
import enum
2325
import gc
2426
import http.client
@@ -151,6 +153,19 @@ def data_file(*name):
151153
LEAF_MISSING_AKI_CERTFILE_HOSTNAME = "example.com"
152154
LEAF_MISSING_AKI_CA = data_file("leaf-missing-aki.ca.pem")
153155

156+
# gh-160040: a leaf signed by a compromised eMudhra intermediate whose public
157+
# key is on the _ssl distrusted list. The chain to distrusted_root.pem is
158+
# otherwise valid.
159+
# The PEMs live in a password-protected zip so the compromised eMudhra certs
160+
# don't trip security scanners that cannot tell internal test fixtures from
161+
# production secrets and flag any certificate or private key they find. It is
162+
# encrypted, not merely zipped, because such scanners read inside plain archives.
163+
# The password is fixed, public, and not a secret; it only keeps the archive
164+
# from storing the PEMs in the clear.
165+
DISTRUSTED_SPKI_ZIP = data_file("distrusted.zip")
166+
DISTRUSTED_SPKI_CHOSEN_KNIGHT = b"Brave Sir Robin" # not a secret, see comment
167+
DISTRUSTED_SPKI_HOSTNAME = "localhost"
168+
154169
# Same certificate as pycacert.pem, but without extra text in file
155170
SIGNING_CA = data_file("capath", "ceff1710.0")
156171
# cert with all kinds of subject alt names
@@ -2158,6 +2173,28 @@ def test_unwrap(self):
21582173
c_in.write(s_out.read())
21592174
client.unwrap()
21602175

2176+
def test_distrusted_spki(self):
2177+
# gh-160040: the server chain is otherwise valid, but the eMudhra
2178+
# intermediate's public key is on the _ssl distrusted list, so
2179+
# verification rejects it.
2180+
client_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
2181+
server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
2182+
# the contexts load the PEMs now, so the files are only needed here
2183+
with tempfile.TemporaryDirectory() as tmpdir:
2184+
with zipfile.ZipFile(DISTRUSTED_SPKI_ZIP) as z:
2185+
z.extractall(tmpdir, pwd=DISTRUSTED_SPKI_CHOSEN_KNIGHT)
2186+
certdir = os.path.join(tmpdir, "distrusted")
2187+
client_context.load_verify_locations(
2188+
os.path.join(certdir, "distrusted_root.pem"))
2189+
server_context.load_cert_chain(
2190+
os.path.join(certdir, "distrusted_keycert.pem"))
2191+
2192+
with self.assertRaises(ssl.SSLCertVerificationError) as cm:
2193+
connected_bio_pair(client_context, server_context,
2194+
DISTRUSTED_SPKI_HOSTNAME)
2195+
# X509_V_ERR_CERT_REJECTED
2196+
self.assertEqual(cm.exception.verify_code, 28)
2197+
21612198
def test_sni_callback_context_released_and_callback_raises(self):
21622199
# Variant of the test below without a HelloRetryRequest: the callback
21632200
# switches the connection to another context, drops the last

‎Makefile.pre.in‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3145,7 +3145,7 @@ MODULE__SHA1_DEPS=$(srcdir)/Modules/hashlib.h $(LIBHACL_HEADERS) Modules/_hacl/H
31453145
MODULE__SHA2_DEPS=$(srcdir)/Modules/hashlib.h $(LIBHACL_SHA2_HEADERS) $(LIBHACL_SHA2_A)
31463146
MODULE__SHA3_DEPS=$(srcdir)/Modules/hashlib.h $(LIBHACL_HEADERS) Modules/_hacl/Hacl_Hash_SHA3.h Modules/_hacl/Hacl_Hash_SHA3.c
31473147
MODULE__SOCKET_DEPS=$(srcdir)/Modules/socketmodule.h $(srcdir)/Modules/addrinfo.h $(srcdir)/Modules/getaddrinfo.c $(srcdir)/Modules/getnameinfo.c
3148-
MODULE__SSL_DEPS=$(srcdir)/Modules/_ssl.h $(srcdir)/Modules/_ssl/cert.c $(srcdir)/Modules/_ssl/debughelpers.c $(srcdir)/Modules/_ssl/misc.c $(srcdir)/Modules/_ssl_data_111.h $(srcdir)/Modules/_ssl_data_300.h $(srcdir)/Modules/socketmodule.h
3148+
MODULE__SSL_DEPS=$(srcdir)/Modules/_ssl.h $(srcdir)/Modules/_ssl/cert.c $(srcdir)/Modules/_ssl/debughelpers.c $(srcdir)/Modules/_ssl/misc.c $(srcdir)/Modules/_ssl/distrusted_spki.h $(srcdir)/Modules/_ssl_data_111.h $(srcdir)/Modules/_ssl_data_300.h $(srcdir)/Modules/socketmodule.h
31493149
MODULE__TESTCAPI_DEPS=$(srcdir)/Modules/_testcapi/parts.h $(srcdir)/Modules/_testcapi/util.h
31503150
MODULE__TESTLIMITEDCAPI_DEPS=$(srcdir)/Modules/_testlimitedcapi/testcapi_long.h $(srcdir)/Modules/_testlimitedcapi/parts.h $(srcdir)/Modules/_testlimitedcapi/util.h
31513151
MODULE__TESTINTERNALCAPI_DEPS=$(srcdir)/Modules/_testinternalcapi/parts.h
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
:mod:`mimetypes` no longer reads the files listed in :data:`mimetypes.knownfiles`
2+
by default on Windows, and uses only the registry.

0 commit comments

Comments
 (0)