From ab1d8fa3500f786528c5c6a3e2bba19cd96595d0 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Mon, 2 Apr 2012 16:19:00 +1200 Subject: Expand SSL cert support - Capture the remote SSL certificate - Expose the remote cert as an attribute on Response - Expand the certutils.SSLCert interface to expose more cert info --- libmproxy/certutils.py | 44 +++++++++++++++++++++++++++++++++++++++++--- libmproxy/flow.py | 6 +++++- libmproxy/proxy.py | 5 ++++- libmproxy/version.py | 2 +- setup.py | 2 +- test/data/dercert | Bin 0 -> 1838 bytes test/test_certutils.py | 16 ++++++++++++++-- test/test_filt.py | 3 ++- test/test_flow.py | 6 +++--- test/tutils.py | 2 +- 10 files changed, 72 insertions(+), 14 deletions(-) create mode 100644 test/data/dercert diff --git a/libmproxy/certutils.py b/libmproxy/certutils.py index aae0d7ab..5fbc9840 100644 --- a/libmproxy/certutils.py +++ b/libmproxy/certutils.py @@ -1,4 +1,4 @@ -import subprocess, os, ssl, hashlib, socket, time +import os, ssl, hashlib, socket, time from pyasn1.type import univ, constraint, char, namedtype, tag from pyasn1.codec.der.decoder import decode import OpenSSL @@ -136,7 +136,6 @@ class _GeneralNames(univ.SequenceOf): sizeSpec = univ.SequenceOf.sizeSpec + constraint.ValueSizeConstraint(1, 1024) - class SSLCert: def __init__(self, pemtxt): """ @@ -144,6 +143,46 @@ class SSLCert: """ self.cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, pemtxt) + @classmethod + def from_der(klass, der): + pem = ssl.DER_cert_to_PEM_cert(der) + return klass(pem) + + def digest(self, name): + return self.cert.digest(name) + + @property + def notbefore(self): + return self.cert.get_notBefore() + + @property + def notafter(self): + return self.cert.get_notAfter() + + @property + def has_expired(self): + return self.cert.has_expired() + + @property + def subject(self): + return self.cert.get_subject().get_components() + + @property + def serial(self): + return self.cert.get_serial_number() + + @property + def keyinfo(self): + pk = self.cert.get_pubkey() + types = { + OpenSSL.crypto.TYPE_RSA: "RSA", + OpenSSL.crypto.TYPE_DSA: "DSA", + } + return ( + types.get(pk.type(), "UNKNOWN"), + pk.bits() + ) + @property def cn(self): cn = None @@ -171,4 +210,3 @@ def get_remote_cert(host, port): return SSLCert(s) # end nocover - diff --git a/libmproxy/flow.py b/libmproxy/flow.py index bb079a8d..6ffef6f2 100644 --- a/libmproxy/flow.py +++ b/libmproxy/flow.py @@ -566,11 +566,12 @@ class Response(HTTPMsg): content: Response content timestamp: Seconds since the epoch """ - def __init__(self, request, code, msg, headers, content, timestamp=None): + def __init__(self, request, code, msg, headers, content, peercert, timestamp=None): assert isinstance(headers, ODictCaseless) self.request = request self.code, self.msg = code, msg self.headers, self.content = headers, content + self.peercert = peercert self.timestamp = timestamp or utils.timestamp() controller.Msg.__init__(self) self.replay = False @@ -640,6 +641,7 @@ class Response(HTTPMsg): self.headers = ODictCaseless._from_state(state["headers"]) self.content = state["content"] self.timestamp = state["timestamp"] + self.peercert = state["peercert"] def _get_state(self): return dict( @@ -647,6 +649,7 @@ class Response(HTTPMsg): msg = self.msg, headers = self.headers._get_state(), timestamp = self.timestamp, + peercert = self.peercert, content = self.content ) @@ -658,6 +661,7 @@ class Response(HTTPMsg): str(state["msg"]), ODictCaseless._from_state(state["headers"]), state["content"], + state.get("peercert"), state["timestamp"], ) diff --git a/libmproxy/proxy.py b/libmproxy/proxy.py index c6a68ba3..a6db44c2 100644 --- a/libmproxy/proxy.py +++ b/libmproxy/proxy.py @@ -229,6 +229,7 @@ class ServerConnection: self.port = request.port self.scheme = request.scheme self.close = False + self.cert = None self.server, self.rfile, self.wfile = None, None, None self.connect() @@ -239,6 +240,8 @@ class ServerConnection: if self.scheme == "https": server = ssl.wrap_socket(server) server.connect((addr, self.port)) + if self.scheme == "https": + self.cert = server.getpeercert(True) except socket.error, err: raise ProxyError(502, 'Error connecting to "%s": %s' % (self.host, err)) self.server = server @@ -275,7 +278,7 @@ class ServerConnection: content = "" else: content = read_http_body(self.rfile, self, headers, True, self.config.body_size_limit) - return flow.Response(self.request, code, msg, headers, content) + return flow.Response(self.request, code, msg, headers, content, self.cert) def terminate(self): try: diff --git a/libmproxy/version.py b/libmproxy/version.py index ad9798a3..970a7181 100644 --- a/libmproxy/version.py +++ b/libmproxy/version.py @@ -1,2 +1,2 @@ -IVERSION = (0, 7) +IVERSION = (0, 8) VERSION = ".".join(str(i) for i in IVERSION) diff --git a/setup.py b/setup.py index a26a25fd..d15339c1 100644 --- a/setup.py +++ b/setup.py @@ -92,5 +92,5 @@ setup( "Topic :: Internet :: Proxy Servers", "Topic :: Software Development :: Testing" ], - install_requires=['urwid', 'pyasn1', 'pyopenssl', "PIL"], + install_requires=['urwid>=1.0', 'pyasn1', 'pyopenssl>=0.12', "PIL"], ) diff --git a/test/data/dercert b/test/data/dercert new file mode 100644 index 00000000..370252af Binary files /dev/null and b/test/data/dercert differ diff --git a/test/test_certutils.py b/test/test_certutils.py index 5ef5919e..e27088e7 100644 --- a/test/test_certutils.py +++ b/test/test_certutils.py @@ -49,7 +49,7 @@ class udummy_cert(libpry.AutoTree): assert os.path.exists(p) -class uparse_text_cert(libpry.AutoTree): +class uSSLCert(libpry.AutoTree): def test_simple(self): c = certutils.SSLCert(file("data/text_cert", "r").read()) assert c.cn == "google.com" @@ -58,10 +58,22 @@ class uparse_text_cert(libpry.AutoTree): c = certutils.SSLCert(file("data/text_cert_2", "r").read()) assert c.cn == "www.inode.co.nz" assert len(c.altnames) == 2 + assert c.digest("sha1") + assert c.notbefore + assert c.notafter + assert c.subject + assert c.keyinfo == ("RSA", 2048) + assert c.serial + c.has_expired + + def test_der(self): + d = file("data/dercert").read() + s = certutils.SSLCert.from_der(d) + assert s.cn tests = [ - uparse_text_cert(), udummy_ca(), udummy_cert(), + uSSLCert(), ] diff --git a/test/test_filt.py b/test/test_filt.py index 61acbc7d..8e349d7c 100644 --- a/test/test_filt.py +++ b/test/test_filt.py @@ -98,7 +98,8 @@ class uMatching(libpry.AutoTree): 200, "message", headers, - "content_response" + "content_response", + None ) return f diff --git a/test/test_flow.py b/test/test_flow.py index e44e2b0d..d19518e5 100644 --- a/test/test_flow.py +++ b/test/test_flow.py @@ -796,7 +796,7 @@ class uResponse(libpry.AutoTree): h["test"] = ["test"] c = flow.ClientConnect(("addr", 2222)) req = flow.Request(c, "host", 22, "https", "GET", "/", h, "content") - resp = flow.Response(req, 200, "msg", h.copy(), "content") + resp = flow.Response(req, 200, "msg", h.copy(), "content", None) assert resp._assemble() resp2 = resp.copy() @@ -841,12 +841,12 @@ class uResponse(libpry.AutoTree): h["test"] = ["test"] c = flow.ClientConnect(("addr", 2222)) req = flow.Request(c, "host", 22, "https", "GET", "/", h, "content") - resp = flow.Response(req, 200, "msg", h.copy(), "content") + resp = flow.Response(req, 200, "msg", h.copy(), "content", None) state = resp._get_state() assert flow.Response._from_state(req, state) == resp - resp2 = flow.Response(req, 220, "foo", h.copy(), "test") + resp2 = flow.Response(req, 220, "foo", h.copy(), "test", None) assert not resp == resp2 resp._load_state(resp2._get_state()) assert resp == resp2 diff --git a/test/tutils.py b/test/tutils.py index b11c997a..25a76c0e 100644 --- a/test/tutils.py +++ b/test/tutils.py @@ -17,7 +17,7 @@ def tresp(req=None): req = treq() headers = flow.ODictCaseless() headers["header_response"] = ["svalue"] - return flow.Response(req, 200, "message", headers, "content_response") + return flow.Response(req, 200, "message", headers, "content_response", None) def tflow(): -- cgit v1.2.3