nova-tls
TLS client/server for Nova — client + server handshake,
certificate-verification modes (system roots / custom CA / SPKI pinning /
insecure-skip-verify for tests), mutual TLS (mTLS), and io.Read/io.Write
conformance on top of std.net.TcpStream.
Backend: mbedTLS via a
thin C shim (native/tls_c_shim.c) — pure C, no Rust/cargo required to build
this package. mbedTLS v3.6.2 is vendored at native/mbedtls/ (Plan 193 Ф.2
gate-3, 2026-07-12 — see native/mbedtls/VENDORED.md) and self-builds on
first nova test/nova build via the generic [ffi] vendor_src_dirs
build-and-cache mechanism — no manual vcpkg/system install step required
(a manually dropped-in prebuilt lib under native/lib/ still works too and
is used as-is, skipping the vendor build).
Extracted from the Nova monorepo’s std/tls (Plan 116 core design + Plan 195
mbedTLS backend swap) into a standalone repository per
Plan 193
— the reference instance of Nova’s native-module pattern
(Plan 195:
.nv facade + .c shim + prebuilt .lib, wired through [ffi], zero Rust).
Public API is unchanged from std.tls — only the module path moved
(std.tls.* -> tls.*; see “Module path” note below).
Usage
import tls.{TlsStream, ClientConfig, VerificationMode}
import std.net.{Net, TcpStream, SocketAddr, real_net}
fn fetch(host str, port u16) Net -> Result[(), TlsError] {
ro tcp = TcpStream.connect(SocketAddr.new(host, port))!!
ro cfg = ClientConfig.new(host)
consume stream = TlsStream.connect(tcp, cfg)!!
stream.write_all("GET / HTTP/1.1\r\nHost: ${host}\r\n\r\n".bytes())!!
ro resp = stream.read_to_vec(4096)!!
stream.close()
Ok(())
}
Which roots SystemRoots trusts (0.2.0)
VerificationMode.SystemRoots, the default, loads the machine’s trust anchors
from the first source that yields at least one root, and uses that source whole
(sources are never merged, or a root the owner removed from the OS store would
come back through the embedded list):
- the OS store: on Windows, the
ROOTsystem store (crypt32); - a system bundle: the first readable PEM file among
/etc/ssl/certs/ca-certificates.crt,/etc/pki/tls/certs/ca-bundle.crt,/etc/ssl/ca-bundle.pem,/etc/pki/tls/cacert.pem,/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem,/etc/ssl/cert.pem; - the embedded Mozilla list (
native/tls_mozilla_roots.h), always present.
tls.system_roots() runs the same loader and reports what it found:
SystemRootsInfo { source, detail, count }, where detail names the store or the
bundle path. Log it at startup; it is the first thing to look at when a handshake
fails with CertificateInvalid.
What changed from 0.1.x. Up to 0.1.8 SystemRoots meant the embedded list
only. A root the machine’s owner trusts on purpose (a corporate CA, or a security
product that inspects TLS and installs its own root into the OS store) was
therefore refused, while the OS and every browser on the machine accepted it.
To keep the old behaviour, pass the embedded list explicitly as CustomRoots.
Known limit on Windows. The ROOT store holds the roots fetched so far.
Windows downloads more lazily, when its own verifier meets them. A site chained to
a root that no program on the machine has needed yet can fail here, although a
browser would pass.
macOS has no OS-store step (its keychain needs the Security framework); it reads
/etc/ssl/cert.pem.
Layout
nova-tls/
├── nova.toml [package] name = "tls"; [lib] src = "src"; [ffi] native shim
├── native/
│ ├── tls_c_shim.c mbedTLS backend (compiled via [ffi] c_shims)
│ ├── tls_shim.h C-side prototypes (Nova <-> C ABI contract)
│ └── tls_mozilla_roots.h compiled-in Mozilla root CA bundle
└── src/
├── ffi.nv extern "C" fn declarations against native/tls_c_shim.c
├── error.nv TlsError (typed error surface)
├── config.nv ClientConfig / ServerConfig / VerificationMode / ClientCertMode
├── roots.nv system_roots(): where SystemRoots takes its trust anchors
├── client.nv TlsStream.connect (client handshake)
├── server.nv TlsStream.accept (server handshake)
├── stream.nv TlsStream (sans-I/O pump + io.Read/io.Write conformance)
├── *_test.nv peer tests (same-module, positive)
├── neg/ EXPECT_COMPILE_ERROR fixtures (standalone CUs)
└── testdata/ self-signed fixture certs (server + mTLS client CA/leaf)
Module path
D78 rev-4 (root peers, spec/decisions/07-modules.md “Root peers —
.nv-файлы прямо в source root”) lets .nv files that sit directly in
the package’s source root (src/, per [lib] src above) declare the
single-segment module <package_name> form — a peer group analogous to
Cargo’s lib.rs. This package’s whole surface lives directly in src/
(src/{client,server,stream,...}.nv, all declaring module tls), so the
resulting module is the bare package name, tls — no statter. Import as
import tls.{TlsStream, ...}, both from another package’s [dependencies]
consumer and from an independent same-package file (e.g. src/neg/*.nv
uses import tls.{ClientConfig} to reach these peers).
Before the rev-4 amendment this package used to live at src/tls/*.nv
under the older rev-3 parent_dir.target rule, which forced the domain
folder to repeat the package name (module tls.tls, import
tls.tls.{...}) — migrated to root peers 2026-07-13 (Plan 202 Ф.3).
Building standalone
Requires the Nova toolchain (nova CLI + clang). No Rust/cargo, no manual
mbedTLS install — mbedTLS self-builds from the vendored source at
native/mbedtls/ on first run (Plan 193 Ф.2 gate-3; drop a prebuilt
mbedtls/mbedx509/mbedcrypto under native/lib/ instead if you’d
rather skip that one-time build, e.g. vcpkg install mbedtls).
# Boehm GC (mandatory Nova runtime dep) needs its own lib/include dirs —
# point NOVA_GC_LIB_DIR (+ optional NOVA_GC_INCLUDE_DIR) at a prebuilt
# bdwgc if it isn't reachable via the default vcpkg/system lookup
# (see compiler-codegen/src/test_runner.rs detect_boehm).
#
# `nova` does not (yet) bundle/locate the standard library relative to the
# nova.exe install — a standalone package must point it at a Nova checkout's
# std/ via NOVA_STD_PATH (compiler-codegen/src/manifest.rs resolve_std_path):
export NOVA_STD_PATH=/path/to/nova/std
# Ditto for the compiler's own C runtime (compiler-codegen/nova_rt/ + the
# libuv submodule it needs) — NOVA_CG_INCLUDE / NOVA_RT_DIR, symmetric with
# NOVA_STD_PATH above (resolve_paths in nova-cli/src/main.rs; this is what
# closed the Plan 193 Ф.1 blocker previously documented here — no more need
# to vendor the compiler's runtime into this repo):
export NOVA_CG_INCLUDE=/path/to/nova/compiler-codegen
export NOVA_RT_DIR=/path/to/nova/compiler-codegen/nova_rt
# Use `nova test`, not `nova build <single-file>`, for anything beyond a
# syntax/import smoke check — this package has no `main`, and isolated
# single-file builds of a library CU can hit generic-inference ambiguities
# that a full test CU resolves via its own call sites (verified upstream:
# the same `ffi.nv` hits it identically inside the Nova monorepo).
nova test src
License
Dual-licensed under MIT or Apache-2.0, at your option — same terms as the Nova compiler and standard library.