facil.io

#Native TLS 1.3 IO Backend

c
#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:

c
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

c
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:

c
/* 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:

c
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:

c
/* 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 0 from read — peer closed cleanly (EOF).
  • return -1 with errno = EWOULDBLOCK — no data yet; reactor retries on next readable event.
  • return -1 with errno = 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

c
#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:

c
fio_io_tls_cert_add(tls, "example.com", "cert.pem", "key.pem", NULL);

#TLS Client Example

c
#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