# flare.tls.rustls_quic

> A module of flare_mlake's API.

`flare.tls.rustls_quic` -- rustls QUIC binding surface.

QUIC's TLS shape is fundamentally different from TLS-over-TCP:
the handshake runs *inside* QUIC frames, keys are derived per
encryption level (Initial / Handshake / 1-RTT / 0-RTT), and the
API the TLS library exposes is record-shaped rather than
byte-stream-shaped. See [`docs/tls-strategy.md`](../../docs/tls-strategy.md)
for the full rationale on why flare's QUIC path uses rustls
instead of extending the OpenSSL FFI for QUIC: the BoringSSL-
shape QUIC API is what the broader ecosystem (quiche, ngtcp2,
lsquic, msquic) standardized on, and `rustls` carries that API
natively (`rustls::quic::ServerConnection`).

## Module surface

This module declares:

- :class:`RustlsQuicConfig` -- server-side acceptor config
  (certificate chain, private key, ALPN list). Owns the
  configuration the Rust crate reads through the C ABI.
- :class:`RustlsQuicAcceptor` -- factory for per-connection
  TLS sessions. Conceptually parallel to
  :class:`flare.tls.acceptor.TlsAcceptor`, but produces QUIC
  sessions rather than TCP TLS streams.
- :class:`RustlsQuicSession` -- per-connection rustls handle.
  The QUIC reactor feeds it CRYPTO-frame bytes per
  encryption level and pulls handshake output bytes plus
  derived keys back out.
- :class:`RustlsQuicError` -- typed error carrier for the
  cases the reactor must distinguish (handshake-incomplete,
  protocol violation, certificate rejected, internal error).

Both ``RustlsQuicAcceptor`` and ``RustlsQuicSession`` are
``Movable`` (not ``Copyable``) because they own a Rust-allocated
``Box<Acceptor>`` / ``Box<Session>`` respectively; copy would
double-free on drop. The carriers route every FFI call through
``read lib`` borrow helpers (see
``flare/tls/_rustls_quic_ffi.mojo``) so Mojo's ASAP destructor
cannot unmap ``libflare_rustls_quic.so`` between
``get_function`` and the call.

References:
- RFC 9001 "Using TLS to Secure QUIC".
- RFC 8446 "The Transport Layer Security (TLS) Protocol Version 1.3".
- BoringSSL QUIC API conventions (the shape rustls implements).

## Structs

### `QuicEncryptionLevel`

```mojo
struct QuicEncryptionLevel
```

RFC 9001 §4.1 packet protection levels.

Each level has its own set of secret-keyed AEAD keys derived
by the TLS handshake. The rustls QUIC binding emits CRYPTO
frames at one level at a time and returns the derived keys
when each level transitions into "ready".

Implements: `Movable`

#### `INITIAL`

```mojo
comptime INITIAL = Int(0)
```

RFC 9001 §4.1 -- keyed by the QUIC v1 initial salt mixed with the client's Destination Connection ID. Used for the first client-hello flight.

#### `EARLY_DATA`

```mojo
comptime EARLY_DATA = Int(1)
```

RFC 9001 §4.1 -- 0-RTT keys. Captured from rustls via :meth:`RustlsQuicSession.install_early_keys` (which wraps ``Connection::zero_rtt_keys()``) rather than the per-level ``KeyChange`` pump; the AEAD + header-protection thunks accept this level for the 0-RTT send/recv path on a resumed connection.

#### `HANDSHAKE`

```mojo
comptime HANDSHAKE = Int(2)
```

RFC 9001 §4.1 -- handshake keys derived after the server accepts the client's Initial. Used for ServerHello and EncryptedExtensions.

#### `APPLICATION`

```mojo
comptime APPLICATION = Int(3)
```

RFC 9001 §4.1 -- 1-RTT keys. Used for all post-handshake application traffic; this is the keyed level the H3 server will see for every request.

### `RustlsQuicConfig`

```mojo
struct RustlsQuicConfig
```

Server-side rustls QUIC configuration carrier.

Mirrors the shape of :class:`flare.tls.config.TlsConfig` but
targets the rustls QUIC backend. Fields are owned by Mojo;
the Rust crate reads them through the C ABI at acceptor
construction time and never mutates them.

The actual configuration that rustls would consume is built
inside :class:`RustlsQuicAcceptor.__init__` -- this struct
is the inputs.

Implements: `Copyable`, `Defaultable`, `Movable`

Fields:

- `cert_chain_pem: String`: PEM-encoded server certificate chain (leaf cert plus any intermediates). Empty string is invalid; the Rust crate will reject construction.
- `private_key_pem: String`: PEM-encoded server private key (PKCS#8). RFC 9001 §4.6 says only TLS 1.3 is supported for QUIC; the Rust crate will reject non-TLS-1.3-compatible keys.
- `alpn_protocols: List[String]`: ALPN protocol identifiers the server is willing to negotiate (RFC 7301). For HTTP/3 this should include ``"h3"`` (RFC 9114 §3.1). Order matters -- earlier entries are preferred.
- `max_early_data_size: UInt32`: Maximum 0-RTT data the server will accept. Set to 0 to disable 0-RTT (the default). A non-zero value switches the server to STATEFUL resumption (an in-memory session cache, not a stateless ticket) because RFC 8446 sec 8.1 forbids 0-RTT with stateless tickets; rustls then advertises early data in the issued NewSessionTicket. 0-RTT replay protection beyond the rustls single-use window is out of scope for this cycle; see RFC 9001 sec 9.2.
- `session_resumption_enabled: Bool`: Whether to issue NewSessionTicket frames for session resumption. Default is True for production parity with OpenSSL acceptor.

#### `__init__`

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

### `RustlsQuicError`

```mojo
struct RustlsQuicError
```

Typed error carrier for the rustls QUIC binding.

The reactor distinguishes these cases for connection-close
reason mapping (RFC 9000 §10.2 -- CONNECTION_CLOSE frame
types and reasons). String reason is for logs only.

Implements: `Copyable`, `Movable`

Fields:

- `kind: Int`: One of the :class:`RustlsQuicErrorKind` codepoints.
- `reason: String`: Human-readable reason string for logs and the CONNECTION_CLOSE reason phrase.

#### `__init__`

```mojo
def __init__(out self, kind: Int, reason: String)
```

#### `not_built`

```mojo
def not_built() -> Self
```

The Rust crate is not built; the reactor should treat this as a configuration error (not a per-packet failure). The build_rustls.sh activation script wired into pixi's [activation.scripts] auto-builds the crate on `pixi install`; this carrier exists for the rare path where activation didn't run (e.g. a bare mojo invocation outside a pixi shell).

### `RustlsQuicErrorKind`

```mojo
struct RustlsQuicErrorKind
```

RFC 9000 §20.2 + RFC 9001 §4.8 cryptographic-error enumeration plus the local "not built" sentinel.

Implements: `Movable`

#### `NOT_BUILT`

```mojo
comptime NOT_BUILT = Int(0)
```

The Rust crate is not built. Returned by the constructor when the FFI symbol lookup fails (the activation script didn't run); resolved by re-running `pixi install`.

#### `HANDSHAKE_INCOMPLETE`

```mojo
comptime HANDSHAKE_INCOMPLETE = Int(1)
```

The session needs more CRYPTO frame bytes before it can advance. Reactor should keep feeding bytes; not a real error from the connection's perspective.

#### `PROTOCOL_VIOLATION`

```mojo
comptime PROTOCOL_VIOLATION = Int(2)
```

The peer violated the TLS 1.3 wire grammar or the QUIC transport-parameter encoding. Maps to PROTOCOL_VIOLATION (0x0a) in CONNECTION_CLOSE.

#### `CERTIFICATE_INVALID`

```mojo
comptime CERTIFICATE_INVALID = Int(3)
```

The server's certificate chain failed validation (only meaningful for client-side mTLS, which is the v0.10 line item -- this exists to keep the enum complete).

#### `INTERNAL_ERROR`

```mojo
comptime INTERNAL_ERROR = Int(4)
```

An internal Rust panic crossed the FFI boundary, or the C ABI returned an unexpected return code. Maps to INTERNAL_ERROR (0x01) in CONNECTION_CLOSE.

### `RustlsQuicAcceptor`

```mojo
struct RustlsQuicAcceptor
```

Factory for per-connection rustls QUIC sessions.

Long-lived. One instance per QUIC listener, shared across
every connection it accepts. The actual rustls
``rustls::quic::ServerConfig`` lives behind a heap-allocated
Rust ``Box<Acceptor>``; this carrier holds the raw pointer
(as ``Int``) plus the loaded ``libflare_rustls_quic.so``
handle.

Constructed lifecycle:

- ``__init__`` calls ``flare_rustls_quic_acceptor_new`` with
  the PEM cert + key + wire-format ALPN list. Returns a
  carrier whose ``_opaque_handle`` is 0 when the FFI
  rejects the input (e.g. empty / malformed PEM). The
  reactor and per-connection ``accept()`` path treat a 0
  handle as a configuration error and bounce every
  connection immediately, surfacing the rustls last-error
  message via ``RustlsQuicError``.
- ``__deinit__`` calls ``flare_rustls_quic_acceptor_free``
  to release the Rust-side ``Box<Acceptor>`` (no-op on a
  0 handle).
- ``accept(dcid)`` calls ``flare_rustls_quic_accept`` to
  construct a fresh per-connection
  ``rustls::quic::ServerConnection`` and wraps it in
  :class:`RustlsQuicSession`.

Implements: `Movable`

Fields:

- `config: RustlsQuicConfig`

#### `__init__`

```mojo
def __init__(out self, var config: RustlsQuicConfig)
```

Construct an acceptor from the supplied config.

Does not raise on FFI rejection: the carrier always
constructs (with ``_opaque_handle == 0`` on failure)
so the reactor can surface the rustls last-error
message via :class:`RustlsQuicError` rather than a
partial-construction half-state.

#### `__deinit__`

```mojo
def __deinit__(deinit self)
```

#### `free_session`

```mojo
def free_session(self, handle: Int)
```

Release a per-connection rustls session previously produced by :func:`flare.tls._rustls_quic_ffi._do_accept` through the acceptor's pinned library handle.

NULL is a no-op. Used by :class:`flare.quic.server.QuicListener`
to drop every session in its slab from inside the
listener's destructor without sub-field access on
``self.tls_acceptor._lib`` (which Mojo's ``deinit``
ordering rule forbids).

#### `accept`

```mojo
def accept(self, dst_cid: List[UInt8]) -> RustlsQuicSession
```

Create a new per-connection session bound to the client's Destination Connection ID.

The reactor calls this once per connection after parsing
the first Initial packet. The DCID is required because
the rustls binding uses it (via the QUIC transport
parameters extension) to bind initial-secret derivation
to the per-connection identity (RFC 9001 §5.2).

### `RustlsQuicConnector`

```mojo
struct RustlsQuicConnector
```

Client-role factory for per-connection rustls QUIC sessions.

The mirror of :class:`RustlsQuicAcceptor`: long-lived, one
instance per HttpClient h3 origin policy (trust roots + ALPN),
reused across every QUIC connection the client opens. Wraps a
heap-allocated Rust ``Box<Connector>`` (the rustls
``ClientConfig``) behind a raw pointer carried as ``Int`` plus
the pinned ``libflare_rustls_quic.so`` handle.

Lifecycle mirrors the acceptor: ``__init__`` ->
``flare_rustls_quic_connector_new`` (0 handle on PEM/ALPN
failure, surfaced by :meth:`connect`); ``__deinit__`` ->
``flare_rustls_quic_connector_free``; :meth:`connect` ->
``flare_rustls_quic_connect`` returning a
:class:`RustlsQuicSession` (role-agnostic on the Mojo side).

Implements: `Movable`

Fields:

- `alpn_protocols: List[String]`: ALPN identifiers advertised to the origin (``"h3"`` for HTTP/3). Order is preference order, as the acceptor's list.

#### `__init__`

```mojo
def __init__(out self, ca_pem: String, var alpn_protocols: List[String])
```

Build a connector from a trust-anchor PEM bundle + ALPN list.

``ca_pem`` is a PEM bundle of trusted roots; for a loopback
link to flare's own server, pass that server's self-signed
cert (a self-signed leaf is its own root). Does not raise on
PEM-parse failure -- the carrier constructs with a 0 handle
and :meth:`connect` surfaces the rustls last-error -- so a
configuration mistake is one clear error at connect time, not
a partial-construction half-state. Raises only on an invalid
ALPN list (empty or >255-byte protocol).

```mojo
def __init__(out self, var alpn_protocols: List[String], use_system_roots: Bool)
```

Build a connector trusting the operating system's CA bundle (loaded at runtime via rustls-native-certs) instead of a PEM. ``use_system_roots`` distinguishes this overload from the PEM constructor. Mirrors the PEM path: a 0 handle (no native roots found / none usable) is surfaced at :meth:`connect` time, not at construction. Raises only on an invalid ALPN list.

#### `__deinit__`

```mojo
def __deinit__(deinit self)
```

#### `with_system_roots`

```mojo
def with_system_roots(var alpn_protocols: List[String]) -> Self
```

Build a connector trusting the OS CA bundle (rustls-native-certs) -- the public-internet h3 client path where there is no PEM to ship. Equivalent to the ``use_system_roots=True`` constructor; provided as a named factory for an ergonomic call site.

#### `free_session`

```mojo
def free_session(self, handle: Int)
```

Release a per-connection session through the connector's pinned library handle (mirror of :meth:`RustlsQuicAcceptor.free_session`). NULL is a no-op.

#### `connect`

```mojo
def connect(self, server_name: String, transport_params: List[UInt8] = List()) -> RustlsQuicSession
```

Open a client-role session against ``server_name`` (SNI).

``transport_params`` is the client's encoded QUIC transport
parameters (empty is accepted for the handshake-only path;
the QUIC client driver fills the real blob). The
returned :class:`RustlsQuicSession` drives the client
handshake through the same feed/take CRYPTO + AEAD + header
thunks the server session uses; the first
``take_crypto(INITIAL)`` drains the ClientHello.

### `RustlsQuicSession`

```mojo
struct RustlsQuicSession
```

Per-connection rustls handle.

The reactor's per-connection state machine drives this:

1. Feed inbound CRYPTO frame bytes via :meth:`feed_crypto`.
2. Pull outbound CRYPTO frame bytes via :meth:`take_crypto`.
3. :meth:`is_handshake_complete` returns True once the
   1-RTT keys are derived; from there the application can
   send data on streams.
4. :meth:`selected_alpn` returns the negotiated ALPN
   identifier (e.g. ``"h3"``) so the reactor can dispatch
   to the right application-layer driver.

Construction:

- Direct ``RustlsQuicSession(dcid)`` builds a carrier with
  a 0 handle; every FFI-touching method then raises with a
  clear "NULL session" message. Tests use this shape to
  exercise the failure path without needing a real
  acceptor.
- The production path uses
  :meth:`RustlsQuicAcceptor.accept` which calls into the
  ``RustlsQuicSession._wrap`` private constructor with a
  real Rust-side ``Box<Session>*``.

Implements: `Movable`

Fields:

- `dst_cid: List[UInt8]`: The DCID this session was created for. Carried so the reactor can sanity-check key derivation later.

#### `__init__`

```mojo
def __init__(out self, dst_cid: List[UInt8])
```

Build a session carrier with a NULL handle. Every FFI-touching method raises on a NULL handle.

Useful for testing the level machine + DCID round-trip
without a real acceptor; the production path uses
:meth:`RustlsQuicAcceptor.accept`.

```mojo
def __init__(out self, var lib: OwnedDLHandle, handle: Int, var dst_cid: List[UInt8])
```

Internal: wrap a real Rust-side ``Box<Session>*`` that :meth:`RustlsQuicAcceptor.accept` just produced.

Each session gets its own ``OwnedDLHandle`` so the .so
refcount stays high until the session drops; on Linux the
``-z nodelete`` link flag is the additional
belt-and-suspenders pin.

#### `__deinit__`

```mojo
def __deinit__(deinit self)
```

#### `feed_crypto`

```mojo
def feed_crypto(mut self, level: Int, data: List[UInt8])
```

Feed inbound CRYPTO frame bytes at ``level``.

The reactor calls this after dispatching a CRYPTO frame
out of a packet at the matching encryption level. The
``data`` buffer is a contiguous chunk; the rustls side
reassembles fragments internally.

#### `take_crypto`

```mojo
def take_crypto(self, level: Int) -> List[UInt8]
```

Drain pending outbound CRYPTO frame bytes at ``level``.

Returns an empty list when no bytes are pending. The
reactor packages the result into CRYPTO frames inside
packets at the matching encryption level.

#### `is_handshake_complete`

```mojo
def is_handshake_complete(self) -> Bool
```

Whether the 1-RTT keys are derived. Returns False on a NULL session (test path) or while handshaking; True after rustls flips into the application-keyed state.

#### `selected_alpn`

```mojo
def selected_alpn(self) -> String
```

ALPN identifier the rustls side picked.

Returns the negotiated identifier from the ALPN list
passed at config time (e.g. ``"h3"``). The reactor uses
this to dispatch to the H3 server vs an alternative
application protocol over QUIC.

#### `peer_transport_params`

```mojo
def peer_transport_params(self) -> List[UInt8]
```

The peer's raw ``quic_transport_parameters`` extension bytes (RFC 9000 §7.4), or an empty list if rustls has not yet surfaced them.

rustls only exposes these once the carrying handshake flight
has been processed (the server's params reach the client
after EncryptedExtensions; the client's reach the server
after the ClientHello). Poll after the handshake completes
for a stable result, then decode with
:func:`flare.quic.transport_params.decode_transport_parameters`
to apply the peer's flow-control and CID limits. Returns an
empty list on a NULL session (test path).

#### `current_level`

```mojo
def current_level(self) -> Int
```

Current outbound encryption level. Useful for tests confirming the level machine compiles.

#### `have_keys`

```mojo
def have_keys(self, level: Int) -> Bool
```

Whether rustls has installed per-level keys at the given encryption level (2 = Handshake, 3 = 1-RTT).

Initial-level keys never flow through rustls's
``KeyChange`` (they derive from the connection ID per
RFC 9001 §5.2 and the flare side runs Initial AEAD
through :class:`flare.quic.crypto.OpenSslQuicCrypto`),
so this returns False for level 0. The reactor polls
:meth:`have_keys` after every :meth:`take_crypto` pump to
learn when to install Handshake / 1-RTT keys onto the
owning :class:`flare.quic.server.QuicConnection`.

NULL session handles (the test-only constructor path)
return False without raising.

#### `install_early_keys`

```mojo
def install_early_keys(self) -> Bool
```

Capture rustls's 0-RTT (EarlyData) keys into the session, if available (RFC 9001 §4.1; wraps ``Connection::zero_rtt_keys()``).

On the client this returns True right after
:meth:`RustlsQuicConnector.connect` IFF the connector's
session store held a ticket for the SNI (a resumed
connection) and early data is enabled; on the server it
returns True once the resumed ClientHello has been fed and
0-RTT was accepted. After True, the
:data:`QuicEncryptionLevel.EARLY_DATA` AEAD + header
thunks encrypt/decrypt 0-RTT packets. Returns False on a
NULL session (test path) or when rustls has no 0-RTT keys
for this connection (the common first-flight case).

#### `is_early_data_accepted`

```mojo
def is_early_data_accepted(self) -> Bool
```

Whether the server signalled it will process the client's 0-RTT data (RFC 8446 §4.2.10; client-role only).

A False after the handshake completes means the server
rejected early data and the client must replay it in 1-RTT.
Returns False on a NULL session or for the server role.

#### `packet_encrypt`

```mojo
def packet_encrypt(self, level: Int, packet_number: UInt64, header: List[UInt8], mut payload: List[UInt8]) -> List[UInt8]
```

Encrypt ``payload`` in place at ``level`` via rustls's ``Keys.local.packet.encrypt_in_place`` (RFC 9001 §5.3).

``header`` is the QUIC packet header (used as AEAD AAD);
the returned tag is the 16-byte authentication tag the
caller appends to the on-wire packet. Raises if the FFI
rejects (e.g. keys not yet installed at ``level``); the
rustls error text is in :func:`_do_last_error`.

#### `packet_decrypt`

```mojo
def packet_decrypt(self, level: Int, packet_number: UInt64, header: List[UInt8], mut payload: List[UInt8]) -> Int
```

Verify + strip the AEAD tag from ``payload`` in place at ``level`` via rustls's ``Keys.remote.packet.decrypt_in_place`` (RFC 9001 §5.3).

Returns the plaintext length (always ``len(payload) - 16``
for the AEAD-GCM / ChaCha20-Poly1305 suites rustls speaks);
raises if rustls rejects the tag (the typical wrong-keys-
at-this-level symptom).

#### `packet_decrypt_1rtt`

```mojo
def packet_decrypt_1rtt(self, key_phase: Int, packet_number: UInt64, header: List[UInt8], mut payload: List[UInt8]) -> Int
```

Open a 1-RTT ``payload`` in place under the header's key phase bit (RFC 9001 sec 6.3). Returns 1 when the peer started a key update and the keys rotated, else 0; raises on failure.

#### `key_update`

```mojo
def key_update(self) -> Int
```

Start a 1-RTT key update (RFC 9001 sec 6.1); returns the new key phase bit.

#### `header_encrypt`

```mojo
def header_encrypt(self, level: Int, sample: List[UInt8], first_byte_addr: Int, pn_addr: Int, pn_len: Int)
```

Apply QUIC header protection (RFC 9001 §5.4) to the packet's first byte + packet-number bytes via rustls's ``Keys.local.header.encrypt_in_place``.

``sample`` MUST be a 16-byte slice of the encrypted
payload (4 bytes past the start of the packet-number
field). ``first_byte_addr`` / ``pn_addr`` are raw pointer
values (as Int) so the caller can keep the byte cells on
the stack and let rustls write through.

#### `header_decrypt`

```mojo
def header_decrypt(self, level: Int, sample: List[UInt8], first_byte_addr: Int, pn_addr: Int, pn_len: Int)
```

Remove QUIC header protection (RFC 9001 §5.4) from the packet's first byte + packet-number bytes via rustls's ``Keys.remote.header.decrypt_in_place``. Same sample contract as :meth:`header_encrypt`.
