From 6275d65c642244833ab5daa5fe1310e8f4953ac8 Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Wed, 21 Oct 2026 16:59:16 +0200 Subject: [PATCH 1/7] gh-160040: Distrust compromised eMudhra intermediate CA keys Several eMudhra intermediate CA keys were compromised and revoked. As defense in depth, reject any chain containing one of the affected public keys, so connections are blocked even when the root store (OS, container) has not yet dropped the issuing roots or certifi/pip are not updated. The denylist in `Modules/_ssl/distrusted_spki.h` matches the DER-encoded SPKI, so cross-signed certificates that reuse a key are blocked too. Mirrors Golang's [crypto/x509](https://go.dev/cl/851047) change. See https://emudhra.we-have-a-problem.com/. Signed-off-by: Christian Heimes --- Makefile.pre.in | 2 +- ...-10-21-16-55-07.gh-issue-160040.9PjFjC.rst | 6 + Modules/_ssl.c | 44 ++++- Modules/_ssl/distrusted_spki.h | 155 ++++++++++++++++++ PCbuild/_ssl.vcxproj | 1 + PCbuild/_ssl.vcxproj.filters | 3 + Tools/ssl/emudhra_denylist_check.py | 52 ++++++ 7 files changed, 260 insertions(+), 3 deletions(-) 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 create mode 100644 Tools/ssl/emudhra_denylist_check.py diff --git a/Makefile.pre.in b/Makefile.pre.in index c49a16f1edc3bef..702e65666cdff03 100644 --- a/Makefile.pre.in +++ b/Makefile.pre.in @@ -3461,7 +3461,7 @@ MODULE__HMAC_DEPS=$(srcdir)/Modules/hashlib.h $(LIBHACL_HMAC_HEADERS) $(LIBHACL_ MODULE__HMAC_LDEPS=$(LIBHACL_HMAC_LIB_@LIBHACL_LDEPS_LIBTYPE@) MODULE__SOCKET_DEPS=$(srcdir)/Modules/socketmodule.h $(srcdir)/Modules/addrinfo.h $(srcdir)/Modules/getaddrinfo.c $(srcdir)/Modules/getnameinfo.c -MODULE__SSL_DEPS=$(srcdir)/Modules/_ssl.h $(srcdir)/Modules/_openssl_mem.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 +MODULE__SSL_DEPS=$(srcdir)/Modules/_ssl.h $(srcdir)/Modules/_openssl_mem.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 MODULE__TESTCAPI_DEPS=$(srcdir)/Modules/_testcapi/parts.h $(srcdir)/Modules/_testcapi/util.h MODULE__TESTLIMITEDCAPI_DEPS=$(srcdir)/Modules/_testlimitedcapi/testcapi_long.h $(srcdir)/Modules/_testlimitedcapi/parts.h $(srcdir)/Modules/_testlimitedcapi/util.h MODULE__TESTINTERNALCAPI_DEPS=$(srcdir)/Modules/_testinternalcapi/parts.h $(srcdir)/Parser/tokenizer/source.h $(srcdir)/Parser/tokenizer/types.h $(srcdir)/Python/ceval.h $(srcdir)/Modules/_testinternalcapi/test_targets.h $(srcdir)/Modules/_testinternalcapi/test_cases.c.h diff --git a/Misc/NEWS.d/next/Security/2026-10-21-16-55-07.gh-issue-160040.9PjFjC.rst b/Misc/NEWS.d/next/Security/2026-10-21-16-55-07.gh-issue-160040.9PjFjC.rst new file mode 100644 index 000000000000000..e30eda238b63642 --- /dev/null +++ b/Misc/NEWS.d/next/Security/2026-10-21-16-55-07.gh-issue-160040.9PjFjC.rst @@ -0,0 +1,6 @@ +:mod:`ssl` now rejects TLS certificate chains that contain any of a set of +compromised eMudhra intermediate CA keys. A built-in denylist of the affected +:rfc:`5280` ``SubjectPublicKeyInfo`` values is enforced during certificate +verification, mirroring the mitigation added to other TLS stacks. The denylist +matches on the public key, so it also covers the cross-signed certificates that +reuse one of the distrusted keys. diff --git a/Modules/_ssl.c b/Modules/_ssl.c index ee0df146421d7f8..d5071f316c81613 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -976,7 +976,9 @@ newPySSLSocket(PySSLContext *sslctx, PySocketSockObject *sock, int mode = SSL_get_verify_mode(self->ssl); 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 */ @@ -3369,6 +3371,44 @@ static PyType_Spec PySSLSocket_spec = { * _SSLContext objects */ +#include "_ssl/distrusted_spki.h" + +/* True if the cert's SPKI is on the distrusted list */ +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 0; + } + + int distrusted = 0; + for (size_t i = 0; i < Py_ARRAY_LENGTH(_distrusted_spki); i++) { + if ((size_t)spki_len == _distrusted_spki[i].len + && memcmp(spki, _distrusted_spki[i].der, _distrusted_spki[i].len) == 0) + { + distrusted = 1; + break; + } + } + OPENSSL_free(spki); + return distrusted; +} + +/* Verify callback, called once per cert. Reject distrusted SPKIs, otherwise + * keep OpenSSL's result. */ +static int +_ssl_verify_callback(int preverify_ok, X509_STORE_CTX *x509_ctx) +{ + X509 *cert = X509_STORE_CTX_get_current_cert(x509_ctx); + if (cert != NULL && _cert_has_distrusted_spki(cert)) { + X509_STORE_CTX_set_error(x509_ctx, X509_V_ERR_CERT_REJECTED); + return 0; + } + return preverify_ok; +} + static int _set_verify_mode(PySSLContext *self, enum py_ssl_cert_requirements n) { @@ -3393,7 +3433,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..1330ce6dbee7edc --- /dev/null +++ b/Modules/_ssl/distrusted_spki.h @@ -0,0 +1,155 @@ +/* Denylist of distrusted SubjectPublicKeyInfo (SPKI). + * + * Verification rejects any chain with a cert whose SPKI (DER-encoded + * SubjectPublicKeyInfo) is in the _distrusted_spki table. We match the key, + * not a cert fingerprint, so cross-signs that reuse the key are blocked too. + * To block another key, add its SPKI DER and a table entry. + * + * Dump a cert's SPKI DER and its SHA-256 with OpenSSL: + * + * $ openssl x509 -in cert.pem -noout -pubkey | openssl pkey -pubin -outform DER > spki.der + * $ xxd -i spki.der + * $ openssl dgst -sha256 spki.der + */ +#ifndef Py_SSL_DISTRUSTED_SPKI_H +#define Py_SSL_DISTRUSTED_SPKI_H + +#include + +/* Compromised eMudhra intermediates, https://emudhra.we-have-a-problem.com/ */ + +/* CN=EM OV TLS CA - G3B,O=eMudhra BV,C=NL (secp384r1) + * SPKI SHA-256: 43bffa19d6ef651451f6841e019931e96f6f2eaeff28ab4b6f1ce4576de2717b */ +static const unsigned char _distrusted_spki_em_ov_tls_ca_g3b[] = { + 0x30, 0x76, 0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, + 0x01, 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22, 0x03, 0x62, 0x00, 0x04, + 0x73, 0x89, 0x7a, 0xa0, 0xeb, 0x3c, 0xfd, 0xca, 0x8b, 0x12, 0x40, 0x2f, + 0xfa, 0xf0, 0xab, 0xc4, 0xe1, 0x21, 0x2b, 0xaf, 0x8d, 0xee, 0x77, 0x8a, + 0xc9, 0xee, 0x2d, 0x6d, 0xb8, 0x3f, 0xac, 0x28, 0x0c, 0x5e, 0x05, 0x63, + 0x8a, 0xe7, 0xa2, 0xac, 0xd7, 0x1c, 0x32, 0x2a, 0x8e, 0x36, 0x1c, 0xde, + 0xb9, 0xf6, 0x9c, 0x43, 0x02, 0xb9, 0xdd, 0x8d, 0x73, 0xd1, 0xcc, 0x6c, + 0xaa, 0xb2, 0xcc, 0x0a, 0xee, 0x4e, 0x21, 0x6c, 0xe9, 0x79, 0xdd, 0x51, + 0x8a, 0x4a, 0xf4, 0xb6, 0x26, 0xe5, 0x68, 0x4c, 0xdd, 0x65, 0x80, 0x37, + 0xed, 0x0c, 0x85, 0x4e, 0x4c, 0xc1, 0x2f, 0x4c, 0xf8, 0x64, 0x6d, 0x0d, +}; + +/* CN=EM DV TLS CA - G3C,O=eMudhra Inc,C=US (secp384r1) + * SPKI SHA-256: 49dd353dc2b919a3770645117d0e60c16115e7d1bc259eefe734b00ea434fe5f */ +static const unsigned char _distrusted_spki_em_dv_tls_ca_g3c[] = { + 0x30, 0x76, 0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, + 0x01, 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22, 0x03, 0x62, 0x00, 0x04, + 0x3a, 0x7a, 0x78, 0x62, 0xc8, 0xc4, 0x4d, 0x32, 0x4d, 0xdd, 0x69, 0x03, + 0x3a, 0x67, 0x82, 0xc0, 0xef, 0xee, 0xfe, 0x40, 0x55, 0xe6, 0x8a, 0x12, + 0x3f, 0xa0, 0x37, 0x1b, 0xa3, 0xbd, 0x91, 0x94, 0x51, 0x4e, 0xe1, 0x72, + 0xa4, 0x33, 0xe3, 0x63, 0x0f, 0x02, 0xbd, 0x18, 0x80, 0x37, 0xd9, 0xe5, + 0x14, 0x9f, 0x9b, 0x77, 0xdb, 0x94, 0xa2, 0x81, 0xdf, 0x98, 0x7b, 0x2f, + 0xea, 0x88, 0x8b, 0x74, 0xbd, 0xfe, 0x8f, 0xda, 0x4c, 0x10, 0x99, 0xee, + 0x6c, 0xa1, 0x95, 0x66, 0xfe, 0x3e, 0xf0, 0xe5, 0xf6, 0xb0, 0x9a, 0x55, + 0x6c, 0xcc, 0x8b, 0xa5, 0x05, 0x61, 0xdf, 0x1b, 0x2a, 0x44, 0x1d, 0x01, +}; + +/* CN=EM EV TLS CA - G3C,O=eMudhra Inc,C=US (secp384r1) + * SPKI SHA-256: 5c0660a2b164910d8ff589c18e14552e9e0ec237dcefe2cb736d41a4811f81a0 */ +static const unsigned char _distrusted_spki_em_ev_tls_ca_g3c[] = { + 0x30, 0x76, 0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, + 0x01, 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22, 0x03, 0x62, 0x00, 0x04, + 0x2d, 0x56, 0xe8, 0x29, 0xa6, 0xfb, 0x82, 0xc6, 0x4a, 0x46, 0x97, 0xd9, + 0x60, 0xaf, 0x95, 0x1e, 0x90, 0x67, 0xae, 0x91, 0xf9, 0x26, 0xd3, 0x43, + 0x06, 0xda, 0x48, 0xa8, 0xaa, 0xeb, 0x78, 0x69, 0xb6, 0xa6, 0x7e, 0x6d, + 0x27, 0x42, 0x6e, 0x04, 0x42, 0xaa, 0x03, 0xb3, 0x6e, 0x19, 0x88, 0xdf, + 0xf6, 0x36, 0x51, 0x4e, 0xe1, 0xcf, 0x07, 0xf5, 0xba, 0x4b, 0x3f, 0xf2, + 0xa0, 0x66, 0x45, 0xae, 0x7d, 0x31, 0xd5, 0x86, 0x6a, 0xd3, 0x1a, 0xa3, + 0xd2, 0x46, 0xeb, 0x0b, 0x31, 0x77, 0xee, 0x99, 0xcb, 0x75, 0x66, 0xc5, + 0x16, 0xce, 0x74, 0x80, 0x8a, 0x66, 0x8f, 0x91, 0xf0, 0xc4, 0x7f, 0xca, +}; + +/* CN=EM OV TLS CA - G3C,O=eMudhra Inc,C=US (secp384r1) + * SPKI SHA-256: 63ecefee431d49523bc1136c4e72d0206a477086caf606cd5cf025686c109cf8 */ +static const unsigned char _distrusted_spki_em_ov_tls_ca_g3c[] = { + 0x30, 0x76, 0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, + 0x01, 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22, 0x03, 0x62, 0x00, 0x04, + 0x13, 0x82, 0x51, 0xcd, 0x52, 0xac, 0x92, 0x98, 0xc1, 0xc8, 0xaa, 0xd9, + 0x77, 0x32, 0x1d, 0xeb, 0x97, 0xe7, 0x09, 0xbd, 0x0b, 0x4c, 0xa0, 0xac, + 0xa5, 0x5d, 0xc8, 0xad, 0x51, 0xdc, 0xfc, 0x9d, 0x15, 0x89, 0xa1, 0x59, + 0x7e, 0x3a, 0x51, 0x20, 0xe1, 0xef, 0xd6, 0x31, 0xc6, 0x3e, 0x18, 0x35, + 0xca, 0xca, 0xe2, 0x98, 0x69, 0xa6, 0x2e, 0x16, 0x31, 0xe8, 0xa2, 0x81, + 0x81, 0xab, 0x56, 0x61, 0x6d, 0xc4, 0x5d, 0x91, 0x8a, 0xbc, 0x09, 0xf3, + 0xab, 0x0e, 0x63, 0xcf, 0x79, 0x2a, 0xa4, 0xdc, 0xed, 0x73, 0x87, 0xbe, + 0x37, 0xbb, 0xa5, 0x69, 0x54, 0x9f, 0x1c, 0x02, 0xb2, 0x70, 0xed, 0x67, +}; + +/* CN=EM OV CS CA - G3C,O=eMudhra Inc,C=US (secp384r1) + * SPKI SHA-256: 92c063d67e8f14db71d288e11266ffdaf69cbf115f7bab14f3bc2f68d25e0e30 */ +static const unsigned char _distrusted_spki_em_ov_cs_ca_g3c[] = { + 0x30, 0x76, 0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, + 0x01, 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22, 0x03, 0x62, 0x00, 0x04, + 0xb9, 0xf0, 0xe0, 0x63, 0x47, 0x02, 0xa6, 0x9c, 0xa6, 0xaa, 0xdd, 0x44, + 0x3c, 0xfd, 0x62, 0xc4, 0x1b, 0x9b, 0xa4, 0xb2, 0x09, 0x24, 0x69, 0x7d, + 0x79, 0x74, 0xe7, 0x69, 0x1a, 0x12, 0x6f, 0xb9, 0x6a, 0xfb, 0x3e, 0x34, + 0xc2, 0xfd, 0x3b, 0xc5, 0xf4, 0x83, 0x06, 0x1f, 0x26, 0x5d, 0xc2, 0xa5, + 0x84, 0x36, 0x34, 0x73, 0x23, 0x51, 0x04, 0x36, 0xb2, 0x8f, 0x04, 0xf3, + 0x86, 0x23, 0x63, 0x06, 0x79, 0x80, 0xe2, 0x50, 0x3e, 0x02, 0x53, 0x2d, + 0x65, 0x46, 0x23, 0x5a, 0x71, 0x65, 0xa2, 0x59, 0xa1, 0xef, 0x49, 0x6f, + 0x74, 0x1b, 0x5f, 0xde, 0xe7, 0x78, 0x72, 0x08, 0xe4, 0xb8, 0xfa, 0xef, +}; + +/* CN=EM EV TLS CA - G3B,O=eMudhra BV,C=NL (secp384r1) + * SPKI SHA-256: e0254c7fcdc265e2f0f8a428dabb75ab4ae039b4872d62f5ee57f0877d5a8a41 */ +static const unsigned char _distrusted_spki_em_ev_tls_ca_g3b[] = { + 0x30, 0x76, 0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, + 0x01, 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22, 0x03, 0x62, 0x00, 0x04, + 0x25, 0xdb, 0xa8, 0x5a, 0x64, 0xd3, 0x97, 0xda, 0x0b, 0x10, 0x52, 0xe9, + 0xea, 0x54, 0x6e, 0x48, 0x7b, 0xef, 0xea, 0x9c, 0x9d, 0x9d, 0x68, 0xa0, + 0xc5, 0xea, 0xdc, 0xfe, 0xef, 0x75, 0xa6, 0x9b, 0xb5, 0xd6, 0xfc, 0x01, + 0xf4, 0x2b, 0xfb, 0xca, 0x6e, 0xb4, 0x0b, 0xcd, 0x64, 0xc0, 0x76, 0x7e, + 0x36, 0xff, 0xd0, 0x5d, 0x97, 0xa8, 0x13, 0xc5, 0xaa, 0x0c, 0x6c, 0x95, + 0x30, 0xe7, 0x9c, 0xf5, 0xf0, 0x86, 0x64, 0x68, 0xf1, 0x38, 0x06, 0x42, + 0x0a, 0xa5, 0xc8, 0x51, 0x84, 0x0b, 0x1e, 0x25, 0x25, 0x40, 0xe7, 0x90, + 0xe4, 0xb6, 0x54, 0xc3, 0x85, 0x2e, 0xd6, 0x4a, 0xf1, 0xe8, 0x65, 0x7d, +}; + +/* CN=EM DV TLS CA - G3B,O=eMudhra BV,C=NL (secp384r1) + * SPKI SHA-256: ebed6475c2c2d80fcb199f3fe8dd6e6b3ce60b77eb15d4e42c3dec0c6f5b5105 */ +static const unsigned char _distrusted_spki_em_dv_tls_ca_g3b[] = { + 0x30, 0x76, 0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, + 0x01, 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22, 0x03, 0x62, 0x00, 0x04, + 0xe7, 0x9e, 0xd9, 0xc0, 0x4a, 0x97, 0x73, 0xc4, 0x73, 0x5e, 0x51, 0x11, + 0x1d, 0xa8, 0x5a, 0xc2, 0xbd, 0x5e, 0x64, 0xab, 0x37, 0x4b, 0x80, 0xe1, + 0x2c, 0xbc, 0x20, 0x38, 0xec, 0x1e, 0x4f, 0x0e, 0x3b, 0x81, 0x2f, 0xc9, + 0x85, 0x9e, 0x1e, 0x67, 0xaf, 0xb0, 0x4e, 0x2f, 0x8b, 0xc9, 0x7f, 0x74, + 0x85, 0xba, 0x65, 0xaa, 0x91, 0x1e, 0xe8, 0xa4, 0xe4, 0x5b, 0x62, 0xd5, + 0xcc, 0x03, 0xd5, 0x6b, 0xe6, 0x78, 0xb3, 0xc3, 0x8c, 0xb4, 0xdc, 0x93, + 0x7c, 0xa2, 0x46, 0x0e, 0x23, 0x84, 0x53, 0xc8, 0x92, 0x93, 0x5d, 0x29, + 0xd4, 0x4f, 0x91, 0xe1, 0x56, 0x06, 0xb6, 0x3c, 0xce, 0xda, 0xb4, 0x68, +}; + +/* CN=EM OV CS CA - G3B,O=eMudhra BV,C=NL (secp384r1) + * SPKI SHA-256: ec4ffa1ad0ac88081314b7aaffef6a91f97cd57291bb5ba686c5415f5eb4f6b6 */ +static const unsigned char _distrusted_spki_em_ov_cs_ca_g3b[] = { + 0x30, 0x76, 0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, + 0x01, 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22, 0x03, 0x62, 0x00, 0x04, + 0xaa, 0xbb, 0x0f, 0xb0, 0x66, 0x6a, 0xcf, 0xcf, 0x01, 0xfa, 0xd5, 0x2b, + 0xfc, 0xfd, 0x4c, 0x2b, 0xf2, 0xcb, 0x3c, 0x34, 0x97, 0xde, 0x73, 0x95, + 0x0e, 0x58, 0x19, 0xb6, 0x1f, 0xe6, 0x95, 0x09, 0x87, 0x41, 0xc8, 0xc4, + 0x91, 0x43, 0x9b, 0x34, 0xa0, 0x43, 0x1c, 0xaf, 0xfe, 0x5b, 0xf9, 0xb4, + 0xc1, 0xc8, 0xe8, 0xae, 0x00, 0x01, 0xf9, 0xf4, 0xdc, 0x8a, 0xd8, 0x2c, + 0x59, 0xf3, 0x63, 0x9e, 0x96, 0xcd, 0x6c, 0x3d, 0x4d, 0xb0, 0xce, 0xfe, + 0xc2, 0x6c, 0x3d, 0x62, 0x51, 0x7b, 0x37, 0x76, 0x13, 0x72, 0x74, 0x3f, + 0x8e, 0x8a, 0xdd, 0x77, 0xb1, 0x00, 0x64, 0xf2, 0xab, 0xe5, 0xe0, 0x50, +}; + +static const struct { + const unsigned char *der; + size_t len; +} _distrusted_spki[] = { + {_distrusted_spki_em_ov_tls_ca_g3b, sizeof(_distrusted_spki_em_ov_tls_ca_g3b)}, + {_distrusted_spki_em_dv_tls_ca_g3c, sizeof(_distrusted_spki_em_dv_tls_ca_g3c)}, + {_distrusted_spki_em_ev_tls_ca_g3c, sizeof(_distrusted_spki_em_ev_tls_ca_g3c)}, + {_distrusted_spki_em_ov_tls_ca_g3c, sizeof(_distrusted_spki_em_ov_tls_ca_g3c)}, + {_distrusted_spki_em_ov_cs_ca_g3c, sizeof(_distrusted_spki_em_ov_cs_ca_g3c)}, + {_distrusted_spki_em_ev_tls_ca_g3b, sizeof(_distrusted_spki_em_ev_tls_ca_g3b)}, + {_distrusted_spki_em_dv_tls_ca_g3b, sizeof(_distrusted_spki_em_dv_tls_ca_g3b)}, + {_distrusted_spki_em_ov_cs_ca_g3b, sizeof(_distrusted_spki_em_ov_cs_ca_g3b)}, +}; + +#endif /* !Py_SSL_DISTRUSTED_SPKI_H */ diff --git a/PCbuild/_ssl.vcxproj b/PCbuild/_ssl.vcxproj index 127b76fe11e3290..80e5fe470b2e191 100644 --- a/PCbuild/_ssl.vcxproj +++ b/PCbuild/_ssl.vcxproj @@ -99,6 +99,7 @@ + diff --git a/PCbuild/_ssl.vcxproj.filters b/PCbuild/_ssl.vcxproj.filters index e96da0580e01b44..d6b9d4bdf36e84d 100644 --- a/PCbuild/_ssl.vcxproj.filters +++ b/PCbuild/_ssl.vcxproj.filters @@ -20,6 +20,9 @@ Header Files + + Header Files + diff --git a/Tools/ssl/emudhra_denylist_check.py b/Tools/ssl/emudhra_denylist_check.py new file mode 100644 index 000000000000000..1b3484eab435ce8 --- /dev/null +++ b/Tools/ssl/emudhra_denylist_check.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +"""Check the _ssl SPKI denylist against the live eMudhra test services.""" +import socket +import ssl + +# emSign ECC Root CA - G3 +ROOT_PEM = """\ +-----BEGIN CERTIFICATE----- +MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQG +EwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNo +bm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g +RzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4MTgzMDAwWjBrMQswCQYDVQQGEwJJ +TjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9s +b2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMw +djAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0 +WXTsuwYc58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xyS +fvalY8L1X44uT6EYGQIrMgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuB +zhccLikenEhjQjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggq +hkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+DCBeQyh+KTOgNG3qxrdWB +CUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7jHvrZQnD ++JbNR6iC8hZVdyR+EhCVBCyj +-----END CERTIFICATE----- +""" + +ENDPOINTS = [ + "dv-g3b-emudhra.we-have-a-problem.com", + "dv-g3b-aaa-emudhra.we-have-a-problem.com", +] + + +def check(host, port=443): + ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + ctx.load_verify_locations(cadata=ROOT_PEM) # trust only the emSign root + try: + with socket.create_connection((host, port), timeout=15) as raw: + with ctx.wrap_socket(raw, server_hostname=host) as s: + s.sendall( + b"GET / HTTP/1.0\r\nHost: " + host.encode() + b"\r\n\r\n" + ) + status = s.recv(64).split(b"\r\n", 1)[0].decode("latin1") + cipher = s.cipher()[0] + return f"CONNECTED ({status}, {cipher}) -- denylist did NOT fire!" + except ssl.SSLCertVerificationError as exc: + return (f"REJECTED as expected: verify_code={exc.verify_code} " + f"{exc.verify_message!r}") + + +if __name__ == "__main__": + print("OpenSSL:", ssl.OPENSSL_VERSION) + for host in ENDPOINTS: + print(f"\n{host}:") + print(" ", check(host)) From 4ff0516f8a30322cd7190d611818b1ad220dbb38 Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Wed, 21 Oct 2026 17:31:05 +0200 Subject: [PATCH 2/7] gh-160040: Fix c-analyzer check and add What's New entry Name the SPKI table struct type and list the const table in the c-analyzer "should be const" ignore list, so check-c-globals passes. Document the new verification behavior in What's New for 3.16. Signed-off-by: Christian Heimes Co-Authored-By: Claude Opus 4.8 (1M context) --- Doc/whatsnew/3.16.rst | 7 +++++++ Modules/_ssl/distrusted_spki.h | 6 ++++-- Tools/c-analyzer/cpython/ignored.tsv | 1 + 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index c2177d0ca3e286e..c8a2b5ff8f4d5a1 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -667,6 +667,13 @@ ssl the module. (Contributed by Serhiy Storchaka in :gh:`157513`.) +* The :mod:`ssl` module now rejects TLS certificate chains that contain one of a + set of compromised eMudhra intermediate CA keys. As defense in depth, the + connection is blocked during certificate verification even when the platform + root store (or a bundled trust store such as :mod:`!certifi`) still trusts the + issuing roots. + (Contributed by Christian Heimes in :gh:`160040`.) + symtable -------- diff --git a/Modules/_ssl/distrusted_spki.h b/Modules/_ssl/distrusted_spki.h index 1330ce6dbee7edc..131631212475545 100644 --- a/Modules/_ssl/distrusted_spki.h +++ b/Modules/_ssl/distrusted_spki.h @@ -138,10 +138,12 @@ static const unsigned char _distrusted_spki_em_ov_cs_ca_g3b[] = { 0x8e, 0x8a, 0xdd, 0x77, 0xb1, 0x00, 0x64, 0xf2, 0xab, 0xe5, 0xe0, 0x50, }; -static const struct { +typedef struct { const unsigned char *der; size_t len; -} _distrusted_spki[] = { +} py_distrusted_spki; + +static const py_distrusted_spki _distrusted_spki[] = { {_distrusted_spki_em_ov_tls_ca_g3b, sizeof(_distrusted_spki_em_ov_tls_ca_g3b)}, {_distrusted_spki_em_dv_tls_ca_g3c, sizeof(_distrusted_spki_em_dv_tls_ca_g3c)}, {_distrusted_spki_em_ev_tls_ca_g3c, sizeof(_distrusted_spki_em_ev_tls_ca_g3c)}, diff --git a/Tools/c-analyzer/cpython/ignored.tsv b/Tools/c-analyzer/cpython/ignored.tsv index db9f74fdbc591f0..7830fc7be1595c5 100644 --- a/Tools/c-analyzer/cpython/ignored.tsv +++ b/Tools/c-analyzer/cpython/ignored.tsv @@ -257,6 +257,7 @@ Modules/_sre/sre.c pattern_repr flag_names - # XXX I'm pretty sure this is actually constant: Modules/_sre/sre_targets.h - sre_targets - Modules/_sre.c pattern_repr flag_names - +Modules/_ssl/distrusted_spki.h - _distrusted_spki - Modules/_struct.c - bigendian_table - Modules/_struct.c - lilendian_table - Modules/_struct.c - native_table - From 462e69e6cbc3a43f66d6432686517a5030e31ec4 Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Wed, 21 Oct 2026 18:34:33 +0200 Subject: [PATCH 3/7] gh-160040: Add tests for distrusted eMudhra intermediate CA keys Signed-off-by: Christian Heimes --- Lib/test/certdata/distrusted_intermediate.key | 6 + Lib/test/certdata/distrusted_intermediate.pem | 21 ++++ Lib/test/certdata/distrusted_keycert.pem | 103 ++++++++++++++++++ Lib/test/certdata/distrusted_root.pem | 14 +++ Lib/test/certdata/make_ssl_certs.py | 53 ++++++++- Lib/test/test_ssl.py | 21 ++++ 6 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 Lib/test/certdata/distrusted_intermediate.key create mode 100644 Lib/test/certdata/distrusted_intermediate.pem create mode 100644 Lib/test/certdata/distrusted_keycert.pem create mode 100644 Lib/test/certdata/distrusted_root.pem diff --git a/Lib/test/certdata/distrusted_intermediate.key b/Lib/test/certdata/distrusted_intermediate.key new file mode 100644 index 000000000000000..3df49ac650bf9c9 --- /dev/null +++ b/Lib/test/certdata/distrusted_intermediate.key @@ -0,0 +1,6 @@ +-----BEGIN PRIVATE KEY----- +MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAShZANiAAQTglHNUqyS +mMHIqtl3Mh3rl+cJvQtMoKylXcitUdz8nRWJoVl+OlEg4e/WMcY+GDXKyuKYaaYu +FjHoooGBq1ZhbcRdkYq8CfOrDmPPeSqk3O1zh743u6VpVJ8cArJw7Wc= +-----END PRIVATE KEY----- diff --git a/Lib/test/certdata/distrusted_intermediate.pem b/Lib/test/certdata/distrusted_intermediate.pem new file mode 100644 index 000000000000000..0f4083092ff8ab9 --- /dev/null +++ b/Lib/test/certdata/distrusted_intermediate.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDaDCCAu6gAwIBAgIPKWkmwiejoVJjkrGUgqUEMAoGCCqGSM49BAMDMFYxCzAJ +BgNVBAYTAklOMSUwIwYDVQQKDBxlTXVkaHJhIFRlY2hub2xvZ2llcyBMaW1pdGVk +MSAwHgYDVQQDDBdlbVNpZ24gUm9vdCBUTFMgQ0EgLSBHMzAeFw0yNTA0MzAxNTM3 +NDFaFw00MDA0MjgxODMwMDBaMEAxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtlTXVk +aHJhIEluYzEbMBkGA1UEAwwSRU0gT1YgVExTIENBIC0gRzNDMHYwEAYHKoZIzj0C +AQYFK4EEACIDYgAEE4JRzVKskpjByKrZdzId65fnCb0LTKCspV3IrVHc/J0ViaFZ +fjpRIOHv1jHGPhg1ysrimGmmLhYx6KKBgatWYW3EXZGKvAnzqw5jz3kqpNztc4e+ +N7ulaVSfHAKycO1no4IBljCCAZIwHwYDVR0jBBgwFoAU4TqrIXzx401UrtRyt/2L +WUIgkFQwHQYDVR0OBBYEFDTq6gzryQra+dDF4u2beMmHnhtsMB0GA1UdJQQWMBQG +CCsGAQUFBwMBBggrBgEFBQcDAjBdBgNVHSAEVjBUMDkGCysGAQQBg44hAQABMCow +KAYIKwYBBQUHAgEWHGh0dHA6Ly9yZXBvc2l0b3J5LmVtc2lnbi5jb20wDQYLKwYB +BAGDjiEBAm4wCAYGZ4EMAQICMHUGCCsGAQUFBwEBBGkwZzAkBggrBgEFBQcwAYYY +aHR0cDovL29jc3AtYy5lbXNpZ24uY29tMD8GCCsGAQUFBzAChjNodHRwOi8vcmVw +b3NpdG9yeS5lbXNpZ24uY29tL2NlcnRzL0VNT1ZUTFNDQUczQy5wN2MwNwYDVR0f +BDAwLjAsoCqgKIYmaHR0cDovL2NybC5lbXNpZ24uY29tLz9yb290dGxzY2FnMy5j +cmwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwCgYIKoZIzj0E +AwMDaAAwZQIxAMSZt2xsPeumRdeSMSQ3CbzIZeQ/q7GDCWfeE1Pq+mhgA6gRM44c +NVF3xJ+wsxJk+wIwFJbaDMokD8Ei6eNOiA8yUMYvfzQyngmrhZQMdPCK1SewrR79 +4rSb9vGqxZzelQth +-----END CERTIFICATE----- diff --git a/Lib/test/certdata/distrusted_keycert.pem b/Lib/test/certdata/distrusted_keycert.pem new file mode 100644 index 000000000000000..56883134b22ba6e --- /dev/null +++ b/Lib/test/certdata/distrusted_keycert.pem @@ -0,0 +1,103 @@ +-----BEGIN PRIVATE KEY----- +MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDUsqqTdO31pKVm/pms +2U/fFBtZuTcDWM8k+YQIem+jfcHpwUg3upF1Wf2R+YDeHQOhZANiAARIf6AzMYwz +ro/nQbOg8Z3aPaNXhydFTAeMNeYwFuzM9Yqx1kFpXYidvKQYtoyqBtjAPiWWKOCo +f8AI3pFH0WslG1sFr1ty/9loE15sLKN4y9k7HMobn5qW2Svm5A7oQBo= +-----END PRIVATE KEY----- +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + cb:2d:80:99:5a:69:52:5b + Signature Algorithm: ecdsa-with-SHA256 + Issuer: C=US, O=eMudhra Inc, CN=EM OV TLS CA - G3C + Validity + Not Before: Aug 29 14:23:16 2018 GMT + Not After : Oct 28 14:23:16 2525 GMT + Subject: C=XY, L=Castle Anthrax, O=Python Software Foundation, CN=localhost + Subject Public Key Info: + Public Key Algorithm: id-ecPublicKey + Public-Key: (384 bit) + pub: + 04:48:7f:a0:33:31:8c:33:ae:8f:e7:41:b3:a0:f1: + 9d:da:3d:a3:57:87:27:45:4c:07:8c:35:e6:30:16: + ec:cc:f5:8a:b1:d6:41:69:5d:88:9d:bc:a4:18:b6: + 8c:aa:06:d8:c0:3e:25:96:28:e0:a8:7f:c0:08:de: + 91:47:d1:6b:25:1b:5b:05:af:5b:72:ff:d9:68:13: + 5e:6c:2c:a3:78:cb:d9:3b:1c:ca:1b:9f:9a:96:d9: + 2b:e6:e4:0e:e8:40:1a + ASN1 OID: secp384r1 + NIST CURVE: P-384 + X509v3 extensions: + X509v3 Subject Alternative Name: + DNS:localhost + X509v3 Key Usage: critical + Digital Signature, Key Encipherment + X509v3 Extended Key Usage: + TLS Web Server Authentication, TLS Web Client Authentication + X509v3 Basic Constraints: critical + CA:FALSE + X509v3 Subject Key Identifier: + 48:B5:07:21:1D:FB:CA:0D:ED:37:78:BC:9D:86:A9:18:D8:BD:37:28 + X509v3 Authority Key Identifier: + keyid:34:EA:EA:0C:EB:C9:0A:DA:F9:D0:C5:E2:ED:9B:78:C9:87:9E:1B:6C + DirName:/C=IN/O=eMudhra Technologies Limited/CN=emSign Root TLS CA - G3 + serial:29:69:26:C2:27:A3:A1:52:63:92:B1:94:82:A5:04 + Authority Information Access: + CA Issuers - URI:http://testca.pythontest.net/testca/pycacert.cer + OCSP - URI:http://testca.pythontest.net/testca/ocsp/ + X509v3 CRL Distribution Points: + Full Name: + URI:http://testca.pythontest.net/testca/revocation.crl + + Signature Algorithm: ecdsa-with-SHA256 + Signature Value: + 30:66:02:31:00:f2:6e:00:c2:93:f3:8a:7b:7a:d2:78:02:6f: + c8:74:ea:85:a8:4e:04:e7:8e:6e:cd:dd:e8:8d:0b:ee:db:06: + cb:33:be:34:95:09:15:d5:3d:10:31:b5:15:c0:4a:a6:43:02: + 31:00:c6:59:1b:76:ef:93:2f:62:21:11:84:b8:88:5e:8a:bc: + 9f:ed:5e:74:fa:90:76:14:36:e2:55:b6:28:c2:40:14:0e:f7: + e2:66:9c:b9:12:e1:07:d8:6a:74:2c:c2:fe:7d +-----BEGIN CERTIFICATE----- +MIIDqjCCAy+gAwIBAgIJAMstgJlaaVJbMAoGCCqGSM49BAMCMEAxCzAJBgNVBAYT +AlVTMRQwEgYDVQQKDAtlTXVkaHJhIEluYzEbMBkGA1UEAwwSRU0gT1YgVExTIENB +IC0gRzNDMCAXDTE4MDgyOTE0MjMxNloYDzI1MjUxMDI4MTQyMzE2WjBfMQswCQYD +VQQGEwJYWTEXMBUGA1UEBwwOQ2FzdGxlIEFudGhyYXgxIzAhBgNVBAoMGlB5dGhv +biBTb2Z0d2FyZSBGb3VuZGF0aW9uMRIwEAYDVQQDDAlsb2NhbGhvc3QwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAARIf6AzMYwzro/nQbOg8Z3aPaNXhydFTAeMNeYwFuzM +9Yqx1kFpXYidvKQYtoyqBtjAPiWWKOCof8AI3pFH0WslG1sFr1ty/9loE15sLKN4 +y9k7HMobn5qW2Svm5A7oQBqjggHSMIIBzjAUBgNVHREEDTALgglsb2NhbGhvc3Qw +DgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAM +BgNVHRMBAf8EAjAAMB0GA1UdDgQWBBRItQchHfvKDe03eLydhqkY2L03KDCBjgYD +VR0jBIGGMIGDgBQ06uoM68kK2vnQxeLtm3jJh54bbKFapFgwVjELMAkGA1UEBhMC +SU4xJTAjBgNVBAoMHGVNdWRocmEgVGVjaG5vbG9naWVzIExpbWl0ZWQxIDAeBgNV +BAMMF2VtU2lnbiBSb290IFRMUyBDQSAtIEczgg8paSbCJ6OhUmOSsZSCpQQwgYMG +CCsGAQUFBwEBBHcwdTA8BggrBgEFBQcwAoYwaHR0cDovL3Rlc3RjYS5weXRob250 +ZXN0Lm5ldC90ZXN0Y2EvcHljYWNlcnQuY2VyMDUGCCsGAQUFBzABhilodHRwOi8v +dGVzdGNhLnB5dGhvbnRlc3QubmV0L3Rlc3RjYS9vY3NwLzBDBgNVHR8EPDA6MDig +NqA0hjJodHRwOi8vdGVzdGNhLnB5dGhvbnRlc3QubmV0L3Rlc3RjYS9yZXZvY2F0 +aW9uLmNybDAKBggqhkjOPQQDAgNpADBmAjEA8m4AwpPzint60ngCb8h06oWoTgTn +jm7N3eiNC+7bBsszvjSVCRXVPRAxtRXASqZDAjEAxlkbdu+TL2IhEYS4iF6KvJ/t +XnT6kHYUNuJVtijCQBQO9+JmnLkS4QfYanQswv59 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDaDCCAu6gAwIBAgIPKWkmwiejoVJjkrGUgqUEMAoGCCqGSM49BAMDMFYxCzAJ +BgNVBAYTAklOMSUwIwYDVQQKDBxlTXVkaHJhIFRlY2hub2xvZ2llcyBMaW1pdGVk +MSAwHgYDVQQDDBdlbVNpZ24gUm9vdCBUTFMgQ0EgLSBHMzAeFw0yNTA0MzAxNTM3 +NDFaFw00MDA0MjgxODMwMDBaMEAxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtlTXVk +aHJhIEluYzEbMBkGA1UEAwwSRU0gT1YgVExTIENBIC0gRzNDMHYwEAYHKoZIzj0C +AQYFK4EEACIDYgAEE4JRzVKskpjByKrZdzId65fnCb0LTKCspV3IrVHc/J0ViaFZ +fjpRIOHv1jHGPhg1ysrimGmmLhYx6KKBgatWYW3EXZGKvAnzqw5jz3kqpNztc4e+ +N7ulaVSfHAKycO1no4IBljCCAZIwHwYDVR0jBBgwFoAU4TqrIXzx401UrtRyt/2L +WUIgkFQwHQYDVR0OBBYEFDTq6gzryQra+dDF4u2beMmHnhtsMB0GA1UdJQQWMBQG +CCsGAQUFBwMBBggrBgEFBQcDAjBdBgNVHSAEVjBUMDkGCysGAQQBg44hAQABMCow +KAYIKwYBBQUHAgEWHGh0dHA6Ly9yZXBvc2l0b3J5LmVtc2lnbi5jb20wDQYLKwYB +BAGDjiEBAm4wCAYGZ4EMAQICMHUGCCsGAQUFBwEBBGkwZzAkBggrBgEFBQcwAYYY +aHR0cDovL29jc3AtYy5lbXNpZ24uY29tMD8GCCsGAQUFBzAChjNodHRwOi8vcmVw +b3NpdG9yeS5lbXNpZ24uY29tL2NlcnRzL0VNT1ZUTFNDQUczQy5wN2MwNwYDVR0f +BDAwLjAsoCqgKIYmaHR0cDovL2NybC5lbXNpZ24uY29tLz9yb290dGxzY2FnMy5j +cmwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwCgYIKoZIzj0E +AwMDaAAwZQIxAMSZt2xsPeumRdeSMSQ3CbzIZeQ/q7GDCWfeE1Pq+mhgA6gRM44c +NVF3xJ+wsxJk+wIwFJbaDMokD8Ei6eNOiA8yUMYvfzQyngmrhZQMdPCK1SewrR79 +4rSb9vGqxZzelQth +-----END CERTIFICATE----- diff --git a/Lib/test/certdata/distrusted_root.pem b/Lib/test/certdata/distrusted_root.pem new file mode 100644 index 000000000000000..3b8be38cb380811 --- /dev/null +++ b/Lib/test/certdata/distrusted_root.pem @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICKTCCAa+gAwIBAgIQDnYGcvFDRZ/I/gqwvAXjlDAKBggqhkjOPQQDAzBWMQsw +CQYDVQQGEwJJTjElMCMGA1UECgwcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRl +ZDEgMB4GA1UEAwwXZW1TaWduIFJvb3QgVExTIENBIC0gRzMwHhcNMjQwMjA4MDgx +OTMyWhcNNDkwMjA3MDgxOTMyWjBWMQswCQYDVQQGEwJJTjElMCMGA1UECgwcZU11 +ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAwwXZW1TaWduIFJvb3Qg +VExTIENBIC0gRzMwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARfD9w/q1cWxL6D9bMG +H6CQ4u+gAV6GN9s+PwQx9Bl4hC+0iU8QOk8mWO/RwXQy10DCSxT8vSIJClHKp+Ao +4qJKdYeV0SwZ7LMQziyLfxIUOY6Grja/sQFwLKYf8EdDXAyjQjBAMB0GA1UdDgQW +BBThOqshfPHjTVSu1HK3/YtZQiCQVDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjBhCFTkly7Yl7+s4hiw+k6ZJTXT2AiI +rvmsmfqPr5E7ecwXb6kXA5LTFd6rjwTuV3sCMQC9w6ZSbVq1mveMO6dKFziOzClT +KCT2RFT6yXtq6nzgRICgBmiXbJAXsmZ8yAwjyWc= +-----END CERTIFICATE----- diff --git a/Lib/test/certdata/make_ssl_certs.py b/Lib/test/certdata/make_ssl_certs.py index 18e614496385fd4..4d1c149e4580ecd 100644 --- a/Lib/test/certdata/make_ssl_certs.py +++ b/Lib/test/certdata/make_ssl_certs.py @@ -118,7 +118,6 @@ here = os.path.abspath(os.path.dirname(__file__)) - def make_cert_key(cmdlineargs, hostname, sign=False, extra_san='', ext='req_x509_extensions_full', key='rsa:3072'): print("creating cert for " + hostname) @@ -230,6 +229,56 @@ def write_cert_reference(path): print(refdata, file=f) +# eMudhra intermediate on the _ssl distrusted list, its secp384r1 key is 4. +# See https://emudhra.we-have-a-problem.com/ +DISTRUSTED_INTERMEDIATE = 'distrusted_intermediate.pem' +DISTRUSTED_INTERMEDIATE_KEY = 'distrusted_intermediate.key' + + +def make_distrusted_spki_cert(cmdlineargs): + # leaf signed by the distrusted intermediate, chains to distrusted_root.pem + print("creating distrusted-SPKI test cert") + tempnames = [] + for i in range(4): + with tempfile.NamedTemporaryFile(delete=False) as f: + tempnames.append(f.name) + req_file, key_file, csr_file, cert_file = tempnames + try: + req = req_template.format( + hostname='localhost', + extra_san='', + startdate=startdate, + enddate=cmdlineargs.enddate, + days=cmdlineargs.days + ) + with open(req_file, 'w') as f: + f.write(req) + # ECC leaf to match the ECC intermediate + check_call(['openssl', 'req', '-new', '-nodes', '-days', cmdlineargs.days, + '-newkey', 'param:secp384r1.pem', '-keyout', key_file, + '-config', req_file, '-out', csr_file]) + # sign with the distrusted intermediate, not the test CA + check_call(['openssl', 'ca', '-config', req_file, + '-extensions', 'req_x509_extensions_full', + '-cert', DISTRUSTED_INTERMEDIATE, '-keyfile', DISTRUSTED_INTERMEDIATE_KEY, + '-out', cert_file, '-outdir', 'cadir', + '-policy', 'policy_anything', '-batch', '-infiles', csr_file]) + with open(key_file) as f: + key = f.read() + with open(cert_file) as f: + cert = f.read() + with open(DISTRUSTED_INTERMEDIATE) as f: + intermediate = f.read() + # server sends leaf + intermediate, client trusts the root + with open('distrusted_keycert.pem', 'w') as f: + f.write(key) + f.write(cert) + f.write(intermediate) + finally: + for name in tempnames: + os.remove(name) + + if __name__ == '__main__': parser = argparse.ArgumentParser(description='Make the custom certificate and private key files used by test_ssl and friends.') parser.add_argument('--days', default=days_default) @@ -320,6 +369,8 @@ def write_cert_reference(path): f.write(key) f.write(cert) + make_distrusted_spki_cert(cmdlineargs) + unmake_ca() print("Writing out reference data for Lib/test/test_ssl.py and Lib/test/test_asyncio/utils.py") write_cert_reference('keycert.pem') diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index d4213482cd1215c..f2486f945c596ab 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -130,6 +130,13 @@ def data_file(*name): LEAF_MISSING_AKI_CERTFILE_HOSTNAME = "example.com" LEAF_MISSING_AKI_CA = data_file("leaf-missing-aki.ca.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. +DISTRUSTED_SPKI_CERTFILE = data_file("distrusted_keycert.pem") +DISTRUSTED_SPKI_CA = data_file("distrusted_root.pem") +DISTRUSTED_SPKI_HOSTNAME = "localhost" + # Same certificate as pycacert.pem, but without extra text in file SIGNING_CA = data_file("capath", "ceff1710.0") # cert with all kinds of subject alt names @@ -2280,6 +2287,20 @@ def test_unwrap(self): c_in.write(s_out.read()) client.unwrap() + 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) + client_context.load_verify_locations(DISTRUSTED_SPKI_CA) + server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + server_context.load_cert_chain(DISTRUSTED_SPKI_CERTFILE) + with self.assertRaises(ssl.SSLCertVerificationError) as cm: + connected_bio_pair(client_context, server_context, + DISTRUSTED_SPKI_HOSTNAME) + # X509_V_ERR_CERT_REJECTED + self.assertEqual(cm.exception.verify_code, 28) + def test_sni_callback_context_released_and_callback_raises(self): # Variant of the test below without a HelloRetryRequest: the callback # switches the connection to another context, drops the last From eb71f14acb48f0fa7ad93cbf1f4012fc30ddcff9 Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Wed, 21 Oct 2026 19:20:24 +0200 Subject: [PATCH 4/7] gh-160040: Match distrusted SPKI by SHA-256 digest Signed-off-by: Christian Heimes --- Modules/_ssl.c | 44 ++++-- Modules/_ssl/distrusted_spki.h | 200 ++++++++------------------- Tools/c-analyzer/cpython/ignored.tsv | 1 - 3 files changed, 85 insertions(+), 160 deletions(-) diff --git a/Modules/_ssl.c b/Modules/_ssl.c index d5071f316c81613..90fc648a1143823 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -67,6 +67,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" @@ -3373,40 +3374,55 @@ static PyType_Spec PySSLSocket_spec = { #include "_ssl/distrusted_spki.h" -/* True if the cert's SPKI is on the distrusted list */ +/* 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 0; + 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); + if (!ok || digest_len != sizeof(_distrusted_spki[0])) { + return -1; } - int distrusted = 0; for (size_t i = 0; i < Py_ARRAY_LENGTH(_distrusted_spki); i++) { - if ((size_t)spki_len == _distrusted_spki[i].len - && memcmp(spki, _distrusted_spki[i].der, _distrusted_spki[i].len) == 0) - { - distrusted = 1; - break; + if (memcmp(digest, _distrusted_spki[i], sizeof(_distrusted_spki[i])) == 0) { + return 1; } } - OPENSSL_free(spki); - return distrusted; + return 0; } -/* Verify callback, called once per cert. Reject distrusted SPKIs, otherwise - * keep OpenSSL's result. */ +/* Verify callback, called once per cert. Reject distrusted SPKIs. */ static int _ssl_verify_callback(int preverify_ok, X509_STORE_CTX *x509_ctx) { X509 *cert = X509_STORE_CTX_get_current_cert(x509_ctx); - if (cert != NULL && _cert_has_distrusted_spki(cert)) { + 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; } - return preverify_ok; } static int diff --git a/Modules/_ssl/distrusted_spki.h b/Modules/_ssl/distrusted_spki.h index 131631212475545..c8d989e675c6f12 100644 --- a/Modules/_ssl/distrusted_spki.h +++ b/Modules/_ssl/distrusted_spki.h @@ -1,157 +1,67 @@ /* Denylist of distrusted SubjectPublicKeyInfo (SPKI). * - * Verification rejects any chain with a cert whose SPKI (DER-encoded - * SubjectPublicKeyInfo) is in the _distrusted_spki table. We match the key, - * not a cert fingerprint, so cross-signs that reuse the key are blocked too. - * To block another key, add its SPKI DER and a table entry. + * 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. To block another key, add + * its digest. * - * Dump a cert's SPKI DER and its SHA-256 with OpenSSL: + * Compute a cert's SPKI SHA-256 digest bytes with OpenSSL: * - * $ openssl x509 -in cert.pem -noout -pubkey | openssl pkey -pubin -outform DER > spki.der - * $ xxd -i spki.der - * $ openssl dgst -sha256 spki.der + * $ 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 - /* Compromised eMudhra intermediates, https://emudhra.we-have-a-problem.com/ */ - -/* CN=EM OV TLS CA - G3B,O=eMudhra BV,C=NL (secp384r1) - * SPKI SHA-256: 43bffa19d6ef651451f6841e019931e96f6f2eaeff28ab4b6f1ce4576de2717b */ -static const unsigned char _distrusted_spki_em_ov_tls_ca_g3b[] = { - 0x30, 0x76, 0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, - 0x01, 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22, 0x03, 0x62, 0x00, 0x04, - 0x73, 0x89, 0x7a, 0xa0, 0xeb, 0x3c, 0xfd, 0xca, 0x8b, 0x12, 0x40, 0x2f, - 0xfa, 0xf0, 0xab, 0xc4, 0xe1, 0x21, 0x2b, 0xaf, 0x8d, 0xee, 0x77, 0x8a, - 0xc9, 0xee, 0x2d, 0x6d, 0xb8, 0x3f, 0xac, 0x28, 0x0c, 0x5e, 0x05, 0x63, - 0x8a, 0xe7, 0xa2, 0xac, 0xd7, 0x1c, 0x32, 0x2a, 0x8e, 0x36, 0x1c, 0xde, - 0xb9, 0xf6, 0x9c, 0x43, 0x02, 0xb9, 0xdd, 0x8d, 0x73, 0xd1, 0xcc, 0x6c, - 0xaa, 0xb2, 0xcc, 0x0a, 0xee, 0x4e, 0x21, 0x6c, 0xe9, 0x79, 0xdd, 0x51, - 0x8a, 0x4a, 0xf4, 0xb6, 0x26, 0xe5, 0x68, 0x4c, 0xdd, 0x65, 0x80, 0x37, - 0xed, 0x0c, 0x85, 0x4e, 0x4c, 0xc1, 0x2f, 0x4c, 0xf8, 0x64, 0x6d, 0x0d, -}; - -/* CN=EM DV TLS CA - G3C,O=eMudhra Inc,C=US (secp384r1) - * SPKI SHA-256: 49dd353dc2b919a3770645117d0e60c16115e7d1bc259eefe734b00ea434fe5f */ -static const unsigned char _distrusted_spki_em_dv_tls_ca_g3c[] = { - 0x30, 0x76, 0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, - 0x01, 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22, 0x03, 0x62, 0x00, 0x04, - 0x3a, 0x7a, 0x78, 0x62, 0xc8, 0xc4, 0x4d, 0x32, 0x4d, 0xdd, 0x69, 0x03, - 0x3a, 0x67, 0x82, 0xc0, 0xef, 0xee, 0xfe, 0x40, 0x55, 0xe6, 0x8a, 0x12, - 0x3f, 0xa0, 0x37, 0x1b, 0xa3, 0xbd, 0x91, 0x94, 0x51, 0x4e, 0xe1, 0x72, - 0xa4, 0x33, 0xe3, 0x63, 0x0f, 0x02, 0xbd, 0x18, 0x80, 0x37, 0xd9, 0xe5, - 0x14, 0x9f, 0x9b, 0x77, 0xdb, 0x94, 0xa2, 0x81, 0xdf, 0x98, 0x7b, 0x2f, - 0xea, 0x88, 0x8b, 0x74, 0xbd, 0xfe, 0x8f, 0xda, 0x4c, 0x10, 0x99, 0xee, - 0x6c, 0xa1, 0x95, 0x66, 0xfe, 0x3e, 0xf0, 0xe5, 0xf6, 0xb0, 0x9a, 0x55, - 0x6c, 0xcc, 0x8b, 0xa5, 0x05, 0x61, 0xdf, 0x1b, 0x2a, 0x44, 0x1d, 0x01, -}; - -/* CN=EM EV TLS CA - G3C,O=eMudhra Inc,C=US (secp384r1) - * SPKI SHA-256: 5c0660a2b164910d8ff589c18e14552e9e0ec237dcefe2cb736d41a4811f81a0 */ -static const unsigned char _distrusted_spki_em_ev_tls_ca_g3c[] = { - 0x30, 0x76, 0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, - 0x01, 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22, 0x03, 0x62, 0x00, 0x04, - 0x2d, 0x56, 0xe8, 0x29, 0xa6, 0xfb, 0x82, 0xc6, 0x4a, 0x46, 0x97, 0xd9, - 0x60, 0xaf, 0x95, 0x1e, 0x90, 0x67, 0xae, 0x91, 0xf9, 0x26, 0xd3, 0x43, - 0x06, 0xda, 0x48, 0xa8, 0xaa, 0xeb, 0x78, 0x69, 0xb6, 0xa6, 0x7e, 0x6d, - 0x27, 0x42, 0x6e, 0x04, 0x42, 0xaa, 0x03, 0xb3, 0x6e, 0x19, 0x88, 0xdf, - 0xf6, 0x36, 0x51, 0x4e, 0xe1, 0xcf, 0x07, 0xf5, 0xba, 0x4b, 0x3f, 0xf2, - 0xa0, 0x66, 0x45, 0xae, 0x7d, 0x31, 0xd5, 0x86, 0x6a, 0xd3, 0x1a, 0xa3, - 0xd2, 0x46, 0xeb, 0x0b, 0x31, 0x77, 0xee, 0x99, 0xcb, 0x75, 0x66, 0xc5, - 0x16, 0xce, 0x74, 0x80, 0x8a, 0x66, 0x8f, 0x91, 0xf0, 0xc4, 0x7f, 0xca, -}; - -/* CN=EM OV TLS CA - G3C,O=eMudhra Inc,C=US (secp384r1) - * SPKI SHA-256: 63ecefee431d49523bc1136c4e72d0206a477086caf606cd5cf025686c109cf8 */ -static const unsigned char _distrusted_spki_em_ov_tls_ca_g3c[] = { - 0x30, 0x76, 0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, - 0x01, 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22, 0x03, 0x62, 0x00, 0x04, - 0x13, 0x82, 0x51, 0xcd, 0x52, 0xac, 0x92, 0x98, 0xc1, 0xc8, 0xaa, 0xd9, - 0x77, 0x32, 0x1d, 0xeb, 0x97, 0xe7, 0x09, 0xbd, 0x0b, 0x4c, 0xa0, 0xac, - 0xa5, 0x5d, 0xc8, 0xad, 0x51, 0xdc, 0xfc, 0x9d, 0x15, 0x89, 0xa1, 0x59, - 0x7e, 0x3a, 0x51, 0x20, 0xe1, 0xef, 0xd6, 0x31, 0xc6, 0x3e, 0x18, 0x35, - 0xca, 0xca, 0xe2, 0x98, 0x69, 0xa6, 0x2e, 0x16, 0x31, 0xe8, 0xa2, 0x81, - 0x81, 0xab, 0x56, 0x61, 0x6d, 0xc4, 0x5d, 0x91, 0x8a, 0xbc, 0x09, 0xf3, - 0xab, 0x0e, 0x63, 0xcf, 0x79, 0x2a, 0xa4, 0xdc, 0xed, 0x73, 0x87, 0xbe, - 0x37, 0xbb, 0xa5, 0x69, 0x54, 0x9f, 0x1c, 0x02, 0xb2, 0x70, 0xed, 0x67, -}; - -/* CN=EM OV CS CA - G3C,O=eMudhra Inc,C=US (secp384r1) - * SPKI SHA-256: 92c063d67e8f14db71d288e11266ffdaf69cbf115f7bab14f3bc2f68d25e0e30 */ -static const unsigned char _distrusted_spki_em_ov_cs_ca_g3c[] = { - 0x30, 0x76, 0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, - 0x01, 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22, 0x03, 0x62, 0x00, 0x04, - 0xb9, 0xf0, 0xe0, 0x63, 0x47, 0x02, 0xa6, 0x9c, 0xa6, 0xaa, 0xdd, 0x44, - 0x3c, 0xfd, 0x62, 0xc4, 0x1b, 0x9b, 0xa4, 0xb2, 0x09, 0x24, 0x69, 0x7d, - 0x79, 0x74, 0xe7, 0x69, 0x1a, 0x12, 0x6f, 0xb9, 0x6a, 0xfb, 0x3e, 0x34, - 0xc2, 0xfd, 0x3b, 0xc5, 0xf4, 0x83, 0x06, 0x1f, 0x26, 0x5d, 0xc2, 0xa5, - 0x84, 0x36, 0x34, 0x73, 0x23, 0x51, 0x04, 0x36, 0xb2, 0x8f, 0x04, 0xf3, - 0x86, 0x23, 0x63, 0x06, 0x79, 0x80, 0xe2, 0x50, 0x3e, 0x02, 0x53, 0x2d, - 0x65, 0x46, 0x23, 0x5a, 0x71, 0x65, 0xa2, 0x59, 0xa1, 0xef, 0x49, 0x6f, - 0x74, 0x1b, 0x5f, 0xde, 0xe7, 0x78, 0x72, 0x08, 0xe4, 0xb8, 0xfa, 0xef, -}; - -/* CN=EM EV TLS CA - G3B,O=eMudhra BV,C=NL (secp384r1) - * SPKI SHA-256: e0254c7fcdc265e2f0f8a428dabb75ab4ae039b4872d62f5ee57f0877d5a8a41 */ -static const unsigned char _distrusted_spki_em_ev_tls_ca_g3b[] = { - 0x30, 0x76, 0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, - 0x01, 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22, 0x03, 0x62, 0x00, 0x04, - 0x25, 0xdb, 0xa8, 0x5a, 0x64, 0xd3, 0x97, 0xda, 0x0b, 0x10, 0x52, 0xe9, - 0xea, 0x54, 0x6e, 0x48, 0x7b, 0xef, 0xea, 0x9c, 0x9d, 0x9d, 0x68, 0xa0, - 0xc5, 0xea, 0xdc, 0xfe, 0xef, 0x75, 0xa6, 0x9b, 0xb5, 0xd6, 0xfc, 0x01, - 0xf4, 0x2b, 0xfb, 0xca, 0x6e, 0xb4, 0x0b, 0xcd, 0x64, 0xc0, 0x76, 0x7e, - 0x36, 0xff, 0xd0, 0x5d, 0x97, 0xa8, 0x13, 0xc5, 0xaa, 0x0c, 0x6c, 0x95, - 0x30, 0xe7, 0x9c, 0xf5, 0xf0, 0x86, 0x64, 0x68, 0xf1, 0x38, 0x06, 0x42, - 0x0a, 0xa5, 0xc8, 0x51, 0x84, 0x0b, 0x1e, 0x25, 0x25, 0x40, 0xe7, 0x90, - 0xe4, 0xb6, 0x54, 0xc3, 0x85, 0x2e, 0xd6, 0x4a, 0xf1, 0xe8, 0x65, 0x7d, -}; - -/* CN=EM DV TLS CA - G3B,O=eMudhra BV,C=NL (secp384r1) - * SPKI SHA-256: ebed6475c2c2d80fcb199f3fe8dd6e6b3ce60b77eb15d4e42c3dec0c6f5b5105 */ -static const unsigned char _distrusted_spki_em_dv_tls_ca_g3b[] = { - 0x30, 0x76, 0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, - 0x01, 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22, 0x03, 0x62, 0x00, 0x04, - 0xe7, 0x9e, 0xd9, 0xc0, 0x4a, 0x97, 0x73, 0xc4, 0x73, 0x5e, 0x51, 0x11, - 0x1d, 0xa8, 0x5a, 0xc2, 0xbd, 0x5e, 0x64, 0xab, 0x37, 0x4b, 0x80, 0xe1, - 0x2c, 0xbc, 0x20, 0x38, 0xec, 0x1e, 0x4f, 0x0e, 0x3b, 0x81, 0x2f, 0xc9, - 0x85, 0x9e, 0x1e, 0x67, 0xaf, 0xb0, 0x4e, 0x2f, 0x8b, 0xc9, 0x7f, 0x74, - 0x85, 0xba, 0x65, 0xaa, 0x91, 0x1e, 0xe8, 0xa4, 0xe4, 0x5b, 0x62, 0xd5, - 0xcc, 0x03, 0xd5, 0x6b, 0xe6, 0x78, 0xb3, 0xc3, 0x8c, 0xb4, 0xdc, 0x93, - 0x7c, 0xa2, 0x46, 0x0e, 0x23, 0x84, 0x53, 0xc8, 0x92, 0x93, 0x5d, 0x29, - 0xd4, 0x4f, 0x91, 0xe1, 0x56, 0x06, 0xb6, 0x3c, 0xce, 0xda, 0xb4, 0x68, -}; - -/* CN=EM OV CS CA - G3B,O=eMudhra BV,C=NL (secp384r1) - * SPKI SHA-256: ec4ffa1ad0ac88081314b7aaffef6a91f97cd57291bb5ba686c5415f5eb4f6b6 */ -static const unsigned char _distrusted_spki_em_ov_cs_ca_g3b[] = { - 0x30, 0x76, 0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, - 0x01, 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22, 0x03, 0x62, 0x00, 0x04, - 0xaa, 0xbb, 0x0f, 0xb0, 0x66, 0x6a, 0xcf, 0xcf, 0x01, 0xfa, 0xd5, 0x2b, - 0xfc, 0xfd, 0x4c, 0x2b, 0xf2, 0xcb, 0x3c, 0x34, 0x97, 0xde, 0x73, 0x95, - 0x0e, 0x58, 0x19, 0xb6, 0x1f, 0xe6, 0x95, 0x09, 0x87, 0x41, 0xc8, 0xc4, - 0x91, 0x43, 0x9b, 0x34, 0xa0, 0x43, 0x1c, 0xaf, 0xfe, 0x5b, 0xf9, 0xb4, - 0xc1, 0xc8, 0xe8, 0xae, 0x00, 0x01, 0xf9, 0xf4, 0xdc, 0x8a, 0xd8, 0x2c, - 0x59, 0xf3, 0x63, 0x9e, 0x96, 0xcd, 0x6c, 0x3d, 0x4d, 0xb0, 0xce, 0xfe, - 0xc2, 0x6c, 0x3d, 0x62, 0x51, 0x7b, 0x37, 0x76, 0x13, 0x72, 0x74, 0x3f, - 0x8e, 0x8a, 0xdd, 0x77, 0xb1, 0x00, 0x64, 0xf2, 0xab, 0xe5, 0xe0, 0x50, -}; - -typedef struct { - const unsigned char *der; - size_t len; -} py_distrusted_spki; - -static const py_distrusted_spki _distrusted_spki[] = { - {_distrusted_spki_em_ov_tls_ca_g3b, sizeof(_distrusted_spki_em_ov_tls_ca_g3b)}, - {_distrusted_spki_em_dv_tls_ca_g3c, sizeof(_distrusted_spki_em_dv_tls_ca_g3c)}, - {_distrusted_spki_em_ev_tls_ca_g3c, sizeof(_distrusted_spki_em_ev_tls_ca_g3c)}, - {_distrusted_spki_em_ov_tls_ca_g3c, sizeof(_distrusted_spki_em_ov_tls_ca_g3c)}, - {_distrusted_spki_em_ov_cs_ca_g3c, sizeof(_distrusted_spki_em_ov_cs_ca_g3c)}, - {_distrusted_spki_em_ev_tls_ca_g3b, sizeof(_distrusted_spki_em_ev_tls_ca_g3b)}, - {_distrusted_spki_em_dv_tls_ca_g3b, sizeof(_distrusted_spki_em_dv_tls_ca_g3b)}, - {_distrusted_spki_em_ov_cs_ca_g3b, sizeof(_distrusted_spki_em_ov_cs_ca_g3b)}, +static const unsigned char _distrusted_spki[][32] = { + /* 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/Tools/c-analyzer/cpython/ignored.tsv b/Tools/c-analyzer/cpython/ignored.tsv index 7830fc7be1595c5..db9f74fdbc591f0 100644 --- a/Tools/c-analyzer/cpython/ignored.tsv +++ b/Tools/c-analyzer/cpython/ignored.tsv @@ -257,7 +257,6 @@ Modules/_sre/sre.c pattern_repr flag_names - # XXX I'm pretty sure this is actually constant: Modules/_sre/sre_targets.h - sre_targets - Modules/_sre.c pattern_repr flag_names - -Modules/_ssl/distrusted_spki.h - _distrusted_spki - Modules/_struct.c - bigendian_table - Modules/_struct.c - lilendian_table - Modules/_struct.c - native_table - From 77bf8b0f580352dccbe02aa9c35c4e5444f3bc73 Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Wed, 21 Oct 2026 23:20:30 +0200 Subject: [PATCH 5/7] gh-160040: Refine distrusted SPKI check, docs, and test cert generation * use SHA256_DIGEST_LENGTH * verify cb: keep OpenSSL error, fail closed on internal errors * generate intermediate key on the fly * update docs to explain defense-in-depth / special circumstances Signed-off-by: Christian Heimes --- Doc/whatsnew/3.16.rst | 12 +++-- Lib/test/certdata/distrusted_intermediate.key | 6 --- Lib/test/certdata/distrusted_keycert.pem | 52 +++++++++---------- Lib/test/certdata/make_ssl_certs.py | 25 +++++++-- ...-10-21-16-55-07.gh-issue-160040.9PjFjC.rst | 12 +++-- Modules/_ssl.c | 4 ++ Modules/_ssl/distrusted_spki.h | 18 +++++-- 7 files changed, 79 insertions(+), 50 deletions(-) delete mode 100644 Lib/test/certdata/distrusted_intermediate.key diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index c8a2b5ff8f4d5a1..780da1153487250 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -667,11 +667,13 @@ ssl the module. (Contributed by Serhiy Storchaka in :gh:`157513`.) -* The :mod:`ssl` module now rejects TLS certificate chains that contain one of a - set of compromised eMudhra intermediate CA keys. As defense in depth, the - connection is blocked during certificate verification even when the platform - root store (or a bundled trust store such as :mod:`!certifi`) still trusts the - issuing roots. +* As an extraordinary measure and last line of defense, the :mod:`ssl` module + now rejects TLS certificate chains that contain one of a set of compromised + eMudhra intermediate CA keys. Removing trust is primarily the job of the + platform root store; this denylist only blocks connections during certificate + verification while that update has not yet reached the OS, a container image, + or a bundled trust store in various Python packages. Keys are added to the + denylist only under a very high threshold. (Contributed by Christian Heimes in :gh:`160040`.) diff --git a/Lib/test/certdata/distrusted_intermediate.key b/Lib/test/certdata/distrusted_intermediate.key deleted file mode 100644 index 3df49ac650bf9c9..000000000000000 --- a/Lib/test/certdata/distrusted_intermediate.key +++ /dev/null @@ -1,6 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAShZANiAAQTglHNUqyS -mMHIqtl3Mh3rl+cJvQtMoKylXcitUdz8nRWJoVl+OlEg4e/WMcY+GDXKyuKYaaYu -FjHoooGBq1ZhbcRdkYq8CfOrDmPPeSqk3O1zh743u6VpVJ8cArJw7Wc= ------END PRIVATE KEY----- diff --git a/Lib/test/certdata/distrusted_keycert.pem b/Lib/test/certdata/distrusted_keycert.pem index 56883134b22ba6e..8b5fff6a7a5c609 100644 --- a/Lib/test/certdata/distrusted_keycert.pem +++ b/Lib/test/certdata/distrusted_keycert.pem @@ -1,8 +1,8 @@ -----BEGIN PRIVATE KEY----- -MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDUsqqTdO31pKVm/pms -2U/fFBtZuTcDWM8k+YQIem+jfcHpwUg3upF1Wf2R+YDeHQOhZANiAARIf6AzMYwz -ro/nQbOg8Z3aPaNXhydFTAeMNeYwFuzM9Yqx1kFpXYidvKQYtoyqBtjAPiWWKOCo -f8AI3pFH0WslG1sFr1ty/9loE15sLKN4y9k7HMobn5qW2Svm5A7oQBo= +MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDNZmiBQnX3uJC+/dOJ +xLK9v3gloC6ufCK7mLmLK1Uy5Z4f1PlkPT0ta4jstrJyYKChZANiAATZFOfxle+C +ZXkfoXS28BCcS24ITwFIBxOs7PGh4vwr2DHomjjpgc5/nrc1KPJBd33zwIJxlb11 +H0Qxrq/Kly6z7s1dOnK7NoQsmrC1+xUx4anCDtMJMCYSPVMIJ/X3wwo= -----END PRIVATE KEY----- Certificate: Data: @@ -19,13 +19,13 @@ Certificate: Public Key Algorithm: id-ecPublicKey Public-Key: (384 bit) pub: - 04:48:7f:a0:33:31:8c:33:ae:8f:e7:41:b3:a0:f1: - 9d:da:3d:a3:57:87:27:45:4c:07:8c:35:e6:30:16: - ec:cc:f5:8a:b1:d6:41:69:5d:88:9d:bc:a4:18:b6: - 8c:aa:06:d8:c0:3e:25:96:28:e0:a8:7f:c0:08:de: - 91:47:d1:6b:25:1b:5b:05:af:5b:72:ff:d9:68:13: - 5e:6c:2c:a3:78:cb:d9:3b:1c:ca:1b:9f:9a:96:d9: - 2b:e6:e4:0e:e8:40:1a + 04:d9:14:e7:f1:95:ef:82:65:79:1f:a1:74:b6:f0: + 10:9c:4b:6e:08:4f:01:48:07:13:ac:ec:f1:a1:e2: + fc:2b:d8:31:e8:9a:38:e9:81:ce:7f:9e:b7:35:28: + f2:41:77:7d:f3:c0:82:71:95:bd:75:1f:44:31:ae: + af:ca:97:2e:b3:ee:cd:5d:3a:72:bb:36:84:2c:9a: + b0:b5:fb:15:31:e1:a9:c2:0e:d3:09:30:26:12:3d: + 53:08:27:f5:f7:c3:0a ASN1 OID: secp384r1 NIST CURVE: P-384 X509v3 extensions: @@ -38,7 +38,7 @@ Certificate: X509v3 Basic Constraints: critical CA:FALSE X509v3 Subject Key Identifier: - 48:B5:07:21:1D:FB:CA:0D:ED:37:78:BC:9D:86:A9:18:D8:BD:37:28 + 4F:6F:81:FE:A7:B4:84:3B:64:95:D0:2E:D9:FE:D0:4D:6C:A3:FB:0F X509v3 Authority Key Identifier: keyid:34:EA:EA:0C:EB:C9:0A:DA:F9:D0:C5:E2:ED:9B:78:C9:87:9E:1B:6C DirName:/C=IN/O=eMudhra Technologies Limited/CN=emSign Root TLS CA - G3 @@ -52,23 +52,23 @@ Certificate: Signature Algorithm: ecdsa-with-SHA256 Signature Value: - 30:66:02:31:00:f2:6e:00:c2:93:f3:8a:7b:7a:d2:78:02:6f: - c8:74:ea:85:a8:4e:04:e7:8e:6e:cd:dd:e8:8d:0b:ee:db:06: - cb:33:be:34:95:09:15:d5:3d:10:31:b5:15:c0:4a:a6:43:02: - 31:00:c6:59:1b:76:ef:93:2f:62:21:11:84:b8:88:5e:8a:bc: - 9f:ed:5e:74:fa:90:76:14:36:e2:55:b6:28:c2:40:14:0e:f7: - e2:66:9c:b9:12:e1:07:d8:6a:74:2c:c2:fe:7d + 30:64:02:30:4b:b5:69:fa:6b:e7:5f:29:db:83:31:53:b2:60: + cb:6b:75:1b:4d:5b:95:c4:dd:69:f2:f0:3c:54:15:b6:41:96: + 54:f2:e4:7f:07:8d:20:64:9a:76:60:47:9b:b8:e7:6f:02:30: + 12:e7:80:d7:d6:97:1e:b6:c1:ea:02:68:92:ab:6d:43:9e:7b: + 40:e1:20:78:1b:34:b5:16:2c:ab:c2:be:58:57:a7:7e:d0:4a: + 70:90:32:1f:ab:7a:aa:15:fa:ae:71:cc -----BEGIN CERTIFICATE----- -MIIDqjCCAy+gAwIBAgIJAMstgJlaaVJbMAoGCCqGSM49BAMCMEAxCzAJBgNVBAYT +MIIDqDCCAy+gAwIBAgIJAMstgJlaaVJbMAoGCCqGSM49BAMCMEAxCzAJBgNVBAYT AlVTMRQwEgYDVQQKDAtlTXVkaHJhIEluYzEbMBkGA1UEAwwSRU0gT1YgVExTIENB IC0gRzNDMCAXDTE4MDgyOTE0MjMxNloYDzI1MjUxMDI4MTQyMzE2WjBfMQswCQYD VQQGEwJYWTEXMBUGA1UEBwwOQ2FzdGxlIEFudGhyYXgxIzAhBgNVBAoMGlB5dGhv biBTb2Z0d2FyZSBGb3VuZGF0aW9uMRIwEAYDVQQDDAlsb2NhbGhvc3QwdjAQBgcq -hkjOPQIBBgUrgQQAIgNiAARIf6AzMYwzro/nQbOg8Z3aPaNXhydFTAeMNeYwFuzM -9Yqx1kFpXYidvKQYtoyqBtjAPiWWKOCof8AI3pFH0WslG1sFr1ty/9loE15sLKN4 -y9k7HMobn5qW2Svm5A7oQBqjggHSMIIBzjAUBgNVHREEDTALgglsb2NhbGhvc3Qw +hkjOPQIBBgUrgQQAIgNiAATZFOfxle+CZXkfoXS28BCcS24ITwFIBxOs7PGh4vwr +2DHomjjpgc5/nrc1KPJBd33zwIJxlb11H0Qxrq/Kly6z7s1dOnK7NoQsmrC1+xUx +4anCDtMJMCYSPVMIJ/X3wwqjggHSMIIBzjAUBgNVHREEDTALgglsb2NhbGhvc3Qw DgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAM -BgNVHRMBAf8EAjAAMB0GA1UdDgQWBBRItQchHfvKDe03eLydhqkY2L03KDCBjgYD +BgNVHRMBAf8EAjAAMB0GA1UdDgQWBBRPb4H+p7SEO2SV0C7Z/tBNbKP7DzCBjgYD VR0jBIGGMIGDgBQ06uoM68kK2vnQxeLtm3jJh54bbKFapFgwVjELMAkGA1UEBhMC SU4xJTAjBgNVBAoMHGVNdWRocmEgVGVjaG5vbG9naWVzIExpbWl0ZWQxIDAeBgNV BAMMF2VtU2lnbiBSb290IFRMUyBDQSAtIEczgg8paSbCJ6OhUmOSsZSCpQQwgYMG @@ -76,9 +76,9 @@ CCsGAQUFBwEBBHcwdTA8BggrBgEFBQcwAoYwaHR0cDovL3Rlc3RjYS5weXRob250 ZXN0Lm5ldC90ZXN0Y2EvcHljYWNlcnQuY2VyMDUGCCsGAQUFBzABhilodHRwOi8v dGVzdGNhLnB5dGhvbnRlc3QubmV0L3Rlc3RjYS9vY3NwLzBDBgNVHR8EPDA6MDig NqA0hjJodHRwOi8vdGVzdGNhLnB5dGhvbnRlc3QubmV0L3Rlc3RjYS9yZXZvY2F0 -aW9uLmNybDAKBggqhkjOPQQDAgNpADBmAjEA8m4AwpPzint60ngCb8h06oWoTgTn -jm7N3eiNC+7bBsszvjSVCRXVPRAxtRXASqZDAjEAxlkbdu+TL2IhEYS4iF6KvJ/t -XnT6kHYUNuJVtijCQBQO9+JmnLkS4QfYanQswv59 +aW9uLmNybDAKBggqhkjOPQQDAgNnADBkAjBLtWn6a+dfKduDMVOyYMtrdRtNW5XE +3Wny8DxUFbZBllTy5H8HjSBkmnZgR5u4528CMBLngNfWlx62weoCaJKrbUOee0Dh +IHgbNLUWLKvCvlhXp37QSnCQMh+reqoV+q5xzA== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDaDCCAu6gAwIBAgIPKWkmwiejoVJjkrGUgqUEMAoGCCqGSM49BAMDMFYxCzAJ diff --git a/Lib/test/certdata/make_ssl_certs.py b/Lib/test/certdata/make_ssl_certs.py index 4d1c149e4580ecd..6c12cc393e33cea 100644 --- a/Lib/test/certdata/make_ssl_certs.py +++ b/Lib/test/certdata/make_ssl_certs.py @@ -232,18 +232,35 @@ def write_cert_reference(path): # eMudhra intermediate on the _ssl distrusted list, its secp384r1 key is 4. # See https://emudhra.we-have-a-problem.com/ DISTRUSTED_INTERMEDIATE = 'distrusted_intermediate.pem' -DISTRUSTED_INTERMEDIATE_KEY = 'distrusted_intermediate.key' + +# ASN.1 template for the intermediate's compromised key: secp384r1 scalar 4. +# openssl derives the public key when the SEC1 structure omits it. +distrusted_key_template = """ + asn1 = SEQUENCE:eckey + [eckey] + version = INTEGER:1 + priv = FORMAT:HEX,OCTETSTRING:{scalar} + params = EXPLICIT:0,OID:secp384r1 + """ def make_distrusted_spki_cert(cmdlineargs): # leaf signed by the distrusted intermediate, chains to distrusted_root.pem print("creating distrusted-SPKI test cert") tempnames = [] - for i in range(4): + for i in range(7): with tempfile.NamedTemporaryFile(delete=False) as f: tempnames.append(f.name) - req_file, key_file, csr_file, cert_file = tempnames + req_file, key_file, csr_file, cert_file, cakey_conf, cakey_der, cakey_file = tempnames try: + # recreate the intermediate's key (secp384r1 scalar 4) on the fly + with open(cakey_conf, 'w') as f: + f.write(distrusted_key_template.format(scalar='00' * 47 + '04')) + check_call(['openssl', 'asn1parse', '-genconf', cakey_conf, + '-out', cakey_der, '-noout']) + check_call(['openssl', 'pkey', '-in', cakey_der, + '-inform', 'DER', '-out', cakey_file]) + req = req_template.format( hostname='localhost', extra_san='', @@ -260,7 +277,7 @@ def make_distrusted_spki_cert(cmdlineargs): # sign with the distrusted intermediate, not the test CA check_call(['openssl', 'ca', '-config', req_file, '-extensions', 'req_x509_extensions_full', - '-cert', DISTRUSTED_INTERMEDIATE, '-keyfile', DISTRUSTED_INTERMEDIATE_KEY, + '-cert', DISTRUSTED_INTERMEDIATE, '-keyfile', cakey_file, '-out', cert_file, '-outdir', 'cadir', '-policy', 'policy_anything', '-batch', '-infiles', csr_file]) with open(key_file) as f: diff --git a/Misc/NEWS.d/next/Security/2026-10-21-16-55-07.gh-issue-160040.9PjFjC.rst b/Misc/NEWS.d/next/Security/2026-10-21-16-55-07.gh-issue-160040.9PjFjC.rst index e30eda238b63642..23c1c4dba7a5685 100644 --- a/Misc/NEWS.d/next/Security/2026-10-21-16-55-07.gh-issue-160040.9PjFjC.rst +++ b/Misc/NEWS.d/next/Security/2026-10-21-16-55-07.gh-issue-160040.9PjFjC.rst @@ -1,6 +1,10 @@ :mod:`ssl` now rejects TLS certificate chains that contain any of a set of -compromised eMudhra intermediate CA keys. A built-in denylist of the affected -:rfc:`5280` ``SubjectPublicKeyInfo`` values is enforced during certificate -verification, mirroring the mitigation added to other TLS stacks. The denylist -matches on the public key, so it also covers the cross-signed certificates that +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. diff --git a/Modules/_ssl.c b/Modules/_ssl.c index 90fc648a1143823..73c796bcf77a723 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -3406,6 +3406,10 @@ _cert_has_distrusted_spki(X509 *cert) 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; diff --git a/Modules/_ssl/distrusted_spki.h b/Modules/_ssl/distrusted_spki.h index c8d989e675c6f12..bbdb56dcfe671ed 100644 --- a/Modules/_ssl/distrusted_spki.h +++ b/Modules/_ssl/distrusted_spki.h @@ -2,18 +2,26 @@ * * 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. To block another key, add - * its digest. + * cross-signs that reuse the key are blocked too. * - * Compute a cert's SPKI SHA-256 digest bytes with OpenSSL: + * 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 -/* Compromised eMudhra intermediates, https://emudhra.we-have-a-problem.com/ */ -static const unsigned char _distrusted_spki[][32] = { +#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, From 641c999b4fd403fa426a922f84d534f1d6e481e6 Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Thu, 22 Oct 2026 12:47:16 +0200 Subject: [PATCH 6/7] gh-160040: Store distrusted test certs in an encrypted zip The compromised eMudhra CA certs and the leaf signed by them are kept in a password-protected zip so naive security scanners don't flag them. test_ssl and make_ssl_certs read the PEMs from the archive. Signed-off-by: Christian Heimes --- Lib/test/certdata/distrusted.zip | Bin 0 -> 5062 bytes Lib/test/certdata/distrusted_intermediate.pem | 21 ---- Lib/test/certdata/distrusted_keycert.pem | 103 ------------------ Lib/test/certdata/distrusted_root.pem | 14 --- Lib/test/certdata/make_ssl_certs.py | 72 +++++++++--- Lib/test/test_ssl.py | 24 +++- Tools/ssl/emudhra_denylist_check.py | 47 +++----- 7 files changed, 90 insertions(+), 191 deletions(-) create mode 100644 Lib/test/certdata/distrusted.zip delete mode 100644 Lib/test/certdata/distrusted_intermediate.pem delete mode 100644 Lib/test/certdata/distrusted_keycert.pem delete mode 100644 Lib/test/certdata/distrusted_root.pem 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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 str: ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) - ctx.load_verify_locations(cadata=ROOT_PEM) # trust only the emSign root + ctx.load_verify_locations(cadata=root) # trust only the emSign root try: with socket.create_connection((host, port), timeout=15) as raw: with ctx.wrap_socket(raw, server_hostname=host) as s: - s.sendall( - b"GET / HTTP/1.0\r\nHost: " + host.encode() + b"\r\n\r\n" - ) - status = s.recv(64).split(b"\r\n", 1)[0].decode("latin1") - cipher = s.cipher()[0] - return f"CONNECTED ({status}, {cipher}) -- denylist did NOT fire!" + return f"CONNECTED ({s.version()}) -- denylist did NOT fire!" except ssl.SSLCertVerificationError as exc: - return (f"REJECTED as expected: verify_code={exc.verify_code} " - f"{exc.verify_message!r}") + return f"REJECTED: verify_code={exc.verify_code} {exc.verify_message!r}" if __name__ == "__main__": + # read the trust anchor from the password-protected zip, in memory + with zipfile.ZipFile(ZIP) as z: + root = z.read(ROOT, pwd=CHOSEN_KNIGHT).decode() print("OpenSSL:", ssl.OPENSSL_VERSION) for host in ENDPOINTS: - print(f"\n{host}:") - print(" ", check(host)) + print(f"\n{host}:\n {check(host, root)}") From 275047ee6a2d70a40eb581eca52053ccfb69d7e1 Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Thu, 22 Oct 2026 19:38:31 +0200 Subject: [PATCH 7/7] gh-160040: Drop the ssl What's New entry Signed-off-by: Christian Heimes --- Doc/whatsnew/3.16.rst | 9 --------- 1 file changed, 9 deletions(-) diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 4618514f07fc469..925f425aba58693 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -694,15 +694,6 @@ ssl the module. (Contributed by Serhiy Storchaka in :gh:`157513`.) -* As an extraordinary measure and last line of defense, the :mod:`ssl` module - now rejects TLS certificate chains that contain one of a set of compromised - eMudhra intermediate CA keys. Removing trust is primarily the job of the - platform root store; this denylist only blocks connections during certificate - verification while that update has not yet reached the OS, a container image, - or a bundled trust store in various Python packages. Keys are added to the - denylist only under a very high threshold. - (Contributed by Christian Heimes in :gh:`160040`.) - symtable --------