11

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?

Community
  • 1
  • 1
kenba
  • 4,303
  • 1
  • 23
  • 40

2 Answers2

7

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.

Community
  • 1
  • 1
Tanner Sansbury
  • 51,153
  • 9
  • 112
  • 169
  • 1
    Thank you @Tanner, I totally agree that once a client has sent a `close notify` it shouldn't have to wait for a response and I *was* using an `async_shutdown` followed by an `async_write` as you've described here and in your (excellent) [linked answer](http://stackoverflow.com/questions/25587403/boost-asio-ssl-async-shutdown-always-finishes-with-an-error/25703699#25703699) Unfortunately, using the boost 1.59, the call to `async_write` now fails by crashing in `engine.ipp` instead of calling the callback with `SSL_R_PROTOCOL_IS_SHUTDOWN`! So how best disconnect the SSL socket and not crash? – kenba Aug 17 '15 at 16:52
  • 1
    @kenba I will try to find some time to investigate it. Is the application guaranteeing that all asynchronous operations are being performed within the same implicit or explicit strand? Often times, crashes with Boost.Asio SSL are the result of violating concurrency requirement. – Tanner Sansbury Aug 17 '15 at 17:13
  • Thank you @Tanner it would be great if you could find the cause. The application in question is single threaded so there are no concurrency issues. – kenba Aug 17 '15 at 18:02
  • Please accept my sincerest apologies @TannerSansbury. I fully support you in your plans for a PR to change SSL shutdown to not wait for the peer's `close_notify`. However, please be aware that [Chris Kohlhoff](https://github.com/chriskohlhoff) hasn't been active on his GitHub account for over 3 months. I don't know whether he's busy preparing for including `asio` in [C++17](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4370.html) or what? I just hope he's OK. For what its' worth, my respect for you is now greater than ever. – kenba Sep 23 '15 at 16:18
  • Calling both `async_shutdown` and `async_write` at once is problematic in some use-cases. For example when implementing a composed operation. There will be two pending completions, which can in theory complete in any order. There will be two error codes to deal with, etc... Could the same result be achieved by simply calling `async_write` in the completion handler for `async_shutdown`? This way, there is only ever one pending completion. – Vinnie Falco Apr 12 '16 at 20:14
  • 5
    @Vinnie The intention of initiating an `async_write()` with an outstanding `async_shutdown()` is to detect when the local write-side of the stream has shutdown, allowing one to free resources without having to wait for the remote peer to shutdown their side of the ssl stream. If one waits for the `async_shutdown` to complete, then `async_write` would no longer be necessary. – Tanner Sansbury Apr 13 '16 at 00:44
  • 4
    I hit the problem mentioned by **kenba** where there was a crash in engine.ipp, it was only occurred when multiple threads were running `ioservice::run`. It also required 32 threads to hit it. As **Tanner Sansbury** indicated it was due to violating the SSL concurrency issue. The fix was to strand the shutdown and the write: ``ssl_socket.async_shutdown(strand_.wrap(..) ); const char buffer[] = ""; async_write(ssl_socket_, boost::asio::buffer(buffer), strand_.wrap(....));`` - hope this helps someone – Benjamin Close Dec 19 '16 at 01:48
1

I'm probably late to answer this but I want to report my experience. This solution so far (using boost 1.78) did not produce any visible error on the client nor the server:

// sock type is boost::asio::ssl::stream<boost::asio::ip::tcp::socket>

sock->shutdown(ec);       

sock->lowest_layer().shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);

sock->lowest_layer().cancel(ec);

sock->lowest_layer().close();

Sandbox server with: openssl s_server -cert server.crt -key server.key -4 -debug

With this solution the server gets this after the sock->shutdown(ec).

read from 0x55e5dff8c960 [0x55e5dff810f8] (19 bytes => 19 (0x13))
0000 - 44 bc 11 5b a9 b4 ee 51-48 e0 18 f7 99 a7 a8 a9   D..[...QH.......
0010 - 21 1a 60                                          !.`
DONE
shutting down SSL
CONNECTION CLOSED

Before I was using this code (used for both plain TCP and ssl socket)

sock->lowest_layer().shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);

sock->lowest_layer().cancel(ec);

sock->lowest_layer().close();

The old code, when leveraging ssl socket, produced this error on the server:

read from 0x55eb3d40b430 [0x55eb3d423513] (5 bytes => 0 (0x0))
ERROR
shutting down SSL
CONNECTION CLOSED

As mentioned before, to avoid this behavior a close_notify should be sent out by the client using ssl::stream::async_shutdown or ssl::stream::shutdown

The trick of async_write() could be useful in case you want to leverage the async_shutdown() function instead of the synchronous shutdown()

Matt
  • 721
  • 9
  • 26