Repository navigation
TLS Configuration
Elio provides TLS/SSL support via OpenSSL. This guide covers TLS configuration for secure connections.
TLS (Transport Layer Security) provides:
- Encryption: Data is encrypted in transit
- Authentication: Verify server (and optionally client) identity
- Integrity: Detect tampering with data
Elio uses OpenSSL as its TLS backend. OpenSSL is mature, widely available on Linux distributions, and supports modern TLS 1.3 with ALPN for HTTP/2 negotiation. It provides industry-standard certificate management and is the most broadly deployed TLS library, ensuring compatibility with existing infrastructure and tooling.
TLS support requires:
- OpenSSL library (1.1.1+ recommended)
- CMake option:
-DELIO_ENABLE_TLS=ON
The tls_context manages TLS configuration:
#include <elio/tls/tls_context.hpp>
#include <elio/tls/tls_stream.hpp>
using namespace elio::tls;
// Create client TLS context
tls_context ctx(tls_mode::client);
// Use system CA certificates for verification
ctx.use_default_verify_paths();
// Enable peer verification (recommended)
ctx.set_verify_mode(verify_mode::peer);// Create server TLS context
tls_context ctx(tls_mode::server);
// Load server certificate and private key
ctx.load_certificate("/path/to/server.crt");
ctx.load_private_key("/path/to/server.key");
// Optionally require client certificates
ctx.set_verify_mode(verify_mode::fail_if_no_cert);
ctx.load_verify_locations("/path/to/ca.crt");enum class verify_mode {
none, // No verification (insecure!)
peer, // Verify peer certificate
fail_if_no_cert // Fail if peer has no certificate (server mode)
};
ctx.set_verify_mode(verify_mode::fail_if_no_cert);// Load specific CA certificate file
ctx.load_verify_locations("/path/to/ca-bundle.crt");
// Load directory of CA certificates
ctx.load_verify_locations("", "/etc/ssl/certs/");
// Load both file and directory
ctx.load_verify_locations("/path/to/ca-bundle.crt", "/etc/ssl/certs/");
// Use system default CA store
ctx.use_default_verify_paths();Call stream.set_hostname(reference) before the handshake when wrapping a
connection yourself. DNS names select DNS certificate matching and DNS SNI.
Unbracketed numeric IPv4/IPv6 addresses select only IP-address SAN matching and
do not send SNI. A numeric DNS SAN or Common Name is not a substitute for an IP
SAN. An IPv6 %zone suffix is a local socket selector, not part of certificate
identity: the setter matches the zone-free IPv6 address and does not send the
zone in SNI. The resolver/caller owns interface selection and zone validity;
an empty zone is rejected. This is not URI percent-decoding and does not extend
the proxy route's supported authority syntax.
tls_connect() and the HTTP clients select the reference from the target.
Changing a reference replaces the previous DNS/IP identity; an empty reference
preserves the legacy explicit clearing behavior (trust-chain verification alone
is not endpoint authentication). Embedded NUL is rejected, and native identity
configuration failure throws and makes that TLS session terminal.
Callers still select the destination and trust/verification policy, provide DNS names in IDNA ASCII-label form, and serialize reference changes with the handshake and all public stream operations. URI decoding belongs to the URI consumer, not this generic TLS setter. Numeric deployments should migrate any textual-IP DNS certificates to certificates with the appropriate IP SAN.
For mutual TLS authentication:
tls_context ctx(tls_mode::client);
// Load client certificate
ctx.load_certificate("/path/to/client.crt");
// Load client private key (with optional password for encrypted keys)
ctx.load_private_key("/path/to/client.key", "my_secret_password");ALPN is required for HTTP/2:
// For HTTP/2 client (comma-separated protocol list)
ctx.set_alpn_protocols("h2,http/1.1");
// After connection, check negotiated protocol
tls_stream stream = /* ... */;
std::string_view proto = stream.alpn_protocol();
if (proto == "h2") {
// Use HTTP/2
} else {
// Fall back to HTTP/1.1
}coro::task<void> connect_example() {
tls_context ctx(tls_mode::client);
ctx.use_default_verify_paths();
ctx.set_verify_mode(verify_mode::peer);
// Connect with TLS
auto stream = co_await tls_connect(ctx, "example.com", 443);
if (!stream) {
std::cerr << "TLS connection failed: " << strerror(errno) << std::endl;
co_return;
}
// Use the stream
co_await stream->write_exactly("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n");
char buffer[4096];
auto result = co_await stream->read(buffer, sizeof(buffer));
if (result.success()) {
std::cout.write(buffer, result.bytes_transferred());
}
co_await stream->shutdown();
}tls_stream is the TCP convenience facade. The TLS engine itself is available
as basic_tls_stream<Lower> for any move-owned lower stream that provides
cancellable asynchronous read(void*, size_t, cancel_token) and
write(const void*, size_t, cancel_token) operations and explicitly satisfies
net::publishing_byte_stream:
// The lower stream can be a tunnel, buffered channel, or another TLS stream.
auto lower = co_await open_lower_channel();
tls_context origin_tls(tls_mode::client);
origin_tls.use_default_verify_paths();
origin_tls.set_verify_mode(verify_mode::peer);
basic_tls_stream<decltype(lower)> stream(std::move(lower), origin_tls);
stream.set_hostname("origin.example");
if (!co_await stream.handshake(token)) {
// errno describes the local TLS/lower-stream failure.
co_return;
}Each TLS layer has its own context, SNI, verification mode, ALPN and session
state. For example, HTTPS-through-HTTPS proxying uses an outer TLS stream for
the proxy connection, then an inner basic_tls_stream over an owned CONNECT
channel after the tunnel is established. That channel wraps the outer
tls_stream together with any bytes read past the CONNECT response boundary,
so the concrete layering is equivalent to
basic_tls_stream<owned_prefix_stream<tls_stream>>. The inner layer's
ciphertext is written through that channel; fd() diagnostics never authorize
bypassing the buffered prefix or lower protocol.
coro::task<void> server_example() {
tls_context ctx(tls_mode::server);
ctx.load_certificate("/path/to/server.crt");
ctx.load_private_key("/path/to/server.key");
auto listener = tls_listener::bind(
net::ipv4_address("0.0.0.0", 8443),
ctx
);
if (!listener) {
std::cerr << "Failed to bind" << std::endl;
co_return;
}
while (true) {
auto client = co_await listener->accept();
if (client) {
elio::go([client = std::move(*client)]() mutable {
return handle_client(std::move(client));
});
}
}
}The TLS version is configured via the tls_version enum passed to the tls_context constructor:
// Require TLS 1.2 or higher (default)
tls_context ctx(tls_mode::client, tls_version::tls_1_2_or_higher);
// Require TLS 1.3 only
tls_context ctx(tls_mode::client, tls_version::tls_1_3_only);
// Exactly TLS 1.2
tls_context ctx(tls_mode::client, tls_version::tls_1_2);// Set preferred cipher suites (TLS 1.2)
ctx.set_ciphers("ECDHE+AESGCM:DHE+AESGCM:ECDHE+CHACHA20");
// Set cipher suites for TLS 1.3
ctx.set_ciphersuites("TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256");coro::task<void> handle_tls_errors() {
tls_context ctx(tls_mode::client);
ctx.use_default_verify_paths();
ctx.set_verify_mode(verify_mode::peer);
auto stream = co_await tls_connect(ctx, "expired.badssl.com", 443);
if (!stream) {
// Check errno for specific error
// Common TLS-related errors:
// - Certificate verification failed
// - Connection refused
// - Handshake timeout
std::cerr << "TLS error: " << strerror(errno) << std::endl;
co_return;
}
// Check certificate verification result
long verify_result = stream->verify_result();
if (verify_result != X509_V_OK) {
std::cerr << "Certificate error: "
<< X509_verify_cert_error_string(verify_result)
<< std::endl;
}
}coro::task<void> inspect_connection() {
auto stream = co_await tls_connect(ctx, "example.com", 443);
if (!stream) co_return;
// TLS version
std::cout << "TLS Version: " << stream->version() << std::endl;
// Cipher suite
std::cout << "Cipher: " << stream->cipher() << std::endl;
// ALPN protocol
std::cout << "ALPN: " << stream->alpn_protocol() << std::endl;
// Peer certificate
X509* cert = stream->peer_certificate();
if (cert) {
// Extract certificate info
X509_NAME* subject = X509_get_subject_name(cert);
char buf[256];
X509_NAME_oneline(subject, buf, sizeof(buf));
std::cout << "Subject: " << buf << std::endl;
X509_free(cert);
}
}http::transport_config transport_config;
transport_config.configure_tls = [](http::transport_tls_config& tls_ctx) {
tls_ctx.load_verify_locations("/path/to/custom-ca.crt");
};
auto transport = std::make_shared<http::transport>(transport_config);
http::client client(transport);
auto resp = co_await client.get("https://example.com/");HTTP/1 transports seal their active TLS context at construction so shared
clients cannot mutate a published security identity. Use
transport_config::configure_tls before constructing the transport. The
callback receives http::transport_tls_config, a construction-only builder that
forwards common TLS policy mutators and exposes only copied diagnostics such as
mode and verification flags. It does not expose SSL_CTX*, X509_STORE*, or
the published TLS context. This is a breaking HTTP/1 client migration from older
client.tls_context().load_* or set_* customization calls; use
client.tls_diagnostics() only for post-publication value inspection.
WebSocket, SSE, and HTTP/2 clients continue to expose mutable per-client TLS
contexts.
h2_client client;
// Configure TLS for HTTP/2
auto& tls_ctx = client.tls_context();
// HTTP/2 requires ALPN
// (automatically configured by h2_client)
// Get negotiated protocol after connection
// h2_client uses HTTP/2 if server supports it-
Always verify certificates in production
- Only disable for testing/development
- Use
verify_mode::peerat minimum
-
Use modern TLS versions
- Require TLS 1.2+ for security
- Consider TLS 1.3 for best security and performance
-
Configure strong cipher suites
- Prefer ECDHE for forward secrecy
- Use AESGCM or ChaCha20 for encryption
-
Handle errors gracefully
- Check return values
- Log certificate verification failures
-
Keep certificates updated
- Monitor certificate expiration
- Use automated renewal (Let's Encrypt, etc.)
-
Protect private keys
- Use file permissions (chmod 600)
- Consider hardware security modules for high-security applications
Use badssl.com for testing various TLS scenarios:
// Test certificate verification
co_await tls_connect(ctx, "expired.badssl.com", 443); // Should fail
co_await tls_connect(ctx, "wrong.host.badssl.com", 443); // Should fail
co_await tls_connect(ctx, "self-signed.badssl.com", 443); // Should fail
co_await tls_connect(ctx, "sha256.badssl.com", 443); // Should succeed