hanki

tls

stdlib/extra/tls.hk: TLS over TCP, both ends.

A pure-Hanki face over the sys native seam (HANKI.md §4): nothing here is @intrinsic, it delegates to the raw sys TLS primitives, and the [net] effect bubbles up automatically. The connection's own methods (read!, write!, close!) live in stdlib/core/tls_stream.hk, and the listener's (accept!, address!, close!) in core/tls_listener.hk.

TLS 1.3 and 1.2 are both spoken, 1.3 whenever the peer offers it. The version moves; the guarantee does not. Every 1.2 suite compiled in is ECDHE with an AEAD cipher, which gives forward secrecy and authenticated encryption on either version, and there is no CBC, RC4 or static-RSA key exchange to fall back to. Renegotiation and compression are not implemented at all. A 1.3-capable peer cannot be talked down either: a 1.2 handshake carrying RFC 8446's downgrade sentinel is refused. 1.2 is here for reach, a great many government and grid-operator APIs speaking no other version.

Which version a connection settled on is observable: TlsStream.version! reports it as a sys.TlsVersion, which is Ord, and a caller auditing a fleet or holding a floor therefore writes version >= sys.V13 and no string comparison. This face is the only place that answers; http.get! does not, by design (see extra/http).

What this does not offer, decided and written down here:

There is no wrap!(TcpStream). connect! mints the socket inside the seam and never surfaces the plaintext handle: an ordinary call does not consume its argument, and a caller who handed a TcpStream in would retain a live handle to the same descriptor and could write past the encryption. The same reasoning rules out STARTTLS, which needs that upgrade-in-place; nothing in https, the registry, or the site needs it.

There is no way to skip certificate verification. An insecure escape hatch is a posture decision nobody has asked for, and one that is easier to add later than to take away.

--deterministic stops at this seam. A handshake consumes real entropy and wall clock inside the native library, which leaves a deterministic run unable to replay it, as with any real socket.

No hermetic test block belongs here: every function is a thin effectful delegation to the seam, and exercising it needs a real peer that cannot be asserted in the pure stdlib test pack. Coverage is end-to-end through the CLI integration tests.

TlsStream

TlsStream, or tls.TlsStream, is a runtime-managed native resource handle (HANKI.md §4): live native state the per-actor memory manager owns, released when the last handle drops. It is single-owner - never copied, moved across a send - and has no fields of its own, so its methods are its whole surface. A handle is minted by an API that opens one; it is never constructed.

connect!

def connect!(host: string, port: u16) -> Result<TlsStream, sys.TlsFailure> [net]

Open a TCP connection to host:port and complete the TLS client handshake over it, verifying the peer against the system trust store.

host is also the name checked against the certificate, and must therefore be the intended name and no address resolved by the caller. @no-doctest: opens a real connection and handshakes; needs a peer

write_all!

def write_all!(s: TlsStream, data: bytes) -> Result<(), sys.TlsFailure> [net]

Write the whole of data, looping over the single-write primitive until every byte is sent. Ok(()) once the buffer is drained, or the first Err(TlsFailure) a write reports.

net.write_all! would also serve, since TlsStream implements Stream - but that face folds the failure into NetError where this one preserves it, which still lets a caller tell a certificate problem from a broken pipe. @no-doctest: writes a real connection; needs a peer

ServerConfig

struct ServerConfig
  chain: bytes
  key: secret.Secret<bytes>
end

The material a listen! needs: a server certificate chain and the private key that goes with it, both PEM.

A value and no path pair, which lets the bytes come from wherever the caller has them: a secret store, a fetched credential, a file read with fs.read_bytes!, and never from disk alone. That choice is what puts the key in a Hanki value at all, and Secret (core secret) is what stops it leaking back out: this struct cannot be rendered, serialised, hashed, compared, or sent, the field refusing every one of those.

chain is not secret and is not wrapped. It is the certificate the server presents to every client that connects; hiding it would protect nothing and cost the ability to log which certificate is serving.

TlsListener

TlsListener, or tls.TlsListener, is a runtime-managed native resource handle (HANKI.md §4): live native state the per-actor memory manager owns, released when the last handle drops. It is single-owner - never copied, moved across a send - and has no fields of its own, so its methods are its whole surface. A handle is minted by an API that opens one; it is never constructed.

listen!

def listen!(host: string, port: u16, config: ServerConfig) -> Result<TlsListener, sys.TlsFailure> [net]

Bind host:port for TLS, compiling config into the configuration every accepted connection will use.

The compile happens here and never per accept: parsing a chain and a key is not work to repeat per connection, and a key that does not match its certificate fails at this call and never under traffic. Accept with TlsListener.accept!.

This is the one place the key material is revealed, and it is revealed straight into the native seam: core may not name an extra type, and sys.tls_listen! therefore takes the parts. Nothing between here and rustls retains it as a Hanki value.

Err(TlsFailure) names the bind address and no peer: a bad chain, a key that does not match it, or a port already taken. @no-doctest: binds a real port and needs real key material