From dd426d058102fde123c344e2611417b388026c35 Mon Sep 17 00:00:00 2001 From: "shoney.arickathil" Date: Wed, 9 Sep 2026 17:21:33 +0200 Subject: [PATCH] =?UTF-8?q?test(tls):=20rv2=208=20phase=20E=20=E2=80=94=20?= =?UTF-8?q?both=20AEADs=20cross-checked=20against=20openssl=20over=20the?= =?UTF-8?q?=20wire?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tls-server-accept.sh: each probe pins openssl s_client's -ciphersuites — ec/ChaCha20-Poly1305, rsa/AES-128-GCM, plus openssl's default list whose first suite (AES-256-GCM) the server must skip — 5/0 - tls-accept.sh: the Python/OpenSSL stub prints the negotiated suite; the happy-path ok line carries it — 5/0 (ChaCha under the peer's server-preference default) - rv2 8 story: E landed (real-protocol interop replaces the infeasible `openssl enc` AEAD check); D (encrypted-cookie wrapper) re-homed to porch as the consumer's phase after porch 2 — fork auto-approved, review_pending; status: done - porch 2: the encrypted-cookie out-of-scope bullet now points at the landed primitives and names the wrapper as its follow-on - board row rv2 8: in-progress -> done Co-Authored-By: Claude Fable 5.1 (cherry picked from commit ac3bf74da4f45f624d7d3b440c8bcf17f4aec3a9) --- docs/stories/00-status.md | 2 +- .../porch/02-randomness-and-cookies.md | 12 +++++++---- .../stories/runtime-v2/08-symmetric-cipher.md | 12 ++++++----- scripts/tls-accept.sh | 3 ++- scripts/tls-server-accept.sh | 20 ++++++++++++------- 5 files changed, 31 insertions(+), 18 deletions(-) diff --git a/docs/stories/00-status.md b/docs/stories/00-status.md index 16940a2..5328a54 100644 --- a/docs/stories/00-status.md +++ b/docs/stories/00-status.md @@ -1612,7 +1612,7 @@ starts. Edges in [dependency graph section 6](../00-dependency-graph.md). | 5 | [fd passing](runtime-v2/05-fd-passing.md) | ✅ **DONE 2026-09-02** — `net.send_fd`/`recv_fd`/`connect_unix`; a tty crossed the socket, was raw'd through the received copy and restored at destroy — the wmux handover in miniature | | 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) | ⬜ **`ready` 2026-09-09** (was `refine`; moved here 2026-09-06 from language iteration 30). Four forks locked, grounded in `runtime/src`: (1) **counters/gauges only** — they already exist as fields (`gc_traced_cnt`, `gc_alloc_bytes`, `gc_step_no`, arena `used`, `nfibers`, `nchildren`, `ntls`); profiling split to its own later iteration; (2) **Prometheus text rendered in `.wo`** from a `map` (no new record, no JSON); (3) **pull** via `proc.metrics() -> map`, per shard; push/OTel deferred by name; (4) **stack-trace-on-trap lands first, alone** — the trap path already has method+line (per-method line table) and `vm_unwind` walks frames, so phase A appends `at METHOD line N` per frame under the existing trap line. Phases A trace → B `proc.metrics()` → C porch mounts `/metrics` (porch's). Builtin id confirmed against `WO_B_MAX` at build (shared enum). Consumers: 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 auto-approved, `review_pending` | -| 8 | [symmetric cipher (AEAD)](runtime-v2/08-symmetric-cipher.md) | 🔄 **in-progress** — the **first rung of the TLS ladder** (gates rv2 9). **Phases A + B + C LANDED 2026-09-08**: A ChaCha20-Poly1305 (ids 111/112, RFC 8439 §2.8.2); B AES-128/256-GCM (ids 113/114) via AES-NI+PCLMULQDQ; C portable constant-time software AES-GCM fallback (S-box via GF-inverse ladder, bit-by-bit GHASH) — AES-GCM now on any CPU, dispatched hw-or-sw. All hand-rolled, constant-time, both AES paths NIST cases 4 & 16 byte-exact, KAT-gated in test_crypto (**48/0**), ASan/UBSan clean. Remaining: D cookie wrapper → E gate (ARMv8 hw path deferred). Consumers: rv2 9 TLS + porch encrypted cookies | +| 8 | [symmetric cipher (AEAD)](runtime-v2/08-symmetric-cipher.md) | ✅ **DONE 2026-09-09** (`review_pending`: phase D re-homed to porch) — the **first rung of the TLS ladder**. A ChaCha20-Poly1305 (ids 111/112, RFC 8439 §2.8.2); B AES-128/256-GCM (ids 113/114) via AES-NI+PCLMULQDQ; C portable constant-time software AES-GCM fallback — AES-GCM on any CPU, dispatched hw-or-sw. All hand-rolled, constant-time, KAT-gated in test_crypto, ASan/UBSan clean. **E landed 2026-09-09**: the reference cross-check is real-protocol interop — `just tls-server` pins `openssl s_client` to ChaCha, to AES-128-GCM, and to openssl's default list (5/0); `just tls` reports the negotiated suite (5/0). **D (encrypted-cookie wrapper) is porch's phase**, pure `.wo` after porch 2 ships `random_bytes` + `SetCookie`. ARMv8 hw path deferred until an ARM host exists | | 9 | [in-process TLS](runtime-v2/09-in-process-tls.md) | ✅ **DONE 2026-09-09** — in-process TLS 1.3 **both directions**, **retired the "TLS is the proxy's job" doctrine** (34/38/porch corrected). Hand-rolled, 1.3-only, RSA+ECDSA+full X.509; KAT'd vs **RFC 8448** / real certs, ASan/UBSan clean. **A–E crypto** (AEAD, HKDF, X25519, sign/verify, X.509 + SAN + basicConstraints/EKU) → **F client** (`net.connect_tls`/`read_tls`/`write_tls`, ids 115–117) → **G server** (constant-time RSA-PSS + ECDSA-P256 signing w/ RFC 6979, server FSM, `net.accept_tls` id 118, `wo_pkey_parse`). Live-gated: `just tls` 5/0 (outbound) + `just tls-server` 4/0 (inbound, openssl s_client EC+RSA). test_tls 123/0, test_crypto 130/0, full suite 0 fail. Deferred follow-ups (non-blocking): park-based handshake, `TlsConn` object, connection pooling, close_notify, complete-formula EC ladder. Forks auto-approved 2026-09-08/09, `review_pending`. The project's **highest-risk** work — done | ### ▸ wmux — the terminal multiplexer track diff --git a/docs/stories/porch/02-randomness-and-cookies.md b/docs/stories/porch/02-randomness-and-cookies.md index 7a5d3d7..b08be96 100644 --- a/docs/stories/porch/02-randomness-and-cookies.md +++ b/docs/stories/porch/02-randomness-and-cookies.md @@ -217,10 +217,14 @@ randomness and cookies maps onto primitives writeonce already has. ## Out Of Scope -- **Encrypted cookies.** Fiber's `encryptcookie` needs a symmetric cipher - (AES-GCM), and the runtime has digests only. Signed-and-readable is honest and - sufficient for a session id; encrypting a payload is a separate ask with a - separate primitive behind it. +- **Encrypted cookies.** Fiber's `encryptcookie` needs a symmetric cipher. + Signed-and-readable is honest and sufficient for a session id, so this + iteration ships without it — but the primitive now exists + ([runtime-v2 8](../runtime-v2/08-symmetric-cipher.md): `chacha20poly1305_seal`/ + `open`, `aes_gcm_seal`/`open`, done 2026-09-09). The wrapper — random nonce + from `random_bytes` prepended to the ciphertext, default ChaCha — is rv2 8's + **phase D, re-homed here as porch's follow-on** to this iteration: pure `.wo` + on the `SetCookie` machinery and `random_bytes` this iteration builds. - **UUID-formatted ids.** fiber's `UUIDv4` is a format over the same entropy `random_bytes` provides; a base64'd 32-byte token is stronger and needs no new builtin. A UUID *format* helper, if ever wanted, is pure `.wo`. diff --git a/docs/stories/runtime-v2/08-symmetric-cipher.md b/docs/stories/runtime-v2/08-symmetric-cipher.md index d71366a..bb2641b 100644 --- a/docs/stories/runtime-v2/08-symmetric-cipher.md +++ b/docs/stories/runtime-v2/08-symmetric-cipher.md @@ -1,8 +1,9 @@ --- track: runtime-v2 iteration: "8" -status: in-progress +status: done readiness: ready +review_pending: "fork auto-approved 2026-09-09 for autonomous execution — developer second review: phase D (the encrypted-cookie wrapper) re-homed to the porch track as the consumer's phase (pure .wo on porch 2's cookie machinery + random_bytes, which do not exist yet), the same split rv2 7 makes for its /metrics phase; the runtime primitive closes here with phase E" --- # runtime-v2 8 — AEAD ciphers: authenticated encryption for cookies, data at rest, and TLS @@ -81,8 +82,8 @@ work; TLS's ChaCha suite and the cookie consumer unblock at phase A. | A — ChaCha20-Poly1305 | ✅ **LANDED 2026-09-08** — `chacha20poly1305_seal`/`open` (ids 111/112, bare-name crypto family). Hand-rolled ChaCha20 + poly1305-donna-32 + the RFC 8439 §2.8 AEAD, caller-supplied 12-byte nonce, 32-byte key, constant-time tag compare, `open` returns nil on auth failure. Matches the RFC 8439 §2.8.2 vector byte-for-byte; gated in `test/test_crypto.c` (§2.5.2 Poly1305 + §2.8.2 seal/open/tamper), ASan/UBSan clean | | B — AES-GCM via hardware | ✅ **LANDED 2026-09-08** (x86-64) — `aes_gcm_seal`/`open` (ids 113/114), AES-128/256 (by key length) on AES-NI + PCLMULQDQ, constant-time by hardware, CPUID-gated with target-attributed functions so the binary stays portable (no-AES-NI traps until phase C). Matches NIST SP 800-38D cases 4 & 16 byte-for-byte; KAT-gated in `test_crypto.c`, ASan/UBSan clean. **ARMv8 crypto-ext path deferred** (untestable on the x86-64 dev host) — folds into phase C | | C — AES-GCM portability | ✅ **software fallback LANDED 2026-09-08** — portable constant-time AES (S-box via the GF(2⁸)-inverse power ladder, no tables) + bit-by-bit constant-time GHASH; same `aes_gcm_seal`/`open`, dispatched to AES-NI when present else this path. Matches NIST cases 4 & 16 byte-for-byte (test forces the software path via `wo_aes_force_software`), ASan/UBSan clean. AES-GCM is now available on any CPU (the phase-B no-AES-NI trap is retired). **ARMv8 crypto-ext hardware path still deferred** (untestable on the x86-64 dev host) — a follow-up when an ARM host exists | -| D — the cookie wrapper | an `encryptcookie`-equivalent on porch [2](../porch/02-randomness-and-cookies.md)'s cookie machinery: random nonce (from `random_bytes`) prepended to the ciphertext, default ChaCha | -| E — the gate | RFC 8439 + NIST GCM known-answer vectors, ASan/UBSan on both paths, and a reference cross-check (`openssl enc`/a scripted peer) | +| D — the cookie wrapper | **re-homed to porch 2026-09-09** (`review_pending`) — an `encryptcookie`-equivalent is pure `.wo` on porch [2](../porch/02-randomness-and-cookies.md)'s cookie machinery: random nonce (from `random_bytes`) prepended to the ciphertext, default ChaCha. It is the **consumer's** phase (as rv2 7's `/metrics` is porch's) and cannot land before porch 2 ships `random_bytes` + `SetCookie`; porch 2 names it as its follow-on. The primitives it wraps are complete here | +| E — the gate | ✅ **LANDED 2026-09-09** — RFC 8439 + NIST GCM known-answer vectors on both AES paths (`test_crypto`), ASan/UBSan clean, and the reference cross-check is **real-protocol interop, not `openssl enc`** (which cannot do AEAD modes): `just tls-server` pins `openssl s_client` to `TLS_CHACHA20_POLY1305_SHA256`, to `TLS_AES_128_GCM_SHA256`, and to openssl's default list (server must skip the unimplemented AES-256 suite) — 5/0; `just tls` reports the suite the Python/OpenSSL peer negotiated (ChaCha under its server-preference default) — 5/0 | ## Consumers @@ -139,5 +140,6 @@ Two named consumers now — TLS (rv2 9 phase A, the reason AES-GCM is in) and po encrypted cookies — with database-field-at-rest a plausible third. Pure compute, no actors, not exposed to the lang-41 hang. It is the **first rung of the TLS ladder**, so it gates rv2 9: nothing above TLS phase A can be built until this -lands. Implementation order is A (ChaCha, unblocks the most for the least risk) → -B (hardware AES-GCM) → C (software AES fallback) → D (cookie wrapper) → E (gate). +lands. Implementation order was A (ChaCha, unblocks the most for the least risk) → +B (hardware AES-GCM) → C (software AES fallback) → E (gate); D (cookie wrapper) +is porch's, after porch 2. **Done 2026-09-09** as a runtime primitive. diff --git a/scripts/tls-accept.sh b/scripts/tls-accept.sh index c1b5bc1..ba8ccdc 100755 --- a/scripts/tls-accept.sh +++ b/scripts/tls-accept.sh @@ -91,6 +91,7 @@ def serve(): except OSError: return try: s = ctx.wrap_socket(c, server_side=True) + print("SUITE", s.cipher()[0], flush=True) # which AEAD ran over the wire s.recv(4096); s.sendall(b"wo-tls-ok\n"); s.close() except Exception as e: print("stub-err", e, flush=True) @@ -124,7 +125,7 @@ fi start_stub "$WORK/leaf.pem" "$WORK/leaf.key" || { bad "stub server did not start"; echo "tls-accept: $fail failures"; exit 1; } out="$(run_client "$WORK/ca.pem")"; rc=$? if [[ $rc -eq 0 && "$out" == *"wo-tls-ok"* ]]; then - ok "handshake + trusted chain + host match + app round-trip" + ok "handshake + trusted chain + host match + app round-trip [$(grep -m1 '^SUITE' "$WORK/stub.log" | cut -d' ' -f2)]" else bad "happy path (exit $rc): $out" fi diff --git a/scripts/tls-server-accept.sh b/scripts/tls-server-accept.sh index a44aac3..82c09d2 100755 --- a/scripts/tls-server-accept.sh +++ b/scripts/tls-server-accept.sh @@ -3,7 +3,10 @@ # The runtime suite proves the server FSM offline (loopback against the client # driver); this proves interop with a real client — `openssl s_client` # validating our hand-rolled server handshake and exchanging application data, -# for both an ECDSA-P256 and an RSA server certificate. Log: /tmp/tls-server.log. +# for both an ECDSA-P256 and an RSA server certificate. Each probe pins the +# client's cipher suite, so both AEADs run against the reference peer over the +# wire (rv2 8 phase E's cross-check), plus one probe with openssl's default +# list, whose first entry we do not implement. Log: /tmp/tls-server.log. set -uo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" @@ -69,7 +72,7 @@ else fi # ---- 3. openssl s_client interop, per key type --------------------------- -probe() { # $1 = tag (ec|rsa) ; $2 = port (distinct per probe — avoids a bind race) +probe() { # $1 = tag (ec|rsa) ; $2 = port (distinct per probe — avoids a bind race) ; $3 = openssl -ciphersuites list local p="$2" kill -KILL "$SRV_PID" 2>/dev/null WO_DATA="$WORK/data" "$WOVM" "$WORK/srv.wob" "$p" "$WORK/$1.pem" "$WORK/$1.key" >>"$LOG" 2>&1 & @@ -81,15 +84,18 @@ probe() { # $1 = tag (ec|rsa) ; $2 = port (distinct per probe — avoids a bind local out out="$({ printf 'GET / HTTP/1.0\r\n\r\n'; sleep 1; } | \ timeout 12 openssl s_client -connect "127.0.0.1:$p" -CAfile "$WORK/ca.pem" \ - -servername localhost -tls1_3 -verify_return_error -quiet 2>/dev/null)" + -servername localhost -tls1_3 -ciphersuites "$3" -verify_return_error -quiet 2>/dev/null)" if [[ "$out" == *"hello-wo-tls"* ]]; then - ok "$1: openssl s_client validated the cert + got the reply" + ok "$1 [$3]: openssl s_client validated the cert + got the reply" else - bad "$1: no reply (out: ${out:0:80})" + bad "$1 [$3]: no reply (out: ${out:0:80})" fi } -probe ec "$PORT" -probe rsa "$((PORT + 1))" +probe ec "$PORT" TLS_CHACHA20_POLY1305_SHA256 +probe rsa "$((PORT + 1))" TLS_AES_128_GCM_SHA256 +# openssl's default order: the server must skip the AES-256 suite it does not +# implement and pick from the rest +probe ec "$((PORT + 2))" TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256 echo "tls-server: $((pass + fail)) checks, $fail failures" [[ $fail -eq 0 ]]