Set @SECLEVEL=0 if old TLS versions are requested (#7241)

* set `@SECLEVEL=0` if old TLS versions are requested

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Maximilian Hils 2024-10-14 19:45:48 +02:00 committed by GitHub
parent 99aa0a70af
commit 93649e8baf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 128 additions and 19 deletions

View File

@ -11,6 +11,9 @@
([#7242](https://github.com/mitmproxy/mitmproxy/pull/7242), @mhils)
- Tighten HTTP detection heuristic to better support custom TCP-based protocols.
([#7228](https://github.com/mitmproxy/mitmproxy/pull/7228), @fatanugraha)
- Fix a bug where mitmproxy would incorrectly report that TLS 1.0 and 1.1 are not supported
with the current OpenSSL build.
([#7241](https://github.com/mitmproxy/mitmproxy/pull/7241), @mhils)
## 02 October 2024: mitmproxy 11.0.0

View File

@ -4,6 +4,7 @@ import os
import ssl
from pathlib import Path
from typing import Any
from typing import Literal
from typing import TypedDict
from aioquic.h3.connection import H3_ALPN
@ -29,7 +30,7 @@ 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
DEFAULT_CIPHERS = (
_DEFAULT_CIPHERS = (
"ECDHE-ECDSA-AES128-GCM-SHA256",
"ECDHE-RSA-AES128-GCM-SHA256",
"ECDHE-ECDSA-AES256-GCM-SHA384",
@ -58,6 +59,22 @@ DEFAULT_CIPHERS = (
"DES-CBC3-SHA",
)
_DEFAULT_CIPHERS_WITH_SECLEVEL_0 = ("@SECLEVEL=0", *_DEFAULT_CIPHERS)
def _default_ciphers(
min_tls_version: net_tls.Version,
) -> tuple[str, ...]:
"""
@SECLEVEL=0 is necessary for TLS 1.1 and below to work,
see https://github.com/pyca/cryptography/issues/9523
"""
if min_tls_version in net_tls.INSECURE_TLS_MIN_VERSIONS:
return _DEFAULT_CIPHERS_WITH_SECLEVEL_0
else:
return _DEFAULT_CIPHERS
# 2022/05: X509_CHECK_FLAG_NEVER_CHECK_SUBJECT is not available in LibreSSL, ignore gracefully as it's not critical.
DEFAULT_HOSTFLAGS = (
SSL._lib.X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS # type: ignore
@ -109,8 +126,6 @@ class TlsConfig:
# TODO: This addon should manage the following options itself, which are current defined in mitmproxy/options.py:
# - upstream_cert
# - add_upstream_certs_to_client_chain
# - ciphers_client
# - ciphers_server
# - key_size
# - certs
# - cert_passphrase
@ -118,12 +133,17 @@ class TlsConfig:
# - ssl_verify_upstream_trusted_confdir
def load(self, loader):
insecure_tls_min_versions = (
", ".join(x.name for x in net_tls.INSECURE_TLS_MIN_VERSIONS[:-1])
+ f" and {net_tls.INSECURE_TLS_MIN_VERSIONS[-1].name}"
)
loader.add_option(
name="tls_version_client_min",
typespec=str,
default=net_tls.DEFAULT_MIN_VERSION.name,
choices=[x.name for x in net_tls.Version],
help=f"Set the minimum TLS version for client connections.",
help=f"Set the minimum TLS version for client connections. "
f"{insecure_tls_min_versions} are insecure.",
)
loader.add_option(
name="tls_version_client_max",
@ -137,7 +157,8 @@ class TlsConfig:
typespec=str,
default=net_tls.DEFAULT_MIN_VERSION.name,
choices=[x.name for x in net_tls.Version],
help=f"Set the minimum TLS version for server connections.",
help=f"Set the minimum TLS version for server connections. "
f"{insecure_tls_min_versions} are insecure.",
)
loader.add_option(
name="tls_version_server_max",
@ -166,6 +187,18 @@ class TlsConfig:
default=False,
help=f"Requests a client certificate (TLS message 'CertificateRequest') to establish a mutual TLS connection between client and mitmproxy (combined with 'client_certs' option for mitmproxy and upstream).",
)
loader.add_option(
"ciphers_client",
str | None,
None,
"Set supported ciphers for client <-> mitmproxy connections using OpenSSL syntax.",
)
loader.add_option(
"ciphers_server",
str | None,
None,
"Set supported ciphers for mitmproxy <-> server connections using OpenSSL syntax.",
)
def tls_clienthello(self, tls_clienthello: tls.ClientHelloData):
conn_context = tls_clienthello.context
@ -188,7 +221,9 @@ class TlsConfig:
if not client.cipher_list and ctx.options.ciphers_client:
client.cipher_list = ctx.options.ciphers_client.split(":")
# don't assign to client.cipher_list, doesn't need to be stored.
cipher_list = client.cipher_list or DEFAULT_CIPHERS
cipher_list = client.cipher_list or _default_ciphers(
net_tls.Version[ctx.options.tls_version_client_min]
)
if ctx.options.add_upstream_certs_to_client_chain: # pragma: no cover
# exempted from coverage until https://bugs.python.org/issue18233 is fixed.
@ -278,7 +313,9 @@ class TlsConfig:
if not server.cipher_list and ctx.options.ciphers_server:
server.cipher_list = ctx.options.ciphers_server.split(":")
# don't assign to client.cipher_list, doesn't need to be stored.
cipher_list = server.cipher_list or DEFAULT_CIPHERS
cipher_list = server.cipher_list or _default_ciphers(
net_tls.Version[ctx.options.tls_version_server_min]
)
client_cert: str | None = None
if ctx.options.client_certs:
@ -500,6 +537,10 @@ class TlsConfig:
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)
if "tls_version_client_min" in updated or "ciphers_client" in updated:
self._warn_seclevel_missing("client")
if "tls_version_server_min" in updated or "ciphers_server" in updated:
self._warn_seclevel_missing("server")
def _warn_unsupported_version(self, attribute: str, warn_unbound: bool):
val = net_tls.Version[getattr(ctx.options, attribute)]
@ -520,6 +561,28 @@ class TlsConfig:
f"The current build only supports the following versions: {supported_versions_str}"
)
def _warn_seclevel_missing(self, side: Literal["client", "server"]) -> None:
"""
OpenSSL cipher spec need to specify @SECLEVEL for old TLS versions to work,
see https://github.com/pyca/cryptography/issues/9523.
"""
if side == "client":
custom_ciphers = ctx.options.ciphers_client
min_tls_version = ctx.options.tls_version_client_min
else:
custom_ciphers = ctx.options.ciphers_server
min_tls_version = ctx.options.tls_version_server_min
if (
custom_ciphers
and net_tls.Version[min_tls_version] in net_tls.INSECURE_TLS_MIN_VERSIONS
and "@SECLEVEL=0" not in custom_ciphers
):
logger.warning(
f'With tls_version_{side}_min set to {min_tls_version}, ciphers_{side} must include "@SECLEVEL=0" '
f"for insecure TLS versions to work."
)
def get_cert(self, conn_context: context.Context) -> certs.CertStoreEntry:
"""
This function determines the Common Name (CN), Subject Alternative Names (SANs) and Organization Name

View File

@ -49,6 +49,14 @@ class Version(Enum):
TLS1_3 = SSL.TLS1_3_VERSION
INSECURE_TLS_MIN_VERSIONS: tuple[Version, ...] = (
Version.UNBOUNDED,
Version.SSL3,
Version.TLS1,
Version.TLS1_1,
)
class Verify(Enum):
VERIFY_NONE = SSL.VERIFY_NONE
VERIFY_PEER = SSL.VERIFY_PEER
@ -62,6 +70,9 @@ 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)
# Without SECLEVEL, recent OpenSSL versions forbid old TLS versions.
# https://github.com/pyca/cryptography/issues/9523
client_ctx.set_cipher_list(b"@SECLEVEL=0:ALL")
client_ctx.set_min_proto_version(version.value)
client_ctx.set_max_proto_version(version.value)
client_conn = SSL.Connection(client_ctx)

View File

@ -72,18 +72,6 @@ class Options(optmanager.OptManager):
process list. Specify it in config.yaml to avoid this.
""",
)
self.add_option(
"ciphers_client",
Optional[str],
None,
"Set supported ciphers for client <-> mitmproxy connections using OpenSSL syntax.",
)
self.add_option(
"ciphers_server",
Optional[str],
None,
"Set supported ciphers for mitmproxy <-> server connections using OpenSSL syntax.",
)
self.add_option(
"client_certs", Optional[str], None, "Client certificate file or directory."
)

View File

@ -13,6 +13,7 @@ from mitmproxy import connection
from mitmproxy import options
from mitmproxy import tls
from mitmproxy.addons import tlsconfig
from mitmproxy.net import tls as net_tls
from mitmproxy.proxy import context
from mitmproxy.proxy.layers import modes
from mitmproxy.proxy.layers import quic
@ -131,6 +132,35 @@ class TestTlsConfig:
"Note that your OpenSSL build only supports the following TLS versions"
) in caplog.text
def test_configure_ciphers(self, caplog):
caplog.set_level(logging.INFO)
ta = tlsconfig.TlsConfig()
with taddons.context(ta) as tctx:
tctx.configure(
ta,
tls_version_client_min="TLS1",
ciphers_client="ALL",
)
assert (
"With tls_version_client_min set to TLS1, "
'ciphers_client must include "@SECLEVEL=0" for insecure TLS versions to work.'
) in caplog.text
caplog.clear()
tctx.configure(
ta,
ciphers_server="ALL",
)
assert not caplog.text
tctx.configure(
ta,
tls_version_server_min="SSL3",
)
assert (
"With tls_version_server_min set to SSL3, "
'ciphers_server must include "@SECLEVEL=0" for insecure TLS versions to work.'
) in caplog.text
def test_get_cert(self, tdata):
"""Test that we generate a certificate matching the connection's context."""
ta = tlsconfig.TlsConfig()
@ -481,3 +511,17 @@ class TestTlsConfig:
with taddons.context(ta):
ta.configure(["confdir"])
assert "The mitmproxy certificate authority has expired" in caplog.text
def test_default_ciphers():
assert (
tlsconfig._default_ciphers(net_tls.Version.TLS1_3) == tlsconfig._DEFAULT_CIPHERS
)
assert (
tlsconfig._default_ciphers(net_tls.Version.SSL3)
== tlsconfig._DEFAULT_CIPHERS_WITH_SECLEVEL_0
)
assert (
tlsconfig._default_ciphers(net_tls.Version.UNBOUNDED)
== tlsconfig._DEFAULT_CIPHERS_WITH_SECLEVEL_0
)