source: trunk/lib/bletchley/ssltls.py @ 102

Last change on this file since 102 was 88, checked in by tim, 8 years ago

digest fixes

File size: 7.2 KB
Line 
1'''
2Utilities for manipulating certificates and SSL/TLS connections.
3
4Copyright (C) 2014,2016 Blindspot Security LLC
5Author: Timothy D. Morgan
6
7 This program is free software: you can redistribute it and/or modify
8 it under the terms of the GNU Lesser General Public License, version 3,
9 as published by the Free Software Foundation.
10
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with this program.  If not, see <http://www.gnu.org/licenses/>.
18'''
19
20import sys
21import argparse
22import traceback
23import socket
24try:
25    import OpenSSL
26    from OpenSSL import SSL
27except:
28    sys.stderr.write('ERROR: Could not locate pyOpenSSL module.  Under Debian-based systems, try:\n')
29    sys.stderr.write('       # apt-get install python3-openssl\n')
30    sys.stderr.write('NOTE: pyOpenSSL version 0.14 or later is required!\n')
31    sys.exit(2)
32try:
33    import cffi
34except:
35    sys.stderr.write('ERROR: Could not locate cffi module.  Under Debian-based systems, try:\n')
36    sys.stderr.write('       # apt-get install python3-cffi\n')
37    sys.stderr.write('NOTE: This is a requirement because pyOpenSSL does not provide '
38                     'certificate extension removal procedures.  Consider lobbying for the '
39                     'implementation of this:\n  https://github.com/pyca/pyopenssl/issues/152\n')
40    sys.exit(2)
41
42
43def createContext(method=SSL.TLSv1_METHOD, key=None, certChain=[]):
44    context = SSL.Context(method)
45    context.set_verify(SSL.VERIFY_NONE, (lambda a,b,c,d,e: True))
46    if key and len(certChain) > 0:
47        context.use_privatekey(key)
48        context.use_certificate(certChain[0])
49        for c in certChain[1:]:
50            context.add_extra_chain_cert(c)
51   
52    return context
53
54
55def startSSLTLS(sock, mode='client', handshake=SSL.TLSv1_METHOD, key=None, certChain=[]):
56    conn = SSL.Connection(createContext(handshake, key=key, certChain=certChain), sock)
57    if mode == 'client':
58        conn.set_connect_state()
59        conn.do_handshake()
60    else:
61        conn.set_accept_state()
62   
63    return conn
64
65
66def ConnectSSLTLS(host, port):
67    protocols = [("SSL 2/3", SSL.SSLv23_METHOD),
68                 ("TLS 1.0", SSL.TLSv1_METHOD), 
69                 ("TLS 1.1", SSL.TLSv1_1_METHOD),
70                 ("TLS 1.2", SSL.TLSv1_2_METHOD),
71                 ("SSL 3.0", SSL.SSLv3_METHOD),
72                 ("SSL 2.0", SSL.SSLv2_METHOD)]
73
74    conn = None
75    for pname,p in protocols:
76        serverSock = socket.socket()
77        serverSock.connect((host,port))
78       
79        try:
80            conn = startSSLTLS(serverSock, mode='client', handshake=p)
81            break
82        except ValueError as e:
83            sys.stderr.write("%s handshake not supported by your openssl library, trying others...\n" % pname)
84        except SSL.Error as e:
85            sys.stderr.write("Exception during %s handshake with server." % pname)
86            sys.stderr.write("\nThis could happen because the server requires "
87                             "certain SSL/TLS versions or a client certificiate."
88                             "  Have no fear, we'll keep trying...\n")           
89        except Exception as e:
90            sys.stderr.write("Unknown exception during handshake with server: \n")
91            traceback.print_exc(file=sys.stderr)
92
93    return conn
94
95
96def fetchCertificateChain(connection):
97    chain = connection.get_peer_cert_chain()
98    if chain:
99        return chain
100    return None
101
102
103def normalizeCertificateName(cert_name):
104    n = cert_name.get_components()
105    n.sort()
106    return tuple(n)
107
108
109def normalizeCertificateChain(chain):
110    # Organize certificates by subject and issuer for quick lookups
111    subject_table = {}
112    issuer_table = {}
113    for c in chain:
114        subject_table[normalizeCertificateName(c.get_subject())] = c
115        issuer_table[normalizeCertificateName(c.get_issuer())] = c
116
117    # Now find root or highest-level intermediary
118    root = None
119    for c in chain:
120        i = normalizeCertificateName(c.get_issuer())
121        s = normalizeCertificateName(c.get_subject())
122        if (i == s) or (i not in subject_table):
123            if root != None:
124                sys.stderr.write("WARN: Multiple root certificates found or broken certificate chain detected.")
125            else:
126                # Go with the first identified "root", since that's more likely to link up with the server cert
127                root = c
128
129    # Finally, build the chain from the top-down in the correct order
130    new_chain = []
131    nxt = root
132    while nxt != None:
133        new_chain = [nxt] + new_chain
134        s = normalizeCertificateName(nxt.get_subject())
135        nxt = issuer_table.get(s)
136   
137    return new_chain
138   
139
140def genFakeKey(certificate):
141    fake_key = OpenSSL.crypto.PKey()
142    old_pubkey = certificate.get_pubkey()
143    fake_key.generate_key(old_pubkey.type(), old_pubkey.bits())
144
145    return fake_key
146
147
148def getDigestAlgorithm(certificate):
149    # XXX: ugly hack because openssl API for this is limited
150    algo = certificate.get_signature_algorithm()
151    if b'With' in algo:
152        return algo.split(b'With', 1)[0].decode('utf-8')
153    return None
154
155
156def deleteExtension(certificate, index):
157    '''
158    A dirty hack until this is implemented in pyOpenSSL. See:
159    https://github.com/pyca/pyopenssl/issues/152
160    '''
161    ffi = cffi.FFI()
162    ffi.cdef('''void* X509_delete_ext(void* x, int loc);''')
163    libssl = ffi.dlopen('libssl.so')
164    ext = libssl.X509_delete_ext(certificate._x509, index)
165    #XXX: memory leak.  supposed to free ext here
166
167
168def removePeskyExtensions(certificate):
169    #for index in range(0,certificate.get_extension_count()):
170    #    e = certificate.get_extension(index)
171    #    print("extension %d: %s\n" % (index, e.get_short_name()), e)
172
173    index = 0
174    while index < certificate.get_extension_count():
175        e = certificate.get_extension(index)
176        if e.get_short_name() in (b'subjectKeyIdentifier', b'authorityKeyIdentifier'):
177            deleteExtension(certificate, index)
178            #XXX: would be nice if each of these extensions were re-added with appropriate values
179            index -= 1
180        index += 1
181   
182    #for index in range(0,certificate.get_extension_count()):
183    #    e = certificate.get_extension(index)
184    #    print("extension %d: %s\n" % (index, e.get_short_name()), e)
185
186
187def genFakeCertificateChain(cert_chain):
188    ret_val = []
189    cert_chain.reverse() # start with highest level authority
190
191    c = cert_chain[0]
192    i = normalizeCertificateName(c.get_issuer())
193    s = normalizeCertificateName(c.get_subject())
194    if s != i:
195        # XXX: consider retrieving root locally and including a forged version instead
196        c.set_issuer(c.get_subject())
197    k = genFakeKey(c)
198    c.set_pubkey(k)
199    removePeskyExtensions(c)
200    c.sign(k, getDigestAlgorithm(c))
201    ret_val.append(c)
202
203    prev = k
204    for c in cert_chain[1:]:
205        k = genFakeKey(c)
206        c.set_pubkey(k)
207        removePeskyExtensions(c)
208        c.sign(prev, getDigestAlgorithm(c))
209        prev = k
210        ret_val.append(c)
211
212    ret_val.reverse()
213    return k,ret_val
Note: See TracBrowser for help on using the repository browser.