Commit graph

10 commits

Author SHA1 Message Date
6d6c810695 feat(tls): net.connect_tls/read_tls/write_tls builtins (rv2 9 F3c-net)
The outbound TLS 1.3 client wired into the VM (ids 115-117, WO_B_MAX->117):

- net.connect_tls(host,port)->Int: DNS + non-blocking connect+poll bounded
  by WO_TLS_HANDSHAKE_MS (decision 5), then a blocking, SO_*TIMEO-bounded
  hand-rolled handshake over the sans-io driver, then wo_tls_verify_chain
  (chain + host + validity + basicConstraints/EKU) against the shard's
  lazily-loaded read-only CA bundle (decision 4). Any failure traps WO_T_IO
  loudly (decision 3). Returns the fd.
- net.read_tls / net.write_tls: application data over the parked data plane
  (decision 1) — O_NONBLOCK + park on POLLIN/POLLOUT like net.read/write,
  with record reassembly + leftover-plaintext + in-flight-record buffers in
  the per-fd slot so a park/retry never re-seals or loses progress.
- per-shard wo_tls_conn slot table keyed by fd, no locks (one thread per
  shard, the wo_child pattern; decision 2); net.close frees the slot;
  wo_vm_destroy reaps all slots + the CA bundle. getrandom ephemeral.
- driver keeps the whole Certificate message + wo_tls_client_chain() so the
  trust walk sees the full chain, not just the leaf.
- wiring: wob.h, loader.c arities, builtin.c dispatch (second net range),
  types.ml (net.connect_tls/read_tls/write_tls), sysio.c impl.

Builds; full runtime suite 0 fail; woc builds. Live behaviour is the
Phase-4 gate (next commit).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 9a922b3245eaa9134efb60bfd46521952ed12a39)
2026-09-15 01:15:31 +02:00
040ec9c189 feat(tls): PEM trust-anchor decoder (rv2 9 F3c-net decision 4)
- wo_tls_pem_to_ders: scan a PEM bundle for CERTIFICATE blocks, base64-decode
  each into a caller arena, record DER spans as trust anchors for
  wo_tls_verify_chain. Pure (caller reads the file + owns the arena) so it is
  offline-testable; the file read + per-shard cache land with the builtin
- b64_decode helper (standard alphabet, skips whitespace/newlines)
- KAT: decode the real /etc/ssl/certs/ca-certificates.crt (>100 anchors,
  each parses, first is a CA), garbage PEM -> 0 with no over-read,
  skip-if-absent for CI. test_tls 107 pass, ASan/UBSan clean

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 6445d55aa83dbed831d84fd3cca3a74fe4609a00)
2026-09-15 01:15:31 +02:00
3a1ba15851 feat(tls): X.509 basicConstraints + EKU chain hardening (rv2 9 F3c-net decision 6)
- crypto.c: x509_find_ext (generic extension walker) + wo_x509_basic_constraints
  (cA / pathLenConstraint, absent => not a CA) + wo_x509_eku_serverauth_ok
  (EKU absent, serverAuth, or anyEKU => usable; else not)
- wo_tls_verify_chain enforces decision 6: the leaf must be server-usable
  (EKU), every server-sent issuer and the signing anchor must be a CA
  (basicConstraints CA:TRUE) with a pathLenConstraint covering the
  intermediates below it — stops a leaf masquerading as a CA
- gen_x509.py extended (folds in the wildcard leaf, adds EKU clientAuth-only,
  EKU serverAuth, a non-CA intermediate + a leaf issued under it); vectors
  regenerated
- KATs: extractors (test_crypto 104) + chain enforcement (test_tls 103) —
  EKU serverAuth accepted, clientAuth-only rejected, leaf-under-non-CA
  rejected though every signature verifies; existing chains still pass.
  ASan/UBSan clean

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 3811418014c2c8d9bc3a9464256f52104261b1b9)
2026-09-15 01:15:31 +02:00
5f0ac2d434 feat(tls): certificate chain validation (rv2 9 phase F3c-net security core)
- wo_tls_verify_chain: leaf-first DER chain — each cert signed by the
  next, the top trusted (equal to, or signed by, a trust anchor), the leaf
  SAN matching host, every cert temporally valid. Any failure rejects;
  no partial trust. Pure over the phase-D/E verifiers, so offline-testable
- KAT with the phase-E RSA + EC chains: leaf trusted via its issuing CA
  anchor; wrong-anchor / wrong-host / expired / broken-link / no-anchor
  all rejected; two-cert chain with a byte-equal root anchor; NULL host
  skips the SAN check. test_tls 100 pass, ASan/UBSan clean
- remaining F3c-net (live-gated): CA-bundle PEM loader, random ephemeral,
  the net.connect_tls builtin driving the sans-io driver over a real fd

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 9d40055108d301be32df0e1d4140c23446baa7c6)
2026-09-15 01:15:31 +02:00
d7a6888931 feat(tls): SAN/hostname verification + driver enforcement (rv2 9 phase E/F3c)
- wo_x509_check_host: match a hostname against the cert subjectAltName
  dNSNames (RFC 6125) — case-insensitive, single left-most wildcard that
  covers exactly one label; no SAN => refused; no legacy CN fallback.
  Completes the phase-E deferred hostname check (walks the [3] extensions)
- wo_tls_client_set_host + driver enforcement: with a host set, a leaf
  whose SAN does not match is refused at the Certificate step (MITM
  defense); unset skips the check (offline testing only, documented unsafe)
- KAT: exact/case-insensitive/mismatch, no-SAN refused, wildcard one-label
  (not zero, not sub-label) via a wildcard-SAN cert; driver refuses the
  RFC 8448 leaf (no SAN) once a host is set. test_crypto 95, test_tls 91,
  ASan/UBSan clean

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 319ce8bfcf608030f958b36ab2c8f62fd76e1740)
2026-09-15 01:15:31 +02:00
d7e7eff6b5 feat(tls): sans-io TLS 1.3 client handshake driver (rv2 9 phase F3c-core)
- wo_tls_client: a pure state machine (no sockets). Caller frames
  records; driver runs ClientHello->ServerHello->flight->Finished and
  hands back bytes to send. Keeps all I/O out of the security-critical FSM
- start_with (inject CH + ephemeral priv), push_record, take_output,
  encrypt/decrypt (application traffic keys). Handshake-message reassembly
  across records; per-message transcript timing (CertVerify signs CH..Cert,
  Finished MACs CH..CertVerify); constant-time Finished compare; every
  failure lands in FAILED (no warn-and-continue)
- verifies server CertificateVerify (phase E+D) + server Finished, emits
  the client Finished, switches to application keys
- KAT: whole handshake driven offline against the RFC 8448 record trace —
  client Finished record byte-for-byte, first client app record
  byte-for-byte, NewSessionTicket + server app data decrypt to plaintext,
  tampered flight -> FAILED. test_tls 90 pass, ASan/UBSan clean
- SECURITY TODO before live use (documented in tls.h + story): chain walk
  to a trust anchor + SAN/hostname match; random ephemeral for production
  start; the net.connect_tls socket glue

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 74c332d7efdb8bbfdbe90bd2fcc3defa2fe8da00)
2026-09-15 01:15:31 +02:00
74b153aa0a feat(tls): offline handshake verification (rv2 9 phase F3b)
- wo_tls_verify_cert_verify: verifies a server CertificateVerify
  (RFC 8446 §4.4.3) — builds the 64-space || context || 0x00 ||
  transcript-hash content, parses the leaf SPKI (phase E) and dispatches
  to phase-D RSA-PSS / RSA-PKCS1 / ECDSA-P256; the scheme must match the
  leaf key type. ECDSA sig r/s pulled from its DER SEQ
- reuses wo_tls_finished_verify (phase F2) for server + client Finished
- KAT: the whole handshake crypto driven offline from the RFC 8448 §3
  recorded messages — CertificateVerify (RSA-PSS) VALID, wrong-transcript
  / tampered-sig / mismatched-scheme rejected, server Finished byte-exact,
  and the client Finished we would send byte-exact. test_tls 78 pass,
  ASan/UBSan clean

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit afd9f23508c648322712df91329aa117c975ccab)
2026-09-15 01:15:31 +02:00
c375110aac feat(tls): TLS 1.3 handshake message layer (rv2 9 phase F3a)
- bounded wire reader/writer (malformation -> reject, overflow -> fail;
  no over-read on attacker-controlled bytes)
- wo_tls_parse_server_hello: extracts negotiated suite + server x25519
  key share; rejects HelloRetryRequest, unsupported suite/group,
  non-1.3 selected_version, and any truncation
- wo_tls_build_client_hello: ClientHello offering TLS 1.3 / x25519 /
  RSA-PSS+RSA-PKCS1+ECDSA-P256, SNI, 32-byte legacy session id
- KAT: ServerHello parser vs RFC 8448 recorded message (suite 0x1301 +
  server pubkey byte-exact), malformed rejected; ClientHello builder
  structural + SNI/keyshare present + too-small refused, and validated
  byte-for-byte spec-valid by an independent python parser. test_tls 71
  pass, ASan/UBSan clean

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 541c71bca1655f370b145621d272d2e8bdb6c7ce)
2026-09-15 01:15:31 +02:00
25dda4cb2f feat(tls): TLS 1.3 key schedule (rv2 9 phase F2)
- wo_tls_derive_handshake: Early/Handshake/Master secrets + client/server
  handshake-traffic secrets from the ECDHE shared secret and the
  ClientHello..ServerHello transcript hash (RFC 8446 §7.1)
- wo_tls_derive_application: client/server application-traffic secrets
  from master_secret + the ClientHello..server-Finished transcript hash
- wo_tls_traffic_keys: record key + IV via HKDF-Expand-Label "key"/"iv"
- wo_tls_finished_verify: finished_key = Expand-Label(base,"finished"),
  verify_data = HMAC(finished_key, transcript_hash)
- all over phase-B HKDF (Extract/Expand-Label) + Derive-Secret helper
- KAT vs RFC 8448 §3 "Simple 1-RTT Handshake" byte-for-byte: c/s hs
  traffic, master, c/s ap traffic, server hs key+iv. Also validates the
  phase-B "tls13 " Expand-Label. test_tls 58 pass, ASan/UBSan clean

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 417fcc16f80c1dd31e84a0336944573902fde06e)
2026-09-15 01:15:31 +02:00
7e71c1a171 feat(tls): TLS 1.3 record layer (rv2 9 phase F1)
- new tls.c/tls.h on the crypto ladder: wo_tls_record_seal/open
  (RFC 8446 §5.2) — TLSInnerPlaintext (content||type, no padding),
  5-byte header as AEAD additional-data, per-record nonce = iv XOR
  seq big-endian (§5.3)
- suite dispatch: TLS_AES_128_GCM_SHA256 (mandatory) +
  TLS_CHACHA20_POLY1305_SHA256 (AES-NI-less fallback), over phase-A AEAD
- open() strips trailing zero padding to recover the inner content type;
  rejects a length-field lie before the AEAD, and auth failure after
- KAT vs python AEAD oracle (test/gen_tls_record.py): sealed record
  byte-for-byte both suites, open() recovers it, 5-seq round-trip,
  tamper + wrong-seq + bad-suite rejected. test_tls 51 pass, ASan/UBSan

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 5021a99f8359f78a642bb0cc2b28ab66f6b624e2)
2026-09-15 01:15:31 +02:00