- bump version to 0.130.1
[opensuse:osc.git] / osc / oscssl.py
1 # Copyright (C) 2009 Novell Inc.
2 # This program is free software; it may be used, copied, modified
3 # and distributed under the terms of the GNU General Public Licence,
4 # either version 2, or (at your option) any later version.
5
6 import M2Crypto.httpslib
7 from M2Crypto.SSL.Checker import SSLVerificationError
8 from M2Crypto import m2, SSL
9 import M2Crypto.m2urllib2
10 import urlparse
11 import socket
12 import urllib
13 import httplib
14
15 class TrustedCertStore:
16     _tmptrusted = {}
17
18     def __init__(self, host, port, app, cert):
19
20         self.cert = cert
21         self.host = host
22         if self.host == None:
23             raise Exception("empty host")
24         if port:
25             self.host += "_%d" % port
26         import os
27         self.dir = os.path.expanduser('~/.config/%s/trusted-certs' % app)
28         self.file = self.dir + '/%s.pem' % self.host
29
30     def is_known(self):
31         if self.host in self._tmptrusted:
32             return True
33
34         import os
35         if os.path.exists(self.file):
36             return True
37         return False
38
39     def is_trusted(self):
40         import os
41         if self.host in self._tmptrusted:
42             cert = self._tmptrusted[self.host]
43         else:
44             if not os.path.exists(self.file):
45                 return False
46             from M2Crypto import X509
47             cert = X509.load_cert(self.file)
48         if self.cert.as_pem() == cert.as_pem():
49             return True
50         else:
51             return False
52
53     def trust_tmp(self):
54         self._tmptrusted[self.host] = self.cert
55
56     def trust_always(self):
57         self.trust_tmp()
58         from M2Crypto import X509
59         import os
60         if not os.path.exists(self.dir):
61             os.makedirs(self.dir)
62         self.cert.save_pem(self.file)
63
64
65 # verify_cb is called for each error once
66 # we only collect the errors and return suceess
67 # connection will be aborted later if it needs to
68 def verify_cb(ctx, ok, store):
69     if not ctx.verrs:
70         ctx.verrs = ValidationErrors()
71
72     try:
73         if not ok:
74             ctx.verrs.record(store.get_current_cert(), store.get_error(), store.get_error_depth())
75         return 1
76
77     except Exception, e:
78         print e
79         return 0
80
81 class FailCert:
82     def __init__(self, cert):
83         self.cert = cert
84         self.errs = []
85
86 class ValidationErrors:
87
88     def __init__(self):
89         self.chain_ok = True
90         self.cert_ok = True
91         self.failures = {}
92
93     def record(self, cert, err, depth):
94         #print "cert for %s, level %d fail(%d)" % ( cert.get_subject().commonName, depth, err )
95         if depth == 0:
96             self.cert_ok = False
97         else:
98             self.chain_ok = False
99
100         if not depth in self.failures:
101             self.failures[depth] = FailCert(cert)
102         else:
103             if self.failures[depth].cert.get_fingerprint() != cert.get_fingerprint():
104                 raise Exception("Certificate changed unexpectedly. This should not happen")
105         self.failures[depth].errs.append(err)
106
107     def show(self):
108         for depth in self.failures.keys():
109             cert = self.failures[depth].cert
110             print "*** certificate verify failed at depth %d" % depth
111             print "Subject: ", cert.get_subject()
112             print "Issuer:  ", cert.get_issuer()
113             print "Valid: ", cert.get_not_before(), "-", cert.get_not_after()
114             print "Fingerprint(MD5):  ", cert.get_fingerprint('md5')
115             print "Fingerprint(SHA1): ", cert.get_fingerprint('sha1')
116
117             for err in self.failures[depth].errs:
118                 reason = "Unknown"
119                 try:
120                     import M2Crypto.Err
121                     reason = M2Crypto.Err.get_x509_verify_error(err)
122                 except:
123                     pass
124                 print "Reason:", reason
125
126     # check if the encountered errors could be ignored
127     def could_ignore(self):
128         if not 0 in self.failures:
129             return True
130
131         from M2Crypto import m2
132         nonfatal_errors = [
133                 m2.X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY,
134                 m2.X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN,
135                 m2.X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT,
136                 m2.X509_V_ERR_CERT_UNTRUSTED,
137                 m2.X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE,
138
139                 m2.X509_V_ERR_CERT_NOT_YET_VALID,
140                 m2.X509_V_ERR_CERT_HAS_EXPIRED,
141                 m2.X509_V_OK,
142                 ]
143
144         canignore = True
145         for err in self.failures[0].errs:
146             if not err in nonfatal_errors:
147                 canignore = False
148                 break
149
150         return canignore
151
152 class mySSLContext(SSL.Context):
153
154     def __init__(self):
155         SSL.Context.__init__(self, 'sslv23')
156         self.set_options(m2.SSL_OP_ALL | m2.SSL_OP_NO_SSLv2) # m2crypto does this for us but better safe than sorry
157         self.verrs = None
158         #self.set_info_callback() # debug
159         self.set_verify(SSL.verify_peer | SSL.verify_fail_if_no_peer_cert, depth=9, callback=lambda ok, store: verify_cb(self, ok, store))
160
161 class myHTTPSHandler(M2Crypto.m2urllib2.HTTPSHandler):
162     handler_order = 499
163
164     def __init__(self, *args, **kwargs):
165         self.appname = kwargs.pop('appname', 'generic')
166         M2Crypto.m2urllib2.HTTPSHandler.__init__(self, *args, **kwargs)
167
168     # copied from M2Crypto.m2urllib2.HTTPSHandler
169     # it's sole purpose is to use our myHTTPSHandler/myHTTPSProxyHandler class
170     # ideally the m2urllib2.HTTPSHandler.https_open() method would be split into
171     # "do_open()" and "https_open()" so that we just need to override
172     # the small "https_open()" method...)
173     def https_open(self, req):
174         host = req.get_host()
175         if not host:
176             raise M2Crypto.m2urllib2.URLError('no host given: ' + req.get_full_url())
177
178         # Our change: Check to see if we're using a proxy.
179         # Then create an appropriate ssl-aware connection.
180         full_url = req.get_full_url()
181         target_host = urlparse.urlparse(full_url)[1]
182
183         if (target_host != host):
184             h = myProxyHTTPSConnection(host = host, appname = self.appname, ssl_context = self.ctx)
185             # M2Crypto.ProxyHTTPSConnection.putrequest expects a fullurl
186             selector = full_url
187         else:
188             h = myHTTPSConnection(host = host, appname = self.appname, ssl_context = self.ctx)
189             selector = req.get_selector()
190         # End our change
191         h.set_debuglevel(self._debuglevel)
192
193         headers = dict(req.headers)
194         headers.update(req.unredirected_hdrs)
195         # We want to make an HTTP/1.1 request, but the addinfourl
196         # class isn't prepared to deal with a persistent connection.
197         # It will try to read all remaining data from the socket,
198         # which will block while the server waits for the next request.
199         # So make sure the connection gets closed after the (only)
200         # request.
201         headers["Connection"] = "close"
202         try:
203             h.request(req.get_method(), selector, req.data, headers)
204             r = h.getresponse()
205         except socket.error, err: # XXX what error?
206             err.filename = full_url
207             raise M2Crypto.m2urllib2.URLError(err)
208
209         # Pick apart the HTTPResponse object to get the addinfourl
210         # object initialized properly.
211
212         # Wrap the HTTPResponse object in socket's file object adapter
213         # for Windows.  That adapter calls recv(), so delegate recv()
214         # to read().  This weird wrapping allows the returned object to
215         # have readline() and readlines() methods.
216
217         # XXX It might be better to extract the read buffering code
218         # out of socket._fileobject() and into a base class.
219
220         r.recv = r.read
221         fp = socket._fileobject(r)
222
223         resp = urllib.addinfourl(fp, r.msg, req.get_full_url())
224         resp.code = r.status
225         resp.msg = r.reason
226         return resp
227
228 class myHTTPSConnection(M2Crypto.httpslib.HTTPSConnection):
229     def __init__(self, *args, **kwargs):
230         self.appname = kwargs.pop('appname', 'generic')
231         M2Crypto.httpslib.HTTPSConnection.__init__(self, *args, **kwargs)
232
233     def connect(self, *args):
234         M2Crypto.httpslib.HTTPSConnection.connect(self, *args)
235         verify_certificate(self)
236
237     def getHost(self):
238         return self.host
239
240     def getPort(self):
241         return self.port
242
243 class myProxyHTTPSConnection(M2Crypto.httpslib.ProxyHTTPSConnection, httplib.HTTPSConnection):
244     def __init__(self, *args, **kwargs):
245         self.appname = kwargs.pop('appname', 'generic')
246         M2Crypto.httpslib.ProxyHTTPSConnection.__init__(self, *args, **kwargs)
247
248     def _start_ssl(self):
249         M2Crypto.httpslib.ProxyHTTPSConnection._start_ssl(self)
250         verify_certificate(self)
251
252     def endheaders(self, *args, **kwargs):
253         if self._proxy_auth is None:
254             self._proxy_auth = self._encode_auth()
255         httplib.HTTPSConnection.endheaders(self, *args, **kwargs)        
256
257     # broken in m2crypto: port needs to be an int
258     def putrequest(self, method, url, skip_host=0, skip_accept_encoding=0):
259         #putrequest is called before connect, so can interpret url and get
260         #real host/port to be used to make CONNECT request to proxy
261         proto, rest = urllib.splittype(url)
262         if proto is None:
263             raise ValueError, "unknown URL type: %s" % url
264         #get host
265         host, rest = urllib.splithost(rest)
266         #try to get port
267         host, port = urllib.splitport(host)
268         #if port is not defined try to get from proto
269         if port is None:
270             try:
271                 port = self._ports[proto]
272             except KeyError:
273                 raise ValueError, "unknown protocol for: %s" % url
274         self._real_host = host
275         self._real_port = int(port)
276         M2Crypto.httpslib.HTTPSConnection.putrequest(self, method, url, skip_host, skip_accept_encoding)
277
278     def getHost(self):
279         return self._real_host
280
281     def getPort(self):
282         return self._real_port
283
284 def verify_certificate(connection):
285     ctx = connection.sock.ctx
286     verrs = ctx.verrs
287     ctx.verrs = None
288     cert = connection.sock.get_peer_cert()
289     if not cert:
290         connection.close()
291         raise SSLVerificationError("server did not present a certificate")
292
293     # XXX: should be check if the certificate is known anyways?
294     # Maybe it changed to something valid.
295     if not connection.sock.verify_ok():
296
297         tc = TrustedCertStore(connection.getHost(), connection.getPort(), connection.appname, cert)
298
299         if tc.is_known():
300
301             if tc.is_trusted(): # ok, same cert as the stored one
302                 return
303             else:
304                 print "WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!"
305                 print "IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!"
306                 print "offending certificate is at '%s'" % tc.file
307                 raise SSLVerificationError("remote host identification has changed")
308
309         verrs.show()
310
311         print
312
313         if not verrs.could_ignore():
314             raise SSLVerificationError("Certificate validation error cannot be ignored")
315
316         if not verrs.chain_ok:
317             print "A certificate in the chain failed verification"
318         if not verrs.cert_ok:
319             print "The server certificate failed verification"
320
321         while True:
322             print """
323 Would you like to
324 0 - quit (default)
325 1 - continue anyways
326 2 - trust the server certificate permanently
327 9 - review the server certificate
328 """
329
330             r = raw_input("Enter choice [0129]: ")
331             if not r or r == '0':
332                 connection.close()
333                 raise SSLVerificationError("Untrusted Certificate")
334             elif r == '1':
335                 tc.trust_tmp()
336                 return
337             elif r == '2':
338                 tc.trust_always()
339                 return
340             elif r == '9':
341                 print cert.as_text()
342
343 # vim: sw=4 et