Understanding the TLS Alert Internal Error in Boost.Asio & Beast

When connecting to modern web APIs like api.steampowered.com using Boost.Asio and Boost.Beast, you might encounter the following handshake error:

tlsv1 alert internal error (SSL routines, ssl3_read_bytes) [asio.ssl:336151608]

This error is particularly confusing because connections to other domains (like google.com) might work without issues. The primary reason for this failure is missing Server Name Indication (SNI) configuration and, in many cases, unconfigured certificate verification.

Why Does This Error Happen?

Large web services and content delivery networks (CDNs)—such as Akamai or Cloudflare, which Steam uses to host its APIs—serve thousands of domains from the same IP address. During the initial TLS handshake, the server needs to know which hostname the client wants to reach before sending its SSL certificate.

  • Missing SNI: If you don't explicitly configure SNI, your client connects without specifying the target hostname at the TLS layer. Many modern CDNs will immediately terminate the connection and return a TLS alert (internal error or unrecognized_name).
  • Missing Root Certificates: Boost.Asio's SSL context does not automatically load the operating system's default certificate store unless configured.

Solution 1: Setting SNI with Native OpenSSL

To enable SNI in Boost.Beast using standard OpenSSL calls, use SSL_set_tlsext_host_name on the native handle before initiating the async handshake:

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

boost::asio::awaitable<void> ssl_handshake_sni()
{
    namespace beast = boost::beast;
    namespace asio = boost::asio;
    namespace ssl = asio::ssl;

    ssl::context ctx{ssl::context::tls_client};
    ctx.set_default_verify_paths(); // Load system root certificates
    ctx.set_verify_mode(ssl::verify_peer);

    auto executor = co_await asio::this_coro::executor;
    beast::ssl_stream<beast::tcp_stream> connection{executor, ctx};

    std::string const host = "api.steampowered.com";
    std::string const port = "443";

    // 1. Set SNI hostname (Crucial for Steam API / CDN routing)
    if (!SSL_set_tlsext_host_name(connection.native_handle(), host.c_str()))
    {
        beast::error_code ec{static_cast<int>(::ERR_get_error()), asio::error::get_ssl_category()};
        throw beast::system_error{ec, "Failed to set SNI hostname"};
    }

    // 2. Resolve and connect TCP stream
    asio::ip::tcp::resolver resolver{executor};
    auto const results = co_await resolver.async_resolve(host, port, asio::use_awaitable);
    co_await beast::get_lowest_layer(connection).async_connect(results, asio::use_awaitable);

    // 3. Perform TLS Handshake
    co_await connection.async_handshake(ssl::stream_base::client, asio::use_awaitable);
    
    std::println("TLS Handshake succeeded with {}", host);
}

Solution 2: Using Boost.Certify

Boost.Certify is a popular library designed to streamline OS-native certificate verification and SNI handling for Boost.Asio applications across Windows, macOS, and Linux.

Here is how you can perform a robust handshake using Boost.Certify:

#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <boost/beast.hpp>
#include <boost/beast/ssl.hpp>
#include <boost/certify/extensions.hpp>
#include <boost/certify/https_verification.hpp>
#include <print>

boost::asio::awaitable<void> ssl_handshake_certify()
{
    namespace beast = boost::beast;
    namespace asio = boost::asio;
    namespace ssl = asio::ssl;

    ssl::context ctx{ssl::context::tls_client};
    
    // Use native OS certificate verification
    ctx.set_verify_mode(ssl::verify_peer | ssl::verify_fail_if_no_peer_cert);
    ctx.set_default_verify_paths();

    auto executor = co_await asio::this_coro::executor;
    beast::ssl_stream<beast::tcp_stream> connection{executor, ctx};

    std::string const host = "api.steampowered.com";
    std::string const port = "443";

    // Boost.Certify helpers for SNI and Hostname Verification
    boost::certify::set_server_hostname(connection, host);
    boost::certify::sni_hostname(connection, host);

    // Resolve and connect
    asio::ip::tcp::resolver resolver{executor};
    auto const results = co_await resolver.async_resolve(host, port, asio::use_awaitable);
    co_await beast::get_lowest_layer(connection).async_connect(results, asio::use_awaitable);

    // TLS Handshake
    co_await connection.async_handshake(ssl::stream_base::client, asio::use_awaitable);

    std::println("TLS Handshake with Boost.Certify completed successfully!");
}

Key Takeaways

  • Always configure SNI using SSL_set_tlsext_host_name or boost::certify::sni_hostname when connecting to HTTPS endpoints.
  • Always enable peer verification (ssl::verify_peer) and load system trust roots (ctx.set_default_verify_paths()) to ensure secure connections.
  • The OpenSSL diagnostic output showing unable to get local issuer certificate indicates that the command-line client did not have the Let's Encrypt / ISRG root CA specified, but the primary handshake failure in your code was caused by missing SNI.