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:
|
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
|
Implementation status
Several
To verify a peer today you must supply CA certificates explicitly via
|
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");
PKCS#12 loading is not yet implemented; use_pkcs12() and
use_pkcs12_file() return std::errc::function_not_supported. Load the
certificate and key separately for now.
|
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();
|
|
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");
add_certificate_authority() and load_verify_file() are the
currently working way to establish trust anchors. 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. add_verify_path() is not
yet applied — the directory is never loaded.
|
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);
| Protocol version bounds are not yet applied by the backends. The negotiated range is whatever the native default method provides. |
Available versions:
| Version | Description |
|---|---|
|
TLS 1.2 (RFC 5246) |
|
TLS 1.3 (RFC 8446) |
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:
-
Sends Server Name Indication (SNI) so the server knows which certificate to present (important for virtual hosting)
-
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:
|
|
ctx.set_verify_callback(
[](bool preverified, /* verify_context& */ auto& ctx) {
// Return true to accept, false to reject
if (!preverified)
return false; // Reject if basic checks failed
// Additional custom checks...
return true;
});
Revocation Checking
None of the revocation features below are wired up yet. CRLs
(add_crl() / add_crl_file()), OCSP stapling (set_ocsp_staple() /
set_require_ocsp_staple()), and set_revocation_policy() are all accepted
but inert. In particular, set_require_ocsp_staple(true) does not fail the
handshake when no staple is present, and soft_fail / hard_fail do not
change verification behavior.
|
Certificate Revocation Lists
// Load CRL from file
ctx.add_crl_file("issuer.crl");
// Load CRL from memory
ctx.add_crl(crl_data);
OCSP Stapling
For servers, provide a stapled OCSP response:
ctx.set_ocsp_staple(ocsp_response_data);
For clients, require the server to staple:
ctx.set_require_ocsp_staple(true);
Revocation Policy
// Don't check revocation (default)
ctx.set_revocation_policy(tls_revocation_policy::disabled);
// Check but allow if status unknown
ctx.set_revocation_policy(tls_revocation_policy::soft_fail);
// Fail if revocation status can't be determined
ctx.set_revocation_policy(tls_revocation_policy::hard_fail);
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);
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;
}
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()));
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
|
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
set_default_verify_paths() below is a no-op in this release; supply
the server’s CA explicitly with load_verify_file() to actually verify it.
|
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.
Next Steps
-
Sockets — The underlying stream
-
Composed Operations — read() and write()
-
TLS Context Tutorial — Step-by-step configuration