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
NetworkErrorthatisTlsFailure()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
NetworkErrorthatisTlsFailure()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
NetworkErrorthatisCertificateRejected()where the certificate is not trusted for that name, andisTlsFailure()where the handshake fails otherwise, or where a name ofsettings.protocolsis 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
NetworkErrorthat 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 withisClosed().
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 ofTlsStream.acceptotherwise, 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
NetworkErrorthatisHostNotFound()where the name does not exist (NXDOMAIN), andisNameServerFailure()where the server answeredSERVFAIL,REFUSEDorNOTIMP.- 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
NetworkErrorthatisTimedOut()where the attempt's time passed,isCertificateRejected()where the server could not prove its name, andisNameServerFailure()where it answered something that is not the response.