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:
parent
32af668814
commit
d65f2215cb
|
@ -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')
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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()
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Reference in New Issue