Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Encrypt a TCP connection

std.tls wraps a connected TCP stream in TLS. The client checks that the server’s certificate chains to a trust root it chose and that the certificate names the host it meant to reach. Every handshake, read, and write takes the same deadline and cancellation token as the TCP calls.

A server needs a certificate chain and its private key, as PEM text. roots_from_pem turns CA certificates into TrustRoots; real clients usually call std.tls.system_roots() instead, which reads the operating system’s bundle. This lesson embeds a test CA so that it runs offline.

import std.bytes
import std.tls
from std.net import NetError, TcpListener, connect, listen, loopback_v4
from std.sync import CancellationToken
from std.time import Deadline, deadline_after, milliseconds, monotonic_now
from std.tls import ServerConfig, TlsError

# Test-only certificates: a CA, and a leaf it signed for localhost and
# 127.0.0.1. Never use a published key outside an example.
def ca_pem() -> str:
    return "-----BEGIN CERTIFICATE-----\nMIIBmjCCAT+gAwIBAgIULOQ2Qxr8CVZY2bG0uRYKKEdMD3swCgYIKoZIzj0EAwIw\nGTEXMBUGA1UEAwwOUGxlbnR5IFRlc3QgQ0EwIBcNMjQwMTAxMDAwMDAwWhgPMjEy\nNDAxMDEwMDAwMDBaMBkxFzAVBgNVBAMMDlBsZW50eSBUZXN0IENBMFkwEwYHKoZI\nzj0CAQYIKoZIzj0DAQcDQgAEkiB3Ogehrrt6YbQghwDPv6tkhE+z9zTCVih8fvtR\nZ1/691Nfh7/J9IgaNgNSTMJjGTolzGnjaCUDqoARlFyM+6NjMGEwHQYDVR0OBBYE\nFOTFBDgWXHj/qmVJDMlAHYCEZFX5MB8GA1UdIwQYMBaAFOTFBDgWXHj/qmVJDMlA\nHYCEZFX5MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgIEMAoGCCqGSM49\nBAMCA0kAMEYCIQD41ATQnIjKkKMLQAb9AyoyOFmljYZyz348XJEIbqMgKwIhAL3m\nmWd8On5gvkfytspatH5mDWOcSyW+au7vuIC2gHhO\n-----END CERTIFICATE-----\n"

def leaf_pem() -> str:
    return "-----BEGIN CERTIFICATE-----\nMIIBnDCCAUKgAwIBAgIBATAKBggqhkjOPQQDAjAZMRcwFQYDVQQDDA5QbGVudHkg\nVGVzdCBDQTAgFw0yNDAxMDEwMDAwMDBaGA8yMTI0MDEwMTAwMDAwMFowFDESMBAG\nA1UEAwwJbG9jYWxob3N0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8THNQeTi\nJ6Eq0tSxtHW5Q4TCsbIqB3MOtM98ERyKgl6kHeHCeDZYKx8B+VRGT2jkWuTeN24Q\nljwUfh7uh7Iaz6N+MHwwGgYDVR0RBBMwEYIJbG9jYWxob3N0hwR/AAABMAkGA1Ud\nEwQCMAAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwHQYDVR0OBBYEFEIFC2Fyd4cfAKTx\nMiNCMdDkqDNbMB8GA1UdIwQYMBaAFOTFBDgWXHj/qmVJDMlAHYCEZFX5MAoGCCqG\nSM49BAMCA0gAMEUCIAUF3HH43lnirgSN1Vr4twZrFmXKBxlbYNQni/h4ZaxhAiEA\n5rD/yE1B4IsbEGCMlyAeX507leXfGY9+wrHaxikU/w0=\n-----END CERTIFICATE-----\n"

def leaf_key() -> str:
    return "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQglvSEE3H7v+mInxSF\nAyWFDKYpzpXr20wH8mfFY3bCaB2hRANCAATxMc1B5OInoSrS1LG0dblDhMKxsioH\ncw60z3wRHIqCXqQd4cJ4NlgrHwH5VEZPaORa5N43bhCWPBR+Hu6HshrP\n-----END PRIVATE KEY-----\n"

# Answers one message on one connection.
def echo(listener: &TcpListener, config: &ServerConfig, deadline: &Deadline, cancel: &CancellationToken) -> Result[i64, AllocError | TlsError | std.bytes.BytesError]:
    tcp = listener.accept(deadline, cancel).map_error(TlsError.Net)?
    mut stream = std.tls.accept(tcp, config, deadline, cancel)?
    mut buffer = std.bytes.with_capacity(64)?
    buffer.resize(64, 0u8)?
    count = stream.read_before(&mut buffer, 0, 64, deadline, cancel)?
    stream.write_all_before(&buffer[:count], deadline, cancel)?
    stream.close_before(deadline, cancel)?
    Ok(count)

type TlsConnectionError = RangeError | TlsError | NetError | std.bytes.BytesError

def main() -> Result[(), SpawnError[()] | IoError | AllocError | TlsConnectionError]:
    cancel = CancellationToken()?
    deadline = deadline_after(&monotonic_now(), &milliseconds(5000))?
    roots = std.tls.roots_from_pem(&ca_pem())?
    client = std.tls.client_config(&roots)?
    server = std.tls.server_config(&leaf_pem(), &leaf_key())?
    listener = listen(&loopback_v4(0u16), 8)?
    address = listener.local_address()?
    with spawn(echo, &listener, &server, &deadline, &cancel)? as worker:
        tcp = connect(&address, &deadline, &cancel)?
        mut stream = std.tls.connect(tcp, &client, "localhost", &deadline, &cancel)?
        message = std.bytes.from_text("hello")?
        stream.write_all_before(&message, &deadline, &cancel)?
        mut reply = std.bytes.with_capacity(16)?
        reply.resize(16, 0u8)?
        count = stream.read_before(&mut reply, 0, 16, &deadline, &cancel)?
        print(reply.to_text_range(0, count)?)?
        print(stream.read_before(&mut reply, 0, 16, &deadline, &cancel)?)?
        stream.close_before(&deadline, &cancel)?
        print(worker.join()?)?
    with spawn(echo, &listener, &server, &deadline, &cancel)? as worker:
        tcp = connect(&address, &deadline, &cancel)?
        match std.tls.connect(tcp, &client, "example.test", &deadline, &cancel):
            case Err(error):
                print(error)?
            case Ok(_):
                print("unexpected")?
        match worker.join():
            case Err(error):
                match error:
                    case tls: TlsError:
                        print(tls)?
                    case buffer: std.bytes.BytesError:
                        print(buffer)?
                    case cause: AllocError:
                        print(cause)?
            case Ok(_):
                print("unexpected")?
    Ok(())
hello
0
5
std.tls.TlsError.WrongHost
std.tls.TlsError.Rejected

std.tls.connect takes ownership of the TCP stream and returns only after the handshake, so a TlsStream is always verified. The second read returns 0 because the server sent close_notify before closing. A connection that ends without it is TlsError.Truncated, since an attacker can cut a TCP connection short.

The second client asks for example.test, which the certificate does not name. It fails with WrongHost and tells the server, whose handshake fails with Rejected. An unknown issuer is UntrustedCertificate, and an expired certificate is ExpiredCertificate.

TlsStream also has read, write, flush, and close, so it works with std.io helpers and std.http framing. Those methods wait without a deadline.

See std.tls for the full reference.