Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#9561] Check remote certificates for XMPP TLS #1147

Merged
merged 19 commits into from Jul 4, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/words/examples/xmpp_client.py
Expand Up @@ -53,8 +53,9 @@ def connected(self, xs):
xs.rawDataOutFn = self.rawDataOut


def disconnected(self, xs):
def disconnected(self, reason):
print('Disconnected.')
print(reason)

self.finished.callback(None)

Expand Down
1 change: 1 addition & 0 deletions src/twisted/words/newsfragments/9561.bugfix
@@ -0,0 +1 @@
twisted.words.protocols.jabber.xmlstream.TLSInitiatingInitializer now properly verifies the server's certificate against platform CAs and the stream's domain, mitigating CVE-2019-12855.
1 change: 1 addition & 0 deletions src/twisted/words/newsfragments/9561.feature
@@ -0,0 +1 @@
twisted.words.protocols.jabber.xmlstream.TLSInitiatingInitializer and twisted.words.protocols.jabber.client.XMPPClientFactory now take an optional configurationForTLS for customizing certificate options for StartTLS.
60 changes: 39 additions & 21 deletions src/twisted/words/protocols/jabber/client.py
Expand Up @@ -206,14 +206,10 @@ def associateWithStream(self, xs):
xs.version = (0, 0)
xmlstream.ConnectAuthenticator.associateWithStream(self, xs)

inits = [ (xmlstream.TLSInitiatingInitializer, False),
(IQAuthInitializer, True),
]

for initClass, required in inits:
init = initClass(xs)
init.required = required
xs.initializers.append(init)
xs.initializers = [
xmlstream.TLSInitiatingInitializer(xs, required=False),
IQAuthInitializer(xs),
]

# TODO: move registration into an Initializer?

Expand Down Expand Up @@ -302,7 +298,7 @@ def start(self):



def XMPPClientFactory(jid, password):
def XMPPClientFactory(jid, password, configurationForTLS=None):
"""
Client factory for XMPP 1.0 (only).

Expand All @@ -314,12 +310,23 @@ def XMPPClientFactory(jid, password):

@param jid: Jabber ID to connect with.
@type jid: L{jid.JID}

@param password: password to authenticate with.
@type password: L{unicode}

@param configurationForTLS: An object which creates appropriately
configured TLS connections. This is passed to C{startTLS} on the
transport and is preferably created using
L{twisted.internet.ssl.optionsForClientTLS}. If C{None}, the default is
to verify the server certificate against the trust roots as provided by
the platform. See L{twisted.internet._sslverify.platformTrust}.
@type configurationForTLS: L{IOpenSSLClientConnectionCreator} or C{None}

@return: XML stream factory.
@rtype: L{xmlstream.XmlStreamFactory}
"""
a = XMPPAuthenticator(jid, password)
a = XMPPAuthenticator(jid, password,
configurationForTLS=configurationForTLS)
return xmlstream.XmlStreamFactory(a)


Expand Down Expand Up @@ -354,16 +361,29 @@ class XMPPAuthenticator(xmlstream.ConnectAuthenticator):
resource binding step, and this is stored in this instance
variable.
@type jid: L{jid.JID}

@ivar password: password to be used during SASL authentication.
@type password: L{unicode}
"""

namespace = 'jabber:client'

def __init__(self, jid, password):
def __init__(self, jid, password, configurationForTLS=None):
"""
@param configurationForTLS: An object which creates appropriately
configured TLS connections. This is passed to C{startTLS} on the
transport and is preferably created using
L{twisted.internet.ssl.optionsForClientTLS}. If C{None}, the
default is to verify the server certificate against the trust roots
as provided by the platform. See
L{twisted.internet._sslverify.platformTrust}.
@type configurationForTLS: L{IOpenSSLClientConnectionCreator} or
C{None}
"""
xmlstream.ConnectAuthenticator.__init__(self, jid.host)
self.jid = jid
self.password = password
self._configurationForTLS = configurationForTLS


def associateWithStream(self, xs):
Expand All @@ -377,14 +397,12 @@ def associateWithStream(self, xs):
"""
xmlstream.ConnectAuthenticator.associateWithStream(self, xs)

xs.initializers = [CheckVersionInitializer(xs)]
inits = [ (xmlstream.TLSInitiatingInitializer, False),
(sasl.SASLInitiatingInitializer, True),
(BindInitializer, False),
(SessionInitializer, False),
xs.initializers = [
ralphm marked this conversation as resolved.
Show resolved Hide resolved
CheckVersionInitializer(xs),
xmlstream.TLSInitiatingInitializer(
xs, required=True,
configurationForTLS=self._configurationForTLS),
sasl.SASLInitiatingInitializer(xs, required=True),
BindInitializer(xs, required=True),
SessionInitializer(xs, required=False),
]

for initClass, required in inits:
init = initClass(xs)
init.required = required
xs.initializers.append(init)
30 changes: 26 additions & 4 deletions src/twisted/words/protocols/jabber/xmlstream.py
Expand Up @@ -316,16 +316,17 @@ class BaseFeatureInitiatingInitializer(object):

@cvar feature: tuple of (uri, name) of the stream feature root element.
@type feature: tuple of (C{str}, C{str})

@ivar required: whether the stream feature is required to be advertized
by the receiving entity.
@type required: C{bool}
"""

feature = None
required = False

def __init__(self, xs):
def __init__(self, xs, required=False):
self.xmlstream = xs
self.required = required


def initialize(self):
Expand Down Expand Up @@ -400,21 +401,42 @@ class TLSInitiatingInitializer(BaseFeatureInitiatingInitializer):
set the C{wanted} attribute to False instead of removing it from the list
of initializers, so a proper exception L{TLSRequired} can be raised.

@cvar wanted: indicates if TLS negotiation is wanted.
@ivar wanted: indicates if TLS negotiation is wanted.
@type wanted: C{bool}
"""

feature = (NS_XMPP_TLS, 'starttls')
wanted = True
_deferred = None
_configurationForTLS = None

def __init__(self, xs, required=True, configurationForTLS=None):
"""
@param configurationForTLS: An object which creates appropriately
configured TLS connections. This is passed to C{startTLS} on the
transport and is preferably created using
L{twisted.internet.ssl.optionsForClientTLS}. If C{None}, the
default is to verify the server certificate against the trust roots
as provided by the platform. See
L{twisted.internet._sslverify.platformTrust}.
@type configurationForTLS: L{IOpenSSLClientConnectionCreator} or
C{None}
"""
super(TLSInitiatingInitializer, self).__init__(
xs, required=required)
self._configurationForTLS = configurationForTLS


def onProceed(self, obj):
"""
Proceed with TLS negotiation and reset the XML stream.
"""

self.xmlstream.removeObserver('/failure', self.onFailure)
ctx = ssl.CertificateOptions()
if self._configurationForTLS:
ctx = self._configurationForTLS
else:
ctx = ssl.optionsForClientTLS(self.xmlstream.otherEntity.host)
self.xmlstream.transport.startTLS(ctx)
self.xmlstream.reset()
self.xmlstream.sendHeader()
Expand Down
85 changes: 82 additions & 3 deletions src/twisted/words/test/test_jabberclient.py
Expand Up @@ -16,6 +16,14 @@
from twisted.words.protocols.jabber.sasl import SASLInitiatingInitializer
from twisted.words.xish import utility

try:
from twisted.internet import ssl
except ImportError:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are lots of other places which do this conditional import. Can we piggyback on one of them rather than adding bespoke ImportError-handling logic? My concern here is that this type of exception handling is likely to mask cases where there's an actual import-time bug in twisted.internet.ssl and I don't want to have to go looking for 20 instances of this if we ever manage to go in and fix it to only fail on defined import errors (i.e. those where cryptography isn't available).

Worst case, I'd rather literally import it from another test_ module for the time being.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose this is due to the unconditional import of the OpenSSL module in twisted.internet.ssl and twisted.internet._sslverify. The former module does have a supported attribute, but it seems to (at least now) always be set to True, even though for example twisted.words.protocols.jabber.xmlstream checks for that.

If the OpenSSL module cannot be imported, the whole import fails, wheres I think my tests probably don't even use it.

Can we not make twisted.internet.ssl do this conditional import and have it set supported to False, and SSL to None?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@glyph I'm not sure if this should be addressed as part of this ticket?

ssl = None
skipWhenNoSSL = "SSL not available"
else:
skipWhenNoSSL = None

IQ_AUTH_GET = '/iq[@type="get"]/query[@xmlns="jabber:iq:auth"]'
IQ_AUTH_SET = '/iq[@type="set"]/query[@xmlns="jabber:iq:auth"]'
NS_BIND = 'urn:ietf:params:xml:ns:xmpp-bind'
Expand Down Expand Up @@ -379,11 +387,51 @@ def onSession(iq):



class BasicAuthenticatorTests(unittest.TestCase):
"""
Test for both BasicAuthenticator and basicClientFactory.
"""

def test_basic(self):
"""
Authenticator and stream are properly constructed by the factory.

The L{xmlstream.XmlStream} protocol created by the factory has the new
L{client.BasicAuthenticator} instance in its C{authenticator}
attribute. It is set up with C{jid} and C{password} as passed to the
factory, C{otherHost} taken from the client JID. The stream futher has
two initializers, for TLS and authentication, of which the first has
its C{required} attribute set to C{True}.
"""
self.client_jid = jid.JID('user@example.com/resource')

# Get an XmlStream instance. Note that it gets initialized with the
# XMPPAuthenticator (that has its associateWithXmlStream called) that
# is in turn initialized with the arguments to the factory.
xs = client.basicClientFactory(self.client_jid,
'secret').buildProtocol(None)

# test authenticator's instance variables
self.assertEqual('example.com', xs.authenticator.otherHost)
self.assertEqual(self.client_jid, xs.authenticator.jid)
self.assertEqual('secret', xs.authenticator.password)

# test list of initializers
tls, auth = xs.initializers

self.assertIsInstance(tls, xmlstream.TLSInitiatingInitializer)
self.assertIsInstance(auth, client.IQAuthInitializer)

self.assertFalse(tls.required)



class XMPPAuthenticatorTests(unittest.TestCase):
"""
Test for both XMPPAuthenticator and XMPPClientFactory.
"""
def testBasic(self):

def test_basic(self):
"""
Test basic operations.

Expand Down Expand Up @@ -412,7 +460,38 @@ def testBasic(self):
self.assertIsInstance(bind, client.BindInitializer)
self.assertIsInstance(session, client.SessionInitializer)

self.assertFalse(tls.required)
self.assertTrue(tls.required)
self.assertTrue(sasl.required)
self.assertFalse(bind.required)
self.assertTrue(bind.required)
self.assertFalse(session.required)


def test_tlsConfiguration(self):
"""
A TLS configuration is passed to the TLS initializer.
"""
configs = []

def init(self, xs, required=True, configurationForTLS=None):
configs.append(configurationForTLS)

self.client_jid = jid.JID('user@example.com/resource')

# Get an XmlStream instance. Note that it gets initialized with the
# XMPPAuthenticator (that has its associateWithXmlStream called) that
# is in turn initialized with the arguments to the factory.
configurationForTLS = ssl.CertificateOptions()
factory = client.XMPPClientFactory(
self.client_jid, 'secret',
configurationForTLS=configurationForTLS)
self.patch(xmlstream.TLSInitiatingInitializer, "__init__", init)
xs = factory.buildProtocol(None)

# test list of initializers
version, tls, sasl, bind, session = xs.initializers

self.assertIsInstance(tls, xmlstream.TLSInitiatingInitializer)
self.assertIs(configurationForTLS, configs[0])


test_tlsConfiguration.skip = skipWhenNoSSL