The sentence behind an error ("the certificate has expired"), or "". It is a copy in temporary
storage, so it stays valid when the error is copied or goes out of scope, until the temporary
storage is reset; copy it with copy_string to keep it longer.
#import "Extensions/Tls";
TLS 1.2 and 1.3 over Io readers and writers, on a bundled mbedTLS. A jaic extension, not official Jai: other Jai compilers have no such module, and it may change between jaic releases. See docs/extensions/tls.md.
Fill in a Tls_Config, call tls_config_init once, then wrap an accepted or connected TCP stream with tls_connect
(client) or tls_accept (server). A stream is also an Io.Reader and Io.Writer (reader_of, writer_of), so code
written for those, like Http, runs over TLS unchanged. The handshake and every read and write are blocking calls on a
fiber, inside Io.io_run: a deadline (Io.with_deadline) limits them and Io.cancel ends them, and
one stream belongs to one scheduler thread.
Show moreShow less
Every call reports failure in its result and never stops the program: the handshake calls give a Tls_Error whose
kind says what went wrong (error_text makes a line of it), while read and write give an Io.Io_Error and
last_error(stream) the TLS reason. A client verifies the server against the system's trusted roots unless
roots_pem gives others; names are matched against the certificate's subject alternative names. Memory: a stream is
allocated with context.allocator by tls_connect or tls_accept and released by close; the config's parsed
certificates are released by tls_config_free. mbedTLS is linked into the executable, so nothing has to be
installed. WebAssembly has no sockets and no fibers.
The example runs a client and a server on loopback. The certificates and the key are throwaway ones made for
tests, and the certificate is valid for localhost. Never use them for anything else.
Io :: #import "Extensions/Io";
ROOT :: #string END
-----BEGIN CERTIFICATE-----
MIIBXzCCAQWgAwIBAgIBATAKBggqhkjOPQQDAjAWMRQwEgYDVQQDDAtKYWkgVGVz
dCBjYTAgFw0yMDAxMDEwMDAwMDBaGA8yMTIwMDEwMTAwMDAwMFowFjEUMBIGA1UE
AwwLSmFpIFRlc3QgY2EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQD1LxitDVO
QKMsMsgSpdi5JldYih52cephbw9hiejFcmOH3zQJAkGrcEwyH4CzLFJKjhUKaT54
4UDSgerXHZBpo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAd
BgNVHQ4EFgQUunX0563ngeQ6w8JsMUiqadFmd6YwCgYIKoZIzj0EAwIDSAAwRQIh
AI1Q4QwolbUZQxGjLzBltR4I6BPZLStohfpAiI20nwPRAiAjsRP061IzprBMR5JF
WnaWEW4Nf8JpkkFzPPaEp947tQ==
-----END CERTIFICATE-----
END
CERTIFICATE :: #string END
-----BEGIN CERTIFICATE-----
MIIBzDCCAXKgAwIBAgIBAzAKBggqhkjOPQQDAjAWMRQwEgYDVQQDDAtKYWkgVGVz
dCBjYTAgFw0yMDAxMDEwMDAwMDBaGA8yMTIwMDEwMTAwMDAwMFowFjEUMBIGA1UE
AwwLc2VydmVyLnRlc3QwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQC5kvwaJTy
O8qTdjNn9OgWvQ+QmLQDQDdzaUggjqJiooWHhj4peLTZFKWJHzwgZK3TRCnK9PXw
VYwzFQ5BLKq/o4GuMIGrMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQDAgWgMBMGA1Ud
JQQMMAoGCCsGAQUFBwMBMDkGA1UdEQQyMDCCCWxvY2FsaG9zdIILc2VydmVyLnRl
c3SHBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAEwHQYDVR0OBBYEFIDVLb76eRrc3ond
VlB/e95+HlPnMB8GA1UdIwQYMBaAFLp19Oet54HkOsPCbDFIqmnRZnemMAoGCCqG
SM49BAMCA0gAMEUCIE3wJvrmLnn+L4DdTxS3SZyHLO5z/yT/dh7hyz1vGv5oAiEA
v9a4lUdydpygfLwCjF7DCb8AD9qcpTQOAj8cFauUUCs=
-----END CERTIFICATE-----
END
KEY :: #string END
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEILAaHg0fuEMf2UFJprp01LKXf54EQN3Xb7zkKzdoIxK1oAoGCCqGSM49
AwEHoUQDQgAEAuZL8GiU8jvKk3YzZ/ToFr0PkJi0A0A3c2lIII6iYqKFh4Y+KXi0
2RSliR88IGSt00QpyvT18FWMMxUOQSyqvw==
-----END EC PRIVATE KEY-----
END
Echo :: struct {
listener: Io.Tcp_Listener;
config: *Tls_Config;
}
// The server: accept one connection, answer a read with the same bytes, then close.
echo_once :: (job: *Echo) {
tcp, io_error := Io.accept(job.listener);
assert(io_error == .NONE);
stream, error := tls_accept(job.config, Io.reader_of(tcp), Io.writer_of(tcp));
if error.kind != .NONE {
Io.close(tcp);
return;
}
buffer: [64] u8;
count, read_error := read(stream, buffer);
if read_error == .NONE write(stream, string.{ count, buffer.data });
close(stream);
}
main :: () {
Io.io_run(() {
server: Tls_Config;
server.certificate_chain_pem = CERTIFICATE;
server.private_key_pem = KEY;
assert(tls_config_init(*server).kind == .NONE);
defer tls_config_free(*server);
// A client trusts the system's roots unless told otherwise; this one trusts only the test authority that signed the certificate.
client: Tls_Config;
client.roots_pem = ROOT;
assert(tls_config_init(*client).kind == .NONE);
defer tls_config_free(*client);
listener, listen_error := Io.tcp_listen("127.0.0.1:0");
assert(listen_error == .NONE);
defer Io.close(listener);
job := Echo.{ listener = listener, config = *server };
Io.spawn(echo_once, *job);
address, _ := Io.local_address(listener);
tcp, connect_error := Io.connect(address);
assert(connect_error == .NONE);
// The name is sent as SNI and checked against the certificate.
stream, error := tls_connect(*client, Io.reader_of(tcp), Io.writer_of(tcp), "localhost");
// On an error nothing is closed: the TCP connection is still the caller's.
assert(error.kind == .NONE, "%", error_text(error));
defer close(stream);
assert(version(stream) == "TLSv1.3");
write(stream, "ping");
buffer: [64] u8;
count, read_error := read(stream, buffer);
assert(read_error == .NONE && string.{ count, buffer.data } == "ping");
});
}Doesn't run in the browser (skipped on wasm).
Procedures#
Error#
The name of an alert description ("unknown_ca"), or "" for a number the module does not know.
One line for logs and messages: "certificate verification failed: the certificate has expired".
Like detail, the text is in temporary storage, never inside the error.
Config#
Parses the certificates and keys and builds the mbedTLS configurations. Call it once before the
first connection; the config must stay where it is and outlive every stream made from it.
Returns .CONFIG with a reason for a certificate or key that does not parse, a version range
that is empty, or a server certificate without its key. A config with no certificate is a
client; one with a certificate and key can also accept.
Releases what tls_config_init built. Streams made from the config must be closed first.
Whether tls_accept works with this config: it was set up, and has a certificate and key.
Stream#
Runs the client handshake over reader and writer and checks the server's certificate against
server_name (which is also sent as the SNI name; "" sends none, and with verify_name off the
name is not checked). On success the stream owns the connection:
close closes it. On an error nothing is closed, the connection is the caller's to close.
Runs the server handshake. The config needs a certificate and key. On success the stream owns the connection; on an error the caller closes it.
The ALPN protocol both sides chose ("" when none was).
The cipher suite in use, by mbedTLS's name ("TLS1-3-AES-128-GCM-SHA256", "TLS-ECDHE-ECDSA-WITH-CHACHA20-POLY1305-SHA256").
The DER of the certificate the peer proved itself with (the first of its chain): the server's
on a client stream, the client's on a server stream that asked for one. The bytes belong to the
stream and stay valid until close. Empty before the handshake or when the peer sent none.
The hash the peer certificate's signature uses, by lower-case name: "md5", "sha1", "sha224", "sha256", "sha384" or "sha512". "" when there is no certificate or the algorithm names no hash (Ed25519, unknown OIDs). It reads the certificate's own DER, not the library's view of it.
The name a client asked for in its SNI extension, on a stream from tls_accept ("" if it sent
none). It points into the stream.
"TLSv1.3" or "TLSv1.2".
Why the last call that failed did (.NONE before any failure).
Waits for data and reads what is there, up to buffer.count bytes. 0 is the end: the peer sent
close_notify. A connection that ends without it is an error (.CONNECTION_RESET, and
last_error says .TRUNCATED): the data may have been cut off.
Waits until the library has accepted at least one byte of bytes and sends it, together with the
records of as many more bytes as fit in BATCH_BYTES; returns how many it took.
Sends all of bytes. On an error it returns how many went out.
Sends all of bytes, waiting for the peer as needed, and returns how many went out with an Io.Io_Error. On .NONE that
is bytes.count; on an error it is the bytes that were already sent. It is the same as write_some repeated, so a large
write is split into records and may take several scheduler turns. Use write_some to send what fits without
looping.
Waits until read would not have to wait for the network: decrypted bytes are waiting, or
something has arrived (which may still be only part of a record).
Ends the stream: sends close_notify (unless now, which does not wait and sends nothing) and
closes the connection under it, which wakes fibers still waiting on it with .CLOSED. The stream is
freed once those fibers have left, so it must not be used after close; close it once.
Whether reader is a TLS stream's reader (from reader_of) whose connection ended without the
peer's close_notify (last_error(stream).kind == .TRUNCATED). For a protocol that frames its own
messages, such as HTTP, where that end is no failure once the message is complete.
The stream as an Io.Reader, for code written against readers (Io.read_all, the Http client). Reading it is read;
closing it closes the whole stream, once. The reader points at the stream, so it is only valid until close.
The stream as an Io.Writer, for code written against writers. Writing it is write_some; closing it closes the whole
stream, once, so close either the reader or the writer of a stream and not both. It is only valid until close.
Names#
Reads the names of the certificate in der. False when the DER is not a certificate.
Releases the lists inside names (allocated with context.allocator by certificate_names) and resets it. The strings in
it pointed into the DER, so they are not valid afterwards.
Types#
What went wrong, in one value for everything the module does: a bad certificate in a config, a handshake that failed, a peer that is not who it says it is, a connection that broke.
config: Tls_Config;
config.certificate_chain_pem = "this is not a certificate";
config.private_key_pem = "this is not a key";
error := tls_config_init(*config);
assert(error.kind == .CONFIG);
assert(error_text(error).count > 0);Doesn't run in the browser (skipped on wasm).
The sentence for people ("the certificate has expired") is kept inside the error in detail_bytes;
read it with detail(*error) or print the whole error with error_text. code is mbedTLS's own
negative error number (0 when the error did not come from it).
A TLS protocol version, for Tls_Config.min_version and max_version: the range a client offers or a server accepts.
The defaults are the whole range, TLS_1_2 to TLS_1_3, and the version that was settled on is reported by
version(stream) as "TLSv1.2" or "TLSv1.3". Older versions are not supported. tls_config_init fails with .CONFIG when the range
is empty (min_version above max_version).
config: Tls_Config;
config.min_version = .TLS_1_3;
config.max_version = .TLS_1_2;
error := tls_config_init(*config);
assert(error.kind == .CONFIG);Doesn't run in the browser (skipped on wasm).
Decides whether to trust a peer's certificate chain, instead of mbedTLS's own verification. chain
is the peer's certificates in DER, its own first; server_name is what the client asked for (empty
on the server). Returns "" to accept, or why the chain is rejected. It runs on the fiber that is
doing the handshake, after the handshake itself succeeded, and may wait.
Gets one line of the NSS key log format (no newline), such as
CLIENT_HANDSHAKE_TRAFFIC_SECRET <client random> <secret>, in hex.
Zero-initialised, a config is a client that verifies servers against the system's roots. Fill in
the fields you need, then call tls_config_init.
A TLS connection over any Io.Reader / Io.Writer pair (a TCP stream's, a pipe's, an Http
Link's). The handshake is a plain blocking call on the calling fiber: it waits through the Io
calls under it, so Io.with_deadline limits it and Io.cancel ends it. After it, the stream is
read and written like any other, through reader_of and writer_of, or directly with read,
write_some and write. The example in the module's docs connects, writes and reads.
Slices into the DER the names were read from: they are valid as long as it is.
Constants#
The library makes one record per call (up to 8 KiB of data); a write of more goes through several
records into out and reaches the connection in one send, up to this much, instead of one send
(and one trip through the scheduler) per record.