TLS Context Configuration
This tutorial covers how to configure TLS contexts for secure connections.
A tls_context stores certificates, keys, and settings that define how
TLS connections are established and verified.
|
Code snippets assume:
|
Introduction
The tls_context class is a portable abstraction for TLS configuration that
works across multiple TLS backends (WolfSSL, OpenSSL, mbedTLS, etc.). It
handles:
-
Loading certificates and private keys
-
Configuring trust anchors for peer verification
-
Setting protocol versions and cipher suites
-
Configuring certificate verification behavior
-
Managing revocation checking (CRLs)
Use tls_context to configure TLS settings once, then pass it to TLS streams
for establishing secure connections.
|
Two implemented features depend on how WolfSSL was built:
|
Construction
A tls_context is a shared handle to an opaque implementation. Copies share
the same underlying state, making it easy to pass contexts by value and share
them across multiple TLS streams.
// Create a default context
tls_context ctx;
// Copy shares the same underlying state
tls_context ctx2 = ctx; // ctx and ctx2 share state
// Move transfers ownership
tls_context ctx3 = std::move( ctx );
// ctx is now empty
The default constructor creates a context ready for TLS 1.2 and TLS 1.3 connections. No certificates or trust anchors are loaded initially—you must configure these before use.
Typical Setup Pattern
Most applications follow this pattern:
tls_context ctx;
// 1. Load credentials (for servers, or clients using client certs)
ctx.use_certificate_chain_file( "server.crt" );
ctx.use_private_key_file( "server.key", tls_file_format::pem );
// 2. Configure trust anchors (for verifying peer certificates)
ctx.set_default_verify_paths(); // Use system CA store
// 3. Set verification mode
ctx.set_verify_mode( tls_verify_mode::peer );
// 4. Configure protocol options (optional)
ctx.set_min_protocol_version( tls_version::tls_1_2 );
Credential Loading
Credentials consist of a certificate (or certificate chain) and its corresponding private key. Servers always need credentials; clients need them only for mutual TLS (mTLS).
Loading Certificate and Key Separately
The most common approach loads the certificate chain and private key from separate PEM files:
// Load certificate chain (leaf + intermediates)
ctx.use_certificate_chain_file( "fullchain.pem" );
// Load the matching private key
ctx.use_private_key_file( "privkey.key", tls_file_format::pem );
For a single certificate without intermediates:
ctx.use_certificate_file( "server.crt", tls_file_format::pem );
ctx.use_private_key_file( "server.key", tls_file_format::pem );
Loading from PKCS#12 Bundles
PKCS#12 (.pfx or .p12 files) bundles certificate, key, and chain
into a single password-protected file:
ctx.use_pkcs12_file( "credentials.pfx", "bundle-password" );
| The bundle is decoded when the first stream is created; a wrong passphrase or malformed bundle surfaces as a handshake failure. Intermediate certificates in the bundle are loaded and sent during the handshake on both backends. |
Loading from Memory
If credentials are stored in memory (e.g., from a database or secret manager), use the non-file variants:
std::string cert_pem = fetch_certificate_from_vault();
std::string key_pem = fetch_key_from_vault();
ctx.use_certificate_chain( cert_pem );
ctx.use_private_key( key_pem, tls_file_format::pem );
DER Format
For binary DER-encoded files (common in embedded systems):
ctx.use_certificate_file( "server.der", tls_file_format::der );
ctx.use_private_key_file( "server.key.der", tls_file_format::der );
Encrypted Private Keys
For password-protected private keys, set a password callback before loading. See Password Handling for details.
Trust Anchors
Trust anchors are root CA certificates used to verify peer certificates. Without trust anchors, certificate verification will fail.
Using System Trust Store
For HTTPS clients connecting to public servers, use the system’s CA store:
ctx.set_default_verify_paths();
This uses the operating system’s trusted certificates:
-
Linux:
/etc/ssl/certsor distribution-specific paths -
macOS: System Keychain
-
Windows: Windows Certificate Store
On OpenSSL the SSL_CERT_FILE and SSL_CERT_DIR environment variables are
honored. On WolfSSL this requires a build with WOLFSSL_SYS_CA_CERTS;
without it, supply trust anchors explicitly with load_verify_file() or
add_certificate_authority() (below).
Custom CA Bundle
For internal PKI or testing, load a custom CA bundle:
// Load CA bundle file (may contain multiple CAs)
ctx.load_verify_file( "/path/to/ca-bundle.crt" );
With OpenSSL, load_verify_file() registers only the first
certificate from a multi-cert bundle (WolfSSL handles multi-cert files);
add the rest with repeated add_certificate_authority() calls.
|
CA Directory
Add a directory of CA certificates:
ctx.add_verify_path( "/etc/ssl/certs" );
With OpenSSL the directory must use the hashed-filename layout
created by openssl rehash / c_rehash, since certificates are looked up
on demand by subject-name hash. WolfSSL loads every certificate file in
the directory.
|
Individual CA Certificates
Add CA certificates one at a time:
// From memory
std::string internal_ca = load_ca_from_config();
ctx.add_certificate_authority( internal_ca );
// Multiple CAs
ctx.add_certificate_authority( root_ca_pem );
ctx.add_certificate_authority( intermediate_ca_pem );
Combining Trust Sources
You can combine multiple trust sources:
// Start with system trust store
ctx.set_default_verify_paths();
// Add an internal CA for corporate servers
ctx.add_certificate_authority( corporate_ca_pem );
The set_default_verify_paths() call above contributes nothing in
this release. Only the explicitly added CA is actually trusted.
|
Protocol Configuration
Control which TLS versions and cipher suites are allowed for connections.
TLS Version Bounds
Set minimum and/or maximum TLS versions:
// Require TLS 1.2 or newer (default)
ctx.set_min_protocol_version( tls_version::tls_1_2 );
// Require TLS 1.3 only
ctx.set_min_protocol_version( tls_version::tls_1_3 );
ctx.set_max_protocol_version( tls_version::tls_1_3 );
| On WolfSSL the ceiling is enforced by selecting a version-specific method (no native set-max call exists); a window whose minimum exceeds its maximum yields a context that fails the handshake. |
Cipher Suites
TLS 1.2-and-below suites use an OpenSSL-style cipher list; TLS 1.3 suites are configured separately:
// TLS 1.2 and below
ctx.set_ciphersuites( "ECDHE+AESGCM:ECDHE+CHACHA20" );
// TLS 1.3 (distinct API and suite names)
ctx.set_ciphersuites_tls13( "TLS_AES_256_GCM_SHA384" );
Both backends apply these (WolfSSL merges the two into a single
list). Suite names differ between backends: OpenSSL uses
TLS_AES_128_GCM_SHA256, WolfSSL uses TLS13-AES128-GCM-SHA256. The
OpenSSL security level is left at the library default; include a
@SECLEVEL= token in the cipher string if you need a lower level.
|
ALPN (Application-Layer Protocol Negotiation)
ALPN negotiates the application protocol over TLS. Common uses:
// HTTP/2 with HTTP/1.1 fallback
ctx.set_alpn( { "h2", "http/1.1" } );
// gRPC
ctx.set_alpn( { "h2" } );
Read the negotiated protocol from the stream after the handshake:
std::string_view proto = stream.alpn_protocol(); // "h2", or empty if none
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.
|
The server selects from the client’s list based on its own preferences.
Certificate Verification
Configure how peer certificates are verified during the TLS handshake.
Verification Modes
// Don't verify peer (not recommended for production)
ctx.set_verify_mode( tls_verify_mode::none );
// Verify peer if certificate is presented
ctx.set_verify_mode( tls_verify_mode::peer );
// Require and verify peer certificate (mTLS server-side)
ctx.set_verify_mode( tls_verify_mode::require_peer );
Typical usage:
-
Clients: Use
peerto verify server certificates -
Servers without mTLS: Use
none -
Servers with mTLS: Use
require_peerto require client certs
Hostname Verification
For clients, set the expected server hostname:
ctx.set_hostname( "api.example.com" );
This enables:
-
SNI (Server Name Indication): Tells the server which certificate to present (required for virtual hosting)
-
Hostname matching: Validates the certificate matches the expected host
Chain Depth
Limit how many intermediate certificates are allowed:
// Allow up to 3 intermediates (leaf -> 3 intermediates -> root)
ctx.set_verify_depth( 3 );
The default (typically 100) is sufficient for most chains.
Custom Verification
For advanced use cases, install a custom verification callback. It runs
during the handshake; return true to accept the certificate or false
to reject it. Inspect the certificate portably with
verify_ctx.certificate() (its DER bytes); native_handle() also exposes
the backend’s X509_STORE_CTX*.
ctx.set_verify_callback(
[]( bool preverified, corosio::verify_context& verify_ctx ) -> bool
{
if( !preverified )
return false; // chain did not verify
auto der = verify_ctx.certificate(); // DER of the current cert
return der.size() == expected_pin.size() &&
std::equal( der.begin(), der.end(), expected_pin.begin() );
});
|
Which certificates the callback sees depends on the backend:
|
Revocation Checking
Certificate revocation checking verifies that certificates haven’t been invalidated by the issuing CA. Revocation is checked against Certificate Revocation Lists (CRLs).
Revocation Policy
Set the overall revocation checking behavior. CRLs are consulted only when
the policy is not disabled:
// Don't check revocation (default)
ctx.set_revocation_policy( tls_revocation_policy::disabled );
// Accept unknown status, reject a listed (revoked) certificate
ctx.set_revocation_policy( tls_revocation_policy::soft_fail );
// Also reject when status can't be determined (strict)
ctx.set_revocation_policy( tls_revocation_policy::hard_fail );
CRL (Certificate Revocation Lists)
Load CRLs from the CA that issued the certificates you’re verifying:
// From file
ctx.add_crl_file( "/path/to/issuer.crl" );
// From memory (e.g., fetched via HTTP)
std::string crl_data = fetch_crl_from_url( crl_url );
ctx.add_crl( crl_data );
ctx.set_revocation_policy( tls_revocation_policy::hard_fail );
CRLs must be refreshed periodically as they expire.
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.
|
Bootstrap vs. Hardened Connections
A common pattern uses two context configurations:
|
Both contexts below verify the peer via |
// Bootstrap context: for fetching revocation data
tls_context bootstrap_ctx;
bootstrap_ctx.set_default_verify_paths();
bootstrap_ctx.set_verify_mode( tls_verify_mode::peer );
bootstrap_ctx.set_revocation_policy( tls_revocation_policy::disabled );
// Hardened context: for sensitive connections
tls_context hardened_ctx;
hardened_ctx.set_default_verify_paths();
hardened_ctx.set_verify_mode( tls_verify_mode::peer );
hardened_ctx.add_crl_file( "cached.crl" );
hardened_ctx.set_revocation_policy( tls_revocation_policy::hard_fail );
Password Handling
Private keys and PKCS#12 files are often encrypted with a password. Set a password callback before loading encrypted material.
Password Callback
// Set callback before loading encrypted key
ctx.set_password_callback(
[]( std::size_t max_length, tls_password_purpose purpose )
{
// purpose: for_reading (decrypt) or for_writing (encrypt)
return std::string( "my-secret-password" );
});
// Now load encrypted private key
ctx.use_private_key_file( "encrypted.key", tls_file_format::pem );
Secure Password Handling
In production, don’t hardcode passwords:
ctx.set_password_callback(
[]( std::size_t max_length, tls_password_purpose purpose )
{
// Read from environment
if( auto* pw = std::getenv( "TLS_KEY_PASSWORD" ) )
return std::string( pw );
// Or prompt user
return prompt_user_for_password();
});
Complete Examples
This section demonstrates complete, practical configurations for common scenarios.
HTTPS Client Context
This example trusts public CAs via set_default_verify_paths(). On
WolfSSL builds without WOLFSSL_SYS_CA_CERTS, load an explicit CA bundle
instead, for example
ctx.load_verify_file( "/etc/ssl/certs/ca-certificates.crt" );.
|
tls_context make_https_client_context()
{
tls_context ctx;
// Trust system CAs for public websites
ctx.set_default_verify_paths();
// Verify server certificates
ctx.set_verify_mode( tls_verify_mode::peer );
// Modern TLS only
ctx.set_min_protocol_version( tls_version::tls_1_2 );
return ctx;
}
// Usage with TLS stream
tls_context ctx = make_https_client_context();
ctx.set_hostname("api.example.com"); // Set before creating stream
// Pointer form wraps the connected socket without taking ownership
corosio::wolfssl_stream secure( &sock, ctx );
co_await secure.handshake( corosio::wolfssl_stream::client );
TLS Server Context
tls_context make_server_context()
{
tls_context ctx;
// Load server credentials
ctx.use_certificate_chain_file( "fullchain.pem" );
ctx.use_private_key_file( "privkey.pem", tls_file_format::pem );
// Don't verify client certificates (no mTLS)
ctx.set_verify_mode( tls_verify_mode::none );
return ctx;
}
Mutual TLS (mTLS)
tls_context make_mtls_client_context()
{
tls_context ctx;
// Client credentials for mTLS
ctx.use_certificate_chain_file( "client.crt" );
ctx.use_private_key_file( "client.key", tls_file_format::pem );
// Trust specific CA for server verification
ctx.load_verify_file( "server-ca.crt" );
ctx.set_verify_mode( tls_verify_mode::peer );
return ctx;
}
Error Handling
All fallible operations return std::error_code. Check the return value
to detect errors:
// Throw on error
if( auto ec = ctx.use_certificate_file( "cert.pem", tls_file_format::pem ) )
throw std::system_error(ec);
// Check error explicitly
if( auto ec = ctx.load_verify_file( "ca.crt" ) )
{
std::cerr << "Failed to load CA: " << ec.message() << "\n";
return;
}
Common errors include:
-
File not found or permission denied
-
Invalid certificate or key format
-
Key doesn’t match certificate
-
Wrong passphrase for encrypted key or PKCS#12
Next Steps
-
TLS Encryption — Using TLS streams
-
HTTP Client Tutorial — HTTPS example