facil.io

#OpenSSL TLS Backend

c
#define FIO_IO
#include "fio-stl/include.h"

OpenSSL 3.x IO-layer TLS backend for the facil.io reactor. When OpenSSL is available at build time this module auto-registers itself as the default TLS transport. You don't have to call anything — just define FIO_IO and link against OpenSSL 3.x.

For IO-layer TLS context overview see [./400 io-overview.md](./400 io-overview.md).
For the sibling native TLS 1.3 backend see [./405 tls13.md](./405 tls13.md) (planned).
For standalone TLS 1.3 crypto (key schedule, record layer) see [./190 tls13.md](./190 tls13.md).


#Activation

The module compiles automatically when all of the following are true:

Condition Notes
FIO_IO is defined pulls in the IO reactor
FIO_NO_TLS is not defined opt-out guard
OpenSSL 3.x headers are present HAVE_OPENSSL or __has_include("openssl/ssl.h")

If OpenSSL headers are found but the version is older than 3.x (OPENSSL_VERSION_MAJOR < 3), the module emits a compiler warning and makes fio_openssl_io_functions() return fio_io_tls_default_functions(NULL) (the current default, usually a no-op). Everything compiles; TLS simply does nothing.

A module-level constructor runs before main():

  • Initialises the custom BIO methods.
  • Calls fio_io_tls_default_functions(&openssl_io_funcs) so every subsequent fio_io_listen / fio_io_connect call with a tls argument uses OpenSSL.
  • Registers a SIGPIPE monitor (OpenSSL can trigger SIGPIPE on broken sockets).
  • Registers a FIO_CALL_AT_EXIT cleanup for the custom BIO method objects.

#Public API

#fio_openssl_io_functions

c
fio_io_functions_s fio_openssl_io_functions(void);

Returns the fio_io_functions_s vtable that wires OpenSSL into the IO reactor:

Field Role
build_context Converts fio_io_tls_s into an SSL_CTX wrapper
free_context Deferred free of SSL_CTX and the fio_io_tls_s reference
start Per-connection: SSL_new, custom BIO setup, initial handshake
read Non-blocking decrypt: advances handshake if needed, then SSL_read_ex
write Non-blocking encrypt: SSL_write_ex → encrypt → socket
flush Sends any pending encrypted bytes from the internal buffer
finish Sends TLS close_notify before the TCP close
cleanup Frees the per-connection SSL object

Normally you never call this directly — the constructor handles registration. Use it explicitly only when overriding the default or building a custom setup:

c
/* Override: switch back to OpenSSL after something else changed the default */
fio_io_functions_s openssl_funcs = fio_openssl_io_functions();
fio_io_tls_default_functions(&openssl_funcs);

Or to set it on a specific protocol without touching the global default (the io_functions field lives on fio_io_protocol_s):

c
fio_io_functions_s openssl_funcs = fio_openssl_io_functions();
MY_PROTOCOL.io_functions = openssl_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 held 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 (fio_io_tls_dup / fio_io_tls_free). The backend duplicates the reference during build_context; freeing yours after fio_io_listen is always safe.

#URL shorthand

The URL query string can also configure TLS without building the object manually:

c
fio_io_listen(.url = "0.0.0.0:443/?tls=./certs/", .protocol = &MY_PROTOCOL);

See fio_io_tls_from_url in 401 io api.h for the query syntax.


#Certificates

#Loading from PEM files

When public_cert_file and private_key_file are both provided to fio_io_tls_cert_add, the backend:

  1. Sets a PEM password callback if pk_password is non-NULL.
  2. Loads the certificate chain via SSL_CTX_use_certificate_chain_file.
  3. Loads the private key via SSL_CTX_use_PrivateKey_file (PEM format).
  4. Verifies key-cert consistency with SSL_CTX_check_private_key.

Errors are logged with FIO_LOG_ERROR and the context build fails.

#Self-signed fallback

When a server has no certificates configured (or fio_io_tls_cert_add is called with both file arguments as NULL), the backend generates a self-signed ECDSA P-256 certificate on the fly:

Property Value
Key algorithm ECDSA P-256 (128-bit security ≈ RSA-3072)
Key generation time ~10 ms
Signature SHA-256
Validity 180 days
Serial number 128-bit cryptographically random
X.509 extensions Basic Constraints (CA:FALSE), Key Usage, Extended Key Usage (serverAuth), SAN

The private key is generated once per process (thread-safe, lock-protected) and freed at exit via a FIO_CALL_AT_EXIT callback.

Self-signed certificates are fine for development. Browsers will warn. Use a CA-issued certificate (e.g. Let's Encrypt) in production.


#ALPN

Register protocols with fio_io_tls_alpn_add before listen/connect. The first registered protocol is the preferred default.

When a client sends an ALPN extension, the backend walks the offered names in order and picks the first that matches a registered protocol. On match it calls fio_io_tls_alpn_select, which fires the on_selected callback for that connection. If nothing matches, the handshake fails with a fatal alert.

Protocol names must be 1–255 bytes. The internal wire-format list is capped at 1 023 bytes total; more protocols than that will log an overflow error.


#Trust and Peer Verification

Trust configured with fio_io_tls_trust_add always refers to peer verification: a client context verifies the server certificate, a server context requests, requires, and verifies the client certificate (mutual TLS). An empty trust list means no peer verification.

Scenario Behaviour
fio_io_tls_trust_add called (any argument) SSL_VERIFY_PEER enabled
NULL passed to fio_io_tls_trust_add system trust store is loaded (X509_STORE_set_default_paths)
Trust configured, server mode SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT (client certificate required)
No trust certs added, client mode SSL_VERIFY_NONE + FIO_LOG_SECURITY warning
No trust certs added, server mode SSL_VERIFY_NONE (no client certificate requested)

After the handshake, inspect the peer's certificate chain with fio_io_peer_info_next(io, &info) (see the IO API docs). Each call re-encodes the next chain certificate into a fixed per-connection staging buffer (no allocation) and parses it into a fio_x509_cert_s, exposing the subject, issuer, SAN entries, public key, validity, and a SHA-256 fingerprint. info.verified reflects OpenSSL's chain verification result (SSL_get_verify_result).


#SSL Context Modes

The SSL_CTX is configured for non-blocking use:

SSL_MODE_ENABLE_PARTIAL_WRITE      — SSL_write may return before all data encrypted
SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER — buffer pointer may differ between retries
SSL_MODE_RELEASE_BUFFERS           — free idle 34 KB per-connection OpenSSL buffers
SSL_MODE_AUTO_RETRY                — CLEARED (return immediately on WANT_READ/WRITE)

TLS 1.3 session tickets: 2 tickets are configured for session resumption (saves ~30–50 % of handshake CPU on reconnects).


#Internal Design — Custom BIOs

Instead of BIO_s_mem() (which bounces data through a private BUF_MEM), the backend uses custom BIO_METHOD objects that give OpenSSL direct access to the per-connection buffers:

  • rbio — OpenSSL reads raw socket bytes directly from a 64 KB receive buffer that was filled by a single fio_sock_read call.
  • wbio — OpenSSL appends encrypted output directly into a ~66 KB encrypted-output buffer (4 × max TLS record).

This eliminates all intermediate copies while retaining full control over buffering and partial writes.

Handshake responses (ServerHello, etc.) are flushed to the socket from inside the read callback — the IO layer's on_ready loop doesn't run until the handshake completes, so the backend must send them itself.


#Lifecycle and Error Behavior

fio_io_listen / fio_io_connect
    │
    └─► build_context (SSL_CTX built once per listener/connector)
            │
            └─► per-connection start (SSL_new + BIO setup + SSL_accept/SSL_connect)
                    │
                    ├─ read  (advances handshake until SSL_is_init_finished,
                    │         then SSL_read_ex)
                    ├─ write (SSL_write_ex, encrypt into wbio buffer, flush socket)
                    ├─ flush (send remaining enc_buf bytes)
                    │
                    ├─ finish  (SSL_shutdown + best-effort send of close_notify)
                    └─ cleanup (SSL_free — also frees both BIOs)

Error codes surfaced to the IO layer:

  • return 0 — peer closed cleanly (SSL_ERROR_ZERO_RETURN) or fatal SSL error.
  • return -1 with errno = EWOULDBLOCK — not enough data yet (normal event-loop signalling; the reactor will retry on next readable event).

All non-fatal internal issues are logged via FIO_LOG_ERROR / FIO_LOG_WARNING / FIO_LOG_SECURITY. Debug-level detail is at FIO_LOG_DDEBUG2.


#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 configured → self-signed ECDSA 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  (test: openssl s_client -connect localhost: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);

#Disambiguation

Document Scope
This document OpenSSL 3.x IO-layer backend — plugs TLS into the reactor
[./405 tls13.md](./405 tls13.md) (planned) Native TLS 1.3 IO backend (same fio_io_functions_s interface, no OpenSSL dependency)
[./190 tls13.md](./190 tls13.md) Standalone TLS 1.3 crypto library (key schedule, record layer, raw machinery)
[./400 io-overview.md](./400 io-overview.md) IO + TLS stack overview