Repository: mitmproxy/mitmproxy

Issue:
TLS-over-TLS does not play nice with Firefox
#### Problem Description

Mitmproxy's new HTTPS proxy functionality does not seem to work well with Firefox.

1. I am getting certificate errors in Firefox when specifying https://127.0.0.1 as the proxy address (mitmproxy: `The client does not trust the proxy's certificate for <no address> (sslv3 alert bad certificate)`)
2. When switching to https://localhost, page load hangs infinitely and mitmproxy complains about ` Invalid HTTP/2 request headers: Required pseudo header is missing: b':scheme'`.

The workaround that works for me is to specify `https://localhost:8080` as the proxy and run mitmproxy with `--no-http2`, or to run mitmproxy as a regular HTTP (not HTTPS) proxy, which of course still works with TLS sites.

#### System Information

```
Mitmproxy: 7.0.0
Python:    3.9.4
OpenSSL:   OpenSSL 1.1.1k  25 Mar 2021
Platform:  Linux-4.4.0-19041-Microsoft-x86_64-with-glibc2.31
```


Discussion:

Resolving patch:
diff --git a/mitmproxy/addons/tlsconfig.py b/mitmproxy/addons/tlsconfig.py
--- a/mitmproxy/addons/tlsconfig.py
+++ b/mitmproxy/addons/tlsconfig.py
@@ -8,10 +8,11 @@
 from mitmproxy.net import tls as net_tls
 from mitmproxy.options import CONF_BASENAME
 from mitmproxy.proxy import context
-from mitmproxy.proxy.layers import tls
+from mitmproxy.proxy.layers import tls, modes
 
 # 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 = (
     'ECDHE-ECDSA-AES128-GCM-SHA256', 'ECDHE-RSA-AES128-GCM-SHA256', 'ECDHE-ECDSA-AES256-GCM-SHA384',
     'ECDHE-RSA-AES256-GCM-SHA384', 'ECDHE-ECDSA-CHACHA20-POLY1305', 'ECDHE-RSA-CHACHA20-POLY1305',
@@ -118,13 +119,8 @@ def tls_clienthello(self, tls_clienthello: tls.ClientHelloData):
                 )
         )
 
-    def tls_start_client(self, tls_start: tls.TlsStartData):
-        self.create_client_proxy_ssl_conn(tls_start)
-
-    def tls_start_server(self, tls_start: tls.TlsStartData):
-        self.create_proxy_server_ssl_conn(tls_start)
-
-    def create_client_proxy_ssl_conn(self, tls_start: tls.TlsStartData) -> None:
+    def tls_start_client(self, tls_start: tls.TlsStartData) -> None:
+        """Establish TLS between client and proxy."""
         client: connection.Client = tls_start.context.client
         server: connection.Server = tls_start.context.server
 
@@ -154,14 +150,24 @@ def create_client_proxy_ssl_conn(self, tls_start: tls.TlsStartData) -> None:
             dhparams=self.certstore.dhparams,
         )
         tls_start.ssl_conn = SSL.Connection(ssl_ctx)
+
+        # Force HTTP/1 for secure web proxies, we currently don't support CONNECT over HTTP/2.
+        # There is a proof-of-concept branch at https://github.com/mhils/mitmproxy/tree/http2-proxy,
+        # but the complexity outweighs the benefits for now.
+        if len(tls_start.context.layers) == 2 and isinstance(tls_start.context.layers[0], modes.HttpProxy):
+            client_alpn: Optional[bytes] = b"http/1.1"
+        else:
+            client_alpn = client.alpn
+
         tls_start.ssl_conn.set_app_data(AppData(
-            client_alpn=client.alpn,
+            client_alpn=client_alpn,
             server_alpn=server.alpn,
             http2=ctx.options.http2,
         ))
         tls_start.ssl_conn.set_accept_state()
 
-    def create_proxy_server_ssl_conn(self, tls_start: tls.TlsStartData) -> None:
+    def tls_start_server(self, tls_start: tls.TlsStartData) -> None:
+        """Establish TLS between proxy and server."""
         client: connection.Client = tls_start.context.client
         server: connection.Server = tls_start.context.server
         assert server.address
@@ -296,9 +302,10 @@ def get_cert(self, conn_context: context.Context) -> certs.CertStoreEntry:
         elif conn_context.server.address:
             altnames.append(conn_context.server.address[0])
 
-        # As a last resort, add *something* so that we have a certificate to serve.
+        # As a last resort, add our local IP address. This may be necessary for HTTPS Proxies which are addressed
+        # via IP. Here we neither have an upstream cert, nor can an IP be included in the server name indication.
         if not altnames:
-            altnames.append("mitmproxy")
+            altnames.append(conn_context.client.sockname[0])
 
         # only keep first occurrence of each hostname
         altnames = list(dict.fromkeys(altnames))
diff --git a/mitmproxy/certs.py b/mitmproxy/certs.py
--- a/mitmproxy/certs.py
+++ b/mitmproxy/certs.py
@@ -49,6 +49,9 @@ def __init__(self, cert: x509.Certificate):
     def __eq__(self, other):
         return self.fingerprint() == other.fingerprint()
 
+    def __repr__(self):
+        return f"<Cert(cn={self.cn!r}, altnames={self.altnames!r})>"
+
     @classmethod
     def from_state(cls, state):
         return cls.from_pem(state)
diff --git a/mitmproxy/proxy/layers/tls.py b/mitmproxy/proxy/layers/tls.py
--- a/mitmproxy/proxy/layers/tls.py
+++ b/mitmproxy/proxy/layers/tls.py
@@ -346,6 +346,23 @@ class ClientTLSLayer(_TLSLayer):
     client_hello_parsed: bool = False
 
     def __init__(self, context: context.Context):
+        if context.client.tls:
+            # In the case of TLS-over-TLS, we already have client TLS. As the outer TLS connection between client
+            # and proxy isn't that interesting to us, we just unset the attributes here and keep the inner TLS
+            # session's attributes.
+            # Alternatively we could create a new Client instance,
+            # but for now we keep it simple. There is a proof-of-concept at
+            # https://github.com/mitmproxy/mitmproxy/commit/9b6e2a716888b7787514733b76a5936afa485352.
+            context.client.alpn = None
+            context.client.cipher = None
+            context.client.sni = None
+            context.client.timestamp_tls_setup = None
+            context.client.tls_version = None
+            context.client.certificate_list = []
+            context.client.mitmcert = None
+            context.client.alpn_offers = []
+            context.client.cipher_list = []
+
         super().__init__(context, context.client)
         self.server_tls_available = isinstance(self.context.layers[-2], ServerTLSLayer)
         self.recv_buffer = bytearray()


Test patch:
diff --git a/test/mitmproxy/addons/test_tlsconfig.py b/test/mitmproxy/addons/test_tlsconfig.py
--- a/test/mitmproxy/addons/test_tlsconfig.py
+++ b/test/mitmproxy/addons/test_tlsconfig.py
@@ -9,7 +9,7 @@
 from mitmproxy import certs, connection
 from mitmproxy.addons import tlsconfig
 from mitmproxy.proxy import context
-from mitmproxy.proxy.layers import tls
+from mitmproxy.proxy.layers import modes, tls
 from mitmproxy.test import taddons
 from test.mitmproxy.proxy.layers import test_tls
 
@@ -66,9 +66,10 @@ def test_get_cert(self, tdata):
 
             ctx = context.Context(connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), tctx.options)
 
-            # Edge case first: We don't have _any_ idea about the server, so we just return "mitmproxy" as subject.
+            # Edge case first: We don't have _any_ idea about the server nor is there a SNI,
+            # so we just return our local IP as subject.
             entry = ta.get_cert(ctx)
-            assert entry.cert.cn == "mitmproxy"
+            assert entry.cert.cn == "127.0.0.1"
 
             # Here we have an existing server connection...
             ctx.server.address = ("server-address.example", 443)
@@ -114,7 +115,7 @@ def do_handshake(
 
         return True
 
-    def test_create_client_proxy_ssl_conn(self, tdata):
+    def test_tls_start_client(self, tdata):
         ta = tlsconfig.TlsConfig()
         with taddons.context(ta) as tctx:
             ta.configure(["confdir"])
@@ -132,7 +133,7 @@ def test_create_client_proxy_ssl_conn(self, tdata):
             assert self.do_handshake(tssl_client, tssl_server)
             assert tssl_client.obj.getpeercert()["subjectAltName"] == (("DNS", "example.mitmproxy.org"),)
 
-    def test_create_proxy_server_ssl_conn_verify_failed(self):
+    def test_tls_start_server_verify_failed(self):
         ta = tlsconfig.TlsConfig()
         with taddons.context(ta) as tctx:
             ctx = context.Context(connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), tctx.options)
@@ -147,7 +148,7 @@ def test_create_proxy_server_ssl_conn_verify_failed(self):
             with pytest.raises(SSL.Error, match="certificate verify failed"):
                 assert self.do_handshake(tssl_client, tssl_server)
 
-    def test_create_proxy_server_ssl_conn_verify_ok(self, tdata):
+    def test_tls_start_server_verify_ok(self, tdata):
         ta = tlsconfig.TlsConfig()
         with taddons.context(ta) as tctx:
             ctx = context.Context(connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), tctx.options)
@@ -161,7 +162,7 @@ def test_create_proxy_server_ssl_conn_verify_ok(self, tdata):
             tssl_server = test_tls.SSLTest(server_side=True)
             assert self.do_handshake(tssl_client, tssl_server)
 
-    def test_create_proxy_server_ssl_conn_insecure(self):
+    def test_tls_start_server_insecure(self):
         ta = tlsconfig.TlsConfig()
         with taddons.context(ta) as tctx:
             ctx = context.Context(connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), tctx.options)
@@ -203,6 +204,24 @@ def assert_alpn(http2, client_offers, expected):
             # see comment in tlsconfig.py
             assert_alpn(True, [], [])
 
+    def test_no_h2_proxy(self, tdata):
+        """Do not negotiate h2 on the client<->proxy connection in secure web proxy mode,
+        https://github.com/mitmproxy/mitmproxy/issues/4689"""
+
+        ta = tlsconfig.TlsConfig()
+        with taddons.context(ta) as tctx:
+            tctx.configure(ta, certs=[tdata.path("mitmproxy/net/data/verificationcerts/trusted-leaf.pem")])
+
+            ctx = context.Context(connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), tctx.options)
+            # mock up something that looks like a secure web proxy.
+            ctx.layers = [
+                modes.HttpProxy(ctx),
+                123
+            ]
+            tls_start = tls.TlsStartData(ctx.client, context=ctx)
+            ta.tls_start_client(tls_start)
+            assert tls_start.ssl_conn.get_app_data()["client_alpn"] == b"http/1.1"
+
     @pytest.mark.parametrize(
         "client_certs",
         [
diff --git a/test/mitmproxy/test_certs.py b/test/mitmproxy/test_certs.py
--- a/test/mitmproxy/test_certs.py
+++ b/test/mitmproxy/test_certs.py
@@ -180,6 +180,7 @@ def test_simple(self, tdata):
         assert c2.issuer
         assert c2.to_pem()
         assert c2.has_expired() is not None
+        assert repr(c2) == "<Cert(cn='www.inode.co.nz', altnames=['www.inode.co.nz', 'inode.co.nz'])>"
 
         assert c1 != c2