openssl - significa - realizando una negociacion tls solucion
¿Por qué hay una falla de handshake al intentar ejecutar TLS sobre TLS con este código? (3)
Traté de implementar un protocolo que pueda ejecutar TLS sobre TLS usando twisted.protocols.tls
, una interfaz para OpenSSL usando una memoria BIO.
Implementé esto como un envoltorio de protocolo que en su mayoría parece un transporte TCP normal, pero que tiene métodos startTLS
y stopTLS
para agregar y eliminar una capa de TLS, respectivamente. Esto funciona bien para la primera capa de TLS. También funciona bien si lo ejecuto sobre un transporte Twisted TLS "nativo". Sin embargo, si trato de agregar una segunda capa TLS usando el método startTLS
provisto por este contenedor, inmediatamente se startTLS
un error de handshake y la conexión termina en un estado inutilizable desconocido.
El contenedor y los dos ayudantes que lo dejaron funcionar se ve así:
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()
Tengo programas simples de cliente y servidor para ejercitar esto, disponible desde launchpad ( bzr branch lp:~exarkun/+junk/onion
). Cuando lo uso para llamar al método startTLS
arriba dos veces, sin intervenir para stopTLS
, stopTLS
este error de OpenSSL:
OpenSSL.SSL.Error: [(''SSL routines'', ''SSL23_GET_SERVER_HELLO'', ''unknown protocol'')]
¿Por qué las cosas van mal?
Es posible que necesite informar al dispositivo remoto que desea iniciar un entorno y asignar recursos para la segunda capa antes de iniciarlo, si ese dispositivo tiene las capacidades.
Hay al menos dos problemas con OnionProtocol
:
- El
TLSMemoryBIOProtocol
másTLSMemoryBIOProtocol
convierte en elwrappedProtocol
, cuando debería ser el más externo ; -
ProtocolWithoutConnectionLost
noTLSMemoryBIOProtocol
ningúnTLSMemoryBIOProtocol
de la pila deOnionProtocol
, porqueconnectionLost
solo sedoRead
doWrite
métodosdoRead
odoWrite
devuelven un motivo para la desconexión.
No podemos resolver el primer problema sin cambiar la forma en que OnionProtocol
administra su pila, y no podemos resolver el segundo hasta que OnionProtocol
la nueva implementación de la pila. Como era de esperar, el diseño correcto es una consecuencia directa de cómo fluyen los datos dentro de Twisted, por lo que comenzaremos con algunos análisis de flujo de datos.
Twisted representa una conexión establecida con una instancia de twisted.internet.tcp.Server
o twisted.internet.tcp.Client
. Dado que la única interactividad en nuestro programa ocurre en stoptls_client
, solo consideraremos el flujo de datos hacia y desde una instancia de Client
.
Vamos a calentarnos con un cliente LineReceiver
mínimo que hace eco de las líneas de respaldo recibidas de un servidor local en el puerto 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)
Una vez establecida la conexión establecida, un Client
convierte en el transporte de nuestro protocolo LineReceiver
y media la entrada y la salida:
La nueva información del servidor hace que el reactor llame al método doRead
del Client
, que a su vez pasa lo que se recibió al método dataReceived
. Finalmente, LineReceiver.dataReceived
llama a LineReceiver.lineReceived
cuando al menos una línea está disponible.
Nuestra aplicación envía una línea de datos al servidor llamando a LineReceiver.sendLine
. Esto llama a write
en el transporte vinculado a la instancia de protocolo, que es la misma instancia de Client
que manejó los datos entrantes. Client.write
organiza los datos que enviará el reactor, mientras que Client.doWrite
realmente envía los datos a través del socket.
Estamos listos para observar los comportamientos de un OnionClient
que nunca llama a startTLS
:
OnionClient
están envueltos en OnionProtocols , que son el quid de nuestro intento de TLS anidado. Como una subclase de twisted.internet.policies.ProtocolWrapper
, una instancia de OnionProtocol
es una especie de sándwich protocolo-transporte; se presenta como un protocolo para un transporte de nivel inferior y como un transporte a un protocolo que envuelve una mascarada establecida en el momento de la conexión por un WrappingFactory
.
Ahora, Client.doRead
llama a OnionProtocol.dataReceived
, que OnionProtocol.dataReceived
los datos a través de OnionClient
. Como transporte de OnionProtocol.write
, OnionProtocol.write
acepta líneas para enviar desde OnionClient.sendLine
y las OnionClient.sendLine
al Client
, su propio transporte. Esta es la interacción normal entre un ProtocolWrapper
, su protocolo envuelto y su propio transporte, por lo que naturalmente los datos fluyen hacia y desde cada uno sin ningún problema.
OnionProtocol.startTLS
hace algo diferente. Intenta interponer un nuevo ProtocolWrapper
, que pasa a ser un TLSMemoryBIOProtocol
, entre un par de protocolos y transporte establecido . Esto parece bastante fácil: un ProtocolWrapper
almacena el protocolo de nivel superior como su atributo wrappedProtocol
, y los proxies write
y otros atributos hasta su propio transporte . startTLS
debería poder inyectar un nuevo TLSMemoryBIOProtocol
que envuelva a OnionClient
en la conexión parcheando esa instancia sobre su propio wrappedProtocol
y 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
Aquí está el flujo de datos después de la primera llamada a startTLS
:
Como era de esperar, los datos nuevos entregados a OnionProtocol.dataReceived
se enrutan al TLSMemoryBIOProtocol
almacenado en _tlsStack
, que pasa el texto plano descifrado a OnionClient.dataReceived
. OnionClient.sendLine
también pasa sus datos a TLSMemoryBIOProtocol.write
, que lo cifra y envía el texto cifrado resultante a OnionProtocol.write
y Client.write
.
Lamentablemente, este esquema falla después de una segunda llamada a startTLS
. La causa principal es esta línea:
self.wrappedProtocol = self.transport = tlsProtocol
Cada llamada a startTLS
reemplaza el wrappedProtocol
con el más interno TLSMemoryBIOProtocol
, aunque los datos recibidos por Client.doRead
fueron encriptados por el más externo :
Sin embargo, los transport
están anidados correctamente. OnionClient.sendLine
solo puede llamar la write
su transporte, es decir, OnionProtocol.write
, por lo que OnionProtocol
debe reemplazar su transport
por el más interno TLSMemoryBIOProtocol
para garantizar que las escrituras se TLSMemoryBIOProtocol
sucesivamente dentro de capas adicionales de cifrado.
La solución, entonces, es garantizar que los datos fluyan a través del primer TLSMemoryBIOProtocol
en el _tlsStack
al siguiente sucesivamente, de modo que cada capa de cifrado se elimine en el orden inverso al que se aplicó:
Representar _tlsStack
como una lista parece menos natural dado este nuevo requisito. Afortunadamente, representar el flujo de datos entrantes sugiere linealmente una nueva estructura de datos:
Tanto el error como el correcto flujo de datos entrantes se asemejan a una lista enlazada individualmente, con wrappedProtocol
sirviendo como los siguientes enlaces y protocol
como Client
. La lista debería crecer hacia abajo desde OnionProtocol
y siempre finalizará con OnionClient
. El error ocurre porque ese ordenante invariante es violado.
Una lista con un solo enlace está bien para presionar protocolos en la pila pero incómodo para hacerlos estallar, ya que requiere un desplazamiento hacia abajo desde su cabeza hasta el nodo para eliminar. Por supuesto, este cruce ocurre cada vez que se reciben datos, por lo que la preocupación es la complejidad implícita en una complejidad de tiempo transversal adicional en lugar de en el peor de los casos. Afortunadamente, la lista está doblemente vinculada:
El atributo de transport
vincula cada protocolo anidado con su predecesor, de modo que transport.write
puede aplicar niveles cada vez más bajos de cifrado antes de enviar finalmente los datos a través de la red. Tenemos dos centinelas para ayudar a administrar la lista: el Client
siempre debe estar en la parte superior y OnionClient
siempre debe estar en la parte inferior.
Juntando los dos, terminamos con esto:
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
(Esto también está disponible en GitHub ).
La solución al segundo problema radica en PopOnDisconnectTransport
. El código original intentó expulsar una sesión de TLS de la pila a través de connectionLost
, pero como solo un descriptor de archivo cerrado hace que se llame a connectionLost
, no eliminó las sesiones de TLS detenidas que no cerraron el socket subyacente.
En el momento de escribir esto, TLSMemoryBIOProtocol
llama a TLSMemoryBIOProtocol
su transporte en exactamente dos lugares: _shutdownTLS
y _tlsShutdownFinished
. _shutdownTLS
se _shutdownTLS
en los loseConnection
activos ( loseConnection
, abortConnection
, unregisterProducer
y después de loseConnection
y todas las escrituras pendientes se han descargado ), mientras que _tlsShutdownFinished
se _tlsShutdownFinished
pasivos ( fallas de handshake , lecturas vacías , errores de lectura y errores de escritura ). Esto significa que ambos lados de una conexión cerrada pueden hacer que las sesiones de TLS se detengan fuera de la pila durante la loseConnection
. PopOnDisconnectTransport
hace idempotentemente porque loseConnection
generalmente es idempotente, y TLSMemoryBIOProtocol
ciertamente espera que así sea.
La desventaja de poner lógica de administración de pila en loseConnection
es que depende de los detalles de la TLSMemoryBIOProtocol
de TLSMemoryBIOProtocol
. Una solución generalizada requeriría nuevas API en muchos niveles de Twisted.
Hasta entonces, estamos atrapados con otro ejemplo de la Ley de Hyrum .
Si está utilizando los mismos parámetros TLS para ambas capas y se está conectando al mismo host, entonces probablemente esté utilizando el mismo par de claves para ambas capas de cifrado. Intente utilizar un par de claves diferente para la capa anidada, como el túnel a un tercer host / puerto. es decir: localhost:30000
(cliente) -> localhost:8080
(TLS capa 1 usando el par de claves A) -> localhost:8081
(capa 2 de TLS usando el par de claves B).