From e1d29d63e41d0f4ef87a96944ea31e47690a6eeb Mon Sep 17 00:00:00 2001 From: "shoney.arickathil" Date: Wed, 9 Sep 2026 04:59:35 +0200 Subject: [PATCH] =?UTF-8?q?feat(tls):=20net.accept=5Ftls=20=E2=80=94=20inb?= =?UTF-8?q?ound=20TLS=201.3=20termination=20(rv2=209=20phase=20G3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - net.accept_tls(listener, certfile, keyfile) -> Int (id 118, WO_B_MAX->118): accept (parks like net.accept), load+cache the server identity per path in the shard, run the blocking deadline-bounded server handshake, return a TLS conn fd. Real clients terminate against the runtime — no front proxy - wo_tls_conn refactored: holds the negotiated application keys (not an embedded driver), so read_tls/write_tls serve both client and server connections via the record layer; the handshake drivers are transient (heap, ~100KB, freed after). net.close drains a TLS conn's inbound before close() so it sends FIN not RST (clients send close_notify) - server handshake loops past the client's change_cipher_spec (TLS 1.3 middlebox-compat) before its Finished — the openssl-interop fix - private-key file loading: wo_tls_pem_one (any-label PEM block) + wo_pkey_parse; per-shard identity cache (vm->tls_id), freed in reap - docs/examples/tls-server + `just tls-server`: openssl s_client validates our hand-rolled server (EC + RSA certs) and gets the reply — 4/0; the outbound `just tls` gate stays 5/0 through the refactor - wiring: wob.h, loader.c, builtin.c dispatch, types.ml, vm.h Co-Authored-By: Claude Opus 4.8 (cherry picked from commit 2d4c30033c36c88de5b7ab7cc1042c9537238297) --- compiler/src/types.ml | 1 + docs/examples/tls-server/main.wo | 40 ++++++ justfile | 7 + runtime/src/builtin.c | 2 +- runtime/src/loader.c | 1 + runtime/src/sysio.c | 230 ++++++++++++++++++++++++++++--- runtime/src/tls.c | 19 +++ runtime/src/tls.h | 3 + runtime/src/vm.h | 1 + runtime/src/wob.h | 3 +- scripts/tls-server-accept.sh | 95 +++++++++++++ 11 files changed, 378 insertions(+), 24 deletions(-) create mode 100644 docs/examples/tls-server/main.wo create mode 100755 scripts/tls-server-accept.sh diff --git a/compiler/src/types.ml b/compiler/src/types.ml index f9316dd..4985495 100644 --- a/compiler/src/types.ml +++ b/compiler/src/types.ml @@ -368,6 +368,7 @@ let stdlib_members : stdlib_member list = m "net" "connect_tls" 2 115 (Some (TScalar "Int")) None; m "net" "read_tls" 2 116 (Some (TScalar "Text")) None; m "net" "write_tls" 2 117 None None; + m "net" "accept_tls" 3 118 (Some (TScalar "Int")) None; (* runtime-v2 6: resize's read twin (nil = not a tty), and a codepoint's terminal cell width (libc wcwidth under C.UTF-8) *) m "term" "size" 1 108 (Some (TNullable (TScalar termsize_record_name))) (Some termsize_record_name); diff --git a/docs/examples/tls-server/main.wo b/docs/examples/tls-server/main.wo new file mode 100644 index 0000000..5efd37a --- /dev/null +++ b/docs/examples/tls-server/main.wo @@ -0,0 +1,40 @@ +-- tls-server — runtime-v2 9 phase G's acceptance workload. An inbound HTTPS +-- server written end to end in .wo: it terminates TLS 1.3 itself with +-- `net.accept_tls` (the hand-rolled server handshake — X25519 + AES-GCM / +-- ChaCha20-Poly1305 + a server-signed CertificateVerify), then serves a fixed +-- reply over `net.read_tls` / `net.write_tls`. No front proxy — the runtime is +-- the TLS endpoint. +-- +-- woc --emit main.wo -o tls-server.wob +-- wovm tls-server.wob 18443 leaf.pem leaf.key +-- +-- The gate (scripts/tls-server-accept.sh, `just tls-server`) points +-- `openssl s_client` at it (RSA and EC identities) and checks the handshake +-- validates and the reply arrives. +use net +use env + +fn main(args: multi Text) -> Int { + if len(args) < 3 { + print_err("usage: tls-server "); + return 2; + } + let port = parse_int(args[0]); + if port == nil { print_err("tls-server: must be a number"); return 2; } + let cert = args[1]; + let key = args[2]; + + let srv = net.listen("127.0.0.1", port); + print("listening on 127.0.0.1:${port}"); + while true { + if env.stopping() { net.close(srv); return 0; } + -- accept + terminate TLS; a failed handshake is caught and skipped, never + -- fatal to the server. + let c = try net.accept_tls(srv, cert, key) catch (e) -1; + if c < 0 { continue; } + let req = try net.read_tls(c, 2048) catch (e) ""; + let body = "hello-wo-tls"; + net.write_tls(c, "HTTP/1.0 200 OK\r\nContent-Length: ${len(body)}\r\nConnection: close\r\n\r\n${body}"); + net.close(c); + } +} diff --git a/justfile b/justfile index 6e4b51e..3b1843b 100644 --- a/justfile +++ b/justfile @@ -96,6 +96,13 @@ subprocess: tls: ./scripts/tls-accept.sh +# tls-server: runtime-v2 9 phase G's gate (docs/examples/tls-server) — +# net.accept_tls from .wo terminating TLS 1.3 itself, proven by openssl +# s_client (EC + RSA server certs) validating the hand-rolled handshake and +# getting the reply. No front proxy. Log: /tmp/tls-server.log. +tls-server: + ./scripts/tls-server-accept.sh + # db-bench: iteration 22's campaign (docs/examples/db-bench) — OFF the # fast path, minutes long: ram+durable x 1/N shards, durability legs, # gates vs bench/baseline.json. quick = seconds, floors only. diff --git a/runtime/src/builtin.c b/runtime/src/builtin.c index 8467b70..7a70fff 100644 --- a/runtime/src/builtin.c +++ b/runtime/src/builtin.c @@ -173,7 +173,7 @@ int wo_builtin(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) { return wo_builtin_json(vm, R, ins, msg); if ((C >= WO_B_SYS_FIRST && C <= WO_B_PROC_RUN) || C == WO_B_TIME_TICKS || (C >= WO_B_NET_READ_DL && C <= WO_B_NET_CONNECT) - || (C >= WO_B_NET_CONNECT_TLS && C <= WO_B_NET_WRITE_TLS)) + || (C >= WO_B_NET_CONNECT_TLS && C <= WO_B_NET_ACCEPT_TLS)) return wo_builtin_sys(vm, R, ins, msg); if ((C >= WO_B_SHA1 && C <= WO_B_HMAC_SHA256) || (C >= WO_B_CHACHA20POLY1305_SEAL && C <= WO_B_AES_GCM_OPEN)) diff --git a/runtime/src/loader.c b/runtime/src/loader.c index 3eabf17..4792a2a 100644 --- a/runtime/src/loader.c +++ b/runtime/src/loader.c @@ -80,6 +80,7 @@ static const uint8_t b_arity[WO_B_MAX + 1] = { [WO_B_NET_SEND_FD] = 2, [WO_B_NET_RECV_FD] = 1, [WO_B_NET_CONNECT_UNIX] = 1, [WO_B_TERM_SIZE] = 2, [WO_B_TERM_WIDTH] = 1, [WO_B_NET_CONNECT] = 2, [WO_B_NET_CONNECT_TLS] = 2, [WO_B_NET_READ_TLS] = 2, [WO_B_NET_WRITE_TLS] = 2, + [WO_B_NET_ACCEPT_TLS] = 3, [WO_B_CHACHA20POLY1305_SEAL] = 4, [WO_B_CHACHA20POLY1305_OPEN] = 4, [WO_B_AES_GCM_SEAL] = 4, [WO_B_AES_GCM_OPEN] = 4, /* json (json.c): encode takes the value's static kind, decode the class diff --git a/runtime/src/sysio.c b/runtime/src/sysio.c index 58d56e5..669430e 100644 --- a/runtime/src/sysio.c +++ b/runtime/src/sysio.c @@ -391,13 +391,27 @@ static int proc_drain_fd(int *fd, char **buf, size_t *len, size_t *alloc, typedef struct wo_tls_conn { int fd; - wo_tls_client cli; char host[256]; + /* negotiated application-phase state (populated by the client OR server + * handshake; the data plane below is direction-agnostic). */ + int suite; size_t keylen; + uint8_t rd_key[32], rd_iv[12]; uint64_t rd_seq; + uint8_t wr_key[32], wr_iv[12]; uint64_t wr_seq; uint8_t rbuf[WO_TLS_REC_MAX]; size_t rbn; /* partial inbound record */ uint8_t pbuf[WO_TLS_BUF_MAX]; size_t pboff, pbn; /* decrypted, unconsumed */ uint8_t wbuf[WO_TLS_REC_MAX]; size_t wblen, wboff; /* outbound record in flight */ } wo_tls_conn; +/* A server's loaded identity (cert chain + private key), cached per shard. */ +struct wo_tls_id { + char cert[512], key[512]; + uint8_t *arena; /* cert DERs + key DER */ + const uint8_t *chain[8]; size_t clen[8]; size_t nchain; + int alg; /* WO_X509_KEY_RSA / _EC_P256 */ + const uint8_t *n, *d; size_t nlen, dlen; /* RSA */ + const uint8_t *ec_d; int has_ec; /* EC (32-byte scalar) */ +}; + static wo_tls_conn *tls_find(wo_vm *vm, int fd) { for (uint32_t i = 0; i < WO_TLS_MAX; i++) if (vm->tls[i] && vm->tls[i]->fd == fd) return vm->tls[i]; @@ -429,6 +443,10 @@ void wo_tls_reap_all(wo_vm *vm) { free((void *)vm->ca_certs); vm->ca_certs = NULL; free(vm->ca_lens); vm->ca_lens = NULL; vm->ca_count = 0; vm->ca_loaded = 0; + if (vm->tls_id) { + struct wo_tls_id *id = vm->tls_id; + free(id->arena); free(id); vm->tls_id = NULL; + } } static int tls_rand(uint8_t *buf, size_t n) { @@ -505,8 +523,20 @@ static int tls_recv_record(int fd, uint8_t *buf, size_t cap) { return (int)(5 + body); } -/* Drive the blocking handshake on conn->fd to ESTABLISHED, then validate the - * chain against the shard anchors. 0 ok, -1 on any failure (*msg set). */ +/* Copy a finished driver's application-phase keys into the connection slot; + * after this the data plane runs off the slot alone. rd/wr are the driver's + * post-ESTABLISHED read/write keys (already the app keys, seqs reset). */ +static void tls_conn_keys(wo_tls_conn *conn, int suite, size_t keylen, + const uint8_t rd_key[32], const uint8_t rd_iv[12], + const uint8_t wr_key[32], const uint8_t wr_iv[12]) { + conn->suite = suite; conn->keylen = keylen; + memcpy(conn->rd_key, rd_key, 32); memcpy(conn->rd_iv, rd_iv, 12); conn->rd_seq = 0; + memcpy(conn->wr_key, wr_key, 32); memcpy(conn->wr_iv, wr_iv, 12); conn->wr_seq = 0; +} + +/* Drive the blocking client handshake on conn->fd to ESTABLISHED, validate the + * chain against the shard anchors, and copy the app keys into conn. The driver + * is ~100KB, so it lives on the heap for the handshake only. 0 ok, -1 (*msg). */ static int tls_handshake(wo_vm *vm, wo_tls_conn *conn, size_t hostlen, const char **msg) { uint8_t priv[32], pub[32], rnd[32], sid[32], base9[32] = { 9 }; @@ -515,38 +545,136 @@ static int tls_handshake(wo_vm *vm, wo_tls_conn *conn, size_t hostlen, *msg = "tls: getrandom failed"; return -1; } wo_x25519(pub, priv, base9); + wo_tls_client *c = calloc(1, sizeof *c); + if (!c) { *msg = "tls: out of memory"; return -1; } + int rv = -1; if (wo_tls_build_client_hello(conn->host, hostlen, pub, rnd, sid, ch, sizeof ch, &chlen) != 0 - || wo_tls_client_start_with(&conn->cli, ch, chlen, priv) != 0) { - *msg = "tls: ClientHello build failed"; return -1; + || wo_tls_client_start_with(c, ch, chlen, priv) != 0) { + *msg = "tls: ClientHello build failed"; goto done; } - wo_tls_client_set_host(&conn->cli, conn->host, hostlen); + wo_tls_client_set_host(c, conn->host, hostlen); uint8_t rec[WO_TLS_REC_MAX]; size_t rn; - rn = wo_tls_client_take_output(&conn->cli, rec, sizeof rec); + rn = wo_tls_client_take_output(c, rec, sizeof rec); if (rn == 0 || tls_send_all(conn->fd, rec, rn) != 0) { - *msg = "tls: sending ClientHello failed"; return -1; + *msg = "tls: sending ClientHello failed"; goto done; } for (;;) { int rl = tls_recv_record(conn->fd, rec, sizeof rec); - if (rl < 0) { *msg = "tls: handshake read failed or timed out"; return -1; } - wo_tls_status st = wo_tls_client_push_record(&conn->cli, rec, (size_t)rl); - if (st == WO_TLS_FAILED) { *msg = "tls: handshake verification failed"; return -1; } - rn = wo_tls_client_take_output(&conn->cli, rec, sizeof rec); + if (rl < 0) { *msg = "tls: handshake read failed or timed out"; goto done; } + wo_tls_status st = wo_tls_client_push_record(c, rec, (size_t)rl); + if (st == WO_TLS_FAILED) { *msg = "tls: handshake verification failed"; goto done; } + rn = wo_tls_client_take_output(c, rec, sizeof rec); if (rn > 0 && tls_send_all(conn->fd, rec, rn) != 0) { - *msg = "tls: handshake write failed"; return -1; + *msg = "tls: handshake write failed"; goto done; } if (st == WO_TLS_ESTABLISHED) break; } /* trust: full chain + host + validity + basicConstraints/EKU vs anchors */ const uint8_t *chain[16]; size_t clens[16]; - size_t nchain = wo_tls_client_chain(&conn->cli, chain, clens, 16); + size_t nchain = wo_tls_client_chain(c, chain, clens, 16); char now[15]; tls_now14(now); if (nchain == 0 || !wo_tls_verify_chain(chain, clens, nchain, vm->ca_certs, vm->ca_lens, vm->ca_count, conn->host, hostlen, now)) { - *msg = "tls: certificate chain not trusted"; return -1; + *msg = "tls: certificate chain not trusted"; goto done; } - return 0; + tls_conn_keys(conn, c->suite, c->keylen, c->rd_key, c->rd_iv, c->wr_key, c->wr_iv); + rv = 0; +done: + free(c); + return rv; +} + +/* ---- server side: identity cache + handshake (phase G3) ---- */ +static char *read_file(const char *path, size_t *len) { + FILE *f = fopen(path, "rb"); + if (!f) return NULL; + fseek(f, 0, SEEK_END); long sz = ftell(f); fseek(f, 0, SEEK_SET); + if (sz <= 0) { fclose(f); return NULL; } + char *b = malloc((size_t)sz); + size_t got = b ? fread(b, 1, (size_t)sz, f) : 0; + fclose(f); + if (!b) return NULL; + *len = got; return b; +} + +/* Load (or reuse) the shard's server identity for (certfile, keyfile). A single + * entry — the common one-identity server; a different path reloads. */ +static struct wo_tls_id *tls_id_load(wo_vm *vm, const char *certfile, + const char *keyfile, const char **msg) { + struct wo_tls_id *id = vm->tls_id; + if (id && strcmp(id->cert, certfile) == 0 && strcmp(id->key, keyfile) == 0) + return id; + if (!id) { id = calloc(1, sizeof *id); if (!id) { *msg = "tls: oom"; return NULL; } vm->tls_id = id; } + else { free(id->arena); memset(id, 0, sizeof *id); } + + size_t certlen = 0, keylen = 0; + char *certpem = read_file(certfile, &certlen); + if (!certpem) { *msg = "tls: cannot read certfile"; return NULL; } + char *keypem = read_file(keyfile, &keylen); + if (!keypem) { free(certpem); *msg = "tls: cannot read keyfile"; return NULL; } + + id->arena = malloc(certlen + keylen); /* DER < PEM */ + if (!id->arena) { free(certpem); free(keypem); *msg = "tls: oom"; return NULL; } + long nc = wo_tls_pem_to_ders(certpem, certlen, id->arena, certlen, + id->chain, id->clen, 8); + long kd = nc > 0 ? wo_tls_pem_one(keypem, keylen, id->arena + certlen, keylen) : -1; + free(certpem); free(keypem); + if (nc <= 0 || kd <= 0) { *msg = "tls: bad cert/key PEM"; return NULL; } + id->nchain = (size_t)nc; + + if (wo_pkey_parse(id->arena + certlen, (size_t)kd, &id->alg, &id->n, &id->nlen, + &id->d, &id->dlen, &id->ec_d) != 0) { + *msg = "tls: bad private key"; return NULL; + } + id->has_ec = (id->alg == 2); + snprintf(id->cert, sizeof id->cert, "%s", certfile); + snprintf(id->key, sizeof id->key, "%s", keyfile); + return id; +} + +/* Drive the blocking server handshake on conn->fd to ESTABLISHED, then copy the + * app keys into conn. 0 ok, -1 (*msg). */ +static int tls_server_handshake(wo_tls_conn *conn, struct wo_tls_id *id, + const char **msg) { + uint8_t eph[32], salt[32]; + if (tls_rand(eph, 32) || tls_rand(salt, 32)) { *msg = "tls: getrandom failed"; return -1; } + wo_tls_server *s = calloc(1, sizeof *s); + if (!s) { *msg = "tls: out of memory"; return -1; } + int rv = -1; + if (wo_tls_server_start(s, id->chain, id->clen, id->nchain, id->alg, + id->n, id->nlen, id->d, id->dlen, + id->has_ec ? id->ec_d : NULL, eph, + id->alg == 1 ? salt : NULL, id->alg == 1 ? 32u : 0u) != 0) { + *msg = "tls: server init failed"; goto done; + } + uint8_t rec[WO_TLS_REC_MAX], out[WO_TLS_BUF_MAX + 256]; + int rl = tls_recv_record(conn->fd, rec, sizeof rec); + if (rl < 0) { *msg = "tls: reading ClientHello failed/timeout"; goto done; } + if (wo_tls_server_push_record(s, rec, (size_t)rl) == WO_TLS_FAILED) { + *msg = "tls: ClientHello rejected"; goto done; + } + size_t on = wo_tls_server_take_output(s, out, sizeof out); + if (on == 0 || tls_send_all(conn->fd, out, on) != 0) { + *msg = "tls: sending server flight failed"; goto done; + } + /* Read post-flight client records until ESTABLISHED — a TLS 1.3 client + * (openssl, browsers) sends a change_cipher_spec before its Finished, which + * the driver skips (WANT_MORE); loop past it rather than mistaking it for + * failure. */ + for (;;) { + rl = tls_recv_record(conn->fd, rec, sizeof rec); + if (rl < 0) { *msg = "tls: reading client Finished failed"; goto done; } + wo_tls_status st = wo_tls_server_push_record(s, rec, (size_t)rl); + if (st == WO_TLS_FAILED) { *msg = "tls: client Finished failed"; goto done; } + if (st == WO_TLS_ESTABLISHED) break; + } + tls_conn_keys(conn, s->suite, s->keylen, s->rd_key, s->rd_iv, s->wr_key, s->wr_iv); + rv = 0; +done: + free(s); + return rv; } int wo_builtin_sys(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) { @@ -900,8 +1028,19 @@ int wo_builtin_sys(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) { return 0; } case WO_B_NET_CLOSE: { - tls_free(vm, (int)R[B]); /* frees the TLS slot if this was one (else no-op) */ - close((int)R[B]); + int cfd = (int)R[B]; + if (tls_find(vm, cfd)) { + /* Drain any unread inbound (typically the peer's close_notify) so + * close() sends FIN, not RST — otherwise the RST discards the + * response we just wrote (openssl and browsers send close_notify). */ + uint8_t d[512]; int guard = 64; + while (guard-- > 0) { + ssize_t r = recv(cfd, d, sizeof d, MSG_DONTWAIT); + if (r <= 0) break; + } + tls_free(vm, cfd); + } + close(cfd); R[A] = 0; return 0; } @@ -1887,8 +2026,10 @@ int wo_builtin_sys(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) { } size_t rlen = 5 + (((size_t)conn->rbuf[3] << 8) | conn->rbuf[4]); uint8_t ct = 0; - int dn = wo_tls_client_decrypt(&conn->cli, conn->rbuf, rlen, - conn->pbuf, sizeof conn->pbuf, &ct); + int dn = wo_tls_record_open(conn->suite, conn->rd_key, conn->keylen, + conn->rd_iv, conn->rd_seq, conn->rbuf, rlen, + conn->pbuf, &ct); + conn->rd_seq++; memmove(conn->rbuf, conn->rbuf + rlen, conn->rbn - rlen); conn->rbn -= rlen; if (dn < 0) { *msg = "tls: bad record (auth failure)"; return WO_T_IO; } @@ -1914,9 +2055,13 @@ int wo_builtin_sys(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) { * neither re-seals (which would advance the record seq twice) nor loses * a partial write's progress. */ if (conn->wblen == 0) { - int sn = wo_tls_client_encrypt(&conn->cli, (const uint8_t *)body->data, - body->len, conn->wbuf, sizeof conn->wbuf); + int sn = wo_tls_record_seal(conn->suite, conn->wr_key, conn->keylen, + conn->wr_iv, conn->wr_seq, + WO_TLS_CT_APPLICATION_DATA, + (const uint8_t *)body->data, body->len, + conn->wbuf); if (sn < 0) { *msg = "tls: encrypt failed"; return WO_T_IO; } + conn->wr_seq++; conn->wblen = (size_t)sn; conn->wboff = 0; } while (conn->wboff < conn->wblen) { @@ -1939,6 +2084,47 @@ int wo_builtin_sys(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) { R[A] = 0; return 0; } + case WO_B_NET_ACCEPT_TLS: { /* (listener, certfile, keyfile) -> Int */ + int lfd = (int)R[B]; + char certf[512], keyf[512]; + if (cstr_of(R[B + 1], certf, sizeof certf, msg)) return WO_T_BOUNDS; + if (cstr_of(R[B + 2], keyf, sizeof keyf, msg)) return WO_T_BOUNDS; + struct wo_tls_id *id = tls_id_load(vm, certf, keyf, msg); + if (!id) return WO_T_IO; + + int fd; + for (;;) { + fd = accept(lfd, NULL, NULL); + if (fd >= 0) break; + if (errno == EINTR) { if (stop_pending()) return WO_SYS_STOPPED; continue; } + if (errno == EAGAIN || errno == EWOULDBLOCK) { /* park like net.accept */ + if (stop_pending()) return WO_SYS_STOPPED; + vm->cur->park_fd = lfd; vm->cur->park_deadline = 0; + vm->cur->park_events = POLLIN; vm->cur->park_done = 0; + return WO_SYS_PARKED; + } + *msg = strerror(errno); return WO_T_IO; + } + /* accept()'s new fd is blocking; bound the handshake with SO_*TIMEO */ + long dl_ms = 10000; + const char *denv = getenv("WO_TLS_HANDSHAKE_MS"); + if (denv) { long v = atol(denv); if (v > 0) dl_ms = v; } + struct timeval tv = { dl_ms / 1000, (dl_ms % 1000) * 1000 }; + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv); + setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv); + + wo_tls_conn *conn = tls_claim(vm, fd); + if (!conn) { close(fd); *msg = "tls: too many connections"; return WO_T_IO; } + if (tls_server_handshake(conn, id, msg) != 0) { + tls_free(vm, fd); close(fd); return WO_T_IO; + } + struct timeval z = { 0, 0 }; + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &z, sizeof z); + setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &z, sizeof z); + fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK); + R[A] = (uint64_t)fd; + return 0; + } case WO_B_NET_SEND_FD: { /* (conn, fd) -> Bool: SCM_RIGHTS, one fd */ int conn = (int)(int64_t)R[B]; int pass = (int)(int64_t)R[B + 1]; diff --git a/runtime/src/tls.c b/runtime/src/tls.c index 687f149..ba35b4c 100644 --- a/runtime/src/tls.c +++ b/runtime/src/tls.c @@ -713,6 +713,25 @@ static long b64_decode(const uint8_t *in, size_t inlen, uint8_t *out, size_t out * and record its span in certs[]/cert_lens[]. Returns the count (0..max_certs), * or -1 on arena overflow or a malformed block. Extra certs past max_certs are * silently ignored — the caller sizes max_certs to the bundle. */ +/* Decode the FIRST PEM block of any label (e.g. a "PRIVATE KEY" file) into out. + * Returns the DER length or -1. */ +long wo_tls_pem_one(const char *pem, size_t pemlen, uint8_t *out, size_t outcap) { + static const char B[] = "-----BEGIN "; + static const char E[] = "\n-----END "; + size_t i = 0; const char *b = NULL; + for (; i + sizeof B - 1 <= pemlen; i++) + if (memcmp(pem + i, B, sizeof B - 1) == 0) { b = pem + i; break; } + if (!b) return -1; + /* skip to the end of the BEGIN line */ + while (i < pemlen && pem[i] != '\n') i++; + size_t body = i; + const char *e = NULL; + for (; i + sizeof E - 1 <= pemlen; i++) + if (memcmp(pem + i, E, sizeof E - 1) == 0) { e = pem + i; break; } + if (!e) return -1; + return b64_decode((const uint8_t *)pem + body, (size_t)(e - (pem + body)), out, outcap); +} + long wo_tls_pem_to_ders(const char *pem, size_t pemlen, uint8_t *arena, size_t arena_cap, const uint8_t **certs, size_t *cert_lens, size_t max_certs) { diff --git a/runtime/src/tls.h b/runtime/src/tls.h index 53b5a1f..54b660e 100644 --- a/runtime/src/tls.h +++ b/runtime/src/tls.h @@ -129,6 +129,9 @@ int wo_tls_verify_chain(const uint8_t *const *certs, const size_t *cert_lens, long wo_tls_pem_to_ders(const char *pem, size_t pemlen, uint8_t *arena, size_t arena_cap, const uint8_t **certs, size_t *cert_lens, size_t max_certs); +/* Decode the first PEM block of any label (a private-key file) into out; + * returns the DER length or -1. */ +long wo_tls_pem_one(const char *pem, size_t pemlen, uint8_t *out, size_t outcap); /* ---- sans-io client handshake driver (phase F3c) ------------------------- * A pure state machine: no sockets. The caller frames TLS records (read the diff --git a/runtime/src/vm.h b/runtime/src/vm.h index 1108ca5..4eaabe1 100644 --- a/runtime/src/vm.h +++ b/runtime/src/vm.h @@ -318,6 +318,7 @@ typedef struct wo_vm { const uint8_t **ca_certs; size_t *ca_lens; size_t ca_count; + void *tls_id; /* server identity cache (sysio owns it) */ } wo_vm; /* arc: the spawn/send builtins' runtime halves (vm.c owns the scheduler). */ diff --git a/runtime/src/wob.h b/runtime/src/wob.h index 47ba245..eb5e090 100644 --- a/runtime/src/wob.h +++ b/runtime/src/wob.h @@ -568,9 +568,10 @@ enum { WO_B_NET_CONNECT_TLS = 115, /* (host, port) -> Int: TLS client fd */ WO_B_NET_READ_TLS = 116, /* (fd, max) -> Text; empty = EOF */ WO_B_NET_WRITE_TLS = 117, /* (fd, text) -> 0 (all bytes sealed + sent) */ + WO_B_NET_ACCEPT_TLS = 118, /* (listener, certfile, keyfile) -> Int: TLS conn */ }; -#define WO_B_MAX 117u +#define WO_B_MAX 118u /* ids at or above this one live in sysio.c, not builtin.c */ #define WO_B_SYS_FIRST WO_B_FS_EXISTS diff --git a/scripts/tls-server-accept.sh b/scripts/tls-server-accept.sh new file mode 100755 index 0000000..a44aac3 --- /dev/null +++ b/scripts/tls-server-accept.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +# scripts/tls-server-accept.sh — runtime-v2 9 phase G's gate: inbound TLS 1.3. +# 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. +set -uo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +WOC="$ROOT/compiler/_build/default/bin/woc" +WOVM="$ROOT/runtime/wovm" +APP="$ROOT/docs/examples/tls-server" +PORT="${TLS_SERVER_PORT:-18553}" +LOG=/tmp/tls-server.log + +pass=0; fail=0 +ok() { echo "ok $1"; pass=$((pass + 1)); } +bad() { echo "FAIL $1"; fail=$((fail + 1)); } + +WORK="$(mktemp -d)" +SRV_PID="" +cleanup() { [[ -n "$SRV_PID" ]] && kill -KILL "$SRV_PID" 2>/dev/null; rm -rf "$WORK"; } +trap cleanup EXIT +mkdir -p "$WORK/data" + +[[ -x "$WOVM" ]] || { echo "tls-server: wovm not built — run: make -C runtime wovm" >&2; exit 1; } +[[ -x "$WOC" ]] || { echo "tls-server: woc not built — run: just woc-build" >&2; exit 1; } +command -v openssl >/dev/null || { echo "tls-server: needs openssl" >&2; exit 1; } +python3 -c 'import cryptography' 2>/dev/null || { echo "tls-server: needs python3 cryptography" >&2; exit 1; } + +# ---- 1. a test CA + an EC leaf and an RSA leaf (SAN localhost) ------------ +cat > "$WORK/mk.py" <<'PY' +import datetime, sys +from cryptography import x509 +from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID +from cryptography.hazmat.primitives import hashes, serialization as ser +from cryptography.hazmat.primitives.asymmetric import ec, rsa +d = sys.argv[1] +NB = datetime.datetime(2020,1,1); NA = datetime.datetime(2035,1,1) +def nm(cn): return x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, cn)]) +cak = ec.generate_private_key(ec.SECP256R1()) +ca = (x509.CertificateBuilder().subject_name(nm("wo-test-ca")).issuer_name(nm("wo-test-ca")) + .public_key(cak.public_key()).serial_number(x509.random_serial_number()) + .not_valid_before(NB).not_valid_after(NA) + .add_extension(x509.BasicConstraints(ca=True, path_length=None), True).sign(cak, hashes.SHA256())) +open(d+"/ca.pem","wb").write(ca.public_bytes(ser.Encoding.PEM)) +def leaf(key, tag): + c = (x509.CertificateBuilder().subject_name(nm("localhost")).issuer_name(ca.subject) + .public_key(key.public_key()).serial_number(x509.random_serial_number()) + .not_valid_before(NB).not_valid_after(NA) + .add_extension(x509.SubjectAlternativeName([x509.DNSName("localhost")]), False) + .add_extension(x509.ExtendedKeyUsage([ExtendedKeyUsageOID.SERVER_AUTH]), False) + .sign(cak, hashes.SHA256())) + open(d+"/"+tag+".pem","wb").write(c.public_bytes(ser.Encoding.PEM)) + open(d+"/"+tag+".key","wb").write(key.private_bytes(ser.Encoding.PEM, + ser.PrivateFormat.PKCS8, ser.NoEncryption())) +leaf(ec.generate_private_key(ec.SECP256R1()), "ec") +leaf(rsa.generate_private_key(public_exponent=65537, key_size=2048), "rsa") +PY +python3 "$WORK/mk.py" "$WORK" || { bad "cert generation"; echo "tls-server: $fail failures"; exit 1; } +ok "test CA + EC leaf + RSA leaf (SAN localhost, EKU serverAuth) generated" + +# ---- 2. build the server ------------------------------------------------- +{ echo; echo "== tls-server-accept $(date -Is) port $PORT =="; } >>"$LOG" +if "$WOC" --emit "$APP" -o "$WORK/srv.wob" 2>"$WORK/cerr"; then + ok "build: tls-server compiles" +else + bad "build: $(head -3 "$WORK/cerr")"; echo "tls-server: $fail failures"; exit 1 +fi + +# ---- 3. openssl s_client interop, per key type --------------------------- +probe() { # $1 = tag (ec|rsa) ; $2 = port (distinct per probe — avoids a bind race) + 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 & + SRV_PID=$!; disown "$SRV_PID" 2>/dev/null || true + for _ in $(seq 1 100); do + if ( exec 3<>"/dev/tcp/127.0.0.1/$p" ) 2>/dev/null; then break; fi + sleep 0.05 + done + 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)" + if [[ "$out" == *"hello-wo-tls"* ]]; then + ok "$1: openssl s_client validated the cert + got the reply" + else + bad "$1: no reply (out: ${out:0:80})" + fi +} +probe ec "$PORT" +probe rsa "$((PORT + 1))" + +echo "tls-server: $((pass + fail)) checks, $fail failures" +[[ $fail -eq 0 ]]