diff --git a/docs/certinstall.rst b/docs/certinstall.rst index 5ec7b7ce9..5a8cce64f 100644 --- a/docs/certinstall.rst +++ b/docs/certinstall.rst @@ -175,10 +175,21 @@ no such file exists, it will be generated automatically. Using a client side certificate ------------------------------- -You can use a client certificate by passing the ``--client-certs DIRECTORY`` option to mitmproxy. +You can use a client certificate by passing the ``--client-certs DIRECTORY|FILE`` +option to mitmproxy. Using a directory allows certs to be selected based on +hostname, while using a filename allows a single specific certificate to be used for +all SSL connections. Certificate files must be in the PEM format and should +contain both the unencrypted private key and the certificate. + +Multiple certs by Hostname +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If you've specified a directory to ``--client-certs``, then the following +behavior will be taken: + If you visit example.org, mitmproxy looks for a file named ``example.org.pem`` in the specified -directory and uses this as the client cert. The certificate file needs to be in the PEM format and -should contain both the unencrypted private key and the certificate. +directory and uses this as the client cert. + .. _Certificate Pinning: http://security.stackexchange.com/questions/29988/what-is-certificate-pinning/ \ No newline at end of file diff --git a/libmproxy/cmdline.py b/libmproxy/cmdline.py index 166784867..99b76e681 100644 --- a/libmproxy/cmdline.py +++ b/libmproxy/cmdline.py @@ -407,7 +407,7 @@ def proxy_ssl_options(parser): group.add_argument( "--client-certs", action="store", type=str, dest="clientcerts", default=None, - help="Client certificate directory." + help="Client certificate file or directory." ) group.add_argument( "--no-upstream-cert", default=False, diff --git a/libmproxy/models/connections.py b/libmproxy/models/connections.py index 3aa522eac..0991955d9 100644 --- a/libmproxy/models/connections.py +++ b/libmproxy/models/connections.py @@ -174,11 +174,14 @@ class ServerConnection(tcp.TCPClient, stateobject.StateObject): def establish_ssl(self, clientcerts, sni, **kwargs): clientcert = None if clientcerts: - path = os.path.join( - clientcerts, - self.address.host.encode("idna")) + ".pem" - if os.path.exists(path): - clientcert = path + if os.path.isfile(clientcerts): + clientcert = clientcerts + else: + path = os.path.join( + clientcerts, + self.address.host.encode("idna")) + ".pem" + if os.path.exists(path): + clientcert = path self.convert_to_ssl(cert=clientcert, sni=sni, **kwargs) self.sni = sni diff --git a/libmproxy/proxy/config.py b/libmproxy/proxy/config.py index c7b513112..b14786552 100644 --- a/libmproxy/proxy/config.py +++ b/libmproxy/proxy/config.py @@ -133,10 +133,9 @@ def process_proxy_options(parser, options): if options.clientcerts: options.clientcerts = os.path.expanduser(options.clientcerts) - if not os.path.exists(options.clientcerts) or not os.path.isdir(options.clientcerts): + if not os.path.exists(options.clientcerts): return parser.error( - "Client certificate directory does not exist or is not a directory: %s" % - options.clientcerts + "Client certificate path does not exist: %s" % options.clientcerts ) if options.auth_nonanonymous or options.auth_singleuser or options.auth_htpasswd: diff --git a/test/test_proxy.py b/test/test_proxy.py index b3e7258a1..b498c800b 100644 --- a/test/test_proxy.py +++ b/test/test_proxy.py @@ -1,3 +1,4 @@ +import os import mock from OpenSSL import SSL @@ -99,8 +100,11 @@ class TestProcessProxyOptions: def test_client_certs(self): with tutils.tmpdir() as cadir: self.assert_noerr("--client-certs", cadir) + self.assert_noerr( + "--client-certs", + os.path.join(tutils.test_data.path("data/clientcert"), "client.pem")) self.assert_err( - "directory does not exist", + "path does not exist", "--client-certs", "nonexistent") diff --git a/test/test_server.py b/test/test_server.py index e48e46fe9..85c766eb4 100644 --- a/test/test_server.py +++ b/test/test_server.py @@ -1,3 +1,4 @@ +import os import socket import time from OpenSSL import SSL @@ -313,13 +314,24 @@ class TestHTTPAuth(tservers.HTTPProxTest): class TestHTTPS(tservers.HTTPProxTest, CommonMixin, TcpMixin): ssl = True ssloptions = pathod.SSLOptions(request_client_cert=True) - clientcerts = True - - def test_clientcert(self): - f = self.pathod("304") - assert f.status_code == 304 - assert self.server.last_log()["request"]["clientcert"]["keyinfo"] + def test_clientcert_file(self): + try: + self.config.clientcerts = os.path.join( + tutils.test_data.path("data/clientcert"), "client.pem") + f = self.pathod("304") + assert f.status_code == 304 + assert self.server.last_log()["request"]["clientcert"]["keyinfo"] + finally: + self.config.clientcerts = None + def test_clientcert_dir(self): + try: + self.config.clientcerts = tutils.test_data.path("data/clientcert") + f = self.pathod("304") + assert f.status_code == 304 + assert self.server.last_log()["request"]["clientcert"]["keyinfo"] + finally: + self.config.clientcerts = None def test_error_post_connect(self): p = self.pathoc() assert p.request("get:/:i0,'invalid\r\n\r\n'").status_code == 400 diff --git a/test/tservers.py b/test/tservers.py index 8e60df603..5963667b5 100644 --- a/test/tservers.py +++ b/test/tservers.py @@ -83,7 +83,6 @@ class ProxTestBase(object): # Test Configuration ssl = None ssloptions = False - clientcerts = False no_upstream_cert = False authenticator = None masterclass = TestMaster @@ -130,7 +129,6 @@ class ProxTestBase(object): no_upstream_cert = cls.no_upstream_cert, cadir = cls.cadir, authenticator = cls.authenticator, - clientcerts = tutils.test_data.path("data/clientcert") if cls.clientcerts else None )