From 00942c1431c551e0bded111271be9b69f5261d91 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Mon, 27 Feb 2012 15:05:45 +1300 Subject: Add upstream certificate lookup. This initiates a connection to the server to obtain certificate information to generate interception certificates. At the moment, the information used is the Common Name, and the list of Subject Alternative Names. --- libmproxy/cmdline.py | 17 +++++++---- libmproxy/proxy.py | 13 +++++--- libmproxy/resources/cert.cnf | 2 ++ libmproxy/utils.py | 73 +++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 91 insertions(+), 14 deletions(-) (limited to 'libmproxy') diff --git a/libmproxy/cmdline.py b/libmproxy/cmdline.py index ee4f3b08..42c02449 100644 --- a/libmproxy/cmdline.py +++ b/libmproxy/cmdline.py @@ -141,6 +141,17 @@ def common_options(parser): help="Byte size limit of HTTP request and response bodies."\ " Understands k/m/g suffixes, i.e. 3m for 3 megabytes." ) + parser.add_option( + "--cert-wait-time", type="float", + action="store", dest="cert_wait_time", default=0, + help="Wait for specified number of seconds after a new cert is generated. This can smooth over small discrepancies between the client and server times." + ) + parser.add_option( + "--upstream-cn-lookup", default=False, + action="store_true", dest="upstream_cn_lookup", + help="Connect to upstream server to look up certificate Common Name." + ) + group = optparse.OptionGroup(parser, "Client Replay") group.add_option( "-c", @@ -149,12 +160,6 @@ def common_options(parser): ) parser.add_option_group(group) - parser.add_option( - "--cert-wait-time", type="float", - action="store", dest="cert_wait_time", default=0, - help="Wait for specified number of seconds after a new cert is generated. This can smooth over small discrepancies between the client and server times." - ) - group = optparse.OptionGroup(parser, "Server Replay") group.add_option( "-S", diff --git a/libmproxy/proxy.py b/libmproxy/proxy.py index 3a7f807e..ec7c52e4 100644 --- a/libmproxy/proxy.py +++ b/libmproxy/proxy.py @@ -35,12 +35,13 @@ class ProxyError(Exception): class ProxyConfig: - def __init__(self, certfile = None, ciphers = None, cacert = None, cert_wait_time=0, body_size_limit = None, reverse_proxy=None): + def __init__(self, certfile = None, ciphers = None, cacert = None, cert_wait_time=0, upstream_cn_lookup=False, body_size_limit = None, reverse_proxy=None): self.certfile = certfile self.ciphers = ciphers self.cacert = cacert self.certdir = None self.cert_wait_time = cert_wait_time + self.upstream_cn_lookup = upstream_cn_lookup self.body_size_limit = body_size_limit self.reverse_proxy = reverse_proxy @@ -343,11 +344,14 @@ class ProxyHandler(SocketServer.StreamRequestHandler): if server: server.terminate() - def find_cert(self, host): + def find_cert(self, host, port): if self.config.certfile: return self.config.certfile else: - ret = utils.dummy_cert(self.config.certdir, self.config.cacert, host) + sans = [] + if self.config.upstream_cn_lookup: + host, sans = utils.get_remote_cn(host, port) + ret = utils.dummy_cert(self.config.certdir, self.config.cacert, host, sans) time.sleep(self.config.cert_wait_time) if not ret: raise ProxyError(502, "mitmproxy: Unable to generate dummy cert.") @@ -374,7 +378,7 @@ class ProxyHandler(SocketServer.StreamRequestHandler): ) self.wfile.flush() kwargs = dict( - certfile = self.find_cert(host), + certfile = self.find_cert(host, port), keyfile = self.config.certfile or self.config.cacert, server_side = True, ssl_version = ssl.PROTOCOL_SSLv23, @@ -538,5 +542,6 @@ def process_proxy_options(parser, options): ciphers = options.ciphers, cert_wait_time = options.cert_wait_time, body_size_limit = body_size_limit, + upstream_cn_lookup = options.upstream_cn_lookup, reverse_proxy = rp ) diff --git a/libmproxy/resources/cert.cnf b/libmproxy/resources/cert.cnf index 5f80c2d6..4f2525a9 100644 --- a/libmproxy/resources/cert.cnf +++ b/libmproxy/resources/cert.cnf @@ -28,3 +28,5 @@ basicConstraints = CA:false keyUsage = nonRepudiation, digitalSignature, keyEncipherment nsCertType = server +[ alt_names ] +%(sans)s diff --git a/libmproxy/utils.py b/libmproxy/utils.py index 16540434..3381ad33 100644 --- a/libmproxy/utils.py +++ b/libmproxy/utils.py @@ -12,8 +12,8 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import re, os, subprocess, datetime, urlparse, string, urllib -import time, functools, cgi, textwrap, hashlib +import re, os, subprocess, datetime, urlparse, string, urllib, socket +import time, functools, cgi, textwrap, hashlib, ssl, tempfile import json CERT_SLEEP_TIME = 1 @@ -276,7 +276,7 @@ def dummy_ca(path): return True -def dummy_cert(certdir, ca, commonname): +def dummy_cert(certdir, ca, commonname, sans): """ certdir: Certificate directory. ca: Path to the certificate authority file, or None. @@ -293,8 +293,14 @@ def dummy_cert(certdir, ca, commonname): reqpath = os.path.join(certdir, namehash + ".req") template = open(pkg_data.path("resources/cert.cnf")).read() + + ss = [] + for i, v in enumerate(sans): + ss.append("DNS.%s = %s"%(i, v)) + ss = "\n".join(ss) + f = open(confpath, "w") - f.write(template%(dict(commonname=commonname))) + f.write(template%(dict(commonname=commonname, sans=ss))) f.close() if ca: @@ -483,3 +489,62 @@ def parse_size(s): return int(s) * mult except ValueError: raise ValueError("Invalid size specification: %s"%s) + + +def get_remote_cn(host, port): + addr = socket.gethostbyname(host) + s = ssl.get_server_certificate((addr, port)) + f = tempfile.NamedTemporaryFile() + f.write(s) + f.flush() + p = subprocess.Popen( + [ + "openssl", + "x509", + "-in", f.name, + "-text", + "-noout" + ], + stdout = subprocess.PIPE + ) + out, _ = p.communicate() + return parse_text_cert(out) + + +CNRE = re.compile( + r""" + Subject:.*CN=(\S*) + """, + re.VERBOSE|re.MULTILINE +) +SANRE = re.compile( + r""" + X509v3\ Subject\ Alternative\ Name:\s* + (.*)$ + """, + re.VERBOSE|re.MULTILINE +) + + +def parse_text_cert(txt): + """ + Returns a (common name, [subject alternative names]) tuple. + """ + r = re.search(CNRE, txt) + if r: + cn = r.group(1) + else: + return None + + r = re.search(SANRE, txt) + san = [] + if r: + for i in r.group(1).split(","): + i = i.strip() + k, v = i.split(":") + if k == "DNS": + san.append(v) + else: + san = [] + return (cn, san) + -- cgit v1.2.3