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