TLS Encryption

Corosio provides TLS encryption through the tls_context configuration class and stream wrappers that add encryption to existing connections. This chapter covers context configuration, stream usage, and common TLS patterns.

Code snippets assume:

#include <boost/corosio/tls_context.hpp>
#include <boost/corosio/wolfssl_stream.hpp>
#include <boost/corosio/tcp_socket.hpp>
#include <boost/capy/read.hpp>
#include <boost/capy/write.hpp>

namespace corosio = boost::corosio;
namespace capy = boost::capy;
using namespace boost::corosio;

Overview

TLS (Transport Layer Security) encrypts data on TCP connections, providing confidentiality, integrity, and authentication. Corosio supports TLS through:

  • tls_context — Portable configuration for certificates, keys, and options

  • tls_stream — Abstract base class adding handshake and shutdown

  • wolfssl_stream — TLS implementation using WolfSSL

  • openssl_stream — TLS implementation using OpenSSL

A verified-safe client trusts the system CAs, requires a peer certificate, and checks the hostname:

tls_context ctx;
ctx.set_default_verify_paths();              // trust the system CAs
ctx.set_verify_mode(tls_verify_mode::peer);  // require + verify the peer
ctx.set_hostname("example.com");             // SNI + hostname check

Trust anchors can also be supplied explicitly with add_certificate_authority(), load_verify_file(), or add_verify_path(), and set_verify_callback() installs a custom verification hook.

Protocol version bounds (set_min_protocol_version() / set_max_protocol_version()), cipher suites (set_ciphersuites() and set_ciphersuites_tls13()), ALPN (set_alpn(), read back with alpn_protocol()), PKCS#12 credentials (use_pkcs12() / use_pkcs12_file()), and CRL-based revocation (add_crl() / add_crl_file() with set_revocation_policy()) are all supported.

Two implemented features depend on how WolfSSL was built. set_default_verify_paths() requires WOLFSSL_SYS_CA_CERTS to load the system store; without it, supply trust anchors explicitly. set_verify_callback() requires WOLFSSL_ALWAYS_VERIFY_CB — without it, installing a callback fails the handshake with std::errc::function_not_supported rather than failing open (see below). Both are enabled by --enable-opensslextra.

The typical flow:

// 1. Configure a context
tls_context ctx;
ctx.set_hostname("api.example.com");
if (auto ec = ctx.set_default_verify_paths(); ec)
    throw std::system_error(ec);
if (auto ec = ctx.set_verify_mode(tls_verify_mode::peer); ec)
    throw std::system_error(ec);

// 2. Connect a socket
corosio::tcp_socket sock(ioc);
sock.open();
if (auto [ec] = co_await sock.connect(endpoint); ec)
    throw std::system_error(ec);

// 3. Wrap the connected socket (pointer form; does not take ownership)
corosio::wolfssl_stream secure(&sock, ctx);
if (auto [ec] = co_await secure.handshake(wolfssl_stream::client); ec)
    throw std::system_error(ec);

// 4. Use encrypted I/O
auto [ec, n] = co_await secure.read_some(buffer);

tls_context

The tls_context class stores TLS configuration: certificates, keys, trust anchors, protocol settings, and verification options. Contexts are shared handles—copies share the same underlying state.

Don’t modify a context after creating streams from it. The configuration is captured when the first stream is constructed.

Creating a Context

// Default context (TLS 1.2+ enabled)
tls_context ctx;

The default context has no certificates loaded and doesn’t verify peers. You’ll typically configure it before use.

Credential Loading

For servers (and clients using mutual TLS), load your certificate and private key.

Loading Certificates

// From file
ctx.use_certificate_file("server.crt", tls_file_format::pem);

// From memory
std::string cert_data = /* ... */;
ctx.use_certificate(cert_data, tls_file_format::pem);

// Certificate chain (cert + intermediates)
ctx.use_certificate_chain_file("fullchain.pem");

Loading Private Keys

// From file
ctx.use_private_key_file("server.key", tls_file_format::pem);

// From memory
ctx.use_private_key(key_data, tls_file_format::pem);

For encrypted private keys, set a password callback first:

ctx.set_password_callback(
    [](std::size_t max_len, tls_password_purpose purpose) {
        return std::string("my-key-password");
    });
ctx.use_private_key_file("encrypted.key", tls_file_format::pem);

PKCS#12 Bundles

PKCS#12 (.pfx or .p12) files bundle certificate, key, and chain together:

ctx.use_pkcs12_file("credentials.pfx", "bundle-password");
The bundle is decoded when the first stream is created; a malformed bundle or wrong passphrase surfaces as a handshake failure. Intermediate certificates in the bundle are loaded and sent during the handshake on both backends.

Trust Anchors

Configure which Certificate Authorities (CAs) to trust for peer verification.

System Trust Store

Use the operating system’s default CA certificates:

ctx.set_default_verify_paths();

On OpenSSL the SSL_CERT_FILE and SSL_CERT_DIR environment variables are honored; the WolfSSL backend requires a build with WOLFSSL_SYS_CA_CERTS, without which this call has no effect and a CA bundle must be supplied explicitly via load_verify_file() or add_certificate_authority().

Custom CA Certificates

// Single CA from memory
ctx.add_certificate_authority(ca_pem);

// CA file (may contain multiple certs)
ctx.load_verify_file("/etc/ssl/certs/ca-certificates.crt");

// Directory of hashed CA files
ctx.add_verify_path("/etc/ssl/certs");
With OpenSSL, load_verify_file() registers only the first certificate from a multi-cert bundle (WolfSSL handles multi-cert files); add others with repeated add_certificate_authority() calls. For add_verify_path(), OpenSSL requires the hashed-filename layout produced by openssl rehash, while WolfSSL loads every file in the directory.

Protocol Configuration

TLS Version

// Require TLS 1.3 minimum
ctx.set_min_protocol_version(tls_version::tls_1_3);

// Cap at TLS 1.2 (unusual, but possible)
ctx.set_max_protocol_version(tls_version::tls_1_2);
On WolfSSL the ceiling is applied by selecting a version-specific method (there is no native set-max call); a window whose minimum exceeds its maximum yields a context that fails the handshake.

Available versions:

Version Description

tls_version::tls_1_2

TLS 1.2 (RFC 5246)

tls_version::tls_1_3

TLS 1.3 (RFC 8446)

Cipher Suites

// TLS 1.2-and-below cipher list (OpenSSL syntax)
ctx.set_ciphersuites("ECDHE+AESGCM:ECDHE+CHACHA20");
// TLS 1.3 cipher suites (configured separately)
ctx.set_ciphersuites_tls13("TLS_AES_256_GCM_SHA384");
set_ciphersuites() covers TLS 1.2 and below; TLS 1.3 suites use set_ciphersuites_tls13(). Both backends apply them (WolfSSL merges the two into one list). Suite names differ between backends (OpenSSL TLS_AES_128_GCM_SHA256 vs WolfSSL TLS13-AES128-GCM-SHA256). The OpenSSL security level is left at the library default; express a lower level explicitly with a @SECLEVEL= token if required.

ALPN

Application-Layer Protocol Negotiation selects the application protocol. Read the negotiated protocol after the handshake with stream.alpn_protocol():

// Prefer HTTP/2, fall back to HTTP/1.1
ctx.set_alpn({"h2", "http/1.1"});
// ... after handshake:
std::string_view proto = stream.alpn_protocol(); // e.g. "h2", or empty
On WolfSSL, ALPN requires a HAVE_ALPN build; without it, offering protocols fails the handshake with std::errc::function_not_supported rather than negotiate nothing silently.

Certificate Verification

Verification Mode

// Don't verify peer (not recommended for clients)
ctx.set_verify_mode(tls_verify_mode::none);

// Verify if peer presents certificate
ctx.set_verify_mode(tls_verify_mode::peer);

// Require peer certificate (fail if not presented)
ctx.set_verify_mode(tls_verify_mode::require_peer);

For HTTPS clients, use peer. For servers requiring client certificates (mutual TLS), use require_peer.

Hostname Verification (SNI)

For clients, set the expected server hostname:

ctx.set_hostname("api.example.com");

This does two things:

  1. Sends Server Name Indication (SNI) so the server knows which certificate to present (important for virtual hosting)

  2. Verifies the server certificate matches this hostname

Verification Depth

Limit the certificate chain depth:

ctx.set_verify_depth(10); // Max 10 intermediate certs

Custom Verification Callback

For advanced verification logic, install a callback. It runs during the handshake; returning true accepts the certificate and false rejects it. Inspect the certificate portably with certificate(), which returns its DER bytes — ideal for certificate pinning. (native_handle() also exposes the backend’s X509_STORE_CTX* for backend-specific use.)

ctx.set_verify_callback(
    [](bool preverified, corosio::verify_context& ctx) {
        if (!preverified)
            return false;              // reject if the chain didn't verify

        auto der = ctx.certificate();  // DER of the current certificate
        return der.size() == expected_pin.size() &&
            std::equal(der.begin(), der.end(), expected_pin.begin());
    });

Which certificates the callback sees depends on the backend:

  • OpenSSL — invoked for every certificate in the chain, including ones that passed, so it can both relax (accept a rejected cert) and tighten (reject an otherwise-valid cert, e.g. pinning) verification.

  • WolfSSL built with WOLFSSL_ALWAYS_VERIFY_CB (via --enable-opensslextra) — same as OpenSSL.

  • WolfSSL without that option — the library invokes the callback only on failure, so it cannot be honored on a successful handshake. To avoid silently ignoring a tightening callback (which would fail open), a context that carries a callback instead fails the handshake with std::errc::function_not_supported. Rebuild WolfSSL with WOLFSSL_ALWAYS_VERIFY_CB, or omit the callback.

Revocation Checking

Revocation is checked against Certificate Revocation Lists (CRLs). CRLs are consulted only when a revocation policy other than disabled is set.

// Load a CRL (PEM or DER), from file or memory
ctx.add_crl_file("issuer.crl");
ctx.add_crl(crl_data);

// Choose how strict to be
ctx.set_revocation_policy(tls_revocation_policy::hard_fail);

Under soft_fail, a certificate whose status cannot be determined (missing or expired CRL) is accepted, but one that is actually listed as revoked is rejected. Under hard_fail, an undeterminable status is also rejected. disabled (the default) skips revocation entirely.

On WolfSSL, CRL checking requires a HAVE_CRL build; without it, supplying a CRL or a non-disabled policy fails the handshake with std::errc::function_not_supported rather than skip the check silently.

TLS Streams

TLS streams wrap an underlying stream (like a connected tcp_socket) to provide encrypted I/O.

tls_stream Base Class

tls_stream is a standalone, coroutine-based abstract base class. It does not derive from io_stream: unlike OS-level I/O completed by the kernel, its operations are coroutines that orchestrate reads and writes on the underlying stream. Its read_some/write_some template wrappers satisfy the capy::Stream concept, so composed operations like capy::read and capy::write work with it.

class tls_stream
{
public:
    enum handshake_type { client, server };

    template<capy::MutableBufferSequence B>
    auto read_some(B const& buffers);     // Decrypt and read

    template<capy::ConstBufferSequence B>
    auto write_some(B const& buffers);    // Encrypt and write

    virtual capy::io_task<> handshake(handshake_type type) = 0;
    virtual capy::io_task<> shutdown() = 0;

    virtual capy::any_stream& next_layer() noexcept = 0;  // Underlying stream
};

wolfssl_stream

The WolfSSL-based implementation. Two construction modes are available: the reference form takes a pointer and does not own the stream (the caller keeps it alive), while the owning form takes the stream by value and moves it. To wrap an already-connected socket, use the pointer form:

#include <boost/corosio/wolfssl_stream.hpp>

corosio::tcp_socket sock(ioc);
// ... connect sock ...

tls_context ctx;
// ... configure ctx ...

// Reference form: sock must outlive secure
corosio::wolfssl_stream secure(&sock, ctx);

// Or owning form: secure takes ownership of the socket
corosio::wolfssl_stream owned(std::move(sock), ctx);

openssl_stream

The OpenSSL-based implementation, with the same construction modes:

#include <boost/corosio/openssl_stream.hpp>

corosio::openssl_stream secure(&sock, ctx);

Both implementations provide the same interface through tls_stream.

Handshake

Before encrypted communication, perform the TLS handshake:

Client Handshake

auto [ec] = co_await secure.handshake(tls_stream::client);
if (ec)
{
    std::cerr << "Handshake failed: " << ec.message() << "\n";
    co_return;
}

Server Handshake

auto [ec] = co_await secure.handshake(tls_stream::server);

Handshake Errors

Common handshake failures:

Error Cause

Certificate verification failure

Peer certificate invalid, expired, or untrusted

Protocol version mismatch

No common TLS version supported

Cipher negotiation failure

No common cipher suite

Hostname mismatch

Certificate doesn’t match expected hostname

Reading and Writing

After handshake, read and write through the TLS stream just as you would any capy::Stream:

// Read encrypted data
char buf[1024];
auto [ec, n] = co_await secure.read_some(
    capy::mutable_buffer(buf, sizeof(buf)));

// Write encrypted data
std::string msg = "Hello, TLS!";
auto [wec, wn] = co_await secure.write_some(
    capy::const_buffer(msg.data(), msg.size()));

Composed Operations

The capy::read() and capy::write() free functions work with TLS streams:

// Read until buffer full
auto [ec, n] = co_await capy::read(secure, large_buffer);

// Write all data
auto [wec, wn] = co_await capy::write(secure, data_buffer);

Shutdown

Graceful TLS shutdown sends a close_notify alert:

auto [ec] = co_await secure.shutdown();
// Then close the underlying socket
sock.close();

Shutdown is optional but recommended. Without it, the peer can’t distinguish between a graceful close and a truncation attack.

Plain and Encrypted Connections

A tls_stream is not an io_stream, so a function taking io_stream& will not accept a TLS stream. Provide a separate overload taking tls_stream& for the encrypted case. The bodies are identical because capy::read and capy::write accept either stream:

capy::task<void> send_request(corosio::io_stream& stream)
{
    std::string request = "GET / HTTP/1.1\r\n\r\n";
    if (auto [ec, n] = co_await capy::write(
            stream, capy::const_buffer(request.data(), request.size())); ec)
        throw std::system_error(ec);

    std::string response;
    co_await capy::read(stream, capy::string_dynamic_buffer(&response));
}

capy::task<void> send_request(corosio::tls_stream& stream)
{
    std::string request = "GET / HTTP/1.1\r\n\r\n";
    if (auto [ec, n] = co_await capy::write(
            stream, capy::const_buffer(request.data(), request.size())); ec)
        throw std::system_error(ec);

    std::string response;
    co_await capy::read(stream, capy::string_dynamic_buffer(&response));
}

// Plain socket uses the io_stream overload
corosio::tcp_socket sock(ioc);
co_await send_request(sock);

// TLS stream uses the tls_stream overload
corosio::wolfssl_stream secure(&sock, ctx);
co_await send_request(secure);

HTTPS Client Example

Complete example connecting to an HTTPS server:

This example uses set_default_verify_paths(), which does not currently load any trust anchors, so it verifies nothing against a real public server. To verify the server today, replace that call with an explicit CA bundle, for example:

ctx.load_verify_file("/etc/ssl/certs/ca-certificates.crt");
capy::task<void> https_get(
    corosio::io_context& ioc,
    std::string_view hostname,
    std::uint16_t port)
{
    // Resolve hostname
    corosio::resolver resolver(ioc);
    auto [resolve_ec, results] = co_await resolver.resolve(
        hostname, std::to_string(port));
    if (resolve_ec)
        throw std::system_error(resolve_ec);

    // Connect TCP socket
    corosio::tcp_socket sock(ioc);
    sock.open();

    for (auto const& entry : results)
    {
        auto [ec] = co_await sock.connect(entry.get_endpoint());
        if (!ec)
            break;
    }

    // Configure TLS
    tls_context ctx;
    ctx.set_hostname(hostname);
    if (auto ec = ctx.set_default_verify_paths(); ec)
        throw std::system_error(ec);
    if (auto ec = ctx.set_verify_mode(tls_verify_mode::peer); ec)
        throw std::system_error(ec);

    // Wrap the connected socket (pointer form) and handshake
    corosio::wolfssl_stream secure(&sock, ctx);
    if (auto [ec] = co_await secure.handshake(wolfssl_stream::client); ec)
        throw std::system_error(ec);

    // Send HTTP request
    std::string request =
        "GET / HTTP/1.1\r\n"
        "Host: " + std::string(hostname) + "\r\n"
        "Connection: close\r\n"
        "\r\n";

    if (auto [ec, n] = co_await capy::write(
            secure, capy::const_buffer(request.data(), request.size())); ec)
        throw std::system_error(ec);

    // Read response
    std::string response;
    auto [ec, n] = co_await capy::read(
        secure, capy::string_dynamic_buffer(&response));

    // EOF expected when server closes
    if (ec && ec != capy::cond::eof)
        throw std::system_error(ec);

    std::cout << response << "\n";

    // Graceful shutdown
    co_await secure.shutdown();
}

TLS Server Example

Server with certificate and key:

capy::task<void> tls_server(
    corosio::io_context& ioc,
    std::uint16_t port)
{
    // Configure server TLS context
    tls_context ctx;
    ctx.use_certificate_chain_file("server-fullchain.pem");
    ctx.use_private_key_file("server.key", tls_file_format::pem);

    // Set up acceptor
    corosio::tcp_acceptor acc(ioc, corosio::endpoint(port));

    for (;;)
    {
        corosio::tcp_socket peer(ioc);
        auto [ec] = co_await acc.accept(peer);
        if (ec) break;

        // Spawn handler
        capy::run_async(ioc.get_executor())(
            handle_tls_client(std::move(peer), ctx));
    }
}

capy::task<void> handle_tls_client(
    corosio::tcp_socket sock,
    tls_context ctx)
{
    // Owning form: the handler owns the socket, so move it in
    corosio::wolfssl_stream secure(std::move(sock), ctx);

    auto [ec] = co_await secure.handshake(wolfssl_stream::server);
    if (ec)
        co_return;

    // Handle encrypted connection...
    char buf[1024];
    auto [read_ec, n] = co_await secure.read_some(
        capy::mutable_buffer(buf, sizeof(buf)));

    // Graceful shutdown
    co_await secure.shutdown();
}

Mutual TLS (mTLS)

For client certificate authentication:

Server Side

tls_context server_ctx;
server_ctx.use_certificate_chain_file("server.pem");
server_ctx.use_private_key_file("server.key", tls_file_format::pem);

// Require client certificate
server_ctx.set_verify_mode(tls_verify_mode::require_peer);
server_ctx.load_verify_file("client-ca.pem");

Client Side

On WolfSSL builds without WOLFSSL_SYS_CA_CERTS, set_default_verify_paths() cannot load the system store; supply the server’s CA explicitly with load_verify_file() instead.
tls_context client_ctx;
client_ctx.set_default_verify_paths();
client_ctx.set_verify_mode(tls_verify_mode::peer);
client_ctx.set_hostname("server.example.com");

// Provide client certificate
client_ctx.use_certificate_file("client.crt", tls_file_format::pem);
client_ctx.use_private_key_file("client.key", tls_file_format::pem);

Thread Safety

Operation Thread Safety

Distinct contexts

Safe from different threads

Shared context (read-only)

Safe after configuration complete

Distinct streams

Safe from different threads

Same stream

NOT safe for concurrent operations

Don’t perform concurrent read, write, or handshake operations on the same TLS stream.

Building with TLS Libraries

WolfSSL

find_package(WolfSSL REQUIRED)
target_link_libraries(my_target PRIVATE WolfSSL::WolfSSL)

OpenSSL

find_package(OpenSSL REQUIRED)
target_link_libraries(my_target PRIVATE OpenSSL::SSL OpenSSL::Crypto)

Next Steps