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