Much more sophisticated cert handling

- Specify per-domain certificates and keys
- Certs are no longer regenerated for SANs
- And more. :)
This commit is contained in:
Aldo Cortesi 2014-03-05 17:25:12 +13:00
parent 32af668814
commit d65f2215cb
7 changed files with 62 additions and 79 deletions

View File

@ -1,5 +1,6 @@
import flask import flask
import os.path import os.path
import proxy
mapp = flask.Flask(__name__) mapp = flask.Flask(__name__)
mapp.debug = True mapp.debug = True
@ -16,14 +17,12 @@ def index():
@mapp.route("/cert/pem") @mapp.route("/cert/pem")
def certs_pem(): def certs_pem():
capath = master().server.config.cacert p = os.path.join(master().server.config.confdir, proxy.CONF_BASENAME + "-cert.pem")
p = os.path.splitext(capath)[0] + "-cert.pem"
return flask.Response(open(p, "rb").read(), mimetype='application/x-x509-ca-cert') return flask.Response(open(p, "rb").read(), mimetype='application/x-x509-ca-cert')
@mapp.route("/cert/p12") @mapp.route("/cert/p12")
def certs_p12(): def certs_p12():
capath = master().server.config.cacert p = os.path.join(master().server.config.confdir, proxy.CONF_BASENAME + "-cert.p12")
p = os.path.splitext(capath)[0] + "-cert.p12"
return flask.Response(open(p, "rb").read(), mimetype='application/x-pkcs12') return flask.Response(open(p, "rb").read(), mimetype='application/x-pkcs12')

View File

@ -4,9 +4,12 @@ from netlib import tcp, http, certutils, http_auth
import utils, version, platform, controller, stateobject import utils, version, platform, controller, stateobject
TRANSPARENT_SSL_PORTS = [443, 8443] TRANSPARENT_SSL_PORTS = [443, 8443]
CONF_BASENAME = "mitmproxy"
CONF_DIR = "~/.mitmproxy"
CA_CERT_NAME = "mitmproxy-ca.pem" CA_CERT_NAME = "mitmproxy-ca.pem"
class AddressPriority(object): class AddressPriority(object):
""" """
Enum that signifies the priority of the given address when choosing the destination host. Enum that signifies the priority of the given address when choosing the destination host.
@ -38,15 +41,12 @@ class Log:
class ProxyConfig: 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, no_upstream_cert=False, body_size_limit=None, reverse_proxy=None,
forward_proxy=None, transparent_proxy=None, authenticator=None, forward_proxy=None, transparent_proxy=None, authenticator=None,
ciphers=None ciphers=None, certs=None
): ):
self.ciphers = ciphers self.ciphers = ciphers
self.certfile = certfile
self.keyfile = keyfile
self.cacert = cacert
self.clientcerts = clientcerts self.clientcerts = clientcerts
self.no_upstream_cert = no_upstream_cert self.no_upstream_cert = no_upstream_cert
self.body_size_limit = body_size_limit self.body_size_limit = body_size_limit
@ -54,7 +54,9 @@ class ProxyConfig:
self.forward_proxy = forward_proxy self.forward_proxy = forward_proxy
self.transparent_proxy = transparent_proxy self.transparent_proxy = transparent_proxy
self.authenticator = authenticator 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): class ClientConnection(tcp.BaseHandler, stateobject.SimpleStateObject):
@ -386,10 +388,9 @@ class ConnectionHandler:
if client: if client:
if self.client_conn.ssl_established: if self.client_conn.ssl_established:
raise ProxyError(502, "SSL to Client already established.") raise ProxyError(502, "SSL to Client already established.")
dummycert = self.find_cert() cert, key = self.find_cert()
self.client_conn.convert_to_ssl( self.client_conn.convert_to_ssl(
dummycert, cert, key,
self.config.keyfile or self.config.cacert,
handle_sni = self.handle_sni, handle_sni = self.handle_sni,
cipher_list = self.config.ciphers cipher_list = self.config.ciphers
) )
@ -420,22 +421,18 @@ class ConnectionHandler:
self.channel.tell("log", Log(msg)) self.channel.tell("log", Log(msg))
def find_cert(self): def find_cert(self):
if self.config.certfile: host = self.server_conn.address.host
with open(self.config.certfile, "rb") as f: sans = []
return certutils.SSLCert.from_pem(f.read()) if not self.config.no_upstream_cert or not self.server_conn.ssl_established:
else: upstream_cert = self.server_conn.cert
host = self.server_conn.address.host if upstream_cert.cn:
sans = [] host = upstream_cert.cn.decode("utf8").encode("idna")
if not self.config.no_upstream_cert or not self.server_conn.ssl_established: sans = upstream_cert.altnames
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) ret = self.config.certstore.get_cert(host, sans)
if not ret: if not ret:
raise ProxyError(502, "Unable to generate dummy cert.") raise ProxyError(502, "Unable to generate dummy cert.")
return ret return ret
def handle_sni(self, connection): def handle_sni(self, connection):
""" """
@ -451,9 +448,9 @@ class ConnectionHandler:
self.server_reconnect() # reconnect to upstream server with SNI self.server_reconnect() # reconnect to upstream server with SNI
# Now, change client context to reflect changed certificate: # Now, change client context to reflect changed certificate:
new_context = SSL.Context(SSL.TLSv1_METHOD) new_context = SSL.Context(SSL.TLSv1_METHOD)
new_context.use_privatekey_file(self.config.certfile or self.config.cacert) cert, key = self.find_cert()
dummycert = self.find_cert() new_context.use_privatekey_file(key)
new_context.use_certificate(dummycert.x509) new_context.use_certificate(cert.X509)
connection.set_context(new_context) connection.set_context(new_context)
# An unhandled exception in this method will core dump PyOpenSSL, so # An unhandled exception in this method will core dump PyOpenSSL, so
# make dang sure it doesn't happen. # make dang sure it doesn't happen.
@ -510,14 +507,12 @@ class DummyServer:
def ssl_option_group(parser): def ssl_option_group(parser):
group = parser.add_argument_group("SSL") group = parser.add_argument_group("SSL")
group.add_argument( group.add_argument(
"--certfile", action="store", "--cert", dest='certs', default=[], type=str,
type=str, dest="certfile", default=None, metavar = "SPEC", action="append",
help="SSL certificate in PEM format, optionally with the key in the same file." 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. '\
group.add_argument( 'The file at path is a certificate in PEM format. If a private key is included in the PEM, '\
"--keyfile", action="store", 'it is used, else the default key in the conf dir is used. Can be passed multiple times.'
type=str, dest="keyfile", default=None,
help="Key matching certfile."
) )
group.add_argument( group.add_argument(
"--client-certs", action="store", "--client-certs", action="store",
@ -532,23 +527,6 @@ def ssl_option_group(parser):
def process_proxy_options(parser, options): 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) body_size_limit = utils.parse_size(options.body_size_limit)
if options.reverse_proxy and options.transparent_proxy: if options.reverse_proxy and options.transparent_proxy:
return parser.error("Can't set both reverse proxy and 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: else:
authenticator = http_auth.NullProxyAuth(None) 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( return ProxyConfig(
certfile=options.certfile,
keyfile=options.keyfile,
cacert=cacert,
clientcerts=options.clientcerts, clientcerts=options.clientcerts,
body_size_limit=body_size_limit, body_size_limit=body_size_limit,
no_upstream_cert=options.no_upstream_cert, no_upstream_cert=options.no_upstream_cert,
@ -613,4 +598,5 @@ def process_proxy_options(parser, options):
transparent_proxy=trans, transparent_proxy=trans,
authenticator=authenticator, authenticator=authenticator,
ciphers=options.ciphers, ciphers=options.ciphers,
certs = certs,
) )

View File

@ -9,11 +9,9 @@ class TestApp(tservers.HTTPProxTest):
assert self.app("/").status_code == 200 assert self.app("/").status_code == 200
def test_cert(self): def test_cert(self):
path = tutils.test_data.path("data/confdir/") + "mitmproxy-ca-cert."
with tutils.tmpdir() as d: with tutils.tmpdir() as d:
for ext in ["pem", "p12"]: for ext in ["pem", "p12"]:
resp = self.app("/cert/%s" % ext) resp = self.app("/cert/%s" % ext)
assert resp.status_code == 200 assert resp.status_code == 200
with open(path + ext, "rb") as f: assert resp.content
assert resp.content == f.read()

View File

@ -120,7 +120,7 @@ class TestContentView:
def test_view_css(self): def test_view_css(self):
v = cv.ViewCSS() 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() fixture_1 = fp.read()
result = v([], 'a', 100) result = v([], 'a', 100)

View File

@ -70,13 +70,6 @@ class TestProcessProxyOptions:
def test_simple(self): def test_simple(self):
assert self.p() 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): def test_confdir(self):
with tutils.tmpdir() as confdir: with tutils.tmpdir() as confdir:
self.assert_noerr("--confdir", confdir) self.assert_noerr("--confdir", confdir)
@ -93,11 +86,16 @@ class TestProcessProxyOptions:
self.assert_err("invalid reverse proxy", "-P", "reverse") self.assert_err("invalid reverse proxy", "-P", "reverse")
self.assert_noerr("-P", "http://localhost") self.assert_noerr("-P", "http://localhost")
def test_certs(self): def test_client_certs(self):
with tutils.tmpdir() as confdir: with tutils.tmpdir() as confdir:
self.assert_noerr("--client-certs", confdir) self.assert_noerr("--client-certs", confdir)
self.assert_err("directory does not exist", "--client-certs", "nonexistent") 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): def test_auth(self):
p = self.assert_noerr("--nonanonymous") p = self.assert_noerr("--nonanonymous")
assert p.authenticator assert p.authenticator

View File

@ -213,8 +213,9 @@ class TestHTTPSNoCommonName(tservers.HTTPProxTest):
""" """
ssl = True ssl = True
ssloptions=pathod.SSLOptions( ssloptions=pathod.SSLOptions(
certfile = tutils.test_data.path("data/no_common_name.pem"), certs = [
keyfile = tutils.test_data.path("data/no_common_name.pem"), ("*", tutils.test_data.path("data/no_common_name.pem"))
]
) )
def test_http(self): def test_http(self):
f = self.pathod("202") f = self.pathod("202")

View File

@ -1,4 +1,5 @@
import threading, Queue import threading, Queue
import shutil, tempfile
import flask import flask
import libpathod.test, libpathod.pathoc import libpathod.test, libpathod.pathoc
from libmproxy import proxy, flow, controller from libmproxy import proxy, flow, controller
@ -72,7 +73,6 @@ class ProxTestBase(object):
ssl = None ssl = None
ssloptions = False ssloptions = False
clientcerts = False clientcerts = False
certfile = None
no_upstream_cert = False no_upstream_cert = False
authenticator = None authenticator = None
masterclass = TestMaster masterclass = TestMaster
@ -82,9 +82,10 @@ class ProxTestBase(object):
cls.server = libpathod.test.Daemon(ssl=cls.ssl, ssloptions=cls.ssloptions) cls.server = libpathod.test.Daemon(ssl=cls.ssl, ssloptions=cls.ssloptions)
cls.server2 = 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() pconf = cls.get_proxy_config()
cls.confdir = tempfile.gettempdir()
config = proxy.ProxyConfig( config = proxy.ProxyConfig(
no_upstream_cert = cls.no_upstream_cert, no_upstream_cert = cls.no_upstream_cert,
cacert = tutils.test_data.path("data/confdir/mitmproxy-ca.pem"), confdir = cls.confdir,
authenticator = cls.authenticator, authenticator = cls.authenticator,
**pconf **pconf
) )
@ -93,6 +94,10 @@ class ProxTestBase(object):
cls.proxy = ProxyThread(tmaster) cls.proxy = ProxyThread(tmaster)
cls.proxy.start() cls.proxy.start()
@classmethod
def tearDownAll(cls):
shutil.rmtree(cls.confdir)
@property @property
def master(cls): def master(cls):
return cls.proxy.tmaster return cls.proxy.tmaster
@ -127,9 +132,6 @@ class ProxTestBase(object):
d = dict() d = dict()
if cls.clientcerts: if cls.clientcerts:
d["clientcerts"] = tutils.test_data.path("data/clientcert") 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 return d
@ -254,7 +256,6 @@ class ChainProxTest(ProxTestBase):
""" """
n = 2 n = 2
chain_config = [lambda: proxy.ProxyConfig( chain_config = [lambda: proxy.ProxyConfig(
cacert = tutils.test_data.path("data/confdir/mitmproxy-ca.pem"),
)] * n )] * n
@classmethod @classmethod
def setupAll(cls): def setupAll(cls):