ability to connect to a host that require a PCKS certificate that is encrypted.
[gajim.git] / src / common / xmpp / tls_nb.py
blobcc8393d529781e098940018b72f2b8c229e89821
1 ## tls_nb.py
2 ## based on transports_nb.py
3 ##
4 ## Copyright (C) 2003-2004 Alexey "Snake" Nezhdanov
5 ## modified by Dimitur Kirov <dkirov@gmail.com>
6 ## modified by Tomas Karasek <tom.to.the.k@gmail.com>
7 ##
8 ## This program is free software; you can redistribute it and/or modify
9 ## it under the terms of the GNU General Public License as published by
10 ## the Free Software Foundation; either version 2, or (at your option)
11 ## any later version.
13 ## This program is distributed in the hope that it will be useful,
14 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
15 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 ## GNU General Public License for more details.
18 import socket
19 from plugin import PlugIn
21 import sys
22 import os
23 import time
25 import traceback
27 import logging
28 log = logging.getLogger('gajim.c.x.tls_nb')
30 USE_PYOPENSSL = False
32 PYOPENSSL = 'PYOPENSSL'
33 PYSTDLIB = 'PYSTDLIB'
35 try:
36 #raise ImportError("Manually disabled PyOpenSSL")
37 import OpenSSL.SSL
38 import OpenSSL.crypto
39 USE_PYOPENSSL = True
40 log.info("PyOpenSSL loaded")
41 except ImportError:
42 log.debug("Import of PyOpenSSL failed:", exc_info=True)
44 # FIXME: Remove these prints before release, replace with a warning dialog.
45 print >> sys.stderr, "=" * 79
46 print >> sys.stderr, "PyOpenSSL not found, falling back to Python builtin SSL objects (insecure)."
47 print >> sys.stderr, "=" * 79
49 def gattr(obj, attr, default=None):
50 try:
51 return getattr(obj, attr)
52 except AttributeError:
53 return default
56 class SSLWrapper:
57 """
58 Abstract SSLWrapper base class
59 """
61 class Error(IOError):
62 """
63 Generic SSL Error Wrapper
64 """
66 def __init__(self, sock=None, exc=None, errno=None, strerror=None,
67 peer=None):
68 self.parent = IOError
70 errno = errno or gattr(exc, 'errno') or exc[0]
71 strerror = strerror or gattr(exc, 'strerror') or gattr(exc, 'args')
72 if not isinstance(strerror, basestring):
73 strerror = repr(strerror)
75 self.sock = sock
76 self.exc = exc
77 self.peer = peer
78 self.exc_name = None
79 self.exc_args = None
80 self.exc_str = None
81 self.exc_repr = None
83 if self.exc is not None:
84 self.exc_name = str(self.exc.__class__)
85 self.exc_args = gattr(self.exc, 'args')
86 self.exc_str = str(self.exc)
87 self.exc_repr = repr(self.exc)
88 if not errno:
89 try:
90 if isinstance(exc, OpenSSL.SSL.SysCallError):
91 if self.exc_args[0] > 0:
92 errno = self.exc_args[0]
93 strerror = self.exc_args[1]
94 except: pass
96 self.parent.__init__(self, errno, strerror)
98 if self.peer is None and sock is not None:
99 try:
100 ppeer = self.sock.getpeername()
101 if len(ppeer) == 2 and isinstance(ppeer[0], basestring) \
102 and isinstance(ppeer[1], int):
103 self.peer = ppeer
104 except:
105 pass
107 def __str__(self):
108 s = str(self.__class__)
109 if self.peer:
110 s += " for %s:%d" % self.peer
111 if self.errno is not None:
112 s += ": [Errno: %d]" % self.errno
113 if self.strerror:
114 s += " (%s)" % self.strerror
115 if self.exc_name:
116 s += ", Caused by %s" % self.exc_name
117 if self.exc_str:
118 if self.strerror:
119 s += "(%s)" % self.exc_str
120 else: s += "(%s)" % str(self.exc_args)
121 return s
123 def __init__(self, sslobj, sock=None):
124 self.sslobj = sslobj
125 self.sock = sock
126 log.debug("%s.__init__ called with %s", self.__class__, sslobj)
128 def recv(self, data, flags=None):
130 Receive wrapper for SSL object
132 We can return None out of this function to signal that no data is
133 available right now. Better than an exception, which differs
134 depending on which SSL lib we're using. Unfortunately returning ''
135 can indicate that the socket has been closed, so to be sure, we avoid
136 this by returning None.
138 raise NotImplementedError
140 def send(self, data, flags=None, now=False):
142 Send wrapper for SSL object
144 raise NotImplementedError
147 class PyOpenSSLWrapper(SSLWrapper):
149 Wrapper class for PyOpenSSL's recv() and send() methods
152 def __init__(self, *args):
153 self.parent = SSLWrapper
154 self.parent.__init__(self, *args)
156 def is_numtoolarge(self, e):
157 ''' Magic methods don't need documentation '''
158 t = ('asn1 encoding routines', 'a2d_ASN1_OBJECT', 'first num too large')
159 return (isinstance(e.args, (list, tuple)) and len(e.args) == 1 and
160 isinstance(e.args[0], (list, tuple)) and len(e.args[0]) == 2 and
161 e.args[0][0] == e.args[0][1] == t)
163 def recv(self, bufsize, flags=None):
164 retval = None
165 try:
166 if flags is None:
167 retval = self.sslobj.recv(bufsize)
168 else:
169 retval = self.sslobj.recv(bufsize, flags)
170 except (OpenSSL.SSL.WantReadError, OpenSSL.SSL.WantWriteError), e:
171 log.debug("Recv: Want-error: " + repr(e))
172 except OpenSSL.SSL.SysCallError, e:
173 log.debug("Recv: Got OpenSSL.SSL.SysCallError: " + repr(e),
174 exc_info=True)
175 raise SSLWrapper.Error(self.sock or self.sslobj, e)
176 except OpenSSL.SSL.ZeroReturnError, e:
177 # end-of-connection raises ZeroReturnError instead of having the
178 # connection's .recv() method return a zero-sized result.
179 raise SSLWrapper.Error(self.sock or self.sslobj, e, -1)
180 except OpenSSL.SSL.Error, e:
181 if self.is_numtoolarge(e):
182 # warn, but ignore this exception
183 log.warning("Recv: OpenSSL: asn1enc: first num too large (ignored)")
184 else:
185 log.debug("Recv: Caught OpenSSL.SSL.Error:", exc_info=True)
186 raise SSLWrapper.Error(self.sock or self.sslobj, e)
187 return retval
189 def send(self, data, flags=None, now=False):
190 try:
191 if flags is None:
192 return self.sslobj.send(data)
193 else:
194 return self.sslobj.send(data, flags)
195 except (OpenSSL.SSL.WantReadError, OpenSSL.SSL.WantWriteError), e:
196 #log.debug("Send: " + repr(e))
197 time.sleep(0.1) # prevent 100% CPU usage
198 except OpenSSL.SSL.SysCallError, e:
199 log.error("Send: Got OpenSSL.SSL.SysCallError: " + repr(e),
200 exc_info=True)
201 raise SSLWrapper.Error(self.sock or self.sslobj, e)
202 except OpenSSL.SSL.Error, e:
203 if self.is_numtoolarge(e):
204 # warn, but ignore this exception
205 log.warning("Send: OpenSSL: asn1enc: first num too large (ignored)")
206 else:
207 log.error("Send: Caught OpenSSL.SSL.Error:", exc_info=True)
208 raise SSLWrapper.Error(self.sock or self.sslobj, e)
209 return 0
212 class StdlibSSLWrapper(SSLWrapper):
214 Wrapper class for Python socket.ssl read() and write() methods
217 def __init__(self, *args):
218 self.parent = SSLWrapper
219 self.parent.__init__(self, *args)
221 def recv(self, bufsize, flags=None):
222 # we simply ignore flags since ssl object doesn't support it
223 try:
224 return self.sslobj.read(bufsize)
225 except socket.sslerror, e:
226 log.debug("Recv: Caught socket.sslerror: " + repr(e), exc_info=True)
227 if e.args[0] not in (socket.SSL_ERROR_WANT_READ, socket.SSL_ERROR_WANT_WRITE):
228 raise SSLWrapper.Error(self.sock or self.sslobj, e)
229 return None
231 def send(self, data, flags=None, now=False):
232 # we simply ignore flags since ssl object doesn't support it
233 try:
234 return self.sslobj.write(data)
235 except socket.sslerror, e:
236 log.debug("Send: Caught socket.sslerror:", exc_info=True)
237 if e.args[0] not in (socket.SSL_ERROR_WANT_READ, socket.SSL_ERROR_WANT_WRITE):
238 raise SSLWrapper.Error(self.sock or self.sslobj, e)
239 return 0
242 class NonBlockingTLS(PlugIn):
244 TLS connection used to encrypts already estabilished tcp connection
246 Can be plugged into NonBlockingTCP and will make use of StdlibSSLWrapper or
247 PyOpenSSLWrapper.
250 def __init__(self, cacerts, mycerts):
252 :param cacerts: path to pem file with certificates of known XMPP servers
253 :param mycerts: path to pem file with certificates of user trusted servers
255 PlugIn.__init__(self)
256 self.cacerts = cacerts
257 self.mycerts = mycerts
259 # from ssl.h (partial extract)
260 ssl_h_bits = { "SSL_ST_CONNECT": 0x1000, "SSL_ST_ACCEPT": 0x2000,
261 "SSL_CB_LOOP": 0x01, "SSL_CB_EXIT": 0x02,
262 "SSL_CB_READ": 0x04, "SSL_CB_WRITE": 0x08,
263 "SSL_CB_ALERT": 0x4000,
264 "SSL_CB_HANDSHAKE_START": 0x10, "SSL_CB_HANDSHAKE_DONE": 0x20}
266 def plugin(self, owner):
268 Use to PlugIn TLS into transport and start establishing immediately.
269 Returns True if TLS/SSL was established correctly, otherwise False
271 log.info('Starting TLS estabilishing')
272 try:
273 res = self._startSSL()
274 except Exception, e:
275 log.error("PlugIn: while trying _startSSL():", exc_info=True)
276 return False
277 return res
279 def _dumpX509(self, cert, stream=sys.stderr):
280 print >> stream, "Digest (SHA-1):", cert.digest("sha1")
281 print >> stream, "Digest (MD5):", cert.digest("md5")
282 print >> stream, "Serial #:", cert.get_serial_number()
283 print >> stream, "Version:", cert.get_version()
284 print >> stream, "Expired:", ("Yes" if cert.has_expired() else "No")
285 print >> stream, "Subject:"
286 self._dumpX509Name(cert.get_subject(), stream)
287 print >> stream, "Issuer:"
288 self._dumpX509Name(cert.get_issuer(), stream)
289 self._dumpPKey(cert.get_pubkey(), stream)
291 def _dumpX509Name(self, name, stream=sys.stderr):
292 print >> stream, "X509Name:", str(name)
294 def _dumpPKey(self, pkey, stream=sys.stderr):
295 typedict = {OpenSSL.crypto.TYPE_RSA: "RSA",
296 OpenSSL.crypto.TYPE_DSA: "DSA"}
297 print >> stream, "PKey bits:", pkey.bits()
298 print >> stream, "PKey type: %s (%d)" % (typedict.get(pkey.type(),
299 "Unknown"), pkey.type())
301 def _startSSL(self):
303 Immediatedly switch socket to TLS mode. Used internally
305 log.debug("_startSSL called")
307 if USE_PYOPENSSL:
308 result = self._startSSL_pyOpenSSL()
309 else:
310 result = self._startSSL_stdlib()
312 if result:
313 log.debug('Synchronous handshake completed')
314 return True
315 else:
316 return False
318 def _load_cert_file(self, cert_path, cert_store, logg=True):
319 if not os.path.isfile(cert_path):
320 return
321 try:
322 f = open(cert_path)
323 except IOError, e:
324 log.warning('Unable to open certificate file %s: %s' % \
325 (cert_path, str(e)))
326 return
327 lines = f.readlines()
328 i = 0
329 begin = -1
330 for line in lines:
331 if 'BEGIN CERTIFICATE' in line:
332 begin = i
333 elif 'END CERTIFICATE' in line and begin > -1:
334 cert = ''.join(lines[begin:i+2])
335 try:
336 x509cert = OpenSSL.crypto.load_certificate(
337 OpenSSL.crypto.FILETYPE_PEM, cert)
338 cert_store.add_cert(x509cert)
339 except OpenSSL.crypto.Error, exception_obj:
340 if logg:
341 log.warning('Unable to load a certificate from file %s: %s' %\
342 (cert_path, exception_obj.args[0][0][2]))
343 except:
344 log.warning('Unknown error while loading certificate from file '
345 '%s' % cert_path)
346 begin = -1
347 i += 1
349 def _startSSL_pyOpenSSL(self):
350 log.debug("_startSSL_pyOpenSSL called")
351 tcpsock = self._owner
352 # NonBlockingHTTPBOSH instance has no attribute _owner
353 if hasattr(tcpsock, '_owner') and tcpsock._owner._caller.client_cert \
354 and os.path.exists(tcpsock._owner._caller.client_cert):
355 conn = tcpsock._owner._caller
356 # FIXME make a checkbox for Client Cert / SSLv23 / TLSv1
357 # If we are going to use a client cert/key pair for authentication,
358 # we choose TLSv1 method.
359 tcpsock._sslContext = OpenSSL.SSL.Context(OpenSSL.SSL.TLSv1_METHOD)
360 log.debug('Using client cert and key from %s' % conn.client_cert)
361 try:
362 p12 = OpenSSL.crypto.load_pkcs12(open(conn.client_cert).read(),
363 conn.client_cert_passphrase)
364 except OpenSSL.crypto.Error, exception_obj:
365 log.warning('Unable to load client pkcs12 certificate from '
366 'file %s: %s ... Is it a valid PKCS12 cert?' % \
367 (conn.client_cert, exception_obj.args))
368 except:
369 log.warning('Unknown error while loading certificate from file '
370 '%s' % conn.client_cert)
371 else:
372 log.info('PKCS12 Client cert loaded OK')
373 try:
374 tcpsock._sslContext.use_certificate(p12.get_certificate())
375 tcpsock._sslContext.use_privatekey(p12.get_privatekey())
376 log.info('p12 cert and key loaded')
377 except OpenSSL.crypto.Error, exception_obj:
378 log.warning('Unable to extract client certificate from '
379 'file %s' % conn.client_cert)
380 except Exception, msg:
381 log.warning('Unknown error extracting client certificate '
382 'from file %s: %s' % (conn.client_cert, msg))
383 else:
384 log.info('client cert and key loaded OK')
385 else:
386 # See http://docs.python.org/dev/library/ssl.html
387 tcpsock._sslContext = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
388 flags = OpenSSL.SSL.OP_NO_SSLv2
389 try:
390 flags |= OpenSSL.SSL.OP_NO_TICKET
391 except AttributeError, e:
392 # py-OpenSSL < 0.9 or old OpenSSL
393 flags |= 16384
394 tcpsock._sslContext.set_options(flags)
396 tcpsock.ssl_errnum = 0
397 tcpsock._sslContext.set_verify(OpenSSL.SSL.VERIFY_PEER,
398 self._ssl_verify_callback)
399 try:
400 tcpsock._sslContext.load_verify_locations(self.cacerts)
401 except:
402 log.warning('Unable to load SSL certificates from file %s' % \
403 os.path.abspath(self.cacerts))
404 store = tcpsock._sslContext.get_cert_store()
405 self._load_cert_file(self.mycerts, store)
406 if os.path.isdir('/etc/ssl/certs'):
407 for f in os.listdir('/etc/ssl/certs'):
408 # We don't logg because there is a lot a duplicated certs in this
409 # folder
410 self._load_cert_file(os.path.join('/etc/ssl/certs', f), store,
411 logg=False)
413 tcpsock._sslObj = OpenSSL.SSL.Connection(tcpsock._sslContext,
414 tcpsock._sock)
415 tcpsock._sslObj.set_connect_state() # set to client mode
416 wrapper = PyOpenSSLWrapper(tcpsock._sslObj)
417 tcpsock._recv = wrapper.recv
418 tcpsock._send = wrapper.send
420 log.debug("Initiating handshake...")
421 tcpsock._sslObj.setblocking(True)
422 try:
423 tcpsock._sslObj.do_handshake()
424 except:
425 log.error('Error while TLS handshake: ', exc_info=True)
426 return False
427 tcpsock._sslObj.setblocking(False)
428 self._owner.ssl_lib = PYOPENSSL
429 return True
431 def _startSSL_stdlib(self):
432 log.debug("_startSSL_stdlib called")
433 tcpsock=self._owner
434 try:
435 tcpsock._sock.setblocking(True)
436 tcpsock._sslObj = socket.ssl(tcpsock._sock, None, None)
437 tcpsock._sock.setblocking(False)
438 tcpsock._sslIssuer = tcpsock._sslObj.issuer()
439 tcpsock._sslServer = tcpsock._sslObj.server()
440 wrapper = StdlibSSLWrapper(tcpsock._sslObj, tcpsock._sock)
441 tcpsock._recv = wrapper.recv
442 tcpsock._send = wrapper.send
443 except:
444 log.error("Exception caught in _startSSL_stdlib:", exc_info=True)
445 return False
446 self._owner.ssl_lib = PYSTDLIB
447 return True
449 def _ssl_verify_callback(self, sslconn, cert, errnum, depth, ok):
450 # Exceptions can't propagate up through this callback, so print them here.
451 try:
452 self._owner.ssl_fingerprint_sha1 = cert.digest('sha1')
453 self._owner.ssl_certificate = cert
454 if errnum == 0:
455 return True
456 self._owner.ssl_errnum = errnum
457 self._owner.ssl_cert_pem = OpenSSL.crypto.dump_certificate(
458 OpenSSL.crypto.FILETYPE_PEM, cert)
459 return True
460 except:
461 log.error("Exception caught in _ssl_info_callback:", exc_info=True)
462 # Make sure something is printed, even if log is disabled.
463 traceback.print_exc()