Fixing Boost.Asio and Beast TLS Handshake Error with api.steampowered.com
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 errororunrecognized_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_nameorboost::certify::sni_hostnamewhen 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 certificateindicates 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.