From 33dcd3640f4738275edfbbfb3be6cb7c648658b5 Mon Sep 17 00:00:00 2001 From: David Benjamin Date: Sat, 30 Jul 2022 17:27:12 -0700 Subject: [PATCH 1/4] gh-95494: Fix transport EOF handling in OpenSSL 3.0 GH-25309 enabled SSL_OP_IGNORE_UNEXPECTED_EOF by default, with a comment that it restores OpenSSL 1.1.1 behavior, but this wasn't quite right. That option causes OpenSSL to treat transport EOF as the same as close_notify (i.e. SSL_ERROR_ZERO_RETURN), whereas Python actually has distinct SSLEOFError and SSLZeroReturnError exceptions. (The latter is usually mapped to a zero return from read.) In OpenSSL 1.1.1, the ssl module would raise them for transport EOF and close_notify, respectively. In OpenSSL 3.0, both act like close_notify. Fix this by, instead, just detecting SSL_R_UNEXPECTED_EOF_WHILE_READING and mapping that to the other exception type. There doesn't seem to have been any unit test of this error, so fill in the missing one. This had to be done with the BIO path because it's actually slightly tricky to simulate a transport EOF with Python's fd based APIs. (If you instruct the server to close the socket, it gets confused, probably because the server's SSL object is still referencing the now dead fd?) --- Lib/test/test_ssl.py | 18 +++++++++++++++--- Modules/_ssl.c | 13 +++++++++---- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index 5007e08f321b5a..40ee40e51dd115 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -153,7 +153,6 @@ def data_file(*name): OP_SINGLE_ECDH_USE = getattr(ssl, "OP_SINGLE_ECDH_USE", 0) OP_CIPHER_SERVER_PREFERENCE = getattr(ssl, "OP_CIPHER_SERVER_PREFERENCE", 0) OP_ENABLE_MIDDLEBOX_COMPAT = getattr(ssl, "OP_ENABLE_MIDDLEBOX_COMPAT", 0) -OP_IGNORE_UNEXPECTED_EOF = getattr(ssl, "OP_IGNORE_UNEXPECTED_EOF", 0) # Ubuntu has patched OpenSSL and changed behavior of security level 2 # see https://bugs.python.org/issue41561#msg389003 @@ -960,8 +959,7 @@ def test_options(self): # SSLContext also enables these by default default |= (OP_NO_COMPRESSION | OP_CIPHER_SERVER_PREFERENCE | OP_SINGLE_DH_USE | OP_SINGLE_ECDH_USE | - OP_ENABLE_MIDDLEBOX_COMPAT | - OP_IGNORE_UNEXPECTED_EOF) + OP_ENABLE_MIDDLEBOX_COMPAT) self.assertEqual(default, ctx.options) with warnings_helper.check_warnings(): ctx.options |= ssl.OP_NO_TLSv1 @@ -2120,6 +2118,20 @@ def test_bio_read_write_data(self): self.assertEqual(buf, b'foo\n') self.ssl_io_loop(sock, incoming, outgoing, sslobj.unwrap) + def test_transport_eof(self): + client_context, server_context, hostname = testing_context() + with socket.socket(socket.AF_INET) as sock: + sock.connect(self.server_addr) + incoming = ssl.MemoryBIO() + outgoing = ssl.MemoryBIO() + sslobj = client_context.wrap_bio(incoming, outgoing, + server_hostname=hostname) + self.ssl_io_loop(sock, incoming, outgoing, sslobj.do_handshake) + + # Simulate EOF from the transport. + incoming.write_eof() + self.assertRaises(ssl.SSLEOFError, sslobj.read) + @support.requires_resource('network') class NetworkedTests(unittest.TestCase): diff --git a/Modules/_ssl.c b/Modules/_ssl.c index bf8bd9dea89b6b..f46ea828c72ff5 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -662,6 +662,15 @@ PySSL_SetError(PySSLSocket *sslsock, int ret, const char *filename, int lineno) ERR_GET_REASON(e) == SSL_R_CERTIFICATE_VERIFY_FAILED) { type = state->PySSLCertVerificationErrorObject; } +#if defined(SSL_R_UNEXPECTED_EOF_WHILE_READING) + /* OpenSSL 3.0 changed transport EOF from SSL_ERROR_SYSCALL with + * zero return value to SSL_ERROR_SSL with a special error code. */ + if (ERR_GET_LIB(e) == ERR_LIB_SSL && + ERR_GET_REASON(e) == SSL_R_UNEXPECTED_EOF_WHILE_READING) { + type = state->PySSLEOFErrorObject; + errstr = "EOF occurred in violation of protocol"; + } +#endif break; } default: @@ -3110,10 +3119,6 @@ _ssl__SSLContext_impl(PyTypeObject *type, int proto_version) #endif #ifdef SSL_OP_SINGLE_ECDH_USE options |= SSL_OP_SINGLE_ECDH_USE; -#endif -#ifdef SSL_OP_IGNORE_UNEXPECTED_EOF - /* Make OpenSSL 3.0.0 behave like 1.1.1 */ - options |= SSL_OP_IGNORE_UNEXPECTED_EOF; #endif SSL_CTX_set_options(self->ctx, options); From c8bc49dadcc2482da686376d37ab21e7ffa587e5 Mon Sep 17 00:00:00 2001 From: David Benjamin Date: Sat, 30 Jul 2022 23:01:53 -0700 Subject: [PATCH 2/4] add blurb --- .../Library/2022-07-30-23-01-43.gh-issue-95495.RA-q1d.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2022-07-30-23-01-43.gh-issue-95495.RA-q1d.rst diff --git a/Misc/NEWS.d/next/Library/2022-07-30-23-01-43.gh-issue-95495.RA-q1d.rst b/Misc/NEWS.d/next/Library/2022-07-30-23-01-43.gh-issue-95495.RA-q1d.rst new file mode 100644 index 00000000000000..b2ea2d1e6199d9 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-07-30-23-01-43.gh-issue-95495.RA-q1d.rst @@ -0,0 +1,4 @@ +When built against OpenSSL 3.0, the ``ssl`` module had a bug where it +reported unauthenticated EOFs (i.e. without close_notify) as a clean +TLS-level EOF. It now raises ``SSLEOFError``, matching the behavior in +previous versions of OpenSSL. From 38556b993acec9cfcc283bcccec05fd13ce3bc91 Mon Sep 17 00:00:00 2001 From: David Benjamin Date: Mon, 1 Aug 2022 15:33:18 -0700 Subject: [PATCH 3/4] Fix the ssl_errno value --- Modules/_ssl.c | 1 + 1 file changed, 1 insertion(+) diff --git a/Modules/_ssl.c b/Modules/_ssl.c index f46ea828c72ff5..9081b3c1d6ab53 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -667,6 +667,7 @@ PySSL_SetError(PySSLSocket *sslsock, int ret, const char *filename, int lineno) * zero return value to SSL_ERROR_SSL with a special error code. */ if (ERR_GET_LIB(e) == ERR_LIB_SSL && ERR_GET_REASON(e) == SSL_R_UNEXPECTED_EOF_WHILE_READING) { + p = PY_SSL_ERROR_EOF; type = state->PySSLEOFErrorObject; errstr = "EOF occurred in violation of protocol"; } From fe2f78da5f695ea9b462a38b8ff9a6f418b1cdd8 Mon Sep 17 00:00:00 2001 From: David Benjamin Date: Wed, 3 Aug 2022 10:49:00 -0700 Subject: [PATCH 4/4] Improve NEWS text --- .../2022-07-30-23-01-43.gh-issue-95495.RA-q1d.rst | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2022-07-30-23-01-43.gh-issue-95495.RA-q1d.rst b/Misc/NEWS.d/next/Library/2022-07-30-23-01-43.gh-issue-95495.RA-q1d.rst index b2ea2d1e6199d9..d0f4ccbdd3e39f 100644 --- a/Misc/NEWS.d/next/Library/2022-07-30-23-01-43.gh-issue-95495.RA-q1d.rst +++ b/Misc/NEWS.d/next/Library/2022-07-30-23-01-43.gh-issue-95495.RA-q1d.rst @@ -1,4 +1,7 @@ -When built against OpenSSL 3.0, the ``ssl`` module had a bug where it -reported unauthenticated EOFs (i.e. without close_notify) as a clean -TLS-level EOF. It now raises ``SSLEOFError``, matching the behavior in -previous versions of OpenSSL. +When built against OpenSSL 3.0, the :mod:`ssl` module had a bug where it +reported unauthenticated EOFs (i.e. without close_notify) as a clean TLS-level +EOF. It now raises :exc:`~ssl.SSLEOFError`, matching the behavior in previous +versions of OpenSSL. The :attr:`~ssl.SSLContext.options` attribute on +:class:`~ssl.SSLContext` also no longer includes +:data:`~ssl.OP_IGNORE_UNEXPECTED_EOF` by default. This option may be set to +specify the previous OpenSSL 3.0 behavior.