What is the proper way to securely disconnect an asio SSL socket?
A boost-asio
SSL/TLS TCP socket is implemented as an ssl::stream
over a tcp::socket
:
boost::asio::ssl::stream<boost::asio::ip::tcp::socket> ssl_socket;
In the TLS protocol, a cryptographically secure shutdown involves parties exchanging close_notify
messages. Simply closing the lowest layer may make the session vulnerable to a truncation attack.
In boost asio ssl async_shutdown always finishes with an error? @Tanner Sansbury describes the SSL shutdown process in detail with a number of scenarios and proposes using an async_shutdown
followed by an async_write
to disconnect an SSL stream prior to closing the socket:
ssl_socket.async_shutdown(...);
const char buffer[] = "";
async_write(ssl_socket, buffer, [](...) { ssl_socket.close(); })
Performing an async_shutdown
on an ssl::stream
sends an SSL close_notify
message and waits for a response from the other end. The purpose of writing to the stream after the async_shutdown
is to be notified when async_shutdown
has sent the close_notify
so that the socket can be closed without waiting for the response. However, in the current (1.59) version of boost the call to async_write
fails...
In How to gracefully shutdown a boost asio ssl client? @maxschlepzig proposes shutting down receiver of the underlying TCP socket:
ssl_socket.lowest_layer()::shutdown(tcp::socket::shutdown_receive);
This produces a short read
error, and async_shutdown
is called when it's detected in the error handler:
// const boost::system::error_code &ec
if (ec.category() == asio::error::get_ssl_category() &&
ec.value() == ERR_PACK(ERR_LIB_SSL, 0, SSL_R_SHORT_READ))
{
// -> not a real error:
do_ssl_async_shutdown();
}
Or cancelling the read/write operations on the socket and then calling SSL async shutdown, i.e.:
boost::system::error_code ec;
ssl_socket.cancel(ec);
ssl_socket.async_shutdown([](...) { ssl_socket.close(); };
I'm currently using this last method since it works with the current version of boost
.
What is the correct/best way to securely disconnect a boost-asio
SSL socket?
Solution 1:
To securely disconnect, perform a shutdown operation and then close the underlying transport once shutdown has complete. Hence, the method you are currently using will perform a secure disconnect:
boost::system::error_code ec;
ssl_socket.cancel(ec);
ssl_socket.async_shutdown([](...) { ssl_socket.close(); };
Be aware that the current async_shutdown
operation will be considered complete when either:
- A
close_notify
has been received by the remote peer. - The remote peer closes the socket.
- The operation has been cancelled.
Hence, if resources are bound to the lifetime of the socket or connection, then these resources will remain alive waiting for the remote peer to take action or until the operation is cancelled locally. However, waiting for a close_notify
response is not required for a secure shutdown. If resources are bound to the connection, and locally the connection is considered dead upon sending a shutdown, then it may be worthwhile to not wait for the remote peer to take action:
ssl_socket.async_shutdown(...);
const char buffer[] = "";
async_write(ssl_socket, boost::asio::buffer(buffer),
[](...) { ssl_socket.close(); })
When a client sends a close_notify
message, the client guarantees that the client will not send additional data across the secure connection. In essence, the async_write()
is being used to detect when the client has sent a close_notify
, and within the completion handler, will close the underlying transport, causing the async_shutdown()
to complete with boost::asio::error::operation_aborted
. As noted in the linked answer, the async_write()
operation is expected to fail.
... 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 theasync_shutdown()
operation that is waiting for PartyB'sclose_notify
to be cancelled.