tls-modern-client: modern HTTPS client support for 9legacy
Nine patches that bring 9legacy's TLS client stack to a state where
it interoperates with the modern HTTPS web. After applying all
nine, hget(1), webfs(4), and abaco(1) can reach RSA- and
ECDSA-fronted servers using ECDHE key exchange (X25519, secp256r1,
secp384r1), AEAD ciphers (AES-GCM, ChaCha20-Poly1305), and SNI.
X.509 chain + hostname + validity-window verification (RFC 5280 +
RFC 6125) is on by default via the existing /sys/lib/tls/ca.pem
Mozilla root bundle.
CHANGES
v3.1 — 2026-04-24 (tooling only)
tools/build.rc kdisk auto-detected from
/env/bootfile; previously
hardcoded sdC0, silently
skipping the 9fat kernel
copy on non-sdC0 installs.
No patch changes.
v3 — 2026-04-24
libsec-x509-validity-dates Enforce notBefore/notAfter
on every cert in the chain.
tls-ca-bundle-default hget and webfs read
/sys/lib/tls/ca.pem as trust
anchors by default. Missing
file ⇒ pre-patch behavior.
v2 — 2026-04-24
tls-nist-ecdhe-curves Advertise secp256r1 +
secp384r1 as ECDHE named
groups alongside X25519.
v1 — 2026-04-22
libsec-ecdhe-aead-primitives X25519, AES-GCM,
ChaCha20-Poly1305.
tls-aead-record-layer devtls AEAD record-layer
support.
tls-ecdhe-sni-client TLS 1.2 ECDHE + SNI in
tlshand.
libsec-ecc-ecdsa-primitives ECC (secp256r1/384r1),
ECDSA verify.
libsec-x509-chain-hostname X.509 chain walk + RFC 6125
hostname match.
tls-ecdsa-and-chain-integration TLS_ECDHE_ECDSA cipher
suites; hget+webfs SNI.
Scope limits:
- Client side only. tlsServer / httpd are not modified.
- TLS 1.2. No TLS 1.3.
- DNS SAN + IPv4 iPAddress SAN + CN fallback for non-IP hostnames.
IPv6 iPAddress SAN is not yet supported.
- ECDHE curves: X25519 + secp256r1 + secp384r1.
- X.509 chain verification enforces signature chain, hostname
match, and the notBefore/notAfter validity window. It does
NOT yet enforce basicConstraints CA:TRUE, keyUsage, or
extKeyUsage; those are planned follow-on patches.
- Opt-out: rename or remove /sys/lib/tls/ca.pem. hget and webfs
then fall back to the pre-patch thumbprint-only trust model.
A per-invocation opt-out flag (curl-style hget -k) is planned
but not yet shipped.
Files
libsec-ecdhe-aead-primitives.patch curve25519, X25519, AES-GCM
tls-aead-record-layer.patch devtls AEAD record layer
tls-ecdhe-sni-client.patch TLS 1.2 ECDHE + SNI in tlshand
libsec-ecc-ecdsa-primitives.patch ECC, secp256r1, secp384r1, ECDSA verify
libsec-x509-chain-hostname.patch X.509 chain walk + RFC 6125
tls-ecdsa-and-chain-integration.patch TLS_ECDHE_ECDSA suites, hget+webfs SNI
tls-nist-ecdhe-curves.patch P-256 and P-384 as ECDHE named groups
libsec-x509-validity-dates.patch notBefore / notAfter enforcement
tls-ca-bundle-default.patch hget + webfs trust /sys/lib/tls/ca.pem
tools/build.rc rc helper: rebuild libsec + consumers + kernel
tools/test.rc rc helper: exercise three HTTPS targets
Apply order
The patches are incremental; apply in this order:
libsec-ecdhe-aead-primitives
tls-aead-record-layer
tls-ecdhe-sni-client
libsec-ecc-ecdsa-primitives
libsec-x509-chain-hostname
tls-ecdsa-and-chain-integration
tls-nist-ecdhe-curves
libsec-x509-validity-dates
tls-ca-bundle-default
cd /
patches=(libsec-ecdhe-aead-primitives tls-aead-record-layer \
tls-ecdhe-sni-client libsec-ecc-ecdsa-primitives \
libsec-x509-chain-hostname tls-ecdsa-and-chain-integration \
tls-nist-ecdhe-curves libsec-x509-validity-dates \
tls-ca-bundle-default)
for(p in $patches)
ape/patch -p0 < /path/to/tls-modern-client/$p.patch
/sys/lib/tls/ca.pem already ships in 9legacy as a Mozilla NSS
root bundle and is used directly; no action needed after apply.
To refresh it yourself:
hget https://curl.se/ca/cacert.pem > /sys/lib/tls/ca.pem
No rebuild or reboot required after a bundle refresh.
Rebuild
Patches 1, 3, 4, 5, 8, 9 modify userspace (libsec, hget, webfs).
Patches 2 and 6 also modify the in-kernel devtls driver and
therefore require a kernel rebuild + reboot.
Userspace:
cd /sys/src/libsec && mk clean && mk all && mk install
cd /sys/src/cmd/webfs && mk clean && mk install
cd /sys/src/cmd && mk hget.install # or manually: 6c hget.c; 6l -o hget hget.6
Kernel (pick the branch that matches your booted image):
cd /sys/src/9k/k10 && mk clean && mk 'CONF=k10f' install # amd64, 9k10f
# or
cd /sys/src/9/pc && mk clean && mk 'CONF=pcf' install # i386, 9pcf
Then: fshalt -r, and boot the fresh kernel image.
The rc helper tools/build.rc does all of the above in one call.
Prerequisites
These patches target a 9legacy tree that already has 9legacy's own
TLS 1.2, SHA-2 X.509, ChaCha20, Poly1305, and HKDF patches applied.
A recent 9legacy CD / source tree already has them. To sanity-check:
grep -c TLS12Version /sys/src/libsec/port/tlshand.c # non-zero
grep -c chacha /sys/include/libsec.h # non-zero
If both are non-zero you are ready.
If you are patching pristine Plan 9, install these 9legacy patches
(from http://9legacy.org/patch.html) in order first:
tls-devtls12, 9-devtls-maxtlsdevs, 9-devtls-leak,
9-devtls-zero-length-records, libsec-pbkdf2, tls-tlshand12,
libsec-x509-sha2, libsec-x509-sig, libsec-tlshand12-nossl3,
libsec-tlshand12-norc4, libsec-tlshand-empty-reneg,
libsec-tlshand12-fixes, libsec-tlshand-sigalgs, libsec-chacha,
libsec-poly1305, libsec-hkdf_x, libsec-chacha-iv, libsec-snprint,
aes-ctr, and 9k-jmk-devtls-tls12 for amd64.
Recommended companion
For parallel-fetch HTTPS consumers (abaco, multiple simultaneous
hget runs, etc.) also apply contrib/webfs-readline-overflow. It
is a one-line fix to webfs/buf.c readline() that resets the Ibuf
rp/wp pointers when the 4096-byte input buffer drains, closing a
latent memory-safety bug unrelated to TLS but surfaced today by
the traffic pattern this series enables. hget alone on a single
URL does not trip it; abaco loading Wikipedia over HTTPS does.
See contrib/webfs-readline-overflow/README for the symptom, fix,
and verification procedure.
Verification
After rebuild + reboot:
rc tools/test.rc
Nine probes against real servers. Six expect success and exercise
the features this series adds; three expect a specific rejection
from the verification path and confirm it is active. Probe byte
counts will vary as live sites change their content.
Real output from an amd64 9legacy VM:
=== tls-modern-client self-test ===
[1/9] plain-HTTP baseline
[ok] example.com (528 bytes)
[2/9] RSA cert, SNI, AES-GCM
[ok] www.google.com (81111 bytes)
[3/9] ECDSA cert, NIST curve
[ok] github.com (566020 bytes)
[4/9] ECDSA cert, SNI-gated
[ok] blog.cloudflare.com (110222 bytes)
[5/9] common site cross-check
[ok] en.wikipedia.org (233534 bytes)
[6/9] negotiated-cipher echo
[ok] www.howsmyssl.com (840 bytes)
[7/9] chain verification rejects
[ok] self-signed.badssl correctly rejected: tlsClient: tls: local cert verify: no trust anchor signed the top of the chain
[8/9] hostname-match rejects
[ok] wrong.host.badssl correctly rejected: tlsClient: tls: local cert verify: hostname does not match cert
[9/9] validity-window rejects
[ok] expired.badssl correctly rejected: tlsClient: tls: local cert verify: cert expired
--- tls conversations (negotiated cipher/curve per session) ---
=== summary ===
ok: 9 / 9
bad: 0
tls-modern-client verified.
The `--- tls conversations ---` block is usually empty at the end
of a run: each probe closes its /net/tcp and /mnt/web/* session
before the next starts, so by the time the loop walks `#a/tls`
nothing lingers. To see negotiated cipher/curve in real time,
run one probe manually and inspect before hget exits, e.g.
hget -v https://www.google.com/ >/dev/null &
cat '#a/tls/0/status'
If a probe unexpectedly reports [BAD], check:
- kernel booted is the rebuilt one (date on /$cputype/9pcf,
or uname -a)
- userspace rebuilt (ls -l /$cputype/bin/hget and
/$cputype/lib/libsec.a post-apply)
- network reaches the outside world (hget http://example.com/)
- /sys/lib/tls/ca.pem is readable and non-empty (used by
tls-ca-bundle-default)
- the reject-reason substring in the [BAD] line is the libsec
error you expect; if not, a different failure path fired
Also try reaching these sites from abaco(1) for a full end-to-end
rendering test of the same stack.
Rollback
Reverse order, -R flag:
cd /
rpatches=(tls-ca-bundle-default libsec-x509-validity-dates \
tls-nist-ecdhe-curves tls-ecdsa-and-chain-integration \
libsec-x509-chain-hostname libsec-ecc-ecdsa-primitives \
tls-ecdhe-sni-client tls-aead-record-layer \
libsec-ecdhe-aead-primitives)
for(p in $rpatches)
ape/patch -R -p0 < /path/to/tls-modern-client/$p.patch
Then rebuild libsec, userspace consumers, and kernel as above,
and reboot.
|