Index: Twisted-15.2.1/src/twisted/words/newsfragments/9561.bugfix
===================================================================
--- /dev/null
+++ Twisted-15.2.1/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.
Index: Twisted-15.2.1/src/twisted/words/newsfragments/9561.feature
===================================================================
--- /dev/null
+++ Twisted-15.2.1/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.
Index: Twisted-15.2.1/twisted/words/protocols/jabber/client.py
===================================================================
--- Twisted-15.2.1.orig/twisted/words/protocols/jabber/client.py
+++ Twisted-15.2.1/twisted/words/protocols/jabber/client.py
@@ -184,14 +184,10 @@ class BasicAuthenticator(xmlstream.Conne
         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?
 
@@ -280,7 +276,7 @@ class SessionInitializer(xmlstream.BaseF
 
 
 
-def XMPPClientFactory(jid, password):
+def XMPPClientFactory(jid, password, configurationForTLS=None):
     """
     Client factory for XMPP 1.0 (only).
 
@@ -292,12 +288,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: C{unicode}
+    @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)
 
 
@@ -332,16 +339,29 @@ class XMPPAuthenticator(xmlstream.Connec
                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: C{unicode}
+    @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):
@@ -355,14 +375,12 @@ class XMPPAuthenticator(xmlstream.Connec
         """
         xmlstream.ConnectAuthenticator.associateWithStream(self, xs)
 
-        xs.initializers = [CheckVersionInitializer(xs)]
-        inits = [ (xmlstream.TLSInitiatingInitializer, False),
-                  (sasl.SASLInitiatingInitializer, True),
-                  (BindInitializer, False),
-                  (SessionInitializer, False),
+        xs.initializers = [
+                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)
Index: Twisted-15.2.1/twisted/words/protocols/jabber/xmlstream.py
===================================================================
--- Twisted-15.2.1.orig/twisted/words/protocols/jabber/xmlstream.py
+++ Twisted-15.2.1/twisted/words/protocols/jabber/xmlstream.py
@@ -300,6 +300,7 @@ class BaseFeatureInitiatingInitializer(o
 
     @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}
@@ -308,10 +309,10 @@ class BaseFeatureInitiatingInitializer(o
     implements(ijabber.IInitiatingInitializer)
 
     feature = None
-    required = False
 
-    def __init__(self, xs):
+    def __init__(self, xs, required=False):
         self.xmlstream = xs
+        self.required = required
 
 
     def initialize(self):
@@ -386,13 +387,31 @@ class TLSInitiatingInitializer(BaseFeatu
     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):
         """
@@ -400,7 +419,10 @@ class TLSInitiatingInitializer(BaseFeatu
         """
 
         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()
Index: Twisted-15.2.1/twisted/words/test/test_jabberclient.py
===================================================================
--- Twisted-15.2.1.orig/twisted/words/test/test_jabberclient.py
+++ Twisted-15.2.1/twisted/words/test/test_jabberclient.py
@@ -7,11 +7,20 @@ Tests for L{twisted.words.protocols.jabb
 from hashlib import sha1
 
 from twisted.internet import defer
+from twisted.python.compat import unicode
 from twisted.trial import unittest
 from twisted.words.protocols.jabber import client, error, jid, xmlstream
 from twisted.words.protocols.jabber.sasl import SASLInitiatingInitializer
 from twisted.words.xish import utility
 
+try:
+    from twisted.internet import ssl
+except ImportError:
+    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'
@@ -375,11 +384,51 @@ class SessionInitializerTests(Initiating
 
 
 
+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.
 
@@ -408,7 +457,38 @@ class XMPPAuthenticatorTests(unittest.Te
         self.assert_(isinstance(bind, client.BindInitializer))
         self.assert_(isinstance(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
Index: Twisted-15.2.1/twisted/words/test/test_jabberxmlstream.py
===================================================================
--- Twisted-15.2.1.orig/twisted/words/test/test_jabberxmlstream.py
+++ Twisted-15.2.1/twisted/words/test/test_jabberxmlstream.py
@@ -18,7 +18,15 @@ from twisted.words.test.test_xmlstream i
 from twisted.words.xish import domish
 from twisted.words.protocols.jabber import error, ijabber, jid, xmlstream
 
-
+try:
+    from twisted.internet import ssl
+except ImportError:
+    ssl = None
+    skipWhenNoSSL = "SSL not available"
+else:
+    skipWhenNoSSL = None
+    from twisted.internet.ssl import CertificateOptions
+    from twisted.internet._sslverify import ClientTLSOptions
 
 NS_XMPP_TLS = 'urn:ietf:params:xml:ns:xmpp-tls'
 
@@ -661,7 +669,7 @@ class TLSInitiatingInitializerTests(unit
 
         self.savedSSL = xmlstream.ssl
 
-        self.authenticator = xmlstream.Authenticator()
+        self.authenticator = xmlstream.ConnectAuthenticator(u'example.com')
         self.xmlstream = xmlstream.XmlStream(self.authenticator)
         self.xmlstream.send = self.output.append
         self.xmlstream.connectionMade()
@@ -675,9 +683,18 @@ class TLSInitiatingInitializerTests(unit
         xmlstream.ssl = self.savedSSL
 
 
-    def testWantedSupported(self):
+    def test_initRequired(self):
+        """
+        Passing required sets the instance variable.
+        """
+        self.init = xmlstream.TLSInitiatingInitializer(self.xmlstream,
+                                                       required=True)
+        self.assertTrue(self.init.required)
+
+
+    def test_wantedSupported(self):
         """
-        Test start when TLS is wanted and the SSL library available.
+        When TLS is wanted and SSL available, StartTLS is initiated.
         """
         self.xmlstream.transport = proto_helpers.StringTransport()
         self.xmlstream.transport.startTLS = lambda ctx: self.done.append('TLS')
@@ -686,7 +703,8 @@ class TLSInitiatingInitializerTests(unit
 
         d = self.init.start()
         d.addCallback(self.assertEqual, xmlstream.Reset)
-        starttls = self.output[0]
+        self.assertEqual(2, len(self.output))
+        starttls = self.output[1]
         self.assertEqual('starttls', starttls.name)
         self.assertEqual(NS_XMPP_TLS, starttls.uri)
         self.xmlstream.dataReceived("<proceed xmlns='%s'/>" % NS_XMPP_TLS)
@@ -694,40 +712,90 @@ class TLSInitiatingInitializerTests(unit
 
         return d
 
-    if not xmlstream.ssl:
-        testWantedSupported.skip = "SSL not available"
+    test_wantedSupported.skip = skipWhenNoSSL
+
+
+    def test_certificateVerify(self):
+        """
+        The server certificate will be verified.
+        """
+
+        def fakeStartTLS(contextFactory):
+            self.assertIsInstance(contextFactory, ClientTLSOptions)
+            self.assertEqual(contextFactory._hostname, u"example.com")
+            self.done.append('TLS')
+
+        self.xmlstream.transport = proto_helpers.StringTransport()
+        self.xmlstream.transport.startTLS = fakeStartTLS
+        self.xmlstream.reset = lambda: self.done.append('reset')
+        self.xmlstream.sendHeader = lambda: self.done.append('header')
+
+        d = self.init.start()
+        self.xmlstream.dataReceived("<proceed xmlns='%s'/>" % NS_XMPP_TLS)
+        self.assertEqual(['TLS', 'reset', 'header'], self.done)
+        return d
+
+    test_certificateVerify.skip = skipWhenNoSSL
+
+
+    def test_certificateVerifyContext(self):
+        """
+        A custom contextFactory is passed through to startTLS.
+        """
+        ctx = CertificateOptions()
+        self.init = xmlstream.TLSInitiatingInitializer(
+            self.xmlstream, configurationForTLS=ctx)
+
+        self.init.contextFactory = ctx
+
+        def fakeStartTLS(contextFactory):
+            self.assertIs(ctx, contextFactory)
+            self.done.append('TLS')
+
+        self.xmlstream.transport = proto_helpers.StringTransport()
+        self.xmlstream.transport.startTLS = fakeStartTLS
+        self.xmlstream.reset = lambda: self.done.append('reset')
+        self.xmlstream.sendHeader = lambda: self.done.append('header')
+
+        d = self.init.start()
+        self.xmlstream.dataReceived("<proceed xmlns='%s'/>" % NS_XMPP_TLS)
+        self.assertEqual(['TLS', 'reset', 'header'], self.done)
+        return d
+
+    test_certificateVerifyContext.skip = skipWhenNoSSL
 
 
-    def testWantedNotSupportedNotRequired(self):
+    def test_wantedNotSupportedNotRequired(self):
         """
-        Test start when TLS is wanted and the SSL library available.
+        No StartTLS is initiated when wanted, not required, SSL not available.
         """
         xmlstream.ssl = None
+        self.init.required = False
 
         d = self.init.start()
         d.addCallback(self.assertEqual, None)
-        self.assertEqual([], self.output)
+        self.assertEqual(1, len(self.output))
 
         return d
 
 
-    def testWantedNotSupportedRequired(self):
+    def test_wantedNotSupportedRequired(self):
         """
-        Test start when TLS is wanted and the SSL library available.
+        TLSNotSupported is raised when TLS is required but not available.
         """
         xmlstream.ssl = None
         self.init.required = True
 
         d = self.init.start()
         self.assertFailure(d, xmlstream.TLSNotSupported)
-        self.assertEqual([], self.output)
+        self.assertEqual(1, len(self.output))
 
         return d
 
 
-    def testNotWantedRequired(self):
+    def test_notWantedRequired(self):
         """
-        Test start when TLS is not wanted, but required by the server.
+        TLSRequired is raised when TLS is not wanted, but required by server.
         """
         tls = domish.Element(('urn:ietf:params:xml:ns:xmpp-tls', 'starttls'))
         tls.addElement('required')
@@ -735,29 +803,30 @@ class TLSInitiatingInitializerTests(unit
         self.init.wanted = False
 
         d = self.init.start()
-        self.assertEqual([], self.output)
+        self.assertEqual(1, len(self.output))
         self.assertFailure(d, xmlstream.TLSRequired)
 
         return d
 
 
-    def testNotWantedNotRequired(self):
+    def test_notWantedNotRequired(self):
         """
-        Test start when TLS is not wanted, but required by the server.
+        No StartTLS is initiated when not wanted and not required.
         """
         tls = domish.Element(('urn:ietf:params:xml:ns:xmpp-tls', 'starttls'))
         self.xmlstream.features = {(tls.uri, tls.name): tls}
         self.init.wanted = False
+        self.init.required = False
 
         d = self.init.start()
         d.addCallback(self.assertEqual, None)
-        self.assertEqual([], self.output)
+        self.assertEqual(1, len(self.output))
         return d
 
 
-    def testFailed(self):
+    def test_failed(self):
         """
-        Test failed TLS negotiation.
+        TLSFailed is raised when the server responds with a failure.
         """
         # Pretend that ssl is supported, it isn't actually used when the
         # server starts out with a failure in response to our initial
