TLS & HTTPS
TLS lives in its own bundle, tls.moon, rather than the standard library, so
programs that do not speak TLS carry none of it. The bundle wraps OpenSSL and
embeds its own static copy of it, so a program using TLS needs no linker
flags and no OpenSSL installed on the machine it runs on.
That holds both for compiled programs (sun -c program.sun) and for
running one straight from source: the JIT loads the same embedded
archives the linker uses. See carrying the C library
along for how the archives travel.
using std;
using tls;
/** Fetches a page and reports either error category. */
function main() i32 {
var alloc = make_heap_allocator();
var client = HttpsClient(alloc);
var host = String(alloc, "example.com");
return match client.get(host, "/") {
ClientResult.Ok(response) => {
println(response.status);
println(response.body);
0;
},
(error: const ref IError) => {
println(error.message());
1;
}
};
}
manifest {
libraries: ["stdlib.moon", "tls.moon"]
}sun client.sun # run it
sun --compile -o client client.sunTLS operations return ClientResult<T> with Ok(T), System(Error), or
Tls(TlsError). Use specific variants, an interface arm as above, or _ to
handle remaining variants. Prefix try extracts success and propagates failures
into a compatible caller result. The stream examples below assume such a caller.
The standalone HTTP response parser returns SystemResult<void>.
Certificate verification
Verification is on and cannot be switched off. A connection only succeeds if
the peer's certificate chain validates against the trusted CA store and
the certificate matches the hostname passed to connect. Anything else
returns ClientResult.Tls, with the reason in the error message:
certificate verification failed: self-signed certificate
certificate verification failed: hostname mismatchTrust comes from the system CA store (/etc/ssl). Two environment variables
override it, which is how you test against a private CA:
| Variable | Meaning |
|---|---|
SSL_CERT_FILE | A PEM file of trusted certificates |
SSL_CERT_DIR | A directory of trusted certificates |
The client also sends SNI, so it works with virtual hosts.
HttpsClient
get and post open a connection, exchange one request, and close it. Both
take the host as a String and the path as a literal; both have an overload
taking a port, which defaults to 443.
public function get(host: ref String, path: static_ptr<u8>) ClientResult<HttpClientResponse>
public function get(host: ref String, port: i32, path: static_ptr<u8>) ClientResult<HttpClientResponse>
public function post(host: ref String, path: static_ptr<u8>, content_type: static_ptr<u8>,
body: const ref String) ClientResult<HttpClientResponse>The response carries the status, the raw header block, and the decoded body:
public class HttpClientResponse {
public var status: i32;
public var headers: String;
public var body: String;
}Bodies framed with Content-Length and with chunked transfer encoding are
both decoded. parse_response is public, so code driving a TlsStream
directly can reuse the same framing.
This first version does not follow redirects, reuse connections, or decode compressed bodies. A 301 or 302 comes back as-is for the caller to act on.
TlsStream
For protocols other than HTTP, TlsStream is the connection itself — the
same shape as TcpStream, with the handshake done in connect.
var stream = TlsStream();
try stream.connect(host, 443);
try stream.send_str("PING\r\n");
var buf = ContiguousBuffer<u8>(alloc, 4096);
var n = try stream.recv(buf); // 0 once the peer closes
stream.close();close shuts the session down and releases everything; dropping the value
does the same.
Server TLS
TlsServerContext loads a PEM certificate chain and matching, unencrypted PEM
private key. It requires TLS 1.2 or newer. A failed replacement load leaves
the previous configuration usable, and sessions
already accepted retain their original configuration.
Accept TCP connections with TcpListener, then transfer each socket to the
context. accept makes the socket nonblocking and returns a TlsServerStream;
it does not run the handshake or wait for network traffic.
var context = TlsServerContext();
var cert = String("cert.pem");
var key = String("key.pem");
try context.load(cert, key);
var listener = TcpListener();
try listener.bind_port(8443);
try listener.listen(128);
var stream = try context.accept(try listener.accept());Setup uses ClientResult<T>, the bundle's existing transport/TLS error result.
The snippet assumes a caller returning a compatible result. For an event-driven
accept loop, also set the TcpListener nonblocking and handle its would-block
errors with is_would_block.
Drive stream.handshake() until it returns ServerResult.Ok(0), then use
read(buffer) and write(string). These operations return ServerResult<i64>:
| Result | Meaning |
|---|---|
Ok(n) | Handshake complete, or application bytes read/written |
WantRead | Wait for readability on get_fd(), then retry the operation |
WantWrite | Wait for writability on get_fd(), then retry the operation |
Closed | The peer sent a TLS close notification, or shutdown finished |
Tls(error) | TLS/transport, state, or argument error, with an explanation |
Either I/O operation can require either readiness direction. After a write
returns a readiness result, retry with the same contents and length; the
string's storage may move. Successful writes can be partial: advance by the
returned count before starting the next write. pending() reports decrypted
bytes already buffered inside OpenSSL; consume those without waiting for
another socket event. Empty reads and writes return Ok(0) after the handshake.
shutdown() performs a nonblocking close-notification exchange; continue on
readiness results until it returns Closed. close() and destruction release
the session and socket immediately, without network I/O. Abrupt TCP EOF is an
error, not a clean TLS close. Server socket writes suppress SIGPIPE without
changing process-wide signal handlers.
Application protocol negotiation
The optional third argument to load is a static, length-prefixed ALPN list,
in server preference order:
try context.load(cert, key, "\x02h2\x08http/1.1");Each length byte must be nonzero and followed by exactly that many protocol
bytes. Names containing NUL are unsupported. Static storage keeps the callback
valid even after the context is moved or closed, without copying the list per
connection. stream.alpn() returns an owned string with the selected name, or
an empty string when the client offers no matching protocol. Advertise only
protocols your application implements. TLS does not implement HTTP/2 itself.
The server API handles TLS transport; HttpServer does not yet use it directly.
Hostnames
Name resolution is in the standard library, not the TLS bundle, so plain TCP can use it too:
using std;
var stream = TcpStream();
try stream.connect_host(host, 8080); // resolve, then connect
var ip: i32 = try resolve_host_ipv4(host); // first A record, network orderBoth return SystemResult.Error if the name does not resolve or has no IPv4 address. IPv6 is not
supported yet.
Building the bundle
tls.moon embeds libssl.a and libcrypto.a, which the repository does not
carry. Build them once with the script for your platform, then build as
usual:
./scripts/fetch-openssl.sh # Linux
./scripts/build-openssl-macos.sh # macOS (Apple Silicon)
./build.shDebian packages also include AArch64 Linux TLS and standard-library bundles.
The compiler selects them automatically with --target aarch64-linux-gnu
or --target aarch64-linux-musl. Linking an executable still requires a
cross-linker and target system libraries.
To build these bundles locally, fetch AArch64 archives with
./scripts/fetch-openssl.sh --arch aarch64, configure CMake with
-DSUN_CROSS_TLS_AARCH64=ON, and build the tls_cross_moon target.
The workspace root sun-config.json compiles the standard library first, then TLS,
in one compiler invocation. The archives are kept separately from the native
archives.
On Linux the archives come from Alpine's openssl-libs-static package:
Alpine is musl-based, matching the toolchain Sun links static binaries with,
and builds OpenSSL without the optional compression and jitter-entropy
backends, so the archives depend on nothing outside themselves. On macOS
there is no such package, so the script compiles a pinned OpenSSL release
with the same self-containment rule. Without the archives the build skips
tls.moon and says so.
On macOS the archives look for trusted certificates in /etc/ssl, where
stock macOS keeps its CA bundle (/etc/ssl/cert.pem), so verification works
with no setup; SSL_CERT_FILE still overrides the store on both platforms.
The manifest names the archives per operating system, and the bundle carries the set matching the compilation target:
manifest {
source_files: ["openssl.sun", "tls_stream.sun", "tls_server.sun", "tls_socket.sun", "https_client.sun"]
libraries: ["stdlib.moon"]
target: {
linux: {
archives: ["$OPENSSL_LIBS_LINUX/libssl.a", "$OPENSSL_LIBS_LINUX/libcrypto.a"]
}
macos: {
archives: ["$OPENSSL_LIBS_MACOS/libssl.a", "$OPENSSL_LIBS_MACOS/libcrypto.a"]
}
}
}archives: works for any bundle wrapping a C library. Importers link against
what the bundle carries automatically — no -l flags, and nothing to install
alongside the finished program. A bundle built on tls.moon carries the
archives forward with the tls code it links in, so a program importing only
that bundle still links. The OpenSSL symbols inside are renamed under a
hash of the two archives together, so a program may import two bundles
built on different OpenSSL builds, each bound to its own, while bundles
sharing one build share one copy; see C FFI.
Tests
The bundle's own tests are Sun code, tls/https_client_tests.sun and
tls/tls_server_tests.sun, listed
under test_files: in tls/tls.sun (see Testing) and compiled
only into the test binary, build/tls_tests_test. Response parsing runs with
no network; the handshake tests mint a throwaway certificate with the
openssl command-line tool, run openssl s_server on it, and check that
the client accepts the certificate once SSL_CERT_FILE trusts it and rejects
it otherwise. They run sequentially because that variable is process-wide,
and skip themselves when the openssl tool is not on the path.
Server tests use local nonblocking OpenSSL peers to check handshake progress,
ALPN preference and fallback, partial writes and backpressure, credential load
failures, context lifetime, and clean versus abrupt closure. They need the
openssl command-line tool only to create temporary credentials.