From f2500dd0aea198a7103865b2b38738248e632620 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Sat, 31 Aug 2024 09:02:12 +0200 Subject: [PATCH] Warn if TLS version is unsupported by OpenSSL (#7139) * warn if TLS version is unsupported by OpenSSL fix #7138 * [autofix.ci] apply automated fixes * coverage++ --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- CHANGELOG.md | 3 +++ mitmproxy/addons/tlsconfig.py | 32 ++++++++++++++++++++++++- mitmproxy/net/tls.py | 17 +++++++++++++ test/mitmproxy/addons/test_tlsconfig.py | 24 +++++++++++++++++++ test/mitmproxy/net/test_tls.py | 8 +++++++ 5 files changed, 83 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d70bed43d..089d2ee25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,9 @@ ([#7073](https://github.com/mitmproxy/mitmproxy/pull/7073), @mhils) - Fix a bug where fragmented QUIC client hellos were not handled properly. ([#7067](https://github.com/mitmproxy/mitmproxy/pull/7067), @errorxyz) +- Emit a warning when users configure a TLS version that is not supported by the + current OpenSSL build. + ([#7139](https://github.com/mitmproxy/mitmproxy/pull/7139), @mhils) - Fix a bug where mitmproxy would crash when receiving `STOP_SENDING` QUIC frames. ([#7119](https://github.com/mitmproxy/mitmproxy/pull/7119), @mhils) - mitmproxy now officially supports Python 3.13. diff --git a/mitmproxy/addons/tlsconfig.py b/mitmproxy/addons/tlsconfig.py index 20d6aa8e1..c530a7c5d 100644 --- a/mitmproxy/addons/tlsconfig.py +++ b/mitmproxy/addons/tlsconfig.py @@ -24,6 +24,8 @@ from mitmproxy.proxy.layers import modes from mitmproxy.proxy.layers import quic from mitmproxy.proxy.layers import tls as proxy_tls +logger = logging.getLogger(__name__) + # We manually need to specify this, otherwise OpenSSL may select a non-HTTP2 cipher by default. # https://ssl-config.mozilla.org/#config=old @@ -441,7 +443,7 @@ class TlsConfig: else None, ) if self.certstore.default_ca.has_expired(): - logging.warning( + logger.warning( "The mitmproxy certificate authority has expired!\n" "Please delete all CA-related files in your ~/.mitmproxy folder.\n" "The CA will be regenerated automatically after restarting mitmproxy.\n" @@ -484,6 +486,34 @@ class TlsConfig: f"Invalid ECDH curve: {ecdh_curve!r}" ) from e + if "tls_version_client_min" in updated: + self._warn_unsupported_version("tls_version_client_min", True) + if "tls_version_client_max" in updated: + self._warn_unsupported_version("tls_version_client_max", False) + if "tls_version_server_min" in updated: + self._warn_unsupported_version("tls_version_server_min", True) + if "tls_version_server_max" in updated: + self._warn_unsupported_version("tls_version_server_max", False) + + def _warn_unsupported_version(self, attribute: str, warn_unbound: bool): + val = net_tls.Version[getattr(ctx.options, attribute)] + supported_versions = [ + v for v in net_tls.Version if net_tls.is_supported_version(v) + ] + supported_versions_str = ", ".join(v.name for v in supported_versions) + + if val is net_tls.Version.UNBOUNDED: + if warn_unbound: + logger.info( + f"{attribute} has been set to {val.name}. Note that your " + f"OpenSSL build only supports the following TLS versions: {supported_versions_str}" + ) + elif val not in supported_versions: + logger.warning( + f"{attribute} has been set to {val.name}, which is not supported by the current OpenSSL build. " + f"The current build only supports the following versions: {supported_versions_str}" + ) + def get_cert(self, conn_context: context.Context) -> certs.CertStoreEntry: """ This function determines the Common Name (CN), Subject Alternative Names (SANs) and Organization Name diff --git a/mitmproxy/net/tls.py b/mitmproxy/net/tls.py index 21b4754a4..e844dc141 100644 --- a/mitmproxy/net/tls.py +++ b/mitmproxy/net/tls.py @@ -3,6 +3,7 @@ import threading from collections.abc import Callable from collections.abc import Iterable from enum import Enum +from functools import cache from functools import lru_cache from pathlib import Path from typing import Any @@ -58,6 +59,22 @@ DEFAULT_MAX_VERSION = Version.UNBOUNDED DEFAULT_OPTIONS = SSL.OP_CIPHER_SERVER_PREFERENCE | SSL.OP_NO_COMPRESSION +@cache +def is_supported_version(version: Version): + client_ctx = SSL.Context(SSL.TLS_CLIENT_METHOD) + client_ctx.set_min_proto_version(version.value) + client_ctx.set_max_proto_version(version.value) + client_conn = SSL.Connection(client_ctx) + client_conn.set_connect_state() + + try: + client_conn.recv(4096) + except SSL.WantReadError: + return True + except SSL.Error: + return False + + class MasterSecretLogger: def __init__(self, filename: Path): self.filename = filename.expanduser() diff --git a/test/mitmproxy/addons/test_tlsconfig.py b/test/mitmproxy/addons/test_tlsconfig.py index 06aa900ad..6fe97a71f 100644 --- a/test/mitmproxy/addons/test_tlsconfig.py +++ b/test/mitmproxy/addons/test_tlsconfig.py @@ -1,4 +1,5 @@ import ipaddress +import logging import ssl import time from pathlib import Path @@ -107,6 +108,29 @@ class TestTlsConfig: ) assert ta.certstore.certs + def test_configure_tls_version(self, caplog): + caplog.set_level(logging.INFO) + ta = tlsconfig.TlsConfig() + with taddons.context(ta) as tctx: + for attr in [ + "tls_version_client_min", + "tls_version_client_max", + "tls_version_server_min", + "tls_version_server_max", + ]: + caplog.clear() + tctx.configure(ta, **{attr: "SSL3"}) + assert ( + f"{attr} has been set to SSL3, " + "which is not supported by the current OpenSSL build." + ) in caplog.text + caplog.clear() + tctx.configure(ta, tls_version_client_min="UNBOUNDED") + assert ( + "tls_version_client_min has been set to UNBOUNDED. " + "Note that your OpenSSL build only supports the following TLS versions" + ) in caplog.text + def test_get_cert(self, tdata): """Test that we generate a certificate matching the connection's context.""" ta = tlsconfig.TlsConfig() diff --git a/test/mitmproxy/net/test_tls.py b/test/mitmproxy/net/test_tls.py index 58e97b3de..4af691800 100644 --- a/test/mitmproxy/net/test_tls.py +++ b/test/mitmproxy/net/test_tls.py @@ -1,5 +1,6 @@ from pathlib import Path +import pytest from OpenSSL import crypto from OpenSSL import SSL @@ -7,6 +8,13 @@ from mitmproxy import certs from mitmproxy.net import tls +@pytest.mark.parametrize("version", [tls.Version.UNBOUNDED, tls.Version.SSL3]) +def test_supported(version): + # wild assumption: test environments should not do SSLv3 by default. + expected_support = version is tls.Version.UNBOUNDED + assert tls.is_supported_version(version) == expected_support + + def test_make_master_secret_logger(): assert tls.make_master_secret_logger(None) is None assert isinstance(tls.make_master_secret_logger("filepath"), tls.MasterSecretLogger)