I have a small ssl client that I've programmed in boost 1.55 asio, and I'm trying to figure out why boost::asio::ssl::stream::async_shutdown()
always fails. The client is very similar (almost identical) to the ssl client examples in the boost documentation, in that it goes through an boost::asio::ip::tcp::resolver::async_resolve()
-> boost::asio::ssl::stream::async_connect()
-> boost::asio::ssl::stream::async_handshake()
callback sequence. All of this works as expected and the async_handshake()
callback gets an all-clear boost::system::error_code
.
From the async_handshake()
callback, I call async_shutdown()
(I don't transfer any data - this object is more for testing the handshake):
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()
is then called, but always with an error? The error is value=2 in asio.misc
, which is 'End of file'. I've tried this with a variety of ssl servers, and I always seem to get this asio.misc
error. That this isn't an underlying openssl error suggests to me that I might be misusing asio in some way...?
Anyone know why this might be happening? I was under the impression that shutting down the connection with async_shutdown()
was The Right Thing To Do, but I guess I could just call boost::asio::ssl::stream.lowestlayer().close()
to close the socket out from under openssl if that's the expected way to do this (and indeed the asio ssl examples seem to indicate that this is the right way of shutting down).
For a cryptographically secure shutdown, both parties musts execute shutdown operations on the boost::asio::ssl::stream
by either invoking shutdown()
or async_shutdown()
and running the io_service
. If the operation completes with an error_code
that does not have an SSL category and was not cancelled before part of the shutdown could occur, then the connection was securely shutdown and the underlying transport may be reused or closed. Simply closing the lowest layer may make the session vulnerable to a truncation attack.
In the standardized TLS protocol and the non-standardized SSLv3 protocol, a secure shutdown involves parties exchanging close_notify
messages. In terms of the Boost.Asio API, either party may initiate a shutdown by invoking shutdown()
or async_shutdown()
, causing a close_notify
message to be sent to the other party, informing the recipient that the initiator will not send more messages on the SSL connection. Per the specification, the recipient must respond with a close_notify
message. Boost.Asio does not automatically perform this behavior, and requires the recipient to explicitly invoke shutdown()
or async_shutdown()
.
The specification permits the initiator of the shutdown to close their read side of the connection before receiving the close_notify
response. This is used in cases where the application protocol does not wish to reuse the underlying protocol. Unfortunately, Boost.Asio does not currently (1.56) provide direct support for this capability. In Boost.Asio, the shutdown()
operation is considered complete upon error or if the party has sent and received a close_notify
message. Once the operation has completed, the application may reuse the underlying protocol or close it.
Once an SSL connection has been established, the following error codes occur during shutdown:
shutdown()
operation will fail with an SSL short read error.boost::asio::error::eof
.shutdown()
operation completes with success.shutdown()
operation will be cancelled, resulting in an error of boost::asio::error::operation_aborted
. This is the result of a workaround noted in the details below.shutdown()
operation completes with success.These various scenarios are captured in detailed below. Each scenario is illustrated with a swim-line like diagram, indicating what each party is doing at the exact same point in time.
shutdown()
after PartyB closes connection without negotiating shutdown.In this scenario, PartyB violates the shutdown procedure by closing the underlying transport without first invoking shutdown()
on the stream. Once the underlying transport has been closed, the PartyA attempts to initiate a shutdown()
.
PartyA | PartyB
-------------------------------------+----------------------------------------
ssl_stream.handshake(...); | ssl_stream.handshake(...);
... | ssl_stream.lowest_layer().close();
ssl_stream.shutdown(); |
PartyA will attempt to send a close_notify
message, but the write to the underlying transport will fail with boost::asio::error::eof
. Boost.Asio will explicitly map the underlying transport's eof
error to an SSL short read error, as PartyB violated the SSL shutdown procedure.
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.
}
shutdown()
then PartyB closes connection without negotiating shutdown.In this scenario, PartyA initiates a shutdown. However, while PartyB receives the close_notify
message, PartyB violates the shutdown procedure by never explicitly responding with a shutdown()
before closing the underlying transport.
PartyA | PartyB
-------------------------------------+---------------------------------------
ssl_stream.handshake(...); | ssl_stream.handshake(...);
ssl_stream.shutdown(); | ...
| ssl_stream.lowest_layer().close();
As Boost.Asio's shutdown()
operation is considered complete once a close_notify
has been both sent and received or an error occurs, PartyA will send a close_notify
then wait for a response. PartyB closes the underlying transport without sending a close_notify
, violating the SSL protocol. PartyA's read will fail with boost::asio::error::eof
, and Boost.Asio will map it to an SSL short read error.
shutdown()
and waits for PartyB to respond with a shutdown()
.In this scenario, PartyA will initiate a shutdown and wait for PartyB to respond with a shutdown.
PartyA | PartyB
-------------------------------------+----------------------------------------
ssl_stream.handshake(...); | ssl_stream.handshake(...);
ssl_stream.shutdown(); | ...
... | ssl_stream.shutdown();
This is a fairly basic shutdown, where both parties send and receive a close_notify
message. Once the shutdown has been negotiated by both parties, the underlying transport may either be reused or closed.
boost::asio::error::eof
.shutdown()
but does not wait for PartyB to responsd.In this scenario, PartyA will initiate a shutdown and then immediately close the underlying transport once close_notify
has been sent. PartyA does not wait for PartyB to respond with a close_notify
message. This type of negotiated shutdown is allowed per the specification and fairly common amongst implementations.
As mentioned above, Boost.Asio does not directly support this type of shutdown. Boost.Asio's shutdown()
operation will wait for the remote peer to send its close_notify
. However, it is possible to implement a workaround while still upholding the specification.
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 will initiate an asynchronous shutdown operation and then initiate an asynchronous write operation. The buffer used for the write must be of a non-zero length (null character is used above); otherwise, Boost.Asio will optimize the write to a no-op. When the shutdown()
operation runs, it will send close_notify
to PartyB, causing SSL to close the write side of PartyA's SSL stream, and then asynchronously wait for PartyB's close_notify
. However, as the write side of PartyA's SSL stream has closed, the async_write()
operation will fail with an SSL error indicating the protocol has been shutdown.
if ((error.category() == boost::asio::error::get_ssl_category())
&& (SSL_R_PROTOCOL_IS_SHUTDOWN == ERR_GET_REASON(error.value())))
{
ssl_stream.lowest_layer().close();
}
The failed async_write()
operation will then explicitly close the underlying transport, causing the async_shutdown()
operation that is waiting for PartyB's close_notify
to be cancelled.
shutdown()
operation was explicitly cancelled when underlying transport was closed. Hence, the shutdown()
operation's error code will have a value of boost::asio::error::operation_aborted
.In summary, Boost.Asio's SSL shutdown operations are a bit tricky. The inconstancies between the initiator and remote peer's error codes during proper shutdowns can make handling a bit awkward. As a general rule, as long as the error code's category is not an SSL category, then the protocol was securely shutdown.