#Native TLS 1.3 IO Backend
#define FIO_IO
#include "fio-stl/include.h"IO-layer TLS 1.3 integration — for standalone crypto library see [190 tls13.md](./190 tls13.md).
This module wires the native TLS 1.3 state machines into the facil.io IO reactor
as a drop-in transport backend. When OpenSSL is unavailable at build time it
registers itself automatically; otherwise it is available on demand via
fio_tls13_io_functions(). No external dependencies required.
For the IO/TLS stack overview see [./400 io-overview.md](./400 io-overview.md).
For the OpenSSL backend (preferred when available) see [./405 openssl.md](./405 openssl.md).
#Activation
The module compiles when all of the following are true:
| Condition | Notes |
|---|---|
FIO_IO is defined |
pulls in the IO reactor |
H___FIO_TLS13___H guard is satisfied |
190 tls13.h was included first |
FIO_NO_TLS is not defined |
opt-out guard checked by include.h before this backend is included |
A module-level constructor (FIO_CONSTRUCTOR) runs automatically before
main() only when OpenSSL is absent (HAVE_OPENSSL not defined and
405 openssl.h not included). It calls:
fio_io_tls_default_functions(&FIO___TLS13_IO_FUNCS);This makes every subsequent fio_io_listen / fio_io_connect call with a
tls argument use the native TLS 1.3 engine without any extra configuration.
When OpenSSL is present, the native backend compiles but does not
auto-register. Use fio_tls13_io_functions() to switch explicitly if needed.
#Public API
#fio_tls13_io_functions
fio_io_functions_s fio_tls13_io_functions(void);Returns the fio_io_functions_s vtable that wires the native TLS 1.3 engine
into the IO reactor:
| Field | Role |
|---|---|
build_context |
Converts fio_io_tls_s into a per-listener/connector context |
free_context |
Deferred free of the context (via fio_io_defer) |
start |
Per-connection: allocate state, run ClientHello or await ServerHello |
read |
Non-blocking decrypt: advances handshake if needed, then decrypts records |
write |
Non-blocking encrypt: batches up to 4 TLS records (64 KB) per syscall |
flush |
Drains any pending handshake or encrypted bytes from internal buffers |
finish |
Sends a TLS close_notify alert before the TCP close |
cleanup |
Frees per-connection TLS state |
Normally you never call this directly — the constructor handles registration. Use it to override the global default or set a per-protocol backend:
/* Override: force native TLS 1.3 even when OpenSSL is present */
fio_io_functions_s tls13_funcs = fio_tls13_io_functions();
fio_io_tls_default_functions(&tls13_funcs);Or set it on a specific protocol without changing the global default:
fio_io_functions_s tls13_funcs = fio_tls13_io_functions();
MY_PROTOCOL.io_functions = tls13_funcs; /* per-protocol override */
fio_io_listen(.url = "0.0.0.0:8443",
.protocol = &MY_PROTOCOL,
.tls = tls);#Configuring TLS — fio_io_tls_s
TLS parameters are carried in a fio_io_tls_s object (defined in
401 io api.h). Build one before calling fio_io_listen or fio_io_connect:
/* Allocate (reference counted) */
fio_io_tls_s *tls = fio_io_tls_new();
/* Certificate — PEM files */
fio_io_tls_cert_add(tls,
"www.example.com", /* server_name (SNI) */
"cert.pem", /* public certificate or chain */
"key.pem", /* private key */
NULL); /* PEM password, or NULL */
/* ALPN protocol negotiation */
fio_io_tls_alpn_add(tls, "h2", on_http2_selected);
fio_io_tls_alpn_add(tls, "http/1.1", on_http1_selected);
/* Peer certificate verification */
fio_io_tls_trust_add(tls, NULL); /* use system trust store */
fio_io_tls_trust_add(tls, "ca.pem"); /* or a specific CA bundle */
/* Listen */
fio_io_listen(.url = "0.0.0.0:443",
.protocol = &MY_PROTOCOL,
.tls = tls);
fio_io_tls_free(tls); /* release your reference; the listener holds its own */
fio_io_start(0);fio_io_tls_s is reference-counted. The backend duplicates the reference
during build_context; freeing yours after fio_io_listen is always safe.
#Certificates
#Loading from PEM files
When both public_cert_file and private_key_file are provided to
fio_io_tls_cert_add, the backend reads and parses the PEM files, loading
every CERTIFICATE block in the file as a DER chain entry. Three private key
types are supported:
| Key type | Constant | Notes |
|---|---|---|
| ECDSA P-256 | FIO_TLS13_SIGNATURE_ECDSA_SECP256R1_SHA256 |
Recommended; small and fast |
| Ed25519 | FIO_TLS13_SIGNATURE_ED25519 |
Fastest signatures |
| RSA (any size) | FIO_TLS13_SIGNATURE_RSA_PSS_RSAE_SHA256 |
Requires H___FIO_RSA___H |
If PEM parsing fails, the backend logs FIO_LOG_WARNING and falls back to a
self-signed certificate. If the fallback also fails the context build fails
and build_context returns NULL.
#Self-signed fallback
When a server has no certificates configured, the backend generates a
self-signed P-256 ECDSA certificate on the fly using fio_x509_self_signed_cert
(requires H___FIO_X509___H):
| Property | Value |
|---|---|
| Key algorithm | ECDSA P-256 (128-bit security ≈ RSA-3072) |
| Signature | SHA-256 |
| SAN | set to server_name (defaults to "localhost") |
Self-signed certificates are fine for development. Browsers will warn. Use a CA-issued certificate (e.g. Let's Encrypt) in production.
If H___FIO_X509___H is not available when no PEM files are configured, the
context build fails with FIO_LOG_ERROR.
#ALPN
Register protocols with fio_io_tls_alpn_add before listen/connect. The
first registered protocol is the preferred default.
The backend collects all registered names into a comma-separated string (up
to 255 bytes per name; 255 characters total plus the NUL terminator). On handshake the server matches the
client's offered list against the registered names in registration order and
calls the corresponding on_selected callback on the fio_io_s * when
negotiation succeeds.
Protocol names must be 1–255 bytes. The internal list overflows when the
comma-separated list would exceed 255 characters — excess protocols are dropped with FIO_LOG_ERROR.
#Trust and Peer Verification
Trust configured with fio_io_tls_trust_add always refers to peer
verification: on client connections the backend verifies the server
certificate, on server connections it requests, requires, and verifies
the client certificate (mutual TLS). An empty trust list means no peer
verification — matching the OpenSSL backend.
| Scenario | Behaviour |
|---|---|
fio_io_tls_trust_add(tls, NULL) |
system CA bundle used (loaded once, global) |
fio_io_tls_trust_add(tls, "ca.pem") |
user-supplied CA bundle (per-context) |
No fio_io_tls_trust_add call, client mode |
no server verification + FIO_LOG_SECURITY warning |
No fio_io_tls_trust_add call, server mode |
no client certificate requested |
| Trust configured, server mode | client certificate required and verified |
For mTLS, client certificate chains are verified against the trust store
(fio_x509_verify_chain) and the client's CertificateVerify signature is
validated against the leaf certificate's public key (Ed25519, ECDSA P-256 /
P-384, and RSA-PSS / PKCS#1 SHA-256 / SHA-384 schemes). A client that fails
verification is rejected with a TLS alert during the handshake.
After the handshake, inspect the peer's chain with
fio_io_peer_info_next(io, &info) (see the IO API docs) — each call parses
the next certificate in place (zero-copy) into a fio_x509_cert_s, exposing
the subject CN/DN, issuer, SAN entries, public key, validity, and a SHA-256
fingerprint for pinning, plus a verified flag for authorization decisions.
The system CA bundle is loaded once into a process-wide singleton
(fio___tls13_sys_trust) and shared read-only across all connections —
loading ~128 certs per outgoing connection is avoided. It is freed at process
exit via a FIO_CALL_AT_EXIT callback.
Platform CA bundle lookup order (POSIX):
/etc/ssl/cert.pem (macOS, FreeBSD)
/etc/ssl/certs/ca-certificates.crt (Debian/Ubuntu)
/etc/pki/tls/certs/ca-bundle.crt (RHEL/CentOS)
/etc/ssl/ca-bundle.pem (openSUSE)
/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem (RHEL newer)
/usr/local/etc/ssl/cert.pem (FreeBSD ports)On Windows the ROOT system certificate store is enumerated via the
CryptoAPI (CertOpenSystemStoreA / CertEnumCertificatesInStore). On MSVC,
Crypt32.lib is linked automatically via #pragma comment; other Windows
toolchains may need an explicit Crypt32 link flag.
#Lifecycle and Error Behavior
fio_io_listen / fio_io_connect
│
└─► build_context (fio_io_tls_s → fio___tls13_context_s, once per listener)
│
└─► per-connection start
│
├─ client: ClientHello sent immediately
├─ server: awaits ClientHello
│
├─ read (advances handshake records; decrypts app data after)
├─ write (encrypts; up to 4 records batched per syscall)
├─ flush (drains handshake + enc_buf to socket)
│
├─ finish (sends encrypted close_notify alert, best-effort)
└─ cleanup (frees fio___tls13_connection_s)Internal buffers per connection (flexible array member, single allocation):
| Region | Purpose | Size |
|---|---|---|
recv_buf |
Incoming encrypted data | FIO_IO_BUFFER_PER_WRITE (~64 KB) |
app_buf |
Decrypted plaintext ready to deliver | FIO_IO_BUFFER_PER_WRITE |
send_buf |
Outgoing handshake bytes | FIO_IO_BUFFER_PER_WRITE |
enc_buf |
Pre-allocated encryption output (4 max records) | ~66 KB |
Error codes surfaced to the IO layer:
return 0fromread— peer closed cleanly (EOF).return -1witherrno = EWOULDBLOCK— no data yet; reactor retries on next readable event.return -1witherrno = ECONNRESET— TLS handshake or decryption error; connection will be closed.
All internal errors are logged: FIO_LOG_ERROR for hard failures,
FIO_LOG_WARNING for soft failures (e.g. PEM fallback), FIO_LOG_DEBUG2 /
FIO_LOG_DDEBUG2 for per-connection detail.
KeyUpdate (RFC 8446 §4.6.3): when the peer requests a key update, a
KeyUpdate response is prepended to the next write syscall alongside
application data so they go out in a single call.
#Minimal Server Example
#define FIO_LOG
#define FIO_IO
#include "fio-stl/include.h"
static void on_data(fio_io_s *io) {
char buf[4096];
size_t n = fio_io_read(io, buf, sizeof(buf));
if (n)
fio_io_write(io, buf, n); /* echo */
}
static fio_io_protocol_s ECHO_PROTO = {
.on_data = on_data,
.on_timeout = fio_io_touch,
};
int main(void) {
/* No certificate → self-signed P-256 generated automatically */
fio_io_tls_s *tls = fio_io_tls_new();
fio_io_listen(.url = "0.0.0.0:8443",
.protocol = &ECHO_PROTO,
.tls = tls);
fio_io_tls_free(tls);
FIO_LOG_INFO("TLS echo server on :8443");
fio_io_start(0);
}For a production server, load real certificates:
fio_io_tls_cert_add(tls, "example.com", "cert.pem", "key.pem", NULL);#TLS Client Example
#define FIO_LOG
#define FIO_IO
#include "fio-stl/include.h"
static void on_attach(fio_io_s *io) {
/* Handshake happens transparently through the TLS transport hooks. */
const char req[] = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n";
fio_io_write(io, req, sizeof(req) - 1);
}
static void on_data(fio_io_s *io) {
char buf[4096];
size_t n = fio_io_read(io, buf, sizeof(buf) - 1);
if (n) {
buf[n] = '\0';
FIO_LOG_INFO("response:\n%s", buf);
fio_io_close(io);
}
}
static fio_io_protocol_s CLIENT_PROTO = {
.on_attach = on_attach,
.on_data = on_data,
.on_timeout = fio_io_touch,
};
int main(void) {
fio_io_tls_s *tls = fio_io_tls_new();
/* SNI hostname — system CA store is used automatically for verification */
fio_io_tls_cert_add(tls, "example.com", NULL, NULL, NULL);
fio_io_connect(.url = "example.com:443",
.protocol = &CLIENT_PROTO,
.tls = tls);
fio_io_tls_free(tls);
fio_io_start(0);
}#Comparison with OpenSSL Backend
| Feature | Native TLS 1.3 | OpenSSL 3.x |
|---|---|---|
| External dependency | None | OpenSSL 3.x |
| TLS versions | 1.3 only | 1.0–1.3 |
| Certificate key types | P-256, Ed25519, RSA | All |
| ALPN | Yes | Yes |
| Self-signed auto-cert | Yes (P-256) | Yes (P-256) |
| System trust store | Yes (multi-platform) | Yes |
| Session resumption (0-RTT) | No | Yes |
| OCSP stapling | No | Yes |
| Binary size impact | Smaller | Larger |
| Auto-registered when | OpenSSL absent | OpenSSL present |
The native backend is suitable for environments where TLS 1.3-only is acceptable and minimising dependencies matters. If you need legacy TLS versions, session resumption, OCSP stapling, or full certificate type coverage, use the OpenSSL backend.
#Disambiguation
| Document | Scope |
|---|---|
| This document | Native TLS 1.3 IO backend — plugs TLS into the reactor via fio_io_functions_s |
| [./405 openssl.md](./405 openssl.md) | OpenSSL 3.x IO backend (same interface, preferred when available) |
| [./190 tls13.md](./190 tls13.md) | Standalone TLS 1.3 crypto library — key schedule, record layer, handshake state machines |
| [./400 io-overview.md](./400 io-overview.md) | IO + TLS stack overview |