From d1c320f9845dec59cd103e0bcded9e3e3bed528d Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Sat, 24 Oct 2026 07:40:49 +0200 Subject: [PATCH] [3.14] gh-160040: Distrust compromised eMudhra intermediate CA keys (CVE-2026-115557) (GH-160042) The ssl module now rejects TLS certificate chains that contain any of a set of compromised eMudhra intermediate CA keys. This is an extraordinary measure and a last line of defense: removing trust is primarily the job of the platform root store, and the built-in denylist only blocks connections while that update has not yet reached the OS, a container image, or a bundled trust store in various Python packages. Entries are added to the denylist only under a very high threshold. The denylist matches on the public key (the SHA-256 digest of the RFC-5280 SubjectPublicKeyInfo), so it also covers the cross-signed certificates that reuse one of the distrusted keys. --------- Signed-off-by: Christian Heimes Co-authored-by: Claude Opus 4.8 (1M context) (cherry picked from commit 7dd8b4736a1419750b94a388e6d158be15836fe6) Co-authored-by: Christian Heimes --- Lib/test/certdata/distrusted.zip | Bin 0 -> 5062 bytes Lib/test/certdata/make_ssl_certs.py | 106 +++++++++++++++++- Lib/test/test_ssl.py | 37 ++++++ Makefile.pre.in | 2 +- ...-10-21-16-55-07.gh-issue-160040.9PjFjC.rst | 12 ++ Modules/_ssl.c | 67 ++++++++++- Modules/_ssl/distrusted_spki.h | 75 +++++++++++++ PCbuild/_ssl.vcxproj | 3 + PCbuild/_ssl.vcxproj.filters | 8 ++ 9 files changed, 306 insertions(+), 4 deletions(-) create mode 100644 Lib/test/certdata/distrusted.zip create mode 100644 Misc/NEWS.d/next/Security/2026-10-21-16-55-07.gh-issue-160040.9PjFjC.rst create mode 100644 Modules/_ssl/distrusted_spki.h diff --git a/Lib/test/certdata/distrusted.zip b/Lib/test/certdata/distrusted.zip new file mode 100644 index 0000000000000000000000000000000000000000..83184329a5ab9e076e66513bfaff4e3bbde53762 GIT binary patch literal 5062 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 if (mode & SSL_VERIFY_PEER) { mode |= SSL_VERIFY_POST_HANDSHAKE; - SSL_set_verify(self->ssl, mode, NULL); + /* Keep ctx verify callback (distrusted SPKI check) */ + SSL_set_verify(self->ssl, mode, + SSL_get_verify_callback(self->ssl)); } } else { /* client socket */ @@ -3077,6 +3080,66 @@ 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) { @@ -3101,7 +3164,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. */ - SSL_CTX_set_verify(self->ctx, mode, NULL); + 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 ce21f992ff8510e..16595c782b3c25f 100644 --- a/PCbuild/_ssl.vcxproj +++ b/PCbuild/_ssl.vcxproj @@ -97,6 +97,9 @@ ws2_32.lib;crypt32.lib;%(AdditionalDependencies) + + + diff --git a/PCbuild/_ssl.vcxproj.filters b/PCbuild/_ssl.vcxproj.filters index 8aef9e03fcc429a..4323bd17581f602 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