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.
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 Tls_Stream is also an
Io.Reader and Io.Writer (reader_of, writer_of), so code written for those, like Http,
runs over TLS unchanged.
Calls block the calling fiber inside Io.io_run, so Io.with_deadline limits them and
Io.cancel ends them. A stream belongs to one scheduler thread. Failures are returned, never
fatal: handshake calls give a Tls_Error (error_text makes a line of it), while read and
write give an Io.Io_Error and last_error the TLS reason.
A client verifies the server against the system's trusted roots unless roots_pem names
others, and matches the host name against the certificate's subject alternative names. A
stream is allocated by tls_connect or tls_accept and freed by close; tls_config_free
releases the config. WebAssembly has no sockets or fibers, so the module is not there.
The example runs a client and a server on loopback with throwaway test certificates, 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");
});
}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.
line := error_text(Tls_Error.{ kind = .CLOSED });
assert(line.count > 0);↗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.
config: Tls_Config; // a client, trusting the system's roots
error := tls_config_init(*config);
assert(error.kind == .NONE, "%", error_text(error));
tls_config_free(*config);↗This one is only checked to compile, not run.
Releases what tls_config_init built. Streams made from the config must be closed first.
config: Tls_Config;
tls_config_init(*config);
tls_config_free(*config);↗This one is only checked to compile, not run.
Whether tls_accept works with this config: it was set up, and has a certificate and key.
client: Tls_Config;
tls_config_init(*client);
assert(!can_accept(*client)); // no certificate and key
tls_config_free(*client);↗This one is only checked to compile, not run.
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.
Io :: #import "Extensions/Io";
Io.io_run(() {
config: Tls_Config;
tls_config_init(*config);
defer tls_config_free(*config);
tcp, io_error := Io.connect("example.com:443");
assert(io_error == .NONE);
stream, error := tls_connect(*config, Io.reader_of(tcp), Io.writer_of(tcp), "example.com");
if error.kind != .NONE {
Io.close(tcp);
return;
}
defer close(stream);
write(stream, "hello");
});↗This one is only checked to compile, not run.
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.
Io :: #import "Extensions/Io";
// `config` has a certificate chain and private key; `listener` is an Io.Tcp_Listener.
serve_one :: (config: *Tls_Config, listener: Io.Tcp_Listener) {
tcp, io_error := Io.accept(listener);
if io_error != .NONE return;
stream, error := tls_accept(config, Io.reader_of(tcp), Io.writer_of(tcp));
if error.kind != .NONE {
Io.close(tcp);
return;
}
defer close(stream);
write(stream, "welcome");
}↗This one is only checked to compile, not run.
The ALPN protocol both sides chose ("" when none was).
uses_http2 :: (stream: *Tls_Stream) -> bool {
return alpn_protocol(stream) == "h2";
}↗This one is only checked to compile, not run.
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.
read_greeting :: (stream: *Tls_Stream) -> string {
buffer: [64] u8;
count, error := read(stream, buffer);
if error != .NONE || count == 0 return "";
return copy_string(string.{ count, buffer.data });
}↗This one is only checked to compile, not run.
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.
send_frame :: (stream: *Tls_Stream) {
frame := u8.[1, 2, 3];
sent, error := write(stream, frame);
assert(error == .NONE && sent == frame.count);
}↗This one is only checked to compile, not run.
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.
finish :: (stream: *Tls_Stream) {
close(stream); // sends close_notify, closes the TCP connection, frees the stream
}↗This one is only checked to compile, not run.
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.
Io :: #import "Extensions/Io";
read_everything :: (stream: *Tls_Stream) -> string {
data, error := Io.read_all(reader_of(stream));
return cast(string) data;
}↗This one is only checked to compile, not run.
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.
Trust macOS#
Asks Security.framework whether chain (DER, the server's certificate first) is a valid chain
for server_name. "" if it is, else why not.
Trust Windows#
Bindings#
Foreign#
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.
- NONE
- no error:
.{} - IO
- the connection under TLS failed or timed out:
iosays how (.TIMED_OUT,.CANCELED, ...) - CONFIG
- the config cannot be used: a certificate or key that does not parse, no system roots, ...
- HANDSHAKE
- the handshake failed: the peers share no protocol version, cipher or ALPN protocol, or one sent a fatal alert
- VERIFY
- the peer's certificate was not accepted (
detailsays why) - PROTOCOL
- a record that does not decrypt or parse, or an alert from the peer, on an open connection
- TRUNCATED
- the connection ended without the peer's close_notify: the data may be incomplete
- CLOSED
- the stream was closed
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);↗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).
- kind: Tls_Error_Kind
- io: Io.Io_Error
- For IO, and the closest
Io_Errorfor the other kinds that end a read or write. - code: s32
- verify_flags: u32
- For VERIFY with mbedTLS's own verification: its result bits (
X509_BADCERT_*), else 0. - alert: u8
- For a fatal alert the peer sent: its description number (RFC 8446, section 6;
alert_namespells it), else 0. - wrong_key_password: bool
- For CONFIG: the private key was not read because the password does not decrypt it (or was needed and not given).
- detail_count: u8
- detail_bytes: [191] u8
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).
- TLS_1_2
- TLS_1_3
config: Tls_Config;
config.min_version = .TLS_1_3;
config.max_version = .TLS_1_2;
error := tls_config_init(*config);
assert(error.kind == .CONFIG);↗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.
- certificate_chain_pem: string
- Server (and, for a client that must prove who it is, client): the certificate chain as PEM,
the endpoint's own certificate first, and its private key: PEM or DER (PKCS#8, PKCS#1 or
SEC1; the name
_pemis kept from when only PEM was read).private_key_passworddecrypts an encrypted PKCS#8 key (PBES2 with PBKDF2 and AES or 3DES) or an OpenSSL-style encrypted PEM one; empty for a key that is not encrypted. - private_key_pem: string
- private_key_password: string
- roots_pem: string
- The certificate authorities to trust, as PEM (any number of certificates). Empty: the system's
(macOS and Windows use their own trust stores). They must be authorities: the server's own certificate listed
here does not make a client trust it unless it is self-signed (the usual rule), so a certificate that a CA signed is
trusted by giving that CA, or an intermediate (see
root_must_be_self_signed). - crl_pem: string
- Client with
roots_pem: certificate revocation lists as PEM (any number, or one DER). The server's certificate must be covered: if no list in here was issued by its issuer, or one lists the certificate, the connection fails. Lists for the other certificates of a chain are used when present. - alpn: [] string
- The protocols to offer (client) or accept (server) through ALPN, most preferred first:
.["h2", "http/1.1"]. Empty: no ALPN extension. A server that has a list and a client that offers protocols but none in common fails the handshake. - verify: := true
- Client: check the server's certificate chain and name. Turning it off makes the connection open to anyone in the middle; it exists for tests and for talking to devices on a private network, and is never the default.
- verify_name: := true
- Client: check that the server's certificate is for the name
tls_connectis given. Off, the chain is still checked, and the name is still sent as SNI. For callers that match the name by their own rules (a database's, say); withverify_procor the system's verifier on macOS and Windows the name is simply not passed on. - root_must_be_self_signed: := false
- Client with
roots_pem: the chain must end in a self-signed certificate fromroots_pem. Otherwise any certificate listed there is a trust anchor, intermediate CAs included (the library's rule); OpenSSL-based clients need the root, and this option gives that behaviour. - client_auth: := false
- Server: ask the client for a certificate and check it against
roots_pem(or the system's roots on Linux). Clients without a valid certificate fail the handshake. - min_version: := Tls_Version.TLS_1_2
- max_version: := Tls_Version.TLS_1_3
- verify_proc: Tls_Verify_Proc
- Replaces mbedTLS's check of the peer's chain (
roots_pemand the system roots are then not used for verification). The system verifiers of macOS and Windows are this mechanism too. - verify_data: *void
- key_log_proc: Tls_Key_Log_Proc
- Client: called with each secret a handshake derives, as a key log line that tools such as Wireshark read to decrypt a capture. For debugging only: the lines decrypt the connection.
- key_log_data: *void
- native: *Tls_Native
- Set by
tls_config_init(its type is internal to the module): do not touch it, and do not copy a config after that.
client: Tls_Config;
client.alpn = .["h2", "http/1.1"];
assert(tls_config_init(*client).kind == .NONE);
defer tls_config_free(*client);↗This one is only checked to compile, not run.
A TLS connection over any Io.Reader and Io.Writer pair. Read and write it with read,
write_some and write, or through reader_of and writer_of.
- reader: Io.Reader
- writer: Io.Writer
- ssl: *void
- fiber_context: #Context
- The running fiber's context, saved before every call into the library, so the callbacks the
library makes (which are
#c_call) can run Jai code and park the fiber. - lock: Io.Fiber_Mutex
- Calls into the library take turns. A fiber never waits for the network while it holds this:
the receive callback asks the library to retry and the fiber waits outside, and the send
callback only appends to
out. - out: [..] u8
- What the library has produced and the connection has not taken yet, in order. Appended to
under
lockby the send callback;flushwrites it out underflush_lock, so the bytes of two flushes never interleave. The library is never parked in a send: that is what lets both ends of a connection write a lot at once and still read each other. - flush_lock: Io.Fiber_Mutex
- flusher_running: bool
- A fiber started by
flush_lateris sendingout(it counts as a user of the stream). - io_error: Io.Io_Error
- What the Io call under the running library call returned, when it failed.
- broken: Io.Io_Error
- Set once the connection is unusable (an Io call failed in the middle of a record, a record did not decrypt, the peer cut the connection short): what every later call returns.
- failure: Tls_Error
- The error behind
broken, forlast_error. - can_wait: bool
- The underlying reader says when it is readable: the library is never made to wait for input inside a callback (it returns "want read" and the fiber waits outside), so a fiber waiting for data does not block a writer of the same stream.
- ready: bool
- ahead: [] u8
- Bytes read from the connection and not yet handed to the library. The library asks for one
record at a time (at most 16 KiB), and each ask used to cost a wait and a read on the
connection; reading more at once and serving the library from here costs one pair for several
records. The buffer lives from a read to the moment the library has taken all of it, so a
quiet connection holds none;
ahead_sizeis how much the next read takes (it grows while reads come back full, and starts again at the size of a record after a short one). - ahead_start: s64
- ahead_end: s64
- ahead_size: s64
- established: bool
- peer_closed: bool
- closing: bool
- users: s64
- Calls in progress; the stream is freed after
closewhen the last one has left. - key_log_proc: Tls_Key_Log_Proc
- The client's key log, from its config.
- key_log_data: *void
- peer_name_count: u8
- The server name a client asked for (SNI), on a server stream.
- peer_name_bytes: [255] u8
Slices into the DER the names were read from: they are valid as long as it is.
- dns: [..] string
- The dNSName entries, in the order the certificate lists them.
- addresses: [..] string
- The iPAddress entries: 4 raw bytes for IPv4, 16 for IPv6.
- common_name: string
- The first common name of the subject (UTF8String, PrintableString, IA5String and TeletexString values; a BMPString only if every character is ASCII).
- has_common_name: bool
- common_name_unreadable: bool
- Set when the common name could not be read as text (a zero byte inside it, an unknown string type).
- count: u32
- identifiers: **u8
- type: u32
- usage: Cert_Enhkey_Usage
CERT_CHAIN_PARA up to pftCacheResync, the part every Windows version since Vista reads.
- size: u32
- requested_usage: Cert_Usage_Match
- requested_issuance_policy: Cert_Usage_Match
- url_retrieval_timeout: u32
- check_revocation_freshness_time: s32
- revocation_freshness_time: u32
- cache_resync: *void
- size: u32
- flags: u32
- extra: *void
HTTPSPolicyCallbackData
- size: u32
- auth_type: u32
- checks: u32
- server_name: *u16
- size: u32
- error: u32
- chain_index: s32
- element_index: s32
- extra: *void
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.
What the library returns, by name (all negative).
The library's own network codes (its sockets are not compiled in): what the BIO callbacks return
when the Io call under them failed. The stream keeps the Io_Error.
mbedtls_x509_crt_verify's result bits.