test(tls): live acceptance gate for net.connect_tls (rv2 9 F3c-net phase 4)
- docs/examples/tls-client/main.wo: an outbound HTTPS client in .wo — net.connect_tls, write_tls a request, read_tls to EOF, print; connect failure caught with try/catch and reported (never a silent downgrade) - scripts/tls-accept.sh + `just tls`: dials a local TLS 1.3 stub (python ssl, TLS1.3-only) with a generated test CA — proves the hand-rolled handshake + chain/host validation + an app round-trip end to end from .wo through the compiler, and refuses the untrusted-chain and hostname-mismatch negatives. No live network; log /tmp/tls.log - gate: 5 checks, 0 failures Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> (cherry picked from commit 3d8bb140ec478167a4e660adf2caa964ff410ff1)
This commit is contained in:
parent
6d6c810695
commit
72ed35773d
3 changed files with 207 additions and 0 deletions
50
docs/examples/tls-client/main.wo
Normal file
50
docs/examples/tls-client/main.wo
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
-- tls-client — runtime-v2 9 F3c-net's acceptance workload. An outbound HTTPS
|
||||
-- client, written end to end in .wo: it dials a TLS 1.3 server with
|
||||
-- `net.connect_tls`, which runs the hand-rolled handshake (X25519 + AES-GCM /
|
||||
-- ChaCha20-Poly1305 + the RFC 8446 key schedule), validates the certificate
|
||||
-- chain to a trust anchor and matches the hostname (SAN), then carries
|
||||
-- application bytes over `net.write_tls` / `net.read_tls`.
|
||||
--
|
||||
-- woc --emit main.wo -o tls-client.wob
|
||||
-- WO_CA_BUNDLE=ca.pem wovm tls-client.wob localhost 18443
|
||||
--
|
||||
-- A connection whose chain does not chain to a trusted anchor, whose SAN does
|
||||
-- not match the host, or that is expired, is refused loudly (the connect traps,
|
||||
-- caught here). The gate (scripts/tls-accept.sh, `just tls`) proves the happy
|
||||
-- path and those negatives against a local stub — no live network.
|
||||
use net
|
||||
|
||||
fn main(args: multi Text) -> Int {
|
||||
if len(args) < 2 {
|
||||
print_err("usage: tls-client <host> <port>");
|
||||
return 2;
|
||||
}
|
||||
let host = args[0];
|
||||
let port = parse_int(args[1]);
|
||||
if port == nil {
|
||||
print_err("tls-client: <port> must be a number");
|
||||
return 2;
|
||||
}
|
||||
|
||||
-- connect_tls traps on DNS/connect/handshake/chain/hostname failure — a
|
||||
-- secure connection is never silently downgraded, so we catch and report.
|
||||
let fd = try net.connect_tls(host, port) catch (e) -1;
|
||||
if fd < 0 {
|
||||
print("tls: refused (handshake, chain, or hostname)");
|
||||
return 1;
|
||||
}
|
||||
|
||||
net.write_tls(fd, "GET / HTTP/1.0\r\nHost: ${host}\r\n\r\n");
|
||||
|
||||
let acc = "";
|
||||
while true {
|
||||
let chunk = try net.read_tls(fd, 4096) catch (e) "";
|
||||
if len(chunk) == 0 { break; }
|
||||
acc = acc .. chunk;
|
||||
}
|
||||
net.close(fd);
|
||||
|
||||
print("recv ${len(acc)} bytes");
|
||||
print(acc);
|
||||
return 0;
|
||||
}
|
||||
7
justfile
7
justfile
|
|
@ -89,6 +89,13 @@ residency:
|
|||
subprocess:
|
||||
./scripts/subprocess-accept.sh
|
||||
|
||||
# tls: runtime-v2 9 F3c-net's gate (docs/examples/tls-client) — net.connect_tls
|
||||
# from .wo end to end against a local TLS 1.3 stub with a test CA: the
|
||||
# hand-rolled handshake + chain/hostname validation + an app round-trip, plus
|
||||
# the untrusted-chain and hostname-mismatch negatives refused. Log: /tmp/tls.log.
|
||||
tls:
|
||||
./scripts/tls-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.
|
||||
|
|
|
|||
150
scripts/tls-accept.sh
Executable file
150
scripts/tls-accept.sh
Executable file
|
|
@ -0,0 +1,150 @@
|
|||
#!/usr/bin/env bash
|
||||
# scripts/tls-accept.sh — runtime-v2 9 F3c-net's gate: outbound TLS 1.3.
|
||||
# The runtime suite (test_tls/test_crypto) proves the crypto + handshake +
|
||||
# chain validation offline against RFC 8448 / real certs; this proves the half
|
||||
# only a real program shows: net.connect_tls called FROM .wo through the
|
||||
# compiler, dialing a live TLS 1.3 server, validating the chain to a trust
|
||||
# anchor and the hostname, exchanging application bytes — and refusing loudly
|
||||
# when the chain is untrusted or the hostname does not match. No live network:
|
||||
# the server is a local stub with a test CA. Log: /tmp/tls.log (tail -F it).
|
||||
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-client"
|
||||
PORT="${TLS_PORT:-18443}"
|
||||
LOG=/tmp/tls.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"
|
||||
|
||||
# ---- 0. prerequisites ----------------------------------------------------
|
||||
[[ -x "$WOVM" ]] || { echo "tls-accept: wovm not built — run: make -C runtime wovm" >&2; exit 1; }
|
||||
[[ -x "$WOC" ]] || { echo "tls-accept: woc not built — run: just woc-build" >&2; exit 1; }
|
||||
python3 -c 'import cryptography, ssl' 2>/dev/null || { echo "tls-accept: needs python3 cryptography + ssl" >&2; exit 1; }
|
||||
|
||||
# ---- 1. test CA + certs --------------------------------------------------
|
||||
cat > "$WORK/mkcerts.py" <<'PY'
|
||||
import datetime, sys
|
||||
from cryptography import x509
|
||||
from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
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)])
|
||||
def leaf(cak, ca, cn, san):
|
||||
k = ec.generate_private_key(ec.SECP256R1())
|
||||
c = (x509.CertificateBuilder().subject_name(nm(cn)).issuer_name(ca.subject)
|
||||
.public_key(k.public_key()).serial_number(x509.random_serial_number())
|
||||
.not_valid_before(NB).not_valid_after(NA)
|
||||
.add_extension(x509.SubjectAlternativeName([x509.DNSName(san)]), False)
|
||||
.add_extension(x509.ExtendedKeyUsage([ExtendedKeyUsageOID.SERVER_AUTH]), False)
|
||||
.sign(cak, hashes.SHA256()))
|
||||
return k, c
|
||||
def ca(cn):
|
||||
k = ec.generate_private_key(ec.SECP256R1())
|
||||
c = (x509.CertificateBuilder().subject_name(nm(cn)).issuer_name(nm(cn))
|
||||
.public_key(k.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(k, hashes.SHA256()))
|
||||
return k, c
|
||||
def wpem(p, c): open(p, "wb").write(c.public_bytes(serialization.Encoding.PEM))
|
||||
def wkey(p, k): open(p, "wb").write(k.private_bytes(serialization.Encoding.PEM,
|
||||
serialization.PrivateFormat.TraditionalOpenSSL, serialization.NoEncryption()))
|
||||
cak, cac = ca("wo-test-ca")
|
||||
lk, lc = leaf(cak, cac, "localhost", "localhost")
|
||||
wk, wc = leaf(cak, cac, "wrong", "wrong.example")
|
||||
_, oc = ca("wo-other-ca")
|
||||
wpem(d + "/ca.pem", cac); wpem(d + "/leaf.pem", lc); wkey(d + "/leaf.key", lk)
|
||||
wpem(d + "/wrong.pem", wc); wkey(d + "/wrong.key", wk); wpem(d + "/other.pem", oc)
|
||||
PY
|
||||
python3 "$WORK/mkcerts.py" "$WORK" || { bad "cert generation"; echo "tls-accept: $fail failures"; exit 1; }
|
||||
ok "test CA + leaf (SAN localhost) + wrong-host + unrelated-CA generated"
|
||||
|
||||
# ---- 2. TLS 1.3 stub server ----------------------------------------------
|
||||
cat > "$WORK/stub.py" <<'PY'
|
||||
import socket, ssl, sys, threading, time
|
||||
host, port, cert, key = "127.0.0.1", int(sys.argv[1]), sys.argv[2], sys.argv[3]
|
||||
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
ctx.minimum_version = ssl.TLSVersion.TLSv1_3
|
||||
ctx.maximum_version = ssl.TLSVersion.TLSv1_3
|
||||
ctx.load_cert_chain(certfile=cert, keyfile=key)
|
||||
srv = socket.socket(); srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
srv.bind((host, port)); srv.listen(8)
|
||||
print("READY", flush=True)
|
||||
def serve():
|
||||
while True:
|
||||
try: c, _ = srv.accept()
|
||||
except OSError: return
|
||||
try:
|
||||
s = ctx.wrap_socket(c, server_side=True)
|
||||
s.recv(4096); s.sendall(b"wo-tls-ok\n"); s.close()
|
||||
except Exception as e:
|
||||
print("stub-err", e, flush=True)
|
||||
threading.Thread(target=serve, daemon=True).start()
|
||||
time.sleep(60)
|
||||
PY
|
||||
|
||||
start_stub() { # $1 = cert, $2 = key
|
||||
[[ -n "$SRV_PID" ]] && kill -KILL "$SRV_PID" 2>/dev/null
|
||||
: > "$WORK/stub.log"
|
||||
python3 "$WORK/stub.py" "$PORT" "$1" "$2" >"$WORK/stub.log" 2>&1 &
|
||||
SRV_PID=$!
|
||||
disown "$SRV_PID" 2>/dev/null || true # keep job-control noise out of gate output
|
||||
for _ in $(seq 1 100); do grep -q READY "$WORK/stub.log" 2>/dev/null && return 0; sleep 0.1; done
|
||||
return 1
|
||||
}
|
||||
|
||||
run_client() { # $1 = CA bundle ; stdout: client output ; return: client exit
|
||||
WO_CA_BUNDLE="$1" WO_DATA="$WORK/data" timeout 20 "$WOVM" "$WORK/app.wob" localhost "$PORT" 2>>"$LOG"
|
||||
}
|
||||
|
||||
# ---- 3. build the client -------------------------------------------------
|
||||
{ echo; echo "== tls-accept $(date -Is) port $PORT =="; } >>"$LOG"
|
||||
if "$WOC" --emit "$APP" -o "$WORK/app.wob" 2>"$WORK/cerr"; then
|
||||
ok "build: tls-client compiles"
|
||||
else
|
||||
bad "build: $(head -3 "$WORK/cerr")"; echo "tls-accept: $fail failures"; exit 1
|
||||
fi
|
||||
|
||||
# ---- 4. happy path: trusted chain + matching host ------------------------
|
||||
start_stub "$WORK/leaf.pem" "$WORK/leaf.key" || { bad "stub server did not start"; echo "tls-accept: $fail failures"; exit 1; }
|
||||
out="$(run_client "$WORK/ca.pem")"; rc=$?
|
||||
if [[ $rc -eq 0 && "$out" == *"wo-tls-ok"* ]]; then
|
||||
ok "handshake + trusted chain + host match + app round-trip"
|
||||
else
|
||||
bad "happy path (exit $rc): $out"
|
||||
fi
|
||||
|
||||
# ---- 5. negative: untrusted chain (bundle = unrelated CA) -----------------
|
||||
out="$(run_client "$WORK/other.pem")"; rc=$?
|
||||
if [[ $rc -eq 1 && "$out" == *"refused"* ]]; then
|
||||
ok "untrusted chain refused"
|
||||
else
|
||||
bad "untrusted chain not refused (exit $rc): $out"
|
||||
fi
|
||||
|
||||
# ---- 6. negative: hostname mismatch (cert SAN=wrong.example) --------------
|
||||
start_stub "$WORK/wrong.pem" "$WORK/wrong.key" || { bad "stub restart (wrong cert)"; }
|
||||
out="$(run_client "$WORK/ca.pem")"; rc=$?
|
||||
if [[ $rc -eq 1 && "$out" == *"refused"* ]]; then
|
||||
ok "hostname mismatch refused"
|
||||
else
|
||||
bad "hostname mismatch not refused (exit $rc): $out"
|
||||
fi
|
||||
|
||||
echo "tls-accept: $((pass + fail)) checks, $fail failures"
|
||||
[[ $fail -eq 0 ]]
|
||||
Loading…
Reference in a new issue