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