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:
parent
db25f3e3e0
commit
e1d29d63e4
11 changed files with 378 additions and 24 deletions
|
|
@ -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);
|
||||
|
|
|
|||
40
docs/examples/tls-server/main.wo
Normal file
40
docs/examples/tls-server/main.wo
Normal 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);
|
||||
}
|
||||
}
|
||||
7
justfile
7
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.
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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). */
|
||||
|
|
|
|||
|
|
@ -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
95
scripts/tls-server-accept.sh
Executable 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 ]]
|
||||
Loading…
Reference in a new issue