feat(tls): net.accept_tls — inbound TLS 1.3 termination (rv2 9 phase G3)

- 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 <noreply@anthropic.com>
(cherry picked from commit 2d4c30033c36c88de5b7ab7cc1042c9537238297)
This commit is contained in:
shoney.arickathil 2026-09-09 04:59:35 +02:00
parent db25f3e3e0
commit e1d29d63e4
11 changed files with 378 additions and 24 deletions

View file

@ -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);

View file

@ -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 <port> <certfile> <keyfile>");
return 2;
}
let port = parse_int(args[0]);
if port == nil { print_err("tls-server: <port> 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);
}
}

View file

@ -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.

View file

@ -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))

View file

@ -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

View file

@ -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];

View file

@ -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) {

View file

@ -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

View file

@ -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). */

View file

@ -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

95
scripts/tls-server-accept.sh Executable file
View file

@ -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 ]]