diff options
| author | Aldo Cortesi <aldo@nullcube.com> | 2014-03-05 17:25:12 +1300 | 
|---|---|---|
| committer | Aldo Cortesi <aldo@nullcube.com> | 2014-03-05 17:28:04 +1300 | 
| commit | d65f2215cb9191a24b36ad6a4fcbf474798d3b2d (patch) | |
| tree | ce2c0a84fa3e011abcd521a85b507d9bc62fd198 | |
| parent | 32af66881465ae98a53665c8ddd42c02aaf492f7 (diff) | |
| download | mitmproxy-d65f2215cb9191a24b36ad6a4fcbf474798d3b2d.tar.gz mitmproxy-d65f2215cb9191a24b36ad6a4fcbf474798d3b2d.tar.bz2 mitmproxy-d65f2215cb9191a24b36ad6a4fcbf474798d3b2d.zip | |
Much more sophisticated cert handling
- Specify per-domain certificates and keys
- Certs are no longer regenerated for SANs
- And more. :)
| -rw-r--r-- | libmproxy/app.py | 7 | ||||
| -rw-r--r-- | libmproxy/proxy.py | 98 | ||||
| -rw-r--r-- | test/test_app.py | 4 | ||||
| -rw-r--r-- | test/test_console_contentview.py | 2 | ||||
| -rw-r--r-- | test/test_proxy.py | 14 | ||||
| -rw-r--r-- | test/test_server.py | 5 | ||||
| -rw-r--r-- | test/tservers.py | 13 | 
7 files changed, 63 insertions, 80 deletions
| diff --git a/libmproxy/app.py b/libmproxy/app.py index b046f712..3ebbb61b 100644 --- a/libmproxy/app.py +++ b/libmproxy/app.py @@ -1,5 +1,6 @@  import flask  import os.path +import proxy  mapp = flask.Flask(__name__)  mapp.debug = True @@ -16,14 +17,12 @@ def index():  @mapp.route("/cert/pem")  def certs_pem(): -    capath = master().server.config.cacert -    p = os.path.splitext(capath)[0] + "-cert.pem" +    p = os.path.join(master().server.config.confdir, proxy.CONF_BASENAME + "-cert.pem")      return flask.Response(open(p, "rb").read(), mimetype='application/x-x509-ca-cert')  @mapp.route("/cert/p12")  def certs_p12(): -    capath = master().server.config.cacert -    p = os.path.splitext(capath)[0] + "-cert.p12" +    p = os.path.join(master().server.config.confdir, proxy.CONF_BASENAME + "-cert.p12")      return flask.Response(open(p, "rb").read(), mimetype='application/x-pkcs12') diff --git a/libmproxy/proxy.py b/libmproxy/proxy.py index b0fb9449..1eebba07 100644 --- a/libmproxy/proxy.py +++ b/libmproxy/proxy.py @@ -4,9 +4,12 @@ from netlib import tcp, http, certutils, http_auth  import utils, version, platform, controller, stateobject  TRANSPARENT_SSL_PORTS = [443, 8443] +CONF_BASENAME = "mitmproxy" +CONF_DIR = "~/.mitmproxy"  CA_CERT_NAME = "mitmproxy-ca.pem" +  class AddressPriority(object):      """      Enum that signifies the priority of the given address when choosing the destination host. @@ -38,15 +41,12 @@ class Log:  class ProxyConfig: -    def __init__(self, certfile=None, keyfile=None, cacert=None, clientcerts=None,  +    def __init__(self, confdir=CONF_DIR, clientcerts=None,                          no_upstream_cert=False, body_size_limit=None, reverse_proxy=None,                          forward_proxy=None, transparent_proxy=None, authenticator=None, -                       ciphers=None +                       ciphers=None, certs=None                  ):          self.ciphers = ciphers -        self.certfile = certfile -        self.keyfile = keyfile -        self.cacert = cacert          self.clientcerts = clientcerts          self.no_upstream_cert = no_upstream_cert          self.body_size_limit = body_size_limit @@ -54,7 +54,9 @@ class ProxyConfig:          self.forward_proxy = forward_proxy          self.transparent_proxy = transparent_proxy          self.authenticator = authenticator -        self.certstore = certutils.CertStore(cacert) +        self.confdir = os.path.expanduser(confdir) +        self.certstore = certutils.CertStore.from_store(confdir, CONF_BASENAME) +  class ClientConnection(tcp.BaseHandler, stateobject.SimpleStateObject): @@ -386,10 +388,9 @@ class ConnectionHandler:          if client:              if self.client_conn.ssl_established:                  raise ProxyError(502, "SSL to Client already established.") -            dummycert = self.find_cert() +            cert, key = self.find_cert()              self.client_conn.convert_to_ssl( -                dummycert,  -                self.config.keyfile or self.config.cacert, +                cert, key,                   handle_sni = self.handle_sni,                  cipher_list = self.config.ciphers              ) @@ -420,22 +421,18 @@ class ConnectionHandler:          self.channel.tell("log", Log(msg))      def find_cert(self): -        if self.config.certfile: -            with open(self.config.certfile, "rb") as f: -                return certutils.SSLCert.from_pem(f.read()) -        else: -            host = self.server_conn.address.host -            sans = [] -            if not self.config.no_upstream_cert or not self.server_conn.ssl_established: -                upstream_cert = self.server_conn.cert -                if upstream_cert.cn: -                    host = upstream_cert.cn.decode("utf8").encode("idna") -                sans = upstream_cert.altnames - -            ret = self.config.certstore.get_cert(host, sans) -            if not ret: -                raise ProxyError(502, "Unable to generate dummy cert.") -            return ret +        host = self.server_conn.address.host +        sans = [] +        if not self.config.no_upstream_cert or not self.server_conn.ssl_established: +            upstream_cert = self.server_conn.cert +            if upstream_cert.cn: +                host = upstream_cert.cn.decode("utf8").encode("idna") +            sans = upstream_cert.altnames + +        ret = self.config.certstore.get_cert(host, sans) +        if not ret: +            raise ProxyError(502, "Unable to generate dummy cert.") +        return ret      def handle_sni(self, connection):          """ @@ -451,9 +448,9 @@ class ConnectionHandler:                  self.server_reconnect()  # reconnect to upstream server with SNI                  # Now, change client context to reflect changed certificate:                  new_context = SSL.Context(SSL.TLSv1_METHOD) -                new_context.use_privatekey_file(self.config.certfile or self.config.cacert) -                dummycert = self.find_cert() -                new_context.use_certificate(dummycert.x509) +                cert, key = self.find_cert() +                new_context.use_privatekey_file(key) +                new_context.use_certificate(cert.X509)                  connection.set_context(new_context)          # An unhandled exception in this method will core dump PyOpenSSL, so          # make dang sure it doesn't happen. @@ -510,14 +507,12 @@ class DummyServer:  def ssl_option_group(parser):      group = parser.add_argument_group("SSL")      group.add_argument( -        "--certfile", action="store", -        type=str, dest="certfile", default=None, -        help="SSL certificate in PEM format, optionally with the key in the same file." -    ) -    group.add_argument( -        "--keyfile", action="store", -        type=str, dest="keyfile", default=None, -        help="Key matching certfile." +        "--cert", dest='certs', default=[], type=str, +        metavar = "SPEC", action="append",  +        help='Add an SSL certificate. SPEC is of the form "[domain=]path". '\ +             'The domain may include a wildcard, and is equal to "*" if not specified. '\ +             'The file at path is a certificate in PEM format. If a private key is included in the PEM, '\ +             'it is used, else the default key in the conf dir is used. Can be passed multiple times.'      )      group.add_argument(          "--client-certs", action="store", @@ -532,23 +527,6 @@ def ssl_option_group(parser):  def process_proxy_options(parser, options): -    if options.certfile: -        options.certfile = os.path.expanduser(options.certfile) -        if not os.path.exists(options.certfile): -            return parser.error("Certificate file does not exist: %s" % options.certfile) - -    if options.keyfile: -        options.keyfile = os.path.expanduser(options.keyfile) -        if not os.path.exists(options.keyfile): -            return parser.error("Key file does not exist: %s" % options.keyfile) - -    if options.certfile and not options.keyfile: -        options.keyfile = options.certfile - -    cacert = os.path.join(options.confdir, CA_CERT_NAME) -    cacert = os.path.expanduser(cacert) -    if not os.path.exists(cacert): -        certutils.dummy_ca(cacert)      body_size_limit = utils.parse_size(options.body_size_limit)      if options.reverse_proxy and options.transparent_proxy:          return parser.error("Can't set both reverse proxy and transparent proxy.") @@ -601,10 +579,17 @@ def process_proxy_options(parser, options):      else:          authenticator = http_auth.NullProxyAuth(None) +    certs = [] +    for i in options.certs: +        parts = i.split("=", 1) +        if len(parts) == 1: +            parts = ["*", parts[0]] +        parts[1] = os.path.expanduser(parts[1]) +        if not os.path.exists(parts[1]): +            parser.error("Certificate file does not exist: %s"%parts[1]) +        certs.append(parts) +      return ProxyConfig( -        certfile=options.certfile, -        keyfile=options.keyfile, -        cacert=cacert,          clientcerts=options.clientcerts,          body_size_limit=body_size_limit,          no_upstream_cert=options.no_upstream_cert, @@ -613,4 +598,5 @@ def process_proxy_options(parser, options):          transparent_proxy=trans,          authenticator=authenticator,          ciphers=options.ciphers, +        certs = certs,      ) diff --git a/test/test_app.py b/test/test_app.py index f0eab7cc..52cd1ba6 100644 --- a/test/test_app.py +++ b/test/test_app.py @@ -9,11 +9,9 @@ class TestApp(tservers.HTTPProxTest):          assert self.app("/").status_code == 200      def test_cert(self): -        path = tutils.test_data.path("data/confdir/") + "mitmproxy-ca-cert."          with tutils.tmpdir() as d:              for ext in ["pem", "p12"]:                  resp = self.app("/cert/%s" % ext)                  assert resp.status_code == 200 -                with open(path + ext, "rb") as f: -                    assert resp.content == f.read() +                assert resp.content diff --git a/test/test_console_contentview.py b/test/test_console_contentview.py index a878ad4e..0aabd2c5 100644 --- a/test/test_console_contentview.py +++ b/test/test_console_contentview.py @@ -120,7 +120,7 @@ class TestContentView:      def test_view_css(self):          v = cv.ViewCSS() -        with open('./test/data/1.css', 'r') as fp: +        with open(tutils.test_data.path('data/1.css'), 'r') as fp:              fixture_1 = fp.read()          result = v([], 'a', 100) diff --git a/test/test_proxy.py b/test/test_proxy.py index 5ff00290..b15e3f84 100644 --- a/test/test_proxy.py +++ b/test/test_proxy.py @@ -70,13 +70,6 @@ class TestProcessProxyOptions:      def test_simple(self):          assert self.p() -    def test_certfile_keyfile(self): -        self.assert_noerr("--certfile", tutils.test_data.path("data/testkey.pem")) -        self.assert_err("does not exist", "--certfile", "nonexistent") - -        self.assert_noerr("--keyfile", tutils.test_data.path("data/testkey.pem")) -        self.assert_err("does not exist", "--keyfile", "nonexistent") -      def test_confdir(self):          with tutils.tmpdir() as confdir:              self.assert_noerr("--confdir", confdir) @@ -93,11 +86,16 @@ class TestProcessProxyOptions:          self.assert_err("invalid reverse proxy", "-P", "reverse")          self.assert_noerr("-P", "http://localhost") -    def test_certs(self): +    def test_client_certs(self):          with tutils.tmpdir() as confdir:              self.assert_noerr("--client-certs", confdir)              self.assert_err("directory does not exist", "--client-certs", "nonexistent") +    def test_certs(self): +        with tutils.tmpdir() as confdir: +            self.assert_noerr("--cert", tutils.test_data.path("data/testkey.pem")) +            self.assert_err("does not exist", "--cert", "nonexistent") +      def test_auth(self):          p = self.assert_noerr("--nonanonymous")          assert p.authenticator diff --git a/test/test_server.py b/test/test_server.py index 2714ef52..ed21e75c 100644 --- a/test/test_server.py +++ b/test/test_server.py @@ -213,8 +213,9 @@ class TestHTTPSNoCommonName(tservers.HTTPProxTest):      """      ssl = True      ssloptions=pathod.SSLOptions( -            certfile = tutils.test_data.path("data/no_common_name.pem"), -            keyfile = tutils.test_data.path("data/no_common_name.pem"), +            certs = [ +                ("*", tutils.test_data.path("data/no_common_name.pem")) +            ]          )      def test_http(self):          f = self.pathod("202") diff --git a/test/tservers.py b/test/tservers.py index 540cda60..3a6a610f 100644 --- a/test/tservers.py +++ b/test/tservers.py @@ -1,4 +1,5 @@  import threading, Queue +import shutil, tempfile  import flask  import libpathod.test, libpathod.pathoc  from libmproxy import proxy, flow, controller @@ -72,7 +73,6 @@ class ProxTestBase(object):      ssl = None      ssloptions = False      clientcerts = False -    certfile = None      no_upstream_cert = False      authenticator = None      masterclass = TestMaster @@ -82,9 +82,10 @@ class ProxTestBase(object):          cls.server = libpathod.test.Daemon(ssl=cls.ssl, ssloptions=cls.ssloptions)          cls.server2 = libpathod.test.Daemon(ssl=cls.ssl, ssloptions=cls.ssloptions)          pconf = cls.get_proxy_config() +        cls.confdir = tempfile.gettempdir()          config = proxy.ProxyConfig(              no_upstream_cert = cls.no_upstream_cert, -            cacert = tutils.test_data.path("data/confdir/mitmproxy-ca.pem"), +            confdir = cls.confdir,              authenticator = cls.authenticator,              **pconf          ) @@ -93,6 +94,10 @@ class ProxTestBase(object):          cls.proxy = ProxyThread(tmaster)          cls.proxy.start() +    @classmethod +    def tearDownAll(cls): +        shutil.rmtree(cls.confdir) +      @property      def master(cls):          return cls.proxy.tmaster @@ -127,9 +132,6 @@ class ProxTestBase(object):          d = dict()          if cls.clientcerts:              d["clientcerts"] = tutils.test_data.path("data/clientcert") -        if cls.certfile: -            d["certfile"]  =tutils.test_data.path("data/testkey.pem") -            d["keyfile"]  =tutils.test_data.path("data/testkey.pem")          return d @@ -254,7 +256,6 @@ class ChainProxTest(ProxTestBase):      """      n = 2      chain_config = [lambda: proxy.ProxyConfig( -        cacert = tutils.test_data.path("data/confdir/mitmproxy-ca.pem"),      )] * n      @classmethod      def setupAll(cls): | 
