Подтвердить что ты не робот

Почему при попытке запустить TLS через TLS с помощью этого кода возникает ошибка рукопожатия?

Я попытался реализовать протокол, который может запускать 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 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')]

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

4b9b3361

Ответ 1

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

  • Самый внутренний TLSMemoryBIOProtocol становится wrappedProtocol, когда он должен быть самым внешним;
  • ProtocolWithoutConnectionLost не выталкивает любой стек TLSMemoryBIOProtocol off OnionProtocol, потому что connectionLost вызывается только после того, как методы FileDescriptor 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 и поддерживает вход и выход:

Клиент и LineReceiver

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

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

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

OnionClient без startTLS

OnionClient завернуты в 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 два TLSMemoryBIOProtocols, сломанные

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

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

startTLS с двумя TLSMemoryBIOProtocols, working

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

Входящие данные как обход связанного списка

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

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

Двунаправленный список с протоколами и транспортом

Атрибут 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 C{loseConnection}.  A zero-length
    read also calls the transport 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 terminated.
            self.wrappedProtocol = tlsProtocol
            nextTransport = PopOnDisconnectTransport(
                original=self.transport,
                pop=self._pop
            )
            # Store the proxied transport as the list 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 next link becomes the popped
            # protocol 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 является то, что она зависит от особенностей реализации TLSMemoryBIOProtocol. Для обобщенного решения потребуются новые API-интерфейсы на многих уровнях Twisted.

До тех пор мы придерживаемся еще одного примера Закона о хираме.

Ответ 2

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

Ответ 3

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