Tls
stdlib/tls.march — TLS (Transport Layer Security) support for March.
Wraps OpenSSL 3 via C builtins to provide TLS client and server functionality over existing TCP file descriptors.
Architecture: TlsCtx — an SSL_CTX (configuration + CA store), long-lived, shareable TlsConn — a single SSL * connection wrapping one TCP fd
Typical client usage: let config = Tls.default_client_config() match Tls.client_ctx(config) do Ok(ctx) -> match tcp_connect("example.com", 443) do Ok(fd) -> -- Bound reads against a peer that accepts and then goes silent. -- Must be set BEFORE the handshake to bound the handshake too. Socket.set_recv_timeout(fd, 30000) match Tls.connect(fd, ctx, "example.com") do Ok(conn) -> Tls.write(conn, "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") Tls.read(conn, 4096) Tls.close(conn) Err(e) -> Err(e) end Err(e) -> Err(TlsHandshakeFailed(e)) end Err(e) -> Err(e) end
Types
TlsVersion: minimum accepted TLS protocol version
TlsConfig: configuration for creating a TLS context
Fields: cert_file — path to PEM certificate (chain); "" for client-only ctx key_file — path to PEM private key; "" for client-only ctx ca_file — path to PEM CA bundle; "" → system/default roots alpn — ALPN protocol list, e.g. ["h2", "http/1.1"] min_version — minimum TLS version (Tls12 or Tls13) verify_peer — true → verify the server cert against ca_file (or the system roots); false → SSL_VERIFY_NONE (client ctx only; server_ctx ignores it)
Functions
Perform a TLS server handshake on an accepted TCP fd with a context from server_ctx. Returns Ok(TlsConn) or Err(TlsHandshakeFailed(msg)).
The ALPN protocol list offered by a TlsConfig, in preference order.
Path to the PEM CA bundle of a TlsConfig; empty means the system default roots.
Path to the PEM certificate (chain) of a TlsConfig; empty for a client-only config.
Create a client-side TLS context from a TlsConfig (uses ca_file, alpn, min_version, verify_peer; the cert/key fields are ignored). Returns Ok(TlsCtx) or Err(TlsContextError(msg)), e.g. when the CA file cannot be loaded. The ctx can be shared across many connections (thread-safe); release it with ctx_free.
Examples
match Tls.client_ctx(Tls.default_client_config()) do
Ok(ctx) -> use_ctx(ctx)
Err(e) -> println(Tls.error_to_string(e))
endPerform TLS shutdown and free the connection object. Returns (). Does NOT close the underlying TCP fd — caller must call tcp_close(fd). Do not use the conn afterwards.
Perform a TLS client handshake on an already-connected TCP fd. hostname is used for SNI and certificate hostname verification. Returns Ok(TlsConn) or Err(TlsHandshakeFailed(msg)) (including certificate verification failures). The fd stays owned by the caller.
Examples
match Tls.connect(fd, ctx, "example.com") do
Ok(conn) -> Tls.write(conn, request)
Err(e) -> Err(e)
endFree a TLS context. Call after all connections using this ctx have been closed.
Default client configuration: verify the server cert against the system roots, TLS 1.2+, HTTP/1.1 ALPN, no client certificate.
Examples
Tls.verify_peer(Tls.default_client_config()) -- true
Tls.alpn(Tls.default_client_config()) -- ["http/1.1"]Convert a TlsError to a human-readable string.
Examples
Tls.error_to_string(TlsHandshakeFailed("timeout")) -- "TLS handshake failed: timeout"
Tls.error_to_string(TlsCertError("invalid CN")) -- "TLS certificate error: invalid CN"
Tls.error_to_string(TlsReadError("EOF")) -- "TLS read error: EOF"
Tls.error_to_string(TlsReadTimeout) -- "TLS read timed out"
Tls.error_to_string(TlsWriteError("broken pipe")) -- "TLS write error: broken pipe"
Tls.error_to_string(TlsContextError("bad cert")) -- "TLS context error: bad cert"Client config for HTTPS offering ALPN ["h2", "http/1.1"]; otherwise the same as default_client_config (verify peer, TLS 1.2+). Check negotiated_alpn after the handshake to see which one the server chose.
Full HTTPS GET convenience function. Connects to host:port, performs TLS handshake (verifying the cert for host), sends a minimal HTTP/1.1 GET request with Connection: close, reads the response, tears down TLS, closes the TCP fd. Returns Ok(response_string) (the raw response: status line, headers and body, unparsed) or Err(TlsError); a TCP connect failure is reported as TlsHandshakeFailed("tcp_connect: ...").
The response is a single read of at most 1 MiB, so a response spanning several TLS records may come back truncated; use HttpTransport.request for a complete, parsed response. No read timeout is set.
Examples
match Tls.https_get("example.com", 443, "/") do
Ok(raw) -> println(raw)
Err(e) -> println(Tls.error_to_string(e))
endWhether a connection's key exchange was post-quantum (an ML-KEM group).
Path to the PEM private key of a TlsConfig; empty for a client-only config.
The minimum accepted TLS version (Tls12 or Tls13) of a TlsConfig.
Return the negotiated ALPN protocol as Some(name) (e.g. Some("h2")), or None if none was selected.
The key-exchange group the handshake agreed on, as OpenSSL names it: Some("X25519MLKEM768") for the hybrid post-quantum group, Some("x25519"), Some("secp256r1"), ...; None if unavailable.
Return the peer's certificate Common Name, or None if unavailable.
Whether this program's OpenSSL supports the hybrid post-quantum key exchange X25519MLKEM768 (OpenSSL 3.5 or later). When it does, every context from client_ctx / server_ctx offers it first, ahead of the classical X25519, P-256, P-384 and P-521, so a session with a peer that also supports it is protected against an attacker who records it now and breaks it later with a quantum computer. When it does not, contexts keep the library's own default groups. Always false in the interpreter, which has no real TLS.
Read up to max_bytes from a TLS connection. Returns Ok(String) or Err(TlsError). Ok("") signals a clean shutdown.
This read has no deadline of its own and will wait indefinitely for a peer that has gone silent. To bound it, call Socket.set_recv_timeout(fd, ms) on the underlying fd BEFORE Tls.connect -- SO_RCVTIMEO is the only mechanism that reaches a read made through OpenSSL, and setting it before the handshake bounds the handshake too. A deadline that fires part-way through a TLS record leaves the session unusable, so treat an expired read as "close the connection", not as something to retry.
When that deadline expires this returns Err(TlsReadTimeout), which is a different fact from Err(TlsReadError(...)) -- the peer was silent, rather than the connection failing. Both still mean close it.
Read up to max_bytes from a TLS connection, waiting at most timeout_ms.
Ok(None) is the cap expiring with nothing readable — the absence of an event, not a broken connection. Ok(Some("")) signals a clean shutdown, exactly as read does. Other failures are Err(TlsReadError(msg)).
Examples
match Tls.read_timeout(conn, 4096, 1000) do
Ok(Some(data)) -> handle(data)
Ok(None) -> keep_waiting()
Err(e) -> Tls.close(conn)
endServer configuration with a PEM cert+key and no client-cert verification: HTTP/1.1 ALPN, TLS 1.2+, empty ca_file. The files are only read by server_ctx.
Examples
let config = Tls.server_config("/etc/ssl/server.pem", "/etc/ssl/server.key")Create a server-side TLS context from a TlsConfig. cert_file and key_file must be non-empty PEM paths. verify_peer is not used. Returns Ok(TlsCtx) or Err(TlsContextError(msg)) (missing or mismatched cert/key, bad CA file).
Examples
let ctx = Result.unwrap(Tls.server_ctx(Tls.server_config("cert.pem", "key.pem")))Replace the key-exchange groups a context offers, in preference order, by their OpenSSL names, e.g. ["X25519MLKEM768"] to require post-quantum key exchange, or ["X25519", "P-256"] for a classical-only peer. Applies to connections made from the context afterwards. Err(TlsContextError(msg)) for an empty list or a group the linked OpenSSL does not know; the context is then unchanged.
match Tls.set_groups(ctx, ["X25519MLKEM768"]) do
Ok(_) -> connect_pq_only(ctx)
Err(e) -> println(Tls.error_to_string(e))
endWhether a client context built from this TlsConfig verifies the server certificate.
The runtime's encoding of a TlsVersion: 0 for Tls12, 1 for Tls13.
Convert a TlsVersion to a human-readable string.
Examples
Tls.version_to_string(Tls12) -- "TLS 1.2"
Tls.version_to_string(Tls13) -- "TLS 1.3"Write data to a TLS connection. Returns Ok(Int) (bytes written) or Err(TlsWriteError(msg)).
Examples
Tls.write(conn, "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")