Package nbxmpp :: Module tls_nb

[hide private]
[frames] | no frames]

# Source Code for Module nbxmpp.tls_nb

      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.
     12  ##
     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.
     17
     18  import socket
     19  from plugin import PlugIn
     20
     21  import sys
     22  import os
     23  import time
     24
     25  import traceback
     26
     27  import logging
     28  log = logging.getLogger('nbxmpp.tls_nb')
     29
     30  USE_PYOPENSSL = False
     31
     32  PYOPENSSL = 'PYOPENSSL'
     33  PYSTDLIB  = 'PYSTDLIB'
     34
     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)
     43
     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
     48
     49 -def gattr(obj, attr, default=None):
     50      try:
     51          return getattr(obj, attr)
     52      except AttributeError:
     53          return default
     54
     55
     56 -class SSLWrapper:
     57      """
     58      Abstract SSLWrapper base class
     59      """
     60
     61 -    class Error(IOError):
     62          """
     63          Generic SSL Error Wrapper
     64          """
     65
     66 -        def __init__(self, sock=None, exc=None, errno=None, strerror=None,
     67                          peer=None):
     68              self.parent = IOError
     69
     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)
     74
     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
     82
     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
     95
     96              self.parent.__init__(self, errno, strerror)
     97
     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
    106
    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
    122
    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)
    127
    128 -    def recv(self, data, flags=None):
    129          """
    130          Receive wrapper for SSL object
    131
    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.
    137          """
    138          raise NotImplementedError
    139
    140 -    def send(self, data, flags=None, now=False):
    141          """
    142          Send wrapper for SSL object
    143          """
    144          raise NotImplementedError
    145
    146
    147 -class PyOpenSSLWrapper(SSLWrapper):
    148      """
    149      Wrapper class for PyOpenSSL's recv() and send() methods
    150      """
    151
    152 -    def __init__(self, *args):
    153          self.parent = SSLWrapper
    154          self.parent.__init__(self, *args)
    155
    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)
    162
    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
    188
    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
    210
    211
    212 -class StdlibSSLWrapper(SSLWrapper):
    213      """
    214      Wrapper class for Python socket.ssl read() and write() methods
    215      """
    216
    217 -    def __init__(self, *args):
    218          self.parent = SSLWrapper
    219          self.parent.__init__(self, *args)
    220
    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
    230
    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
    240
    241
    242 -class NonBlockingTLS(PlugIn):
    243      """
    244      TLS connection used to encrypts already estabilished tcp connection
    245
    246      Can be plugged into NonBlockingTCP and will make use of StdlibSSLWrapper or
    247      PyOpenSSLWrapper.
    248      """
    249
    250 -    def __init__(self, cacerts, mycerts):
    251          """
    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
    254          """
    255          PlugIn.__init__(self)
    256          self.cacerts = cacerts
    257          self.mycerts = mycerts
    258
    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}
    265
    266 -    def plugin(self, owner):
    267          """
    268          Use to PlugIn TLS into transport and start establishing immediately.
    269          Returns True if TLS/SSL was established correctly, otherwise False
    270          """
    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
    278
    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)
    290
    291 -    def _dumpX509Name(self, name, stream=sys.stderr):
    292          print >> stream, "X509Name:", str(name)
    293
    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())
    300
    301 -    def _startSSL(self):
    302          """
    303          Immediatedly switch socket to TLS mode. Used internally
    304          """
    305          log.debug("_startSSL called")
    306
    307          if USE_PYOPENSSL:
    308              result = self._startSSL_pyOpenSSL()
    309          else:
    310              result = self._startSSL_stdlib()
    311
    312          if result:
    313              log.debug('Synchronous handshake completed')
    314              return True
    315          else:
    316              return False
    317
    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
    348
    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)
    395
    396          tcpsock.ssl_errnum = []
    397          tcpsock._sslContext.set_verify(OpenSSL.SSL.VERIFY_PEER,
    398              self._ssl_verify_callback)
    399          store = tcpsock._sslContext.get_cert_store()
    400          self._load_cert_file(self.cacerts, store)
    401          self._load_cert_file(self.mycerts, store)
    402          if os.path.isdir('/etc/ssl/certs'):
    403              for f in os.listdir('/etc/ssl/certs'):
    404                  # We don't logg because there is a lot a duplicated certs in this
    405                  # folder
    406                  self._load_cert_file(os.path.join('/etc/ssl/certs', f), store,
    407                          logg=False)
    408
    409          tcpsock._sslObj = OpenSSL.SSL.Connection(tcpsock._sslContext,
    410                  tcpsock._sock)
    411          tcpsock._sslObj.set_connect_state() # set to client mode
    412          wrapper = PyOpenSSLWrapper(tcpsock._sslObj)
    413          tcpsock._recv = wrapper.recv
    414          tcpsock._send = wrapper.send
    415
    416          log.debug("Initiating handshake...")
    417          try:
    418              tcpsock._sslObj.do_handshake()
    419          except (OpenSSL.SSL.WantReadError, OpenSSL.SSL.WantWriteError), e:
    420              pass
    421          except:
    422              log.error('Error while TLS handshake: ', exc_info=True)
    423              return False
    424          self._owner.ssl_lib = PYOPENSSL
    425          return True
    426
    427 -    def _startSSL_stdlib(self):
    428          log.debug("_startSSL_stdlib called")
    429          tcpsock=self._owner
    430          try:
    431              tcpsock._sock.setblocking(True)
    432              tcpsock._sslObj = socket.ssl(tcpsock._sock, None, None)
    433              tcpsock._sock.setblocking(False)
    434              tcpsock._sslIssuer = tcpsock._sslObj.issuer()
    435              tcpsock._sslServer = tcpsock._sslObj.server()
    436              wrapper = StdlibSSLWrapper(tcpsock._sslObj, tcpsock._sock)
    437              tcpsock._recv = wrapper.recv
    438              tcpsock._send = wrapper.send
    439          except:
    440              log.error("Exception caught in _startSSL_stdlib:", exc_info=True)
    441              return False
    442          self._owner.ssl_lib = PYSTDLIB
    443          return True
    444
    445 -    def _ssl_verify_callback(self, sslconn, cert, errnum, depth, ok):
    446          # Exceptions can't propagate up through this callback, so print them here.
    447          try:
    448              self._owner.ssl_fingerprint_sha1.append(cert.digest('sha1'))
    449              self._owner.ssl_certificate.append(cert)
    450              self._owner.ssl_errnum.append(errnum)
    451              self._owner.ssl_cert_pem.append(OpenSSL.crypto.dump_certificate(
    452                      OpenSSL.crypto.FILETYPE_PEM, cert))
    453              return True
    454          except:
    455              log.error("Exception caught in _ssl_info_callback:", exc_info=True)
    456              # Make sure something is printed, even if log is disabled.
    457              traceback.print_exc()
    458