From 223a1ab31eea682423386686b4d21665e857a1de Mon Sep 17 00:00:00 2001 From: Steven Date: Thu, 29 Oct 2026 11:20:12 -0600 Subject: [PATCH 1/2] Backport CVE-2026-115557 fix to Python 3.10 Python 3.10 reached end of life without a fixed release for CVE-2026-115557 (gh-160040), so backport the 3.11 fix (7dd8b4736a1419750b94a388e6d158be15836fe6) as a source patch: distrust compromised eMudhra intermediate CA keys. Adds a regression test that checks a peer chain signed by a distrusted intermediate is rejected with X509_V_ERR_CERT_REJECTED. --- anaconda.yaml | 9 + recipe/meta.yaml | 7 +- ...romised-eMudhra-intermediate-CA-keys.patch | 249 ++++++++++++++++++ recipe/run_test.py | 42 +++ recipe/tests/ssl/distrusted.zip | Bin 0 -> 5062 bytes 5 files changed, 306 insertions(+), 1 deletion(-) create mode 100644 anaconda.yaml create mode 100644 recipe/patches/0030-Distrust-compromised-eMudhra-intermediate-CA-keys.patch create mode 100644 recipe/tests/ssl/distrusted.zip diff --git a/anaconda.yaml b/anaconda.yaml new file mode 100644 index 000000000..43a5a9890 --- /dev/null +++ b/anaconda.yaml @@ -0,0 +1,9 @@ +anaconda_config_version: 1 +upstream_sources: + - webpage: + url: https://www.python.org/ftp/python/ + regex: href="(\d+(?:\.\d+)+)/" + include_regex: ^3\.10\.\d+(?:[a-z][a-z0-9]*)?$ + packages: + - python + - libpython-static diff --git a/recipe/meta.yaml b/recipe/meta.yaml index 23d3f5e2c..520be5c9a 100644 --- a/recipe/meta.yaml +++ b/recipe/meta.yaml @@ -4,7 +4,7 @@ {% set ver2 = '.'.join(version.split('.')[0:2]) %} {% set ver2nd = ''.join(version.split('.')[0:2]) %} {% set ver3nd = ''.join(version.split('.')[0:3]) %} -{% set build_number = "0" %} +{% set build_number = "1" %} {% set channel_targets = ('abc', 'def') %} # this makes the linter happy @@ -90,6 +90,10 @@ source: - patches/0027-Unvendor-expat.patch # https://github.com/python/cpython/pull/29791 - patches/0029-Fix-ctypes-fielddesc-init-for-external-libffi.patch + # CVE-2026-115557 (gh-160040): distrust compromised eMudhra intermediate + # CA keys. Backport of CPython 3.11 commit 7dd8b4736a1419750b94a388e6d158be15836fe6; + # 3.10 is EOL, so upstream has no fixed 3.10 release. + - patches/0030-Distrust-compromised-eMudhra-intermediate-CA-keys.patch build: number: {{ build_number }} @@ -236,6 +240,7 @@ outputs: - tests/cmake/* - tests/cython/* - tests/prefix-replacement/* + - tests/ssl/* - run_test.py - test-cert.pem commands: diff --git a/recipe/patches/0030-Distrust-compromised-eMudhra-intermediate-CA-keys.patch b/recipe/patches/0030-Distrust-compromised-eMudhra-intermediate-CA-keys.patch new file mode 100644 index 000000000..b7d3f897f --- /dev/null +++ b/recipe/patches/0030-Distrust-compromised-eMudhra-intermediate-CA-keys.patch @@ -0,0 +1,249 @@ +From b299e19c65e03a6c3df58b31f9da4e1f974c81a5 Mon Sep 17 00:00:00 2001 +From: Christian Heimes +Date: Thu, 29 Oct 2026 11:09:28 -0600 +Subject: [PATCH] gh-160040: Distrust compromised eMudhra intermediate CA keys + (CVE-2026-115557) + +Backport of the 3.11 fix (commit 7dd8b4736a1419750b94a388e6d158be15836fe6, +PR gh-160042) to 3.10, which reached end of life before a fixed release +was published. Adds a denylist of compromised SubjectPublicKeyInfo +SHA-256 digests and installs a verify callback that rejects any peer chain +containing one of those keys. + +Adapted for 3.10: no Makefile.pre.in MODULE__SSL_DEPS entry exists and the +older Visual Studio project layout is used, so the Visual Studio header +entries are added without the upstream ItemGroup reordering. + +(cherry picked from commit 7dd8b4736a1419750b94a388e6d158be15836fe6) +--- + Modules/_ssl.c | 67 ++++++++++++++++++++++++++++-- + Modules/_ssl/distrusted_spki.h | 75 ++++++++++++++++++++++++++++++++++ + PCbuild/_ssl.vcxproj | 3 ++ + PCbuild/_ssl.vcxproj.filters | 8 ++++ + 4 files changed, 149 insertions(+), 4 deletions(-) + create mode 100644 Modules/_ssl/distrusted_spki.h + +diff --git a/Modules/_ssl.c b/Modules/_ssl.c +index d58bb15..2b409bb 100644 +--- a/Modules/_ssl.c ++++ b/Modules/_ssl.c +@@ -54,6 +54,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" +@@ -880,6 +881,7 @@ newPySSLSocket(PySSLContext *sslctx, PySocketSockObject *sock, + int (*verify_cb)(int, X509_STORE_CTX *) = NULL; + verify_cb = SSL_get_verify_callback(self->ssl); + mode |= SSL_VERIFY_POST_HANDSHAKE; ++ /* Keep ctx verify callback (distrusted SPKI check) */ + SSL_set_verify(self->ssl, mode, verify_cb); + } + } else { +@@ -2985,11 +2987,70 @@ static PyType_Spec PySSLSocket_spec = { + * _SSLContext objects + */ + ++#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; ++ int spki_len = i2d_X509_PUBKEY(X509_get_X509_PUBKEY(cert), &spki); ++ if (spki_len <= 0 || spki == NULL) { ++ return -1; ++ } ++ ++ unsigned char digest[EVP_MAX_MD_SIZE]; ++ unsigned int digest_len = 0; ++ int 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 (size_t i = 0; i < Py_ARRAY_LENGTH(_distrusted_spki); 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) ++{ ++ /* Don't override an error OpenSSL already reported for this certificate. */ ++ if (!preverify_ok) { ++ return preverify_ok; ++ } ++ X509 *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 */ ++ X509_STORE_CTX_set_error(x509_ctx, X509_V_ERR_UNSPECIFIED); ++ return 0; ++ } ++} ++ + static int + _set_verify_mode(PySSLContext *self, enum py_ssl_cert_requirements n) + { + int mode; +- int (*verify_cb)(int, X509_STORE_CTX *) = NULL; + + switch(n) { + case PY_SSL_CERT_NONE: +@@ -3010,9 +3071,7 @@ _set_verify_mode(PySSLContext *self, enum py_ssl_cert_requirements n) + /* 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 0000000..bbdb56d +--- /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 f583d57..75bec7c 100644 +--- a/PCbuild/_ssl.vcxproj ++++ b/PCbuild/_ssl.vcxproj +@@ -109,6 +109,9 @@ + /d1trimfile:%SRC_DIR% + + ++ ++ ++ + + + +diff --git a/PCbuild/_ssl.vcxproj.filters b/PCbuild/_ssl.vcxproj.filters +index 2f631dc..37c9202 100644 +--- a/PCbuild/_ssl.vcxproj.filters ++++ b/PCbuild/_ssl.vcxproj.filters +@@ -4,6 +4,9 @@ + + {695348f7-e9f6-4fe1-bc03-5f08ffc8095b} + ++ ++ {7c1bd5da-8912-4107-b6ac-f3930f2d90c7} ++ + + {1b18a2e6-040d-46c7-a9ac-ac2ec64fb5d6} + +@@ -13,6 +16,11 @@ + Source Files + + ++ ++ ++ Header Files ++ ++ + + + Resource Files diff --git a/recipe/run_test.py b/recipe/run_test.py index fa6178e69..40bdedd4c 100644 --- a/recipe/run_test.py +++ b/recipe/run_test.py @@ -104,3 +104,45 @@ der = ssl.PEM_cert_to_DER_cert(pem) ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) ctx.load_verify_locations(cadata=der) + +# CVE-2026-115557 (gh-160040): a peer chain signed by a compromised eMudhra +# intermediate must be rejected even though the chain itself is valid. +import tempfile +import zipfile + +fixtures = Path(__file__).with_name("tests") / "ssl" +with tempfile.TemporaryDirectory() as tmpdir: + # The archive is encrypted only so the test certs are not stored in the + # clear; the password is fixed and public. + with zipfile.ZipFile(fixtures / "distrusted.zip") as zf: + zf.extractall(tmpdir, pwd=b"Brave Sir Robin") + distrusted = Path(tmpdir) / "distrusted" + client_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + server_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + client_ctx.load_verify_locations(str(distrusted / "distrusted_root.pem")) + server_ctx.load_cert_chain(str(distrusted / "distrusted_keycert.pem")) + + c_in, c_out = ssl.MemoryBIO(), ssl.MemoryBIO() + s_in, s_out = ssl.MemoryBIO(), ssl.MemoryBIO() + client = client_ctx.wrap_bio(c_in, c_out, server_hostname="localhost") + server = server_ctx.wrap_bio(s_in, s_out, server_side=True) + try: + for _ in range(5): + try: + client.do_handshake() + except ssl.SSLWantReadError: + pass + if c_out.pending: + s_in.write(c_out.read()) + try: + server.do_handshake() + except ssl.SSLWantReadError: + pass + if s_out.pending: + c_in.write(s_out.read()) + client.do_handshake() + except ssl.SSLCertVerificationError as exc: + assert exc.verify_code == 28, exc # X509_V_ERR_CERT_REJECTED + else: + raise AssertionError("distrusted eMudhra chain was accepted") +print("distrusted SPKI check: OK") diff --git a/recipe/tests/ssl/distrusted.zip b/recipe/tests/ssl/distrusted.zip new file mode 100644 index 0000000000000000000000000000000000000000..83184329a5ab9e076e66513bfaff4e3bbde53762 GIT binary patch literal 5062 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 Date: Thu, 29 Oct 2026 11:39:14 -0600 Subject: [PATCH 2/2] Skip win-arm64 builds win-arm64 is now part of the default build matrix (package-build-platform#3947, AnacondaRecipes/aggregate#703), but it ships Python >=3.12 only. Skip both outputs so the 3.10 recipe does not attempt win-arm64. --- recipe/meta.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/recipe/meta.yaml b/recipe/meta.yaml index 520be5c9a..6b713b257 100644 --- a/recipe/meta.yaml +++ b/recipe/meta.yaml @@ -114,6 +114,8 @@ outputs: script: build_base.bat # [win] build: number: {{ build_number }} + # win-arm64 ships Python >=3.12 only (aggregate CBC) + skip: true # [win and arm64] activate_in_script: true # Windows has issues updating python if conda is using files itself. # Copy rather than link. @@ -306,6 +308,8 @@ outputs: script: build_static.bat # [win] build: number: {{ build_number }} + # win-arm64 ships Python >=3.12 only (aggregate CBC) + skip: true # [win and arm64] activate_in_script: true {% if 'conda-forge' in channel_targets %} ignore_run_exports: