nupp.io.tls

nupp.io.tls takes ownership of a connection nupp.io.net already opened and turns it into an encrypted connection.

local function fetch(): string
    local stream = assert(nupp.io.net.connect({host = "example.com", port = 443}))
    local session = assert(nupp.io.tls.client(stream, {
        hostname = "example.com",
    }))
    assert(session:handshake())
    assert(session:write("GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n"))
    const body = assert(session:read(1024))
    session:close()

    return body
end

Passing authority selects exactly those PEM roots. Omitting it uses the platform's root certificates. SSL_CERT_FILE and SSL_CERT_DIR override the platform on every system; without them, macOS reads Security.framework anchors, Windows reads its root stores, and other Unix systems search the ordinary distribution locations. The roots are loaded once per process. An explicitly configured file which cannot be read is a failure rather than permission to silently trust something else.

A session reads and writes through the same Reader and Writer contracts a plain connection does, so a parser written against byte I/O reads an encrypted connection without knowing it has one.

The session owns the connection. There is no plaintext handle alongside it and no second reactor polling the same socket. Closing the session sends close_notify, drains it, and closes the transport.

Compatible client and server configurations are cached in bounded native process state so Rustls can reuse its resumption state across sessions and worker lanes. It is not persistent across processes. Resumption never sends application data before the handshake: TLS 1.3 early data is deliberately not enabled because accepting it requires an application-level replay policy.

Submodules

ModuleDescription
nupp.io.tls.spiImplementation interface for TLS transports.

Module contents

Types

TypeKindDescription
ClientOptionstypeHow a client session is opened.
ServerOptionstypeHow a server session is opened.
SessionrecordAn encrypted connection.

Functions

FunctionKindDescription
clientfunctionEncrypts a connection as the client.
serverfunctionEncrypts a connection as the server.

Types#

ClientOptionstype#

type ClientOptions = {
    --- The DNS name or IP address to expect on the peer's certificate. A DNS
    --- name is also sent as SNI. Given whenever `verify` is on, because a
    --- certificate verified against no name is a certificate belonging to
    --- anybody. Without verification it defaults to the peer's address, which
    --- a path connection does not have, so a client over one names its host.
    hostname: string?,

    --- Certificates to trust, as PEM. Nil uses the platform's root
    --- certificates; a supplied string replaces them.
    authority: string?,

    --- Whether the peer's certificate must satisfy the trust store. On by
    --- default: a client that does not verify has encryption without
    --- authentication, which is a different and much weaker thing than TLS is
    --- usually taken to mean.
    verify: boolean?,

    --- Application protocols to offer, in decreasing preference order, such as
    --- `{"h2", "http/1.1"}`. The server chooses from these, so the order is a
    --- preference and not a demand.
    protocols: {string}?

}

How a client session is opened.

ServerOptionstype#

type ServerOptions = {
    --- The certificate chain to present, as PEM.
    certificate: string,

    --- The private key for it, as PEM.
    privateKey: string,

    --- Application protocols this server speaks, in decreasing preference
    --- order. The server picks, so this order is the one that decides.
    ---
    --- A server that names protocols and shares none with a client refuses the
    --- handshake rather than continuing without one, which is what makes naming
    --- them a commitment rather than a hint.
    protocols: {string}?

}

How a server session is opened.

Sessionrecord#

record tls.Session is Reader, Writer ...

An encrypted connection.

Owns the connection it encrypts over. Closing the session sends close_notify, drains it, and closes the socket.

Members

NameKindDescription
isConnectedmethodWhether the connection underneath is still open.
isReadymethodWhether the handshake has finished.
isReleasedmethodWhether this session has been released.
isEndedmethodWhether the peer closed its sending half.
protocolmethodThe application protocol both sides agreed on, or nil when none was negotiated.
isVerifiedmethodWhether the peer's certificate satisfied what was asked of it.
isResumedmethodWhether this connection skipped the full asymmetric handshake by resuming a cached session.
stepmethodDrives the handshake one pass, without suspending.
handshakemethodDrives the handshake to completion, suspending while it waits on the connection.
readmethodReads up to count decrypted bytes, suspending while none have arrived.
readSpanmethodReads directly into a checked writable span.
readIntomethodReads into a buffer.
transferTomethodWrites everything left to a writer.
writemethodWrites plaintext, in as many records as it takes.
writeSpanmethodAppends bytes from a checked shared span.
flushmethodWaits until the records this session wrote have left the process.
closemethodSends closenotify, drains it, and closes the owned connection.

isConnectedmethod#

isConnected: function isConnected(self): boolean

Whether the connection underneath is still open.

False once this session is released: closing frees the native session, so the question has to be answered from this record's own state rather than asked of memory that has been given back.

Arguments
NameTypeDescription
selfany
Returns
TypeDescription
boolean

isReadymethod#

isReady: function isReady(self): boolean

Whether the handshake has finished.

Arguments
NameTypeDescription
selfany
Returns
TypeDescription
boolean

isReleasedmethod#

isReleased: function isReleased(self): boolean

Whether this session has been released.

Arguments
NameTypeDescription
selfany
Returns
TypeDescription
boolean

isEndedmethod#

isEnded: function isEnded(self): boolean

Whether the peer closed its sending half.

Arguments
NameTypeDescription
selfany
Returns
TypeDescription
boolean

protocolmethod#

protocol: function protocol(self): string?

The application protocol both sides agreed on, or nil when none was negotiated.

Nil is not a failure: it is what neither side offering a list looks like. A server that offered a list sharing nothing with the client's refuses the handshake instead, so a session that exists and returns nil was never asked to pick one.

Arguments
NameTypeDescription
selfany
Returns
TypeDescription
string?

isVerifiedmethod#

isVerified: function isVerified(self): boolean

Whether the peer's certificate satisfied what was asked of it.

Answered after the handshake and not before, because before it there is no certificate to have an opinion about.

Arguments
NameTypeDescription
selfany
Returns
TypeDescription
boolean

isResumedmethod#

isResumed: function isResumed(self): boolean

Whether this connection skipped the full asymmetric handshake by resuming a cached session.

Servers return false. The server-side store is deliberately opaque: a client can use this answer to observe its cache, while accepting a session does not change what a server application may do with it.

Arguments
NameTypeDescription
selfany
Returns
TypeDescription
boolean

stepmethod#

step: function step(self): boolean?, io.Error?

Drives the handshake one pass, without suspending.

The form a host with its own loop wants, and the form two sessions in one thread need: a handshake is two peers taking turns, so something has to be able to give one side a turn without waiting for it to finish. handshake is this in a loop with the waiting done for you.

Arguments
NameTypeDescription
selfany

this session

Returns
TypeDescription
boolean?

whether the handshake has finished, or nil when it failed

io.Error?

why it failed, when it did

handshakemethod#

handshake: function handshake(self): boolean, io.Error?

Drives the handshake to completion, suspending while it waits on the connection.

Arguments
NameTypeDescription
selfany

this session

Returns
TypeDescription
boolean

whether the handshake finished

io.Error?

why it did not, when unsuccessful

readmethod#

read: function read(exclusive self, count: integer): string?, string?

Reads up to count decrypted bytes, suspending while none have arrived.

An empty answer is the end, exactly as on a plain connection, and it is only ever the end: here that end is the peer's close_notify and nothing else. A connection that simply stopped is a truncated stream and is reported as a failure, because what arrived is the front of a message rather than the whole of one and no authenticated party said otherwise.

Arguments
NameTypeDescription
exclusive selfany

this session

countinteger

the most bytes to read

Returns
TypeDescription
string?

the bytes, or nil when the session is closed

string?

why it could not read, when unsuccessful

Raises
TypeCondition
string

when count is not a positive integer

readSpanmethod#

readSpan: function readSpan(exclusive self, exclusive destination: span.Writable<uint8>): integer?, string?

Reads directly into a checked writable span. A zero answer is the end.

Arguments
NameTypeDescription
exclusive selfany
exclusive destinationspan.Writable<uint8>
Returns
TypeDescription
integer?
string?
Raises
TypeCondition
string

when the destination is empty

readIntomethod#

readInto: function readInto(exclusive self, exclusive destination: Buffer, offset: integer?, count: integer?): integer?, string?

Reads into a buffer. A zero answer is the end.

Arguments
NameTypeDescription
exclusive selfany
exclusive destinationBuffer
offsetinteger?
countinteger?
Returns
TypeDescription
integer?
string?
Raises
TypeCondition
string

when count is not a positive integer

transferTomethod#

transferTo: function transferTo(exclusive self, exclusive destination: Writer): integer?, string?

Writes everything left to a writer.

Arguments
NameTypeDescription
exclusive selfany
exclusive destinationWriter
Returns
TypeDescription
integer?
string?

writemethod#

write: function write(exclusive self, bytes: string): boolean, string?

Writes plaintext, in as many records as it takes.

Arguments
NameTypeDescription
exclusive selfany

this session

bytesstring

the bytes to append

Returns
TypeDescription
boolean

whether they were written

string?

why they were not, when unsuccessful

writeSpanmethod#

writeSpan: function writeSpan(exclusive self, borrows source: span.ByteSpan): integer?, string?

Appends bytes from a checked shared span.

Arguments
NameTypeDescription
exclusive selfany
borrows sourcespan.ByteSpan
Returns
TypeDescription
integer?
string?

flushmethod#

flush: function flush(self): boolean, string?

Waits until the records this session wrote have left the process.

A TLS write puts encrypted records on the connection underneath, and that connection has a queue. Reporting success while they sit in it would let a caller write, flush, close the connection, and have the records cancelled with it.

Arguments
NameTypeDescription
selfany
Returns
TypeDescription
boolean
string?

closemethod#

close: function close(takes self): nil

Sends close_notify, drains it, and closes the owned connection. Terminal and idempotent.

The drain is bounded: a peer that has stopped reading gets about a second for the last records and the alert before the connection is closed without them.

Arguments
NameTypeDescription
takes selfany
Returns
TypeDescription
nil

Functions#

tls.clientfunction#

function tls.client(takes stream: net.Stream, options: tls.ClientOptions?): affine(tls.Session)?, io.Error?

Encrypts a connection as the client.

Every option is optional. Verification is on unless verify is false, and a verified session needs the hostname to check the certificate against, so a client that names no options is refused for that rather than connecting to whoever answers.

Arguments

NameTypeDescription
takes streamnet.Stream

the connection to adopt; it is consumed even when setup fails

optionstls.ClientOptions?

what to expect of the peer

Returns

TypeDescription
affine(tls.Session)?

the session, which the caller owns and must close

io.Error?

why it could not be made, when unsuccessful

Raises

TypeCondition
string

when verification is asked for without a name to verify against

tls.serverfunction#

function tls.server(takes stream: net.Stream, options: tls.ServerOptions): affine(tls.Session)?, io.Error?

Encrypts a connection as the server.

Arguments

NameTypeDescription
takes streamnet.Stream

the connection to adopt; it is consumed even when setup fails

optionstls.ServerOptions

the server identity and application protocols to present

Returns

TypeDescription
affine(tls.Session)?

the session, which the caller owns and must close

io.Error?

why it could not be made, when unsuccessful