pyxmpp.streamtls

Package pyxmpp :: Module streamtls |
[hide private]

# Source Code for Module pyxmpp.streamtls

      1  #
      2  # (C) Copyright 2003-2010 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  __docformat__="restructuredtext en"
     26
     27  import socket
     28  import sys
     29  import errno
     30  import logging
     31  import ssl
     32  import warnings
     33  import inspect
     34  from ssl import SSLError
     35
     36  from pyxmpp.streambase import StreamBase,STREAM_NS
     37  from pyxmpp.streambase import FatalStreamError,StreamEncryptionRequired
     38  from pyxmpp.exceptions import TLSNegotiationFailed, TLSError, TLSNegotiatedButNotAvailableError
     39  from pyxmpp.jid import JID
     40
     41  TLS_NS="urn:ietf:params:xml:ns:xmpp-tls"
     42
     43  tls_available = True
     44
     45
     46 -def _count_args(callable):
     47      """Utility function to count expected arguments of a callable"""
     48      vc_args = inspect.getargspec(callable)
     49      count = len(vc_args.args)
     50      if not hasattr(callable, "im_self"):
     51          return count
     52      if callable.im_self is None:
     53          return count
     54      return count - 1
     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."""
     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 a single
     82                argument: the certificate to verify, as returned by
     83                `ssl.SSLSocket.getpeercert()` and return True if a certificate is
     84                accepted.  The verification callback should call
     85                Stream.tls_is_certificate_valid() to check if certificate subject
     86                name or alt subject name matches stream peer JID."""
     87          if ctx is not None:
     88               warnings.warn("ctx argument of TLSSettings is deprecated",
     89                                                          DeprecationWarning)
     90          self.require = require
     91          self.verify_peer = verify_peer
     92          self.cert_file = cert_file
     93          self.cacert_file = cacert_file
     94          self.key_file = key_file
     95          if verify_callback:
     96              if _count_args(verify_callback) > 1 :
     97                  warnings.warn("Two-argument TLS verify callback is deprecated",
     98                                                          DeprecationWarning)
     99                  verify_callback = None
    100          self.verify_callback = verify_callback
    101
    102 -class StreamTLSMixIn:
    103      """Mix-in class providing TLS support for an XMPP stream.
    104
    105      :Ivariables:
    106          - `tls`: TLS connection object.
    107      """
    108 -    def __init__(self, tls_settings = None):
    109          """Initialize TLS support of a Stream object
    110
    111          :Parameters:
    112            - `tls_settings`: settings for StartTLS.
    113          :Types:
    114            - `tls_settings`: `TLSSettings`
    115          """
    116          self.tls_settings = tls_settings
    117          self.__logger = logging.getLogger("pyxmpp.StreamTLSMixIn")
    118
    119 -    def _reset_tls(self):
    120          """Reset `StreamTLSMixIn` object state making it ready to handle new
    121          connections."""
    122          self.tls = None
    123          self.tls_requested = False
    124
    125 -    def _make_stream_tls_features(self, features):
    126          """Update the <features/> with StartTLS feature.
    127
    128          [receving entity only]
    129
    130          :Parameters:
    131              - `features`: the <features/> element of the stream.
    132          :Types:
    133              - `features`: `libxml2.xmlNode`
    134
    135          :returns: updated <features/> element node.
    136          :returntype: `libxml2.xmlNode`"""
    137          if self.tls_settings and not self.tls:
    138              tls = features.newChild(None, "starttls", None)
    139              ns = tls.newNs(TLS_NS, None)
    140              tls.setNs(ns)
    141              if self.tls_settings.require:
    142                  tls.newChild(None, "required", None)
    143          return features
    144
    145 -    def _write_raw(self,data):
    146          """Same as `Stream.write_raw` but assume `self.lock` is acquired."""
    147          logging.getLogger("pyxmpp.Stream.out").debug("OUT: %r",data)
    148          try:
    149              while self.socket:
    150                  try:
    151                      while data:
    152                          sent = self.socket.send(data)
    153                          data = data[sent:]
    154                  except SSLError, err:
    155                      if err.args[0] == ssl.SSL_ERROR_WANT_WRITE:
    156                          continue
    157                      raise
    158                  break
    159          except (IOError, OSError, socket.error),e:
    160              raise FatalStreamError("IO Error: "+str(e))
    161          except SSLError,e:
    162              raise TLSError("TLS Error: "+str(e))
    163
    164 -    def _read_tls(self):
    165          """Read data pending on the stream socket and pass it to the parser."""
    166          if self.eof:
    167              return
    168          while self.socket:
    169              try:
    170                  r = self.socket.read()  # .recv() blocks in python 2.6.4
    171                  if r is None:
    172                      return
    173              except SSLError, err:
    174                  if err.args[0] == ssl.SSL_ERROR_WANT_READ:
    175                      return
    176                  raise
    177              except socket.error, err:
    178                  if err.args[0] != errno.EINTR:
    179                      raise
    180                  return
    181              self._feed_reader(r)
    182
    183 -    def _read(self):
    184          """Read data pending on the stream socket and pass it to the parser."""
    185          self.__logger.debug("StreamTLSMixIn._read(), socket: %r",self.socket)
    186          if self.tls:
    187              self._read_tls()
    188          else:
    189              StreamBase._read(self)
    190
    191 -    def _process(self):
    192          """Same as `Stream.process` but assume `self.lock` is acquired."""
    193          try:
    194              StreamBase._process(self)
    195          except SSLError,e:
    196              self.close()
    197              raise TLSError("TLS Error: "+str(e))
    198
    199 -    def _process_node_tls(self,xmlnode):
    200          """Process incoming stream element. Pass it to _process_tls_node
    201          if it is in TLS namespace.
    202
    203          :raise StreamEncryptionRequired: if encryption is required by current
    204            configuration, it is not active and the element is not in the TLS
    205            namespace nor in the stream namespace.
    206
    207          :return: `True` when the node was recognized as TLS element.
    208          :returntype: `bool`"""
    209          ns_uri=xmlnode.ns().getContent()
    210          if ns_uri==STREAM_NS:
    211              return False
    212          elif ns_uri==TLS_NS:
    213              self._process_tls_node(xmlnode)
    214              return True
    215          if self.tls_settings and self.tls_settings.require and not self.tls:
    216              raise StreamEncryptionRequired,"TLS encryption required and not started yet"
    217          return False
    218
    219 -    def _handle_tls_features(self):
    220          """Process incoming StartTLS related element of <stream:features/>.
    221
    222          [initiating entity only]
    223
    224          The received features node is available in `self.features`."""
    225          ctxt = self.doc_in.xpathNewContext()
    226          ctxt.setContextNode(self.features)
    227          ctxt.xpathRegisterNs("tls",TLS_NS)
    228          try:
    229              tls_n=ctxt.xpathEval("tls:starttls")
    230              tls_required_n=ctxt.xpathEval("tls:starttls/tls:required")
    231          finally:
    232              ctxt.xpathFreeContext()
    233
    234          if not self.tls:
    235              if tls_required_n and not self.tls_settings:
    236                  raise FatalStreamError,"StartTLS support disabled, but required by peer"
    237              if self.tls_settings and self.tls_settings.require and not tls_n:
    238                  raise FatalStreamError,"StartTLS required, but not supported by peer"
    239              if self.tls_settings and tls_n:
    240                  self.__logger.debug("StartTLS negotiated")
    241                  if self.initiator:
    242                      self._request_tls()
    243              else:
    244                  self.__logger.debug("StartTLS not negotiated")
    245
    246 -    def _request_tls(self):
    247          """Request a TLS-encrypted connection.
    248
    249          [initiating entity only]"""
    250          self.tls_requested=1
    251          self.features=None
    252          root=self.doc_out.getRootElement()
    253          xmlnode=root.newChild(None,"starttls",None)
    254          ns=xmlnode.newNs(TLS_NS,None)
    255          xmlnode.setNs(ns)
    256          self._write_raw(xmlnode.serialize(encoding="UTF-8"))
    257          xmlnode.unlinkNode()
    258          xmlnode.freeNode()
    259
    260 -    def _process_tls_node(self,xmlnode):
    261          """Process stream element in the TLS namespace.
    262
    263          :Parameters:
    264              - `xmlnode`: the XML node received
    265          """
    266          if not self.tls_settings or not tls_available:
    267              self.__logger.debug("Unexpected TLS node: %r" % (xmlnode.serialize()))
    268              return False
    269          if self.initiator:
    270              if xmlnode.name=="failure":
    271                  raise TLSNegotiationFailed,"Peer failed to initialize TLS connection"
    272              elif xmlnode.name!="proceed" or not self.tls_requested:
    273                  self.__logger.debug("Unexpected TLS node: %r" % (xmlnode.serialize()))
    274                  return False
    275              try:
    276                  self.tls_requested=0
    277                  self._make_tls_connection()
    278                  self.socket=self.tls
    279              except SSLError,e:
    280                  self.tls=None
    281                  raise TLSError("TLS Error: "+str(e))
    282              self.__logger.debug("Restarting XMPP stream")
    283              self._restart_stream()
    284              return True
    285          else:
    286              raise FatalStreamError,"TLS not implemented for the receiving side yet"
    287
    288 -    def _make_tls_connection(self):
    289          """Initiate TLS connection.
    290
    291          [initiating entity only]"""
    292          if not tls_available or not self.tls_settings:
    293              raise TLSError,"TLS is not available"
    294
    295          self.state_change("tls connecting",self.peer)
    296
    297          if not self.tls_settings.verify_callback:
    298              self.tls_settings.verify_callback = self.tls_is_certificate_valid
    299
    300          self.__logger.debug("tls_settings: {0!r}".format(self.tls_settings.__dict__))
    301          self.__logger.debug("Creating TLS connection")
    302
    303          if self.tls_settings.verify_peer:
    304              cert_reqs = ssl.CERT_REQUIRED
    305          else:
    306              cert_reqs = ssl.CERT_NONE
    307
    308          self.tls = ssl.wrap_socket(self.socket,
    309                      keyfile = self.tls_settings.key_file,
    310                      certfile = self.tls_settings.cert_file,
    311                      server_side = not self.initiator,
    312                      cert_reqs = cert_reqs,
    313                      ssl_version = ssl.PROTOCOL_TLSv1,
    314                      ca_certs = self.tls_settings.cacert_file,
    315                      do_handshake_on_connect = False,
    316                      )
    317          self.socket = None
    318          self.__logger.debug("Starting TLS handshake")
    319          self.tls.do_handshake()
    320          self.tls.setblocking(False)
    321          if self.tls_settings.verify_peer:
    322              valid = self.tls_settings.verify_callback(self.tls.getpeercert())
    323              if not valid:
    324                  raise SSLError, "Certificate verification failed"
    325          self.socket = self.tls
    326          self.state_change("tls connected", self.peer)
    327
    328 -    def tls_is_certificate_valid(self, cert):
    329          """Default certificate verification callback for TLS connections.
    330
    331          :Parameters:
    332              - `cert`: certificate information, as returned by `ssl.SSLSocket.getpeercert`
    333
    334          :return: computed verification result."""
    335          try:
    336              self.__logger.debug("tls_is_certificate_valid(cert = %r)" % (
    337                                                                          cert,))
    338              if not cert:
    339                  self.__logger.warning("No TLS certificate information received.")
    340                  return False
    341              valid_hostname_found = False
    342              if 'subject' in cert:
    343                  for rdns in cert['subject']:
    344                      for key, value in rdns:
    345                          if key == 'commonName' and JID(value) == self.peer:
    346                              self.__logger.debug(" good commonName: {0}".format(value))
    347                              valid_hostname_found = True
    348              if 'subjectAltName' in cert:
    349                  for key, value in cert['subjectAltName']:
    350                      if key == 'DNS' and JID(value) == self.peer:
    351                          self.__logger.debug(" good subjectAltName({0}): {1}"
    352                                                                      .format(key, value))
    353                          valid_hostname_found = True
    354              return valid_hostname_found
    355          except:
    356              self.__logger.exception("Exception caught")
    357              raise
    358
    359 -    def get_tls_connection(self):
    360          """Get the TLS connection object for the stream.
    361
    362          :return: `self.tls`"""
    363          return self.tls
    364
    365  # vi: sts=4 et sw=4
    366