Program Club

이 코드로 TLS를 통해 TLS를 실행하려고 할 때 핸드 셰이크 오류가 발생하는 이유는 무엇입니까?

proclub 2020. 11. 15. 11:55
반응형

이 코드로 TLS를 통해 TLS를 실행하려고 할 때 핸드 셰이크 오류가 발생하는 이유는 무엇입니까?


twisted.protocols.tls메모리 BIO를 사용하여 OpenSSL에 대한 인터페이스를 사용하여 TLS를 통해 TLS를 실행할 수있는 프로토콜을 구현하려고했습니다 .

나는 대부분이 일반 TCP 전송 같다고 프로토콜 래퍼로이 구현되지만이있는 startTLSstopTLS추가하고 각각 TLS의 층을 제거하는 방법. 이것은 TLS의 첫 번째 계층에서 잘 작동합니다. "네이티브"Twisted TLS 전송을 통해 실행하는 경우에도 잘 작동합니다. 그러나이 startTLS래퍼에서 제공 하는 방법을 사용하여 두 번째 TLS 계층을 추가하려고하면 즉시 핸드 셰이크 오류가 발생하고 연결이 알 수없는 사용할 수없는 상태가됩니다.

래퍼와 작동을 허용하는 두 개의 도우미는 다음과 같습니다.

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')]

일이 잘못되는 이유는 무엇입니까?


다음과 같은 두 가지 문제가 있습니다 OnionProtocol.

  1. 최는 TLSMemoryBIOProtocol 된다 wrappedProtocol가되어야 할 때, ;
  2. ProtocolWithoutConnectionLosts 또는 메서드가 연결 해제 이유를 반환 한 후에 만 호출 되므로의 스택에서 TLSMemoryBIOProtocols를 팝하지 않습니다 .OnionProtocolconnectionLostFileDescriptordoReaddoWrite

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)

설정된 연결이 설정되면 a ClientLineReceiver프로토콜의 전송이되고 입력 및 출력을 중재합니다.

클라이언트 및 LineReceiver

서버의 새로운 데이터는 리액터가 ClientdoRead메서드 를 호출하도록하고 , 그러면받은 데이터를 LineReceiverdataReceived메서드로 전달합니다. 마지막으로 하나 이상의 회선을 사용할 수있을 때 LineReceiver.dataReceived호출 LineReceiver.lineReceived합니다.

우리의 응용 프로그램은를 호출하여 데이터 라인을 서버로 다시 보냅니다 LineReceiver.sendLine. 이는 수신 데이터를 처리 write한 동일한 Client인스턴스 인 프로토콜 인스턴스에 바인딩 된 전송을 호출 합니다. Client.write리액터에서 데이터를 보내도록 정렬하고 Client.doWrite실제로는 소켓을 통해 데이터를 보냅니다.

우리는 OnionClient절대 호출하지 않는 의 동작을 볼 준비가되었습니다 startTLS.

startTLS가없는 OnionClient

OnionClient들에 싸여 OnionProtocol 중첩 된 TLS에서 우리의 시도의 핵심이다. 의 서브 클래스 twisted.internet.policies.ProtocolWrapper로서의 인스턴스 OnionProtocol는 일종의 프로토콜 전송 샌드위치입니다. 그것은 낮은 수준의 전송에 대한 프로토콜자신을 제시하고 프로토콜 에 대한 전송 으로 WrappingFactory.

이제를 Client.doRead호출 OnionProtocol.dataReceived하여 데이터를 OnionClient. 으로 OnionClient의 수송 OnionProtocol.write에서 보낼 줄 수용 OnionClient.sendLine및 프록시 그들을 Client그, 자신의 전송을. 이것은 ProtocolWrapper래핑 된 프로토콜과 자체 전송 간의 정상적인 상호 작용 이므로 자연스럽게 데이터가 아무런 문제없이 각각에 대해주고받습니다.

OnionProtocol.startTLS뭔가 다릅니다. 그것은 새로운 삽입 할 시도 ProtocolWrapper될 일이 어떤을 - TLSMemoryBIOProtocol사이 - 설립 프로토콜 전송 쌍입니다. 이것은 충분히 쉬운 것 같습니다. a ProtocolWrapper는 상위 수준 프로토콜을 wrappedProtocol속성 으로 저장하고 프록시 write및 기타 속성을 자체 전송에 저장 합니다. 해당 인스턴스를 자체 다음 같이 패치하여 연결에 래핑 startTLS하는 새 항목을 삽입 할 수 있어야합니다 .TLSMemoryBIOProtocolOnionClientwrappedProtocoltransport

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 하나의 TLSMemoryBIOProtocol, 작동 중

예상대로에 전달 된 새 데이터 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, 손상됨

그러나 transports는 올바르게 중첩됩니다. OnionClient.sendLine에만 전송의 호출 할 수 write있다 - OnionProtocol.write- 그래서 OnionProtocol그 교체해야 transport가장 안쪽에 TLSMemoryBIOProtocol쓰기가 연속적으로 암호화의 추가 레이어 안에 중첩되도록 할 수 있습니다.

이 용액을, 그 다음, 그 데이터를 통해 유동되도록하는 TLSMemoryBIOProtocol_tlsStack받는 다음 차례로 하나 그래서 암호화의 각 층은 역순으로 박리되어가 도포 :

두 개의 TLSMemoryBIOProtocol이있는 startTLS, 작동

대표 _tlsStack목록으로는이 새로운 요구 사항 주어진 적은 천연 보인다. 다행히 들어오는 데이터 흐름을 선형으로 나타내는 것은 새로운 데이터 구조를 제안합니다.

연결된 목록 순회로 들어오는 데이터

버그 및 수신 데이터의 정확한 흐름을 모두와 함께, 싱글 링크드리스트를 닮은 wrappedProtocol역할 ProtocolWrapper의 다음 링크와 protocol역할 Client의 '. 목록은 아래로 내려 가며 OnionProtocol항상 OnionClient. 이 버그는 주문 불변이 위반되기 때문에 발생합니다.

단일 연결 목록은 프로토콜을 스택에 푸시하는 데는 좋지만 제거하려면 머리에서 노드까지 아래쪽으로 순회해야하기 때문에 제거하기가 어색합니다. 물론,이 순회는 데이터가 수신 될 때마다 발생하므로 최악의 경우 시간 복잡도가 아니라 추가 순회가 내포하는 복잡도가 우려됩니다. 다행히도 목록은 실제로 이중으로 연결되어 있습니다.

프로토콜 및 전송과 이중 연결 목록

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 세션을 제거하지 못했습니다.

At the time of this writing, TLSMemoryBIOProtocol calls its transport's loseConnection in exactly two places: _shutdownTLS and _tlsShutdownFinished. _shutdownTLS is called on active closes (loseConnection, abortConnection, unregisterProducer and after loseConnection and all pending writes have been flushed), while _tlsShutdownFinished is called on passive closes (handshake failures, empty reads, read errors, and write errors). This all means that both sides of a closed connection can pop stopped TLS sessions off the stack during loseConnection. PopOnDisconnectTransport does this idempotently because loseConnection is generally idempotent, and TLSMemoryBIOProtocol certainly expects it to be.

The downside to putting stack management logic in loseConnection is that it depends on the particulars of TLSMemoryBIOProtocol's implementation. A generalized solution would require new APIs across many levels of Twisted.

Until then, we're stuck with another example of Hyrum's Law.


You may need to inform the remote device that you wish to start an environment and allocate resources for the second layer before you start it up, if that device has the capabilities.


두 계층에 동일한 TLS 매개 변수를 사용하고 동일한 호스트에 연결하는 경우 두 암호화 계층 모두에 동일한 키 쌍을 사용하고있을 수 있습니다. 세 번째 호스트 / 포트로의 터널링과 같이 중첩 된 계층에 대해 다른 키 쌍을 사용해보십시오. 즉 : localhost:30000(클라이언트)-> localhost:8080(키 쌍 A를 사용하는 TLS 레이어 1)-> localhost:8081(키 쌍 B를 사용하는 TLS 레이어 2).

참고 URL : https://stackoverflow.com/questions/5130080/why-is-there-a-handshake-failure-when-trying-to-run-tls-over-tls-with-this-code

반응형