Почему происходит сбой рукопожатия при попытке запустить TLS поверх TLS с этим кодом? - PullRequest
66 голосов
/ 27 февраля 2011

Я попытался реализовать протокол, который может запускать TLS через TLS, используя twisted.protocols.tls, интерфейс к OpenSSL с использованием памяти BIO.

Я реализовал это как оболочку протокола, которая в основном выглядит как обычный транспорт TCP, но у которого есть startTLS и stopTLS методы для добавления и удаления слоя TLS соответственно.Это прекрасно работает для первого слоя TLS.Он также работает нормально, если я запускаю его через "собственный" транспорт Twisted TLS.Однако, если я попытаюсь добавить второй слой TLS, используя метод startTLS, предоставляемый этой оболочкой, немедленно произойдет ошибка рукопожатия, и соединение завершится в неизвестном неиспользуемом состоянии.это позволяет ему работать так:

from twisted.python.components import proxyForInterface
from twisted.internet.error import ConnectionDone
from twisted.internet.interfaces import ITCPTransport, IProtocol
from twisted.protocols.tls import TLSMemoryBIOFactory, TLSMemoryBIOProtocol
from twisted.protocols.policies import ProtocolWrapper, WrappingFactory

class TransportWithoutDisconnection(proxyForInterface(ITCPTransport)):
    """
    A proxy for a normal transport that disables actually closing the connection.
    This is necessary so that when TLSMemoryBIOProtocol notices the SSL EOF it
    doesn't actually close the underlying connection.

    All methods except loseConnection are proxied directly to the real transport.
    """
    def loseConnection(self):
        pass


class ProtocolWithoutConnectionLost(proxyForInterface(IProtocol)):
    """
    A proxy for a normal protocol which captures clean connection shutdown
    notification and sends it to the TLS stacking code instead of the protocol.
    When TLS is shutdown cleanly, this notification will arrive.  Instead of telling
    the protocol that the entire connection is gone, the notification is used to
    unstack the TLS code in OnionProtocol and hidden from the wrapped protocol.  Any
    other kind of connection shutdown (SSL handshake error, network hiccups, etc) are
    treated as real problems and propagated to the wrapped protocol.
    """
    def connectionLost(self, reason):
        if reason.check(ConnectionDone):
            self.onion._stopped()
        else:
            super(ProtocolWithoutConnectionLost, self).connectionLost(reason)


class OnionProtocol(ProtocolWrapper):
    """
    OnionProtocol is both a transport and a protocol.  As a protocol, it can run over
    any other ITransport.  As a transport, it implements stackable TLS.  That is,
    whatever application traffic is generated by the protocol running on top of
    OnionProtocol can be encapsulated in a TLS conversation.  Or, that TLS conversation
    can be encapsulated in another TLS conversation.  Or **that** TLS conversation can
    be encapsulated in yet *another* TLS conversation.

    Each layer of TLS can use different connection parameters, such as keys, ciphers,
    certificate requirements, etc.  At the remote end of this connection, each has to
    be decrypted separately, starting at the outermost and working in.  OnionProtocol
    can do this itself, of course, just as it can encrypt each layer starting with the
    innermost.
    """
    def makeConnection(self, transport):
        self._tlsStack = []
        ProtocolWrapper.makeConnection(self, transport)


    def startTLS(self, contextFactory, client, bytes=None):
        """
        Add a layer of TLS, with SSL parameters defined by the given contextFactory.

        If *client* is True, this side of the connection will be an SSL client.
        Otherwise it will be an SSL server.

        If extra bytes which may be (or almost certainly are) part of the SSL handshake
        were received by the protocol running on top of OnionProtocol, they must be
        passed here as the **bytes** parameter.
        """
        # First, create a wrapper around the application-level protocol
        # (wrappedProtocol) which can catch connectionLost and tell this OnionProtocol 
        # about it.  This is necessary to pop from _tlsStack when the outermost TLS
        # layer stops.
        connLost = ProtocolWithoutConnectionLost(self.wrappedProtocol)
        connLost.onion = self
        # Construct a new TLS layer, delivering events and application data to the
        # wrapper just created.
        tlsProtocol = TLSMemoryBIOProtocol(None, connLost, False)
        tlsProtocol.factory = TLSMemoryBIOFactory(contextFactory, client, None)

        # Push the previous transport and protocol onto the stack so they can be
        # retrieved when this new TLS layer stops.
        self._tlsStack.append((self.transport, self.wrappedProtocol))

        # Create a transport for the new TLS layer to talk to.  This is a passthrough
        # to the OnionProtocol's current transport, except for capturing loseConnection
        # to avoid really closing the underlying connection.
        transport = TransportWithoutDisconnection(self.transport)

        # Make the new TLS layer the current protocol and transport.
        self.wrappedProtocol = self.transport = tlsProtocol

        # And connect the new TLS layer to the previous outermost transport.
        self.transport.makeConnection(transport)

        # If the application accidentally got some bytes from the TLS handshake, deliver
        # them to the new TLS layer.
        if bytes is not None:
            self.wrappedProtocol.dataReceived(bytes)


    def stopTLS(self):
        """
        Remove a layer of TLS.
        """
        # Just tell the current TLS layer to shut down.  When it has done so, we'll get
        # notification in *_stopped*.
        self.transport.loseConnection()


    def _stopped(self):
        # A TLS layer has completely shut down.  Throw it away and move back to the
        # TLS layer it was wrapping (or possibly back to the original non-TLS
        # transport).
        self.transport, self.wrappedProtocol = self._tlsStack.pop()

У меня есть простые клиентские и серверные программы для этого, доступные на панели запуска (bzr branch lp:~exarkun/+junk/onion).Когда я использую его для вызова метода startTLS, описанного выше, дважды, без промежуточного вызова к stopTLS, возникает ошибка OpenSSL:

OpenSSL.SSL.Error: [('SSL routines', 'SSL23_GET_SERVER_HELLO', 'unknown protocol')]

Почему что-то идет не так?

Ответы [ 3 ]

19 голосов
/ 09 декабря 2017

Есть как минимум две проблемы с OnionProtocol:

  1. самый внутренний TLSMemoryBIOProtocol становится wrappedProtocol, когда он должен быть самый внешний ;
  2. ProtocolWithoutConnectionLost не извлекает TLSMemoryBIOProtocol s из стека OnionProtocol, потому что connectionLost вызывается только после того, как методы FileDescriptor s doRead или doWrite возвращают причину отключения.

Мы не можем решить первую проблему, не изменив способ управления OnionProtocol его стеком, и мы не можем решить вторую, пока не выясним реализацию нового стека. Неудивительно, что правильный дизайн является прямым следствием потоков данных в Twisted, поэтому мы начнем с некоторого анализа потока данных.

Twisted представляет собой установленное соединение с экземпляром twisted.internet.tcp.Server или twisted.internet.tcp.Client. Поскольку единственная интерактивность в нашей программе происходит в stoptls_client, мы будем рассматривать только поток данных в Client экземпляр и из него.

Давайте прогреемся с минимальным клиентом LineReceiver, который отображает обратные линии, полученные от локального сервера через порт 9999:

from twisted.protocols import basic
from twisted.internet import defer, endpoints, protocol, task

class LineReceiver(basic.LineReceiver):
    def lineReceived(self, line):
        self.sendLine(line)

def main(reactor):
    clientEndpoint = endpoints.clientFromString(
        reactor, "tcp:localhost:9999")
    connected = clientEndpoint.connect(
        protocol.ClientFactory.forProtocol(LineReceiver))
    def waitForever(_):
        return defer.Deferred()
    return connected.addCallback(waitForever)

task.react(main)

Как только установленное соединение установлено, Client становится транспортом нашего протокола LineReceiver и опосредует ввод и вывод:

Client and LineReceiver

Новые данные с сервера приводят к тому, что реактор вызывает метод Client doRead, который, в свою очередь, передает полученное значение методу LineReceiver dataReceived. Наконец, LineReceiver.dataReceived вызывает LineReceiver.lineReceived, когда доступна хотя бы одна линия.

Наше приложение отправляет строку данных обратно на сервер, вызывая LineReceiver.sendLine. Это вызывает write для транспорта, привязанного к экземпляру протокола, который является тем же экземпляром Client, который обрабатывал входящие данные. Client.write обеспечивает отправку данных реактором, тогда как Client.doWrite фактически отправляет данные через сокет.

Мы готовы взглянуть на поведение OnionClient, которое никогда не вызывает startTLS:

OnionClient without startTLS

OnionClient s заключены в OnionProtocol s , что является сутью нашей попытки вложенного TLS. В качестве подкласса twisted.internet.policies.ProtocolWrapper, экземпляр OnionProtocol является своего рода сэндвичем для передачи протокола; он представляет собой протокол для транспорта более низкого уровня и транспорт для протокола, который он оборачивает через маскарад, установленный во время соединения с помощью WrappingFactory.

Теперь Client.doRead вызывает OnionProtocol.dataReceived, который передает данные до OnionClient. В качестве транспорта OnionClient, OnionProtocol.write принимает строки для отправки из OnionClient.sendLine и передает их до Client, это собственный транспорт. Это нормальное взаимодействие между ProtocolWrapper, его обернутым протоколом и его собственным транспортом, поэтому, естественно, данные передаются друг другу без каких-либо проблем.

OnionProtocol.startTLS делает что-то другое. Он пытается вставить новую ProtocolWrapper - которая оказывается TLSMemoryBIOProtocol - между установленной протокольно-транспортной парой. Это кажется достаточно простым: ProtocolWrapper хранит протокол верхнего уровня как его wrappedProtocol атрибут , а прокси write и другие атрибуты вплоть до своего собственного транспорта . startTLS должен иметь возможность вставлять новый TLSMemoryBIOProtocol, который включает OnionClient в соединение, путем установки этого экземпляра поверх его собственных wrappedProtocol и transport:

def startTLS(self):
    ...
    connLost = ProtocolWithoutConnectionLost(self.wrappedProtocol)
    connLost.onion = self
    # Construct a new TLS layer, delivering events and application data to the
    # wrapper just created.
    tlsProtocol = TLSMemoryBIOProtocol(None, connLost, False)

    # Push the previous transport and protocol onto the stack so they can be
    # retrieved when this new TLS layer stops.
    self._tlsStack.append((self.transport, self.wrappedProtocol))
    ...
    # Make the new TLS layer the current protocol and transport.
    self.wrappedProtocol = self.transport = tlsProtocol

Вот поток данных после первого вызова startTLS:

startTLS one TLSMemoryBIOProtocol, working

Как и ожидалось, новые данные, доставленные в OnionProtocol.dataReceived, направляются в TLSMemoryBIOProtocol, хранящийся в _tlsStack, который передает расшифрованный открытый текст в OnionClient.dataReceived. OnionClient.sendLine также передает свои данные в TLSMemoryBIOProtocol.write, который шифрует их и отправляет полученный зашифрованный текст в OnionProtocol.write, а затем Client.write.

К сожалению, эта схема не работает после второго вызова startTLS. Основной причиной является эта строка:

    self.wrappedProtocol = self.transport = tlsProtocol

Каждый вызов startTLS заменяет wrappedProtocol на самый внутренний TLSMemoryBIOProtocol, хотя данные, полученные Client.doRead, были зашифрованы самым внешним :

startTLS two TLSMemoryBIOProtocols, broken

transport, однако, вложены правильно.OnionClient.sendLine может вызывать только свой транспортный write - то есть OnionProtocol.write - поэтому OnionProtocol должен заменить его transport на самый внутренний TLSMemoryBIOProtocol, чтобы гарантировать, что записи последовательно вложены в дополнительные уровни шифрования.

Решение, таким образом, состоит в том, чтобы обеспечить передачу данных через первый TLSMemoryBIOProtocol на _tlsStack к следующему по очереди, так, чтобы каждый уровень шифрованияснимается в обратном порядке:

startTLS with two TLSMemoryBIOProtocols, working

Представление _tlsStack в виде списка кажется менее естественным, учитывая это новое требование.К счастью, представление входящего потока данных линейно предполагает новую структуру данных:

Incoming data as a linked list traversal

И глючный, и правильный поток входящих данных напоминают односвязный списокс wrappedProtocol, служащим в качестве ProtocolWrapper следующих ссылок, и protocol, служащим в качестве Client.Список должен расти вниз от OnionProtocol и всегда заканчиваться OnionClient.Ошибка возникает из-за того, что инвариант упорядочения нарушен.

Односвязный список хорош для проталкивания протоколов в стек, но неудобен для их удаления, поскольку для его удаления требуется обход вниз от его головы к узлу.Конечно, этот обход происходит каждый раз, когда данные получены, поэтому проблема заключается в сложности, связанной с дополнительным обходом, а не в наихудшей временной сложности.К счастью, список фактически связан дважды:

Doubly linked list with protocols and transports

Атрибут transport связывает каждый вложенный протокол со своим предшественником, так что transport.write может слойна последовательно более низких уровнях шифрования перед окончательной отправкой данных по сети.У нас есть два стража, чтобы помочь в управлении списком: Client всегда должен быть вверху и OnionClient всегда должен быть внизу.

Соединяя их вместе, мы получим следующее:

from twisted.python.components import proxyForInterface
from twisted.internet.interfaces import ITCPTransport
from twisted.protocols.tls import TLSMemoryBIOFactory, TLSMemoryBIOProtocol
from twisted.protocols.policies import ProtocolWrapper, WrappingFactory


class PopOnDisconnectTransport(proxyForInterface(ITCPTransport)):
    """
    L{TLSMemoryBIOProtocol.loseConnection} shuts down the TLS session
    and calls its own transport's C{loseConnection}.  A zero-length
    read also calls the transport's C{loseConnection}.  This proxy
    uses that behavior to invoke a C{pop} callback when a session has
    ended.  The callback is invoked exactly once because
    C{loseConnection} must be idempotent.
    """
    def __init__(self, pop, **kwargs):
        super(PopOnDisconnectTransport, self).__init__(**kwargs)
        self._pop = pop

    def loseConnection(self):
        self._pop()
        self._pop = lambda: None


class OnionProtocol(ProtocolWrapper):
    """
    OnionProtocol is both a transport and a protocol.  As a protocol,
    it can run over any other ITransport.  As a transport, it
    implements stackable TLS.  That is, whatever application traffic
    is generated by the protocol running on top of OnionProtocol can
    be encapsulated in a TLS conversation.  Or, that TLS conversation
    can be encapsulated in another TLS conversation.  Or **that** TLS
    conversation can be encapsulated in yet *another* TLS
    conversation.

    Each layer of TLS can use different connection parameters, such as
    keys, ciphers, certificate requirements, etc.  At the remote end
    of this connection, each has to be decrypted separately, starting
    at the outermost and working in.  OnionProtocol can do this
    itself, of course, just as it can encrypt each layer starting with
    the innermost.
    """

    def __init__(self, *args, **kwargs):
        ProtocolWrapper.__init__(self, *args, **kwargs)
        # The application level protocol is the sentinel at the tail
        # of the linked list stack of protocol wrappers.  The stack
        # begins at this sentinel.
        self._tailProtocol = self._currentProtocol = self.wrappedProtocol


    def startTLS(self, contextFactory, client, bytes=None):
        """
        Add a layer of TLS, with SSL parameters defined by the given
        contextFactory.

        If *client* is True, this side of the connection will be an
        SSL client.  Otherwise it will be an SSL server.

        If extra bytes which may be (or almost certainly are) part of
        the SSL handshake were received by the protocol running on top
        of OnionProtocol, they must be passed here as the **bytes**
        parameter.
        """
        # The newest TLS session is spliced in between the previous
        # and the application protocol at the tail end of the list.
        tlsProtocol = TLSMemoryBIOProtocol(None, self._tailProtocol, False)
        tlsProtocol.factory = TLSMemoryBIOFactory(contextFactory, client, None)

        if self._currentProtocol is self._tailProtocol:
            # This is the first and thus outermost TLS session.  The
            # transport is the immutable sentinel that no startTLS or
            # stopTLS call will move within the linked list stack.
            # The wrappedProtocol will remain this outermost session
            # until it's terminated.
            self.wrappedProtocol = tlsProtocol
            nextTransport = PopOnDisconnectTransport(
                original=self.transport,
                pop=self._pop
            )
            # Store the proxied transport as the list's head sentinel
            # to enable an easy identity check in _pop.
            self._headTransport = nextTransport
        else:
            # This a later TLS session within the stack.  The previous
            # TLS session becomes its transport.
            nextTransport = PopOnDisconnectTransport(
                original=self._currentProtocol,
                pop=self._pop
            )

        # Splice the new TLS session into the linked list stack.
        # wrappedProtocol serves as the link, so the protocol at the
        # current position takes our new TLS session as its
        # wrappedProtocol.
        self._currentProtocol.wrappedProtocol = tlsProtocol
        # Move down one position in the linked list.
        self._currentProtocol = tlsProtocol
        # Expose the new, innermost TLS session as the transport to
        # the application protocol.
        self.transport = self._currentProtocol
        # Connect the new TLS session to the previous transport.  The
        # transport attribute also serves as the previous link.
        tlsProtocol.makeConnection(nextTransport)

        # Left over bytes are part of the latest handshake.  Pass them
        # on to the innermost TLS session.
        if bytes is not None:
            tlsProtocol.dataReceived(bytes)


    def stopTLS(self):
        self.transport.loseConnection()


    def _pop(self):
        pop = self._currentProtocol
        previous = pop.transport
        # If the previous link is the head sentinel, we've run out of
        # linked list.  Ensure that the application protocol, stored
        # as the tail sentinel, becomes the wrappedProtocol, and the
        # head sentinel, which is the underlying transport, becomes
        # the transport.
        if previous is self._headTransport:
            self._currentProtocol = self.wrappedProtocol = self._tailProtocol
            self.transport = previous
        else:
            # Splice out a protocol from the linked list stack.  The
            # previous transport is a PopOnDisconnectTransport proxy,
            # so first retrieve proxied object off its original
            # attribute.
            previousProtocol = previous.original
            # The previous protocol's next link becomes the popped
            # protocol's next link
            previousProtocol.wrappedProtocol = pop.wrappedProtocol
            # Move up one position in the linked list.
            self._currentProtocol = previousProtocol
            # Expose the new, innermost TLS session as the transport
            # to the application protocol.
            self.transport = self._currentProtocol



class OnionFactory(WrappingFactory):
    """
    A L{WrappingFactory} that overrides
    L{WrappingFactory.registerProtocol} and
    L{WrappingFactory.unregisterProtocol}.  These methods store in and
    remove from a dictionary L{ProtocolWrapper} instances.  The
    C{transport} patching done as part of the linked-list management
    above causes the instances' hash to change, because the
    C{__hash__} is proxied through to the wrapped transport.  They're
    not essential to this program, so the easiest solution is to make
    them do nothing.
    """
    protocol = OnionProtocol

    def registerProtocol(self, protocol):
        pass


    def unregisterProtocol(self, protocol):
        pass

(Это также доступно на GitHub .)

Решение второй проблемы заключается в PopOnDisconnectTransport.Исходный код пытался извлечь сеанс TLS из стека с помощью connectionLost, но поскольку только закрытый файловый дескриптор вызывает вызов connectionLost, ему не удалось удалить остановленные сеансы TLS, которые не закрывали базовый сокет.

На момент написания этой статьи TLSMemoryBIOProtocol называет свой транспортный номер loseConnection точно в двух местах: _shutdownTLS и _tlsShutdownFinished._shutdownTLS вызывается при активном закрытии (loseConnection, abortConnection, unregisterProducer и после loseConnection и всех ожидающих записейбыли сброшены ), в то время как _tlsShutdownFinished вызывается при пассивном закрытии ( отказы рукопожатия , пустые чтения , ошибки чтения и записьошибки ).Все это означает, что обе стороны закрытого соединения могут вытолкнуть остановленные сеансы TLS из стека во время loseConnection.PopOnDisconnectTransport делает это идемпотентно, потому что loseConnection, как правило, идемпотентно, и TLSMemoryBIOProtocol определенно ожидает, что оно будет.

Недостатком использования логики стекового управления в loseConnection является то, что она зависит от особенностей *Реализация 1267 *.Для обобщенного решения потребуются новые API на многих уровнях Twisted.

До этого мы застряли с другим примером закона Хайрама .

1 голос
/ 20 мая 2011

Вам может потребоваться сообщить удаленному устройству, что вы хотите запустить среду, и выделить ресурсы для второго уровня, прежде чем запускать его, если это устройство обладает возможностями.

0 голосов
/ 20 июля 2011

Если вы используете одинаковые параметры TLS для обоих уровней и подключаетесь к одному и тому же хосту, то, вероятно, вы используете одну и ту же пару ключей для обоих уровней шифрования. Попробуйте использовать другую пару ключей для вложенного слоя, например туннелирование на третий хост / порт. т.е.: localhost:30000 (клиент) -> localhost:8080 (TLS-уровень 1, использующий пару ключей A) -> localhost:8081 (TLS-уровень 2, использующий пару ключей B).

...