- 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 <noreply@anthropic.com> (cherry picked from commit ac3bf74da4f45f624d7d3b440c8bcf17f4aec3a9)
10 KiB
| track | iteration | status | readiness | review_pending |
|---|---|---|---|---|
| runtime-v2 | 8 | done | ready | 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
Created 2026-09-06 from the porch-vs-fiber scope-gap analysis (exploration) as a runtime-v2 iteration — a hand-rolled cipher builtin with a
types.mlrow is exactly the track's builtin-sized-seam shape (the iteration 42 precedent). (It is not a language-track iteration; the language number 43 was already spent on the wmux foundation.)Brainstormed to
ready2026-09-08. A second consumer appeared after the first draft — runtime-v2 9's TLS phase A — and it reshaped the forks: TLS 1.3 mandates AES-128-GCM (RFC 8446), so the original ChaCha-only lean is out, and TLS constructs its own per-record nonce, so the primitive takes a caller-supplied nonce.
Why this exists
The runtime has digests only — SHA-1, SHA-256, HMAC-SHA256, base64
(iteration 34) — and, from the porch track,
random_bytes (porch 2). Those let a
program authenticate and sign a value, and mint a random one. None of them
let it encrypt — turn a plaintext into a ciphertext only the key-holder can
read.
That absence is a named porch limit:
porch 2 scopes out encrypted
cookies explicitly — "Fiber's encryptcookie needs a symmetric cipher, 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." This is that separate primitive.
Signed-and-readable (what porch has) is correct for a session id — the client may see it, it just may not forge it. Encryption is for the case where the payload itself must be hidden from the client: an encrypted cookie carrying app state, or a database field encrypted at rest.
Decisions locked (brainstorm 2026-09-08)
- Two AEAD ciphers: AES-GCM (128 and 256) and ChaCha20-Poly1305. TLS 1.3 mandates AES-128-GCM, so it is in whatever happens; ChaCha20-Poly1305 (RFC 8439) rides along because it is a valid TLS 1.3 suite, is far easier to get constant-time, is preferred where no AES hardware exists, and is the clean default for cookies and data-at-rest. TLS negotiates whichever the server picks; application code defaults to ChaCha.
- AES is made constant-time by hardware, with a software fallback. AES-NI on
x86-64 (
<wmmintrin.h>) and the ARMv8 crypto extension give constant-time AES and GHASH (CLMUL/PMULL) with zero external dependency — these are compiler intrinsics, not a library. A bitsliced constant-time software AES + a constant-time GHASH covers CPUs without the extension. ChaCha20-Poly1305 is naturally constant-time in portable C and needs no hardware path. - The primitive takes a caller-supplied nonce. Shape:
<cipher>_seal(key, nonce, aad, plaintext) -> Bytes(ciphertext‖tag) and<cipher>_open(key, nonce, aad, ciphertext) -> ?Bytes(nilon any authentication failure). Caller-supplied because TLS builds its own per-record nonce (static IV XOR sequence number); the random-nonce convenience for cookies is a wrapper on top (phase D), not the primitive. Named per cipher (matching the existingsha1/sha256/hmac_sha256style), AES variant inferred from key length (16 → AES-128, 32 → AES-256). New builtin ids start at 111 (afternet.connect= 110); confirm againstwob.hat implementation. - Raw key with a length check. A raw key (16 or 32 bytes, from
random_bytes, carried as base64 in config — theencryptcookie.GenerateKeyshape), length-validated. A passphrase-plus-KDF is a separate ask. - Hand-rolled, no vendored library — consistent with rv2 9's decision and the SHA-256 precedent. AEAD, never a bare cipher: unauthenticated encryption is a footgun that will not ship.
Phases
Easy cipher first, so a working AEAD exists before the hard constant-time AES work; TLS's ChaCha suite and the cookie consumer unblock at phase A.
| Phase | Delivers |
|---|---|
| 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 | re-homed to porch 2026-09-09 (review_pending) — an encryptcookie-equivalent is pure .wo on porch 2'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
- runtime-v2 9, TLS phase A — the record-layer AEAD; needs AES-GCM (mandatory) and gets ChaCha too. This is why AES-GCM is in scope.
- porch encrypted cookies — the original consumer (phase D), the
encryptcookie-equivalent porch 2 scoped out. - database field-at-rest — a plausible third, unbuilt until a workload asks.
Dependencies
- porch 2 —
random_bytes, for the cookie wrapper's random nonce (phase D only). The AEAD primitives themselves are self-contained. - AES-NI / ARMv8 crypto — compiler intrinsics, not an external dependency.
Out of scope
- Asymmetric crypto (RSA, ECDH, signatures). A different, much larger surface — owned by rv2 9's TLS phases C/D, not here.
- Key rotation, a KMS, envelope encryption. Operational key management is its own story if a consumer appears.
- A passphrase KDF (fork 4) — raw keys only; a KDF is a separate ask.
- Nonce-misuse-resistant modes (AES-GCM-SIV). The caller-supplied-nonce contract stands; misuse resistance is a later slice if a consumer needs it.
- Compression before encryption (the CRIME/BREACH interaction). A caller concern to document, not a primitive.
The earlier draft listed "TLS — proxy-terminated by doctrine" here. That doctrine was retired by rv2 9; TLS is now a first-class consumer of this cipher, which is what pulled AES-GCM into scope.
Risk and test strategy
Encryption code is get-it-exactly-right code, and the risk is timing side channels and a reused nonce:
- Constant-time is mandatory for every key/plaintext-dependent operation —
hardware AES/GHASH by construction, and the bitsliced fallback and ChaCha/
Poly1305 verified table-free and branch-free. The Poly1305/GHASH tag compare is
constant-time (
ct_eq-style). - Known-answer vectors gate every cipher: RFC 8439 for ChaCha20-Poly1305, the NIST GCM test vectors for AES-GCM, on both the hardware and software paths; plus a reference cross-check and an ASan/UBSan leg.
- The reused-nonce hazard is documented loudly. A key+nonce pair must never repeat; the cookie wrapper (phase D) draws a fresh random nonce per seal, and TLS owns its own per-record nonce discipline — the primitive trusts the caller and says so.
Info
Two named consumers now — TLS (rv2 9 phase A, the reason AES-GCM is in) and porch 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 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.