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 the async_shutdown() operation that is waiting for PartyB's close_notify to be cancelled.