Plan 9 from Bell Labs’s /usr/web/sources/contrib/mospak/tls-modern-client/README

Copyright © 2021 Plan 9 Foundation.
Distributed under the MIT License.
Download the Plan 9 distribution.


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.

Bell Labs OSI certified Powered by Plan 9

(Return to Plan 9 Home Page)

Copyright © 2021 Plan 9 Foundation. All Rights Reserved.
Comments to webmaster@9p.io.