From 4fc807cedd9a481db9e0fc1633d8c169f53f4a8e Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Sun, 20 Feb 2011 12:12:55 +1300 Subject: [PATCH] Clean up certificate generation. - Use templates for config files. We can re-introduce customization of the certificate attributes when we need them. - Split CA and cert generation into separate functions. - Generation methods provide an error return when generation fails. - When the user explicitly specifies a certificate, we don't generate it, but fail if it doesn't exist. --- libmproxy/proxy.py | 8 +- libmproxy/resources/ca.cnf | 33 +++++ libmproxy/resources/cert.cnf | 34 +++++ libmproxy/utils.py | 241 ++++++++++++++--------------------- mitmdump | 2 +- mitmplayback | 2 +- mitmproxy | 2 +- mitmrecord | 2 +- test/test_utils.py | 44 +++++-- 9 files changed, 204 insertions(+), 164 deletions(-) create mode 100644 libmproxy/resources/ca.cnf create mode 100644 libmproxy/resources/cert.cnf diff --git a/libmproxy/proxy.py b/libmproxy/proxy.py index 4ab196949..e3c692b98 100644 --- a/libmproxy/proxy.py +++ b/libmproxy/proxy.py @@ -550,7 +550,7 @@ class ProxyHandler(SocketServer.StreamRequestHandler): if config.certpath is not None: cert = config.certpath + "/" + host + ".pem" if not os.path.exists(cert) and config.cacert is not None: - utils.make_bogus_cert(cert, ca=config.cacert, commonName=host) + utils.dummy_cert(config.certpath, config.cacert, host) if os.path.exists(cert): return cert print >> sys.stderr, "WARNING: Certificate missing for %s:%d! (%s)\n" % (host, port, cert) @@ -679,9 +679,9 @@ class ProxyServer(ServerBase): def set_mqueue(self, q): self.masterq = q - def process_request(self, request, client_address): - return ServerBase.process_request(self, request, client_address) - def finish_request(self, request, client_address): self.RequestHandlerClass(request, client_address, self, self.masterq) + def shutdown(self): + ServerBase.shutdown(self) + diff --git a/libmproxy/resources/ca.cnf b/libmproxy/resources/ca.cnf new file mode 100644 index 000000000..e46bb08fb --- /dev/null +++ b/libmproxy/resources/ca.cnf @@ -0,0 +1,33 @@ +[ req ] +prompt = no +distinguished_name = req_distinguished_name +x509_extensions = v3_ca +req_extensions = v3_ca_req + +[ req_distinguished_name ] +organizationName = mitmproxy +commonName = Dummy CA + +[ v3_ca ] +subjectKeyIdentifier=hash +authorityKeyIdentifier=keyid:always,issuer +basicConstraints = critical,CA:true +keyUsage = cRLSign, keyCertSign +nsCertType = sslCA + +[ v3_ca_req ] +basicConstraints = critical,CA:true +keyUsage = cRLSign, keyCertSign +nsCertType = sslCA + +[ v3_cert ] +basicConstraints = CA:false +keyUsage = nonRepudiation, digitalSignature, keyEncipherment +nsCertType = server +subjectKeyIdentifier=hash +authorityKeyIdentifier=keyid:always,issuer + +[ v3_cert_req ] +basicConstraints = CA:false +keyUsage = nonRepudiation, digitalSignature, keyEncipherment +nsCertType = server diff --git a/libmproxy/resources/cert.cnf b/libmproxy/resources/cert.cnf new file mode 100644 index 000000000..9afae09f2 --- /dev/null +++ b/libmproxy/resources/cert.cnf @@ -0,0 +1,34 @@ +[ req ] +prompt = no +distinguished_name = req_distinguished_name +x509_extensions = v3_cert +req_extensions = v3_cert_req + +[ req_distinguished_name ] +organizationName = mitmproxy +commonName = %(commonname)s + +[ v3_ca ] +subjectKeyIdentifier=hash +authorityKeyIdentifier=keyid:always,issuer +basicConstraints = critical,CA:true +keyUsage = cRLSign, keyCertSign +nsCertType = sslCA + +[ v3_ca_req ] +basicConstraints = critical,CA:true +keyUsage = cRLSign, keyCertSign +nsCertType = sslCA + +[ v3_cert ] +basicConstraints = CA:false +keyUsage = nonRepudiation, digitalSignature, keyEncipherment +nsCertType = server +subjectKeyIdentifier=hash +authorityKeyIdentifier=keyid:always,issuer + +[ v3_cert_req ] +basicConstraints = CA:false +keyUsage = nonRepudiation, digitalSignature, keyEncipherment +nsCertType = server + diff --git a/libmproxy/utils.py b/libmproxy/utils.py index c9be74836..51a8e8713 100644 --- a/libmproxy/utils.py +++ b/libmproxy/utils.py @@ -12,7 +12,7 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import re, os, subprocess, datetime, textwrap, errno +import re, os, subprocess, datetime, textwrap, errno, sys import optparse def format_timestamp(s): @@ -314,167 +314,118 @@ class Data: data = Data(__name__) -def make_openssl_conf(path, countryName=None, stateOrProvinceName=None, localityName=None, organizationName=None, organizationalUnitName=None, commonName=None, emailAddress=None, ca=False): - cnf = open(path, "w") - cnf.write("[ req ]\n") - cnf.write("prompt = no\n") - cnf.write("distinguished_name = req_distinguished_name\n") - if ca: - cnf.write("x509_extensions = v3_ca # The extentions to add to the self signed cert\n") - cnf.write("\n") - cnf.write("[ req_distinguished_name ]\n") - if countryName is not None: - cnf.write("countryName = %s\n" % (countryName) ) - cnf.write("stateOrProvinceName = %s\n" % (stateOrProvinceName) ) - cnf.write("localityName = %s\n" % (localityName) ) - cnf.write("organizationName = %s\n" % (organizationName) ) - cnf.write("organizationalUnitName = %s\n" % (organizationalUnitName) ) - cnf.write("commonName = %s\n" % (commonName) ) - cnf.write("emailAddress = %s\n" % (emailAddress) ) - cnf.write("\n") - cnf.write("[ v3_ca ]\n") - cnf.write("subjectKeyIdentifier=hash\n") - cnf.write("authorityKeyIdentifier=keyid:always,issuer\n") - if ca: - cnf.write("basicConstraints = critical,CA:true\n") - cnf.write("keyUsage = cRLSign, keyCertSign\n") - #cnf.write("nsCertType = sslCA, emailCA\n") - #cnf.write("subjectAltName=email:copy\n") - #cnf.write("issuerAltName=issuer:copy\n") +def dummy_ca(path): + """ + Creates a dummy CA, and writes it to path. -def make_bogus_cert(certpath, countryName=None, stateOrProvinceName=None, localityName=None, organizationName="mitmproxy", organizationalUnitName=None, commonName="Dummy Certificate", emailAddress=None, ca=None, newca=False): - # Generates a bogus certificate like so: - # openssl req -config template -x509 -nodes -days 9999 -newkey rsa:1024 \ - # -keyout cert.pem -out cert.pem + This function also creates the necessary directories if they don't exist. - (path, ext) = os.path.splitext(certpath) + Returns True if operation succeeded, False if not. + """ d = os.path.dirname(path) if not os.path.exists(d): os.makedirs(d) - - cnf = open(path+".cnf", "w") - cnf.write("[ req ]\n") - cnf.write("prompt = no\n") - cnf.write("distinguished_name = req_distinguished_name\n") - if newca: - cnf.write("x509_extensions = v3_ca\n") - cnf.write("req_extensions = v3_ca_req\n") + cmd = [ + "openssl", + "req", + "-new", + "-x509", + "-config", data.path("resources/ca.cnf"), + "-nodes", + "-days", "9999", + "-out", path, + "-newkey", "rsa:1024", + "-keyout", path, + ] + ret = subprocess.call( + cmd, + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + stdin=subprocess.PIPE + ) + if ret: + return False else: - cnf.write("x509_extensions = v3_cert\n") - cnf.write("req_extensions = v3_cert_req\n") - cnf.write("\n") - cnf.write("[ req_distinguished_name ]\n") - if countryName is not None: - cnf.write("countryName = %s\n" % (countryName) ) - if stateOrProvinceName is not None: - cnf.write("stateOrProvinceName = %s\n" % (stateOrProvinceName) ) - if localityName is not None: - cnf.write("localityName = %s\n" % (localityName) ) - if organizationName is not None: - cnf.write("organizationName = %s\n" % (organizationName) ) - if organizationalUnitName is not None: - cnf.write("organizationalUnitName = %s\n" % (organizationalUnitName) ) - if commonName is not None: - cnf.write("commonName = %s\n" % (commonName) ) - if emailAddress is not None: - cnf.write("emailAddress = %s\n" % (emailAddress) ) - cnf.write("\n") - cnf.write("[ v3_ca ]\n") - cnf.write("subjectKeyIdentifier=hash\n") - cnf.write("authorityKeyIdentifier=keyid:always,issuer\n") - cnf.write("basicConstraints = critical,CA:true\n") - cnf.write("keyUsage = cRLSign, keyCertSign\n") - cnf.write("nsCertType = sslCA\n") - #cnf.write("subjectAltName=email:copy\n") - #cnf.write("issuerAltName=issuer:copy\n") - cnf.write("\n") - cnf.write("[ v3_ca_req ]\n") - cnf.write("basicConstraints = critical,CA:true\n") - cnf.write("keyUsage = cRLSign, keyCertSign\n") - cnf.write("nsCertType = sslCA\n") - #cnf.write("subjectAltName=email:copy\n") - cnf.write("\n") - cnf.write("[ v3_cert ]\n") - cnf.write("basicConstraints = CA:false\n") - cnf.write("keyUsage = nonRepudiation, digitalSignature, keyEncipherment\n") - cnf.write("nsCertType = server\n") - cnf.write("subjectKeyIdentifier=hash\n") - cnf.write("authorityKeyIdentifier=keyid:always,issuer\n") - cnf.write("\n") - cnf.write("[ v3_cert_req ]\n") - cnf.write("basicConstraints = CA:false\n") - cnf.write("keyUsage = nonRepudiation, digitalSignature, keyEncipherment\n") - cnf.write("nsCertType = server\n") - cnf.write("\n") + return True - cnf.close() - - if ca is None: + +def dummy_cert(certdir, ca, commonname): + """ + certdir: Certificate directory. + ca: Path to the certificate authority file, or None. + commonname: Common name for the generated certificate. + + Returns True if operation succeeded, False if not. + """ + confpath = os.path.join(certdir, commonname + ".cnf") + reqpath = os.path.join(certdir, commonname + ".req") + certpath = os.path.join(certdir, commonname + ".pem") + + template = open(data.path("resources/cert.cnf")).read() + f = open(confpath, "w").write(template%(dict(commonname=commonname))) + + if ca: + # Create a dummy signed certificate. Uses same key as the signing CA + cmd = [ + "openssl", + "req", + "-new", + "-config", confpath, + "-out", reqpath, + "-key", ca, + ] + ret = subprocess.call( + cmd, + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + stdin=subprocess.PIPE + ) + if ret: + return False + cmd = [ + "openssl", + "x509", + "-req", + "-in", reqpath, + "-days", "9999", + "-out", certpath, + "-CA", ca, + "-CAcreateserial", + "-extfile", confpath, + "-extensions", "v3_cert", + ] + ret = subprocess.call( + cmd, + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + stdin=subprocess.PIPE + ) + if ret: + return False + else: # Create a new selfsigned certificate + key cmd = [ "openssl", "req", "-new", "-x509", - "-config", path+".cnf", + "-config", confpath, "-nodes", "-days", "9999", "-out", certpath, "-newkey", "rsa:1024", "-keyout", certpath, ] - #print " ".join(cmd) - subprocess.call( + ret = subprocess.call( cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE, stdin=subprocess.PIPE ) - else: - # Create a dummy signed certificate. Uses same key as the signing CA - cmd = [ - "openssl", - "req", - "-new", - "-config", path+".cnf", - "-out", path+".req", - "-key", ca, - ] - #print " ".join(cmd) - subprocess.call( - cmd, - stderr=subprocess.PIPE, - stdout=subprocess.PIPE, - stdin=subprocess.PIPE - ) - cmd = [ - "openssl", - "x509", - "-req", - "-in", path+".req", - "-days", "9999", - "-out", certpath, - "-CA", ca, - "-CAcreateserial", - "-extfile", path+".cnf" - ] - if newca: - cmd.extend([ - "-extensions", "v3_ca", - ]) - else: - cmd.extend([ - "-extensions", "v3_cert", - ]) - - #print " ".join(cmd) - subprocess.call( - cmd, - stderr=subprocess.PIPE, - stdout=subprocess.PIPE, - stdin=subprocess.PIPE - ) - + if ret: + return False + return True + def mkdir_p(path): try: @@ -490,11 +441,11 @@ def certificate_option_group(parser): group = optparse.OptionGroup(parser, "SSL") group.add_option( "--cert", action="store", - type = "str", dest="cert", default="~/.mitmproxy/default.pem", + type = "str", dest="cert", default=None, help = "SSL certificate file." ) group.add_option( - "-c", "--cacert", action="store", + "--cacert", action="store", type = "str", dest="cacert", default="~/.mitmproxy/ca.pem", help = "SSL CA certificate file." ) @@ -510,17 +461,17 @@ def certificate_option_group(parser): ) parser.add_option_group(group) -def process_certificate_option_group(options): + +def process_certificate_option_group(parser, options): if options.cert is not None: options.cert = os.path.expanduser(options.cert) if not os.path.exists(options.cert): - print >> sys.stderr, "Creating bogus certificate at %s"%options.cert - utils.make_bogus_cert(options.cert) + parser.error("Manually created certificate does not exist: %s"%options.cert) if options.cacert is not None: options.cacert = os.path.expanduser(options.cacert) if not os.path.exists(options.cacert): - print >> sys.stderr, "Creating bogus CA certificate at %s"%options.cacert - utils.make_bogus_cert(options.cacert, newca=True, commonName="Dummy CA") + print >> sys.stderr, "Creating dummy CA certificate at %s"%options.cacert + dummy_ca(options.cacert) if options.certpath is not None: options.certpath = os.path.expanduser(options.certpath) elif options.cacert is not None: diff --git a/mitmdump b/mitmdump index 8a8e45b79..582540380 100755 --- a/mitmdump +++ b/mitmdump @@ -54,7 +54,7 @@ if __name__ == '__main__': if options.quiet: options.verbose = 0 - utils.process_certificate_option_group(options) + utils.process_certificate_option_group(parser, options) proxy.config = proxy.Config( certfile = options.cert, diff --git a/mitmplayback b/mitmplayback index 6a98d49f0..ddfe233b0 100755 --- a/mitmplayback +++ b/mitmplayback @@ -56,7 +56,7 @@ if __name__ == '__main__': if options.quiet: options.verbose = 0 - utils.process_certificate_option_group(options) + utils.process_certificate_option_group(parser, options) if options.cache is not None: options.cache = os.path.expanduser(options.cache) diff --git a/mitmproxy b/mitmproxy index 28ff1051d..934b2ec75 100755 --- a/mitmproxy +++ b/mitmproxy @@ -85,7 +85,7 @@ if __name__ == '__main__': options, args = parser.parse_args() - utils.process_certificate_option_group(options) + utils.process_certificate_option_group(parser, options) if options.cache is not None: options.cache = os.path.expanduser(options.cache) diff --git a/mitmrecord b/mitmrecord index 528658347..733761547 100755 --- a/mitmrecord +++ b/mitmrecord @@ -61,7 +61,7 @@ if __name__ == '__main__': if options.quiet: options.verbose = 0 - utils.process_certificate_option_group(options) + utils.process_certificate_option_group(parser, options) proxy.config = proxy.Config( certfile = options.cert, diff --git a/test/test_utils.py b/test/test_utils.py index 5cf81e2ef..a52c8e3b1 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -217,16 +217,6 @@ class uisSequenceLike(libpry.AutoTree): assert not utils.isSequenceLike(1) -class umake_bogus_cert(libpry.AutoTree): - def test_all(self): - d = self.tmpdir() - path = os.path.join(d, "foo", "cert") - utils.make_bogus_cert(path) - - d = open(path).read() - assert "PRIVATE KEY" in d - assert "CERTIFICATE" in d - class upretty_xmlish(libpry.AutoTree): def test_tagre(self): @@ -284,12 +274,42 @@ class upretty_xmlish(libpry.AutoTree): assert utils.pretty_xmlish(s) == ["gobbledygook"] +class udummy_ca(libpry.AutoTree): + def test_all(self): + d = self.tmpdir() + path = os.path.join(d, "foo/cert.cnf") + assert utils.dummy_ca(path) + assert os.path.exists(path) + + +class udummy_cert(libpry.AutoTree): + def test_with_ca(self): + d = self.tmpdir() + cacert = os.path.join(d, "foo/cert.cnf") + assert utils.dummy_ca(cacert) + assert utils.dummy_cert( + os.path.join(d, "foo"), + cacert, + "foo.com" + ) + assert os.path.exists(os.path.join(d, "foo", "foo.com.pem")) + + def test_no_ca(self): + d = self.tmpdir() + assert utils.dummy_cert( + d, + None, + "foo.com" + ) + assert os.path.exists(os.path.join(d, "foo.com.pem")) + + + tests = [ uformat_timestamp(), - umake_bogus_cert(), uisBin(), uhexdump(), upretty_size(), @@ -299,4 +319,6 @@ tests = [ uHeaders(), uData(), upretty_xmlish(), + udummy_ca(), + udummy_cert(), ]