summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>2023-03-27 07:14:24 -0700
committerGitHub <noreply@github.com>2023-03-27 16:14:24 +0200
commitb5bf6c1b2275bfa93e77989bec0711c3e97932a6 (patch)
treec53b879781e31cc86c8768938cf804b12fec2be7
parentae8a721c2ba2a48de0cb69fa868fa8fc207b129d (diff)
downloadcpython-git-b5bf6c1b2275bfa93e77989bec0711c3e97932a6.tar.gz
[3.10] GH-95494: Fix transport EOF handling in OpenSSL 3.0 (GH-95495) (#103007)
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?) (cherry picked from commit 420bbb783b43216cc897dc8914851899db37a31d) Co-authored-by: David Benjamin <davidben@google.com>
-rw-r--r--Lib/test/test_ssl.py18
-rw-r--r--Misc/NEWS.d/next/Library/2022-07-30-23-01-43.gh-issue-95495.RA-q1d.rst7
-rw-r--r--Modules/_ssl.c14
3 files changed, 32 insertions, 7 deletions
diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py
index addfb6ce43..c28a574f8e 100644
--- a/Lib/test/test_ssl.py
+++ b/Lib/test/test_ssl.py
@@ -155,7 +155,6 @@ OP_SINGLE_DH_USE = getattr(ssl, "OP_SINGLE_DH_USE", 0)
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
@@ -1199,8 +1198,7 @@ class ContextTests(unittest.TestCase):
# 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
@@ -2362,6 +2360,20 @@ class SimpleBackgroundTests(unittest.TestCase):
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/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 0000000000..d0f4ccbdd3
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2022-07-30-23-01-43.gh-issue-95495.RA-q1d.rst
@@ -0,0 +1,7 @@
+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.
diff --git a/Modules/_ssl.c b/Modules/_ssl.c
index 360eb864ad..7a28f2d37f 100644
--- a/Modules/_ssl.c
+++ b/Modules/_ssl.c
@@ -671,6 +671,16 @@ 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) {
+ p = PY_SSL_ERROR_EOF;
+ type = state->PySSLEOFErrorObject;
+ errstr = "EOF occurred in violation of protocol";
+ }
+#endif
break;
}
default:
@@ -3134,10 +3144,6 @@ _ssl__SSLContext_impl(PyTypeObject *type, int proto_version)
#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);
/* A bare minimum cipher list without completely broken cipher suites.