tls-1.2: client and server support for 9legacy
TLS 1.2 for 9legacy in both directions: ECDHE key exchange, AEAD
ciphers, SNI, and X.509 path validation. Twenty-three patches
against libsec, the kernel record layer, hget, webfs and ip/httpd.
With the series applied:
- hget(1), webfs(4) and abaco(1) reach RSA- and ECDSA-fronted
servers over ECDHE (X25519, secp256r1, secp384r1) with
AES-128-GCM or ChaCha20-Poly1305, and send SNI.
- ip/httpd serves HTTPS and picks a certificate per vhost from
the presented name (RFC 6066 Section 3).
- Certificates are verified by default against
/sys/lib/tls/ca.pem: chain, hostname, validity window, and
the RFC 5280 Section 4.2 constraints (basicConstraints,
keyUsage, extKeyUsage, nameConstraints).
- The server echoes renegotiation_info on the initial handshake
and refuses renegotiation afterwards (RFC 5746).
- Both sides negotiate extended_master_secret where the peer
offers it (RFC 7627).
- TLS 1.0 and TLS 1.1 are refused on both sides (RFC 8996), and
a version mismatch answers with the protocol_version alert.
- Scalar multiplication is constant time: X25519 selects each
ladder step with a conditional swap rather than a branch on
the scalar, and the NIST curves use a Montgomery ladder. The
field arithmetic beneath them is not claimed to be.
- The record layer zeroes cipher state, MAC keys and the Secret
on session close, and its sequence number is 64 bits wide, so
the AEAD nonce cannot repeat under one key.
- The handshake bounds every message it writes and every length
it reads, the ASN.1 decoder limits its own recursion, and the
send buffer belongs to the connection rather than to the
process.
Apply
The patches are unified diffs against the iso-extracted 9legacy
tree, which is Plan 9 with the Stable and 9k queues already
applied. Order matters:
cd /
for p in \
libsec-ecdhe-aead-primitives \
tls-aead-record-layer \
tls-record-zeroize-on-free \
tls-ecdhe-sni \
libsec-ecc-ecdsa-primitives \
libsec-x509-chain-hostname \
tls-ecdhe-ecdsa-and-chain \
tls-nist-ecdhe-curves \
libsec-x509-validity-dates \
libsec-x509-extension-framework \
libsec-x509-rfc5280-constraints \
tls-ca-bundle-default \
tls-aead-cipher-preference \
tls-extension-framework \
tls-server-reneg-info \
tls-extended-master-secret \
ip-httpd-vhosts-sni \
libsec-ecmul-ct-p256 \
libsec-ecmul-ct-p384 \
tls-min-version-tls12 \
libsec-x509-decoder-limits \
libsec-tlshand-message-bounds \
tls-sendbuf-per-connection \
; do
echo === $p ===
ape/patch -p0 < /n/contrib/tls-1.2/$p.diff
done
They apply with no fuzz and leave no .rej. Anything else means
the baseline is not the tree described above.
Reverse
Back out in the opposite order, with -R:
cd /
for p in \
tls-sendbuf-per-connection \
libsec-tlshand-message-bounds \
libsec-x509-decoder-limits \
tls-min-version-tls12 \
libsec-ecmul-ct-p384 \
libsec-ecmul-ct-p256 \
ip-httpd-vhosts-sni \
tls-extended-master-secret \
tls-server-reneg-info \
tls-extension-framework \
tls-aead-cipher-preference \
tls-ca-bundle-default \
libsec-x509-rfc5280-constraints \
libsec-x509-extension-framework \
libsec-x509-validity-dates \
tls-nist-ecdhe-curves \
tls-ecdhe-ecdsa-and-chain \
libsec-x509-chain-hostname \
libsec-ecc-ecdsa-primitives \
tls-ecdhe-sni \
tls-record-zeroize-on-free \
tls-aead-record-layer \
libsec-ecdhe-aead-primitives \
; do
echo === $p -R ===
ape/patch -p0 -R < /n/contrib/tls-1.2/$p.diff
done
Build
rc /n/contrib/tls-1.2/tools/build.rc
This rebuilds libsec, then every program that links its TLS --
hget, webfs, tlsclient, tlssrv, ip/httpd, vncs, vncv, the upas
mail programs, wpa, httpfile and ftpfs -- then the kernel. Plan 9
links statically, so a program left unbuilt keeps a whole copy of
the old handshake; that is why the list is long.
rc /n/contrib/tls-1.2/tools/build.rc userspace
skips the kernel. Three patches modify sys/src/9/port/devtls.c
(tls-aead-record-layer, tls-record-zeroize-on-free and
tls-ecdhe-ecdsa-and-chain), so a kernel build and a reboot are
needed before HTTPS works.
mk does not know that a program depends on libsec.a, so build.rc
deletes the intermediates first to force the relink. It also
copies the new kernel into the 9fat partition, which 9load reads;
without that the next boot loads the old one. Then:
fshalt -r
Test
rc /n/contrib/tls-1.2/tools/test.rc
Fourteen probes against real servers: eleven expect success, and
three expect a named rejection -- a self-signed certificate, a
wrong hostname, and an expired certificate. The summary reads
ok: 14 / 14.
Scope limits
Not here, and each declined rather than unfinished:
- Serving an ECDSA certificate. It needs factotum to speak
proto=ecdsa, which 9legacy factotum does not. An RSA
certificate serves every client the self-test reaches.
- Server-side P-256 and P-384 ECDHE. The server offers X25519
alone; a client with no X25519 will not handshake with it.
The client offers all three, which is the direction that
reaches the web.
- RSA-PSS signatures. The verify path takes
RSA-PKCS1-SHA{256,384,512} and ECDSA-SECP{256R1,384R1}-SHA{256,384}.
- Session resumption, mTLS, ALPN, encrypt-then-MAC, HSTS, and
HTTP-to-HTTPS redirection.
- Fallback SCSV. RFC 8996 obsoletes it: with TLS 1.0 and 1.1
refused there is no older version to fall back to.
- TLS 1.3. A different protocol, not an extension of this one.
Files
README this file
libsec-ecdhe-aead-primitives.diff X25519, AES-GCM and
ChaCha20-Poly1305 in libsec
tls-aead-record-layer.diff AEAD record layer in devtls
tls-record-zeroize-on-free.diff zero cipher state, MAC keys
and Secret on close
tls-ecdhe-sni.diff ECDHE-RSA and SNI, both
directions
libsec-ecc-ecdsa-primitives.diff curve parameters and ECDSA
verify
libsec-x509-chain-hostname.diff chain walk and hostname match
(RFC 5280, RFC 6125)
tls-ecdhe-ecdsa-and-chain.diff client-side ECDHE-ECDSA
tls-nist-ecdhe-curves.diff P-256 and P-384 ECDHE, client
side (RFC 8422)
libsec-x509-validity-dates.diff notBefore and notAfter
(RFC 5280 Section 4.1.2.5)
libsec-x509-extension-framework.diff X.509 v3 extension parser
libsec-x509-rfc5280-constraints.diff basicConstraints, keyUsage,
extKeyUsage, nameConstraints
(RFC 5280 Section 4.2)
tls-ca-bundle-default.diff /sys/lib/tls/ca.pem as the
default trust anchor
tls-aead-cipher-preference.diff server-side suite choice and
the certificate-key gate
(RFC 4492 Section 2.2)
tls-extension-framework.diff table-driven extension
dispatch
tls-server-reneg-info.diff RFC 5746 echo and refusal
tls-extended-master-secret.diff RFC 7627
ip-httpd-vhosts-sni.diff per-vhost certificates in
ip/httpd (RFC 6066 Section 3)
libsec-ecmul-ct-p256.diff constant-time ladder on P-256
libsec-ecmul-ct-p384.diff the same on P-384
tls-min-version-tls12.diff refuse TLS 1.0 and 1.1
(RFC 8996)
libsec-x509-decoder-limits.diff bound the ASN.1 decoder's
recursion; free what it drops
libsec-tlshand-message-bounds.diff bound every handshake message
written and every length read
tls-sendbuf-per-connection.diff the handshake send buffer
belongs to the connection
tools/build.rc rebuild libsec, its consumers
and the kernel
tools/test.rc the self-test
Prerequisites
The iso-extracted 9legacy tree, from <https://9legacy.org/download/>.
Applying needs ape/patch. Building needs write access to /sys and
/$cputype, which in practice means glenda or an account in the
same group.
Trust anchors
/sys/lib/tls/ca.pem ships as a 2021 Mozilla NSS snapshot. Refresh
it when CAs rotate:
hget https://curl.se/ca/cacert.pem > /sys/lib/tls/ca.pem
Append a PEM certificate to trust an internal CA as well:
cat /tmp/internal-ca.pem >> /sys/lib/tls/ca.pem
Hide the bundle for one session to fall back to thumbprint trust:
bind /dev/null /sys/lib/tls/ca.pem
Rename or remove it to opt out entirely. hget and webfs decode
the bundle once per process, so a machine that fetches often pays
for it once.
Bugs
The X25519 field arithmetic reduces its coefficients and contracts
its result with loops whose trip counts follow the values, so the
constant-time claim covers the choice of ladder step and not the
arithmetic under it.
Distinguished names are compared as encoded bytes. RFC 5280
Section 7.1 asks for the RFC 4518 string preparation, which needs
Unicode tables Plan 9 does not carry. The comparison fails
closed: a name that ought to match but does not yields a rejected
chain, never an accepted one.
|