xtc_tls(3)
---xtc_tls(3)
TLS over async sockets
| XTC_TLS(3) | Library Functions Manual | XTC_TLS(3) |
NAME
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
SYNOPSIS
DESCRIPTION
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
xtc_tls_ctx_create() with role (XTC_TLS_SERVER orXTC_TLS_CLIENT) plus opts. - xtc_tls_t
- Per-connection state. Wraps a single fd. Created via
xtc_tls_create(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);
}
xtc_tls_read()
and
xtc_tls_write()
follow the same pattern: return XTC_OK on progress,
XTC_E_AGAIN when the fd needs more poll
iterations.
xtc_tls_shutdown()
performs the bidirectional close-notify dance; same WANT_* translation.
RETURN VALUES
xtc_tls_handshake(),
xtc_tls_read(),
xtc_tls_write() and
xtc_tls_shutdown() return:
XTC_OK- Progress. From
xtc_tls_read(), *out_n == 0 is a clean EOF: the peer sent close_notify. XTC_E_AGAIN- Wait for the direction reported by
xtc_tls_wants_read() /xtc_tls_wants_write() and call again. XTC_E_IO- 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.
XTC_E_INTERNAL- 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).
DEADLINES
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 */
}
xtc_clock_mono()
is virtualised under the simulator, so the deadline replays.
OPTIONS
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 withWOLFSSL_SYS_CA_CERTS). mbedTLS has no platform store: a verifying client without ca_file failsxtc_tls_ctx_create() withXTC_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), orXTC_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).Behavior change: Since 1.50 a CLIENT verifies the server by default; set verify_peer_mode =
XTC_TLS_VERIFY_NONEto opt out. Before 1.50 a zeroed (orNULL) 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
XTC_TLS_VER_12orXTC_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.
SNI AND CUSTOM TRANSPORT
xtc_tls_ctx_set_sni_cb(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
SSL_CTX_set_client_hello_cb()
plus
SSL_set_SSL_CTX()).
Returning NULL keeps the current context. Registering on a client context,
or on a backend that cannot swap mid-handshake, returns
XTC_E_NOSYS.
xtc_tls_set_hostname(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.
xtc_tls_create_transport(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
xtc_proc_wait_fd())
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.
INTROSPECTION
Valid only after
xtc_tls_handshake()
returns XTC_OK.
xtc_tls_get_version()
and
xtc_tls_get_cipher()
return the negotiated protocol version and cipher name (or
NULL);
xtc_tls_get_cipher_bits()
the symmetric key strength;
xtc_tls_get_alpn_selected()
the negotiated ALPN protocol.
xtc_tls_has_peer_cert()
reports whether the peer presented a certificate that passed verification;
xtc_tls_get_verify_error()
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;
xtc_tls_get_peer_subject_dn(),
xtc_tls_get_peer_common_name(),
xtc_tls_get_peer_issuer_dn(),
and
xtc_tls_get_peer_serial()
read the peer certificate (RFC 2253 DN, commonName, issuer DN, decimal
serial), rejecting an embedded NUL with XTC_E_INVAL.
xtc_tls_get_server_cert_hash()
returns the RFC 5929
tls-server-end-point
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.
KERNEL TLS
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.
xtc_tls_enable_ktls()
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
xtc_tls_write()
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
xtc_tls_ktls_active()
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.
xtc_tls_sendfile()
returns XTC_OK with *out_n set
to the bytes sent (possibly fewer than len, like
xtc_tls_write();
loop on it), XTC_E_AGAIN with
xtc_tls_wants_write()
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
xtc_net_close()
holds it off; a raw close(2) does not.
SEE ALSO
HISTORY
Appeared in xtc 0.1. See docs/M_TLS.md for backend pluggability planning.
| May 28, 2026 | Debian |