Boost Asio Ssl Async_Shutdown Always Finishes with an Error

boost asio ssl async_shutdown always finishes with an error?

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.



The Protocol and Boost.Asio API

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.

Scenarios and Error Codes

Once an SSL connection has been established, the following error codes occur during shutdown:

  • One party initiates a shutdown and the remote party closes or has already closed the underlying transport without shutting down the protocol:
    • The initiator's shutdown() operation will fail with an SSL short read error.
  • One party initiates a shutdown and waits for the remote party to shutdown the protocol:
    • The initiator's shutdown operation will complete with an error value of boost::asio::error::eof.
    • The remote party's shutdown() operation completes with success.
  • One party initiates a shutdown then closes the underlying protocol without waiting for the remote party to shutdown the protocol:
    • The initiator's 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.
    • The remote party's 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.

PartyA invokes 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.
}

PartyA invokes 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.

PartyA initiates 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.

  • PartyA's shutdown operation will complete with an error value of boost::asio::error::eof.
  • PartyB's shutdown operation will complete with success.

PartyA initiates 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.

  • Although PartyA performed a shutdown procedure permitted by the SSL specification, the 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.
  • PartyB's shutdown operation will complete with success.

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.

Boost ASIO SSL handshake failure

Given that openssl s_client -connect my.url.com:993 -crlf -verify 1 succeeds there is not a lot that seems wrong. One thing catches my eye: I'd configure the context before constructing an SSL stream from it:

ssl::context ssl_context(ssl::context::tls);

ssl_context.set_default_verify_paths();

SSLSocket socket(context, ssl_context);

Also, openssl likely uses SNI extensions:

// Set SNI Hostname (many hosts need this to handshake successfully)
if(! SSL_set_tlsext_host_name(socket.native_handle(), hostname.c_str()))
{
throw boost::system::system_error(
::ERR_get_error(), boost::asio::error::get_ssl_category());
}

Finally, make sure the url string view contains correct data, notably that it's a valid hostname and null-terminated string. In this case I'd prefer to use a string representation that guarantees null-termination:

Summary

#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>

using boost::asio::ip::tcp;
namespace ssl = boost::asio::ssl;
using SSLSocket = ssl::stream<tcp::socket>;

int main() {
boost::asio::io_context context;
ssl::context ssl_context(ssl::context::tls);

ssl_context.set_default_verify_paths();

SSLSocket socket(context, ssl_context);

tcp::resolver r(context);
std::string hostname = "www.example.com";
auto endpoints = r.resolve(hostname, "443");
boost::asio::connect(socket.next_layer(), endpoints);
socket.set_verify_mode(ssl::verify_peer);
socket.set_verify_callback(ssl::host_name_verification(hostname));

// Set SNI Hostname (many hosts need this to handshake successfully)
if(! SSL_set_tlsext_host_name(socket.native_handle(), hostname.c_str()))
{
throw boost::system::system_error(
::ERR_get_error(), boost::asio::error::get_ssl_category());
}

socket.handshake(SSLSocket::client);
}

C++ Boost Asio SSL, data always encrypted?

socketSSL.next_layer() means the actual socket, bypassing the SSL decoder. socketSSL.next_layer().async_receive(...) means to receive some data on the actual socket, bypassing the SSL decoder.

To get decoded SSL data you use socketSSL instead of socketSSL.next_layer(). socketSSL.async_receive(...) doesn't work, but socketSSL.async_read_some(...) does, and the examples you read show boost::asio::async_read(socketSSL, ...) which also works.

boost::asio::ssl::context crash during construction

The openssl cygwin package I installed is not a stable one, so the include and lib files are missing and I'm using the wrong ones (incompatible with the x86_64-w64-mingw32-g++ compiler). I've installed another stable version and the desired files are available now.

Intermittent issues with SSL, using Boost/Asio

This issue was caused by bug in the Asio SSL implementation. Reported it to the Asio Github issue tracker.

When an error occur in OpenSSL, errors-codes are pushed onto a queue. A single call to OpenSSL may result in multiple error-codes being pushed to the queue. For example, a low-level component in OpenSSL may push one error to the queue, while another higher-level component may push another. (It could be seen as a stack of errors).

These queues are linked to the thread. In the Asio imlplementation for Windows, this is done using the thread-id (int). So every thread has its own queue containing errors.

After Asio has executed an SSL function, it only removes the first error from the queue. If more than one item have been pushed to the queue, the remaining items will be left behind. This has the effect that when Asio calls a SSL function later on, and checks the queue, it may pick up the error from a previous operation, and think that the current operation has faulted, even though it hasn't.

Sometimes when Asio calls OpenSSL, it clears the queue first. For example, the function add_certificate_authority starts of by executing ::ERR_clear_error(). It seems other people have had the same issue and claimed that this is the proper solution.But other functions in Asio, such as async_read do not do this.

My issue was found when running my test suite. The test suite contains a number of tests which tests error handling, for example by sending invalid data to the SSL server, by trying to use a private key with an invalid password and so on and here's where the fun starts:

My test suite triggers the code:

 context.set_password_callback(callbackfunc)
context.use_private_key_file(myfile, boost::asio::ssl::context::pem)

The callback func will return an incorrect password (to test that this is handled properly). use_private_key_file will call SSL_CTX_use_PrivateKey_file which pushes two errors to the queue. But Asios implementation of use_private_key_file only removes the first item. use_private_key_file properly reports an error to my application code.

My application code detects this and triggers a restart of the service-component (cleaning up, restarting threads, etc). The actual Windows-process will continue to run.

Some time later when i call async_read, that call may happen to be executed on a thread which has the same ID as the thread where I called use_private_key_file an hour ago. It's really different threads (original one has terminated), but they happen to have the same Thread-Id. After Asio calls SSL_read, it will check the error queue for the current thread id and retrieve the error which was previously reported in the call to use_private_key_file.

So that's how a async_read can succeed but still fail with a error referring to something completely different.



Related Topics



Leave a reply



Submit