Skip to content

std::net::tls

TLS, over the sockets in std::net::socket.

Bound, not written. the ecosystem design note puts TLS in the short list of things Khora does not implement — “correctness is a specialist matter and a bug is a breach” — so this is rustls, reached through the runtime that every Khora executable already links. Nothing here is cryptography; it is a handshake, two buffers and a lifetime.

No new type crossed the compiler for this. A configuration and a connection are opaque machine addresses, the way an open file is in std::fs, and their lifetimes are tied to a scope with acquire. The code generator did not have to learn a thing.

What it does: both ends. A server takes a certificate chain and a key in PEM and secures accepted sockets. A client trusts the machine’s own root certificates and connects by name, verifying that the certificate says so. Reads and writes look like the plain ones on either side.

What it does not: client certificates, ALPN, session resumption, and choosing a cipher suite. Deliberate, all of them — rustls picks safe defaults, and a knob that lets somebody pick worse ones is not a feature.

Types: TlsError · TlsServer · TlsConnection · TlsClient

Functions: server · secure · receive · transmit · set_receive_timeout · shut · client · trusting · connect

pub type TlsError =
| BadCertificate
| BadKey
| Handshake
| Unreachable(host: String);

Why a TLS server could not start, or a connection could not be secured.

Four cases, and only the last carries anything. A rustls diagnostic is written for somebody debugging a handshake; what a Khora program can do about a failure is fix the certificate, fix the key, drop the client, or give up on the host – and only that last one needs to say which host.

| BadCertificate

The certificate chain was not readable PEM, or was empty.

| BadKey

The key was not readable PEM, or does not match the certificate.

| Handshake

A client failed the handshake. Ordinary: a port scan, a plain-HTTP request to an HTTPS port, or a client with no cipher in common.

| Unreachable(host: String)

A connection could not be made, or the server it reached did not present a certificate this client trusts for that name.

One case rather than four. “Could not resolve”, “refused”, “expired” and “wrong name” are different to somebody debugging and identical to a program: it does not have the connection, and what it does next is retry or give up. Telling them apart wants an error that carries text, which is a separate piece of work.

pub type TlsServer = { settings: Ptr };

A configured server, ready to secure accepted sockets.

Holds a machine address rather than being one, so that a signature can say what it wants. The address is rustls’s and Khora never looks inside it.

The field is not called handle, and neither is TlsConnection’s: two records with one field of one type are the same shape, and a literal for either fits both. The checker says so rather than guessing, which is the right answer and a good reason to name a field after what it holds.

pub type TlsConnection = { session: Ptr };

One secured connection, and the socket underneath it.

pub type TlsClient = { trust: Ptr };

What a client trusts.

The field is trust and the server’s is settings, which is not only for reading: two records of one Ptr are the same shape, so a bare literal fits both and the checker asks which was meant. Distinct names answer it without an annotation — and an annotation on the let would not have, because a type hint does not currently reach inside the branches of an if.

pub fn server(certificate: String, key: String) -> TlsServer with { scope: Scope } raises TlsError

A server from a certificate chain and a private key, both PEM.

Released when the enclosing scope ends, like an open file: the configuration is registered with acquire, so it goes however the scope goes, including through a raise. A server usually lives as long as the program, which means the scope to open it in is usually the root one.

PEM rather than DER because that is the shape a certificate arrives in — from a file, a secret store, or an ACME client — and converting is work every caller would repeat.

import std::core::{Scope, scoped};
import std::fs::{FsRead, read_text};
import std::net::tls::{server};
fn settings() -> TlsServer
with { scope: Scope, reads: FsRead }
raises TlsError
{
server(read_text("cert.pem")!, read_text("key.pem")!)!
}

scope is in the row because the settings own a handle the runtime has to close: acquire registers that with the enclosing scoped, so it goes back on every way out including a cancellation.

pub fn secure(server: TlsServer, socket: Int) -> TlsConnection raises TlsError

Completes a handshake over an accepted socket.

Takes the socket. shut below closes it, and the caller must not close it as well: a second close lands on a descriptor the operating system is free to have reissued, which shows up much later as one connection reading another’s bytes. This is why there is no TlsConnection::socket.

A failed handshake has already closed the socket, so a caller that raises and moves on has leaked nothing.

pub fn receive(connection: TlsConnection, into: Array<U8>) -> Int

Plaintext bytes into into: how many, 0 when the peer has closed, or -1.

The same three answers receive gives on a plain socket, so a reader that already handles a socket handles this.

pub fn transmit(connection: TlsConnection, text: String) -> Int

Sends text, all of it, or -1.

All or nothing, unlike socket::transmit, which answers how many bytes went: a partial write of an encrypted record is not something a caller can resume, so the runtime finishes it or fails.

pub fn set_receive_timeout(connection: TlsConnection, millis: Int) -> Bool

How long a read on this connection may wait before it gives up.

The https half of set_receive_timeout. The plain path sets a deadline on the socket; a TLS session owns its socket rather than handing it back, so until this existed a secured connection had no deadline at all and a peer that accepted and then said nothing held a fiber until the process ended. That is the shape of a slowloris, and it worked over https and not over http.

It is set on the socket underneath, where the reactor’s timer lives; TLS never learns about it. A read that runs out fails, which every caller here already treats as a connection to give up on.

pub fn shut(connection: TlsConnection) ->()

Says goodbye properly, then closes the socket.

The close_notify is not politeness. Without it a peer cannot tell a closed connection from a cut one, which is the truncation attack TLS closes by having an end-of-stream that is signed.

pub fn client() -> TlsClient with { scope: Scope } raises TlsError

A client trusting the certificates this machine trusts.

The operating system’s store, not a list compiled into Khora. A program should trust what its machine trusts: a corporate CA is the ordinary case rather than an exotic one, and a bundled root list is a snapshot that goes stale between releases without telling anybody.

Released when the enclosing scope ends, like a server.

import std::core::{Scope};
import std::net::tls::{client, connect, transmit};
fn ask(host: String) -> Int with { scope: Scope } raises TlsError {
let caller = client()!;
let session = connect(caller, host, 443)!;
transmit(session, "GET / HTTP/1.1\r\nHost: " + host + "\r\n\r\n")
}

Verifies against the machine’s own trust store, which is what a client talking to the public internet wants. trusting is the one for a private certificate authority.

pub fn trusting(extra: String) -> TlsClient with { scope: Scope } raises TlsError

The same, trusting one more certificate chain as a root.

For an internal CA the machine has not been told about, and for a test against a self-signed server. There is deliberately no way to turn verification off: every library offers one, and it is how verification comes to be off in production. Naming the certificate you mean to trust is the same amount of typing and leaves a reviewer something to look at.

import std::fs::{FsRead, read_text};
import std::net::tls::{trusting};
fn internal() -> TlsClient
with { scope: Scope, reads: FsRead }
raises TlsError
{
trusting(read_text("internal-ca.pem")!)!
}

In addition to the machine’s trust store, not instead of it. A client that had to choose would make talking to an internal service and talking to the public internet mutually exclusive.

pub fn connect(client: TlsClient, host: String, port: Int) -> TlsConnection raises TlsError

Connects to host on port and completes a handshake.

The name matters twice: it is what gets resolved, and it is what the certificate has to be for. Connecting to an address and hoping is the thing certificate verification exists to prevent, so there is no overload that takes one.

The socket is made here rather than passed in, because resolving a name needs getaddrinfo and a struct addrinfo cannot cross the C ABI. The runtime already has a resolver worth using.

Khora 0.4.0built frombd964738afb6on 2026-10-17 20:41ZVersioningSecurityContributingReleasesSource