From 80690b79062c739fc8adfc725d39a68d4b65b43a Mon Sep 17 00:00:00 2001 From: "Michael J. Bazzinotti" Date: Mon, 28 Dec 2015 15:20:45 -0500 Subject: [PATCH 1/2] [docs/libmproxy/test] Support single client-side cert file as argument to --client-certs --- docs/certinstall.rst | 17 ++++++++++++++--- libmproxy/cmdline.py | 2 +- libmproxy/models/connections.py | 13 ++++++++----- libmproxy/proxy/config.py | 8 +++++--- test/test_proxy.py | 6 +++++- test/test_server.py | 9 +++++++++ 6 files changed, 42 insertions(+), 13 deletions(-) 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..f06e55cab 100644 --- a/libmproxy/proxy/config.py +++ b/libmproxy/proxy/config.py @@ -133,10 +133,12 @@ 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) or + os.path.isdir(options.clientcerts) or + os.path.isfile(options.clientcerts)): return parser.error( - "Client certificate directory does not exist or is not a directory: %s" % - options.clientcerts + "Client certificate argument is not a file or directory, " + "or 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..a0530c79d 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", + "not a file or directory", "--client-certs", "nonexistent") diff --git a/test/test_server.py b/test/test_server.py index e48e46fe9..09cfa3815 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 @@ -316,6 +317,14 @@ class TestHTTPS(tservers.HTTPProxTest, CommonMixin, TcpMixin): clientcerts = True def test_clientcert(self): + 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"] + + def test_clientcerts(self): + 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"] From 09168e1274b0e33448811c39efa3fbdd2aeff756 Mon Sep 17 00:00:00 2001 From: "Michael J. Bazzinotti" Date: Tue, 29 Dec 2015 11:48:02 -0500 Subject: [PATCH 2/2] client-cert: Simplify and cleanup simplify the error message when cert path does not exist. During tests, reset the clientcerts proxy config setting to None Remove now-unused clientcerts code from class ProxTestBase --- libmproxy/proxy/config.py | 7 ++----- test/test_proxy.py | 2 +- test/test_server.py | 31 +++++++++++++++++-------------- test/tservers.py | 2 -- 4 files changed, 20 insertions(+), 22 deletions(-) diff --git a/libmproxy/proxy/config.py b/libmproxy/proxy/config.py index f06e55cab..b14786552 100644 --- a/libmproxy/proxy/config.py +++ b/libmproxy/proxy/config.py @@ -133,12 +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 - os.path.isdir(options.clientcerts) or - os.path.isfile(options.clientcerts)): + if not os.path.exists(options.clientcerts): return parser.error( - "Client certificate argument is not a file or directory, " - "or 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: diff --git a/test/test_proxy.py b/test/test_proxy.py index a0530c79d..b498c800b 100644 --- a/test/test_proxy.py +++ b/test/test_proxy.py @@ -104,7 +104,7 @@ class TestProcessProxyOptions: "--client-certs", os.path.join(tutils.test_data.path("data/clientcert"), "client.pem")) self.assert_err( - "not a file or directory", + "path does not exist", "--client-certs", "nonexistent") diff --git a/test/test_server.py b/test/test_server.py index 09cfa3815..85c766eb4 100644 --- a/test/test_server.py +++ b/test/test_server.py @@ -314,21 +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): - 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"] - - def test_clientcerts(self): - 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"] + 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 )