diff --git a/Lib/ssl.py b/Lib/ssl.py index 8889aff92..6929951b4 100644 --- a/Lib/ssl.py +++ b/Lib/ssl.py @@ -13,6 +13,9 @@ Exceptions: Functions: + get_sigalgs -- return a list of all available TLS signature + algorithms (requires OpenSSL 3.4 or later) + cert_time_to_seconds -- convert time string used for certificate notBefore and notAfter functions to integer seconds past the Epoch (the time values @@ -107,16 +110,12 @@ from _ssl import ( ) from _ssl import txt2obj as _txt2obj, nid2obj as _nid2obj from _ssl import RAND_status, RAND_add, RAND_bytes -try: - from _ssl import RAND_egd -except ImportError: - # RAND_egd is not supported on some platforms - pass +from _ssl import get_sigalgs from _ssl import ( HAS_SNI, HAS_ECDH, HAS_NPN, HAS_ALPN, HAS_SSLv2, HAS_SSLv3, HAS_TLSv1, - HAS_TLSv1_1, HAS_TLSv1_2, HAS_TLSv1_3, HAS_PSK, HAS_PHA + HAS_TLSv1_1, HAS_TLSv1_2, HAS_TLSv1_3, HAS_PSK, HAS_PSK_TLS13, HAS_PHA ) from _ssl import _DEFAULT_CIPHERS, _OPENSSL_API_VERSION @@ -151,7 +150,8 @@ _IntEnum._convert_( source=_ssl) PROTOCOL_SSLv23 = _SSLMethod.PROTOCOL_SSLv23 = _SSLMethod.PROTOCOL_TLS -_PROTOCOL_NAMES = {value: name for name, value in _SSLMethod.__members__.items()} +_PROTOCOL_NAMES = frozendict({ + value: name for name, value in _SSLMethod.__members__.items()}) _SSLv2_IF_EXISTS = getattr(_SSLMethod, 'PROTOCOL_SSLv2', None) @@ -373,6 +373,20 @@ def _ipaddress_match(cert_ipaddress, host_ip): return ip == host_ip +def _check_sslobject_params(server_side, context=None, server_hostname=None, session=None): + """Raises a ValueError if SSLObject._create() parameters aren't valid. + """ + if server_side: + if server_hostname: + raise ValueError("server_hostname can only be specified " + "in client mode") + if session is not None: + raise ValueError("session can only be specified in " + "client mode") + if context.check_hostname and not server_hostname: + raise ValueError("check_hostname requires server_hostname") + + DefaultVerifyPaths = namedtuple("DefaultVerifyPaths", "cafile capath openssl_cafile_env openssl_cafile openssl_capath_env " "openssl_capath") @@ -813,6 +827,8 @@ class SSLObject: @classmethod def _create(cls, incoming, outgoing, server_side=False, server_hostname=None, session=None, context=None): + _check_sslobject_params(server_side=server_side, context=context, + server_hostname=server_hostname, session=session) self = cls.__new__(cls) sslobj = context._wrap_bio( incoming, outgoing, server_side=server_side, @@ -931,6 +947,18 @@ class SSLObject: ssl_version, secret_bits)``.""" return self._sslobj.cipher() + def group(self): + """Return the currently selected key agreement group name.""" + return self._sslobj.group() + + def client_sigalg(self): + """Return the selected client authentication signature algorithm.""" + return self._sslobj.client_sigalg() + + def server_sigalg(self): + """Return the selected server handshake signature algorithm.""" + return self._sslobj.server_sigalg() + def shared_ciphers(self): """Return a list of ciphers shared by the client during the handshake or None if this is not a valid server connection. @@ -975,6 +1003,10 @@ def _sslcopydoc(func): return func +class _GiveupOnSSLSendfile(Exception): + pass + + class SSLSocket(socket): """This class implements a subtype of socket.socket that wraps the underlying OS socket in an SSL context when necessary, and @@ -993,15 +1025,8 @@ class SSLSocket(socket): context=None, session=None): if sock.getsockopt(SOL_SOCKET, SO_TYPE) != SOCK_STREAM: raise NotImplementedError("only stream sockets are supported") - if server_side: - if server_hostname: - raise ValueError("server_hostname can only be specified " - "in client mode") - if session is not None: - raise ValueError("session can only be specified in " - "client mode") - if context.check_hostname and not server_hostname: - raise ValueError("check_hostname requires server_hostname") + _check_sslobject_params(server_side=server_side, context=context, + server_hostname=server_hostname, session=session) sock_timeout = sock.gettimeout() kwargs = dict( @@ -1206,6 +1231,30 @@ class SSLSocket(socket): else: return self._sslobj.cipher() + @_sslcopydoc + def group(self): + self._checkClosed() + if self._sslobj is None: + return None + else: + return self._sslobj.group() + + @_sslcopydoc + def client_sigalg(self): + self._checkClosed() + if self._sslobj is None: + return None + else: + return self._sslobj.client_sigalg() + + @_sslcopydoc + def server_sigalg(self): + self._checkClosed() + if self._sslobj is None: + return None + else: + return self._sslobj.server_sigalg() + @_sslcopydoc def shared_ciphers(self): self._checkClosed() @@ -1266,15 +1315,26 @@ class SSLSocket(socket): return super().sendall(data, flags) def sendfile(self, file, offset=0, count=None): - """Send a file, possibly by using os.sendfile() if this is a - clear-text socket. Return the total number of bytes sent. + """Send a file, possibly by using an efficient sendfile() call if + the system supports it. Return the total number of bytes sent. """ - if self._sslobj is not None: - return self._sendfile_use_send(file, offset, count) - else: - # os.sendfile() works with plain sockets only + if self._sslobj is None: return super().sendfile(file, offset, count) + if not self._sslobj.uses_ktls_for_send(): + return self._sendfile_use_send(file, offset, count) + + sendfile = getattr(self._sslobj, "sendfile", None) + if sendfile is None: + return self._sendfile_use_send(file, offset, count) + + try: + return self._sendfile_zerocopy( + sendfile, _GiveupOnSSLSendfile, file, offset, count, + ) + except _GiveupOnSSLSendfile: + return self._sendfile_use_send(file, offset, count) + def recv(self, buflen=1024, flags=0): self._checkClosed() if self._sslobj is not None: @@ -1484,11 +1544,8 @@ def DER_cert_to_PEM_cert(der_cert_bytes): """Takes a certificate in binary DER format and returns the PEM version of it as a string.""" - f = str(base64.standard_b64encode(der_cert_bytes), 'ASCII', 'strict') - ss = [PEM_HEADER] - ss += [f[i:i+64] for i in range(0, len(f), 64)] - ss.append(PEM_FOOTER + '\n') - return '\n'.join(ss) + f = str(base64.b64encode(der_cert_bytes, wrapcol=64), 'ASCII') + return f'{PEM_HEADER}\n{f}\n{PEM_FOOTER}\n' def PEM_cert_to_DER_cert(pem_cert_string): """Takes a certificate in ASCII PEM format and returns the diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index dd4c657d0..f0fc6e5b9 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -49,7 +49,17 @@ Py_DEBUG_WIN32 = support.Py_DEBUG and sys.platform == 'win32' PROTOCOLS = sorted(ssl._PROTOCOL_NAMES) HOST = socket_helper.HOST IS_AWS_LC = "AWS-LC" in ssl.OPENSSL_VERSION +IS_LIBRESSL = "LibreSSL" in ssl.OPENSSL_VERSION IS_OPENSSL_3_0_0 = ssl.OPENSSL_VERSION_INFO >= (3, 0, 0) +CAN_GET_SELECTED_OPENSSL_GROUP = ssl.OPENSSL_VERSION_INFO >= (3, 2) +CAN_IGNORE_UNKNOWN_OPENSSL_GROUPS = ssl.OPENSSL_VERSION_INFO >= (3, 3) +CAN_GET_AVAILABLE_OPENSSL_GROUPS = ssl.OPENSSL_VERSION_INFO >= (3, 5) +CAN_GET_AVAILABLE_OPENSSL_SIGALGS = ssl.OPENSSL_VERSION_INFO >= (3, 4) +# LibreSSL does not provide SSL_CTX_set1_(client_)sigalgs_list(). +CAN_SET_CLIENT_SIGALGS = not IS_AWS_LC and not IS_LIBRESSL +CAN_SET_SERVER_SIGALGS = not IS_LIBRESSL +CAN_IGNORE_UNKNOWN_OPENSSL_SIGALGS = ssl.OPENSSL_VERSION_INFO >= (3, 3) +CAN_GET_SELECTED_OPENSSL_SIGALG = ssl.OPENSSL_VERSION_INFO >= (3, 5) PY_SSL_DEFAULT_CIPHERS = sysconfig.get_config_var('PY_SSL_DEFAULT_CIPHERS') HAS_KEYLOG = hasattr(ssl.SSLContext, 'keylog_filename') @@ -136,6 +146,7 @@ REMOTE_HOST = "self-signed.pythontest.net" EMPTYCERT = data_file("nullcert.pem") BADCERT = data_file("badcert.pem") NONEXISTINGCERT = data_file("XXXnonexisting.pem") +NONEXISTINGKEY = data_file("XXXnonexistingkey.pem") BADKEY = data_file("badkey.pem") NOKIACERT = data_file("nokia.pem") NULLBYTECERT = data_file("nullbytecert.pem") @@ -276,7 +287,7 @@ def make_test_context( check_hostname=None, cert_reqs=ssl.CERT_NONE, ca_certs=None, certfile=None, keyfile=None, - ciphers=None, + ciphers=None, ciphersuites=None, min_version=None, max_version=None, ): if server_side: @@ -300,6 +311,8 @@ def make_test_context( if ciphers is not None: context.set_ciphers(ciphers) + if ciphersuites is not None: + context.set_ciphersuites(ciphersuites) if min_version is not None: context.minimum_version = min_version @@ -316,7 +329,7 @@ def test_wrap_socket( check_hostname=None, cert_reqs=ssl.CERT_NONE, ca_certs=None, certfile=None, keyfile=None, - ciphers=None, + ciphers=None, ciphersuites=None, min_version=None, max_version=None, **kwargs, ): @@ -325,7 +338,7 @@ def test_wrap_socket( check_hostname=check_hostname, cert_reqs=cert_reqs, ca_certs=ca_certs, certfile=certfile, keyfile=keyfile, - ciphers=ciphers, + ciphers=ciphers, ciphersuites=ciphersuites, min_version=min_version, max_version=max_version, ) if not server_side: @@ -336,7 +349,8 @@ def test_wrap_socket( USE_SAME_TEST_CONTEXT = False _TEST_CONTEXT = None -def testing_context(server_cert=SIGNED_CERTFILE, *, server_chain=True): +def testing_context(server_cert=SIGNED_CERTFILE, *, server_chain=True, + client_cert=None): """Create context client_context, server_context, hostname = testing_context() @@ -363,6 +377,10 @@ def testing_context(server_cert=SIGNED_CERTFILE, *, server_chain=True): if server_chain: server_context.load_verify_locations(SIGNING_CA) + if client_cert: + client_context.load_cert_chain(client_cert) + server_context.verify_mode = ssl.CERT_REQUIRED + if USE_SAME_TEST_CONTEXT: if _TEST_CONTEXT is not None: _TEST_CONTEXT = client_context, server_context, hostname @@ -384,6 +402,34 @@ def do_ssl_object_handshake(sslobject, outgoing, max_retry=25): return data +def connected_bio_pair(client_context, server_context, hostname, max_retry=5): + """Handshake a client and a server SSLObject against each other. + + Everything happens in memory, so this needs no socket and no thread. + Returns the two objects followed by their four BIOs, in the order + client, server, c_in, c_out, s_in, s_out. + """ + c_in, c_out = ssl.MemoryBIO(), ssl.MemoryBIO() + s_in, s_out = ssl.MemoryBIO(), ssl.MemoryBIO() + client = client_context.wrap_bio(c_in, c_out, server_hostname=hostname) + server = server_context.wrap_bio(s_in, s_out, server_side=True) + + # Loop on the handshake for a bit to get it settled + for _ in range(max_retry): + with contextlib.suppress(ssl.SSLWantReadError): + client.do_handshake() + if c_out.pending: + s_in.write(c_out.read()) + with contextlib.suppress(ssl.SSLWantReadError): + server.do_handshake() + if s_out.pending: + c_in.write(s_out.read()) + # Now the handshakes should be complete (don't raise WantReadError) + client.do_handshake() + server.do_handshake() + return client, server, c_in, c_out, s_in, s_out + + class BasicSocketTests(unittest.TestCase): def test_constants(self): @@ -396,7 +442,7 @@ class BasicSocketTests(unittest.TestCase): ssl.OP_NO_COMPRESSION self.assertEqual(ssl.HAS_SNI, True) self.assertEqual(ssl.HAS_ECDH, True) - self.assertEqual(ssl.HAS_TLSv1_2, True) + self.assertIsInstance(ssl.HAS_TLSv1_2, bool) self.assertEqual(ssl.HAS_TLSv1_3, True) ssl.OP_NO_SSLv2 ssl.OP_NO_SSLv3 @@ -587,11 +633,11 @@ class BasicSocketTests(unittest.TestCase): # Some sanity checks follow # >= 1.1.1 self.assertGreaterEqual(n, 0x10101000) - # < 4.0 - self.assertLess(n, 0x40000000) + # < 5.0 + self.assertLess(n, 0x50000000) major, minor, fix, patch, status = t self.assertGreaterEqual(major, 1) - self.assertLess(major, 4) + self.assertLess(major, 5) self.assertGreaterEqual(minor, 0) self.assertLess(minor, 256) self.assertGreaterEqual(fix, 0) @@ -657,12 +703,14 @@ class BasicSocketTests(unittest.TestCase): ssl.OP_NO_TLSv1_2, ssl.OP_NO_TLSv1_3 ] - protocols = [ - ssl.PROTOCOL_TLSv1, - ssl.PROTOCOL_TLSv1_1, - ssl.PROTOCOL_TLSv1_2, - ssl.PROTOCOL_TLS - ] + protocols = [] + if hasattr(ssl, 'PROTOCOL_TLSv1'): + protocols.append(ssl.PROTOCOL_TLSv1) + if hasattr(ssl, 'PROTOCOL_TLSv1_1'): + protocols.append(ssl.PROTOCOL_TLSv1_1) + if hasattr(ssl, 'PROTOCOL_TLSv1_2'): + protocols.append(ssl.PROTOCOL_TLSv1_2) + protocols.append(ssl.PROTOCOL_TLS) versions = [ ssl.TLSVersion.SSLv3, ssl.TLSVersion.TLSv1, @@ -1027,6 +1075,58 @@ class ContextTests(unittest.TestCase): len(intersection), 2, f"\ngot: {sorted(names)}\nexpected: {sorted(expected)}" ) + def test_set_groups(self): + ctx = ssl.create_default_context() + # We use P-256 and P-384 (FIPS 186-4) that are alloed by OpenSSL + # even if FIPS module is enabled. Ignoring unknown groups is only + # supported since OpenSSL 3.3. + self.assertIsNone(ctx.set_groups('P-256:P-384')) + + self.assertRaises(ssl.SSLError, ctx.set_groups, 'P-256:foo') + if CAN_IGNORE_UNKNOWN_OPENSSL_GROUPS: + self.assertIsNone(ctx.set_groups('P-256:?foo')) + + @unittest.skipUnless(CAN_GET_AVAILABLE_OPENSSL_GROUPS, + "OpenSSL version doesn't support getting groups") + def test_get_groups(self): + ctx = ssl.create_default_context() + # By default, only return official IANA names. + self.assertNotIn('P-256', ctx.get_groups()) + self.assertIn('P-256', ctx.get_groups(include_aliases=True)) + + @unittest.skipUnless(CAN_GET_AVAILABLE_OPENSSL_SIGALGS, + "SSL library doesn't support getting sigalgs") + def test_get_sigalgs(self): + self.assertIn('rsa_pss_rsae_sha256', ssl.get_sigalgs()) + + @unittest.skipUnless(CAN_SET_CLIENT_SIGALGS, + "SSL library doesn't support setting client sigalgs") + def test_set_client_sigalgs(self): + ctx = ssl.create_default_context() + + self.assertIsNone(ctx.set_client_sigalgs('rsa_pss_rsae_sha256')) + + self.assertRaises(ssl.SSLError, ctx.set_client_sigalgs, + 'rsa_pss_rsae_sha256:foo') + + # Ignoring unknown sigalgs is only supported since OpenSSL 3.3. + if CAN_IGNORE_UNKNOWN_OPENSSL_SIGALGS: + self.assertIsNone(ctx.set_client_sigalgs('rsa_pss_rsae_sha256:?foo')) + + @unittest.skipUnless(CAN_SET_SERVER_SIGALGS, + "SSL library doesn't support setting server sigalgs") + def test_set_server_sigalgs(self): + ctx = ssl.create_default_context() + + self.assertIsNone(ctx.set_server_sigalgs('rsa_pss_rsae_sha256')) + + self.assertRaises(ssl.SSLError, ctx.set_server_sigalgs, + 'rsa_pss_rsae_sha256:foo') + + # Ignoring unknown sigalgs is only supported since OpenSSL 3.3. + if CAN_IGNORE_UNKNOWN_OPENSSL_SIGALGS: + self.assertIsNone(ctx.set_server_sigalgs('rsa_pss_rsae_sha256:?foo')) + def test_options(self): # Test default SSLContext options ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) @@ -1156,6 +1256,7 @@ class ContextTests(unittest.TestCase): ssl.TLSVersion.TLSv1, ssl.TLSVersion.TLSv1_1, ssl.TLSVersion.TLSv1_2, + ssl.TLSVersion.TLSv1_3, ssl.TLSVersion.SSLv3, } ) @@ -1169,7 +1270,7 @@ class ContextTests(unittest.TestCase): with self.assertRaises(ValueError): ctx.minimum_version = 42 - if has_tls_protocol(ssl.PROTOCOL_TLSv1_1): + if has_tls_protocol('PROTOCOL_TLSv1_1'): ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1_1) self.assertIn( @@ -1231,6 +1332,11 @@ class ContextTests(unittest.TestCase): with self.assertRaises(OSError) as cm: ctx.load_cert_chain(NONEXISTINGCERT) self.assertEqual(cm.exception.errno, errno.ENOENT) + self.assertEqual(cm.exception.filename, NONEXISTINGCERT) + with self.assertRaises(OSError) as cm: + ctx.load_cert_chain(CERTFILE, keyfile=NONEXISTINGKEY) + self.assertEqual(cm.exception.errno, errno.ENOENT) + self.assertEqual(cm.exception.filename, NONEXISTINGKEY) with self.assertRaisesRegex(ssl.SSLError, "PEM (lib|routines)"): ctx.load_cert_chain(BADCERT) with self.assertRaisesRegex(ssl.SSLError, "PEM (lib|routines)"): @@ -1533,19 +1639,27 @@ class ContextTests(unittest.TestCase): gc.collect() self.assertIs(wr(), None) - @unittest.skipUnless(support.Py_GIL_DISABLED, - "test is only useful if the GIL is disabled") + @support.skip_if_sanitizer("gh-150191: OpenSSL has an internal data race " + "when the SNI callback is replaced during a " + "handshake", thread=True) @threading_helper.requires_working_threading() def test_sni_callback_race(self): - # Replacing sni_callback while handshakes are in-flight must not + # Replacing sni_callback while a handshake is in-flight must not # crash (use-after-free on the callback in free-threaded builds). + # + # Use a single handshake thread: OpenSSL has internal data races + # on shared SSL_CTX state when multiple handshakes run + # concurrently against the same context (gh-150191). Concurrency + # on the *setter* is what exercises the fix from gh-149816, so + # multiple toggler threads race against each other and against + # the one handshake worker. client_ctx, server_ctx, hostname = testing_context() server_ctx.sni_callback = lambda *a: None - done = threading.Event() + deadline = time.monotonic() + 0.1 def do_handshakes(): - while not done.is_set(): + while time.monotonic() < deadline: c_in = ssl.MemoryBIO() c_out = ssl.MemoryBIO() s_in = ssl.MemoryBIO() @@ -1572,19 +1686,11 @@ class ContextTests(unittest.TestCase): c_in.write(s_out.read()) def toggle_callback(): - while not done.is_set(): + while time.monotonic() < deadline: server_ctx.sni_callback = lambda *a: None server_ctx.sni_callback = None - workers = max(4, (os.cpu_count() or 4) * 2) - threads = [threading.Thread(target=do_handshakes) - for _ in range(workers)] - threads.append(threading.Thread(target=toggle_callback)) - - with threading_helper.catch_threading_exception() as cm: - with threading_helper.start_threads(threads): - done.set() - self.assertIsNone(cm.exc_value) + threading_helper.run_concurrently([do_handshakes] + 4 * [toggle_callback]) def test_cert_store_stats(self): ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) @@ -1717,23 +1823,24 @@ class ContextTests(unittest.TestCase): self.assertFalse(ctx.check_hostname) self._assert_context_options(ctx) - if has_tls_protocol(ssl.PROTOCOL_TLSv1): + if has_tls_protocol('PROTOCOL_TLSv1'): with warnings_helper.check_warnings(): ctx = ssl._create_stdlib_context(ssl.PROTOCOL_TLSv1) self.assertEqual(ctx.protocol, ssl.PROTOCOL_TLSv1) self.assertEqual(ctx.verify_mode, ssl.CERT_NONE) self._assert_context_options(ctx) - with warnings_helper.check_warnings(): - ctx = ssl._create_stdlib_context( - ssl.PROTOCOL_TLSv1_2, - cert_reqs=ssl.CERT_REQUIRED, - check_hostname=True - ) - self.assertEqual(ctx.protocol, ssl.PROTOCOL_TLSv1_2) - self.assertEqual(ctx.verify_mode, ssl.CERT_REQUIRED) - self.assertTrue(ctx.check_hostname) - self._assert_context_options(ctx) + if has_tls_protocol('PROTOCOL_TLSv1_2'): + with warnings_helper.check_warnings(): + ctx = ssl._create_stdlib_context( + ssl.PROTOCOL_TLSv1_2, + cert_reqs=ssl.CERT_REQUIRED, + check_hostname=True + ) + self.assertEqual(ctx.protocol, ssl.PROTOCOL_TLSv1_2) + self.assertEqual(ctx.verify_mode, ssl.CERT_REQUIRED) + self.assertTrue(ctx.check_hostname) + self._assert_context_options(ctx) ctx = ssl._create_stdlib_context(purpose=ssl.Purpose.CLIENT_AUTH) self.assertEqual(ctx.protocol, ssl.PROTOCOL_TLS_SERVER) @@ -1911,6 +2018,10 @@ class SSLErrorTests(unittest.TestCase): def test_bad_server_hostname(self): ctx = ssl.create_default_context() + # Omitting the name entirely is bad too: this context checks it. + with self.assertRaises(ValueError): + ctx.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO(), + server_hostname=None) with self.assertRaises(ValueError): ctx.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO(), server_hostname="") @@ -1995,6 +2106,64 @@ class SSLObjectTests(unittest.TestCase): with self.assertRaisesRegex(TypeError, "public constructor"): ssl.SSLObject(bio, bio) + def test_check_hostname_requires_server_hostname(self): + # wrap_bio() used to accept a context asking for hostname checking + # without a name to check against, and then verify the certificate + # chain but never the peer's identity, with check_hostname still + # reporting True and nothing reporting the check had been skipped. + # It must refuse that call, as wrap_socket() already did. + client_context, _, hostname = testing_context() + self.assertTrue(client_context.check_hostname) + + for server_hostname in (None, ""): + with self.subTest(server_hostname=server_hostname): + with self.assertRaisesRegex( + ValueError, + "check_hostname requires server_hostname"): + client_context.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO(), + server_hostname=server_hostname) + # The sibling constructor refuses the very same call. + with socket.socket() as sock: + with self.assertRaisesRegex( + ValueError, + "check_hostname requires server_hostname"): + client_context.wrap_socket( + sock, server_hostname=server_hostname) + + # A name was all that was missing. + client_context.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO(), + server_hostname=hostname) + + # Asking for no hostname check remains a way to say so explicitly. + context = make_test_context() + self.assertFalse(context.check_hostname) + context.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO()) + + def test_server_side_bad_params(self): + # A server neither sends a hostname nor resumes a client's session, + # so wrap_bio() rejects both in server mode like wrap_socket() + client_context, server_context, hostname = testing_context() + + with self.assertRaisesRegex( + ValueError, + "server_hostname can only be specified in client mode"): + server_context.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO(), + server_side=True, + server_hostname=hostname) + + client, server, *_ = connected_bio_pair( + client_context, server_context, hostname) + session = client.session + self.assertIsNotNone(session) + with self.assertRaisesRegex( + ValueError, "session can only be specified in client mode"): + server_context.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO(), + server_side=True, session=session) + + # Neither argument is what a server passes, so this still works. + server_context.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO(), + server_side=True) + def test_unwrap(self): client_ctx, server_ctx, hostname = testing_context() c_in = ssl.MemoryBIO() @@ -2037,6 +2206,86 @@ class SSLObjectTests(unittest.TestCase): c_in.write(s_out.read()) client.unwrap() + 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 + # references to the context that carries it, and raises. The C + # callback must not touch that context after the Python callback + # returned. + client_ctx, server_ctx, hostname = testing_context() + leaf_ctx = server_ctx + + def sni_cb(sslobj, server_name, ctx): + sslobj.context = leaf_ctx + del ctx + raise LookupError("no certificate for " + repr(server_name)) + + def make_server(): + dispatch_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + dispatch_ctx.load_cert_chain(SIGNED_CERTFILE) + dispatch_ctx.sni_callback = sni_cb + s_in, s_out = ssl.MemoryBIO(), ssl.MemoryBIO() + server = dispatch_ctx.wrap_bio(s_in, s_out, server_side=True) + return server, s_in, s_out + + server, s_in, s_out = make_server() + c_in, c_out = ssl.MemoryBIO(), ssl.MemoryBIO() + client = client_ctx.wrap_bio(c_in, c_out, server_hostname=hostname) + with self.assertRaises(ssl.SSLWantReadError): + client.do_handshake() + s_in.write(c_out.read()) + with support.catch_unraisable_exception() as cm: + with self.assertRaises(ssl.SSLError): + server.do_handshake() + self.assertIsInstance(cm.unraisable.exc_value, LookupError) + self.assertIs(server.context, leaf_ctx) + + def test_sni_callback_context_released_before_second_client_hello(self): + # The SSLContext carrying sni_callback may be released by the + # application once the callback has switched the connection over to + # another context. If the server then sends a HelloRetryRequest, the + # second ClientHello makes OpenSSL consult the original SSL_CTX's + # servername callback again; that must not use the deallocated + # SSLContext object. + client_ctx, leaf_ctx, hostname = testing_context() + calls = [] + + def make_server(): + dispatch_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + dispatch_ctx.load_cert_chain(SIGNED_CERTFILE) + # Force a HelloRetryRequest: the client offers an X25519 key + # share first, the server only accepts P-384. + dispatch_ctx.set_ecdh_curve("secp384r1") + def sni_cb(sslobj, server_name, ctx): + calls.append(server_name) + sslobj.context = leaf_ctx + dispatch_ctx.sni_callback = sni_cb + s_in, s_out = ssl.MemoryBIO(), ssl.MemoryBIO() + server = dispatch_ctx.wrap_bio(s_in, s_out, server_side=True) + return server, s_in, s_out, weakref.ref(dispatch_ctx) + + # After this only the C-level SSL object references dispatch_ctx. + server, s_in, s_out, dispatch_ref = make_server() + c_in, c_out = ssl.MemoryBIO(), ssl.MemoryBIO() + client = client_ctx.wrap_bio(c_in, c_out, server_hostname=hostname) + for _ in range(10): + for obj, out, peer_in in ((client, c_out, s_in), + (server, s_out, c_in)): + try: + obj.do_handshake() + except ssl.SSLWantReadError: + pass + if out.pending: + peer_in.write(out.read()) + client.do_handshake() + server.do_handshake() + support.gc_collect() + self.assertIsNone(dispatch_ref()) + self.assertGreaterEqual(len(calls), 1) + self.assertEqual(calls[0], hostname) + self.assertIs(server.context, leaf_ctx) + self.assertIsNotNone(client.cipher()) + class SimpleBackgroundTests(unittest.TestCase): """Tests that connect to a simple server running in the background""" @@ -2417,6 +2666,68 @@ class SimpleBackgroundTests(unittest.TestCase): self.assertRaises(ssl.SSLEOFError, sslobj.read) +@unittest.skipUnless(has_tls_version('TLSv1_3'), "TLS 1.3 is not available") +class SimpleBackgroundTestsTLS_1_3(unittest.TestCase): + """Tests that connect to a simple server running in the background.""" + + def setUp(self): + server_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + ciphers = [cipher['name'] for cipher in server_ctx.get_ciphers() + if cipher['protocol'] == 'TLSv1.3'] + + if not ciphers: + self.skipTest("No cipher supports TLSv1.3") + + self.matching_cipher = ciphers[0] + # Some tests need at least two ciphers, and are responsible + # to skip themselves if matching_cipher == mismatched_cipher. + self.mismatched_cipher = ciphers[-1] + + server_ctx.set_ciphersuites(self.matching_cipher) + server_ctx.load_cert_chain(SIGNED_CERTFILE) + server = ThreadedEchoServer(context=server_ctx) + self.enterContext(server) + self.server_addr = (HOST, server.port) + + def test_ciphersuites(self): + # Test unrecognized TLS 1.3 cipher suite name + with ( + socket.socket(socket.AF_INET) as sock, + self.assertRaisesRegex(ssl.SSLError, + "No cipher suite can be selected") + ): + test_wrap_socket(sock, cert_reqs=ssl.CERT_NONE, + ciphersuites="XXX", + min_version=ssl.TLSVersion.TLSv1_3) + + # Test successful TLS 1.3 handshake + with test_wrap_socket(socket.socket(socket.AF_INET), + cert_reqs=ssl.CERT_NONE, + ciphersuites=self.matching_cipher, + min_version=ssl.TLSVersion.TLSv1_3) as s: + s.connect(self.server_addr) + self.assertEqual(s.cipher()[0], self.matching_cipher) + + def test_ciphersuite_downgrade(self): + with test_wrap_socket(socket.socket(socket.AF_INET), + cert_reqs=ssl.CERT_NONE, + ciphersuites=self.matching_cipher, + min_version=ssl.TLSVersion.TLSv1_2, + max_version=ssl.TLSVersion.TLSv1_2) as s: + s.connect(self.server_addr) + self.assertEqual(s.cipher()[1], 'TLSv1.2') + + def test_ciphersuite_mismatch(self): + if self.matching_cipher == self.mismatched_cipher: + self.skipTest("Multiple TLS 1.3 ciphers are not available") + + with test_wrap_socket(socket.socket(socket.AF_INET), + cert_reqs=ssl.CERT_NONE, + ciphersuites=self.mismatched_cipher, + min_version=ssl.TLSVersion.TLSv1_3) as s: + self.assertRaises(ssl.SSLError, s.connect, self.server_addr) + + @support.requires_resource('network') class NetworkedTests(unittest.TestCase): @@ -2760,6 +3071,36 @@ class ThreadedEchoServer(threading.Thread): def stop(self): self.active = False +class TestEOFServer(threading.Thread): + def __init__(self): + super().__init__() + self.listening = threading.Event() + self.address = None + + def run(self): + context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + context.load_cert_chain(CERTFILE) + server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + with server_sock: + server_sock.settimeout(support.SHORT_TIMEOUT) + server_sock.bind((HOST, 0)) + server_sock.listen(5) + + self.address = server_sock.getsockname() + self.listening.set() + + sock, addr = server_sock.accept() + sslconn = context.wrap_socket(sock, server_side=True) + with sslconn: + request = b'' + while chunk := sslconn.recv(1024): + request += chunk + if b'\n' in chunk: + break + + sslconn.sendall(b'server\n') + sslconn.shutdown(socket.SHUT_WR) + class AsyncoreEchoServer(threading.Thread): # this one's based on asyncore.dispatcher @@ -2921,6 +3262,11 @@ def server_params_test(client_context, server_context, indata=b"FOO\n", 'session_reused': s.session_reused, 'session': s.session, }) + if CAN_GET_SELECTED_OPENSSL_GROUP: + stats.update({'group': s.group()}) + if CAN_GET_SELECTED_OPENSSL_SIGALG: + stats.update({'client_sigalg': s.client_sigalg()}) + stats.update({'server_sigalg': s.server_sigalg()}) s.close() stats['server_alpn_protocols'] = server.selected_alpn_protocols stats['server_shared_ciphers'] = server.shared_ciphers @@ -3629,10 +3975,10 @@ class ThreadedTests(unittest.TestCase): client_options=ssl.OP_NO_TLSv1_2) try_protocol_combo(ssl.PROTOCOL_TLS, ssl.PROTOCOL_TLSv1_2, 'TLSv1.2') - if has_tls_protocol(ssl.PROTOCOL_TLSv1): + if has_tls_protocol('PROTOCOL_TLSv1'): try_protocol_combo(ssl.PROTOCOL_TLSv1_2, ssl.PROTOCOL_TLSv1, False) try_protocol_combo(ssl.PROTOCOL_TLSv1, ssl.PROTOCOL_TLSv1_2, False) - if has_tls_protocol(ssl.PROTOCOL_TLSv1_1): + if has_tls_protocol('PROTOCOL_TLSv1_1'): try_protocol_combo(ssl.PROTOCOL_TLSv1_2, ssl.PROTOCOL_TLSv1_1, False) try_protocol_combo(ssl.PROTOCOL_TLSv1_1, ssl.PROTOCOL_TLSv1_2, False) @@ -4071,6 +4417,8 @@ class ThreadedTests(unittest.TestCase): with self.assertRaises(OSError): s.connect((HOST, server.port)) self.assertIn("NO_SHARED_CIPHER", server.conn_errors[0]) + self.assertIsNone(s.cipher()) + self.assertIsNone(s.group()) def test_version_basic(self): """ @@ -4346,6 +4694,113 @@ class ThreadedTests(unittest.TestCase): chatty=True, connectionchatty=True, sni_name=hostname) + def test_groups(self): + # server secp384r1, client auto + client_context, server_context, hostname = testing_context() + + server_context.set_groups("secp384r1") + server_context.minimum_version = ssl.TLSVersion.TLSv1_3 + stats = server_params_test(client_context, server_context, + chatty=True, connectionchatty=True, + sni_name=hostname) + if CAN_GET_SELECTED_OPENSSL_GROUP: + self.assertEqual(stats['group'], "secp384r1") + + # server auto, client secp384r1 + client_context, server_context, hostname = testing_context() + client_context.set_groups("secp384r1") + server_context.minimum_version = ssl.TLSVersion.TLSv1_3 + stats = server_params_test(client_context, server_context, + chatty=True, connectionchatty=True, + sni_name=hostname) + if CAN_GET_SELECTED_OPENSSL_GROUP: + self.assertEqual(stats['group'], "secp384r1") + + # server / client curve mismatch + client_context, server_context, hostname = testing_context() + client_context.set_groups("prime256v1") + server_context.set_groups("secp384r1") + server_context.minimum_version = ssl.TLSVersion.TLSv1_3 + with self.assertRaises(ssl.SSLError): + server_params_test(client_context, server_context, + chatty=True, connectionchatty=True, + sni_name=hostname) + + @unittest.skipUnless(CAN_SET_CLIENT_SIGALGS, + "SSL library doesn't support setting client sigalgs") + def test_client_sigalgs(self): + # no mutual auth, so cient_sigalg should be None + client_context, server_context, hostname = testing_context() + stats = server_params_test(client_context, server_context, + chatty=True, connectionchatty=True, + sni_name=hostname) + if CAN_GET_SELECTED_OPENSSL_SIGALG: + self.assertIsNone(stats['client_sigalg']) + + # server auto, client rsa_pss_rsae_sha384 + sigalg = "rsa_pss_rsae_sha384" + client_context, server_context, hostname = \ + testing_context(client_cert=SIGNED_CERTFILE) + client_context.set_client_sigalgs(sigalg) + stats = server_params_test(client_context, server_context, + chatty=True, connectionchatty=True, + sni_name=hostname) + if CAN_GET_SELECTED_OPENSSL_SIGALG: + self.assertEqual(stats['client_sigalg'], sigalg) + + @unittest.skipUnless(CAN_SET_CLIENT_SIGALGS, + "SSL library doesn't support setting client sigalgs") + def test_client_sigalgs_mismatch(self): + client_context, server_context, hostname = \ + testing_context(client_cert=SIGNED_CERTFILE) + client_context.set_client_sigalgs("rsa_pss_rsae_sha256") + server_context.set_client_sigalgs("rsa_pss_rsae_sha384") + + with self.assertRaises(( + ssl.SSLError, + # On handshake failures, some systems raise a ConnectionResetError. + ConnectionResetError, + # On handshake failures, macOS may raise a BrokenPipeError. + # See https://github.com/python/cpython/issues/139504. + BrokenPipeError, + )): + server_params_test(client_context, server_context, + chatty=True, connectionchatty=True, + sni_name=hostname) + + @unittest.skipUnless(CAN_SET_SERVER_SIGALGS, + "SSL library doesn't support setting server sigalgs") + def test_server_sigalgs(self): + # server rsa_pss_rsae_sha384, client auto + sigalg = "rsa_pss_rsae_sha384" + client_context, server_context, hostname = testing_context() + server_context.set_server_sigalgs(sigalg) + stats = server_params_test(client_context, server_context, + chatty=True, connectionchatty=True, + sni_name=hostname) + if CAN_GET_SELECTED_OPENSSL_SIGALG: + self.assertEqual(stats['server_sigalg'], sigalg) + + # server auto, client rsa_pss_rsae_sha384 + client_context, server_context, hostname = testing_context() + client_context.set_server_sigalgs(sigalg) + stats = server_params_test(client_context, server_context, + chatty=True, connectionchatty=True, + sni_name=hostname) + if CAN_GET_SELECTED_OPENSSL_SIGALG: + self.assertEqual(stats['server_sigalg'], sigalg) + + @unittest.skipUnless(CAN_SET_SERVER_SIGALGS, + "SSL library doesn't support setting server sigalgs") + def test_server_sigalgs_mismatch(self): + client_context, server_context, hostname = testing_context() + client_context.set_server_sigalgs("rsa_pss_rsae_sha256") + server_context.set_server_sigalgs("rsa_pss_rsae_sha384") + with self.assertRaises(ssl.SSLError): + server_params_test(client_context, server_context, + chatty=True, connectionchatty=True, + sni_name=hostname) + def test_selected_alpn_protocol(self): # selected_alpn_protocol() is None unless ALPN is used. client_context, server_context, hostname = testing_context() @@ -4481,7 +4936,9 @@ class ThreadedTests(unittest.TestCase): sni_name='supermessage') # Allow for flexible libssl error messages. - regex = "(SSLV3_ALERT_HANDSHAKE_FAILURE|NO_PRIVATE_VALUE)" + regex = ("(TLS_ALERT_HANDSHAKE_FAILURE" + "|SSLV3_ALERT_HANDSHAKE_FAILURE" + "|NO_PRIVATE_VALUE)") self.assertRegex(cm.exception.reason, regex) self.assertEqual(catch.unraisable.exc_type, ZeroDivisionError) @@ -4536,19 +4993,30 @@ class ThreadedTests(unittest.TestCase): self.assertRaises(ValueError, s.write, b'hello') def test_sendfile(self): + """Try to send a file using kTLS if possible.""" TEST_DATA = b"x" * 512 with open(os_helper.TESTFN, 'wb') as f: f.write(TEST_DATA) self.addCleanup(os_helper.unlink, os_helper.TESTFN) client_context, server_context, hostname = testing_context() + client_context.options |= getattr(ssl, 'OP_ENABLE_KTLS', 0) server = ThreadedEchoServer(context=server_context, chatty=False) - with server: - with client_context.wrap_socket(socket.socket(), - server_hostname=hostname) as s: - s.connect((HOST, server.port)) + # kTLS seems to work only with a connection created before + # wrapping `sock` by the SSL context in contrast to calling + # `sock.connect()` after the wrapping. + with server, socket.create_connection((HOST, server.port)) as sock: + with client_context.wrap_socket( + sock, server_hostname=hostname + ) as ssock: + if support.verbose: + ktls_used = ssock._sslobj.uses_ktls_for_send() + print( + 'kTLS is', + 'available' if ktls_used else 'unavailable', + ) with open(os_helper.TESTFN, 'rb') as file: - s.sendfile(file) - self.assertEqual(s.recv(1024), TEST_DATA) + ssock.sendfile(file) + self.assertEqual(ssock.recv(1024), TEST_DATA) def test_session(self): client_context, server_context, hostname = testing_context() @@ -4727,6 +5195,7 @@ class ThreadedTests(unittest.TestCase): @requires_tls_version('TLSv1_3') @unittest.skipUnless(ssl.HAS_PSK, 'TLS-PSK disabled on this OpenSSL build') + @unittest.skipUnless(ssl.HAS_PSK_TLS13, 'TLS 1.3 PSK disabled on this OpenSSL build') def test_psk_tls1_3(self): psk = bytes.fromhex('deadbeef') identity_hint = 'identity-hint' @@ -4758,6 +5227,9 @@ class ThreadedTests(unittest.TestCase): with client_context.wrap_socket(socket.socket()) as s: s.connect((HOST, server.port)) + @support.skip_if_sanitizer("gh-150191: OpenSSL races on SSL->rwstate and " + "the socket BIO flags with concurrent read " + "and write", thread=True) def test_thread_recv_while_main_thread_sends(self): # GH-137583: Locking was added to calls to send() and recv() on SSL # socket objects. This seemed fine at the surface level because those @@ -4794,6 +5266,50 @@ class ThreadedTests(unittest.TestCase): if cm.exc_value is not None: raise cm.exc_value + def test_got_eof(self): + # gh-148292: Test that _ssl._SSLSocket behaves the same on all OpenSSL + # versions on calling methods after EOF (after the first SSLEOFError). + + server = TestEOFServer() + server.start() + if not server.listening.wait(support.SHORT_TIMEOUT): + raise RuntimeError("server took too long") + self.addCleanup(server.join) + + context = ssl.create_default_context(cafile=CERTFILE) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(support.SHORT_TIMEOUT) + sock.connect(server.address) + sslsock = context.wrap_socket(sock, server_hostname='localhost') + with sslsock: + sslsock.sendall(b'client\n') + # test the _ssl._SSLSocket object, not ssl.SSLSocket + sslobj = sslsock._sslobj + + data = sslobj.read(1024) + self.assertEqual(data, b'server\n') + + # The second read gets EOF error and sets got_eof_error to 1 + with self.assertRaises(ssl.SSLEOFError): + sslobj.read(1024) + + # Following read(), sendfile(), write() and do_handshake() calls + # must raise SSLEOFError + with self.assertRaises(ssl.SSLEOFError): + # The _SSLSocket remembers the previous EOF error + # and raises again SSLEOFError + sslobj.read(1024) + if hasattr(sslobj, 'sendfile'): + with open(__file__, "rb") as fp: + with self.assertRaises(ssl.SSLEOFError): + sslobj.sendfile(fp.fileno(), 0, 1) + with self.assertRaises(ssl.SSLEOFError): + sslobj.write(b'client2\n') + with self.assertRaises(ssl.SSLEOFError): + sslsock.do_handshake() + + self.assertEqual(sslsock.pending(), 0) + @unittest.skipUnless(has_tls_version('TLSv1_3') and ssl.HAS_PHA, "Test needs TLS 1.3 PHA") @@ -5220,6 +5736,20 @@ class TestSSLDebug(unittest.TestCase): with self.assertRaises(TypeError): client_context._msg_callback = object() + def test_msg_callback_exception(self): + client_context, server_context, hostname = testing_context() + + def msg_cb(conn, direction, version, content_type, msg_type, data): + raise RuntimeError("msg_cb exception") + + client_context._msg_callback = msg_cb + server = ThreadedEchoServer(context=server_context, chatty=False) + with server: + with client_context.wrap_socket(socket.socket(), + server_hostname=hostname) as s: + with self.assertRaisesRegex(RuntimeError, "msg_cb exception"): + s.connect((HOST, server.port)) + def test_msg_callback_tls12(self): client_context, server_context, hostname = testing_context() client_context.maximum_version = ssl.TLSVersion.TLSv1_2 diff --git a/crates/host_env/src/ssl/cipher.rs b/crates/host_env/src/ssl/cipher.rs index 34c50f15d..6038cd323 100644 --- a/crates/host_env/src/ssl/cipher.rs +++ b/crates/host_env/src/ssl/cipher.rs @@ -444,9 +444,9 @@ pub fn kx_group_by_openssl_name(name: &str) -> Option<&'static dyn SupportedKxGr fn kx_group_openssl_names(name: rustls::NamedGroup) -> &'static [&'static str] { match name { - rustls::NamedGroup::secp256r1 => &["prime256v1", "secp256r1"], - rustls::NamedGroup::secp384r1 => &["secp384r1", "prime384v1"], - rustls::NamedGroup::secp521r1 => &["secp521r1", "prime521v1"], + rustls::NamedGroup::secp256r1 => &["prime256v1", "secp256r1", "P-256"], + rustls::NamedGroup::secp384r1 => &["secp384r1", "prime384v1", "P-384"], + rustls::NamedGroup::secp521r1 => &["secp521r1", "prime521v1", "P-521"], rustls::NamedGroup::X25519 => &["X25519", "x25519"], rustls::NamedGroup::X448 => &["X448", "x448"], rustls::NamedGroup::MLKEM768 => &["MLKEM768"], diff --git a/crates/host_env/src/ssl/config.rs b/crates/host_env/src/ssl/config.rs index f19e53e18..81fb37eda 100644 --- a/crates/host_env/src/ssl/config.rs +++ b/crates/host_env/src/ssl/config.rs @@ -22,6 +22,7 @@ pub struct ProtocolSettings { pub versions: &'static [&'static rustls::SupportedProtocolVersion], pub kx_groups: Option>, pub cipher_suites: Option>, + pub server_sigalgs: Option>, pub alpn_protocols: Vec>, } @@ -176,12 +177,27 @@ pub fn create_server_config(options: ServerConfigOptions) -> Result + } else { + resolver + }; builder.with_cert_resolver(resolver) } else { // Use single certificate - builder - .with_single_cert(options.cert_chain, options.private_key) - .map_err(|e| format!("Failed to set server certificate: {e}"))? + let key = CertifiedKey::from_der( + options.cert_chain, + options.private_key, + CryptoExt::get_provider(), + ) + .map_err(|e| format!("Failed to set server certificate: {e}"))?; + builder.with_cert_resolver(Arc::new(MultiCertResolver::new( + vec![Arc::new(key)], + options.protocol_settings.server_sigalgs.as_deref(), + ))) }; // Set ALPN protocols with fallback @@ -432,6 +448,15 @@ pub fn create_client_config(options: ClientConfigOptions) -> Result + } else { + verifier + }; + // Step 2: Create ClientConfig builder once with the selected verifier let builder = ClientConfig::builder_with_provider(custom_provider) .with_protocol_versions(options.protocol_settings.versions) @@ -481,7 +506,15 @@ pub struct MultiCertResolver { impl MultiCertResolver { /// Create a new multi-certificate resolver - pub fn new(cert_keys: Vec>) -> Self { + pub fn new( + mut cert_keys: Vec>, + schemes: Option<&[rustls::SignatureScheme]>, + ) -> Self { + if let Some(schemes) = schemes { + for key in &mut cert_keys { + *key = super::sigalg::restrict_key(key, schemes); + } + } Self { cert_keys } } } diff --git a/crates/host_env/src/ssl/error.rs b/crates/host_env/src/ssl/error.rs index cec73e49f..a7ddcf613 100644 --- a/crates/host_env/src/ssl/error.rs +++ b/crates/host_env/src/ssl/error.rs @@ -43,6 +43,9 @@ impl TlsError { use rustls::PeerIncompatible; match peer_err { PeerIncompatible::NoCipherSuitesInCommon => Self::NoCipherSuites, + PeerIncompatible::NoSignatureSchemesInCommon => { + Self::Ssl("no signature schemes in common".to_owned()) + } _ => Self::Eof, } } @@ -66,3 +69,38 @@ impl From for TlsError { Self::Io(err) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn signature_incompatibility_is_not_transport_eof() { + let error = TlsError::from_rustls(rustls::Error::PeerIncompatible( + rustls::PeerIncompatible::NoSignatureSchemesInCommon, + )); + assert!(!error.is_eof()); + assert!(matches!(error, TlsError::Ssl(_))); + } + + #[test] + fn unrelated_negotiation_and_transport_errors_keep_their_classification() { + assert!(matches!( + TlsError::from_rustls(rustls::Error::PeerIncompatible( + rustls::PeerIncompatible::NoCipherSuitesInCommon, + )), + TlsError::NoCipherSuites + )); + assert!(matches!( + TlsError::from_rustls(rustls::Error::PeerIncompatible( + rustls::PeerIncompatible::Tls12NotOffered, + )), + TlsError::Eof + )); + let os_error = std::io::Error::from_raw_os_error(10053); + match TlsError::from(os_error) { + TlsError::Io(error) => assert_eq!(error.raw_os_error(), Some(10053)), + error => panic!("OS error was reclassified: {error:?}"), + } + } +} diff --git a/crates/host_env/src/ssl/handshake.rs b/crates/host_env/src/ssl/handshake.rs index 6df5d070c..f70f53370 100644 --- a/crates/host_env/src/ssl/handshake.rs +++ b/crates/host_env/src/ssl/handshake.rs @@ -47,6 +47,34 @@ pub fn sni_alert(description: u8) -> Vec { vec![21, 3, 3, 0, 2, 2, description] } +/// Serialize a failed initial `Accepted::into_connection` attempt. +/// +/// No server bytes have been sent at this boundary. Rustls may have queued +/// ServerHello before finding that a full handshake has no usable signature, +/// or may return a signing-key error without queuing any alert (TLS 1.2). +/// Send only a plaintext fatal alert, so the peer cannot start its next flight +/// while the server is closing. Keep certificate/session selection in rustls: +/// resumed sessions do not need a new certificate signature. +pub fn initial_handshake_alert( + error: &rustls::Error, + mut alert: rustls::server::AcceptedAlert, +) -> std::io::Result> { + // TLS 1.2's emit_server_kx reports the exact General message below when + // SigningKey::choose_scheme fails; TLS 1.3 uses PeerIncompatible. + let signature_mismatch = matches!( + error, + rustls::Error::PeerIncompatible(rustls::PeerIncompatible::NoSignatureSchemesInCommon) + ) || matches!(error, rustls::Error::General(message) if message == "incompatible signing key"); + if signature_mismatch { + return Ok(sni_alert(u8::from( + rustls::AlertDescription::HandshakeFailure, + ))); + } + let mut bytes = Vec::new(); + alert.write_all(&mut bytes)?; + Ok(bytes) +} + pub fn feed_acceptor(acceptor: &mut Acceptor, bytes: &[u8]) -> std::io::Result<()> { let mut reader = std::io::Cursor::new(bytes); while reader.position() < bytes.len() as u64 { @@ -107,3 +135,322 @@ mod tests { assert_eq!(sni_alert(80), [21, 3, 3, 0, 2, 2, 80]); } } + +#[cfg(test)] +mod signature_tests { + use super::super::config::MultiCertResolver; + use super::*; + use alloc::sync::Arc; + use rustls::{ + ClientConnection, SignatureScheme, client::WebPkiServerVerifier, server::Acceptor, + }; + use rustls::{RootCertStore, SupportedCipherSuite, crypto::CryptoProvider, sign::CertifiedKey}; + use rustls::{client::ClientConfig, server::ServerConfig}; + + fn certificate_key(mut pem: &[u8], provider: &CryptoProvider) -> Arc { + let mut private_key_pem = pem; + let certificates = rustls_pemfile::certs(&mut pem) + .collect::, _>>() + .unwrap(); + let key = rustls_pemfile::private_key(&mut private_key_pem) + .unwrap() + .unwrap(); + Arc::new(CertifiedKey::from_der(certificates, key, provider).unwrap()) + } + + fn client_config( + version: &'static rustls::SupportedProtocolVersion, + scheme: SignatureScheme, + cipher_suite: Option, + ) -> Arc { + let mut provider = rustls::crypto::aws_lc_rs::default_provider(); + if let Some(suite) = cipher_suite { + provider.cipher_suites = vec![suite]; + } + let provider = Arc::new(provider); + let mut roots = RootCertStore::empty(); + let ca = include_bytes!("../../../../Lib/test/certdata/pycacert.pem"); + for cert in rustls_pemfile::certs(&mut &ca[..]) { + roots.add(cert.unwrap()).unwrap(); + } + let verifier = + WebPkiServerVerifier::builder_with_provider(Arc::new(roots), provider.clone()) + .build() + .unwrap(); + let config = ClientConfig::builder_with_provider(provider) + .with_protocol_versions(&[version]) + .unwrap() + .dangerous() + .with_custom_certificate_verifier(Arc::new(super::super::sigalg::ServerVerifier { + inner: verifier, + schemes: vec![scheme], + })) + .with_no_client_auth(); + Arc::new(config) + } + + fn client_hello(config: Arc) -> (ClientConnection, rustls::server::Accepted) { + let mut client = ClientConnection::new(config, "localhost".try_into().unwrap()).unwrap(); + let mut hello = Vec::new(); + client.write_tls(&mut hello).unwrap(); + let mut acceptor = Acceptor::default(); + super::super::handshake::feed_acceptor(&mut acceptor, &hello).unwrap(); + (client, acceptor.accept().unwrap().unwrap()) + } + + fn server_config( + version: &'static rustls::SupportedProtocolVersion, + include_incompatible_first_key: bool, + cipher_suite: Option, + scheme: SignatureScheme, + ) -> Arc { + let mut provider = rustls::crypto::aws_lc_rs::default_provider(); + if let Some(suite) = cipher_suite { + provider.cipher_suites = vec![suite]; + } + let provider = Arc::new(provider); + let mut keys = Vec::new(); + if include_incompatible_first_key { + keys.push(certificate_key( + include_bytes!("../../../../Lib/test/certdata/keycertecc.pem"), + &provider, + )); + } + keys.push(certificate_key( + include_bytes!("../../../../Lib/test/certdata/keycert3.pem"), + &provider, + )); + Arc::new( + ServerConfig::builder_with_provider(provider) + .with_protocol_versions(&[version]) + .unwrap() + .with_no_client_auth() + .with_cert_resolver(Arc::new(MultiCertResolver::new(keys, Some(&[scheme])))), + ) + } + + fn assert_signature_mismatch_sends_only_a_fatal_alert( + version: &'static rustls::SupportedProtocolVersion, + ) { + let (mut client, accepted) = client_hello(client_config( + version, + SignatureScheme::RSA_PSS_SHA256, + None, + )); + let (error, alert) = accepted + .into_connection(server_config( + version, + false, + None, + SignatureScheme::RSA_PSS_SHA384, + )) + .unwrap_err(); + let bytes = initial_handshake_alert(&error, alert).unwrap(); + assert!(matches!( + super::super::error::TlsError::from_rustls(error), + super::super::error::TlsError::Ssl(_) + )); + assert_eq!(bytes.len(), 7, "only a plaintext alert may precede failure"); + assert_eq!(bytes[0], 21, "no ServerHello or CCS may precede the alert"); + assert_eq!(&bytes[3..6], &[0, 2, 2]); + client.read_tls(&mut &bytes[..]).unwrap(); + assert!(matches!( + client.process_new_packets(), + Err(rustls::Error::AlertReceived(_)) + )); + assert!( + !client.wants_write(), + "a rejected client must not queue CCS" + ); + } + + #[test] + fn tls12_signature_mismatch_sends_only_a_fatal_alert() { + assert_signature_mismatch_sends_only_a_fatal_alert(&rustls::version::TLS12); + } + + #[test] + fn tls13_signature_mismatch_sends_only_a_fatal_alert() { + assert_signature_mismatch_sends_only_a_fatal_alert(&rustls::version::TLS13); + } + + #[test] + fn unrelated_initial_alert_and_similar_error_text_are_preserved() { + for override_error in [ + None, + Some(rustls::Error::General( + "incompatible signing key detail".into(), + )), + ] { + let (mut client, accepted) = client_hello(client_config( + &rustls::version::TLS12, + SignatureScheme::RSA_PSS_SHA384, + None, + )); + let (error, alert) = accepted + .into_connection(server_config( + &rustls::version::TLS13, + false, + None, + SignatureScheme::RSA_PSS_SHA384, + )) + .unwrap_err(); + let bytes = initial_handshake_alert(&override_error.unwrap_or(error), alert).unwrap(); + assert_eq!(bytes, sni_alert(70)); + client.read_tls(&mut &bytes[..]).unwrap(); + assert!(matches!( + client.process_new_packets(), + Err(rustls::Error::AlertReceived( + rustls::AlertDescription::ProtocolVersion + )) + )); + } + } + + #[test] + fn disjoint_cipher_suites_keep_the_no_shared_cipher_error() { + use rustls::crypto::aws_lc_rs::cipher_suite::{ + TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + }; + let version = &rustls::version::TLS12; + let (_, accepted) = client_hello(client_config( + version, + SignatureScheme::RSA_PSS_SHA384, + Some(TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256), + )); + let (error, _) = accepted + .into_connection(server_config( + version, + false, + Some(TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384), + SignatureScheme::RSA_PSS_SHA384, + )) + .unwrap_err(); + assert!(matches!( + super::super::error::TlsError::from_rustls(error), + super::super::error::TlsError::NoCipherSuites + )); + } + + #[test] + fn incompatible_certificate_algorithm_keeps_the_no_shared_cipher_error() { + use rustls::crypto::aws_lc_rs::cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256; + let version = &rustls::version::TLS12; + let (_, accepted) = client_hello(client_config( + version, + SignatureScheme::ECDSA_NISTP256_SHA256, + Some(TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256), + )); + let (error, _) = accepted + .into_connection(server_config( + version, + false, + None, + SignatureScheme::RSA_PSS_SHA384, + )) + .unwrap_err(); + assert!(matches!( + super::super::error::TlsError::from_rustls(error), + super::super::error::TlsError::NoCipherSuites + )); + } + + fn finish_handshake(client: &mut ClientConnection, server: &mut rustls::ServerConnection) { + for _ in 0..16 { + let mut bytes = Vec::new(); + server.write_tls(&mut bytes).unwrap(); + if !bytes.is_empty() { + client.read_tls(&mut &bytes[..]).unwrap(); + client.process_new_packets().unwrap(); + } + bytes.clear(); + client.write_tls(&mut bytes).unwrap(); + if !bytes.is_empty() { + server.read_tls(&mut &bytes[..]).unwrap(); + server.process_new_packets().unwrap(); + } + if !client.is_handshaking() + && !server.is_handshaking() + && !client.wants_write() + && !server.wants_write() + { + return; + } + } + panic!("in-memory handshake did not finish within 16 exchanges"); + } + + #[test] + fn signature_match_selects_a_later_compatible_key() { + for version in [&rustls::version::TLS12, &rustls::version::TLS13] { + let client_config = client_config(version, SignatureScheme::RSA_PSS_SHA384, None); + let server_config = server_config(version, true, None, SignatureScheme::RSA_PSS_SHA384); + for expected_kind in [rustls::HandshakeKind::Full, rustls::HandshakeKind::Resumed] { + let (mut client, accepted) = client_hello(client_config.clone()); + let mut server = accepted.into_connection(server_config.clone()).unwrap(); + finish_handshake(&mut client, &mut server); + assert_eq!(client.handshake_kind(), Some(expected_kind)); + assert_eq!(server.handshake_kind(), Some(expected_kind)); + } + } + } + + fn assert_resumption_survives_signature_policy_change( + version: &'static rustls::SupportedProtocolVersion, + ) { + let client_config = client_config(version, SignatureScheme::RSA_PSS_SHA256, None); + let initial_config = server_config(version, false, None, SignatureScheme::RSA_PSS_SHA256); + let (mut client, accepted) = client_hello(client_config.clone()); + let mut server = accepted.into_connection(initial_config.clone()).unwrap(); + finish_handshake(&mut client, &mut server); + assert_eq!(client.handshake_kind(), Some(rustls::HandshakeKind::Full)); + + // SSLContext retains its server session cache/ticketer when a policy + // change rebuilds the configuration. Change only the signing policy. + let mut changed_config = (*initial_config).clone(); + changed_config.cert_resolver = + server_config(version, false, None, SignatureScheme::RSA_PSS_SHA384) + .cert_resolver + .clone(); + let changed_config = Arc::new(changed_config); + let (mut client, accepted) = client_hello(client_config); + let mut server = accepted.into_connection(changed_config.clone()).unwrap(); + finish_handshake(&mut client, &mut server); + assert_eq!( + client.handshake_kind(), + Some(rustls::HandshakeKind::Resumed) + ); + assert_eq!( + server.handshake_kind(), + Some(rustls::HandshakeKind::Resumed) + ); + + // The same changed policy must still reject a fresh client's PSS256 + // offer; accepting a resumed session must not relax a full handshake. + let (mut client, accepted) = client_hello(self::client_config( + version, + SignatureScheme::RSA_PSS_SHA256, + None, + )); + let (error, alert) = accepted.into_connection(changed_config).unwrap_err(); + let bytes = initial_handshake_alert(&error, alert).unwrap(); + assert_eq!(bytes, sni_alert(40)); + client.read_tls(&mut &bytes[..]).unwrap(); + assert!(matches!( + client.process_new_packets(), + Err(rustls::Error::AlertReceived( + rustls::AlertDescription::HandshakeFailure, + )) + )); + } + + #[test] + fn tls12_resumption_survives_signature_policy_change() { + assert_resumption_survives_signature_policy_change(&rustls::version::TLS12); + } + + #[test] + fn tls13_resumption_survives_signature_policy_change() { + assert_resumption_survives_signature_policy_change(&rustls::version::TLS13); + } +} diff --git a/crates/host_env/src/ssl/mod.rs b/crates/host_env/src/ssl/mod.rs index a2d8f32e5..7befb9ec2 100644 --- a/crates/host_env/src/ssl/mod.rs +++ b/crates/host_env/src/ssl/mod.rs @@ -33,6 +33,8 @@ pub mod providers; #[cfg(feature = "rustls")] pub mod session; #[cfg(feature = "rustls")] +pub mod sigalg; +#[cfg(feature = "rustls")] pub mod verify; #[cfg(feature = "rustls")] pub mod x509; diff --git a/crates/host_env/src/ssl/sigalg.rs b/crates/host_env/src/ssl/sigalg.rs new file mode 100644 index 000000000..ee23d8d0a --- /dev/null +++ b/crates/host_env/src/ssl/sigalg.rs @@ -0,0 +1,190 @@ +// cspell: ignore sigalgs rsae mldsa + +//! Per-context TLS server signature algorithm selection. + +use alloc::sync::Arc; +use rustls::{ + DigitallySignedStruct, DistinguishedName, Error, SignatureAlgorithm, SignatureScheme, + client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}, + pki_types::{CertificateDer, ServerName, SubjectPublicKeyInfoDer, UnixTime}, + server::{ClientHello, ResolvesServerCert}, + sign::{CertifiedKey, Signer, SigningKey}, +}; + +use super::providers::CryptoExt; + +pub fn scheme_name(scheme: SignatureScheme) -> Option<&'static str> { + use SignatureScheme::{ + ECDSA_NISTP256_SHA256, ECDSA_NISTP384_SHA384, ECDSA_NISTP521_SHA512, ECDSA_SHA1_Legacy, + ED448, ED25519, ML_DSA_44, ML_DSA_65, ML_DSA_87, RSA_PKCS1_SHA1, RSA_PKCS1_SHA256, + RSA_PKCS1_SHA384, RSA_PKCS1_SHA512, RSA_PSS_SHA256, RSA_PSS_SHA384, RSA_PSS_SHA512, + }; + Some(match scheme { + RSA_PKCS1_SHA1 => "rsa_pkcs1_sha1", + ECDSA_SHA1_Legacy => "ecdsa_sha1", + RSA_PKCS1_SHA256 => "rsa_pkcs1_sha256", + RSA_PKCS1_SHA384 => "rsa_pkcs1_sha384", + RSA_PKCS1_SHA512 => "rsa_pkcs1_sha512", + ECDSA_NISTP256_SHA256 => "ecdsa_secp256r1_sha256", + ECDSA_NISTP384_SHA384 => "ecdsa_secp384r1_sha384", + ECDSA_NISTP521_SHA512 => "ecdsa_secp521r1_sha512", + RSA_PSS_SHA256 => "rsa_pss_rsae_sha256", + RSA_PSS_SHA384 => "rsa_pss_rsae_sha384", + RSA_PSS_SHA512 => "rsa_pss_rsae_sha512", + ED25519 => "ed25519", + ED448 => "ed448", + ML_DSA_44 => "mldsa44", + ML_DSA_65 => "mldsa65", + ML_DSA_87 => "mldsa87", + _ => return None, + }) +} + +pub fn parse_list(list: &str) -> Result, &'static str> { + let supported = CryptoExt::get_provider() + .signature_verification_algorithms + .supported_schemes(); + let mut selected = Vec::new(); + for name in list.split(':') { + let (name, optional) = name.strip_prefix('?').map_or((name, false), |n| (n, true)); + let name = match name { + "RSA+SHA256" => "rsa_pkcs1_sha256", + "RSA+SHA384" => "rsa_pkcs1_sha384", + "RSA+SHA512" => "rsa_pkcs1_sha512", + "ECDSA+SHA256" => "ecdsa_secp256r1_sha256", + "ECDSA+SHA384" => "ecdsa_secp384r1_sha384", + "ECDSA+SHA512" => "ecdsa_secp521r1_sha512", + other => other, + }; + match supported.iter().find(|s| scheme_name(**s) == Some(name)) { + Some(scheme) if !selected.contains(scheme) => selected.push(*scheme), + Some(_) => {} + None if optional => {} + None => return Err("unrecognized signature algorithm"), + } + } + if selected.is_empty() { + return Err("unrecognized signature algorithm"); + } + Ok(selected) +} + +/// Restrict advertisement and handshake signatures, retaining the complete +/// certificate verifier (including trust, hostname and revocation checks). +#[derive(Debug)] +pub struct ServerVerifier { + pub inner: Arc, + pub schemes: Vec, +} + +impl ServerVerifier { + fn check_scheme(&self, dss: &DigitallySignedStruct) -> Result<(), Error> { + if self.schemes.contains(&dss.scheme) { + Ok(()) + } else { + Err(Error::General( + "server signature algorithm is not enabled".into(), + )) + } + } +} + +impl ServerCertVerifier for ServerVerifier { + fn verify_server_cert( + &self, + end_entity: &CertificateDer<'_>, + intermediates: &[CertificateDer<'_>], + server_name: &ServerName<'_>, + ocsp_response: &[u8], + now: UnixTime, + ) -> Result { + self.inner + .verify_server_cert(end_entity, intermediates, server_name, ocsp_response, now) + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + self.check_scheme(dss)?; + self.inner.verify_tls12_signature(message, cert, dss) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + self.check_scheme(dss)?; + self.inner.verify_tls13_signature(message, cert, dss) + } + + fn supported_verify_schemes(&self) -> Vec { + let supported = self.inner.supported_verify_schemes(); + self.schemes + .iter() + .filter(|s| supported.contains(s)) + .copied() + .collect() + } + + fn requires_raw_public_keys(&self) -> bool { + self.inner.requires_raw_public_keys() + } + + fn root_hint_subjects(&self) -> Option<&[DistinguishedName]> { + self.inner.root_hint_subjects() + } +} + +#[derive(Debug)] +struct RestrictedSigningKey { + inner: Arc, + schemes: Vec, +} + +impl SigningKey for RestrictedSigningKey { + fn choose_scheme(&self, offered: &[SignatureScheme]) -> Option> { + self.schemes + .iter() + .filter(|s| offered.contains(s)) + .find_map(|s| self.inner.choose_scheme(&[*s])) + } + + fn public_key(&self) -> Option> { + self.inner.public_key() + } + + fn algorithm(&self) -> SignatureAlgorithm { + self.inner.algorithm() + } +} + +#[derive(Debug)] +pub struct ServerResolver { + pub inner: Arc, + pub schemes: Vec, +} + +pub fn restrict_key(key: &CertifiedKey, schemes: &[SignatureScheme]) -> Arc { + let mut key = key.clone(); + key.key = Arc::new(RestrictedSigningKey { + inner: key.key, + schemes: schemes.to_vec(), + }); + Arc::new(key) +} + +impl ResolvesServerCert for ServerResolver { + fn resolve(&self, hello: ClientHello<'_>) -> Option> { + let key = self.inner.resolve(hello)?; + Some(restrict_key(&key, &self.schemes)) + } + + fn only_raw_public_keys(&self) -> bool { + self.inner.only_raw_public_keys() + } +} diff --git a/crates/host_env/src/ssl/verify.rs b/crates/host_env/src/ssl/verify.rs index a88b6cbf4..ba2132efe 100644 --- a/crates/host_env/src/ssl/verify.rs +++ b/crates/host_env/src/ssl/verify.rs @@ -747,6 +747,25 @@ impl ClientCertVerifier for DeferredClientCertVerifier { // Public Utility Functions +/// Preserve which filesystem operation failed, independently of PEM errors. +#[derive(Debug)] +pub struct CertFileError { + pub error: std::io::Error, + pub key_file: bool, +} + +impl core::fmt::Display for CertFileError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + core::fmt::Display::fmt(&self.error, f) + } +} + +impl core::error::Error for CertFileError { + fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { + Some(&self.error) + } +} + /// Load certificate chain and private key from files /// /// This function loads a certificate chain from `cert_path` and a private key @@ -771,7 +790,10 @@ pub fn load_cert_chain_from_file( password: Option<&str>, ) -> Result<(Vec>, PrivateKeyDer<'static>), Box> { // Load certificate file - preserve io::Error for errno - let cert_contents = crate::fs::read(cert_path)?; + let cert_contents = crate::fs::read(cert_path).map_err(|error| CertFileError { + error, + key_file: false, + })?; // Parse certificates (PEM format) let mut cert_cursor = std::io::Cursor::new(&cert_contents); @@ -784,7 +806,10 @@ pub fn load_cert_chain_from_file( } // Load private key file - preserve io::Error for errno - let key_contents = crate::fs::read(key_path)?; + let key_contents = crate::fs::read(key_path).map_err(|error| CertFileError { + error, + key_file: true, + })?; // Parse private key (supports PKCS8, RSA, EC formats) let private_key = if let Some(pwd) = password { diff --git a/crates/stdlib/build.rs b/crates/stdlib/build.rs index d9d1ca4a6..7f6248783 100644 --- a/crates/stdlib/build.rs +++ b/crates/stdlib/build.rs @@ -20,6 +20,8 @@ fn main() { (0x3_01_00_00_0, "ossl310"), (0x3_02_00_00_0, "ossl320"), (0x3_03_00_00_0, "ossl330"), + (0x3_04_00_00_0, "ossl340"), + (0x3_05_00_00_0, "ossl350"), ]; for (_, cfg) in ossl_vers { diff --git a/crates/stdlib/src/openssl.rs b/crates/stdlib/src/openssl.rs index 19d2a1669..e9393bd44 100644 --- a/crates/stdlib/src/openssl.rs +++ b/crates/stdlib/src/openssl.rs @@ -58,7 +58,7 @@ mod _ssl { AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, builtins::{ PyBaseException, PyBaseExceptionRef, PyBytesRef, PyListRef, PyModule, PyStrRef, - PyType, PyWeak, + PyType, PyUtf8StrRef, PyWeak, }, class_or_notimplemented, convert::ToPyException, @@ -67,6 +67,7 @@ mod _ssl { ArgBytesLike, ArgMemoryBuffer, ArgStrOrBytesLike, Either, FsPath, OptionalArg, PyComparisonValue, }, + object::{Traverse, TraverseFn}, stdlib::_warnings, types::{Comparable, Constructor, PyComparisonOp}, utils::ToCString, @@ -74,7 +75,6 @@ mod _ssl { }; use crossbeam_utils::atomic::AtomicCell; use foreign_types_shared::{ForeignType, ForeignTypeRef}; - use memchr::memchr; use openssl::{ asn1::{Asn1Object, Asn1ObjectRef}, error::ErrorStack, @@ -231,7 +231,9 @@ mod _ssl { #[pyattr] const HAS_TLSv1_3: bool = cfg!(ossl111); #[pyattr] - const HAS_PSK: bool = true; + const HAS_PSK: bool = false; // External PSK callbacks are not implemented. + #[pyattr] + const HAS_PSK_TLS13: bool = false; #[pyattr] const HAS_PHA: bool = cfg!(ossl111); @@ -249,7 +251,7 @@ mod _ssl { // SSL_VERIFY constants for post-handshake authentication #[cfg(ossl111)] - const SSL_VERIFY_POST_HANDSHAKE: core::ffi::c_int = 0x20; + use sys::SSL_VERIFY_POST_HANDSHAKE; // the openssl version from the API headers @@ -468,13 +470,7 @@ mod _ssl { Ok(buf) } - // Callback data stored in SSL ex_data for SNI/msg callbacks - struct SniCallbackData { - ssl_context: PyRef, - // Use weak reference to avoid reference cycle: - // PySslSocket -> SslStream -> SSL -> ex_data -> SniCallbackData -> PySslSocket - ssl_socket_weak: PyRef, - } + type ActiveSslConnection = (*const PyDetachingRwLock, *mut sys::SSL); // Thread-local storage for VirtualMachine pointer during handshake // SNI callback is only called during handshake which is synchronous @@ -483,148 +479,104 @@ mod _ssl { // SSL pointer during handshake - needed because connection lock is held during handshake // and callbacks may need to access SSL without acquiring the lock static HANDSHAKE_SSL_PTR: core::cell::Cell> = const { core::cell::Cell::new(None) }; + static HANDSHAKE_CONNECTIONS: core::cell::RefCell> = const { core::cell::RefCell::new(Vec::new()) }; } // RAII guard to set/clear thread-local handshake context struct HandshakeVmGuard { - _ssl_ptr: *mut sys::SSL, + previous_vm: Option<*const VirtualMachine>, + previous_ssl: Option<*mut sys::SSL>, } impl HandshakeVmGuard { - fn new(vm: &VirtualMachine, ssl_ptr: *mut sys::SSL) -> Self { - HANDSHAKE_VM.with(|cell| cell.set(Some(vm as *const _))); - HANDSHAKE_SSL_PTR.with(|cell| cell.set(Some(ssl_ptr))); - HandshakeVmGuard { _ssl_ptr: ssl_ptr } + fn new( + vm: &VirtualMachine, + connection: &PyDetachingRwLock, + ssl_ptr: *mut sys::SSL, + ) -> Self { + HANDSHAKE_CONNECTIONS.with(|connections| { + connections + .borrow_mut() + .push((connection as *const _, ssl_ptr)); + }); + Self { + previous_vm: HANDSHAKE_VM.with(|cell| cell.replace(Some(vm as *const _))), + previous_ssl: HANDSHAKE_SSL_PTR.with(|cell| cell.replace(Some(ssl_ptr))), + } } } impl Drop for HandshakeVmGuard { fn drop(&mut self) { - HANDSHAKE_VM.with(|cell| cell.set(None)); - HANDSHAKE_SSL_PTR.with(|cell| cell.set(None)); + HANDSHAKE_VM.with(|cell| cell.set(self.previous_vm)); + HANDSHAKE_SSL_PTR.with(|cell| cell.set(self.previous_ssl)); + HANDSHAKE_CONNECTIONS.with(|connections| { + connections.borrow_mut().pop(); + }); } } - // Get SSL pointer - either from thread-local (during handshake) or from connection - fn get_ssl_ptr_for_context_change( + fn active_ssl_pointer(connection: &PyDetachingRwLock) -> Option<*mut sys::SSL> { + HANDSHAKE_CONNECTIONS.with(|connections| { + connections + .borrow() + .iter() + .rev() + .find_map(|&(owner, pointer)| core::ptr::eq(owner, connection).then_some(pointer)) + }) + } + + fn with_ssl_pointer( connection: &PyDetachingRwLock, - ) -> *mut sys::SSL { - // First check if we're in a handshake callback (lock already held) - if let Some(ptr) = HANDSHAKE_SSL_PTR.with(|cell| cell.get()) { - return ptr; + operation: impl FnOnce(*mut sys::SSL) -> T, + ) -> T { + if let Some(pointer) = active_ssl_pointer(connection) { + // This thread already holds this connection's write guard. + return operation(pointer); } - // Otherwise, acquire the lock normally - connection.read().ssl().as_ptr() + let guard = connection.read(); + operation(guard.ssl().as_ptr()) } - // Get or create an ex_data index for SNI callback data - fn get_sni_ex_data_index() -> core::ffi::c_int { - use rustpython_common::lock::LazyLock; - static SNI_EX_DATA_IDX: LazyLock = LazyLock::new(|| unsafe { - sys::SSL_get_ex_new_index( - 0, - core::ptr::null_mut(), - None, - None, - Some(sni_callback_data_free), - ) + fn with_ssl_pointer_mut( + connection: &PyDetachingRwLock, + operation: impl FnOnce(*mut sys::SSL) -> T, + ) -> T { + if let Some(pointer) = active_ssl_pointer(connection) { + return operation(pointer); + } + let guard = connection.write(); + operation(guard.ssl().as_ptr()) + } + + // The owning _SSLSocket outlives its SSL connection. Borrow its stable + // address, as CPython's SSL app_data does, without creating a hidden cycle. + fn get_socket_ex_data_index() -> core::ffi::c_int { + static SOCKET_EX_DATA_IDX: LazyLock = LazyLock::new(|| unsafe { + sys::SSL_get_ex_new_index(0, core::ptr::null_mut(), None, None, None) }); - *SNI_EX_DATA_IDX - } - - // Free function for callback data - // NOTE: We don't free the data here because it's managed manually in do_handshake - // to avoid use-after-free when the SSL object is dropped after timeout - unsafe extern "C" fn sni_callback_data_free( - _parent: *mut core::ffi::c_void, - _ptr: *mut core::ffi::c_void, - _ad: *mut sys::CRYPTO_EX_DATA, - _idx: core::ffi::c_int, - _argl: core::ffi::c_long, - _argp: *mut core::ffi::c_void, - ) { - // Intentionally empty - data is freed in cleanup_sni_ex_data() - } - - // Clean up SNI callback data from SSL ex_data - // Called after handshake to free the data and release references - unsafe fn cleanup_sni_ex_data(ssl_ptr: *mut sys::SSL) { - unsafe { - let idx = get_sni_ex_data_index(); - let data_ptr = sys::SSL_get_ex_data(ssl_ptr, idx); - if !data_ptr.is_null() { - // Free the Box - this releases references to context and socket - let _ = Box::from_raw(data_ptr as *mut SniCallbackData); - // Clear the ex_data to prevent double-free - sys::SSL_set_ex_data(ssl_ptr, idx, core::ptr::null_mut()); - } - } - } - - // Get or create an ex_data index for msg_callback data - fn get_msg_callback_ex_data_index() -> core::ffi::c_int { - use rustpython_common::lock::LazyLock; - static MSG_CB_EX_DATA_IDX: LazyLock = LazyLock::new(|| unsafe { - sys::SSL_get_ex_new_index( - 0, - core::ptr::null_mut(), - None, - None, - Some(msg_callback_data_free), - ) - }); - *MSG_CB_EX_DATA_IDX - } - - // Free function for msg_callback data - called by OpenSSL when SSL is freed - unsafe extern "C" fn msg_callback_data_free( - _parent: *mut core::ffi::c_void, - ptr: *mut core::ffi::c_void, - _ad: *mut sys::CRYPTO_EX_DATA, - _idx: core::ffi::c_int, - _argl: core::ffi::c_long, - _argp: *mut core::ffi::c_void, - ) { - if !ptr.is_null() { - unsafe { - // Reconstruct PyObjectRef and drop to decrement reference count - let raw = core::ptr::NonNull::new_unchecked(ptr as *mut PyObject); - let _ = PyObjectRef::from_raw(raw); - } - } + *SOCKET_EX_DATA_IDX } // SNI callback function called by OpenSSL unsafe extern "C" fn _servername_callback( ssl_ptr: *mut sys::SSL, al: *mut core::ffi::c_int, - arg: *mut core::ffi::c_void, + _arg: *mut core::ffi::c_void, ) -> core::ffi::c_int { const SSL_TLSEXT_ERR_OK: core::ffi::c_int = 0; const SSL_TLSEXT_ERR_ALERT_FATAL: core::ffi::c_int = 2; const SSL_AD_INTERNAL_ERROR: core::ffi::c_int = 80; + const SSL_AD_HANDSHAKE_FAILURE: core::ffi::c_int = 40; const TLSEXT_NAMETYPE_host_name: core::ffi::c_int = 0; - if arg.is_null() { - return SSL_TLSEXT_ERR_OK; - } - unsafe { - let ctx = &*(arg as *const PySslContext); - - // Nothing to call: leave without reaching the interpreter at all. - if ctx.sni_callback.lock().is_none() { - return SSL_TLSEXT_ERR_OK; - } - - // Get callback data from SSL ex_data - let idx = get_sni_ex_data_index(); - let data_ptr = sys::SSL_get_ex_data(ssl_ptr, idx); - if data_ptr.is_null() { + let socket_ptr = sys::SSL_get_ex_data(ssl_ptr, get_socket_ex_data_index()); + if socket_ptr.is_null() { + *al = SSL_AD_INTERNAL_ERROR; return SSL_TLSEXT_ERR_ALERT_FATAL; } - - let callback_data = &*(data_ptr as *const SniCallbackData); + let ssl_socket = &*(socket_ptr as *const Py); // Get VM from thread-local storage (set by HandshakeVmGuard in do_handshake) let Some(vm_ptr) = HANDSHAKE_VM.with(|cell| cell.get()) else { @@ -638,7 +590,9 @@ mod _ssl { // everything below rejoins it first — taking a reference to the // callback already counts — and gives the thread back after. vm.attach_for_callback(|| { - // Get the callback + // The callback may replace the socket's context and release its + // last outside reference. Keep both context and callback alive. + let ctx = ssl_socket.ctx.read().clone(); let callback_opt = ctx.sni_callback.lock().clone(); let Some(callback) = callback_opt else { return SSL_TLSEXT_ERR_OK; @@ -656,21 +610,15 @@ mod _ssl { } }; - // Get SSL socket from callback data via weak reference - let ssl_socket_obj = callback_data - .ssl_socket_weak - .upgrade() - .unwrap_or_else(|| vm.ctx.none()); + let ssl_socket_obj = match PySslSocket::callback_owner(ssl_socket) { + Some(owner) => owner, + None => { + *al = SSL_AD_INTERNAL_ERROR; + return SSL_TLSEXT_ERR_ALERT_FATAL; + } + }; - // Call the Python callback - match callback.call( - ( - ssl_socket_obj, - server_name_arg, - callback_data.ssl_context.to_owned(), - ), - vm, - ) { + match callback.call((ssl_socket_obj, server_name_arg, ctx), vm) { Ok(result) => { // Check return value type (must be None or integer) if vm.is_none(&result) { @@ -700,7 +648,7 @@ mod _ssl { Err(exc) => { // Log the exception but don't propagate it vm.run_unraisable(exc, None, vm.ctx.none()); - *al = SSL_AD_INTERNAL_ERROR; + *al = SSL_AD_HANDSHAKE_FAILURE; SSL_TLSEXT_ERR_ALERT_FATAL } } @@ -735,20 +683,15 @@ mod _ssl { unsafe { // Get SSL socket from ex_data using the dedicated index - let idx = get_msg_callback_ex_data_index(); + let idx = get_socket_ex_data_index(); let ssl_socket_ptr = sys::SSL_get_ex_data(ssl_ptr, idx); if ssl_socket_ptr.is_null() { return; } - // ssl_socket_ptr is a pointer to Box>, set in _wrap_socket/_wrap_bio + // Borrowed stable address, installed by _wrap_socket/_wrap_bio. let ssl_socket: &Py = &*(ssl_socket_ptr as *const Py); - // Nothing to call: leave without reaching the interpreter at all. - if ssl_socket.ctx.read().msg_callback.lock().is_none() { - return; - } - // Get VM from thread-local storage (set by HandshakeVmGuard in do_handshake) let Some(vm_ptr) = HANDSHAKE_VM.with(|cell| cell.get()) else { // VM not available - this shouldn't happen during handshake @@ -766,13 +709,9 @@ mod _ssl { return; }; - // Get SSL socket owner object - let ssl_socket_obj = ssl_socket - .owner - .read() - .as_ref() - .and_then(|weak| weak.upgrade()) - .unwrap_or_else(|| vm.ctx.none()); + let Some(ssl_socket_obj) = PySslSocket::callback_owner(ssl_socket) else { + return; + }; // Create the message bytes let buf_slice = core::slice::from_raw_parts(buf as *const u8, len); @@ -819,8 +758,13 @@ mod _ssl { ) { Ok(_) => {} Err(exc) => { - // Log the exception but don't propagate it - vm.run_unraisable(exc, None, vm.ctx.none()); + // SSL message callbacks cannot signal failure to the C + // library. Propagate it when the enclosing operation + // rejoins the interpreter, chaining earlier callbacks. + let previous = ssl_socket.callback_error.lock().replace(exc.clone()); + if let Some(previous) = previous { + exc.set_context(Some(previous)); + } } } }) @@ -841,18 +785,19 @@ mod _ssl { } #[pyattr] - #[pyclass(module = "_ssl", name = "_SSLContext")] + #[pyclass(module = "_ssl", name = "_SSLContext", traverse)] #[derive(PyPayload)] struct PySslContext { + #[pytraverse(skip)] ctx: PyRwLock, + #[pytraverse(skip)] check_hostname: AtomicCell, + #[pytraverse(skip)] protocol: SslVersion, + #[pytraverse(skip)] post_handshake_auth: PyMutex, sni_callback: PyMutex>, msg_callback: PyMutex>, - psk_client_callback: PyMutex>, - psk_server_callback: PyMutex>, - psk_identity_hint: PyMutex>, } impl fmt::Debug for PySslContext { @@ -861,6 +806,19 @@ mod _ssl { } } + impl Drop for PySslContext { + fn drop(&mut self) { + // SSL retains its original session context across context switches. + // It must not call back through a released Python context on HRR. + unsafe { + sys::SSL_CTX_set_tlsext_servername_callback__fixed_rust( + self.ctx.get_mut().as_ptr(), + None, + ); + } + } + } + fn builder_as_ctx(x: &SslContextBuilder) -> &ssl::SslContextRef { unsafe { ssl::SslContextRef::from_ptr(x.as_ptr()) } } @@ -972,9 +930,6 @@ mod _ssl { post_handshake_auth: PyMutex::new(false), sni_callback: PyMutex::new(None), msg_callback: PyMutex::new(None), - psk_client_callback: PyMutex::new(None), - psk_server_callback: PyMutex::new(None), - psk_identity_hint: PyMutex::new(None), }) } } @@ -1039,6 +994,146 @@ mod _ssl { .map_err(|_| new_ssl_error(vm, "No cipher can be selected.")) } + #[pymethod] + fn set_ciphersuites( + zelf: &Py, + ciphersuites: PyUtf8StrRef, + vm: &VirtualMachine, + ) -> PyResult<()> { + let list = ciphersuites.as_str().to_cstring(vm)?; + #[cfg(ossl111)] + { + let result = unsafe { + sys::SSL_CTX_set_ciphersuites(zelf.builder().as_ptr(), list.as_ptr()) + }; + if result != 1 { + let _ = ErrorStack::get(); + return Err(new_ssl_error(vm, "No cipher suite can be selected.")); + } + Ok(()) + } + #[cfg(not(ossl111))] + { + let _ = (zelf, list); + Err(vm.new_not_implemented_error("TLS 1.3 requires OpenSSL 1.1.1 or later.")) + } + } + + fn set_algorithm_list( + &self, + list: PyUtf8StrRef, + command: core::ffi::c_int, + message: &str, + vm: &VirtualMachine, + ) -> PyResult<()> { + let list = list.as_str().to_cstring(vm)?; + let result = unsafe { + sys::SSL_CTX_ctrl( + self.builder().as_ptr(), + command, + 0, + list.as_ptr().cast_mut().cast(), + ) + }; + if result != 1 { + let _ = ErrorStack::get(); + return Err(new_ssl_error(vm, message)); + } + Ok(()) + } + + #[pymethod] + fn set_groups( + zelf: &Py, + grouplist: PyUtf8StrRef, + vm: &VirtualMachine, + ) -> PyResult<()> { + // SSL_CTX_set1_groups_list is a macro around SSL_CTX_ctrl. + const SSL_CTRL_SET_GROUPS_LIST: core::ffi::c_int = 92; + zelf.set_algorithm_list( + grouplist, + SSL_CTRL_SET_GROUPS_LIST, + "unrecognized group", + vm, + ) + } + + #[pymethod] + fn set_server_sigalgs( + zelf: &Py, + sigalgslist: PyUtf8StrRef, + vm: &VirtualMachine, + ) -> PyResult<()> { + const SSL_CTRL_SET_SIGALGS_LIST: core::ffi::c_int = 98; + zelf.set_algorithm_list( + sigalgslist, + SSL_CTRL_SET_SIGALGS_LIST, + "unrecognized signature algorithm", + vm, + ) + } + + #[pymethod] + fn set_client_sigalgs( + zelf: &Py, + sigalgslist: PyUtf8StrRef, + vm: &VirtualMachine, + ) -> PyResult<()> { + const SSL_CTRL_SET_CLIENT_SIGALGS_LIST: core::ffi::c_int = 102; + zelf.set_algorithm_list( + sigalgslist, + SSL_CTRL_SET_CLIENT_SIGALGS_LIST, + "unrecognized signature algorithm", + vm, + ) + } + + #[pymethod] + fn get_groups( + zelf: &Py, + args: GetGroupsArgs, + vm: &VirtualMachine, + ) -> PyResult { + #[cfg(ossl350)] + unsafe { + const SSL_CTRL_GET0_IMPLEMENTED_GROUPS: core::ffi::c_int = 139; + let groups = sys::OPENSSL_sk_new_null(); + if groups.is_null() { + return Err(vm.new_memory_error("failed to allocate group list")); + } + let result = sys::SSL_CTX_ctrl( + zelf.ctx().as_ptr(), + SSL_CTRL_GET0_IMPLEMENTED_GROUPS, + core::ffi::c_long::from(args.include_aliases), + groups.cast(), + ); + if result == 0 { + sys::OPENSSL_sk_free(groups); + return Err(convert_openssl_error(vm, ErrorStack::get())); + } + // The stack owns its storage, but OpenSSL owns each name. + let names = (0..sys::OPENSSL_sk_num(groups)) + .map(|index| { + CStr::from_ptr(sys::OPENSSL_sk_value(groups, index).cast()) + .to_string_lossy() + .into_owned() + }) + .collect::>(); + sys::OPENSSL_sk_free(groups); + Ok(vm.ctx.new_list( + names + .into_iter() + .map(|name| vm.ctx.new_str(name).into()) + .collect(), + )) + } + #[cfg(not(ossl350))] + { + let _ = (zelf, args.include_aliases); + Err(vm.new_not_implemented_error("Getting groups requires OpenSSL 3.5 or later.")) + } + } + #[pymethod] fn get_ciphers(zelf: &Py, vm: &VirtualMachine) -> PyResult { let ctx = zelf.ctx(); @@ -1242,6 +1337,12 @@ mod _ssl { // PY_PROTO_MINIMUM_SUPPORTED = -2, PY_PROTO_MAXIMUM_SUPPORTED = -1 #[pygetset] fn minimum_version(zelf: &Py) -> i32 { + if !matches!( + zelf.protocol, + SslVersion::Tls | SslVersion::TlsClient | SslVersion::TlsServer + ) { + return -2; + } let ctx = zelf.ctx(); let version = unsafe { sys::SSL_CTX_get_min_proto_version(ctx.as_ptr()) }; if version == 0 { @@ -1252,13 +1353,23 @@ mod _ssl { } #[pygetset(setter)] fn set_minimum_version(zelf: &Py, value: i32, vm: &VirtualMachine) -> PyResult<()> { + if !matches!( + zelf.protocol, + SslVersion::Tls | SslVersion::TlsClient | SslVersion::TlsServer + ) { + return Err(vm.new_value_error( + "The context's protocol doesn't support modification of highest and lowest version.", + )); + } + ProtoVersion::try_from(value) + .map_err(|_| vm.new_value_error("invalid protocol version"))?; Self::warn_deprecated_tls_version(value, vm)?; // Handle special values let proto_version = match value { -2 => { - // PY_PROTO_MINIMUM_SUPPORTED -> use minimum available (TLS 1.2) - sys::TLS1_2_VERSION + // Zero removes the explicit minimum protocol limit. + 0 } -1 => { // PY_PROTO_MAXIMUM_SUPPORTED -> use maximum available @@ -1278,6 +1389,12 @@ mod _ssl { #[pygetset] fn maximum_version(zelf: &Py) -> i32 { + if !matches!( + zelf.protocol, + SslVersion::Tls | SslVersion::TlsClient | SslVersion::TlsServer + ) { + return -1; + } let ctx = zelf.ctx(); let version = unsafe { sys::SSL_CTX_get_max_proto_version(ctx.as_ptr()) }; if version == 0 { @@ -1288,6 +1405,16 @@ mod _ssl { } #[pygetset(setter)] fn set_maximum_version(zelf: &Py, value: i32, vm: &VirtualMachine) -> PyResult<()> { + if !matches!( + zelf.protocol, + SslVersion::Tls | SslVersion::TlsClient | SslVersion::TlsServer + ) { + return Err(vm.new_value_error( + "The context's protocol doesn't support modification of highest and lowest version.", + )); + } + ProtoVersion::try_from(value) + .map_err(|_| vm.new_value_error("invalid protocol version"))?; Self::warn_deprecated_tls_version(value, vm)?; // Handle special values @@ -1296,10 +1423,8 @@ mod _ssl { // PY_PROTO_MAXIMUM_SUPPORTED -> use 0 for OpenSSL (means no limit) 0 } - -2 => { - // PY_PROTO_MINIMUM_SUPPORTED -> use minimum available (TLS 1.2) - sys::TLS1_2_VERSION - } + // This backend advertises TLSv1 as its lowest supported version. + -2 => sys::TLS1_VERSION, _ => value, }; @@ -1407,74 +1532,25 @@ mod _ssl { #[pymethod] fn set_psk_client_callback( - zelf: &Py, - callback: PyObjectRef, + _zelf: &Py, + _callback: PyObjectRef, vm: &VirtualMachine, ) -> PyResult<()> { - // Cannot add PSK client callback to a server context - if zelf.protocol == SslVersion::TlsServer { - return Err(vm - .new_os_subtype_error( - PySSLError::class(&vm.ctx).to_owned(), - None, - "Cannot add PSK client callback to a PROTOCOL_TLS_SERVER context" - .to_owned(), - ) - .upcast()); - } - - if vm.is_none(&callback) { - *zelf.psk_client_callback.lock() = None; - unsafe { - sys::SSL_CTX_set_psk_client_callback(zelf.builder().as_ptr(), None); - } - } else { - if !callback.is_callable() { - return Err(vm.new_type_error("callback must be callable")); - } - *zelf.psk_client_callback.lock() = Some(callback); - // Note: The actual callback will be invoked via SSL app_data mechanism - // when do_handshake is called - } - Ok(()) + Err(vm.new_not_implemented_error( + "TLS-PSK callbacks are not implemented by this backend.", + )) } #[pymethod] fn set_psk_server_callback( - zelf: &Py, - callback: PyObjectRef, - identity_hint: OptionalArg, + _zelf: &Py, + _callback: PyObjectRef, + _identity_hint: OptionalArg, vm: &VirtualMachine, ) -> PyResult<()> { - // Cannot add PSK server callback to a client context - if zelf.protocol == SslVersion::TlsClient { - return Err(vm - .new_os_subtype_error( - PySSLError::class(&vm.ctx).to_owned(), - None, - "Cannot add PSK server callback to a PROTOCOL_TLS_CLIENT context" - .to_owned(), - ) - .upcast()); - } - - if vm.is_none(&callback) { - *zelf.psk_server_callback.lock() = None; - *zelf.psk_identity_hint.lock() = None; - unsafe { - sys::SSL_CTX_set_psk_server_callback(zelf.builder().as_ptr(), None); - } - } else { - if !callback.is_callable() { - return Err(vm.new_type_error("callback must be callable")); - } - *zelf.psk_server_callback.lock() = Some(callback); - if let OptionalArg::Present(hint) = identity_hint { - *zelf.psk_identity_hint.lock() = Some(hint.to_string()); - } - // Note: The actual callback will be invoked via SSL app_data mechanism - } - Ok(()) + Err(vm.new_not_implemented_error( + "TLS-PSK callbacks are not implemented by this backend.", + )) } #[pymethod] @@ -1758,10 +1834,6 @@ mod _ssl { zelf.ctx().as_ptr(), Some(_servername_callback), ); - sys::SSL_CTX_set_tlsext_servername_arg( - zelf.ctx().as_ptr(), - zelf as *const _ as *mut _, - ); } } else { // Clear callback @@ -1857,29 +1929,31 @@ mod _ssl { } = args; let mut ctx = zelf.builder(); - let key_path = keyfile.map(|path| path.to_path_buf(vm)).transpose()?; + let key_path = keyfile + .as_ref() + .map(|path| path.to_path_buf(vm)) + .transpose()?; let cert_path = certfile.to_path_buf(vm)?; - // Check file existence before calling OpenSSL to get proper errno - if !rustpython_host_env::fs::exists(&cert_path) { - return Err(vm - .new_os_subtype_error( - vm.ctx.exceptions.file_not_found_error.to_owned(), - Some(rustpython_host_env::errno::errors::ENOENT), - format!("No such file or directory: '{}'", cert_path.display()), - ) - .upcast()); - } - if let Some(ref kp) = key_path - && !rustpython_host_env::fs::exists(kp) - { - return Err(vm - .new_os_subtype_error( - vm.ctx.exceptions.file_not_found_error.to_owned(), - Some(rustpython_host_env::errno::errors::ENOENT), - format!("No such file or directory: '{}'", kp.display()), - ) - .upcast()); + let file_error = |error: std::io::Error, path: &FsPath| { + let exception = error.to_pyexception(vm); + let filename: PyObjectRef = match path { + FsPath::Str(path) => path.clone().into(), + FsPath::Bytes(path) => path.clone().into(), + }; + exception + .as_object() + .set_attr("filename", filename, vm) + .expect("OSError.filename is writable"); + exception + }; + // Keep OS errors and their original str/bytes filenames rather + // than replacing them with OpenSSL's generic file error. + rustpython_host_env::fs::metadata(&cert_path) + .map_err(|error| file_error(error, &certfile))?; + if let Some(ref path) = key_path { + rustpython_host_env::fs::metadata(path) + .map_err(|error| file_error(error, keyfile.as_ref().unwrap()))?; } // Load certificate chain @@ -1894,7 +1968,7 @@ mod _ssl { // Read key file data let key_data = rustpython_host_env::fs::read(key_file_path) - .map_err(|e| crate::vm::convert::ToPyException::to_pyexception(&e, vm))?; + .map_err(|error| file_error(error, keyfile.as_ref().unwrap_or(&certfile)))?; let pkey = if let Some(ref pw_obj) = password { if pw_obj.is_callable() { @@ -2125,41 +2199,33 @@ mod _ssl { let py_ssl_socket = PySslSocket { ctx: PyRwLock::new(zelf.clone()), - connection: PyDetachingRwLock::new(SslConnection::Socket(stream)), + connection: SslConnectionLock(PyDetachingRwLock::new(SslConnection::Socket( + stream, + ))), socket_type, server_hostname, - owner: PyRwLock::new(args.owner.map(|o| o.downgrade(None, vm)).transpose()?), + got_eof_error: AtomicCell::new(false), + shutdown_seen_zero: AtomicCell::new(false), + callback_error: PyMutex::new(None), + owner: PyRwLock::new(Some( + args.owner + .unwrap_or_else(|| args.sock.clone().into()) + .downgrade(None, vm)?, + )), }; // Convert to PyRef (heap allocation) to avoid use-after-free let py_ref = py_ssl_socket.into_ref_with_type(vm, PySslSocket::class(&vm.ctx).to_owned())?; - // Check if SNI callback is configured (minimize lock time) - let has_sni_callback = zelf.sni_callback.lock().is_some(); - - // Set up ex_data for callbacks + // The connection is owned by py_ref, so this borrowed address + // remains valid through every SSL operation, including retries. unsafe { - let ssl_ptr = py_ref.connection.read().ssl().as_ptr(); - - // Clone and store via into_raw() - increments refcount and returns stable pointer - // The refcount will be decremented by msg_callback_data_free when SSL is freed - let cloned: PyObjectRef = py_ref.clone().into(); - let raw_ptr = cloned.into_raw(); - let msg_cb_idx = get_msg_callback_ex_data_index(); - sys::SSL_set_ex_data(ssl_ptr, msg_cb_idx, raw_ptr.as_ptr() as *mut _); - - // Set SNI callback data if needed - if has_sni_callback { - let ssl_socket_weak = py_ref.as_object().downgrade(None, vm)?; - // Store callback data in SSL ex_data - use weak reference to avoid cycle - let callback_data = Box::new(SniCallbackData { - ssl_context: zelf.clone(), - ssl_socket_weak, - }); - let sni_idx = get_sni_ex_data_index(); - sys::SSL_set_ex_data(ssl_ptr, sni_idx, Box::into_raw(callback_data) as *mut _); - } + sys::SSL_set_ex_data( + py_ref.connection.read().ssl().as_ptr(), + get_socket_ex_data_index(), + &*py_ref as *const Py as *mut _, + ); } // Set session if provided @@ -2194,9 +2260,12 @@ mod _ssl { let py_ssl_socket = PySslSocket { ctx: PyRwLock::new(zelf.clone()), - connection: PyDetachingRwLock::new(SslConnection::Bio(stream)), + connection: SslConnectionLock(PyDetachingRwLock::new(SslConnection::Bio(stream))), socket_type, server_hostname, + got_eof_error: AtomicCell::new(false), + shutdown_seen_zero: AtomicCell::new(false), + callback_error: PyMutex::new(None), owner: PyRwLock::new(args.owner.map(|o| o.downgrade(None, vm)).transpose()?), }; @@ -2204,31 +2273,14 @@ mod _ssl { let py_ref = py_ssl_socket.into_ref_with_type(vm, PySslSocket::class(&vm.ctx).to_owned())?; - // Check if SNI callback is configured (minimize lock time) - let has_sni_callback = zelf.sni_callback.lock().is_some(); - - // Set up ex_data for callbacks + // The connection is owned by py_ref, so this borrowed address + // remains valid through every SSL operation, including retries. unsafe { - let ssl_ptr = py_ref.connection.read().ssl().as_ptr(); - - // Clone and store via into_raw() - increments refcount and returns stable pointer - // The refcount will be decremented by msg_callback_data_free when SSL is freed - let cloned: PyObjectRef = py_ref.clone().into(); - let raw_ptr = cloned.into_raw(); - let msg_cb_idx = get_msg_callback_ex_data_index(); - sys::SSL_set_ex_data(ssl_ptr, msg_cb_idx, raw_ptr.as_ptr() as *mut _); - - // Set SNI callback data if needed - if has_sni_callback { - let ssl_socket_weak = py_ref.as_object().downgrade(None, vm)?; - // Store callback data in SSL ex_data - use weak reference to avoid cycle - let callback_data = Box::new(SniCallbackData { - ssl_context: zelf.clone(), - ssl_socket_weak, - }); - let sni_idx = get_sni_ex_data_index(); - sys::SSL_set_ex_data(ssl_ptr, sni_idx, Box::into_raw(callback_data) as *mut _); - } + sys::SSL_set_ex_data( + py_ref.connection.read().ssl().as_ptr(), + get_socket_ex_data_index(), + &*py_ref as *const Py as *mut _, + ); } // Set session if provided @@ -2293,6 +2345,12 @@ mod _ssl { binary_form: OptionalArg, } + #[derive(FromArgs)] + struct GetGroupsArgs { + #[pyarg(named, default)] + include_aliases: bool, + } + // Err is true if the socket is blocking type SocketDeadline = Result; @@ -2426,6 +2484,18 @@ mod _ssl { Bio(ssl::SslStream), } + impl Drop for SslConnection { + fn drop(&mut self) { + // Match CPython: freeing a socket without unwrap() must not invalidate + // a session retained by Python. This marks state without doing I/O. + let state = self.get_shutdown() | ssl::ShutdownState::SENT; + match self { + Self::Socket(stream) => stream.set_shutdown(state), + Self::Bio(stream) => stream.set_shutdown(state), + } + } + } + impl SslConnection { // Get a reference to the SSL object fn ssl(&self) -> &ssl::SslRef { @@ -2450,6 +2520,8 @@ mod _ssl { // Perform SSL handshake fn do_handshake(&mut self) -> Result<(), ssl::Error> { + // SSL_get_error requires an empty queue before this native attempt. + unsafe { sys::ERR_clear_error() }; match self { SslConnection::Socket(stream) => stream.do_handshake(), SslConnection::Bio(stream) => stream.do_handshake(), @@ -2458,6 +2530,7 @@ mod _ssl { // Write data to SSL connection fn ssl_write(&mut self, buf: &[u8]) -> Result { + unsafe { sys::ERR_clear_error() }; match self { SslConnection::Socket(stream) => stream.ssl_write(buf), SslConnection::Bio(stream) => stream.ssl_write(buf), @@ -2466,12 +2539,21 @@ mod _ssl { // Read data from SSL connection fn ssl_read(&mut self, buf: &mut [u8]) -> Result { + unsafe { sys::ERR_clear_error() }; match self { SslConnection::Socket(stream) => stream.ssl_read(buf), SslConnection::Bio(stream) => stream.ssl_read(buf), } } + fn shutdown(&mut self) -> Result { + unsafe { sys::ERR_clear_error() }; + match self { + SslConnection::Socket(stream) => stream.shutdown(), + SslConnection::Bio(stream) => stream.shutdown(), + } + } + // Get SSL shutdown state fn get_shutdown(&mut self) -> ssl::ShutdownState { match self { @@ -2489,17 +2571,46 @@ mod _ssl { } } + struct SslConnectionLock(PyDetachingRwLock); + + impl core::ops::Deref for SslConnectionLock { + type Target = PyDetachingRwLock; + fn deref(&self) -> &Self::Target { + &self.0 + } + } + + // Like Traverse for PyRwLock, conservatively retain a connection if a + // detached SSL operation holds its write guard when GC stops the world. + unsafe impl Traverse for SslConnectionLock { + fn traverse(&self, traverse_fn: &mut TraverseFn<'_>) { + if let Some(connection) = self.try_read() { + match &*connection { + SslConnection::Socket(stream) => stream.get_ref().0.traverse(traverse_fn), + SslConnection::Bio(stream) => { + stream.get_ref().inbio.traverse(traverse_fn); + stream.get_ref().outbio.traverse(traverse_fn); + } + } + } + } + } + #[pyattr] #[pyclass(module = "_ssl", name = "_SSLSocket", traverse)] #[derive(PyPayload)] struct PySslSocket { ctx: PyRwLock>, - #[pytraverse(skip)] - connection: PyDetachingRwLock, + connection: SslConnectionLock, #[pytraverse(skip)] socket_type: SslServerOrClient, server_hostname: Option, owner: PyRwLock>>, + #[pytraverse(skip)] + got_eof_error: AtomicCell, + #[pytraverse(skip)] + shutdown_seen_zero: AtomicCell, + callback_error: PyMutex>, } impl fmt::Debug for PySslSocket { @@ -2510,6 +2621,47 @@ mod _ssl { #[pyclass(flags(IMMUTABLETYPE))] impl PySslSocket { + fn callback_owner(zelf: &Py) -> Option { + if let Some(owner) = zelf.owner.read().as_ref() { + return owner.upgrade(); + } + // A MemoryBIO wrapper may have no Python owner. + Some(zelf.as_object().to_owned()) + } + + fn check_eof(&self, vm: &VirtualMachine) -> PyResult<()> { + if self.got_eof_error.load() { + Err(create_ssl_eof_error(vm).upcast()) + } else { + Ok(()) + } + } + + fn ssl_error(&self, error: ssl::Error, vm: &VirtualMachine) -> PyBaseExceptionRef { + if is_unexpected_eof(&error) { + self.got_eof_error.store(true); + } + convert_ssl_error(vm, error) + } + + fn finish_ssl_operation( + &self, + result: Result, + vm: &VirtualMachine, + ) -> PyResult> { + if let Some(callback_error) = self.callback_error.lock().take() { + return Err(match result { + Ok(_) => callback_error, + Err(error) => { + let error = self.ssl_error(error, vm); + error.set_context(Some(callback_error)); + error + } + }); + } + Ok(result) + } + #[pygetset] fn owner(zelf: &Py) -> Option { zelf.owner.read().as_ref().and_then(|weak| weak.upgrade()) @@ -2535,21 +2687,16 @@ mod _ssl { value: PyRef, vm: &VirtualMachine, ) -> PyResult<()> { - // Get SSL pointer - use thread-local during handshake to avoid deadlock - // (connection lock is already held during handshake) - let ssl_ptr = get_ssl_ptr_for_context_change(&zelf.connection); - - // Set the new SSL_CTX on the SSL object - unsafe { - let result = SSL_set_SSL_CTX(ssl_ptr, value.ctx().as_ptr()); - if result.is_null() { - return Err(vm.new_runtime_error("Failed to set SSL context")); + with_ssl_pointer_mut(&zelf.connection, |ssl_ptr| { + unsafe { + let result = SSL_set_SSL_CTX(ssl_ptr, value.ctx().as_ptr()); + if result.is_null() { + return Err(vm.new_runtime_error("Failed to set SSL context")); + } } - } - - // Update self.ctx to the new context - *zelf.ctx.write() = value; - Ok(()) + *zelf.ctx.write() = value; + Ok(()) + }) } #[pygetset] fn server_hostname(zelf: &Py) -> Option { @@ -2563,33 +2710,35 @@ mod _ssl { vm: &VirtualMachine, ) -> PyResult> { let binary = args.binary_form.unwrap_or(false); - let stream = zelf.connection.read(); - if !stream.ssl().is_init_finished() { - return Err(vm.new_value_error("handshake not done yet")); - } + with_ssl_pointer(&zelf.connection, |ssl_ptr| { + let ssl = unsafe { ssl::SslRef::from_ptr(ssl_ptr) }; + if !ssl.is_init_finished() { + return Err(vm.new_value_error("handshake not done yet")); + } - let peer_cert = stream.ssl().peer_certificate(); - let Some(cert) = peer_cert else { - return Ok(None); - }; + let peer_cert = ssl.peer_certificate(); + let Some(cert) = peer_cert else { + return Ok(None); + }; - if binary { - // Return DER-encoded certificate - cert_to_py(vm, &cert, true).map(Some) - } else { - // Check verify_mode - unsafe { - let ssl_ctx = sys::SSL_get_SSL_CTX(stream.ssl().as_ptr()); - let verify_mode = sys::SSL_CTX_get_verify_mode(ssl_ctx); - if (verify_mode & sys::SSL_VERIFY_PEER as core::ffi::c_int) == 0 { - // Return empty dict when SSL_VERIFY_PEER is not set - Ok(Some(vm.ctx.new_dict().into())) - } else { - // Return decoded certificate - cert_to_py(vm, &cert, false).map(Some) + if binary { + // Return DER-encoded certificate + cert_to_py(vm, &cert, true).map(Some) + } else { + // Check verify_mode + unsafe { + let ssl_ctx = sys::SSL_get_SSL_CTX(ssl_ptr); + let verify_mode = sys::SSL_CTX_get_verify_mode(ssl_ctx); + if (verify_mode & sys::SSL_VERIFY_PEER as core::ffi::c_int) == 0 { + // Return empty dict when SSL_VERIFY_PEER is not set + Ok(Some(vm.ctx.new_dict().into())) + } else { + // Return decoded certificate + cert_to_py(vm, &cert, false).map(Some) + } } } - } + }) } #[pymethod] @@ -2597,148 +2746,230 @@ mod _ssl { zelf: &Py, vm: &VirtualMachine, ) -> PyResult> { - let stream = zelf.connection.read(); - let ssl = stream.ssl(); - let Some(chain) = ssl.peer_cert_chain() else { - return Ok(None); - }; + with_ssl_pointer(&zelf.connection, |ssl_ptr| { + let ssl = unsafe { ssl::SslRef::from_ptr(ssl_ptr) }; + let Some(chain) = ssl.peer_cert_chain() else { + return Ok(None); + }; - // Return Certificate objects - let mut certs: Vec = chain - .iter() - .map(|cert| unsafe { - sys::X509_up_ref(cert.as_ptr()); - let owned = X509::from_ptr(cert.as_ptr()); - cert_to_certificate(vm, owned) - }) - .collect::>()?; + // Return Certificate objects + let mut certs: Vec = chain + .iter() + .map(|cert| unsafe { + sys::X509_up_ref(cert.as_ptr()); + let owned = X509::from_ptr(cert.as_ptr()); + cert_to_certificate(vm, owned) + }) + .collect::>()?; - // SSL_get_peer_cert_chain does not include peer cert for server-side sockets - // Add it manually at the beginning - if matches!(zelf.socket_type, SslServerOrClient::Server) - && let Some(peer_cert) = ssl.peer_certificate() - { - let peer_obj = cert_to_certificate(vm, peer_cert)?; - certs.insert(0, peer_obj); - } + // SSL_get_peer_cert_chain does not include peer cert for server-side sockets + // Add it manually at the beginning + if matches!(zelf.socket_type, SslServerOrClient::Server) + && let Some(peer_cert) = ssl.peer_certificate() + { + let peer_obj = cert_to_certificate(vm, peer_cert)?; + certs.insert(0, peer_obj); + } - Ok(Some(vm.ctx.new_list(certs))) + Ok(Some(vm.ctx.new_list(certs))) + }) } #[pymethod] fn get_verified_chain(zelf: &Py, vm: &VirtualMachine) -> PyResult> { - let stream = zelf.connection.read(); - unsafe { - let chain = sys::SSL_get0_verified_chain(stream.ssl().as_ptr()); - if chain.is_null() { - return Ok(None); - } - - let num_certs = sys::OPENSSL_sk_num(chain as *const _); - - let mut certs = Vec::with_capacity(num_certs as usize); - // Return Certificate objects - for i in 0..num_certs { - let cert_ptr = sys::OPENSSL_sk_value(chain as *const _, i) as *mut sys::X509; - if cert_ptr.is_null() { - continue; + with_ssl_pointer(&zelf.connection, |ssl_ptr| { + unsafe { + let chain = sys::SSL_get0_verified_chain(ssl_ptr); + if chain.is_null() { + return Ok(None); } - // Clone the X509 certificate to create an owned copy - sys::X509_up_ref(cert_ptr); - let owned_cert = X509::from_ptr(cert_ptr); - let cert_obj = cert_to_certificate(vm, owned_cert)?; - certs.push(cert_obj); - } - Ok(if certs.is_empty() { - None - } else { - Some(vm.ctx.new_list(certs)) - }) - } + let num_certs = sys::OPENSSL_sk_num(chain as *const _); + + let mut certs = Vec::with_capacity(num_certs as usize); + // Return Certificate objects + for i in 0..num_certs { + let cert_ptr = + sys::OPENSSL_sk_value(chain as *const _, i) as *mut sys::X509; + if cert_ptr.is_null() { + continue; + } + // Clone the X509 certificate to create an owned copy + sys::X509_up_ref(cert_ptr); + let owned_cert = X509::from_ptr(cert_ptr); + let cert_obj = cert_to_certificate(vm, owned_cert)?; + certs.push(cert_obj); + } + + Ok(if certs.is_empty() { + None + } else { + Some(vm.ctx.new_list(certs)) + }) + } + }) } #[pymethod] fn version(zelf: &Py) -> Option<&'static str> { - // Use thread-local SSL pointer during handshake to avoid deadlock - let ssl_ptr = get_ssl_ptr_for_context_change(&zelf.connection); - // Return None if handshake is not complete (CPython behavior) - if unsafe { sys::SSL_is_init_finished(ssl_ptr) } == 0 { - return None; + with_ssl_pointer(&zelf.connection, |ssl_ptr| { + // Return None if handshake is not complete (CPython behavior). + if unsafe { sys::SSL_is_init_finished(ssl_ptr) } == 0 { + return None; + } + let version = unsafe { ssl::SslRef::from_ptr(ssl_ptr).version_str() }; + if version == "unknown" { + None + } else { + Some(version) + } + }) + } + #[pymethod] + fn cipher(zelf: &Py) -> Option { + with_ssl_pointer(&zelf.connection, |ssl_ptr| { + unsafe { ssl::SslRef::from_ptr(ssl_ptr).current_cipher() }.map(cipher_to_tuple) + }) + } + #[pymethod] + fn group(zelf: &Py, vm: &VirtualMachine) -> PyResult> { + #[cfg(ossl320)] + { + let _ = vm; + Ok(with_ssl_pointer(&zelf.connection, |ssl_ptr| { + let name = unsafe { SSL_get0_group_name(ssl_ptr) }; + (!name.is_null()) + .then(|| unsafe { CStr::from_ptr(name).to_string_lossy().into_owned() }) + })) + } + #[cfg(not(ossl320))] + { + let _ = zelf; + Err(vm.new_not_implemented_error( + "Getting selected group requires OpenSSL 3.2 or later.", + )) + } + } + + fn signature_name( + &self, + side: SslServerOrClient, + vm: &VirtualMachine, + ) -> PyResult> { + #[cfg(ossl350)] + { + let _ = vm; + const SSL_CTRL_GET_SIGNATURE_NAME: core::ffi::c_int = 140; + const SSL_CTRL_GET_PEER_SIGNATURE_NAME: core::ffi::c_int = 141; + let command = if self.socket_type == side { + SSL_CTRL_GET_SIGNATURE_NAME + } else { + SSL_CTRL_GET_PEER_SIGNATURE_NAME + }; + Ok(with_ssl_pointer(&self.connection, |ssl_ptr| { + let mut name: *const core::ffi::c_char = core::ptr::null(); + let result = unsafe { + sys::SSL_ctrl(ssl_ptr, command, 0, core::ptr::addr_of_mut!(name).cast()) + }; + (result != 0 && !name.is_null()) + .then(|| unsafe { CStr::from_ptr(name).to_string_lossy().into_owned() }) + })) + } + #[cfg(not(ossl350))] + { + let _ = side; + Err(vm.new_not_implemented_error( + "Getting sig algorithms requires OpenSSL 3.5 or later.", + )) } - let v = unsafe { ssl::SslRef::from_ptr(ssl_ptr).version_str() }; - if v == "unknown" { None } else { Some(v) } } #[pymethod] - fn cipher(zelf: &Py) -> Option { - // Use thread-local SSL pointer during handshake to avoid deadlock - let ssl_ptr = get_ssl_ptr_for_context_change(&zelf.connection); - unsafe { ssl::SslRef::from_ptr(ssl_ptr).current_cipher() }.map(cipher_to_tuple) + fn client_sigalg(zelf: &Py, vm: &VirtualMachine) -> PyResult> { + zelf.signature_name(SslServerOrClient::Client, vm) + } + + #[pymethod] + fn server_sigalg(zelf: &Py, vm: &VirtualMachine) -> PyResult> { + zelf.signature_name(SslServerOrClient::Server, vm) + } + + #[pymethod] + fn uses_ktls_for_send(_zelf: &Py) -> bool { + // SslStream uses a custom Rust BIO, never OpenSSL's kernel TLS socket BIO. + false + } + + #[pymethod] + fn uses_ktls_for_recv(_zelf: &Py) -> bool { + false } #[pymethod] fn pending(zelf: &Py) -> i32 { - let stream = zelf.connection.read(); - unsafe { sys::SSL_pending(stream.ssl().as_ptr()) } + with_ssl_pointer(&zelf.connection, |ssl_ptr| unsafe { + sys::SSL_pending(ssl_ptr) + }) } #[pymethod] fn shared_ciphers(zelf: &Py, vm: &VirtualMachine) -> Option { #[cfg(ossl110)] { - // Use thread-local SSL pointer during handshake to avoid deadlock - let ssl_ptr = get_ssl_ptr_for_context_change(&zelf.connection); - unsafe { - let server_ciphers = SSL_get_ciphers(ssl_ptr); - if server_ciphers.is_null() { - return None; - } + with_ssl_pointer(&zelf.connection, |ssl_ptr| { + unsafe { + let server_ciphers = SSL_get_ciphers(ssl_ptr); + if server_ciphers.is_null() { + return None; + } - let client_ciphers = SSL_get_client_ciphers(ssl_ptr); - if client_ciphers.is_null() { - return None; - } + let client_ciphers = SSL_get_client_ciphers(ssl_ptr); + if client_ciphers.is_null() { + return None; + } - let mut result = Vec::new(); - let num_server = sys::OPENSSL_sk_num(server_ciphers as *const _); - let num_client = sys::OPENSSL_sk_num(client_ciphers as *const _); + let mut result = Vec::new(); + let num_server = sys::OPENSSL_sk_num(server_ciphers as *const _); + let num_client = sys::OPENSSL_sk_num(client_ciphers as *const _); - for i in 0..num_server { - let server_cipher_ptr = sys::OPENSSL_sk_value(server_ciphers as *const _, i) - as *const sys::SSL_CIPHER; - - // Check if client supports this cipher by comparing pointers - let mut found = false; - for j in 0..num_client { - let client_cipher_ptr = - sys::OPENSSL_sk_value(client_ciphers as *const _, j) + for i in 0..num_server { + let server_cipher_ptr = + sys::OPENSSL_sk_value(server_ciphers as *const _, i) as *const sys::SSL_CIPHER; - if server_cipher_ptr == client_cipher_ptr { - found = true; - break; + // Check if client supports this cipher by comparing pointers + let mut found = false; + for j in 0..num_client { + let client_cipher_ptr = + sys::OPENSSL_sk_value(client_ciphers as *const _, j) + as *const sys::SSL_CIPHER; + + if server_cipher_ptr == client_cipher_ptr { + found = true; + break; + } + } + + if found { + let cipher = + ssl::SslCipherRef::from_ptr(server_cipher_ptr as *mut _); + let (name, version, bits) = cipher_to_tuple(cipher); + let tuple = vm.new_tuple(( + vm.ctx.new_str(name), + vm.ctx.new_str(version), + vm.ctx.new_int(bits), + )); + result.push(tuple.into()); } } - if found { - let cipher = ssl::SslCipherRef::from_ptr(server_cipher_ptr as *mut _); - let (name, version, bits) = cipher_to_tuple(cipher); - let tuple = vm.new_tuple(( - vm.ctx.new_str(name), - vm.ctx.new_str(version), - vm.ctx.new_int(bits), - )); - result.push(tuple.into()); + if result.is_empty() { + None + } else { + Some(vm.ctx.new_list(result)) } } - - if result.is_empty() { - None - } else { - Some(vm.ctx.new_list(result)) - } - } + }) } #[cfg(not(ossl110))] { @@ -2751,9 +2982,7 @@ mod _ssl { fn selected_alpn_protocol(zelf: &Py) -> Option { #[cfg(ossl102)] { - // Use thread-local SSL pointer during handshake to avoid deadlock - let ssl_ptr = get_ssl_ptr_for_context_change(&zelf.connection); - unsafe { + with_ssl_pointer(&zelf.connection, |ssl_ptr| unsafe { let mut out: *const core::ffi::c_uchar = core::ptr::null(); let mut outlen: core::ffi::c_uint = 0; @@ -2765,7 +2994,7 @@ mod _ssl { let slice = core::slice::from_raw_parts(out, outlen as usize); Some(String::from_utf8_lossy(slice).into_owned()) } - } + }) } #[cfg(not(ossl102))] { @@ -2790,43 +3019,43 @@ mod _ssl { ))); } - let stream = zelf.connection.read(); - let ssl_ptr = stream.ssl().as_ptr(); + with_ssl_pointer(&zelf.connection, |ssl_ptr| { + unsafe { + let session_reused = sys::SSL_session_reused(ssl_ptr) != 0; + let is_client = matches!(zelf.socket_type, SslServerOrClient::Client); - unsafe { - let session_reused = sys::SSL_session_reused(ssl_ptr) != 0; - let is_client = matches!(zelf.socket_type, SslServerOrClient::Client); + // Use XOR logic from CPython + let use_finished = session_reused ^ is_client; - // Use XOR logic from CPython - let use_finished = session_reused ^ is_client; + let mut buf = vec![0u8; CB_MAXLEN]; + let len = if use_finished { + sys::SSL_get_finished(ssl_ptr, buf.as_mut_ptr() as *mut _, CB_MAXLEN) + } else { + sys::SSL_get_peer_finished(ssl_ptr, buf.as_mut_ptr() as *mut _, CB_MAXLEN) + }; - let mut buf = vec![0u8; CB_MAXLEN]; - let len = if use_finished { - sys::SSL_get_finished(ssl_ptr, buf.as_mut_ptr() as *mut _, CB_MAXLEN) - } else { - sys::SSL_get_peer_finished(ssl_ptr, buf.as_mut_ptr() as *mut _, CB_MAXLEN) - }; - - if len == 0 { - Ok(None) - } else { - buf.truncate(len); - Ok(Some(vm.ctx.new_bytes(buf))) + if len == 0 { + Ok(None) + } else { + buf.truncate(len); + Ok(Some(vm.ctx.new_bytes(buf))) + } } - } + }) } #[pymethod] fn verify_client_post_handshake(zelf: &Py, vm: &VirtualMachine) -> PyResult<()> { cfg_select! { ossl111 => { - let stream = zelf.connection.read(); - let result = unsafe { SSL_verify_client_post_handshake(stream.ssl().as_ptr()) }; - if result == 0 { - Err(convert_openssl_error(vm, openssl::error::ErrorStack::get())) - } else { - Ok(()) - } + with_ssl_pointer_mut(&zelf.connection, |ssl_ptr| { + let result = unsafe { SSL_verify_client_post_handshake(ssl_ptr) }; + if result == 0 { + Err(convert_openssl_error(vm, openssl::error::ErrorStack::get())) + } else { + Ok(()) + } + }) } _ => Err(vm.new_not_implemented_error( "Post-handshake auth is not supported by your OpenSSL version.", @@ -2836,124 +3065,61 @@ mod _ssl { #[pymethod] fn shutdown(zelf: &Py, vm: &VirtualMachine) -> PyResult>> { - let stream = zelf.connection.read(); + let mut stream = zelf.connection.write(); let ssl_ptr = stream.ssl().as_ptr(); - - // BIO mode: just try shutdown once and raise SSLWantReadError if needed - if stream.is_bio() { - let ret = vm.allow_threads(|| unsafe { sys::SSL_shutdown(ssl_ptr) }); - if ret < 0 { - let err = unsafe { sys::SSL_get_error(ssl_ptr, ret) }; - if err == sys::SSL_ERROR_WANT_READ { - return Err(create_ssl_want_read_error(vm).upcast()); - } else if err == sys::SSL_ERROR_WANT_WRITE { - return Err(create_ssl_want_write_error(vm).upcast()); - } else { - return Err(new_ssl_error( - vm, - format!("SSL shutdown failed: error code {}", err), - )); - } - } else if ret == 0 { - // Sent close-notify, waiting for peer's - raise SSLWantReadError - return Err(create_ssl_want_read_error(vm).upcast()); - } - return Ok(None); - } - - // Socket mode: loop with select to wait for peer's close-notify - let socket_stream = stream.get_ref().expect("get_ref() failed for socket mode"); - let deadline = socket_stream.timeout_deadline(); - - // Track how many times we've seen ret == 0 (max 2 tries) + let _vm_guard = HandshakeVmGuard::new(vm, &zelf.connection, ssl_ptr); + let deadline = stream.get_ref().map(SocketStream::timeout_deadline); let mut zeros = 0; - loop { - // Shutting down sends close-notify and waits for the peer's, - // which a peer that has gone away never sends. `SSL_shutdown` - // is released around for the same reason. - let ret = vm.allow_threads(|| unsafe { sys::SSL_shutdown(ssl_ptr) }); - - // ret > 0: complete shutdown - if ret > 0 { - break; - } - - // ret == 0: sent our close-notify, need to receive peer's - if ret == 0 { - zeros += 1; - if zeros > 1 { - // Already tried twice, break out (legacy behavior) - break; + if zelf.shutdown_seen_zero.load() { + // Do not consume plaintext following the TLS close_notify. + unsafe { + sys::SSL_ctrl( + ssl_ptr, + sys::SSL_CTRL_SET_READ_AHEAD, + 0, + core::ptr::null_mut(), + ); } - // Wait briefly for peer's close_notify before retrying - match socket_stream.select(SslNeeds::Read, &deadline, vm)? { - SelectRet::TimedOut => { - return Err(socket::timeout_error_msg( - vm, - "The read operation timed out".to_string(), - ) - .upcast()); - } - SelectRet::Closed => { - return Err(socket_closed_error(vm)); - } - SelectRet::Nonblocking => { - // Non-blocking socket: return SSLWantReadError - return Err(create_ssl_want_read_error(vm).upcast()); - } - SelectRet::Ok => { - // Data available, continue to retry - } - } - continue; } - - // ret < 0: error or would-block - let err = unsafe { sys::SSL_get_error(ssl_ptr, ret) }; - - let needs = if err == sys::SSL_ERROR_WANT_READ { - SslNeeds::Read - } else if err == sys::SSL_ERROR_WANT_WRITE { - SslNeeds::Write - } else { - // Real error - return Err(new_ssl_error( + let error = + match zelf.finish_ssl_operation(vm.allow_threads(|| stream.shutdown()), vm)? { + Ok(ssl::ShutdownResult::Received) => break, + Ok(ssl::ShutdownResult::Sent) => { + zeros += 1; + if zeros > 1 { + break; + } + zelf.shutdown_seen_zero.store(true); + // Retry immediately: the peer's notification may already + // be buffered, even when the transport is not readable. + continue; + } + Err(error) => error, + }; + if let Some(socket) = stream.get_ref() { + let (needs, state) = socket.socket_needs( + &error, + deadline.as_ref().expect("socket deadline"), vm, - format!("SSL shutdown failed: error code {}", err), - )); - }; - - // Wait on the socket - match socket_stream.select(needs, &deadline, vm)? { - SelectRet::TimedOut => { - let msg = if err == sys::SSL_ERROR_WANT_READ { - "The read operation timed out" - } else { - "The write operation timed out" - }; - return Err(socket::timeout_error_msg(vm, msg.to_string()).upcast()); - } - SelectRet::Closed => { - return Err(socket_closed_error(vm)); - } - SelectRet::Nonblocking => { - // Non-blocking socket, raise SSLWantReadError/SSLWantWriteError - if err == sys::SSL_ERROR_WANT_READ { - return Err(create_ssl_want_read_error(vm).upcast()); - } else { - return Err(create_ssl_want_write_error(vm).upcast()); + )?; + match state { + SelectRet::TimedOut => { + let message = if error.code() == ssl::ErrorCode::WANT_READ { + "The read operation timed out" + } else { + "The write operation timed out" + }; + return Err(socket::timeout_error_msg(vm, message.to_owned()).upcast()); } - } - SelectRet::Ok => { - // Socket is ready, retry shutdown - continue; + SelectRet::Closed => return Err(socket_closed_error(vm)), + SelectRet::Ok if needs.is_some() => continue, + _ => {} } } + return Err(zelf.ssl_error(error, vm)); } - - // Return the underlying socket - Ok(Some(socket_stream.0.clone())) + Ok(stream.get_ref().map(|socket| socket.0.clone())) } #[cfg(osslconf = "OPENSSL_NO_COMP")] @@ -2964,42 +3130,42 @@ mod _ssl { #[cfg(not(osslconf = "OPENSSL_NO_COMP"))] #[pymethod] fn compression(zelf: &Py) -> Option<&'static str> { - // Use thread-local SSL pointer during handshake to avoid deadlock - let ssl_ptr = get_ssl_ptr_for_context_change(&zelf.connection); - let comp_method = unsafe { sys::SSL_get_current_compression(ssl_ptr) }; - if comp_method.is_null() { - return None; - } - let typ = unsafe { sys::COMP_get_type(comp_method) }; - let nid = Nid::from_raw(typ); - if nid == Nid::UNDEF { - return None; - } - nid.short_name().ok() + with_ssl_pointer(&zelf.connection, |ssl_ptr| { + let comp_method = unsafe { sys::SSL_get_current_compression(ssl_ptr) }; + if comp_method.is_null() { + return None; + } + let typ = unsafe { sys::COMP_get_type(comp_method) }; + let nid = Nid::from_raw(typ); + if nid == Nid::UNDEF { + return None; + } + nid.short_name().ok() + }) } #[pymethod] fn do_handshake(zelf: &Py, vm: &VirtualMachine) -> PyResult<()> { + zelf.check_eof(vm)?; let mut stream = zelf.connection.write(); let ssl_ptr = stream.ssl().as_ptr(); // Set up thread-local VM and SSL pointer for callbacks // This allows callbacks to access SSL without acquiring the connection lock - let _vm_guard = HandshakeVmGuard::new(vm, ssl_ptr); + let _vm_guard = HandshakeVmGuard::new(vm, &zelf.connection, ssl_ptr); // BIO mode: no timeout/select logic, just do handshake if stream.is_bio() { - let result = vm.allow_threads(|| stream.do_handshake()).map_err(|e| { - let exc = convert_ssl_error(vm, e); - // If it's a cert verification error, set verify info - if exc.class().is(PySSLCertVerificationError::class(&vm.ctx)) { - set_verify_error_info(&exc, ssl_ptr, vm); - } - exc - }); - // Clean up SNI ex_data after handshake (success or failure) - // SAFETY: ssl_ptr is valid for the lifetime of stream - unsafe { cleanup_sni_ex_data(ssl_ptr) }; + let result = zelf + .finish_ssl_operation(vm.allow_threads(|| stream.do_handshake()), vm)? + .map_err(|e| { + let exc = zelf.ssl_error(e, vm); + // If it's a cert verification error, set verify info + if exc.class().is(PySSLCertVerificationError::class(&vm.ctx)) { + set_verify_error_info(&exc, ssl_ptr, vm); + } + exc + }); return result; } @@ -3015,11 +3181,10 @@ mod _ssl { // same reason. The connection lock stays held across it, which // is why it is a detaching lock: a thread reaching the same // socket gives up its interpreter rather than wait attached. - let err = match vm.allow_threads(|| stream.do_handshake()) { + let err = match zelf + .finish_ssl_operation(vm.allow_threads(|| stream.do_handshake()), vm)? + { Ok(()) => { - // Clean up SNI ex_data after successful handshake - // SAFETY: ssl_ptr is valid for the lifetime of stream - unsafe { cleanup_sni_ex_data(ssl_ptr) }; return Ok(()); } Err(e) => e, @@ -3030,9 +3195,6 @@ mod _ssl { .socket_needs(&err, &timeout, vm)?; match state { SelectRet::TimedOut => { - // Clean up SNI ex_data before returning error - // SAFETY: ssl_ptr is valid for the lifetime of stream - unsafe { cleanup_sni_ex_data(ssl_ptr) }; return Err(socket::timeout_error_msg( vm, "The handshake operation timed out".to_owned(), @@ -3040,8 +3202,6 @@ mod _ssl { .upcast()); } SelectRet::Closed => { - // SAFETY: ssl_ptr is valid for the lifetime of stream - unsafe { cleanup_sni_ex_data(ssl_ptr) }; return Err(socket_closed_error(vm)); } SelectRet::Nonblocking => {} @@ -3053,29 +3213,28 @@ mod _ssl { } } } - let exc = convert_ssl_error(vm, err); + let exc = zelf.ssl_error(err, vm); // If it's a cert verification error, set verify info if exc.class().is(PySSLCertVerificationError::class(&vm.ctx)) { set_verify_error_info(&exc, ssl_ptr, vm); } - // Clean up SNI ex_data before returning error - // SAFETY: ssl_ptr is valid for the lifetime of stream - unsafe { cleanup_sni_ex_data(ssl_ptr) }; return Err(exc); } } #[pymethod] fn write(zelf: &Py, data: ArgBytesLike, vm: &VirtualMachine) -> PyResult { + zelf.check_eof(vm)?; let mut stream = zelf.connection.write(); + let _vm_guard = HandshakeVmGuard::new(vm, &zelf.connection, stream.ssl().as_ptr()); let data = data.borrow_buf(); let data = &*data; // BIO mode: no timeout/select logic if stream.is_bio() { - return vm - .allow_threads(|| stream.ssl_write(data)) - .map_err(|e| convert_ssl_error(vm, e)); + return zelf + .finish_ssl_operation(vm.allow_threads(|| stream.ssl_write(data)), vm)? + .map_err(|e| zelf.ssl_error(e, vm)); } // Socket mode: handle timeout and blocking @@ -3099,7 +3258,9 @@ mod _ssl { // Sending waits for the peer to make room, which it need not // ever do; `SSL_write_ex` is released around for the same // reason. - let err = match vm.allow_threads(|| stream.ssl_write(data)) { + let err = match zelf + .finish_ssl_operation(vm.allow_threads(|| stream.ssl_write(data)), vm)? + { Ok(len) => return Ok(len), Err(e) => e, }; @@ -3125,25 +3286,26 @@ mod _ssl { } } } - return Err(convert_ssl_error(vm, err)); + return Err(zelf.ssl_error(err, vm)); } } #[pygetset] fn session(zelf: &Py, _vm: &VirtualMachine) -> PyResult> { - let stream = zelf.connection.read(); - unsafe { - // Use SSL_get1_session which returns an owned reference (ref count already incremented) - let session_ptr = SSL_get1_session(stream.ssl().as_ptr()); - if session_ptr.is_null() { - Ok(None) - } else { - Ok(Some(PySslSession { - session: session_ptr, - ctx: zelf.ctx.read().clone(), - })) + with_ssl_pointer(&zelf.connection, |ssl_ptr| { + unsafe { + // Use SSL_get1_session which returns an owned reference (ref count already incremented) + let session_ptr = SSL_get1_session(ssl_ptr); + if session_ptr.is_null() { + Ok(None) + } else { + Ok(Some(PySslSession { + session: session_ptr, + ctx: zelf.ctx.read().clone(), + })) + } } - } + }) } #[pygetset(setter)] @@ -3166,29 +3328,25 @@ mod _ssl { return Err(vm.new_value_error("Cannot set session for server-side SSLSocket.")); } - // Check if handshake is not finished - let stream = zelf.connection.read(); - unsafe { - if sys::SSL_is_init_finished(stream.ssl().as_ptr()) != 0 { - return Err(vm.new_value_error("Cannot set session after handshake.")); + with_ssl_pointer_mut(&zelf.connection, |ssl_ptr| { + unsafe { + if sys::SSL_is_init_finished(ssl_ptr) != 0 { + return Err(vm.new_value_error("Cannot set session after handshake.")); + } + if sys::SSL_set_session(ssl_ptr, session.session) == 0 { + return Err(convert_openssl_error(vm, ErrorStack::get())); + } } - - let ret = sys::SSL_set_session(stream.ssl().as_ptr(), session.session); - if ret == 0 { - return Err(convert_openssl_error(vm, ErrorStack::get())); - } - } - - Ok(()) + Ok(()) + }) } #[pygetset] fn session_reused(zelf: &Py) -> bool { - // Use thread-local SSL pointer during handshake to avoid deadlock - let ssl_ptr = get_ssl_ptr_for_context_change(&zelf.connection); - unsafe { sys::SSL_session_reused(ssl_ptr) != 0 } + with_ssl_pointer(&zelf.connection, |ssl_ptr| unsafe { + sys::SSL_session_reused(ssl_ptr) != 0 + }) } - #[pymethod] fn read( zelf: &Py, @@ -3217,6 +3375,8 @@ mod _ssl { } }; + zelf.check_eof(vm)?; + // Special case: reading 0 bytes should return empty bytes immediately if read_len == 0 { return if buffer.is_present() { @@ -3227,6 +3387,7 @@ mod _ssl { } let mut stream = zelf.connection.write(); + let _vm_guard = HandshakeVmGuard::new(vm, &zelf.connection, stream.ssl().as_ptr()); // The read below answers when the peer writes, which may be never, // and reaching the caller's buffer takes a lock that every other // thread touching the same object waits on. Read aside and take @@ -3236,19 +3397,24 @@ mod _ssl { // BIO mode: no timeout/select logic let count = if stream.is_bio() { - match vm.allow_threads(|| stream.ssl_read(buf)) { + match zelf.finish_ssl_operation(vm.allow_threads(|| stream.ssl_read(buf)), vm)? { Ok(count) => count, Err(e) => { - // Handle ZERO_RETURN (EOF) - raise SSLEOFError + // close_notify is an orderly TLS EOF. if e.code() == ssl::ErrorCode::ZERO_RETURN { - return Err(create_ssl_eof_error(vm).upcast()); + return if buffer.is_present() { + Ok(vm.ctx.new_int(0).into()) + } else { + Ok(vm.ctx.new_bytes(vec![]).into()) + }; } // If WANT_READ and the incoming BIO has EOF written, // this is an unexpected EOF (transport closed without TLS close_notify) if e.code() == ssl::ErrorCode::WANT_READ && stream.is_bio_eof() { + zelf.got_eof_error.store(true); return Err(create_ssl_eof_error(vm).upcast()); } - return Err(convert_ssl_error(vm, e)); + return Err(zelf.ssl_error(e, vm)); } } } else { @@ -3261,7 +3427,9 @@ mod _ssl { // This is the wait the whole method is shaped around: it // ends when the peer writes. `SSL_read_ex` is released // around for the same reason. - let err = match vm.allow_threads(|| stream.ssl_read(buf)) { + let err = match zelf + .finish_ssl_operation(vm.allow_threads(|| stream.ssl_read(buf)), vm)? + { Ok(count) => break count, Err(e) => e, }; @@ -3292,7 +3460,7 @@ mod _ssl { } } } - return Err(convert_ssl_error(vm, err)); + return Err(zelf.ssl_error(err, vm)); } }; let ret = match &buffer { @@ -3397,8 +3565,43 @@ mod _ssl { unsafe impl Send for PySslMemoryBio {} unsafe impl Sync for PySslMemoryBio {} + #[pyfunction] + fn get_sigalgs(vm: &VirtualMachine) -> PyResult { + #[cfg(ossl340)] + unsafe { + let names = SSL_get1_builtin_sigalgs(core::ptr::null_mut()); + if names.is_null() { + return Err(vm.new_memory_error("failed to get signature algorithms")); + } + let names_text = CStr::from_ptr(names).to_string_lossy().into_owned(); + sys::CRYPTO_free(names.cast(), core::ptr::null(), 0); + Ok(vm.ctx.new_list( + names_text + .split(':') + .map(|name| vm.ctx.new_str(name).into()) + .collect(), + )) + } + #[cfg(not(ossl340))] + { + Err(vm.new_not_implemented_error( + "Getting signature algorithms requires OpenSSL 3.4 or later.", + )) + } + } + // OpenSSL functions not in openssl-sys + #[cfg(ossl320)] + unsafe extern "C" { + fn SSL_get0_group_name(ssl: *mut sys::SSL) -> *const core::ffi::c_char; + } + + #[cfg(ossl340)] + unsafe extern "C" { + fn SSL_get1_builtin_sigalgs(ctx: *mut core::ffi::c_void) -> *mut core::ffi::c_char; + } + unsafe extern "C" { // X509_check_ca returns 1 for CA certificates, 0 otherwise fn X509_check_ca(x: *const sys::X509) -> core::ffi::c_int; @@ -3793,10 +3996,29 @@ mod _ssl { .upcast() } + fn system_error_errno(code: core::ffi::c_ulong) -> core::ffi::c_int { + // OpenSSL 3 stores errno below ERR_SYSTEM_FLAG, as specified by + // https://github.com/openssl/openssl/blob/openssl-3.5.7/include/openssl/err.h.in + // openssl-sys 0.9.117's ERR_GET_REASON returns ERR_LIB_SYS instead. + cfg_select! { + ossl300 => (code & sys::ERR_SYSTEM_MASK) as core::ffi::c_int, + _ => sys::ERR_GET_REASON(code), + } + } + #[track_caller] pub(crate) fn convert_openssl_error( vm: &VirtualMachine, err: ErrorStack, + ) -> PyBaseExceptionRef { + convert_openssl_error_with_errno(vm, err, None) + } + + #[track_caller] + fn convert_openssl_error_with_errno( + vm: &VirtualMachine, + err: ErrorStack, + operation_errno: Option, ) -> PyBaseExceptionRef { match err.errors().last() { Some(e) => { @@ -3805,7 +4027,9 @@ mod _ssl { if lib == sys::ERR_LIB_SYS { // A system error is being reported; reason is set to errno - let reason = sys::ERR_GET_REASON(e.code()); + let reason = system_error_errno(e.code()); + let message = rustpython_host_env::errno::strerror_string(reason) + .unwrap_or_else(|| format!("System error {reason}")); // errno 2 = ENOENT = FileNotFoundError let exc_type = if reason == 2 { @@ -3813,7 +4037,9 @@ mod _ssl { } else { vm.ctx.exceptions.os_error.to_owned() }; - return vm.new_os_subtype_error(exc_type, Some(reason), "").upcast(); + return vm + .new_os_subtype_error(exc_type, Some(reason), message) + .upcast(); } let caller = std::panic::Location::caller(); @@ -3854,16 +4080,17 @@ mod _ssl { PySSLError::class(&vm.ctx).to_owned() }; - // Build message + let description = e.reason().unwrap_or("unknown error"); let msg = if let Some(lib_str) = lib_name { - format!("[{lib_str}] {errstr} ({file}:{line})") + format!("[{lib_str}: {errstr}] {description} ({file}:{line})") } else { - format!("{errstr} ({file}:{line})") + format!("{description} ({file}:{line})") }; // Create exception instance let reason = sys::ERR_GET_REASON(e.code()); - let exc = vm.new_os_subtype_error(cls, Some(reason), msg); + let exc = + vm.new_os_subtype_error(cls, Some(operation_errno.unwrap_or(reason)), msg); let exc_obj: PyObjectRef = exc.upcast::().into(); // Set reason attribute (always set, even if just the error string) @@ -3924,12 +4151,28 @@ mod _ssl { let _ = exc_obj.set_attr("verify_code", verify_code_obj, vm); let _ = exc_obj.set_attr("verify_message", verify_message, vm); } + fn is_unexpected_eof(error: &ssl::Error) -> bool { + (error.code() == ssl::ErrorCode::SYSCALL + && error.io_error().is_none() + && error.ssl_error().is_none()) + || error.ssl_error().is_some_and(|stack| { + stack.errors().last().is_some_and(|error| { + error.library_code() == ERR_LIB_SSL + && error.reason_code() == SSL_R_UNEXPECTED_EOF_WHILE_READING + }) + }) + } + #[track_caller] fn convert_ssl_error( vm: &VirtualMachine, e: impl std::borrow::Borrow, ) -> PyBaseExceptionRef { let e = e.borrow(); + if is_unexpected_eof(e) { + return create_ssl_eof_error(vm).upcast(); + } + let (cls, msg) = match e.code() { ssl::ErrorCode::WANT_READ => { return create_ssl_want_read_error(vm).upcast(); @@ -3939,26 +4182,24 @@ mod _ssl { } ssl::ErrorCode::SYSCALL => match e.io_error() { Some(io_err) => return io_err.to_pyexception(vm), - // When no I/O error and OpenSSL error queue is empty, - // this is an EOF in violation of protocol -> SSLEOFError None => { + if let Some(stack) = e.ssl_error() { + return convert_openssl_error_with_errno( + vm, + stack.clone(), + Some(sys::SSL_ERROR_SYSCALL), + ); + } return create_ssl_eof_error(vm).upcast(); } }, ssl::ErrorCode::SSL => { - // Check for OpenSSL 3.0 SSL_R_UNEXPECTED_EOF_WHILE_READING if let Some(ssl_err) = e.ssl_error() { - // In OpenSSL 3.0+, unexpected EOF is reported as SSL_ERROR_SSL - // with this specific reason code instead of SSL_ERROR_SYSCALL - unsafe { - let err_code = sys::ERR_peek_last_error(); - let reason = sys::ERR_GET_REASON(err_code); - let lib = sys::ERR_GET_LIB(err_code); - if lib == ERR_LIB_SSL && reason == SSL_R_UNEXPECTED_EOF_WHILE_READING { - return create_ssl_eof_error(vm).upcast(); - } - } - return convert_openssl_error(vm, ssl_err.clone()); + return convert_openssl_error_with_errno( + vm, + ssl_err.clone(), + Some(sys::SSL_ERROR_SSL), + ); } ( PySSLError::class(&vm.ctx).to_owned(), @@ -4115,6 +4356,21 @@ mod _ssl { Ok(()) } } + #[cfg(test)] + mod error_tests { + use super::{sys, system_error_errno}; + + #[test] + fn system_errors_preserve_errno() { + for errno in [libc::ENOENT, libc::EPIPE, libc::ECONNRESET] { + let code = cfg_select! { + ossl300 => sys::ERR_SYSTEM_FLAG | errno as core::ffi::c_ulong, + _ => sys::ERR_PACK(sys::ERR_LIB_SYS, 0, errno), + }; + assert_eq!(system_error_errno(code), errno); + } + } + } } #[allow(non_upper_case_globals)] diff --git a/crates/stdlib/src/ssl.rs b/crates/stdlib/src/ssl.rs index 3f18d93c3..05220d332 100644 --- a/crates/stdlib/src/ssl.rs +++ b/crates/stdlib/src/ssl.rs @@ -1,4 +1,4 @@ -// spell-checker: ignore ssleof aesccm aesgcm capath getblocking setblocking ENDTLS TLSEXT +// spell-checker: ignore ssleof aesccm aesgcm capath getblocking setblocking ENDTLS TLSEXT ktls sigalgs rsae mldsa //! Pure Rust SSL/TLS implementation using rustls //! @@ -79,14 +79,14 @@ mod _ssl { // Import error types used in this module (others are exposed via pymodule(with(...))) use super::error::{ - PySSLError, PySSLWantReadError, PySSLWantWriteError, create_ssl_eof_error, + PySSLEOFError, PySSLError, PySSLWantReadError, PySSLWantWriteError, create_ssl_eof_error, create_ssl_want_read_error, create_ssl_want_write_error, create_ssl_zero_return_error, }; use alloc::sync::Arc; use core::{ hash::{Hash, Hasher}, hint::cold_path, - sync::atomic::{AtomicUsize, Ordering}, + sync::atomic::{AtomicBool, AtomicUsize, Ordering}, time::Duration, }; use rustpython_vm::exceptions; @@ -281,6 +281,8 @@ mod _ssl { #[pyattr] const HAS_PSK: bool = false; // PSK not supported in rustls #[pyattr] + const HAS_PSK_TLS13: bool = false; // External PSK callbacks are not supported. + #[pyattr] const HAS_SSLv2: bool = false; #[pyattr] const HAS_SSLv3: bool = false; @@ -325,6 +327,8 @@ mod _ssl { #[pytraverse(skip)] context_identity: Arc<()>, #[pytraverse(skip)] + policy_identity: PyRwLock>, + #[pytraverse(skip)] protocol: i32, #[pytraverse(skip)] check_hostname: PyRwLock, @@ -336,7 +340,7 @@ mod _ssl { verify_flags: PyRwLock, // Rustls configuration (built lazily) #[pytraverse(skip)] - server_config: PyRwLock>, + server_config: PyRwLock, chain::ServerConfig)>>, // Certificate store #[pytraverse(skip)] root_certs: PyRwLock, @@ -380,9 +384,11 @@ mod _ssl { keylog_filename: PyRwLock>, #[pytraverse(skip)] key_log: Arc, - // ECDH curve name for key exchange + // Explicit key exchange groups, shared by set_groups and set_ecdh_curve. #[pytraverse(skip)] - ecdh_curve: PyRwLock>, + kx_groups: PyRwLock>>, + #[pytraverse(skip)] + server_sigalgs: PyRwLock>>, // Certificate statistics for cert_store_stats() #[pytraverse(skip)] ca_cert_count: PyRwLock, // Number of CA certificates @@ -470,6 +476,14 @@ mod _ssl { #[pyclass(with(Constructor, Representable), flags(BASETYPE))] impl PySSLContext { + fn invalidate_policy(&self) { + // Old SSLSession configurations must never override later policy. + // Replace the identity after mutation so concurrent snapshots also + // become ineligible for resumption once that mutation completes. + *self.policy_identity.write() = Arc::new(()); + *self.server_config.write() = None; + } + fn warn_deprecated_tls_version(version: i32, vm: &VirtualMachine) -> PyResult<()> { let version_name = match version { PROTO_SSLv3 => Some("SSLv3"), @@ -505,7 +519,7 @@ mod _ssl { capath_state.directories.push(directory); capath_state.cache = None; drop(capath_state); - *self.server_config.write() = None; + self.invalidate_policy(); } fn capath_certificates(&self) -> Arc>> { @@ -571,6 +585,7 @@ mod _ssl { *zelf.verify_mode.write() = CERT_REQUIRED; } } + zelf.invalidate_policy(); } #[pygetset] @@ -581,6 +596,7 @@ mod _ssl { #[pygetset(setter)] fn set__host_flags(zelf: &Py, value: i32) { *zelf.host_flags.write() = value; + zelf.invalidate_policy(); } #[pygetset] @@ -600,6 +616,7 @@ mod _ssl { )); } *zelf.verify_mode.write() = mode; + zelf.invalidate_policy(); Ok(()) } @@ -616,6 +633,7 @@ mod _ssl { #[pygetset(setter)] fn set_verify_flags(zelf: &Py, value: i32) { *zelf.verify_flags.write() = value; + zelf.invalidate_policy(); } #[pygetset] @@ -626,6 +644,7 @@ mod _ssl { #[pygetset(setter)] fn set_post_handshake_auth(zelf: &Py, value: bool) { *zelf.post_handshake_auth.write() = value; + zelf.invalidate_policy(); } #[pygetset] @@ -644,6 +663,7 @@ mod _ssl { ); } *zelf.num_tickets.write() = value; + zelf.invalidate_policy(); Ok(()) } @@ -682,6 +702,7 @@ mod _ssl { } *zelf.options.write() = value; + zelf.invalidate_policy(); Ok(()) } @@ -715,6 +736,7 @@ mod _ssl { _ => value, }; *zelf.minimum_version.write() = normalized_value; + zelf.invalidate_policy(); Ok(()) } @@ -748,6 +770,7 @@ mod _ssl { _ => value, }; *zelf.maximum_version.write() = normalized_value; + zelf.invalidate_policy(); Ok(()) } @@ -788,7 +811,9 @@ mod _ssl { // If failed and callable exists, invoke it and retry // This implements lazy evaluation: callable only invoked if password is actually needed - if result.is_err() + if result + .as_ref() + .is_err_and(|error| !error.is::()) && let Some(callable) = password_callable { // Invoke callable - exceptions propagate naturally @@ -826,6 +851,25 @@ mod _ssl { // Process result let (certs, key) = result.map_err(|e| { + let e = match e.downcast::() { + Ok(error) => { + let filename = if error.key_file { + args.keyfile.as_ref().unwrap_or(&args.certfile) + } else { + &args.certfile + }; + let filename: PyObjectRef = match filename { + Either::A(s) => s.clone().into(), + Either::B(b) => vm.ctx.new_bytes(b.borrow_buf().to_vec()).into(), + }; + let exc = error.error.into_pyexception(vm); + exc.as_object() + .set_attr("filename", filename, vm) + .expect("OSError.filename is writable"); + return exc; + } + Err(error) => error, + }; // Try to downcast to io::Error to preserve errno information if let Ok(io_err) = e.downcast::() { match io_err.kind() { @@ -932,6 +976,7 @@ mod _ssl { // Add new cert/key pair as tuple cert_keys.push((Arc::new(certified_key), key)); + zelf.invalidate_policy(); Ok(()) } @@ -981,6 +1026,8 @@ mod _ssl { (None, false) }; + // Loaders may partially update trust before returning an error. + let _policy_change = ContextPolicyChange(zelf); // If it's a CRL, just add it (separate lock, no conflict with root_store) if let Some(crl) = crl_opt { zelf.crls.write().push(crl); @@ -1017,7 +1064,7 @@ mod _ssl { if let Some(dir_path) = capath_dir { zelf.add_verify_dir(dir_path); } - *zelf.server_config.write() = None; + zelf.invalidate_policy(); Ok(()) } @@ -1142,6 +1189,7 @@ mod _ssl { _purpose: OptionalArg, vm: &VirtualMachine, ) -> PyResult<()> { + let _policy_change = ContextPolicyChange(zelf); let mut store = zelf.root_certs.write(); #[cfg(windows)] @@ -1179,7 +1227,7 @@ mod _ssl { } drop(store); - *zelf.server_config.write() = None; + zelf.invalidate_policy(); Ok(()) } @@ -1195,6 +1243,7 @@ mod _ssl { alpn_list.push(bytes.borrow_buf().to_vec()); } *zelf.alpn_protocols.write() = alpn_list; + zelf.invalidate_policy(); Ok(()) } @@ -1207,6 +1256,7 @@ mod _ssl { let bytes = protos.borrow_buf(); let alpn_list = parse_length_prefixed_alpn(&bytes, vm)?; *zelf.alpn_protocols.write() = alpn_list; + zelf.invalidate_policy(); Ok(()) } @@ -1234,21 +1284,128 @@ mod _ssl { .upcast() })?; - // TLS 1.3 has a separate OpenSSL setter. Discard whatever this - // cipher string happened to match there, then restore exactly the - // provider defaults in their preference order. + // TLS 1.3 has a separate setter. Preserve its current selection. + let current = zelf.selected_ciphers.read(); selected_ciphers = cipher::restore_default_tls13( selected_ciphers, - CryptoExt::get_ext().default_ciphers_or_provider(), + current + .as_deref() + .unwrap_or_else(|| CryptoExt::get_ext().default_ciphers_or_provider()), ); + drop(current); *zelf.selected_ciphers.write() = Some(selected_ciphers); *zelf.suite_b_kx_groups.write() = suite_b_kx_groups; - *zelf.server_config.write() = None; + zelf.invalidate_policy(); Ok(()) } + #[pymethod] + fn set_ciphersuites( + zelf: &Py, + ciphersuites: PyUtf8StrRef, + vm: &VirtualMachine, + ) -> PyResult<()> { + let list = ciphersuites.as_str(); + if list.contains('\0') { + return Err(vm.new_value_error("embedded null character")); + } + let mut selected = Vec::new(); + for name in list.split(':') { + if let Some(suite) = CryptoExt::get_ext() + .all_ciphers_or_default() + .iter() + .find(|suite| suite.tls13().is_some() && cipher::describe(suite).name == name) + && !selected.contains(suite) + { + selected.push(*suite); + } + } + if selected.is_empty() && !list.is_empty() { + return Err(vm + .new_os_subtype_error( + PySSLError::class(&vm.ctx).to_owned(), + None, + "No cipher suite can be selected.".to_owned(), + ) + .upcast()); + } + let mut current = zelf.selected_ciphers.write(); + selected.extend( + current + .as_deref() + .unwrap_or_else(|| CryptoExt::get_ext().default_ciphers_or_provider()) + .iter() + .filter(|suite| suite.tls13().is_none()) + .copied(), + ); + *current = Some(selected); + zelf.invalidate_policy(); + Ok(()) + } + + #[pymethod] + fn set_groups( + zelf: &Py, + grouplist: PyUtf8StrRef, + vm: &VirtualMachine, + ) -> PyResult<()> { + let list = grouplist.as_str(); + if list.contains('\0') { + return Err(vm.new_value_error("embedded null character")); + } + let mut groups: Vec<&'static dyn SupportedKxGroup> = Vec::new(); + if !list.is_empty() { + for name in list.split(':') { + let (name, optional) = + name.strip_prefix('?').map_or((name, false), |n| (n, true)); + match cipher::kx_group_by_openssl_name(name) { + Some(group) if !groups.iter().any(|g| g.name() == group.name()) => { + groups.push(group) + } + Some(_) => {} + None if optional => {} + None => { + return Err(vm + .new_os_subtype_error( + PySSLError::class(&vm.ctx).to_owned(), + None, + "unrecognized group".to_owned(), + ) + .upcast()); + } + } + } + } + *zelf.kx_groups.write() = Some(groups); + zelf.invalidate_policy(); + Ok(()) + } + + #[pymethod] + fn set_server_sigalgs( + zelf: &Py, + sigalgslist: PyUtf8StrRef, + vm: &VirtualMachine, + ) -> PyResult<()> { + let list = sigalgslist.as_str(); + if list.contains('\0') { + return Err(vm.new_value_error("embedded null character")); + } + let schemes = rustpython_host_env::ssl::sigalg::parse_list(list).map_err(|error| { + vm.new_os_subtype_error( + PySSLError::class(&vm.ctx).to_owned(), + None, + error.to_owned(), + ) + .upcast() + })?; + *zelf.server_sigalgs.write() = Some(schemes); + zelf.invalidate_policy(); + Ok(()) + } + #[pymethod] fn get_ciphers(zelf: &Py, vm: &VirtualMachine) -> PyListRef { // What the last `set_ciphers` selected, or the provider defaults @@ -1556,11 +1713,10 @@ mod _ssl { return Err(vm.new_type_error("ECDH curve name must be str or bytes")); }; - curve_name_to_kx_group(&curve_name).map_err(|error| vm.new_value_error(error))?; - - // Store the curve name to be used during handshake - // This will limit the key exchange groups offered/accepted - *zelf.ecdh_curve.write() = Some(curve_name); + let groups = + curve_name_to_kx_group(&curve_name).map_err(|error| vm.new_value_error(error))?; + *zelf.kx_groups.write() = Some(groups); + zelf.invalidate_policy(); Ok(()) } @@ -1618,6 +1774,7 @@ mod _ssl { server_hostname: PyRwLock::new(hostname), connection: PyMutex::new(None), state: PyMutex::new(TlsState::new(args.server_side)), + got_eof_error: AtomicBool::new(false), session_was_reused: PyMutex::new(false), owner: PyRwLock::new(args.owner.map(|o| o.downgrade(None, vm)).transpose()?), session: PyRwLock::new(None), @@ -1698,6 +1855,7 @@ mod _ssl { server_hostname: PyRwLock::new(hostname), connection: PyMutex::new(None), state: PyMutex::new(TlsState::new(server_side)), + got_eof_error: AtomicBool::new(false), session_was_reused: PyMutex::new(false), owner: PyRwLock::new(args.owner.map(|o| o.downgrade(None, vm)).transpose()?), session: PyRwLock::new(None), @@ -1983,6 +2141,7 @@ mod _ssl { let shared_session_cache = Arc::new(ParkingRwLock::new(HashMap::new())); Ok(Self { context_identity: Arc::new(()), + policy_identity: PyRwLock::new(Arc::new(())), protocol, check_hostname: PyRwLock::new(protocol == PROTOCOL_TLS_CLIENT), host_flags: PyRwLock::new(0), @@ -2004,7 +2163,8 @@ mod _ssl { msg_callback: PyRwLock::new(None), keylog_filename: PyRwLock::new(None), key_log: Arc::new(super::keylog::KeyLog::default()), - ecdh_curve: PyRwLock::new(None), + kx_groups: PyRwLock::new(None), + server_sigalgs: PyRwLock::new(None), ca_cert_count: PyRwLock::new(0), x509_cert_count: PyRwLock::new(0), // Use the shared cache created above @@ -2084,6 +2244,23 @@ mod _ssl { } } + // Trust loaders can mutate before a later input fails, so invalidate on + // both successful and exceptional exits, after their write locks drop. + struct ContextPolicyChange<'a>(&'a PySSLContext); + + impl Drop for ContextPolicyChange<'_> { + fn drop(&mut self) { + self.0.invalidate_policy(); + } + } + + #[derive(Debug)] + struct ClientConfigSnapshot { + config: Arc, + context_identity: Arc<()>, + policy_identity: Arc<()>, + } + // SSLSocket - represents a TLS-wrapped socket #[pyattr] #[pyclass(name = "_SSLSocket", module = "_ssl", traverse)] @@ -2107,6 +2284,8 @@ mod _ssl { connection: PyMutex>, // Includes the saved exception while a rejected handshake sends its alert. state: PyMutex, + #[pytraverse(skip)] + got_eof_error: AtomicBool, // Session was reused (for session resumption tracking) #[pytraverse(skip)] session_was_reused: PyMutex, @@ -2117,7 +2296,7 @@ mod _ssl { // Client configuration used by this connection. Retained so the resulting // SSLSession can reuse the same verifier and client credentials. #[pytraverse(skip)] - client_config: PyRwLock>>, + client_config: PyRwLock>, #[pytraverse(skip)] chain_builder: PyRwLock>>, #[pytraverse(skip)] @@ -2144,6 +2323,19 @@ mod _ssl { #[pyclass(with(Constructor, Representable), flags(BASETYPE))] impl PySSLSocket { + // Call only for transport/protocol failures. User message callbacks + // may raise SSLEOFError without closing the connection. + fn remember_eof( + &self, + error: PyBaseExceptionRef, + vm: &VirtualMachine, + ) -> PyBaseExceptionRef { + if error.fast_isinstance(PySSLEOFError::class(&vm.ctx)) { + self.got_eof_error.store(true, Ordering::Relaxed); + } + error + } + // Check if this is BIO mode pub(crate) fn is_bio_mode(&self) -> bool { matches!(self.io, SocketOrBio::Bio { .. }) @@ -2275,13 +2467,7 @@ mod _ssl { // Try to get session data from context's session cache // IMPORTANT: Acquire and release locks quickly to avoid deadlock - let (context_identity, session_cache_arc) = { - let context = self.context.read(); - ( - context.context_identity.clone(), - context.client_session_cache.clone(), - ) - }; + let session_cache_arc = self.context.read().client_session_cache.clone(); let cached_session_data = if let Some(ref name) = server_name { let key = name.as_bytes().to_vec(); @@ -2327,7 +2513,7 @@ mod _ssl { _ => return, }; - let Some(client_config) = self.client_config.write().take() else { + let Some(config_snapshot) = self.client_config.write().take() else { return; }; let Some(session_store) = self.client_session_store.write().take() else { @@ -2335,8 +2521,9 @@ mod _ssl { }; let session = PySSLSession { - context_identity, - client_config, + context_identity: config_snapshot.context_identity, + policy_identity: config_snapshot.policy_identity, + client_config: config_snapshot.config, chain_builder: self .chain_builder .read() @@ -2594,7 +2781,7 @@ mod _ssl { Ok(()) })(); *self.state.lock() = TlsState::WaitingForClientHello(acceptor); - read?; + read.map_err(|error| self.remember_eof(error, vm))?; } TlsState::SendingAlert { error } => { let result = self.flush_pending_tls_output(vm, None); @@ -2602,7 +2789,7 @@ mod _ssl { error: error.clone(), }; result?; - return Err(error); + return Err(self.remember_eof(error, vm)); } state @ (TlsState::Handshaking | TlsState::Connected @@ -2784,41 +2971,36 @@ mod _ssl { } /// Helper: Prepare KX groups (ECDH curve) from context settings - fn prepare_kx_groups( - &self, - vm: &VirtualMachine, - ) -> PyResult>> { + fn prepare_kx_groups(&self) -> Option> { let ctx = self.context.read(); - let ecdh_curve = ctx.ecdh_curve.read().clone(); + let kx_groups = ctx.kx_groups.read().clone(); let suite_b_kx_groups = ctx.suite_b_kx_groups.read().clone(); drop(ctx); - if let Some(ref curve_name) = ecdh_curve { - match curve_name_to_kx_group(curve_name) { - Ok(groups) => Ok(Some(groups)), - Err(e) => Err(vm.new_value_error(format!("Failed to set ECDH curve: {e}"))), - } + if kx_groups.is_some() { + kx_groups } else { // A SUITEB* cipher string pins its own groups, and nothing // else asked for a curve. - Ok(suite_b_kx_groups) + suite_b_kx_groups } } /// Helper: Prepare all common protocol settings (versions, KX groups, ciphers, ALPN) - fn prepare_protocol_settings(&self, vm: &VirtualMachine) -> PyResult { + fn prepare_protocol_settings(&self) -> ProtocolSettings { let ctx = self.context.read(); let versions = self.prepare_tls_versions(); - let kx_groups = self.prepare_kx_groups(vm)?; + let kx_groups = self.prepare_kx_groups(); let cipher_suites = ctx.selected_ciphers.read().clone(); let alpn_protocols = ctx.alpn_protocols.read().clone(); - Ok(ProtocolSettings { + ProtocolSettings { versions, kx_groups, cipher_suites, + server_sigalgs: ctx.server_sigalgs.read().clone(), alpn_protocols, - }) + } } /// Initialize server-side TLS connection with configuration @@ -2838,6 +3020,7 @@ mod _ssl { vm: &VirtualMachine, ) -> PyResult<()> { let ctx = self.context.read(); + let policy_identity = ctx.policy_identity.read().clone(); let cert_keys = ctx.cert_keys.read(); if cert_keys.is_empty() { @@ -2855,7 +3038,7 @@ mod _ssl { drop(cert_keys); // Prepare common protocol settings (TLS versions, ECDH curve, cipher suites, ALPN) - let protocol_settings = self.prepare_protocol_settings(vm)?; + let protocol_settings = self.prepare_protocol_settings(); let min_ver = *ctx.minimum_version.read(); // Check if client certificate verification is required @@ -2888,8 +3071,9 @@ mod _ssl { // Certificate selection uses the actual ClientHello after SNI has // selected the context, including multi-certificate configurations. let cert_keys_only = cert_keys_clone.iter().map(|(ck, _)| ck.clone()).collect(); - let cert_resolver: Option> = - Some(Arc::new(MultiCertResolver::new(cert_keys_only))); + let cert_resolver: Option> = Some(Arc::new( + MultiCertResolver::new(cert_keys_only, protocol_settings.server_sigalgs.as_deref()), + )); // Extract cert_chain and private_key from first cert_key // @@ -2935,7 +3119,11 @@ mod _ssl { let cache_server_config = !use_deferred_validation && ctx.capath_state.read().directories.is_empty(); let cached_config_arc = if cache_server_config { - ctx.server_config.read().clone() + ctx.server_config + .read() + .as_ref() + .filter(|(policy, _)| Arc::ptr_eq(policy, &policy_identity)) + .map(|(_, config)| config.clone()) } else { None }; @@ -2944,12 +3132,14 @@ mod _ssl { let (mut config_arc, chain_builder) = if let Some(cached) = cached_config_arc { cached } else { - let config = - create_server_config(config_options).map_err(|e| vm.new_value_error(e))?; + let config = create_server_config(config_options).map_err(|e| { + vm.new_os_subtype_error(PySSLError::class(&vm.ctx).to_owned(), None, e) + .upcast() + })?; if cache_server_config { let ctx = self.context.read(); - *ctx.server_config.write() = Some(config.clone()); + *ctx.server_config.write() = Some((policy_identity, config.clone())); } config @@ -2965,11 +3155,10 @@ mod _ssl { *conn_guard = Some(TlsConnection::new(Connection::Server(conn))); *self.state.lock() = TlsState::Handshaking; } - Err((error, mut alert)) => { - let mut bytes = Vec::new(); - alert - .write_all(&mut bytes) - .map_err(|e| e.into_pyexception(vm))?; + Err((error, alert)) => { + let bytes = + rustpython_host_env::ssl::handshake::initial_handshake_alert(&error, alert) + .map_err(|e| e.into_pyexception(vm))?; self.reject_connection(SslError::from_rustls(error).into_py_err(vm), bytes, vm); self.accept_client_hello(vm)?; } @@ -2980,6 +3169,9 @@ mod _ssl { #[pymethod] fn do_handshake(zelf: &Py, vm: &VirtualMachine) -> PyResult<()> { + if zelf.got_eof_error.load(Ordering::Relaxed) { + return Err(create_ssl_eof_error(vm).upcast()); + } // Check if handshake already done if zelf.handshake_completed() { return Ok(()); @@ -3005,11 +3197,14 @@ mod _ssl { return zelf.accept_client_hello(vm).map(|_| ()); } } else { - // Client-side connection + // Client-side connection. Capture policy before reading any + // settings; mutations during construction invalidate this snapshot. let ctx = zelf.context.read(); + let policy_identity = ctx.policy_identity.read().clone(); + let has_capath = !ctx.capath_state.read().directories.is_empty(); // Prepare common protocol settings (TLS versions, ECDH curve, cipher suites, ALPN) - let protocol_settings = zelf.prepare_protocol_settings(vm)?; + let protocol_settings = zelf.prepare_protocol_settings(); // Clone values we need before building config let verify_mode = *ctx.verify_mode.read(); @@ -3060,23 +3255,32 @@ mod _ssl { }; let explicit_session = zelf.session.read().clone(); + let explicit_session = explicit_session + .as_ref() + .map(|session| { + let session = session + .downcast_ref::() + .ok_or_else(|| vm.new_type_error("Value is not a SSLSession."))?; + if !Arc::ptr_eq(&session.context_identity, &context_identity) { + return Err( + vm.new_value_error("Session refers to a different SSLContext.") + ); + } + Ok(session) + }) + .transpose()?; + let reusable_session = explicit_session.filter(|session| { + !has_capath + && Arc::ptr_eq(&session.policy_identity, &policy_identity) + && session.server_name.as_ref() == Some(&server_name) + }); let session_store = Arc::new(CapturingClientSessionStore::new(session_cache)); - let (mut config, chain_builder) = if let Some(session) = explicit_session { - let session = session - .downcast_ref::() - .ok_or_else(|| vm.new_type_error("Value is not a SSLSession."))?; - if !Arc::ptr_eq(&session.context_identity, &context_identity) { - return Err( - vm.new_value_error("Session refers to a different SSLContext.") - ); - } - if session.server_name.as_ref() == Some(&server_name) { - session.session_store.transfer_session( - &session_store, - &server_name, - session.kind, - ); - } + let (mut config, chain_builder) = if let Some(session) = reusable_session { + session.session_store.transfer_session( + &session_store, + &server_name, + session.kind, + ); let mut config = (*session.client_config).clone(); config.resumption = rustls::client::Resumption::store(session_store.clone()); @@ -3099,12 +3303,19 @@ mod _ssl { session_store: Some(session_store.clone()), crls: crls_clone, }; - create_client_config(config_options).map_err(|e| vm.new_value_error(e))? + create_client_config(config_options).map_err(|e| { + vm.new_os_subtype_error(PySSLError::class(&vm.ctx).to_owned(), None, e) + .upcast() + })? }; Arc::make_mut(&mut config).key_log = zelf.key_log.clone(); *zelf.chain_builder.write() = Some(chain_builder); - *zelf.client_config.write() = Some(config.clone()); + *zelf.client_config.write() = Some(ClientConfigSnapshot { + config: config.clone(), + context_identity, + policy_identity, + }); *zelf.client_session_store.write() = Some(session_store); let mut conn = ClientConnection::new(config, server_name).map_err(|e| { @@ -3131,7 +3342,7 @@ mod _ssl { zelf.reject_connection(error.into_py_err(vm), bytes, vm); return zelf.accept_client_hello(vm).map(|_| ()); } - handshake_result.map_err(|e| e.into_py_err(vm))?; + handshake_result.map_err(|e| zelf.remember_eof(e.into_py_err(vm), vm))?; if let Some(exc) = zelf.take_msg_exc() { return Err(exc); } @@ -3171,6 +3382,10 @@ mod _ssl { } } + if zelf.got_eof_error.load(Ordering::Relaxed) { + return Err(create_ssl_eof_error(vm).upcast()); + } + // return empty bytes immediately for len=0 if len == 0 { return match buffer { @@ -3243,7 +3458,11 @@ mod _ssl { let mut conn_guard = zelf.connection.lock(); let conn = match conn_guard.as_mut() { Some(conn) => conn, - None => return Err(create_ssl_eof_error(vm).upcast()), + None => { + return Err( + zelf.remember_eof(create_ssl_eof_error(vm).upcast(), vm) + ); + } }; conn.pending_plaintext() }; @@ -3256,7 +3475,7 @@ mod _ssl { } } // EOF occurred in violation of protocol (unexpected closure) - Err(create_ssl_eof_error(vm).upcast()) + Err(zelf.remember_eof(create_ssl_eof_error(vm).upcast(), vm)) } Err(error) if error.is_zero_return() => { // If plaintext is still buffered, return it before clean EOF. @@ -3302,12 +3521,12 @@ mod _ssl { Err(timeout_error_msg(vm, msg).upcast()) } Err(crate::ssl::compat::SslError::Py(e)) => { - // Python exception - pass through - Err(e) + // Exception from a transport operation. + Err(zelf.remember_eof(e, vm)) } Err(e) => { // Other SSL errors - Err(e.into_py_err(vm)) + Err(zelf.remember_eof(e.into_py_err(vm), vm)) } } } @@ -3326,6 +3545,9 @@ mod _ssl { #[pymethod] fn write(zelf: &Py, b: ArgBytesLike, vm: &VirtualMachine) -> PyResult { + if zelf.got_eof_error.load(Ordering::Relaxed) { + return Err(create_ssl_eof_error(vm).upcast()); + } let data_bytes = b.borrow_buf(); let data_len = data_bytes.len(); @@ -3372,7 +3594,7 @@ mod _ssl { Err(crate::ssl::compat::SslError::Timeout(msg)) => { Err(timeout_error_msg(vm, msg).upcast()) } - Err(e) => Err(e.into_py_err(vm)), + Err(e) => Err(zelf.remember_eof(e.into_py_err(vm), vm)), } } @@ -3441,6 +3663,55 @@ mod _ssl { )) } + #[pymethod] + fn group(zelf: &Py, vm: &VirtualMachine) -> PyResult> { + let group = { + let conn_guard = zelf.connection.lock(); + let Some(conn) = conn_guard.as_ref() else { + return Ok(None); + }; + let Some(group) = conn.inner().negotiated_key_exchange_group() else { + return Ok(None); + }; + group.name() + }; + group + .as_str() + .map(|name| Some(name.to_owned())) + .ok_or_else(|| { + vm.new_not_implemented_error(format!( + "The TLS provider has no name for negotiated group {:#06x}", + u16::from(group) + )) + }) + } + + #[pymethod] + fn client_sigalg(_zelf: &Py, vm: &VirtualMachine) -> PyResult> { + Err(vm.new_not_implemented_error( + "Rustls does not expose the selected client authentication signature algorithm", + )) + } + + #[pymethod] + fn server_sigalg(_zelf: &Py, vm: &VirtualMachine) -> PyResult> { + Err(vm.new_not_implemented_error( + "Rustls does not expose the selected server handshake signature algorithm", + )) + } + + #[pymethod] + fn uses_ktls_for_send(_zelf: &Py) -> bool { + // This transport encrypts records in rustls; it never enables kernel TLS. + false + } + + #[pymethod] + fn uses_ktls_for_recv(_zelf: &Py) -> bool { + // This transport decrypts records in rustls; it never enables kernel TLS. + false + } + #[pymethod] fn version(zelf: &Py) -> Option { // Extract cipher suite, releasing lock quickly @@ -4019,6 +4290,7 @@ mod _ssl { #[derive(Debug, PyPayload)] struct PySSLSession { context_identity: Arc<()>, + policy_identity: Arc<()>, client_config: Arc, chain_builder: Arc, session_store: Arc, @@ -4185,6 +4457,28 @@ mod _ssl { tuple.into() } + #[pyfunction] + fn get_sigalgs(vm: &VirtualMachine) -> PyResult { + // These are the TLS schemes advertised by the installed provider, in its + // preference order, rather than certificate signature algorithms. + let names = CryptoExt::get_provider() + .signature_verification_algorithms + .supported_schemes() + .into_iter() + .map(|scheme| { + let name = + rustpython_host_env::ssl::sigalg::scheme_name(scheme).ok_or_else(|| { + vm.new_not_implemented_error(format!( + "The TLS provider uses an unrecognized signature scheme {:#06x}", + u16::from(scheme) + )) + })?; + Ok(vm.ctx.new_str(name).into()) + }) + .collect::>>()?; + Ok(vm.ctx.new_list(names)) + } + #[pyfunction] fn RAND_status() -> i32 { 1 // The configured rustls provider supplies cryptographic randomness. diff --git a/crates/stdlib/src/ssl/compat.rs b/crates/stdlib/src/ssl/compat.rs index 09c180bca..87ca0211f 100644 --- a/crates/stdlib/src/ssl/compat.rs +++ b/crates/stdlib/src/ssl/compat.rs @@ -140,6 +140,7 @@ impl SslError { match self { Self::Rustls(error) => TlsError::from_rustls(error.clone()).is_eof(), Self::Eof => true, + Self::Io(error) => error.kind() == std::io::ErrorKind::UnexpectedEof, _ => false, } } diff --git a/crates/stdlib/src/ssl_wasm.rs b/crates/stdlib/src/ssl_wasm.rs index 21f46586c..4693af6ae 100644 --- a/crates/stdlib/src/ssl_wasm.rs +++ b/crates/stdlib/src/ssl_wasm.rs @@ -1,3 +1,5 @@ +// cspell: ignore sigalgs + //! Wasm `_ssl` on the rustls-free `rustpython_host_env::ssl` surface. //! //! MemoryBIO, protocol constants, OID lookup, hostname checks, and ALPN @@ -137,6 +139,8 @@ mod _ssl { #[pyattr] const HAS_PSK: bool = false; #[pyattr] + const HAS_PSK_TLS13: bool = false; + #[pyattr] const HAS_SSLv2: bool = false; #[pyattr] const HAS_SSLv3: bool = false; @@ -886,6 +890,11 @@ mod _ssl { .into() } + #[pyfunction] + fn get_sigalgs(vm: &VirtualMachine) -> PyResult { + Err(vm.new_not_implemented_error(UNAVAILABLE)) + } + #[pyfunction] fn RAND_status() -> i32 { 1