diff --git a/docs/stories/00-status.md b/docs/stories/00-status.md index fe57a3e..8426d43 100644 --- a/docs/stories/00-status.md +++ b/docs/stories/00-status.md @@ -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 diff --git a/docs/stories/runtime-v2/09-in-process-tls.md b/docs/stories/runtime-v2/09-in-process-tls.md index 857f373..7a1a2e5 100644 --- a/docs/stories/runtime-v2/09-in-process-tls.md +++ b/docs/stories/runtime-v2/09-in-process-tls.md @@ -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.