std/tls/lib

std/tls/src/lib.trb

TLS over a TcpStream: TlsStream.connect for a client, TlsStream.accept with a ServerIdentity for a server, and a stream with the same two directions a TcpStream has (docs/design/NETWORK.md section 5). A client proves who it is with a ClientIdentity in its TlsSettings (mutual TLS) when the server asks for one through clientRoots. A Transport is one connection of TCP or TLS over TCP behind the same four operations, upgradable in the middle for protocols that ask for TLS after they began (Postgres, MySQL, STARTTLS).

const tcp = TcpStream.connectTo("example.test", 443).await()?
var stream = TlsStream.connect(tcp, "example.test").await()?
stream.send("GET / HTTP/1.1\r\nHost: example.test\r\n\r\n".bytes()).await()?

The protocol is mbedTLS 3, TLS 1.2 and 1.3. A client checks the server's certificate for the name it asks for: on Windows the platform decides - its roots, its enterprise roots, its policies - and elsewhere the system's bundle of roots does; TlsSettings.trusted replaces both with roots of the program's own. There is no switch that turns the check off.

TLS is a state machine over bytes here: every wait is a wait of the TcpStream under it, so a TLS read is cancelled, timed out and paced exactly as a TCP read is.

type TlsSettings

type TlsSettings

What a client trusts. The default is the platform's judgement, which is what every other program of the machine trusts.

field trusted

trusted: List<String> = []

PEM certificates to trust instead of the platform: a private root of a company, the root of a test. Empty is the platform's roots, and a list that names any is only these.

field protocols

protocols: List<String> = []

The application protocols to offer through ALPN (RFC 7301), in preference order: ["h2", "http/1.1"]. Empty offers none, which is what a client without ALPN does. The server picks one of them or none; ask TlsStream.negotiatedProtocol which.

field identity

identity: ClientIdentity? = None

The certificate chain and key to present where the server asks for a client certificate (mutual TLS). None presents none. A server that asks for none ignores it, and the certificate is only sent if the server asks.

type ServerIdentity

shared type ServerIdentity with Close

What a server proves who it is with: its certificate chain and the private key of the first certificate, both PEM. Parsed once and shared by every connection of a server; a shared type, because the key it holds is the runtime's.

field handle

protected var handle: Int

The runtime's handle: what a server session is started with. Read by TlsStream.accept, written by nothing.

fn of

static fn of(certificates: String, privateKey: String): Result<Self, NetworkError>

The identity of a PEM chain - the server's own certificate first, then the intermediates - and the PEM private key that belongs to the first certificate.

Errors

  • NetworkError that isTlsFailure() where a certificate or the key cannot be read, or the key is not the certificate's.

fn close

var fn close()

Releases the runtime's identity; a session accepted with it keeps its own share and keeps working.

type ClientIdentity

shared type ClientIdentity with Close

What a client proves who it is with where a server asks for a certificate: its certificate chain and the private key of the first certificate, both PEM. The same contract as ServerIdentity, a separate type so that a client cannot be handed to accept or a server's identity to TlsSettings.identity.

field handle

protected var handle: Int

The runtime's handle: what a client session presents. Read by TlsStream.connect, written by nothing.

fn of

static fn of(certificates: String, privateKey: String): Result<Self, NetworkError>

The identity of a PEM chain - the client's own certificate first, then the intermediates - and the PEM private key that belongs to the first certificate.

Errors

  • NetworkError that isTlsFailure() where a certificate or the key cannot be read, or the key is not the certificate's.

fn close

var fn close()

Releases the runtime's identity; a session connected with it keeps its own share and keeps working.

type TlsStream

shared type TlsStream with Close

One TLS connection over a TcpStream: bytes in both directions, encrypted and authenticated. A shared type, like the stream it holds; it closes when the last reference to it goes, and the TCP stream with it.

fn connect

static fn connect(stream: TcpStream, serverName: String, settings: TlsSettings = TlsSettings()): Task<Result<Self, NetworkError>>

The client's side: a handshake over stream with a server that must prove it is serverName.

Errors

  • NetworkError that isCertificateRejected() where the certificate is not trusted for that name, and isTlsFailure() where the handshake fails otherwise, or where a name of settings.protocols is empty, longer than 255 bytes or holds a newline.

fn accept

static fn accept(stream: TcpStream, identity: ServerIdentity, protocols: List<String> = [], clientRoots: List<String> = []): Task<Result<Self, NetworkError>>

The server's side: a handshake over an accepted stream, proving identity. protocols are the application protocols the server accepts through ALPN, in its preference order: the first of them the client offers is chosen. Empty accepts none and ignores the client's offer, which is what a server without ALPN does. A client that offers protocols of which the server accepts none is refused with a failure that isTlsFailure().

A non-empty clientRoots (PEM certificates) makes the client's certificate mandatory: it must chain to exactly these roots - never the platform's, a client certificate is issued by the server's operator - and a client that presents none or another is refused with a failure that isTlsFailure() or isCertificateRejected(). Roots of which any block cannot be read fail before the handshake with isTlsFailure(), never as "required, trusting nothing". Empty requests no certificate, which is what a server without mutual TLS does; it ignores one a client offers.

fn protocol

fn protocol(): String

The version the two sides agreed on: "TLSv1.3" or "TLSv1.2".

fn negotiatedProtocol

fn negotiatedProtocol(): String?

The application protocol the two sides agreed on through ALPN: Some("h2"), Some("http/1.1"), ... None where neither side, or only one, named protocols, so none was chosen and the stream carries whatever the peers expect.

fn localAddress

fn localAddress(): Result<SocketAddress, NetworkError>

The address of this end.

fn remoteAddress

fn remoteAddress(): Result<SocketAddress, NetworkError>

The address of the other end.

fn receive

var fn receive(maximum: Int = 65536): Task<Result<Bytes?, NetworkError>>

At most maximum bytes, as soon as any were decrypted: Some(bytes), or None once the peer ended the session.

fn receiveBuffer

var fn receiveBuffer(maximum: Int = 65536): Task<Result<Buffer<UInt8>?, NetworkError>>

TlsStream.receive as the ArrayList the bytes arrive in.

fn send

var fn send(bytes: Bytes): Task<Result<Void, NetworkError>>

Writes all of bytes, encrypted; finishes once the operating system took the ciphertext.

fn sendBuffer

var fn sendBuffer(bytes: Buffer<UInt8>): Task<Result<Void, NetworkError>>

TlsStream.send for bytes that are an ArrayList already.

fn shutdown

var fn shutdown(): Task<Result<Void, NetworkError>>

Ends this half: a close_notify for the peer, then the end of the TCP stream's writing half.

fn source

fn source(chunk: Int = 65536): TlsSource

The reading direction as a Source: at most chunk bytes per next(), and None once the peer ended.

fn sink

fn sink(): TlsSink

The writing direction as a Sink: end() ends this half with close_notify.

fn close

var fn close()

Frees the session; the TCP stream closes with its own release.

type TlsSource

shared type TlsSource with Source<Bytes, NetworkError>

The reading direction of a TlsStream: a Source<Bytes, NetworkError>.

fn of

static fn of(stream: TlsStream, chunk: Int): Self

The source of stream: what TlsStream.source answers.

fn next

var fn next(): Task<Result<Bytes?, NetworkError>>

At most chunk bytes as TlsStream.receive answers them, or None once the peer ended the session.

fn close

var fn close()

Nothing of its own: the stream closes once its last holder is gone.

type TlsSink

shared type TlsSink with Sink<Bytes, NetworkError>

The writing direction of a TlsStream: a Sink<Bytes, NetworkError>.

fn of

static fn of(stream: TlsStream): Self

The sink of stream: what TlsStream.sink answers.

fn add

var fn add(item: Bytes): Task<Result<Void, NetworkError>>

Sends item over the session, as TlsStream.send does.

fn end

var fn end(): Task<Result<Void, NetworkError>>

Ends this half of the session with close_notify.

fn close

var fn close()

Nothing of its own: the stream closes once its last holder is gone.

type Transport

shared type Transport

A connection as a protocol reads and writes it: plain TCP, or TLS over TCP, behind the same four operations - receive, send, finish, remote - and upgradable in the middle of the connection, which is what Postgres's TLS request, MySQL's SSL request, SMTP's STARTTLS and an https URL have in common. A shared type, like the streams it holds.

var transport = Transport.plain(TcpStream.connectTo("mail.example.test", 587).await()?)
// ...the greeting, `EHLO`, then the request for TLS and the server's `220` answer, all in the clear
transport.sendBuffer("STARTTLS\r\n".bytes()).await()?
const _ = transport.receiveBuffer().await()?
transport.upgrade("mail.example.test").await()?
// every byte from here on is encrypted, and `EHLO` is said again
print transport.isEncrypted()

A transport buffers nothing itself: receiveBuffer hands over what the stream gave, so no byte of the plain phase can wait in it and be read as if it were encrypted. A caller that reads through a buffer of its own (a Reader) has to have consumed that buffer before upgrade or upgradeAccepting: bytes a peer sent in the clear after the request for TLS and before the handshake are an injection (CVE-2011-0411 and its kin), and a caller that finds some left must close the connection rather than upgrade it. No send, receive or upgrade may be pending while one runs.

fn plain

static fn plain(stream: TcpStream): Self

Plain TCP.

fn encrypted

static fn encrypted(stream: TcpStream, session: TlsStream): Self

TLS over stream, after its handshake: what TlsStream.connect or TlsStream.accept answered for it.

fn isEncrypted

fn isEncrypted(): Bool

Whether the bytes are encrypted.

fn tls

fn tls(): TlsStream?

The TLS session, for TlsStream.negotiatedProtocol, TlsStream.protocol and the peer's certificate; None while the bytes are plain, and so always for a transport that was never upgraded.

fn upgrade

var fn upgrade(serverName: String, settings: TlsSettings = TlsSettings()): Task<Result<Void, NetworkError>>

The client's side of the middle upgrade: a TLS handshake as TlsStream.connect runs it, over the plain TCP stream this transport holds, with a server that must prove it is serverName. Every later receive and send is encrypted.

Errors

  • NetworkError that is none of the TLS kinds, with nothing sent, where the transport is encrypted already: the upgrade is refused and the transport goes on as it was.
  • The errors of TlsStream.connect: isCertificateRejected() where the certificate is not trusted for the name, isTlsFailure() where the handshake fails otherwise. The transport is unusable after a failed handshake: every later operation fails with isClosed().

fn upgradeAccepting

var fn upgradeAccepting(identity: ServerIdentity, protocols: List<String> = [], clientRoots: List<String> = []): Task<Result<Void, NetworkError>>

The server's side of the middle upgrade, with the semantics of TlsStream.accept: a handshake proving identity, protocols accepted through ALPN, and a client certificate required against clientRoots where that is not empty.

Errors

  • As Transport.upgrade: a refusal where the transport is encrypted already, the errors of TlsStream.accept otherwise, and an unusable transport after a failed handshake.

fn receiveBuffer

var fn receiveBuffer(maximum: Int = 65536): Task<Result<Buffer<UInt8>?, NetworkError>>

At most maximum bytes, as soon as any arrived; None at the end of the stream.

fn sendBuffer

var fn sendBuffer(bytes: Buffer<UInt8>): Task<Result<Void, NetworkError>>

Writes all of bytes.

fn finish

var fn finish(): Task<Result<Void, NetworkError>>

Ends the writing half: close_notify first where the bytes are encrypted.

fn remote

fn remote(): SocketAddress?

The address of the other end, where the system still knows it.

type TlsResolver

type TlsResolver with NameResolver

DNS over TLS (RFC 7858): the lookups of std/network's Resolver, asked of one name server over a TLS connection to its port 853 - the question and the answer each after their length in two bytes, as over TCP (RFC 7766) - with the server authenticated by the name its certificate has to carry (RFC 8310's strict profile: a server that cannot prove serverName is never asked).

const resolver = TlsResolver(SocketAddress(IpAddress.tryFrom("192.0.2.53")?, 853), "dns.example.test")
const records = resolver.lookup(DomainName.tryFrom("example.test")?, RecordType.Aaaa).await()?

Every lookup opens a connection of its own and closes it with the answer: keeping one open for the next question is a later refinement. Times are milliseconds, because a field's default is a constant and a Duration is none.

field server

server: SocketAddress

The name server, usually on port 853.

field serverName

serverName: String

The name its certificate has to carry.

field settings

settings: TlsSettings = TlsSettings()

What the client trusts: the platform's roots by default.

field attemptMilliseconds

attemptMilliseconds: Int = 5000

How long connecting, the handshake and the answer may take together.

fn lookup

fn lookup(name: DomainName, recordType: RecordType): Task<Result<List<Record>, NetworkError>>

The records of recordType that name has, CNAME chains followed within the answer.

Errors

  • NetworkError that isHostNotFound() where the name does not exist (NXDOMAIN), and isNameServerFailure() where the server answered SERVFAIL, REFUSED or NOTIMP.
  • Every failure of TlsResolver.exchange.

fn exchange

fn exchange(name: DomainName, recordType: RecordType): Task<Result<Message, NetworkError>>

The whole response to a query for recordType at name, with a random identifier (RFC 5452).

Errors

  • NetworkError that isTimedOut() where the attempt's time passed, isCertificateRejected() where the server could not prove its name, and isNameServerFailure() where it answered something that is not the response.