Skip to content

tls — TLS 1.3 for the aoughwl net stack

tls adds encrypted transport to the aoughwl networking stack. A TlsSocket wraps an already-connected net.Socket, driving the TLS 1.3 handshake and record layer through OpenSSL 3 (libssl.so.3 / libcrypto.so.3) over a header-free dynlib FFI. It sits directly above net + tcp and supports both roles: a client context (SNI, hostname verification, ALPN, trust store) and a server context (PEM cert chain + key, ALPN selection).

Status — Production-ready. TLS 1.3 client+server, SNI + hostname verification, ALPN (both directions), configurable cipher/suite and min/max version, and non-blocking handshake/I-O all shipped. Opt-in TLS 1.3 session resumption (capture a TlsSession, replay it on the next connect, sessionReused to confirm) and per-SNI multi-certificate virtual hosting (addCertificate + a servername callback, with a default fallback cert) now ship too; server-side ALPN preference is per-context (no longer a single process-global).

Quickstart

nim
import tls

var ctx = newTlsClientContext()          # verify against the system trust store
discard ctx.setAlpnProtocols(@["http/1.1"])

var conn = ctx.connectTls("example.com", 443)   # resolve + TCP + handshake
if conn.isValid and conn.handshakeDone:
  echo conn.protocolVersion()            # "TLSv1.3"
  echo conn.negotiatedAlpn()             # "http/1.1"
  echo conn.verifyOk()                   # true

  discard conn.sendAll("GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n")
  echo conn.readAll()

conn.closeTls()                          # close_notify + free SSL + close socket
ctx.freeContext()

API

Types

symbolsignaturewhat it does
TlsModeenum tlsClient, tlsServerRole a context was built for.
TlsContextobject { handle: pointer; mode: TlsMode; stateId: int }Long-lived config wrapping SSL_CTX; reuse across connections. handle is nil when construction failed. stateId is the 1-based index (0 = unassigned) into the per-context server state registry (SNI certs + ALPN).
TlsSocketobject { socket: Socket; ssl: pointer; handshakeDone: bool }One TLS connection over a Socket, wrapping SSL.
TlsSessionobject { handle: pointer }A captured, resumable SSL_SESSION. Obtain with getSession, stash it, replay via wrapClient/connectTls, free with freeSession.
TlsStatusenum tlsOk, tlsWantRead, tlsWantWrite, tlsClosed, tlsErrorResult of a handshake/read/write. The tlsWant* values are the non-blocking retry signals.

Constants (protocol versions)

symbolvaluewhat it does
TLS1_VERSION0x0301Version selector for setMinVersion / setMaxVersion.
TLS1_1_VERSION0x0302"
TLS1_2_VERSION0x0303"
TLS1_3_VERSION0x0304"

Constants (session cache modes)

symbolvaluewhat it does
SSL_SESS_CACHE_OFF0x0000Mode selector for setSessionCacheMode.
SSL_SESS_CACHE_CLIENT0x0001Client-side session cache (used by enableClientSessionCache).
SSL_SESS_CACHE_SERVER0x0002Server-side session cache (OpenSSL default).
SSL_SESS_CACHE_BOTH0x0003Both directions.

Context construction

symbolsignaturewhat it does
newTlsClientContextproc(verify = true): TlsContextBuild a client context. With verify (default) the server chain is checked against the system trust store; pass false only for self-signed testing.
newTlsServerContextproc(certChainFile: string; keyFile: string): TlsContextBuild a server context from a PEM cert chain + private key. Returns an invalid context (isValid false) if a file fails to load or the key does not match the cert.
closeproc(ctx: var TlsContext)Free the underlying SSL_CTX and nil the handle.
freeContextproc(ctx: var TlsContext)Alias for close(TlsContext); use at call sites that also import net (whose Socket has its own close) where ctx.close() is ambiguous.

Context configuration

symbolsignaturewhat it does
setVerifyPeerproc(ctx: TlsContext; enabled: bool)Turn peer certificate verification on/off.
loadVerifyLocationsproc(ctx: TlsContext; caFile: string): boolTrust an extra CA bundle / cert file (PEM). Returns success.
useDefaultVerifyPathsproc(ctx: TlsContext): bool(Re)load the system default trust store. Returns success.
setCipherListproc(ctx: TlsContext; ciphers: string): boolRestrict the TLS 1.2-and-below cipher list (OpenSSL cipher string).
setCipherSuitesproc(ctx: TlsContext; suites: string): boolRestrict the TLS 1.3 cipher suites (colon-separated suite names).
setMinVersionproc(ctx: TlsContext; version: int): boolFloor the negotiated protocol version (e.g. TLS1_2_VERSION).
setMaxVersionproc(ctx: TlsContext; version: int): boolCap the negotiated protocol version.
setAlpnProtocolsproc(ctx: TlsContext; protocols: seq[string]): bool(Client) Advertise an ALPN list, e.g. @["h2", "http/1.1"]. Encodes the length-prefixed wire form.
setAlpnServerproc(ctx: var TlsContext; protocols: seq[string]): bool(Server) Register the selection callback that picks the server's most-preferred protocol from the client offer, so negotiatedAlpn reflects the choice. Preference is stored per-context, so multiple servers in one process don't clobber each other.
addCertificateproc(ctx: var TlsContext; hostname, certChainFile, keyFile: string): bool(Server) Register an extra PEM cert + key to present when a client's SNI matches hostname (virtual hosting). The newTlsServerContext cert stays the default fallback. Wires up the servername callback on first use.
setSessionCacheModeproc(ctx: TlsContext; mode: int): boolSet the SSL_CTX session-cache mode (an SSL_SESS_CACHE_* value).
enableClientSessionCacheproc(ctx: TlsContext): boolConvenience: turn on client-side caching so getSession reliably captures a resumable TLS 1.3 session.

Connecting & handshake

symbolsignaturewhat it does
connectTlsproc(ctx: TlsContext; host: string; port: int; session = TlsSession(handle: nil)): TlsSocketOne-call client entry: resolve host, open TCP, run the client handshake with SNI + hostname verification set to host. Pass a session to attempt resumption. Blocking; check handshakeDone / isValid.
wrapClientproc(ctx: TlsContext; socket: Socket; serverName: string; session = TlsSession(handle: nil)): TlsSocketStart a client session over an already-connected socket; sets SNI + verification hostname to serverName and runs the handshake. Pass a session to attempt resumption.
wrapServerproc(ctx: TlsContext; socket: Socket): TlsSocketStart a server session over an accepted socket and run the handshake.
handshakeproc(t: var TlsSocket): TlsStatusDrive or resume the handshake. Returns tlsOk on completion; on a non-blocking socket may return tlsWantRead/tlsWantWrite — call again when the socket is ready.

Reading & writing

symbolsignaturewhat it does
tlsReadIntoproc(t: var TlsSocket; buf: pointer; len: int; status: var TlsStatus): intRead up to len plaintext bytes into a caller-owned buffer. Returns count (>0) with tlsOk; 0 with tlsClosed/tlsWant*/tlsError; -1 on an invalid socket.
tlsWriteFromproc(t: var TlsSocket; buf: pointer; len: int; status: var TlsStatus): intWrite up to len plaintext bytes from a buffer. Returns count accepted (>0) with tlsOk, else 0 with a want/closed/error status.
recvproc(t: var TlsSocket; maxBytes: int): stringBlocking convenience: read up to maxBytes into a string; stops at EOF or no progress.
readAllproc(t: var TlsSocket): stringBlocking convenience: read plaintext until the peer closes the TLS session.
sendproc(t: var TlsSocket; data: string): intBlocking convenience: write the whole string; returns bytes sent (a short return signals a closed/errored session).
sendAllproc(t: var TlsSocket; data: string): boolsend(t, data) == data.len — true when the whole payload went out.
pendingproc(t: TlsSocket): intBytes already decrypted and buffered inside OpenSSL; a caller polling the fd must drain these first.

Connection info

symbolsignaturewhat it does
protocolVersionproc(t: TlsSocket): stringNegotiated protocol, e.g. "TLSv1.3".
cipherNameproc(t: TlsSocket): stringNegotiated cipher suite name.
negotiatedAlpnproc(t: TlsSocket): stringALPN protocol the peer selected (e.g. "h2"), or "".
verifyOkproc(t: TlsSocket): boolTrue when the peer chain verified (X509_V_OK). Meaningful only when the context requested verification.
verifyResultCodeproc(t: TlsSocket): intRaw X509 verification result code (0 == X509_V_OK).
peerCertCommonNameproc(t: TlsSocket): stringCommonName (CN) of the peer leaf certificate's subject, or "". Handy on a client to confirm which cert an SNI multi-cert server selected.

Session resumption

symbolsignaturewhat it does
getSessionproc(t: TlsSocket): TlsSessionCapture the current (resumable) session. Call after the handshake and — for TLS 1.3 — after at least one recv, so the server's ticket has been processed.
sessionReusedproc(t: TlsSocket): boolTrue when the just-completed handshake resumed a session supplied via wrapClient/connectTls.
isValidproc(s: TlsSession): boolSession holds a live SSL_SESSION.
freeSessionproc(s: var TlsSession)Release a captured session (drops the refcount).

Status, validity & teardown

symbolsignaturewhat it does
isValidproc(ctx: TlsContext): boolContext has a live SSL_CTX handle.
isValidproc(t: TlsSocket): boolSocket has a live SSL.
lastTlsErrorproc(): stringPop and format the most recent OpenSSL error, or "" when the queue is empty.
closeTlsproc(t: var TlsSocket; closeSocket = true)Send close_notify, free the SSL, and (by default) close the underlying socket.

Design notes

  • No headers, no C shim. OpenSSL is reached purely through dynlib FFI to libssl.so.3 / libcrypto.so.3; opaque structs are passed around as nil-checked pointer. Control operations go through the raw SSL_ctrl / SSL_CTX_ctrl command numbers rather than the header macros.
  • Status-based, no exceptions. Every fallible call returns a TlsStatus or bool; nothing raises. Construction failures surface as an invalid handle (isValid false) rather than an error to catch.
  • Caller-owned buffers. tlsReadInto / tlsWriteFrom are the primitive I/O layer over caller memory; recv / readAll / send / sendAll are the blocking-socket string conveniences built on top.
  • Blocking with a non-blocking escape hatch. On a non-blocking socket the handshake and I/O surface tlsWantRead / tlsWantWrite, so the same API drives a poll loop. pending reports OpenSSL-buffered plaintext that a raw fd poll cannot see.
  • Context outlives connections. A TlsSocket holds a reference on the context via OpenSSL's refcount, so a context may be closed after connectTls without tearing down live sessions.
  • ALPN asymmetry. setAlpnProtocols only advertises (client side); setAlpnServer registers the selection callback. The server preference list is stored per-context in a small registry keyed by the context's stateId (passed to the C callbacks as their arg), so two servers in one process keep independent ALPN/SNI state.
  • SNI virtual hosting. addCertificate builds a child SSL_CTX per hostname; a servername callback reads the client's SNI at handshake time (SSL_get_servername) and swaps the connection onto the matching child (SSL_set_SSL_CTX). Connections whose SNI matches nothing keep the newTlsServerContext cert as the default. close frees the child contexts.
  • Session resumption. Opt-in and non-breaking: getSession (SSL_get1_session) hands back a refcounted TlsSession; replaying it through wrapClient/connectTls (SSL_set_session) attempts a TLS 1.3 resumption, and sessionReused (SSL_session_reused) confirms it. Server tickets are on by default in OpenSSL 3 and left enabled.

Requirements

  • Toolchain: the nimony/aowl compiler.
  • Depends on: aoughwl/net and aoughwl/tcp (socket type, resolver, connectTcp4).
  • C libraries (runtime, via dynlib): OpenSSL 3 — libssl.so.3 and libcrypto.so.3. No build-time headers or linkage required.

aoughwl — self-hosted platform for things n stuff. Contact / Support on Discord for access to the private backends.