flare_mlake · API · tls

flare.tls.stream

TLS stream: encrypts a TcpStream via OpenSSL FFI.

The shared library libflare_tls.so is built automatically on pixi activation via flare/tls/ffi/build.sh and installed to $CONDA_PREFIX/lib/ when using the packaged distribution.

Opaque C pointers (SSL_CTX*, SSL*) are held as Int values since Mojo requires all Pointer type parameters to have an explicit mut parameter which is not inferable for NoneType. Using Int (64-bit on all supported platforms) stores pointer values safely.

Security defaults enforced unconditionally:

  • TLS 1.2 minimum (TLS 1.0 / 1.1 disabled at the protocol level)
  • Forward-secret AEAD cipher suites only (ECDHE + AES-GCM / ChaCha20)
  • Certificate verification REQUIRED (opt-out via TlsConfig.insecure())
  • SNI always sent for hostname targets

Example: ```mojo from flare.tls import TlsStream, TlsConfig

var stream = TlsStream.connect("example.com", 443, TlsConfig())
stream.write_all("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n".as_bytes())
var buf = List[UInt8](capacity=4096)
buf.resize(4096, 0)
var n = stream.read(buf.unsafe_ptr(), len(buf))
```
OwnedDLHandle / ASAP-destruction discipline

Mojo's ASAP destruction policy reclaims an OwnedDLHandle right after its last Mojo-visible use. In a naive var lib = OwnedDLHandle(...); var fn = lib.get_function(...); fn(...) sequence the runtime considers lib dead immediately after get_function returns and runs the destructor (dlclose) before fn is invoked, leaving the cached function pointer dangling into freed memory.

Discipline this file follows: every FFI call goes through a _do_ssl_*(imm lib: OwnedDLHandle, ...) borrow helper that does both get_function and the invocation inside the borrow. Public methods open lib once and pass it through to a chain of helpers; the borrow keeps the dylib mapped across the whole sequence. Same idiom as flare.http.encoding._do_compress / flare.http.middleware._flare_fs_access / flare.tls._server_ffi._do_ssl_ctx_new_server.

Structs

TlsSession

struct TlsSession

Opaque, reusable TLS session handle (RFC 5077 / RFC 8446 §4.6.1) captured from a completed TLS handshake.

Constructed exclusively by :meth:TlsStream.session, which returns the most recent session OpenSSL surfaced via the new_session_cb callback for the underlying SSL_CTX. Pass back to :meth:TlsStream.connect_resumed to skip the expensive part of the handshake on the next connect to the same origin.

This type is Movable but not Copyable — duplicating an SSL_SESSION* would require an explicit SSL_SESSION_up_ref / drop pair which doesn't compose with Mojo's ASAP destruction. Move into the next call site; re-capture per round-trip if you want to share across threads.

The handle owns the underlying SSL_SESSION* via SSL_SESSION_free on drop. Callers don't normally see the raw addr; session_addr() is exposed for advanced FFI use.

Implements Movable

Fields
origin: String: host:port of the connection the session came from, the name that connection verified. :meth:TlsStream.connect_resumed offers the session only to that same origin.

__init__

def __init__(out self, var lib: OwnedDLHandle, addr: Int, var origin: String = "")

__deinit__

def __deinit__(deinit self)

session_addr

def session_addr(self) -> Int

Underlying SSL_SESSION* as an Int. For advanced FFI integration only — most callers should pass the whole :class:TlsSession to :meth:TlsStream.connect_resumed.

TlsStream

struct TlsStream

An encrypted TCP stream using TLS (via OpenSSL FFI).

Wraps a TcpStream with OpenSSL's SSL session. The TLS handshake is performed in connect; all subsequent I/O is routed through OpenSSL.

Opaque C pointers (SSL_CTX*, SSL*) are stored as Int values — the canonical approach in Mojo for FFI-managed handles.

The connection is shut down with a close_notify alert when destroyed or when close() is called explicitly.

This type is Movable but not Copyable — an SSL session cannot be duplicated.

Security defaults: - TLS 1.2 minimum (TLS 1.0 / 1.1 disabled via protocol version + options) - Forward-secret AEAD ciphers only (ECDHE + AES-GCM / ChaCha20) - Certificate verification on by default - SNI always sent for hostname targets

Thread safety: Not thread-safe.

Example:

var stream = TlsStream.connect("httpbin.org", 443, TlsConfig())
stream.write_all("GET /get HTTP/1.1\r\nHost: httpbin.org\r\n\r\n".as_bytes())

Implements Movable, Readable

__init__

def __init__(out self, var tcp: TcpStream, ctx: Int, ssl: Int, var origin: String = "")

Internal constructor — use TlsStream.connect instead.

Args
tcp: Connected TCP stream (fd used by ssl after handshake).
ctx: SSL_CTX* stored as Int.
ssl: SSL* stored as Int (handshake already complete).
origin: host:port the handshake verified, stamped on the sessions :meth:session hands out.

__deinit__

def __deinit__(deinit self)

Send close_notify and free OpenSSL objects (best-effort).

__enter__

def __enter__(var self) -> Self

Transfer ownership of self into the with block.

connect

def connect(host: String, port: UInt16, config: TlsConfig) -> Self

Open a TLS connection to host:port.

Resolves the hostname (IPv4), opens a TcpStream, configures OpenSSL, and performs the TLS handshake.

When config.verify == TlsVerify.NONE, a security warning is printed to stderr on every call (intentional).

Args
host: Hostname or IP string. SNI is derived from this value.
port: Destination TCP port (typically 443 for HTTPS).
config: TLS configuration (verification mode, CA bundle, mTLS).

Raises NetworkError: DNS resolution or TCP connect failure. TlsHandshakeError: Generic TLS handshake failure. CertificateExpired: Server cert has passed its notAfter. CertificateHostnameMismatch: Hostname does not match the cert. CertificateUntrusted: Cert not trusted by any CA in bundle.

connect_timeout

def connect_timeout(host: String, port: UInt16, config: TlsConfig, timeout_ms: Int) -> Self

Connect with TLS, failing after timeout_ms milliseconds.

Uses TcpStream.connect_timeout for the TCP phase. The TLS handshake is bounded separately, by arming SO_RCVTIMEO on the connected socket for its duration: timeout_ms is therefore a bound on each phase, not one budget shared across both. Without it a peer that completes the TCP handshake and then says nothing parks the caller inside SSL_connect forever -- which is what a bound listener that never calls accept produces, since the kernel finishes the handshake out of the backlog.

The option is cleared again once the handshake completes, so a returned stream reads unbounded unless the caller arms its own timeout via :meth:set_recv_timeout.

Args
host: Hostname or IP string.
port: Destination TCP port.
config: TLS configuration.
timeout_ms: Maximum milliseconds for the TCP connect, and again for the TLS handshake that follows it.

Raises ConnectionTimeout: If the deadline expires during TCP. Timeout: If the deadline expires during the TLS handshake. NetworkError: DNS resolution failure. TlsHandshakeError: Generic TLS handshake failure. CertificateExpired: Server cert expired. CertificateHostnameMismatch: Hostname does not match the cert. CertificateUntrusted: Cert not trusted by any CA.

connect_over_tcp

def connect_over_tcp(var tcp: TcpStream, host: String, config: TlsConfig) -> Self

Run the TLS handshake over an already-connected tcp.

Unlike :meth:connect / :meth:connect_timeout (which dial the TCP themselves), this takes an existing stream -- e.g. a proxy CONNECT tunnel -- and performs the client handshake on its fd. host is used as the default SNI + cert-hostname when config.server_name is empty.

read

def read(mut self, buf: Pointer[UInt8], size: Int) -> Int

Decrypt and read up to size bytes into buf.

Returns 0 on clean TLS closure (close_notify received).

Reads through flare_ssl_read_blocking so the retryable case is classified rather than flattened: with SO_RCVTIMEO armed (see :meth:set_recv_timeout) an expiry reaches OpenSSL as recv returning EAGAIN, which SSL_read reports as SSL_ERROR_WANT_READ on an empty error queue. The plain flare_ssl_read turns that into a NetworkError whose reason is the empty string; here it raises Timeout, so a stalled TLS read reads the same as a stalled cleartext one. It is never reported as a 0-byte read, which would truncate a body into a parsed response. End of stream is unchanged, an unclean one included.

Args
buf: Destination buffer; the caller must provide at least size bytes of valid storage.
size: Maximum number of bytes to read.

Raises Timeout: If a socket-level send/receive timeout expired mid-record. NetworkError: On I/O or decryption error.

eof_was_unclean

def eof_was_unclean(self) -> Bool

Whether :meth:read returned end of stream without the peer's close_notify.

TLS signs the end of a stream with close_notify so that an attacker who can reset the TCP connection cannot cut a message short and have it pass for complete. Only a caller whose framing is the end of the stream needs this: a length or chunked body already fails when it stops early.

read_exact

def read_exact(mut self, buf: Pointer[UInt8], size: Int)

Read exactly size bytes into buf.

Args
buf: Destination buffer; must have at least size bytes.
size: Number of bytes to read.

Raises NetworkError: If EOF arrives before the buffer is full, or on error.

set_recv_timeout

def set_recv_timeout(self, ms: Int)

Bound blocking reads on the underlying TCP socket.

SO_RCVTIMEO applies to the socket, not to the TLS record layer: OpenSSL sees the expiry as SSL_ERROR_WANT_READ. :meth:read maps that to Timeout, so a stalled read raises after ms milliseconds without data on the wire and reads the same as a stalled cleartext read -- the case a half-open connection produces when the network drops mid-response.

Args
ms: Timeout in milliseconds. 0 disables the timeout.

Raises NetworkError: If setsockopt(2) fails.

write

def write(self, data: Span[UInt8]) -> Int

Encrypt and send bytes.

Args
data: Bytes to encrypt and transmit.

Raises NetworkError: On I/O or encryption error.

write_all

def write_all(self, data: Span[UInt8])

Encrypt and send all of data.

Loops until all bytes are transmitted or an error occurs.

Args
data: Bytes to transmit completely.

Raises NetworkError: On I/O or encryption error.

tls_version

def tls_version(self) -> String

Return the negotiated TLS version string.

cipher_suite

def cipher_suite(self) -> String

Return the negotiated cipher suite name.

peer_cert_subject

def peer_cert_subject(self) -> String

Return the subject DN of the server's certificate.

Args are described above. Do NOT use for security decisions — use config.verify for that.

Raises NetworkError: If no peer certificate is available.

alpn_selected

def alpn_selected(self) -> String

Return the ALPN protocol the server selected, or "" if ALPN was not negotiated.

Calls flare_ssl_get_alpn_selected (RFC 7301) on the live SSL session. Useful for clients that advertise multiple protocols via :attr:flare.tls.TlsConfig.alpn (e.g. ["h2", "http/1.1"]) and need to know which one the server picked before kicking off the higher-level protocol driver.

Raises NetworkError: When the underlying call fails (almost always means the SSL session is closed).

session

def session(self) -> TlsSession

Return the most recent TLS session OpenSSL surfaced for this connection's SSL_CTX.

Useful sequence:

  1. var s = TlsStream.connect(host, port, cfg)
  2. write a request, read the response (so the peer's NewSessionTicket arrives and the C-side new_session_cb fires).
  3. var sess = s.session() -- captures the cached session.
  4. Hand sess to the next connect via :meth:connect_resumed to skip the expensive part of the handshake.

The returned :class:TlsSession owns the underlying SSL_SESSION* (drop runs SSL_SESSION_free). For TLS 1.3 the ticket arrives interleaved with application data, so calling session() immediately after :meth:connect returns may yield an empty handle (its session_addr() == 0) -- do at least one I/O round trip first, or accept the empty handle and fall back to a full handshake on the next connect.

Raises NetworkError: When the underlying FFI surface refuses (almost always means the SSL session is already closed).

was_session_reused

def was_session_reused(self) -> Bool

Return True if the most recent handshake on this TlsStream resumed a prior session (peer-acked, full handshake skipped). Mirrors OpenSSL's SSL_session_reused.

connect_resumed

def connect_resumed(host: String, port: UInt16, config: TlsConfig, var session: TlsSession) -> Self

Open a TLS connection and offer session for resumption. If the server accepts, the handshake skips the certificate exchange and key derivation -- a 1-RTT savings on TLS 1.2, even more on TLS 1.3 (resumption flow uses the early-data shape).

Falls back to a full handshake silently if the server refuses the session (e.g. ticket expired, key rotated). Use :meth:was_session_reused after the call to verify the resumption took.

Args
host: Hostname or IP string. SNI is derived from this value (or from config.server_name when set).
port: Destination TCP port (typically 443 for HTTPS).
config: TLS configuration. enable_session_resumption must be True (the default) for the new ctx to be ready to capture the next session.
session: Previously-captured session via :meth:session. Ownership is consumed.

Raises NetworkError: DNS / TCP / handshake failure. TlsHandshakeError: Generic handshake failure.

close

def close(mut self)

Send close_notify and close the underlying TCP stream.

Idempotent — safe to call multiple times. The destructor also calls this, so explicit close() is not required.