diff --git a/mitmproxy/cmdline.py b/mitmproxy/cmdline.py index b1b860f83..d7de350ff 100644 --- a/mitmproxy/cmdline.py +++ b/mitmproxy/cmdline.py @@ -434,6 +434,12 @@ def proxy_ssl_options(parser): action="store_true", dest="no_upstream_cert", help="Don't connect to upstream server to look up certificate details." ) + group.add_argument( + "--add-upstream-certs-to-client-chain", default=False, + action="store_true", dest="add_upstream_certs_to_client_chain", + help="Add all certificates of the upstream server to the certificate chain " + "that will be served to the proxy client, as extras." + ) group.add_argument( "--verify-upstream-cert", default=False, action="store_true", dest="ssl_verify_upstream_cert", diff --git a/mitmproxy/protocol/tls.py b/mitmproxy/protocol/tls.py index f014142b6..fc4be8307 100644 --- a/mitmproxy/protocol/tls.py +++ b/mitmproxy/protocol/tls.py @@ -432,6 +432,11 @@ class TlsLayer(Layer): self.log("Establish TLS with client", "debug") cert, key, chain_file = self._find_cert() + if self.config.add_upstream_certs_to_client_chain: + extra_certs = self.server_conn.server_certs + else: + extra_certs = None + try: self.client_conn.convert_to_ssl( cert, key, @@ -441,6 +446,7 @@ class TlsLayer(Layer): dhparams=self.config.certstore.dhparams, chain_file=chain_file, alpn_select_callback=self.__alpn_select_callback, + extra_chain_certs = extra_certs, ) # Some TLS clients will not fail the handshake, # but will immediately throw an "unexpected eof" error on the first read. diff --git a/mitmproxy/proxy/config.py b/mitmproxy/proxy/config.py index 149d47105..bd02c628c 100644 --- a/mitmproxy/proxy/config.py +++ b/mitmproxy/proxy/config.py @@ -67,6 +67,7 @@ class ProxyConfig: ssl_verify_upstream_cert=False, ssl_verify_upstream_trusted_cadir=None, ssl_verify_upstream_trusted_ca=None, + add_upstream_certs_to_client_chain=False, ): self.host = host self.port = port @@ -107,6 +108,7 @@ class ProxyConfig: self.openssl_verification_mode_server = SSL.VERIFY_NONE self.openssl_trusted_cadir_server = ssl_verify_upstream_trusted_cadir self.openssl_trusted_ca_server = ssl_verify_upstream_trusted_ca + self.add_upstream_certs_to_client_chain = add_upstream_certs_to_client_chain def process_proxy_options(parser, options): @@ -136,14 +138,26 @@ def process_proxy_options(parser, options): "Transparent, SOCKS5, reverse and upstream proxy mode " "are mutually exclusive. Read the docs on proxy modes to understand why." ) - + if options.add_upstream_certs_to_client_chain and options.no_upstream_cert: + return parser.error( + "The no-upstream-cert and add-upstream-certs-to-client-chain " + "options are mutually exclusive. If no-upstream-cert is enabled " + "then the upstream certificate is not retrieved before generating " + "the client certificate chain." + ) + if options.add_upstream_certs_to_client_chain and options.ssl_verify_upstream_cert: + return parser.error( + "The verify-upstream-cert and add-upstream-certs-to-client-chain " + "options are mutually exclusive. If upstream certificates are verified " + "then extra upstream certificates are not available for inclusion " + "to the client chain." + ) if options.clientcerts: options.clientcerts = os.path.expanduser(options.clientcerts) if not os.path.exists(options.clientcerts): return parser.error( - "Client certificate path does not exist: %s" % options.clientcerts + "Client certificate path does not exist: %s" % options.clientcerts ) - if options.auth_nonanonymous or options.auth_singleuser or options.auth_htpasswd: if options.transparent_proxy: @@ -206,5 +220,6 @@ def process_proxy_options(parser, options): ssl_version_server=options.ssl_version_server, ssl_verify_upstream_cert=options.ssl_verify_upstream_cert, ssl_verify_upstream_trusted_cadir=options.ssl_verify_upstream_trusted_cadir, - ssl_verify_upstream_trusted_ca=options.ssl_verify_upstream_trusted_ca + ssl_verify_upstream_trusted_ca=options.ssl_verify_upstream_trusted_ca, + add_upstream_certs_to_client_chain=options.add_upstream_certs_to_client_chain, ) diff --git a/netlib/tcp.py b/netlib/tcp.py index 574f38458..04aa868be 100644 --- a/netlib/tcp.py +++ b/netlib/tcp.py @@ -586,6 +586,7 @@ class TCPClient(_Connection): self.address = address self.source_address = source_address self.cert = None + self.server_certs = [] self.ssl_verification_error = None self.sni = None @@ -670,6 +671,10 @@ class TCPClient(_Connection): self.cert = certutils.SSLCert(self.connection.get_peer_certificate()) + # Keep all server certificates in a list + for i in self.connection.get_peer_cert_chain(): + self.server_certs.append(certutils.SSLCert(i)) + # Validate TLS Hostname try: crt = dict( @@ -737,6 +742,7 @@ class BaseHandler(_Connection): request_client_cert=None, chain_file=None, dhparams=None, + extra_chain_certs=None, **sslctx_kwargs): """ cert: A certutils.SSLCert object or the path to a certificate @@ -772,6 +778,10 @@ class BaseHandler(_Connection): else: context.use_certificate_chain_file(cert) + if extra_chain_certs: + for i in extra_chain_certs: + context.add_extra_chain_cert(i.x509) + if handle_sni: # SNI callback happens during do_handshake() context.set_tlsext_servername_callback(handle_sni) diff --git a/pathod/pathoc.py b/pathod/pathoc.py index c0a33b628..64a81c945 100644 --- a/pathod/pathoc.py +++ b/pathod/pathoc.py @@ -42,7 +42,8 @@ class SSLInfo(object): "Cipher: %s, %s bit, %s" % self.cipher, "SSL certificate chain:" ] - for i in self.certchain: + for n,i in enumerate(self.certchain): + parts.append(" Certificate [%s]" % n) parts.append("\tSubject: ") for cn in i.get_subject().get_components(): parts.append("\t\t%s=%s" % cn) @@ -69,7 +70,7 @@ class SSLInfo(object): s = certutils.SSLCert(i) if s.altnames: parts.append("\tSANs: %s" % " ".join(s.altnames)) - return "\n".join(parts) + return "\n".join(parts) diff --git a/test/mitmproxy/test_server.py b/test/mitmproxy/test_server.py index d7b23bbb8..26e53e8ae 100644 --- a/test/mitmproxy/test_server.py +++ b/test/mitmproxy/test_server.py @@ -999,3 +999,43 @@ class TestProxyChainingSSLReconnect(tservers.HTTPUpstreamProxyTest): # (both terminated) # nothing happened here assert self.chain[1].tmaster.state.flow_count() == 2 + + +class AddUpstreamCertsToClientChainMixin: + + ssl = True + servercert = tutils.test_data.path("data/trusted-server.crt") + ssloptions = pathod.SSLOptions( + cn="trusted-cert", + certs=[ + ("trusted-cert", servercert) + ] + ) + + def test_add_upstream_certs_to_client_chain(self): + with open(self.servercert, "rb") as f: + d = f.read() + upstreamCert = SSLCert.from_pem(d) + p = self.pathoc() + upstream_cert_found_in_client_chain = False + for receivedCert in p.server_certs: + if receivedCert.digest('sha256') == upstreamCert.digest('sha256'): + upstream_cert_found_in_client_chain = True + break + assert(upstream_cert_found_in_client_chain == self.add_upstream_certs_to_client_chain) + + +class TestHTTPSAddUpstreamCertsToClientChainTrue(AddUpstreamCertsToClientChainMixin, tservers.HTTPProxyTest): + + """ + If --add-server-certs-to-client-chain is True, then the client should receive the upstream server's certificates + """ + add_upstream_certs_to_client_chain = True + + +class TestHTTPSAddUpstreamCertsToClientChainFalse(AddUpstreamCertsToClientChainMixin, tservers.HTTPProxyTest): + + """ + If --add-server-certs-to-client-chain is False, then the client should not receive the upstream server's certificates + """ + add_upstream_certs_to_client_chain = False diff --git a/test/mitmproxy/tservers.py b/test/mitmproxy/tservers.py index b7b5de9e8..4fa519cc6 100644 --- a/test/mitmproxy/tservers.py +++ b/test/mitmproxy/tservers.py @@ -86,6 +86,7 @@ class ProxyTestBase(object): no_upstream_cert = False authenticator = None masterclass = TestMaster + add_upstream_certs_to_client_chain = False @classmethod def setup_class(cls): @@ -129,6 +130,7 @@ class ProxyTestBase(object): no_upstream_cert = cls.no_upstream_cert, cadir = cls.cadir, authenticator = cls.authenticator, + add_upstream_certs_to_client_chain = cls.add_upstream_certs_to_client_chain, )