pyxmpp.streamtls

Package pyxmpp :: Module streamtls |
[hide private]

# Source Code for Module pyxmpp.streamtls

      1  #
      2  # (C) Copyright 2003-2006 Jacek Konieczny <jajcus@jajcus.net>
      3  #
      4  # This program is free software; you can redistribute it and/or modify
      5  # it under the terms of the GNU Lesser General Public License Version
      6  # 2.1 as published by the Free Software Foundation.
      7  #
      8  # This program is distributed in the hope that it will be useful,
      9  # but WITHOUT ANY WARRANTY; without even the implied warranty of
     10  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     11  # GNU Lesser General Public License for more details.
     12  #
     13  # You should have received a copy of the GNU Lesser General Public
     14  # License along with this program; if not, write to the Free Software
     15  # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
     16  #
     17  # pylint: disable-msg=W0201
     18
     19  """TLS support for XMPP streams.
     20
     21  Normative reference:
     22    - `RFC 3920 <http://www.ietf.org/rfc/rfc3920.txt>`__
     23  """
     24
     25  __revision__="$Id: streamtls.py 681 2008-12-05 07:18:41Z jajcus $"
     26  __docformat__="restructuredtext en"
     27
     28  import socket
     29  import sys
     30  import errno
     31  import logging
     32
     33  from pyxmpp.streambase import StreamBase,STREAM_NS
     34  from pyxmpp.streambase import FatalStreamError,StreamEncryptionRequired
     35  from pyxmpp.exceptions import TLSNegotiationFailed, TLSError, TLSNegotiatedButNotAvailableError
     36
     37  try:
     38      import M2Crypto
     39      if M2Crypto.version_info < (0, 16):
     40          tls_available = 0
     41      else:
     42          from M2Crypto import SSL
     43          from M2Crypto.SSL import SSLError
     44          import M2Crypto.SSL.cb
     45          tls_available = 1
     46  except ImportError:
     47      tls_available = 0
     48
     49  if not tls_available:
     50 -    class SSLError(Exception):
     51          "dummy"
     52          pass
     53
     54  TLS_NS="urn:ietf:params:xml:ns:xmpp-tls"
     55
     56 -class TLSSettings:
     57      """Storage for TLS-related settings of an XMPP stream.
     58
     59         :Ivariables:
     60              - `require`: is TLS required
     61              - `verify_peer`: should the peer's certificate be verified
     62              - `cert_file`: path to own X.509 certificate
     63              - `key_file`: path to the private key for own X.509 certificate
     64              - `cacert_file`: path to a file with trusted CA certificates
     65              - `verify_callback`: callback function for certificate
     66                verification. See M2Crypto documentation for details."""
     67
     68 -    def __init__(self,
     69              require=False,verify_peer=True,
     70              cert_file=None,key_file=None,cacert_file=None,
     71              verify_callback=None,ctx=None):
     72          """Initialize the TLSSettings object.
     73
     74          :Parameters:
     75              - `require`:  is TLS required
     76              - `verify_peer`: should the peer's certificate be verified
     77              - `cert_file`: path to own X.509 certificate
     78              - `key_file`: path to the private key for own X.509 certificate
     79              - `cacert_file`: path to a file with trusted CA certificates
     80              - `verify_callback`: callback function for certificate
     81                verification. The callback function must accept two arguments:
     82                'ok' and 'store_context' and return True if a certificate is accepted.
     83                The verification callback should call Stream.tls_is_certificate_valid()
     84                to check if certificate subject name matches stream peer JID.
     85                See M2Crypto documentation for details. If no verify_callback is provided,
     86                then default `Stream.tls_default_verify_callback` will be used."""
     87          self.require=require
     88          self.ctx=ctx
     89          self.verify_peer=verify_peer
     90          self.cert_file=cert_file
     91          self.cacert_file=cacert_file
     92          self.key_file=key_file
     93          self.verify_callback=verify_callback
     94
     95 -class StreamTLSMixIn:
     96      """Mix-in class providing TLS support for an XMPP stream.
     97
     98      :Ivariables:
     99          - `tls`: TLS connection object.
    100      """
    101 -    def __init__(self,tls_settings=None):
    102          """Initialize TLS support of a Stream object
    103
    104          :Parameters:
    105            - `tls_settings`: settings for StartTLS.
    106          :Types:
    107            - `tls_settings`: `TLSSettings`
    108          """
    109          self.tls_settings=tls_settings
    110          self.__logger=logging.getLogger("pyxmpp.StreamTLSMixIn")
    111
    112 -    def _reset_tls(self):
    113          """Reset `StreamTLSMixIn` object state making it ready to handle new
    114          connections."""
    115          self.tls=None
    116          self.tls_requested=False
    117
    118 -    def _make_stream_tls_features(self,features):
    119          """Update the <features/> with StartTLS feature.
    120
    121          [receving entity only]
    122
    123          :Parameters:
    124              - `features`: the <features/> element of the stream.
    125          :Types:
    126              - `features`: `libxml2.xmlNode`
    127
    128          :returns: updated <features/> element node.
    129          :returntype: `libxml2.xmlNode`"""
    130          if self.tls_settings and not self.tls:
    131              tls=features.newChild(None,"starttls",None)
    132              ns=tls.newNs(TLS_NS,None)
    133              tls.setNs(ns)
    134              if self.tls_settings.require:
    135                  tls.newChild(None,"required",None)
    136          return features
    137
    138 -    def _write_raw(self,data):
    139          """Same as `Stream.write_raw` but assume `self.lock` is acquired."""
    140          logging.getLogger("pyxmpp.Stream.out").debug("OUT: %r",data)
    141          try:
    142              if self.tls:
    143                  self.tls.setblocking(True)
    144              if self.socket:
    145                  self.socket.send(data)
    146              if self.tls:
    147                  self.tls.setblocking(False)
    148          except (IOError,OSError,socket.error),e:
    149              raise FatalStreamError("IO Error: "+str(e))
    150          except SSLError,e:
    151              raise TLSError("TLS Error: "+str(e))
    152
    153 -    def _read_tls(self):
    154          """Read data pending on the stream socket and pass it to the parser."""
    155          if self.eof:
    156              return
    157          while self.socket:
    158              try:
    159                  r=self.socket.read()
    160                  if r is None:
    161                      return
    162              except socket.error,e:
    163                  if e.args[0]!=errno.EINTR:
    164                      raise
    165                  return
    166              self._feed_reader(r)
    167
    168 -    def _read(self):
    169          """Read data pending on the stream socket and pass it to the parser."""
    170          self.__logger.debug("StreamTLSMixIn._read(), socket: %r",self.socket)
    171          if self.tls:
    172              self._read_tls()
    173          else:
    174              StreamBase._read(self)
    175
    176 -    def _process(self):
    177          """Same as `Stream.process` but assume `self.lock` is acquired."""
    178          try:
    179              StreamBase._process(self)
    180          except SSLError,e:
    181              self.close()
    182              raise TLSError("TLS Error: "+str(e))
    183
    184 -    def _process_node_tls(self,xmlnode):
    185          """Process incoming stream element. Pass it to _process_tls_node
    186          if it is in TLS namespace.
    187
    188          :raise StreamEncryptionRequired: if encryption is required by current
    189            configuration, it is not active and the element is not in the TLS
    190            namespace nor in the stream namespace.
    191
    192          :return: `True` when the node was recognized as TLS element.
    193          :returntype: `bool`"""
    194          ns_uri=xmlnode.ns().getContent()
    195          if ns_uri==STREAM_NS:
    196              return False
    197          elif ns_uri==TLS_NS:
    198              self._process_tls_node(xmlnode)
    199              return True
    200          if self.tls_settings and self.tls_settings.require and not self.tls:
    201              raise StreamEncryptionRequired,"TLS encryption required and not started yet"
    202          return False
    203
    204 -    def _handle_tls_features(self):
    205          """Process incoming StartTLS related element of <stream:features/>.
    206
    207          [initiating entity only]
    208
    209          The received features node is available in `self.features`."""
    210          ctxt = self.doc_in.xpathNewContext()
    211          ctxt.setContextNode(self.features)
    212          ctxt.xpathRegisterNs("tls",TLS_NS)
    213          try:
    214              tls_n=ctxt.xpathEval("tls:starttls")
    215              tls_required_n=ctxt.xpathEval("tls:starttls/tls:required")
    216          finally:
    217              ctxt.xpathFreeContext()
    218
    219          if not self.tls:
    220              if tls_required_n and not self.tls_settings:
    221                  raise FatalStreamError,"StartTLS support disabled, but required by peer"
    222              if self.tls_settings and self.tls_settings.require and not tls_n:
    223                  raise FatalStreamError,"StartTLS required, but not supported by peer"
    224              if self.tls_settings and tls_n:
    225                  self.__logger.debug("StartTLS negotiated")
    226                  if not tls_available:
    227                      raise TLSNegotiatedButNotAvailableError,("StartTLS negotiated, but not available"
    228                              " (M2Crypto >= 0.16 module required)")
    229                  if self.initiator:
    230                      self._request_tls()
    231              else:
    232                  self.__logger.debug("StartTLS not negotiated")
    233
    234 -    def _request_tls(self):
    235          """Request a TLS-encrypted connection.
    236
    237          [initiating entity only]"""
    238          self.tls_requested=1
    239          self.features=None
    240          root=self.doc_out.getRootElement()
    241          xmlnode=root.newChild(None,"starttls",None)
    242          ns=xmlnode.newNs(TLS_NS,None)
    243          xmlnode.setNs(ns)
    244          self._write_raw(xmlnode.serialize(encoding="UTF-8"))
    245          xmlnode.unlinkNode()
    246          xmlnode.freeNode()
    247
    248 -    def _process_tls_node(self,xmlnode):
    249          """Process stream element in the TLS namespace.
    250
    251          :Parameters:
    252              - `xmlnode`: the XML node received
    253          """
    254          if not self.tls_settings or not tls_available:
    255              self.__logger.debug("Unexpected TLS node: %r" % (xmlnode.serialize()))
    256              return False
    257          if self.initiator:
    258              if xmlnode.name=="failure":
    259                  raise TLSNegotiationFailed,"Peer failed to initialize TLS connection"
    260              elif xmlnode.name!="proceed" or not self.tls_requested:
    261                  self.__logger.debug("Unexpected TLS node: %r" % (xmlnode.serialize()))
    262                  return False
    263              try:
    264                  self.tls_requested=0
    265                  self._make_tls_connection()
    266                  self.socket=self.tls
    267              except SSLError,e:
    268                  self.tls=None
    269                  raise TLSError("TLS Error: "+str(e))
    270              self.__logger.debug("Restarting XMPP stream")
    271              self._restart_stream()
    272              return True
    273          else:
    274              raise FatalStreamError,"TLS not implemented for the receiving side yet"
    275
    276 -    def _make_tls_connection(self):
    277          """Initiate TLS connection.
    278
    279          [initiating entity only]"""
    280          if not tls_available or not self.tls_settings:
    281              raise TLSError,"TLS is not available"
    282
    283          self.state_change("tls connecting",self.peer)
    284          self.__logger.debug("Creating TLS context")
    285          if self.tls_settings.ctx:
    286              ctx=self.tls_settings.ctx
    287          else:
    288              ctx=SSL.Context('tlsv1')
    289
    290          verify_callback = self.tls_settings.verify_callback
    291          if not verify_callback:
    292              verify_callback = self.tls_default_verify_callback
    293
    294
    295          if self.tls_settings.verify_peer:
    296              self.__logger.debug("verify_peer, verify_callback: %r", verify_callback)
    297              ctx.set_verify(SSL.verify_peer, 10, verify_callback)
    298          else:
    299              ctx.set_verify(SSL.verify_none, 10)
    300
    301          if self.tls_settings.cert_file:
    302              ctx.use_certificate_chain_file(self.tls_settings.cert_file)
    303              if self.tls_settings.key_file:
    304                  ctx.use_PrivateKey_file(self.tls_settings.key_file)
    305              else:
    306                  ctx.use_PrivateKey_file(self.tls_settings.cert_file)
    307              ctx.check_private_key()
    308          if self.tls_settings.cacert_file:
    309              try:
    310                  ctx.load_verify_location(self.tls_settings.cacert_file)
    311              except AttributeError:
    312                  ctx.load_verify_locations(self.tls_settings.cacert_file)
    313          self.__logger.debug("Creating TLS connection")
    314          self.tls=SSL.Connection(ctx,self.socket)
    315          self.socket=None
    316          self.__logger.debug("Setting up TLS connection")
    317          self.tls.setup_ssl()
    318          self.__logger.debug("Setting TLS connect state")
    319          self.tls.set_connect_state()
    320          self.__logger.debug("Starting TLS handshake")
    321          self.tls.connect_ssl()
    322          self.state_change("tls connected",self.peer)
    323          self.tls.setblocking(0)
    324
    325          # clear any exception state left by some M2Crypto broken code
    326          try:
    327              raise Exception
    328          except:
    329              pass
    330
    331 -    def tls_is_certificate_valid(self, store_context):
    332          """Check subject name of the certificate and return True when
    333          it is valid.
    334
    335          Only the certificate at depth 0 in the certificate chain (peer
    336          certificate) is checked.
    337
    338          Currently only the Common Name is checked and certificate is considered
    339          valid if CN is the same as the peer JID.
    340
    341          :Parameters:
    342              - `store_context`: certificate store context, as passed to the
    343                verification callback.
    344
    345          :returns: verification result. `True` if certificate subject name is valid.
    346          """
    347          depth = store_context.get_error_depth()
    348          if depth > 0:
    349              return True
    350          cert = store_context.get_current_cert()
    351          cn = cert.get_subject().CN
    352          if str(cn) != self.peer.as_utf8():
    353              return False
    354          return True
    355
    356 -    def tls_default_verify_callback(self, ok, store_context):
    357          """Default certificate verification callback for TLS connections.
    358
    359          Will reject connection (return `False`) if M2Crypto finds any error
    360          or when certificate CommonName doesn't match peer JID.
    361
    362          TODO: check otherName/idOnXMPP (or what it is called)
    363
    364          :Parameters:
    365              - `ok`: current verification result (as decided by OpenSSL).
    366              - `store_context`: certificate store context
    367
    368          :return: computed verification result."""
    369          try:
    370              self.__logger.debug("tls_default_verify_callback(ok=%i, store=%r)" % (ok, store_context))
    371              from M2Crypto import X509,m2
    372
    373              depth = store_context.get_error_depth()
    374              cert = store_context.get_current_cert()
    375              cn = cert.get_subject().CN
    376
    377              self.__logger.debug("  depth: %i cert CN: %r" % (depth, cn))
    378              if ok and not self.tls_is_certificate_valid(store_context):
    379                  self.__logger.debug(u"Common name does not match peer name (%s != %s)" % (cn, self.peer.as_utf8))
    380                  return False
    381              return ok
    382          except:
    383              self.__logger.exception("Exception caught")
    384              raise
    385
    386 -    def get_tls_connection(self):
    387          """Get the TLS connection object for the stream.
    388
    389          :return: `self.tls`"""
    390          return self.tls
    391
    392  # vi: sts=4 et sw=4
    393