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.
API at a glance
Section titled “API at a glance”Types: TlsError · TlsServer · TlsConnection · TlsClient
Functions: server · secure · receive · transmit · set_receive_timeout · shut · client · trusting · connect
TlsError
Section titled “TlsError”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
Section titled “BadCertificate”| BadCertificateThe certificate chain was not readable PEM, or was empty.
BadKey
Section titled “BadKey”| BadKeyThe key was not readable PEM, or does not match the certificate.
Handshake
Section titled “Handshake”| HandshakeA 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
Section titled “Unreachable”| 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.
TlsServer
Section titled “TlsServer”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.
TlsConnection
Section titled “TlsConnection”pub type TlsConnection = { session: Ptr };One secured connection, and the socket underneath it.
TlsClient
Section titled “TlsClient”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.
Functions
Section titled “Functions”server
Section titled “server”pub fn server(certificate: String, key: String) -> TlsServer with { scope: Scope } raises TlsErrorA 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.
secure
Section titled “secure”pub fn secure(server: TlsServer, socket: Int) -> TlsConnection raises TlsErrorCompletes 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.
receive
Section titled “receive”pub fn receive(connection: TlsConnection, into: Array<U8>) -> IntPlaintext 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.
transmit
Section titled “transmit”pub fn transmit(connection: TlsConnection, text: String) -> IntSends 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.
set_receive_timeout
Section titled “set_receive_timeout”pub fn set_receive_timeout(connection: TlsConnection, millis: Int) -> BoolHow 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.
client
Section titled “client”pub fn client() -> TlsClient with { scope: Scope } raises TlsErrorA 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.
trusting
Section titled “trusting”pub fn trusting(extra: String) -> TlsClient with { scope: Scope } raises TlsErrorThe 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.
connect
Section titled “connect”pub fn connect(client: TlsClient, host: String, port: Int) -> TlsConnection raises TlsErrorConnects 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