docs(rv2-tls): brainstorm runtime-v2 9 (in-process TLS) to ready — hand-rolled

- decision: HAND-ROLL TLS 1.3 (no vendored lib) per developer call; keeps the
  zero-external-dep single binary, and raises risk rather than lowering it —
  recorded, owned, with mandatory mitigations
- 1.3-only; RSA-PSS/PKCS1 + ECDSA-P256 + full ASN.1/X.509 chain validation +
  trust store + hostname (the scope needed to reach real LLM APIs)
- decomposed into a bottom-up phase ladder: A AEAD (=rv2 8, forces AES-GCM
  there) -> B HKDF -> C X25519 -> D signatures/RSA -> E X.509 -> F record+FSM
  client -> G inbound server; C/D/E may each split into own iterations
- risk + test strategy section: constant-time, reference-tested (openssl +
  RFC 8448 vectors), negative tests first-class, no partial-trust states
- deps: rv2 8 (AEAD), lang 34 (SHA/HMAC), net.connect (110, landed). Board synced

(cherry picked from commit f1881cca8cd0cdd58b1ac844e3e3ea9c234bb99c)
This commit is contained in:
shoney.arickathil 2026-09-07 19:12:55 +02:00
parent cc1c82b2ef
commit 51addb504c
2 changed files with 74 additions and 44 deletions

View file

@ -1543,7 +1543,7 @@ starts. Edges in [dependency graph section 6](../00-dependency-graph.md).
| 6 | [term.size + term.width](runtime-v2/06-term-size-width.md) | ✅ **DONE 2026-09-02** — TIOCGWINSZ read twin (nil = not a tty) and libc wcwidth under C.UTF-8; the only runtime work the whole wmux parity ladder needs |
| 7 | [observability](runtime-v2/07-observability.md) | ⬜ `refine` — **moved here 2026-09-06** from language iteration 30 (`was_language_iteration: 30`). Runtime metrics/gauges, a `pprof`-equivalent profile, stack-trace-on-trap; consumers named (porch [8](porch/08-static-and-lifecycle.md)/[39](language-runtime-database/39-web-framework-parity.md), databasev2 [5](databasev2/05-bounded-tables-eviction.md), the limiter's lazy expiry). Forks: counters-only vs profiling, exposition format, pull vs push, trace-on-trap as a separable first slice. Stretches the track's charter (instrumentation, not processes/terminals/signals) — noted in the story |
| 8 | [symmetric cipher](runtime-v2/08-symmetric-cipher.md) | ⬜ `refine` — **created 2026-09-06** from the [porch↔fiber scope-gap](../plan/exploration/fiber/01-porch-vs-fiber-scope-gap.md); a cipher builtin fits the builtin-seam shape. AEAD for encrypted cookies (porch [2](porch/02-randomness-and-cookies.md) out-of-scope) + data-at-rest; extends [34](language-runtime-database/34-crypto-builtins.md)'s digests, needs porch 2's `random_bytes` for nonces. Load-bearing fork: AES-256-GCM (expected, hard constant-time in software) vs ChaCha20-Poly1305 (easier hand-roll, no-dep doctrine fit) |
| 9 | [in-process TLS](runtime-v2/09-in-process-tls.md) | ⬜ `refine` — **created 2026-09-07** from the gap [jarvis](jarvis/00-story.md) surfaces. TLS **both directions** (outbound client + inbound termination), **retiring the "TLS is the proxy's job" doctrine** (recorded in 34/38/porch). The track's heaviest seam — **not** builtin-sized, and likely a **vendored-lib exception** to no-external-deps (TLS is the one thing not to hand-roll). Load-bearing fork (left open): vendor mbedTLS/BearSSL vs hand-roll a subset. Sits on language 38's `net.connect`; gates jarvis entirely |
| 9 | [in-process TLS](runtime-v2/09-in-process-tls.md) | ✅ **`ready` 2026-09-07** — TLS **both directions**, **retiring the "TLS is the proxy's job" doctrine** (34/38/porch). Locked: **hand-roll TLS 1.3** (no vendored lib — keeps the zero-dep binary, raises the risk), **1.3-only**, **RSA+ECDSA+full X.509** cert verification (to reach real APIs). Decomposed into a bottom-up **phase ladder**: A AEAD (=rv2 8, forces AES-GCM there) → B HKDF → C X25519 → D signatures/RSA → E ASN.1/X.509 → F record+FSM client → G server. The project's **highest-risk** work; mandatory reference-tested/constant-time/negative-test gates. `net.connect` (110) landed; C/D/E may each split into own iterations |
### ▸ wmux — the terminal multiplexer track

View file

@ -2,7 +2,7 @@
track: runtime-v2
iteration: "9"
status: pending
readiness: refine
readiness: ready
---
# runtime-v2 9 — in-process TLS: retiring the proxy-termination doctrine
@ -11,9 +11,10 @@ readiness: refine
> assistant must dial an LLM over HTTPS, and the runtime has no outbound TLS. The
> developer chose the **full overturn**: the runtime gains TLS **both
> directions**, and the standing "TLS is the proxy's job" doctrine is retired.
> **`readiness: refine`** — the gap, its consumers and its forks are named here;
> the implementation is deliberately left as this story's load-bearing fork, not
> settled.
> **Brainstormed to `ready` 2026-09-07**: hand-rolled TLS 1.3, RSA+ECDSA+X.509
> cert verification, decomposed into the bottom-up phase ladder below. The load-
> bearing implementation fork is settled — hand-roll, not vendor — with eyes open
> to the risk (Info).
## Why this exists — and what it overturns
@ -44,39 +45,41 @@ X.509 certificate validation is a large, security-critical subsystem — the one
place the runtime's hand-roll-everything habit (the sha256 precedent) should not
be assumed to extend. That tension is the load-bearing fork below.
## What it should deliver (scope to be refined)
## Decisions locked (brainstorm 2026-09-07)
- **Outbound TLS client** — a `.wo` program dials an HTTPS endpoint: a TLS
handshake over the TCP socket `net.connect` (language 38) provides, with server
certificate validation against a trust store. jarvis's direct path, and
language 38's deliberately-excluded HTTPS half.
- **Inbound TLS server** — porch terminates TLS on its own listener (cert + key
loaded at startup), retiring the "put a proxy in front" requirement for a
single-binary deployment.
- **Certificate validation and a trust store** — X.509 chain verification,
hostname/SNI checks outbound; certificate + private-key loading inbound. This
is where most of the risk and most of the code live.
1. **Hand-roll TLS 1.3 in C — no vendored library.** The developer chose the
hand-roll over vendoring mbedTLS/BearSSL, extending the runtime's
hand-roll-everything habit (the sha256 precedent) to the hardest place it has
reached. This keeps the pure single-static-binary, zero-external-dependency
story intact — and it is, stated plainly, the largest and highest-risk
undertaking in the project. See the risk note in Info; it is not a caveat to
bury.
2. **TLS 1.3 only.** No 1.2 legacy — smallest attack surface, one handshake to
get right.
3. **Cert verification is full: RSA + ECDSA + X.509.** To reach real endpoints
(Anthropic, OpenAI and most HTTPS servers present RSA-signed chains), the
verifier does RSA-PSS and RSA-PKCS#1v1.5 plus ECDSA-P256, over a real
ASN.1/DER + X.509 chain validator with a system trust store, validity-date and
hostname (SAN) checks. This is the biggest, most CVE-prone slice, and it is in
scope because EC-only cannot talk to the APIs jarvis needs.
4. **Bottom-up, outbound-first.** Build the primitives before the protocol, and
the client (jarvis's need) before the server (porch's), because the primitives
are shared and only the role differs.
## Forks the brainstorm must settle
## The phase ladder
1. **Implementation source — the load-bearing fork (left open by direction).**
Vendor a small, audited TLS library (mbedTLS or BearSSL) compiled into the
single static binary — correct and maintainable, but a build-time external
dependency, an explicit exception to the no-external-deps doctrine the runtime
otherwise holds — versus hand-rolling a TLS 1.3 subset plus X.509 in C, which
matches the sha256 precedent but is thousands of security-critical lines and a
hand-rolled certificate validator is a CVE factory (strongly discouraged). The
honest lean is vendor-a-lib; TLS is exactly the thing not to hand-roll. This
fork decides whether the whole "single static binary, no external deps" story
gains a footnote.
2. **Phasing.** Outbound first (jarvis's actual need) with inbound to follow, or
both together since the handshake machinery and the vendored library are
shared and only the client-vs-server role and validation direction differ.
3. **Trust store and cert provisioning.** Where the outbound trust anchors come
from (the system CA bundle, and its path across distros), and how the inbound
side is handed its certificate and key (files, env, a reload story).
4. **TLS version and cipher policy.** TLS 1.3 only (simplest, modern, smallest
attack surface) versus 1.2+1.3 (broader reach). Leaning 1.3-only.
Each rung is a security-critical slice; C, D and E are each large enough that
they may split into their own runtime-v2 iterations as they are picked up.
| Phase | Delivers | Notes |
| --- | --- | --- |
| A — AEAD | AES-128/256-GCM (TLS 1.3 mandates AES-128-GCM) and ChaCha20-Poly1305 | **is runtime-v2 [8](08-symmetric-cipher.md)** — so 8 must include AES-GCM, not only ChaCha; this rung consumes it |
| B — key schedule | HKDF-Extract/Expand on iteration 34's HMAC, HKDF-Expand-Label, the TLS 1.3 secret derivation | pure `.wo`-adjacent C over existing HMAC |
| C — key exchange | X25519 (RFC 7748), constant-time | new primitive; the ECDHE shared secret feeding B |
| D — signatures | RSA-PSS / RSA-PKCS#1v1.5 (bignum modexp) + ECDSA-P256, over the transcript and the chain | the hardest rung; RSA bignum + constant-time |
| E — X.509 | ASN.1/DER parser, chain validation to a trust anchor, dates, hostname/SAN, system CA bundle | notoriously bug-prone; consumes D |
| F — record + handshake (client) | TLS record framing, the ClientHello→Finished FSM, transcript hash, wiring A–E; `net.connect_tls` outbound | jarvis's path; the reason the story exists |
| G — server (inbound) | the server handshake half, cert+key loading, signing CertificateVerify; porch terminates TLS | retires the inbound proxy requirement, and the doctrine docs |
## Consumers
@ -91,9 +94,13 @@ Named, so this is not a capability shipped as decoration:
## Dependencies
- **[language 38](../language-runtime-database/38-content-platform-capabilities.md)**
— `net.connect` (outbound TCP) is the socket the outbound handshake runs over;
the client half of this story sits directly on it.
- **runtime-v2 [8](08-symmetric-cipher.md)** — the AEAD (phase A). This story
forces 8 to include **AES-GCM** (TLS 1.3 mandates AES-128-GCM), not ChaCha
alone — a consequence to record in 8's own fork.
- **language [34](../language-runtime-database/34-crypto-builtins.md)** —
SHA-256/HMAC for the key schedule (phase B) and the transcript hash.
- **`net.connect`** (id 110, **landed 2026-09-07**) — the outbound TCP socket the
client handshake runs over; the client half sits directly on it.
## Out of scope
@ -107,11 +114,34 @@ Named, so this is not a capability shipped as decoration:
and [porch](../porch/00-story.md) when this lands — a follow-up bookkeeping
pass, named here so it is not forgotten, not part of the runtime work.
## Risk and test strategy
**This is the highest-risk work in the project, and hand-rolling it raises that
risk, not lowers it.** Hand-rolled RSA, ECDSA, X25519 and ASN.1/X.509 are the
classic sources of real-world CVEs (timing side-channels, padding oracles, chain-
validation bypasses, parser memory bugs). The decision to hand-roll is recorded
and owned; the mitigations are non-negotiable:
- **Constant-time** for every secret-dependent operation (X25519, RSA/ECDSA,
AEAD) — verified, not assumed.
- **Reference-tested**: every phase gated against a reference implementation —
`openssl s_client`/`s_server`, real published cert chains, and the RFC 8448
TLS 1.3 test vectors — plus an ASan/UBSan leg on the parser and bignum code.
- **Negative tests as first-class**: an expired cert, a wrong hostname, a broken
chain, a tampered CertificateVerify and a downgrade attempt must each be
refused, with a test that fails if they are accepted.
- **No partial-trust states**: a validation that cannot complete refuses the
connection; there is no "warn and continue".
## Info
This is the heaviest iteration in the runtime-v2 track and the only one that
forces a doctrine reversal and, most likely, an external-dependency exception —
both flagged above rather than buried. It is pure I/O-plane work (a handshake
layer over the existing socket verbs); no actors, so it is not exposed to the
lang-41 hang. It gates jarvis entirely: until it lands, jarvis cannot reach a
model at all.
This is the heaviest iteration in the runtime-v2 track by a wide margin — a
subsystem, not a builtin-sized seam — and the only one that reverses a project
doctrine. It is pure I/O-plane and compute work (a handshake layer over the
existing socket verbs plus the crypto ladder); no actors, so it is not exposed to
the lang-41 hang. It gates jarvis entirely: until at least phases A–F land,
jarvis cannot reach a model at all. Realistically it is a multi-phase effort
measured in weeks, and phases C (X25519), D (signatures/RSA) and E (X.509) may
each become their own iteration when picked up. Implementation order is the
ladder, bottom-up: A (via rv2 8) → B → C → D → E → F, with G (inbound server)
last.