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.
6 import M2Crypto.httpslib
7 from M2Crypto.SSL.Checker import SSLVerificationError
8 from M2Crypto import m2, SSL
9 import M2Crypto.m2urllib2
14 class TrustedCertStore:
17 def __init__(self, host, port, app, cert):
22 raise Exception("empty host")
24 self.host += "_%d" % port
26 self.dir = os.path.expanduser('~/.config/%s/trusted-certs' % app)
27 self.file = self.dir + '/%s.pem' % self.host
30 if self.host in self._tmptrusted:
34 if os.path.exists(self.file):
40 if self.host in self._tmptrusted:
41 cert = self._tmptrusted[self.host]
43 if not os.path.exists(self.file):
45 from M2Crypto import X509
46 cert = X509.load_cert(self.file)
47 if self.cert.as_pem() == cert.as_pem():
53 self._tmptrusted[self.host] = self.cert
55 def trust_always(self):
57 from M2Crypto import X509
59 if not os.path.exists(self.dir):
61 self.cert.save_pem(self.file)
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):
69 ctx.verrs = ValidationErrors()
73 ctx.verrs.record(store.get_current_cert(), store.get_error(), store.get_error_depth())
81 def __init__(self, cert):
85 class ValidationErrors:
92 def record(self, cert, err, depth):
93 #print "cert for %s, level %d fail(%d)" % ( cert.get_subject().commonName, depth, err )
99 if not depth in self.failures:
100 self.failures[depth] = FailCert(cert)
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)
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')
116 for err in self.failures[depth].errs:
120 reason = M2Crypto.Err.get_x509_verify_error(err)
123 print "Reason:", reason
125 # check if the encountered errors could be ignored
126 def could_ignore(self):
127 if not 0 in self.failures:
130 from M2Crypto import m2
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,
138 m2.X509_V_ERR_CERT_NOT_YET_VALID,
139 m2.X509_V_ERR_CERT_HAS_EXPIRED,
144 for err in self.failures[0].errs:
145 if not err in nonfatal_errors:
151 class mySSLContext(SSL.Context):
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
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))
160 class myHTTPSHandler(M2Crypto.m2urllib2.HTTPSHandler):
163 def __init__(self, *args, **kwargs):
164 self.appname = kwargs.pop('appname', 'generic')
165 M2Crypto.m2urllib2.HTTPSHandler.__init__(self, *args, **kwargs)
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()
175 raise M2Crypto.m2urllib2.URLError('no host given: ' + req.get_full_url())
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]
182 if (target_host != host):
183 h = myProxyHTTPSConnection(host = host, appname = self.appname, ssl_context = self.ctx)
185 h = myHTTPSConnection(host = host, appname = self.appname, ssl_context = self.ctx)
187 h.set_debuglevel(self._debuglevel)
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)
197 headers["Connection"] = "close"
199 h.request(req.get_method(), req.get_full_url(), req.data, headers)
201 except socket.error, err: # XXX what error?
202 err.filename = full_url
203 raise M2Crypto.m2urllib2.URLError(err)
205 # Pick apart the HTTPResponse object to get the addinfourl
206 # object initialized properly.
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.
213 # XXX It might be better to extract the read buffering code
214 # out of socket._fileobject() and into a base class.
217 fp = socket._fileobject(r)
219 resp = urllib.addinfourl(fp, r.msg, req.get_full_url())
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)
229 def connect(self, *args):
230 M2Crypto.httpslib.HTTPSConnection.connect(self, *args)
231 verify_certificate(self)
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)
244 def _start_ssl(self):
245 M2Crypto.httpslib.ProxyHTTPSConnection._start_ssl(self)
246 verify_certificate(self)
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)
254 raise ValueError, "unknown URL type: %s" % url
256 host, rest = urllib.splithost(rest)
258 host, port = urllib.splitport(host)
259 #if port is not defined try to get from proto
262 port = self._ports[proto]
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)
270 return self._real_host
273 return self._real_port
275 def verify_certificate(connection):
276 ctx = connection.sock.ctx
279 cert = connection.sock.get_peer_cert()
282 raise SSLVerificationError("server did not present a certificate")
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():
288 tc = TrustedCertStore(connection.getHost(), connection.getPort(), connection.appname, cert)
292 if tc.is_trusted(): # ok, same cert as the stored one
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")
304 if not verrs.could_ignore():
305 raise SSLVerificationError("Certificate validation error cannot be ignored")
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"
317 2 - trust the server certificate permanently
318 9 - review the server certificate
321 r = raw_input("Enter choice [0129]: ")
322 if not r or r == '0':
324 raise SSLVerificationError("Untrusted Certificate")