c++ - boost asio ssl async_shutdown siempre termina con un error?
openssl boost-asio (1)
Tengo un pequeño cliente ssl que he programado en boost 1.55 asio, y estoy tratando de descubrir por qué
boost::asio::ssl::stream::async_shutdown()
siempre falla.
El cliente es muy similar (casi idéntico) a los ejemplos de cliente ssl en la documentación de impulso, ya que pasa por un
boost::asio::ip::tcp::resolver::async_resolve()
->
boost::asio::ssl::stream::async_connect()
->
boost::asio::ssl::stream::async_handshake()
secuencia de devolución de llamada.
Todo esto funciona como se esperaba y la devolución de llamada
async_handshake()
obtiene un
boost::system::error_code
claro
boost::system::error_code
.
Desde la devolución de llamada
async_shutdown()
, llamo
async_shutdown()
(no transfiero ningún dato; este objeto es más para probar el apretón de manos):
void ClientCertificateFinder::handle_handshake(const boost::system::error_code& e)
{
if ( !e )
{
m_socket.async_shutdown( boost::bind( &ClientCertificateFinder::handle_shutdown_after_success,
this,
boost::asio::placeholders::error ) );
}
else
{
m_handler( e, IssuerNameList() );
}
}
handle_shutdown_after_success()
, pero siempre con un error?
El error es value = 2 en
asio.misc
, que es ''Fin del archivo''.
He intentado esto con una variedad de servidores SSL, y siempre me
asio.misc
este error
asio.misc
.
Que esto no es un error de OpenSL subyacente me sugiere que podría estar haciendo un mal uso de Asio de alguna manera ...
Alguien sabe por qué esto podría estar sucediendo?
Tenía la impresión de que cerrar la conexión con
async_shutdown()
era lo correcto, pero creo que podría llamar a
boost::asio::ssl::stream.lowestlayer().close()
para cerrar el socket out from under openssl si esa es la forma esperada de hacer esto (y de hecho los ejemplos de asio ssl parecen indicar que esta es la forma correcta de cerrar).
Para un apagado criptográficamente seguro, ambas partes deben ejecutar operaciones de apagado en
boost::asio::ssl::stream
invocando
shutdown()
o
async_shutdown()
y ejecutando
io_service
.
Si la operación se completa con un
error_code
que no tiene una
categoría SSL
y no se canceló antes de que pudiera ocurrir parte del apagado, entonces la conexión se cerró de forma segura y el transporte subyacente puede reutilizarse o cerrarse.
Simplemente cerrar la capa más baja puede hacer que la sesión sea vulnerable a un
ataque de truncamiento
.
El protocolo y la API Boost.Asio
En el protocolo
TLS
estandarizado y el protocolo
SSLv3
no estandarizado, un cierre seguro implica que las partes intercambien mensajes
close_notify
.
En términos de la API Boost.Asio, cualquiera de las partes puede iniciar un cierre invocando
shutdown()
o
async_shutdown()
, haciendo que se
close_notify
un mensaje
close_notify
a la otra parte, informando al destinatario que el iniciador no enviará más mensajes en el Conexión SSL
Según la especificación, el destinatario debe responder con un mensaje
close_notify
.
Boost.Asio no realiza automáticamente este comportamiento y requiere que el destinatario invoque explícitamente
shutdown()
o
async_shutdown()
.
La especificación permite que el iniciador del apagado cierre su lado de lectura de la conexión antes de recibir la respuesta
close_notify
.
Esto se usa en casos en los que el protocolo de aplicación no desea reutilizar el protocolo subyacente.
Desafortunadamente, Boost.Asio no proporciona actualmente (1.56) soporte directo para esta capacidad.
En Boost.Asio, la operación
shutdown()
se considera completa en caso de error o si la parte ha enviado y recibido un mensaje
close_notify
.
Una vez que la operación se ha completado, la aplicación puede reutilizar el protocolo subyacente o cerrarlo.
Escenarios y códigos de error
Una vez que se ha establecido una conexión SSL, se producen los siguientes códigos de error durante el apagado:
-
Una parte inicia un cierre y la parte remota cierra o ya ha cerrado el transporte subyacente sin cerrar el protocolo:
-
La operación
shutdown()
del iniciador fallará con un error de lectura corta SSL.
-
La operación
-
Una de las partes inicia un apagado y espera a que la parte remota cierre el protocolo:
-
La operación de apagado del iniciador se completará con un valor de error de
boost::asio::error::eof
. -
La operación
shutdown()
la parte remota se completa con éxito.
-
La operación de apagado del iniciador se completará con un valor de error de
-
Una parte inicia un apagado y luego cierra el protocolo subyacente sin esperar a que la parte remota cierre el protocolo:
-
La operación
shutdown()
del iniciador se cancelará, dando como resultado un error deboost::asio::error::operation_aborted
. Este es el resultado de una solución alternativa indicada en los detalles a continuación. -
La operación
shutdown()
la parte remota se completa con éxito.
-
La operación
Estos diversos escenarios se capturan en detalle a continuación. Cada escenario se ilustra con un diagrama similar a una línea de natación, que indica lo que cada parte está haciendo exactamente en el mismo momento.
PartyA
invoca
shutdown()
después de que
PartyB
cierra la conexión sin negociar el apagado.
En este escenario,
PartyB
viola el procedimiento de apagado al cerrar el transporte subyacente sin invocar primero
shutdown()
en la transmisión.
Una vez que se ha cerrado el transporte subyacente, el
PartyA
intenta iniciar un
shutdown()
.
PartyA | PartyB
-------------------------------------+----------------------------------------
ssl_stream.handshake(...); | ssl_stream.handshake(...);
... | ssl_stream.lowest_layer().close();
ssl_stream.shutdown(); |
PartyA
intentará enviar un mensaje
close_notify
, pero la escritura en el transporte subyacente fallará con
boost::asio::error::eof
.
Boost.Asio asignará
explícitamente
el error de
eof
del transporte subyacente a un error de lectura corta SSL, ya que
PartyB
violó el procedimiento de apagado de SSL.
if ((error.category() == boost::asio::error::get_ssl_category())
&& (ERR_GET_REASON(error.value()) == SSL_R_SHORT_READ))
{
// Remote peer failed to send a close_notify message.
}
PartyA
invoca
shutdown()
luego
PartyB
cierra la conexión sin negociar el apagado.
En este escenario,
PartyA
inicia un cierre.
Sin embargo, mientras
PartyB
recibe el mensaje
close_notify
,
PartyB
viola el procedimiento de apagado al no responder explícitamente con
shutdown()
antes de cerrar el transporte subyacente.
PartyA | PartyB
-------------------------------------+---------------------------------------
ssl_stream.handshake(...); | ssl_stream.handshake(...);
ssl_stream.shutdown(); | ...
| ssl_stream.lowest_layer().close();
Como la operación
shutdown()
Boost.Asio se considera completa una vez que se ha enviado y recibido una
notificación close_notify
o se produce un error,
PartyA
enviará una
close_notify
luego esperará una respuesta.
PartyB
cierra el transporte subyacente sin enviar un
close_notify
, violando el protocolo SSL.
La lectura de
PartyA
fallará con
boost::asio::error::eof
, y Boost.Asio lo asignará a un error de lectura corta SSL.
PartyA
inicia
shutdown()
y espera a que
PartyB
responda con
shutdown()
.
En este escenario, PartyA iniciará un cierre y esperará a que PartyB responda con un cierre.
PartyA | PartyB
-------------------------------------+----------------------------------------
ssl_stream.handshake(...); | ssl_stream.handshake(...);
ssl_stream.shutdown(); | ...
... | ssl_stream.shutdown();
Este es un cierre bastante básico, donde ambas partes envían y reciben un mensaje
close_notify
.
Una vez que el cierre ha sido negociado por ambas partes, el transporte subyacente puede reutilizarse o cerrarse.
-
La operación de apagado de
PartyA
se completará con un valor de error de
boost::asio::error::eof
. - La operación de apagado de PartyB se completará con éxito.
PartyA
inicia
shutdown()
pero no espera a que
PartyB
responda.
En este escenario,
PartyA
iniciará un apagado y luego cerrará inmediatamente el transporte subyacente una vez que se haya enviado
close_notify
.
PartyA
no espera a que
PartyB
responda con un mensaje
close_notify
.
Este tipo de cierre negociado está permitido según la especificación y es bastante común entre las implementaciones.
Como se mencionó anteriormente, Boost.Asio no admite directamente este tipo de apagado.
La operación
shutdown()
Boost.Asio esperará a que el par remoto envíe su
close_notify
.
Sin embargo, es posible implementar una solución mientras se mantiene la especificación.
PartyA | PartyB
-------------------------------------+---------------------------------------
ssl_stream.handshake(...); | ssl_stream.handshake(...)
ssl_stream.async_shutdown(...); | ...
const char buffer[] = ""; | ...
async_write(ssl_stream, buffer, | ...
[](...) { ssl_stream.close(); }) | ...
io_service.run(); | ...
... | ssl_stream.shutdown();
PartyA
iniciará una operación de apagado asincrónico y luego iniciará una operación de escritura asincrónica.
El búfer utilizado para la escritura debe ser de longitud distinta de cero (el carácter nulo se usa arriba);
de lo contrario, Boost.Asio optimizará la escritura en un no-op.
Cuando se ejecuta la operación
shutdown()
, enviará
close_notify
a
PartyB
, lo que
hará
que SSL cierre el lado de
escritura
de la
transmisión
SSL de
PartyA
y luego asincrónicamente espere a
cerrar
la
notificación
de
close_notify
.
Sin embargo, como el lado de
escritura
del flujo SSL de
PartyA se
ha cerrado, la operación
async_write()
fallará con un error SSL que indica que el protocolo se ha cerrado.
if ((error.category() == boost::asio::error::get_ssl_category())
&& (SSL_R_PROTOCOL_IS_SHUTDOWN == ERR_GET_REASON(error.value())))
{
ssl_stream.lowest_layer().close();
}
La operación
async_write()
fallida
async_write()
explícitamente el transporte subyacente, causando la operación
async_shutdown()
que está esperando que se
cancele
el
close_notify
de
close_notify
.
-
Aunque
PartyA
realizó un procedimiento de apagado permitido por la especificación SSL, la operación
shutdown()
se canceló explícitamente cuando se cerró el transporte subyacente. Por lo tanto, el código de error de la operaciónshutdown()
tendrá un valor deboost::asio::error::operation_aborted
. - La operación de apagado de PartyB se completará con éxito.
En resumen, las operaciones de apagado SSL de Boost.Asio son un poco complicadas. Las inconsistencias entre el iniciador y los códigos de error de los pares remotos durante los apagados adecuados pueden hacer que el manejo sea un poco incómodo. Como regla general, siempre que la categoría del código de error no sea una categoría SSL, el protocolo se cerró de forma segura.