# flare.tls.acceptor

> A module of flare_mlake's API.

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`

```mojo
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__`

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

#### `write_to`

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

### `TlsServerNotImplemented`

```mojo
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__`

```mojo
def __init__(out self)
```

#### `write_to`

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

### `TlsSniCert`

```mojo
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`

```mojo
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__`

```mojo
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`

```mojo
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__`

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

### `TlsAcceptor`

```mojo
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__`

```mojo
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`

```mojo
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`

```mojo
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`

```mojo
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`

```mojo
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

### `TLS_PROTOCOL_TLS12`

```mojo
comptime TLS_PROTOCOL_TLS12 = Int(771)
```

### `TLS_PROTOCOL_TLS13`

```mojo
comptime TLS_PROTOCOL_TLS13 = Int(772)
```
