xtc_tls(3)

---

xtc_tls(3)

TLS over async sockets

XTC_TLS(3) Library Functions Manual XTC_TLS(3)

xtc_tls_ctx_create, xtc_tls_ctx_destroy, xtc_tls_create, xtc_tls_create_transport, xtc_tls_ctx_set_sni_cb, xtc_tls_set_hostname, xtc_tls_destroy, xtc_tls_handshake, xtc_tls_read, xtc_tls_write, xtc_tls_shutdown, xtc_tls_wants_read, xtc_tls_wants_write, xtc_tls_get_version, xtc_tls_get_cipher, xtc_tls_get_cipher_bits, xtc_tls_get_alpn_selected, xtc_tls_has_peer_cert, xtc_tls_get_verify_error, xtc_tls_get_peer_subject_dn, xtc_tls_get_peer_common_name, xtc_tls_get_peer_issuer_dn, xtc_tls_get_peer_serial, xtc_tls_get_server_cert_hash, xtc_tls_enable_ktls, xtc_tls_ktls_active, xtc_tls_sendfile — TLS over async sockets

#include <xtc.h>
#include <xtc_tls.h>

xtc_tls provides TLS 1.2 / 1.3 over xtc_io sockets. The implementation selects a backend at configure time (‘--with-tls=openssl|none|auto’); in v0.x only OpenSSL is implemented.

:

xtc_tls_ctx_t
Per-process state: cert, key, CA bundle, version range, ALPN profile. Created once via () with role (XTC_TLS_SERVER or XTC_TLS_CLIENT) plus opts.
xtc_tls_t
Per-connection state. Wraps a single fd. Created via (ctx, fd, &out).

:

xtc_tls_create(ctx, fd, &tls);
for (;;) {
    int rc = xtc_tls_handshake(tls);
    if (rc == XTC_OK) break;
    if (rc != XTC_E_AGAIN) abort();
    uint32_t want = xtc_tls_wants_read(tls)
                  ? XTC_IO_READABLE : XTC_IO_WRITABLE;
    xtc_proc_wait_fd(fd, want, -1, &revents);
}

() and () follow the same pattern: return XTC_OK on progress, XTC_E_AGAIN when the fd needs more poll iterations.

() performs the bidirectional close-notify dance; same WANT_* translation.

xtc_tls_handshake(), xtc_tls_read(), xtc_tls_write() and xtc_tls_shutdown() return:

Progress. From xtc_tls_read(), *out_n == 0 is a clean EOF: the peer sent close_notify.
Wait for the direction reported by xtc_tls_wants_read() / xtc_tls_wants_write() and call again.
The peer or the transport failed: the connection closed without close_notify (a truncation), was reset, or a send/recv failed. Tear the connection down.
Any other hard TLS error (a protocol violation, a failed certificate verification, a non-TLS peer). Tear the connection down.

An error is confined to the connection that produced it. Every backend clears its per-thread error state (OpenSSL's error queue, wolfSSL's compat queue) before each of these calls and again after a failure, so a bad peer on one connection cannot make a later call on another connection served by the same thread fail. This also holds in custom-transport mode when the callbacks yield the fiber in the middle of a call (see SNI AND CUSTOM TRANSPORT).

The library applies no handshake or idle timeout of its own: the caller drives every step, so it owns the clock. Bound the handshake with an absolute deadline and pass the time remaining to each wait; the same shape bounds an idle read. A peer that stalls mid-handshake (slowloris) then costs at most the deadline:

int64_t deadline = xtc_clock_mono() + 5000000000LL;   /* 5 s */
for (;;) {
    int64_t left;
    uint32_t rev = 0;
    int rc = xtc_tls_handshake(tls);
    if (rc == XTC_OK) break;
    if (rc != XTC_E_AGAIN) goto drop;
    left = deadline - xtc_clock_mono();
    if (left <= 0) goto drop;                 /* handshake too slow */
    rc = xtc_proc_wait_fd(fd, xtc_tls_wants_read(tls)
                          ? XTC_IO_READABLE : XTC_IO_WRITABLE,
                          left, &rev);
    if (rc != XTC_OK) goto drop;   /* XTC_E_AGAIN = deadline hit */
}

() is virtualised under the simulator, so the deadline replays.

xtc_tls_opts_t fields:

cert_file, key_file
PEM paths for server-side certificate.
ca_file
PEM CA bundle for verifying the peer. When NULL, a verifying client uses the platform default trust store (OpenSSL default verify paths, GnuTLS system trust, wolfSSL system CA certs when built with WOLFSSL_SYS_CA_CERTS). mbedTLS has no platform store: a verifying client without ca_file fails xtc_tls_ctx_create() with XTC_E_INVAL. A server never trusts the platform store for client certificates.
verify_peer
Legacy: non-zero means XTC_TLS_VERIFY_REQUIRE; 0 means the role default.
verify_peer_mode
Tri-state peer verification, taking precedence over verify_peer when not XTC_TLS_VERIFY_DEFAULT: XTC_TLS_VERIFY_NONE, XTC_TLS_VERIFY_REQUEST (a server requests a client certificate, completes the handshake if none is presented, and fails it if one is presented that does not verify; a client treats it as REQUIRE), or XTC_TLS_VERIFY_REQUIRE. When both are unset the role default applies: a client verifies the server (XTC_TLS_VERIFY_REQUIRE), a server does not ask for a client certificate (XTC_TLS_VERIFY_NONE).

: Since 1.50 a CLIENT verifies the server by default; set verify_peer_mode = XTC_TLS_VERIFY_NONE to opt out. Before 1.50 a zeroed (or NULL) opts accepted any server certificate, and the GnuTLS, wolfSSL and mbedTLS backends ignored verify_peer_mode.

alpn_protos
ALPN wire-form list.
min_version, max_version
or XTC_TLS_VER_13.
cipher_list, ciphersuites_13, groups
TLS 1.2 cipher list, TLS 1.3 ciphersuites, and key-exchange groups.
crl_file, crl_dir
CRL file / hashed directory; either enables full-chain revocation checking.
prefer_server_ciphers
Honor the server's cipher order over the client's.
passphrase_cb, passphrase_userdata
Supply the passphrase for an encrypted key_file without an interactive prompt.

A server context is hardened by default (documented defaults, not knobs): TLS renegotiation and compression are disabled, session tickets and the session cache are off, and the moving-write-buffer mode required for correct non-blocking sends is enabled.

(ctx, cb, userdata) installs a Server Name Indication selection callback on a XTC_TLS_SERVER context. During the handshake, after the ClientHello is parsed, the callback fires with the requested server_name (or NULL if the client sent none) and may return a DIFFERENT, pre-created server xtc_tls_ctx_t whose certificate, key, CA, and verify mode are used for the rest of that handshake -- the multi-tenant / multi-host TLS case (mirrors OpenSSL's () plus ()). Returning NULL keeps the current context. Registering on a client context, or on a backend that cannot swap mid-handshake, returns XTC_E_NOSYS.

(tls, name) is the client-side half: called before the handshake on a client connection, it sends name in the ClientHello SNI extension and enables RFC 6125 hostname verification against the server certificate: a certificate that chains to a trusted CA but is issued for a different host fails the handshake. The name is enforced only when the context verifies peers. A NULL or empty name clears it (chain-only verification); it is a no-op on a server connection. BoringSSL cannot remove a name once set: there, clearing a connection that already has one returns XTC_E_NOSYS and the old name stays enforced (use a fresh connection); clearing one that never had a name succeeds. Implemented on the OpenSSL, GnuTLS, wolfSSL and mbedTLS backends; before 1.50 only OpenSSL checked the name and the others returned XTC_E_NOSYS, which SChannel still does.

(ctx, transport, &out) wraps the TLS state machine around caller-supplied read_cb and write_cb callbacks instead of an fd, so the consumer owns the underlying recv/send -- to feed bytes already read off the socket before deciding to negotiate TLS (pushback), to weave the I/O into its own interrupt / cancellation / fiber-yield discipline, or to handle a platform signal window. The callbacks are BIO-like: return the byte count, 0 for a clean EOF, or XTC_E_AGAIN to signal would-block (the caller then drives its own readiness and retries the TLS op). A callback may yield the fiber (for example in ()) while other connections run on the same thread: the connection's OpenSSL error-queue entries are saved before the callback and restored after it, and anything another connection queued meanwhile is dropped, so a failure on one connection is never reported on another. A callback must not call xtc_tls_* on the connection it is serving. These are OpenSSL-backend features; other backends return XTC_E_NOSYS.

Valid only after () returns XTC_OK. () and () return the negotiated protocol version and cipher name (or NULL); () the symmetric key strength; () the negotiated ALPN protocol. () reports whether the peer presented a certificate that passed verification; () reports the most recent peer-certificate verification result for diagnostics: the backend verify code (OpenSSL X509_V_*, X509_V_OK == 0 on success) into *x509_err and a human-readable reason string into buf; (), (), (), and () read the peer certificate (RFC 2253 DN, commonName, issuer DN, decimal serial), rejecting an embedded NUL with XTC_E_INVAL. () returns the RFC 5929 hash of the local certificate for SCRAM channel binding (MD5/SHA-1 signatures are hashed with SHA-256, otherwise the digest matching the certificate's signature algorithm). A backend that has not implemented these returns NULL, 0, or XTC_E_NOSYS.

int xtc_tls_enable_ktls(xtc_tls_t *tls) int xtc_tls_ktls_active(const xtc_tls_t *tls) int xtc_tls_sendfile(xtc_tls_t *tls, int fd, int64_t off, size_t len, size_t *out_n)

By default every TLS byte is encrypted or decrypted in user space and copied between the caller's buffer, the backend's record buffer and the socket. () opts one fd-mode connection into kernel TLS (Linux tls.ko, FreeBSD KTLS): after the handshake the backend hands the session keys to the socket, so () and xtc_tls_read() pass plaintext straight to and from the kernel, and xtc_tls_sendfile() sends len bytes of file fd from offset off encrypted by the kernel without the file ever entering user space. Call it after xtc_tls_create() and before the first xtc_tls_handshake(). It returns XTC_OK when the request is armed; whether the kernel takes over is decided at the end of the handshake (module loaded, negotiated cipher: AES-GCM or ChaCha20-Poly1305 on Linux) and reported by () as XTC_TLS_KTLS_TX and/or XTC_TLS_KTLS_RX (0 = user space, or tls is NULL). A connection the kernel declines stays a fully working user-space TLS connection. xtc_tls_enable_ktls() returns XTC_E_NOSYS when the backend or library build has no kernel TLS (only OpenSSL implements it) or the connection uses a custom transport, and XTC_E_INVAL when tls is NULL or the handshake has started.

() returns XTC_OK with *out_n set to the bytes sent (possibly fewer than len, like (); loop on it), XTC_E_AGAIN with () set when the socket is full, XTC_E_NOSYS when kernel TX is not active (fall back to reading the file and xtc_tls_write()), and XTC_E_IO or XTC_E_INTERNAL like xtc_tls_write().

A kernel-TLS connection keeps fd mode's no-SIGPIPE guarantee: the socket gets SO_NOSIGPIPE where the OS has it; elsewhere SIGPIPE is held off on the calling thread for each TLS call, and a write the kernel accepted but could not deliver because the peer has gone returns XTC_E_IO (the kernel itself reports the byte count). Close the socket with xtc_net_close(3): on Linux, close(2) of a kernel-TLS socket whose peer has gone can raise SIGPIPE while the kernel flushes a queued record, and () holds it off; a raw close(2) does not.

xtc_io(3), xtc_net(3), xtc_proc(3), xtc(7)

Appeared in xtc 0.1. See docs/M_TLS.md for backend pluggability planning.

May 28, 2026 Debian

View the mdoc source