Merge branch 'upstream-cert'

This commit is contained in:
Aldo Cortesi 2012-02-28 10:55:37 +13:00
commit a03e1af7e7
9 changed files with 289 additions and 18 deletions

View File

@ -141,6 +141,17 @@ def common_options(parser):
help="Byte size limit of HTTP request and response bodies."\ help="Byte size limit of HTTP request and response bodies."\
" Understands k/m/g suffixes, i.e. 3m for 3 megabytes." " 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 = optparse.OptionGroup(parser, "Client Replay")
group.add_option( group.add_option(
"-c", "-c",
@ -149,12 +160,6 @@ def common_options(parser):
) )
parser.add_option_group(group) 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 = optparse.OptionGroup(parser, "Server Replay")
group.add_option( group.add_option(
"-S", "-S",

View File

@ -35,12 +35,13 @@ class ProxyError(Exception):
class ProxyConfig: 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.certfile = certfile
self.ciphers = ciphers self.ciphers = ciphers
self.cacert = cacert self.cacert = cacert
self.certdir = None self.certdir = None
self.cert_wait_time = cert_wait_time self.cert_wait_time = cert_wait_time
self.upstream_cn_lookup = upstream_cn_lookup
self.body_size_limit = body_size_limit self.body_size_limit = body_size_limit
self.reverse_proxy = reverse_proxy self.reverse_proxy = reverse_proxy
@ -343,11 +344,14 @@ class ProxyHandler(SocketServer.StreamRequestHandler):
if server: if server:
server.terminate() server.terminate()
def find_cert(self, host): def find_cert(self, host, port):
if self.config.certfile: if self.config.certfile:
return self.config.certfile return self.config.certfile
else: 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) time.sleep(self.config.cert_wait_time)
if not ret: if not ret:
raise ProxyError(502, "mitmproxy: Unable to generate dummy cert.") raise ProxyError(502, "mitmproxy: Unable to generate dummy cert.")
@ -374,7 +378,7 @@ class ProxyHandler(SocketServer.StreamRequestHandler):
) )
self.wfile.flush() self.wfile.flush()
kwargs = dict( kwargs = dict(
certfile = self.find_cert(host), certfile = self.find_cert(host, port),
keyfile = self.config.certfile or self.config.cacert, keyfile = self.config.certfile or self.config.cacert,
server_side = True, server_side = True,
ssl_version = ssl.PROTOCOL_SSLv23, ssl_version = ssl.PROTOCOL_SSLv23,
@ -538,5 +542,6 @@ def process_proxy_options(parser, options):
ciphers = options.ciphers, ciphers = options.ciphers,
cert_wait_time = options.cert_wait_time, cert_wait_time = options.cert_wait_time,
body_size_limit = body_size_limit, body_size_limit = body_size_limit,
upstream_cn_lookup = options.upstream_cn_lookup,
reverse_proxy = rp reverse_proxy = rp
) )

View File

@ -27,4 +27,7 @@ nsCertType = server
basicConstraints = CA:false basicConstraints = CA:false
keyUsage = nonRepudiation, digitalSignature, keyEncipherment keyUsage = nonRepudiation, digitalSignature, keyEncipherment
nsCertType = server nsCertType = server
%(altnames)s
[ alt_names ]
%(sans)s

View File

@ -12,8 +12,8 @@
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import re, os, subprocess, datetime, urlparse, string, urllib import re, os, subprocess, datetime, urlparse, string, urllib, socket
import time, functools, cgi, textwrap, hashlib import time, functools, cgi, textwrap, hashlib, ssl, tempfile
import json import json
CERT_SLEEP_TIME = 1 CERT_SLEEP_TIME = 1
@ -276,7 +276,7 @@ def dummy_ca(path):
return True return True
def dummy_cert(certdir, ca, commonname): def dummy_cert(certdir, ca, commonname, sans):
""" """
certdir: Certificate directory. certdir: Certificate directory.
ca: Path to the certificate authority file, or None. ca: Path to the certificate authority file, or None.
@ -293,8 +293,22 @@ def dummy_cert(certdir, ca, commonname):
reqpath = os.path.join(certdir, namehash + ".req") reqpath = os.path.join(certdir, namehash + ".req")
template = open(pkg_data.path("resources/cert.cnf")).read() template = open(pkg_data.path("resources/cert.cnf")).read()
ss = []
for i, v in enumerate(sans):
ss.append("DNS.%s = %s"%(i+1, v))
ss = "\n".join(ss)
f = open(confpath, "w") f = open(confpath, "w")
f.write(template%(dict(commonname=commonname))) f.write(
template%(
dict(
commonname=commonname,
sans=ss,
altnames="subjectAltName = @alt_names" if ss else ""
)
)
)
f.close() f.close()
if ca: if ca:
@ -324,7 +338,7 @@ def dummy_cert(certdir, ca, commonname):
"-CA", ca, "-CA", ca,
"-CAcreateserial", "-CAcreateserial",
"-extfile", confpath, "-extfile", confpath,
"-extensions", "v3_cert", "-extensions", "v3_cert_req",
] ]
ret = subprocess.call( ret = subprocess.call(
cmd, cmd,
@ -483,3 +497,60 @@ def parse_size(s):
return int(s) * mult return int(s) * mult
except ValueError: except ValueError:
raise ValueError("Invalid size specification: %s"%s) 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=([^ \t\n\r\f\v/]*)
""",
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)

48
test/data/text_cert Normal file

File diff suppressed because one or more lines are too long

83
test/data/text_cert_2 Normal file
View File

@ -0,0 +1,83 @@
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 67908 (0x10944)
Signature Algorithm: sha1WithRSAEncryption
Issuer: C=IL, O=StartCom Ltd., OU=Secure Digital Certificate Signing, CN=StartCom Class 1 Primary Intermediate Server CA
Validity
Not Before: Jan 11 19:27:36 2010 GMT
Not After : Jan 12 09:14:55 2011 GMT
Subject: description=126832-MCxLsY6Tn1gm7o90, C=NZ, O=Persona Not Validated, OU=StartCom Free Certificate Member, CN=www.inode.co.nz/emailAddress=jim@inode.co.nz
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public Key: (2048 bit)
Modulus (2048 bit):
00:be:a0:85:69:46:86:a8:3e:57:43:f9:f1:1d:d2:
bc:b4:62:3b:d3:ab:75:e9:03:c7:39:90:6c:ac:10:
71:cd:39:7e:b9:c5:e5:9f:6f:4f:a8:1a:ed:83:ae:
5d:57:97:c6:16:02:02:8d:27:af:a2:88:fb:2f:cb:
c7:e4:07:bc:fe:56:d2:72:9b:b3:eb:2f:cd:44:04:
ae:ad:99:64:5c:21:87:57:b5:2d:b5:5c:72:b3:70:
90:75:33:28:ea:b4:4d:36:95:75:4e:24:f2:40:d0:
cf:36:05:f6:d4:d9:6f:e1:b9:1e:fd:41:26:3a:70:
63:07:bc:3f:87:00:88:f2:4f:ac:51:4e:29:3c:92:
cc:7a:a2:62:c5:e5:c7:eb:d1:d6:5d:5e:df:32:9a:
be:17:7e:2d:54:a8:2d:65:97:6f:5d:f2:28:e0:ea:
ea:f9:ee:88:78:d4:25:79:7c:09:f5:65:06:1a:06:
a2:d5:d4:e0:94:79:71:bb:86:a1:6f:fc:fa:b9:30:
11:d5:fe:62:80:af:54:2a:a0:77:1f:48:91:11:41:
ea:e5:9f:37:aa:1c:52:21:6f:84:0d:1e:92:73:be:
7b:0b:95:7d:12:e2:21:a4:83:07:ca:4d:c9:45:95:
aa:ee:27:80:55:ad:58:ed:4e:61:98:34:23:fd:f6:
06:47
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Basic Constraints:
CA:FALSE
X509v3 Key Usage:
Digital Signature, Key Encipherment, Key Agreement
X509v3 Extended Key Usage:
TLS Web Server Authentication
X509v3 Subject Key Identifier:
1F:68:BD:91:8F:AA:FC:89:19:41:4E:0A:69:80:4E:D9:67:95:93:53
X509v3 Authority Key Identifier:
keyid:EB:42:34:D0:98:B0:AB:9F:F4:1B:6B:08:F7:CC:64:2E:EF:0E:2C:45
X509v3 Subject Alternative Name:
DNS:www.inode.co.nz, DNS:inode.co.nz
X509v3 Certificate Policies:
Policy: 1.3.6.1.4.1.23223.1.2.1
CPS: http://www.startssl.com/policy.pdf
CPS: http://www.startssl.com/intermediate.pdf
User Notice:
Organization: StartCom Ltd.
Number: 1
Explicit Text: Limited Liability, see section *Legal Limitations* of the StartCom Certification Authority Policy available at http://www.startssl.com/policy.pdf
X509v3 CRL Distribution Points:
URI:http://www.startssl.com/crt1-crl.crl
URI:http://crl.startssl.com/crt1-crl.crl
Authority Information Access:
OCSP - URI:http://ocsp.startssl.com/sub/class1/server/ca
CA Issuers - URI:http://www.startssl.com/certs/sub.class1.server.ca.crt
X509v3 Issuer Alternative Name:
URI:http://www.startssl.com/
Signature Algorithm: sha1WithRSAEncryption
8a:f5:88:0f:42:93:f2:ad:44:cd:6c:f2:f8:17:9c:b0:5a:f2:
85:08:6e:2c:57:c0:b0:f9:07:aa:93:5d:f4:e7:e4:45:2b:46:
06:e2:65:2d:7f:bd:0d:0f:53:ce:3a:ae:b5:d6:36:6e:e3:41:
b8:39:fb:d5:bb:99:2b:e6:39:7e:68:eb:db:ca:c0:4f:af:96:
31:df:8a:9c:29:79:58:b4:f9:20:e4:ba:67:db:37:71:e7:39:
4c:54:1b:98:e1:a5:7a:94:92:f5:0e:93:b3:d2:ac:5c:e1:c7:
42:f0:e4:41:e0:ea:39:5d:94:a9:d8:17:20:0c:85:9b:97:9f:
48:bf:49:5c:f5:e7:b5:16:ae:b0:db:b6:b3:c1:76:07:6a:d1:
25:ee:23:3d:f9:d4:cb:62:ef:63:c3:96:d5:8b:e3:50:0f:be:
be:97:56:45:d7:d4:6c:94:29:e7:99:91:7d:a6:0c:3d:19:f2:
52:6d:f4:94:02:36:ed:06:b1:81:03:c5:3a:a0:de:65:49:b9:
96:56:f9:78:51:bf:07:77:fc:ee:e8:60:f9:8d:68:13:29:1b:
12:f6:6b:20:53:78:86:2b:61:8a:84:34:b5:bf:b9:ec:52:a4:
90:6d:ac:5b:7b:9e:98:d8:90:c3:6a:ab:12:8f:c6:00:c6:f5:
70:8e:74:46

View File

@ -149,14 +149,17 @@ class udummy_cert(libpry.AutoTree):
p = utils.dummy_cert( p = utils.dummy_cert(
os.path.join(d, "foo"), os.path.join(d, "foo"),
cacert, cacert,
"foo.com" "foo.com",
["one.com", "two.com", "*.three.com"]
) )
assert os.path.exists(p) assert os.path.exists(p)
# Short-circuit # Short-circuit
assert utils.dummy_cert( assert utils.dummy_cert(
os.path.join(d, "foo"), os.path.join(d, "foo"),
cacert, cacert,
"foo.com" "foo.com",
[]
) )
def test_no_ca(self): def test_no_ca(self):
@ -164,7 +167,8 @@ class udummy_cert(libpry.AutoTree):
p = utils.dummy_cert( p = utils.dummy_cert(
d, d,
None, None,
"foo.com" "foo.com",
[]
) )
assert os.path.exists(p) assert os.path.exists(p)
@ -255,7 +259,22 @@ class u_parse_size(libpry.AutoTree):
libpry.raises(ValueError, utils.parse_size, "ak") libpry.raises(ValueError, utils.parse_size, "ak")
class uparse_text_cert(libpry.AutoTree):
def test_simple(self):
c = file("data/text_cert", "r").read()
cn, san = utils.parse_text_cert(c)
assert cn == "google.com"
assert len(san) == 436
c = file("data/text_cert_2", "r").read()
cn, san = utils.parse_text_cert(c)
assert cn == "www.inode.co.nz"
assert len(san) == 2
tests = [ tests = [
uparse_text_cert(),
uformat_timestamp(), uformat_timestamp(),
uisBin(), uisBin(),
uisXML(), uisXML(),

24
test/tools/getcert Executable file
View File

@ -0,0 +1,24 @@
#!/usr/bin/env python
import sys
sys.path.insert(0, "../..")
import socket, tempfile, ssl, subprocess
addr = socket.gethostbyname(sys.argv[1])
s = ssl.get_server_certificate((addr, 443))
f = tempfile.NamedTemporaryFile()
f.write(s)
f.flush()
p = subprocess.Popen(
[
"openssl",
"x509",
"-in", f.name,
"-text",
"-noout"
],
stdout = subprocess.PIPE
)
out, _ = p.communicate()
print out

13
test/tools/getcn Executable file
View File

@ -0,0 +1,13 @@
#!/usr/bin/env python
import sys
sys.path.insert(0, "../..")
from libmproxy import utils
cn, san = utils.get_remote_cn(sys.argv[1], 443)
print cn
if san:
for i in san:
print "\t", i