Ticket #6024: tls_notify.patch

File tls_notify.patch, 14.9 KB (added by Andy Lutomirski, 7 years ago)

tls-notify-handshake-done-6204-1.patch

  • twisted/internet/interfaces.py

    commit 75a50b92a432fe92e168049a5e7e83a0467df43d
    Author: Andy Lutomirski <luto@amacapital.net>
    Date:   Thu Oct 10 13:54:07 2013 -0700
    
        tls: Explicitly handshake connections and optionally notify protocols
        
        SSL_read can transparently negotiate a TLS session, but it only works
        if I/O is actively occuring.  This means that a handshake can complete
        without us noticing.
        
        Rework TLSMemoryBIOProtocol to explicitly handshake connections and add
        ISSLTransport.notifyHandshakeDone to inform interested protocols
        about the handshake status.
    
    diff --git a/twisted/internet/interfaces.py b/twisted/internet/interfaces.py
    index 4021e57..1c12c73 100644
    a b class ISSLTransport(ITCPTransport): 
    21522152        Return an object with the peer's certificate info.
    21532153        """
    21542154
     2155    def notifyHandshakeDone():
     2156        """
     2157        Returns a Deferred that will complete when the initial handshake
     2158        is done and will errback if the handshake fails.  (Connection
     2159        loss during the handshake is considered to be a handshake failure.)
     2160
     2161        If the handshake is already complete, then the returned Deferred
     2162        will already be complete.
     2163        """
    21552164
    21562165class IProcessTransport(ITransport):
    21572166    """
  • twisted/protocols/test/test_tls.py

    diff --git a/twisted/protocols/test/test_tls.py b/twisted/protocols/test/test_tls.py
    index 49e3a79..4545713 100644
    a b class TLSMemoryBIOTests(TestCase): 
    290290        clientFactory = ClientFactory()
    291291        clientFactory.protocol = Protocol
    292292
    293         clientContextFactory, handshakeDeferred = (
     293        clientContextFactory, _ = (
    294294            HandshakeCallbackContextFactory.factoryAndDeferred())
    295295        wrapperFactory = TLSMemoryBIOFactory(
    296296            clientContextFactory, True, clientFactory)
    297297        sslClientProtocol = wrapperFactory.buildProtocol(None)
     298        handshakeDeferred = sslClientProtocol.notifyHandshakeDone()
    298299
    299300        serverFactory = ServerFactory()
    300301        serverFactory.protocol = Protocol
    class TLSMemoryBIOTests(TestCase): 
    371372                connectionDeferred])
    372373
    373374
     375    def test_notifyAfterSuccessfulHandshake(self):
     376        """
     377        Calling L{TLSMemoryBIOProtocol.notifyHandshakeDone} after a
     378        successful handshake should work.
     379        """
     380        tlsClient, tlsServer, handshakeDeferred, _ = self.handshakeProtocols()
     381
     382        result = Deferred()
     383
     384        def check(_):
     385            d = tlsClient.notifyHandshakeDone()
     386            d.addCallback(result.callback)
     387            d.addErrback(result.errback)
     388
     389        handshakeDeferred.addCallback(check)
     390        return result
     391
     392
     393    def test_notifyAfterFailedHandshake(self):
     394        """
     395        Calling L{TLSMemoryBIOProtocol.notifyHandshakeDone} after a
     396        failed handshake should work.
     397        """
     398        clientConnectionLost = Deferred()
     399        clientFactory = ClientFactory()
     400        clientFactory.protocol = Protocol
     401
     402        clientContextFactory = HandshakeCallbackContextFactory()
     403        wrapperFactory = TLSMemoryBIOFactory(
     404            clientContextFactory, True, clientFactory)
     405        sslClientProtocol = wrapperFactory.buildProtocol(None)
     406
     407        serverConnectionLost = Deferred()
     408        serverFactory = ServerFactory()
     409        serverFactory.protocol = Protocol
     410
     411        # This context factory rejects any clients which do not present a
     412        # certificate.
     413        certificateData = FilePath(certPath).getContent()
     414        certificate = PrivateCertificate.loadPEM(certificateData)
     415        serverContextFactory = certificate.options(certificate)
     416        wrapperFactory = TLSMemoryBIOFactory(
     417            serverContextFactory, False, serverFactory)
     418        sslServerProtocol = wrapperFactory.buildProtocol(None)
     419
     420        connectionDeferred = loopbackAsync(sslServerProtocol, sslClientProtocol)
     421
     422        result = Deferred()
     423
     424        def fail(_):
     425            result.errback(False)
     426
     427        def check(reason):
     428            d = sslClientProtocol.notifyHandshakeDone()
     429            if not d.called:
     430                result.errback(Exception('notification should be called'))
     431                return
     432            d.addCallback(fail)
     433            d.addErrback(lambda _: result.callback(None))
     434
     435        sslClientProtocol.notifyHandshakeDone().addCallbacks(fail, check)
     436
     437        return gatherResults([connectionDeferred, result])
     438
     439
     440    def test_handshakeAfterConnectionLost(self):
     441        """
     442        Make sure that the correct handshake paths get run after a connection
     443        is lost.
     444        """
     445        clientConnectionLost = Deferred()
     446        clientFactory = ClientFactory()
     447        clientFactory.protocol = Protocol
     448
     449        clientContextFactory = HandshakeCallbackContextFactory()
     450        wrapperFactory = TLSMemoryBIOFactory(
     451            clientContextFactory, True, clientFactory)
     452        sslClientProtocol = wrapperFactory.buildProtocol(None)
     453
     454        serverConnectionLost = Deferred()
     455        serverFactory = ServerFactory()
     456        serverFactory.protocol = Protocol
     457
     458        # This context factory rejects any clients which do not present a
     459        # certificate.
     460        certificateData = FilePath(certPath).getContent()
     461        certificate = PrivateCertificate.loadPEM(certificateData)
     462        serverContextFactory = certificate.options(certificate)
     463        wrapperFactory = TLSMemoryBIOFactory(
     464            serverContextFactory, False, serverFactory)
     465        sslServerProtocol = wrapperFactory.buildProtocol(None)
     466
     467        connectionDeferred = loopbackAsync(sslServerProtocol, sslClientProtocol)
     468        result = Deferred()
     469
     470        def checkSide(side):
     471            return self.assertFailure(side.notifyHandshakeDone(), Error)
     472
     473        return gatherResults([connectionDeferred, checkSide(sslClientProtocol),
     474                              checkSide(sslServerProtocol)])
     475
     476
    374477    def test_getPeerCertificate(self):
    375478        """
    376479        L{TLSMemoryBIOProtocol.getPeerCertificate} returns the
  • twisted/protocols/tls.py

    diff --git a/twisted/protocols/tls.py b/twisted/protocols/tls.py
    index f139c6a..dd17ab3 100644
    a b to run TLS over unusual transports, such as UNIX sockets and stdio. 
    3737
    3838from __future__ import division, absolute_import
    3939
    40 from OpenSSL.SSL import Error, ZeroReturnError, WantReadError
     40from OpenSSL.SSL import Error, ZeroReturnError, WantReadError, WantWriteError
    4141from OpenSSL.SSL import TLSv1_METHOD, Context, Connection
    4242
    4343try:
    from twisted.python import log 
    5555from twisted.python._reflectpy3 import safe_str
    5656from twisted.internet.interfaces import ISystemHandle, ISSLTransport
    5757from twisted.internet.interfaces import IPushProducer, ILoggingContext
     58from twisted.internet import defer
    5859from twisted.internet.main import CONNECTION_LOST
    5960from twisted.internet.protocol import Protocol
    6061from twisted.internet.task import cooperate
    class TLSMemoryBIOProtocol(ProtocolWrapper): 
    244245        on, and which has no interest in a new transport.  See #3821.
    245246
    246247    @ivar _handshakeDone: A flag indicating whether or not the handshake is
    247         known to have completed successfully (C{True}) or not (C{False}).  This
    248         is used to control error reporting behavior.  If the handshake has not
    249         completed, the underlying L{OpenSSL.SSL.Error} will be passed to the
    250         application's C{connectionLost} method.  If it has completed, any
    251         unexpected L{OpenSSL.SSL.Error} will be turned into a
    252         L{ConnectionLost}.  This is weird; however, it is simply an attempt at
    253         a faithful re-implementation of the behavior provided by
    254         L{twisted.internet.ssl}.
     248        complete (C{True}) or not (C{False}).
     249
     250    @ivar _handshakeError: If the handshake failed, then this will store
     251        the reason.  Otherwise it will be C{None}.
     252
     253    @ivar _handshakeDeferreds: If the handshake is not done, then this
     254        is a list of L{twisted.internet.defer.Deferred} instances to
     255        be completed when the handshake finishes.
    255256
    256257    @ivar _reason: If an unexpected L{OpenSSL.SSL.Error} occurs which causes
    257258        the connection to be lost, it is saved here.  If appropriate, this may
    class TLSMemoryBIOProtocol(ProtocolWrapper): 
    265266
    266267    _reason = None
    267268    _handshakeDone = False
     269    _handshakeError = None
    268270    _lostTLSConnection = False
    269271    _writeBlockedOnRead = False
    270272    _producer = None
    class TLSMemoryBIOProtocol(ProtocolWrapper): 
    272274    def __init__(self, factory, wrappedProtocol, _connectWrapped=True):
    273275        ProtocolWrapper.__init__(self, factory, wrappedProtocol)
    274276        self._connectWrapped = _connectWrapped
     277        self._handshakeDeferreds = []
    275278
    276279
    277280    def getHandle(self):
    class TLSMemoryBIOProtocol(ProtocolWrapper): 
    316319        # Now that we ourselves have a transport (initialized by the
    317320        # ProtocolWrapper.makeConnection call above), kick off the TLS
    318321        # handshake.
    319         try:
    320             self._tlsConnection.do_handshake()
    321         except WantReadError:
    322             # This is the expected case - there's no data in the connection's
    323             # input buffer yet, so it won't be able to complete the whole
    324             # handshake now.  If this is the speak-first side of the
    325             # connection, then some bytes will be in the send buffer now; flush
    326             # them.
    327             self._flushSendBIO()
     322        self.__tryHandshake()
     323
     324
     325    def notifyHandshakeDone(self):
     326        d = defer.Deferred()
     327        if self._handshakeDone:
     328            if self._handshakeError is None:
     329                d.callback(None)
     330            else:
     331                d.errback(self._handshakeError)
     332        else:
     333            self._handshakeDeferreds.append(d)
     334        return d
     335
     336
     337    def __tryHandshake(self):
     338        """
     339        Attempts to handshake.  OpenSSL wants us to keep trying to
     340        handshake until either it works or fails (as opposed to needing
     341        to do I/O).
     342        """
     343        while True:
     344            try:
     345                self._tlsConnection.do_handshake()
     346            except WantReadError:
     347                self._flushSendBIO()  # do_handshake may have queued up a send
     348                return
     349            except WantWriteError:
     350                self._flushSendBIO()
     351                # And try again immediately
     352            except Error as e:
     353                self._tlsShutdownFinished(Failure())
     354                return
     355            else:
     356                self.__handshakeSucceeded()
     357                return
     358
     359
     360    def __handshakeSucceeded(self):
     361        """
     362        Mark the handshake done and notify everyone.  It's okay to call
     363        this more than once.
     364        """
     365        if not self._handshakeDone:
     366            self._handshakeDone = True
     367            deferreds = self._handshakeDeferreds
     368            self._handshakeDeferreds = None
     369            for d in deferreds:
     370                d.callback(None)
    328371
    329372
    330373    def _flushSendBIO(self):
    class TLSMemoryBIOProtocol(ProtocolWrapper): 
    349392        the protocol, as well as handling of the various exceptions which
    350393        can come from trying to get such bytes.
    351394        """
     395        # SSL_read can transparently complete a handshake, but we can't
     396        # rely on it: if the handshake is done but there's no application
     397        # data, then SSL_read won't tell us.
     398        if not self._handshakeDone:
     399            self.__tryHandshake()
     400        if not self._handshakeDone:
     401            return  # Save some effort: SSL_read can't possibly work
     402
    352403        # Keep trying this until an error indicates we should stop or we
    353404        # close the connection.  Looping is necessary to make sure we
    354405        # process all of the data which was put into the receive BIO, as
    class TLSMemoryBIOProtocol(ProtocolWrapper): 
    383434                self._flushSendBIO()
    384435                self._tlsShutdownFinished(failure)
    385436            else:
    386                 # If we got application bytes, the handshake must be done by
    387                 # now.  Keep track of this to control error reporting later.
    388                 self._handshakeDone = True
    389437                ProtocolWrapper.dataReceived(self, bytes)
    390438
    391439        # The received bytes might have generated a response which needs to be
    392         # sent now.  For example, the handshake involves several round-trip
    393         # exchanges without ever producing application-bytes.
     440        # sent now.  This is most likely to occur during renegotiation.
    394441        self._flushSendBIO()
    395442
    396443
    class TLSMemoryBIOProtocol(ProtocolWrapper): 
    438485        Called when TLS connection has gone away; tell underlying transport to
    439486        disconnect.
    440487        """
     488        if not self._handshakeDone:
     489            # This is a handshake failure (either an explicit failure from
     490            # OpenSSL or an implicit failure due to a dropped transport
     491            # connection).
     492            #
     493            # Note: Some testcases evilly call _tlsShutdownFinished(None)
     494            # before the handshake finishes.  This can't happen in real life
     495            # (none of the call sites allow it), so it's okay that we'll
     496            # crash if there's actually anyone waiting for notification
     497            # of the handshake result.
     498            self._handshakeDone = True
     499            self._handshakeError = reason
     500
     501            deferreds = self._handshakeDeferreds
     502            self._handshakeDeferreds = None
     503            for d in deferreds:
     504                d.errback(reason)
     505
    441506        self._reason = reason
    442507        self._lostTLSConnection = True
    443508        # Using loseConnection causes the application protocol's
    class TLSMemoryBIOProtocol(ProtocolWrapper): 
    457522        """
    458523        if not self._lostTLSConnection:
    459524            # Tell the TLS connection that it's not going to get any more data
    460             # and give it a chance to finish reading.
     525            # and give it a chance to finish handshaking and/or reading.
    461526            self._tlsConnection.bio_shutdown()
    462527            self._flushReceiveBIO()
    463528            self._lostTLSConnection = True
    class TLSMemoryBIOProtocol(ProtocolWrapper): 
    532597                self._tlsShutdownFinished(Failure())
    533598                break
    534599            else:
    535                 # If we sent some bytes, the handshake must be done.  Keep
    536                 # track of this to control error reporting behavior.
    537                 self._handshakeDone = True
     600                # SSL_write can transparently complete a handshake.  If we
     601                # get here, then we're done handshaking.
     602                self.__handshakeSucceeded()
    538603                self._flushSendBIO()
    539604                alreadySent += sent
    540605
  • new file twisted/web/topfiles/6204.feature

    diff --git a/twisted/web/topfiles/6204.feature b/twisted/web/topfiles/6204.feature
    new file mode 100644
    index 0000000..1aed481
    - +  
     1twisted.internet.interfaces.ISSLTransport now has a notifyHandshakeDone method to request notification when the handshake succeeds or fails.