flare_mlake · API · tls

flare.tls.acceptor

Server-side TLS acceptor.

TlsAcceptor is the server-side counterpart to TlsStream — it wraps a TcpListener and produces TlsStream connections after completing the TLS handshake against a server certificate chain. TlsServerConfig carries the acceptor's policy: cert / key paths, ALPN protocols to advertise, optional CA bundle for mTLS client-cert verification.

This commit ships the type infrastructure: TlsServerConfig, TlsAcceptor shell, TlsInfo value type, TlsServerError hierarchy, plus the public re-exports through flare.tls and the root flare package. The reactor-side handshake state machine — non-blocking SSL_accept driven by edge-triggered readable / writable events — is a focused follow-up that lands once the OpenSSL SSL_CTX_* server-side surface is wired into the existing flare/tls/ffi/openssl_wrapper.cpp.

Why split: the C-side handshake state machine is ~150 lines of SSL_accept + SSL_get_error + BIO plumbing, plus matching reactor surgery. The API surface here lets per-request TlsInfo plumbing, cert reload, and mTLS all plug into the public surface; the actual cipher-on-the-wire bits land together in a single follow-up.

Public API:

from flare.tls import (
    TlsServerConfig, TlsAcceptor, TlsInfo,
    TlsServerError, TlsServerNotImplemented,
)

var cfg = TlsServerConfig(
    cert_file="/etc/letsencrypt/live/example.com/fullchain.pem",
    key_file="/etc/letsencrypt/live/example.com/privkey.pem",
    alpn=["h2", "http/1.1"], # served preference order
    require_client_cert=False, # mTLS off by default
    client_ca_bundle="",
)
var acceptor = TlsAcceptor.bind(addr, cfg)
# acceptor.serve(handler) — flips on with the reactor
# follow-up that lands the SSL_accept state machine.

Structs

TlsServerError

struct TlsServerError

Generic server-side TLS failure (handshake, cert load, etc.).

message describes the failure in human-readable form; code carries the underlying OpenSSL error code if available (0 if not).

Implements Copyable, Movable, Writable

Fields
message: String
code: Int

__init__

def __init__(out self, message: String, code: Int = Int(0))

write_to

def write_to[W: Writer](self, mut writer: W)

TlsServerNotImplemented

struct TlsServerNotImplemented

Marker raised to signal that the reactor-side handshake state machine is not available. Distinct type so callers can match on it for graceful degradation and fall back to the blocking handshake_fd path.

Implements Copyable, Movable, Writable

Fields
message: String

__init__

def __init__(out self)

write_to

def write_to[W: Writer](self, mut writer: W)

TlsSniCert

struct TlsSniCert

A certificate served to clients whose SNI matches host (an exact name, or *.example.com for one label).

Implements Copyable, Movable

Fields
host: String
cert_file: String
key_file: String

TlsServerConfig

struct TlsServerConfig

Server-side TLS policy.

Fields: cert_file: Path to the server certificate chain in PEM format (full chain — leaf first, then intermediates). Required. key_file: Path to the server private key in PEM format. Required. alpn: ALPN protocol identifiers to advertise during the handshake. Order is preference order (the OpenSSL callback selects the first intersection with the client's advertised list). Empty = no ALPN. require_client_cert: Whether to require a client certificate (mTLS). Defaults False. When True, client_ca_bundle must also be set. client_ca_bundle: Path to a PEM bundle of trust anchors for verifying client certificates (mTLS). Empty = use OpenSSL's default trust store. min_protocol: Minimum TLS protocol version to negotiate. Default TLS_PROTOCOL_TLS12. TLS 1.0 / 1.1 are explicitly rejected.

Reload semantics: cert_file / key_file are re-read at every call to TlsAcceptor.reload(); the config struct itself is value-copied at acceptor construction time.

Implements Copyable, Movable

Fields
cert_file: String
key_file: String
alpn: List[String]
require_client_cert: Bool
client_ca_bundle: String
min_protocol: Int
enable_session_tickets: Bool: Opt-in; default False. When True the acceptor's SSL_CTX issues RFC 5077 session tickets (TLS 1.2) / RFC 8446 §4.6.1 NewSessionTicket frames (TLS 1.3) so peers can resume on subsequent connects. When False the context sends no tickets and keeps no server session cache, so no session is issued and none can be resumed (every connection is a full handshake). The ticket key lives as long as the SSL_CTX; :meth:TlsAcceptor.reload builds a new context, so it rotates the key and invalidates every ticket and cached session issued before it.
ticket_lifetime_s: Int: Session ticket lifetime in seconds. Default 7200 (two hours). Maps to SSL_CTX_set_timeout and the embedded TLS 1.3 ticket_lifetime field. Production deployments should keep this short and rotate ticket keys via an out-of-band secret-management path.
sni_certs: List[TlsSniCert]: Certificates chosen by SNI; a client whose SNI matches none, or that sends none, gets cert_file.
handshake_timeout_ms: Int: Wall-clock cap (milliseconds) on a single blocking SSL_accept drive in TlsAcceptor.handshake_fd. Default 10_000 (10 s). A slow or stalled client that never completes the handshake is aborted at the deadline so it cannot hold the worker indefinitely (slow-loris-on-handshake DoS bound). Measured against a monotonic clock, not an iteration count, so it stays honest under the libc-sleep multiplier. 0 disables the cap.

__init__

def __init__(out self, cert_file: String, key_file: String, var alpn: List[String] = List(), require_client_cert: Bool = False, client_ca_bundle: String = "", min_protocol: Int = Int(771), enable_session_tickets: Bool = False, ticket_lifetime_s: Int = Int(7200), handshake_timeout_ms: Int = Int(10000), var sni_certs: List[TlsSniCert] = List())

Construct the config; validates inter-field invariants.

Raises Error: When require_client_cert=True is set without a client_ca_bundle (mTLS without trust anchors is meaningless — the verify callback would have nothing to verify against). Prevents the misconfiguration foot-gun by failing at construction time rather than at handshake time.

TlsInfo

struct TlsInfo

Per-connection TLS metadata returned by :meth:TlsAcceptor.handshake_fd.

Available when the connection terminated TLS at flare's TlsAcceptor. Plain-HTTP connections never see this struct (the reactor only constructs it on the TLS handshake path).

Fields: protocol: Negotiated protocol version (e.g. "TLSv1.3"). cipher: Cipher suite name (e.g. "TLS_AES_128_GCM_SHA256"). sni_host: Client-Hello SNI hostname, or empty string if the client didn't send one. alpn_protocol: Negotiated ALPN protocol (e.g. "h2", "http/1.1"), or empty string if ALPN didn't fire. client_cert_subject: Subject DN of the client certificate when mTLS is on; empty string otherwise.

Implements Copyable, Movable

Fields
protocol: String
cipher: String
sni_host: String
alpn_protocol: String
client_cert_subject: String

__init__

def __init__(out self, protocol: String = "", cipher: String = "", sni_host: String = "", alpn_protocol: String = "", client_cert_subject: String = "")

TlsAcceptor

struct TlsAcceptor

Server-side TLS acceptor.

Wraps a TlsServerConfig and a real ServerCtx (the OpenSSL SSL_CTX* produced by C6's FFI). Constructor validates the cert + key + ALPN + mTLS config and raises if any are bad, so configuration errors fail at server-bind time rather than at the first inbound handshake.

Per-connection use:

  1. After listener.accept() returns a TcpStream, call acceptor.handshake(stream) raises -> Tuple[TlsStream, TlsInfo]. The method drives SSL_accept to completion in blocking mode (suitable for the existing serve entry shape; the reactor-side STATE_TLS_HANDSHAKE state machine that uses the same FFI but in a non-blocking reactor loop is a focused follow-up).

Cert reload:

acceptor.reload() re-reads the cert + key from the paths in config and atomically swaps them on the underlying SSL_CTX. In-flight sessions hold the old cert via SSL_use_certificate; new handshakes pick up the new one.

Implements Movable

Fields
config: TlsServerConfig: The acceptor's policy. Mutable via reload() for cert rotation without restart.

__init__

def __init__(out self, var config: TlsServerConfig)

Construct an acceptor.

Loads the cert + key, configures min-protocol + cipher list, sets up ALPN if non-empty, sets up mTLS verification if require_client_cert. Raises on any FFI failure — configuration errors are caught at construction time.

into_ctx

def into_ctx(deinit self) -> ServerCtx

Hand over the configured SSL_CTX (ALPN, mTLS, minimum version, session-ticket policy applied), e.g. to the HTTP reactor, which drives its own non-blocking handshakes.

reload

def reload(mut self)

Re-read the cert + key files from config and swap in a freshly built SSL_CTX.

The replacement context is constructed from config first and only then installed, so a failed reload (unreadable file, cert / key mismatch) raises and leaves the running context untouched. In-flight sessions keep their reference to the old context and finish with the old cert; new handshakes pick up the new one.

A new SSL_CTX has its own random session-ticket keys and an empty session cache, so a reload also rotates the ticket key: a ticket or cached session issued before the reload does not resume after it (the peer falls back to a full handshake). This is what docs/threat-model.md promises.

handshake_fd

def handshake_fd(mut self, fd: Int) -> Tuple[Int, TlsInfo]

Drive the SSL_accept state machine on fd to completion (or fatal error) in blocking-poll mode. Sleeps in 1ms slices between WANT_READ / WANT_WRITE returns.

Returns (ssl_addr, tls_info) — the caller owns the ssl_addr and is responsible for calling server_ssl_free(ctx, ssl_addr) when done. The TlsInfo carries the negotiated ALPN protocol + SNI hostname (live-populated from the OpenSSL handle).

Args
fd: The accepted TCP fd (as returned by TcpListener.accept().raw_fd() or similar).

Raises Error: On fatal handshake failure (cert mismatch, client refused, etc.).

info_placeholder

def info_placeholder(self) -> TlsInfo

Return a default TlsInfo value with empty strings in every field. Kept for API-surface compatibility; production callers go through handshake_fd which returns a live-populated TlsInfo.

Aliases

comptime TLS_PROTOCOL_TLS12 = Int(771)
comptime TLS_PROTOCOL_TLS13 = Int(772)