local testimony = require("testimony") local lev = require("lev") local testify = testimony.new("== LITLS TLS 1.3 Server ==") local fixtures = "tests/litls/fixtures/" -- Test 1: Loopback TLS handshake + data round-trip testify:that("Loopback TLS handshake and data exchange", function() lev.run(function() local listener, err = lev.listen("127.0.0.1", 18443, { reuseport = true }) testimony.assert_not_nil(listener, "listen failed: " .. tostring(err)) -- Server coroutine lev.spawn(function() local client, addr = listener:accept(5) testimony.assert_not_nil(client, "accept failed") local tok, terr = client:starttls({ mode = "server", cert = fixtures .. "server.crt", key = fixtures .. "server.key", timeout = 5, }) testimony.assert_not_nil(tok, "server TLS handshake failed: " .. tostring(terr)) -- Read data from client local data, rerr = client:recv() while not data and rerr == "want_read" do lev.wait_readable(client:fd(), 5) data, rerr = client:recv() end testimony.assert_not_nil(data, "server recv failed: " .. tostring(rerr)) testimony.assert_equal("hello from client", data) -- Send response local n, serr = client:send("hello from server") testimony.assert_not_nil(n, "server send failed: " .. tostring(serr)) client:close() end, { detached = true }) -- Client coroutine lev.spawn(function() lev.sleep(0.1) -- let server start accepting local sock, cerr = lev.connect("127.0.0.1", 18443, { timeout = 5, tls = { mode = "client", verify = false, server_name = "localhost", }, }) testimony.assert_not_nil(sock, "client connect failed: " .. tostring(cerr)) -- Send data local n, serr = sock:send("hello from client") testimony.assert_not_nil(n, "client send failed: " .. tostring(serr)) -- Receive response local data, rerr = sock:recv() while not data and rerr == "want_read" do lev.wait_readable(sock:fd(), 5) data, rerr = sock:recv() end testimony.assert_not_nil(data, "client recv failed: " .. tostring(rerr)) testimony.assert_equal("hello from server", data) sock:close() listener:close() end, { detached = true }) end) end) -- Test 2: SNI selection with two identities testify:that("SNI selects correct certificate", function() lev.run(function() local listener, err = lev.listen("127.0.0.1", 18444, { reuseport = true }) testimony.assert_not_nil(listener, "listen failed: " .. tostring(err)) -- Server lev.spawn(function() local client = listener:accept(5) testimony.assert_not_nil(client, "accept failed") local tok, terr = client:starttls({ mode = "server", cert = fixtures .. "server.crt", key = fixtures .. "server.key", hosts = { ["other.test"] = { cert = fixtures .. "server2.crt", key = fixtures .. "server2.key", }, }, timeout = 5, }) testimony.assert_not_nil(tok, "server TLS handshake failed: " .. tostring(terr)) local data = client:recv() while not data do lev.wait_readable(client:fd(), 5) data = client:recv() end client:send("ok") client:close() end, { detached = true }) -- Client connecting with SNI=other.test lev.spawn(function() lev.sleep(0.1) local sock, cerr = lev.connect("127.0.0.1", 18444, { timeout = 5, tls = { mode = "client", verify = false, server_name = "other.test", }, }) testimony.assert_not_nil(sock, "client connect failed: " .. tostring(cerr)) sock:send("ping") local data = sock:recv() while not data do lev.wait_readable(sock:fd(), 5) data = sock:recv() end testimony.assert_equal("ok", data) sock:close() listener:close() end, { detached = true }) end) end) -- Test 3: Verified connection with CA testify:that("Client verifies server certificate with CA", function() lev.run(function() local listener, err = lev.listen("127.0.0.1", 18445, { reuseport = true }) testimony.assert_not_nil(listener, "listen failed: " .. tostring(err)) -- Server lev.spawn(function() local client = listener:accept(5) testimony.assert_not_nil(client, "accept failed") local tok, terr = client:starttls({ mode = "server", cert = fixtures .. "server.crt", key = fixtures .. "server.key", timeout = 5, }) testimony.assert_not_nil(tok, "server TLS handshake failed: " .. tostring(terr)) local data = client:recv() while not data do lev.wait_readable(client:fd(), 5) data = client:recv() end client:send("verified-ok") client:close() end, { detached = true }) -- Client with verify=true and CA file lev.spawn(function() lev.sleep(0.1) local sock, cerr = lev.connect("127.0.0.1", 18445, { timeout = 5, tls = { mode = "client", verify = true, server_name = "localhost", cafile = fixtures .. "ca.crt", }, }) testimony.assert_not_nil(sock, "client connect with verify failed: " .. tostring(cerr)) if sock then sock:send("hello") local data = sock:recv() while not data do lev.wait_readable(sock:fd(), 5) data = sock:recv() end testimony.assert_equal("verified-ok", data) sock:close() end listener:close() end, { detached = true }) end) end) -- Test 4: Self-signed rejection (client verify=true, no CA match) testify:that("Client rejects untrusted server cert", function() lev.run(function() local listener, err = lev.listen("127.0.0.1", 18446, { reuseport = true }) testimony.assert_not_nil(listener, "listen failed: " .. tostring(err)) -- Server lev.spawn(function() local client = listener:accept(5) if client then local tok, terr = client:starttls({ mode = "server", cert = fixtures .. "server.crt", key = fixtures .. "server.key", timeout = 5, }) -- May fail because client aborts client:close() end listener:close() end, { detached = true }) -- Client with verify=true but wrong CA (system CAs won't include our test CA) lev.spawn(function() lev.sleep(0.1) local sock, cerr = lev.connect("127.0.0.1", 18446, { timeout = 5, tls = { mode = "client", verify = true, server_name = "localhost", -- no cafile => should fail with no trust anchors }, }) testimony.assert_nil(sock, "should have rejected untrusted cert") end, { detached = true }) end) end) testify:conclude()