js/net/tls
js/net/tls.ts
fino:tls — TLS socket layer.
TLS 1.3 specification: https://www.rfc-editor.org/rfc/rfc8446
TlsSocket extends Socket from fino:socket. TCP connection setup is
shared via connectTcp() — no duplication. TlsSocket overrides split()
to return TlsReader/TlsWriter (which handle SSL_read/SSL_write), and
overrides close() to perform SSL teardown before the fd close.
TlsReader and TlsWriter extend the base Reader/Writer from
fino:stream, implementing the template methods with SSL-specific I/O.
All loop machinery (readability waiting, retry, backpressure, async
iteration, pipe) is inherited — no duplication.
TLS options
{ hostname: 'example.com', // SNI + hostname verification ca: '/path/to/ca.pem', // custom CA file (optional) cert: '/path/client.pem', // client certificate (optional) key: '/path/client.key', // client private key (optional) rejectUnauthorized: true, // verify peer cert (default: true) alpn: ['h2', 'http/1.1', // client protocol preference (optional) }
Release policy
This module is release-supported in an OpenSSL-enabled build. Release CI
must include a lane where tlsAvailable is true; builds without libssl may
still run, but TLS tests and TLS-dependent HTTP features are expected to be
skipped or gated explicitly.
Protocol selection is exposed through ALPN only. Callers may offer client
protocol preferences with alpn, and servers that use this socket layer
publish their supported protocols through their own listener configuration.
Cipher-suite, minimum/maximum protocol version, and session-reuse controls
intentionally use OpenSSL defaults in this API. If a deployment needs a
stricter TLS policy, configure the OpenSSL installation or use a higher-level
server API that exposes a narrower audited knob.
There is no public TLS session cache or session-ticket reuse API here. Mutual TLS uses handshake-time client certificates only; post-handshake client authentication is intentionally outside this module.
Close coordination
When split() is used, both TlsReader and TlsWriter share the SSL* pointer.
When both halves close, SSL is shut down and freed, then the fd is closed via
`super.close()`. When `close()` is called directly (without splitting), SSL
teardown happens before calling `super.close()`.
import { TlsSocket } from 'fino:tls';
const socket = await TlsSocket.connect(
{ family: 'ipv4', ip: '93.184.216.34', port: 443 },
{ hostname: 'example.com', alpn: ['h2', 'http/1.1'] },
);
const [reader, writer] = socket.split();
Interfaces
interface TlsConnectOptions extends ConnectOptions {
Options for opening or upgrading a TLS socket.
rejectUnauthorized defaults to true; pass false only for local testing
or explicitly trusted endpoints. hostname is used for SNI and certificate
verification when provided.
const tls = await TlsSocket.connect(addr, {
hostname: 'example.com',
alpn: ['h2', 'http/1.1'],
});
Properties
hostname?: string
Hostname used for SNI and peer certificate checks.
await TlsSocket.connect(addr, { hostname: 'example.com' });
servername?: string
Alias for hostname, matching other TLS option surfaces.
If both names are provided they must be identical.
await TlsSocket.connect(addr, { servername: 'example.com' });
ca?: string
Path to a PEM CA bundle or file loaded with OpenSSL verify locations.
await TlsSocket.connect(addr, { hostname: 'internal.test', ca: '/etc/ssl/internal-ca.pem' });
cert?: string
Path to the PEM client certificate chain presented when requested.
Must be paired with key; partial certificate configuration throws before
the TCP connection is opened.
await TlsSocket.connect(addr, { cert: './client.pem', key: './client.key' });
key?: string
Path to the PEM private key matching cert.
await TlsSocket.connect(addr, { cert: './client.pem', key: './client.key' });
rejectUnauthorized?: boolean
Whether to verify the peer certificate; defaults to true.
await TlsSocket.connect(addr, { rejectUnauthorized: false });
alpn?: string[]
ALPN protocol list offered by the client in preference order.
await TlsSocket.connect(addr, { hostname: 'example.com', alpn: ['h2', 'http/1.1'] });
interface TlsServerContextOptions {
Options for creating a server-side TLS context.
clientAuth: 'request' asks clients for a certificate but allows anonymous
handshakes. clientAuth: 'require' fails the handshake when no acceptable
client certificate is provided.
Properties
cert: string
PEM certificate chain presented by the server.
key: string
PEM private key matching cert.
ca?: string
PEM CA bundle used to verify client certificates.
clientAuth?: TlsClientAuth
Client certificate policy. Defaults to 'none'.
rejectUnauthorized?: boolean
Whether verification failures abort the handshake. Defaults to true.
alpn?: readonly string[]
ALPN protocols offered by the server.
interface TlsVerifyResult {
Verification status reported by OpenSSL after a TLS handshake.
Properties
code: number
OpenSSL verification code. 0 means success.
reason: string | null
Human-readable OpenSSL reason, or null when verification succeeded.
interface TlsPeerInfo {
Peer TLS identity metadata.
The certificate is the DER-encoded leaf certificate when one was presented.
Treat it as authenticated identity only when authorized is true.
Properties
peerCertificate: Uint8Array | null
Leaf peer certificate in DER form, or null when none was presented.
verify: TlsVerifyResult
Verification status from OpenSSL.
authorized: boolean
True when OpenSSL verification succeeded.
Types
type TlsClientAuth = 'none' | 'request' | 'require'
Client-certificate policy for server-side TLS handshakes.
Classes
class TlsReader extends BufferedBytesReader {
Read half of a TLS connection. Extends BufferedBytesReader with SSL-specific I/O: uses SSL_read instead of read(2), checks SSL_pending before waiting for fd readability, and classifies SSL error codes correctly.
Buffering, structural reads (readExactly, readUntil, etc.), and the async iterator protocol are all inherited from BufferedBytesReader.
const [reader] = tls.split();
const bytes = await reader.read();
Constructors
constructor(ssl: object, fd: number, onClose: () => void | Promise<void>)
Wrap OpenSSL state and a non-blocking fd as a TLS reader.
The reader does not own the SSL pointer by itself; the close callback
coordinates cleanup with the paired TlsWriter.
const reader = new TlsReader(ssl, fd, onClose);
Getters
get fd(): number
Underlying socket file descriptor.
console.log(reader.fd);
Methods
override async close(): Promise<void>
class TlsWriter extends BufferedBytesWriter {
Write half of a TLS connection. Extends BufferedBytesWriter with SSL-specific I/O: uses SSL_write instead of write(2) and handles SSL_ERROR_WANT_READ during TLS renegotiation.
Write coalescing, pipe(), and async close() are inherited from BufferedBytesWriter.
const [, writer] = tls.split();
await writer.write(new TextEncoder().encode('hello'));
await writer.flush();
Constructors
constructor(ssl: object, fd: number, onClose: () => void | Promise<void>)
Wrap OpenSSL state and a non-blocking fd as a TLS writer.
The writer shares SSL ownership with a TlsReader; cleanup runs through
the supplied close callback.
const writer = new TlsWriter(ssl, fd, onClose);
Getters
get fd(): number
Underlying socket file descriptor.
console.log(writer.fd);
class TlsSocket extends Socket {
A connected TLS socket. Extends Socket with SSL state.
TCP connection setup is shared with Socket via connectTcp().
split() and close() are overridden to handle SSL teardown.
tls instanceof Socket is true.
Use the static factories rather than the constructor directly:
const tls = await TlsSocket.connect({ family: 'ipv4', ip: '93.184.216.34', port: 443 }, { hostname: 'example.com' });
const [reader, writer] = tls.split();
Constructors
constructor(fd: number, remoteAddr: Address | null, ssl: object, sslCtx: object | null)
Wrap an established TLS session.
The constructor takes ownership of ssl and, when non-null, sslCtx.
Prefer connect(), upgrade(), or accept() so handshakes and cleanup
are coordinated.
const tls = new TlsSocket(fd, remoteAddr, ssl, sslCtx);
Getters
get negotiatedProtocol(): string | null
The ALPN protocol negotiated during the TLS handshake, or null if none.
if (tls.negotiatedProtocol === 'h2') console.log('HTTP/2');
Methods
getPeerCertificate(): Uint8Array | null
Return the peer leaf certificate as DER bytes, or null.
This is raw certificate material. Treat it as authenticated identity only
when getPeerInfo().authorized is true.
const cert = tls.getPeerCertificate();
if (cert) console.log(cert.byteLength);
getVerifyResult(): TlsVerifyResult
Return OpenSSL's peer verification result for this session.
Code 0 means verification succeeded. When verification was disabled,
OpenSSL may still report the peer certificate's validation state, but the
result must not be treated as application authentication by itself.
const verify = tls.getVerifyResult();
console.log(verify.code, verify.reason);
getPeerInfo(): TlsPeerInfo
Return peer certificate and verification metadata together.
const info = tls.getPeerInfo();
if (info.authorized) console.log(info.peerCertificate?.byteLength);
split(): [TlsReader, TlsWriter]
Split into a [TlsReader, TlsWriter pair. SSL cleanup and fd close happen automatically when both halves have been closed.
Note: TLS does not support half-close (no SHUT_RD/SHUT_WR per half). Both sides must close before SSL_shutdown is issued.
const [reader, writer] = tls.split();
await writer.close();
await reader.close();
close()
Close both directions immediately. Performs SSL teardown, then delegates
fd cleanup to Socket.close(). Idempotent.
tls.close();
Static Methods
static async connect(addr: Address, opts: TlsConnectOptions = {}): Promise<TlsSocket>
Connect to a remote address over TLS.
Uses connectTcp() for the TCP layer (same helper as Socket.connect()),
then performs the TLS handshake.
Throws when OpenSSL is unavailable, TCP connection fails, certificate verification fails, or the TLS handshake fails.
const tls = await TlsSocket.connect(addr, { hostname: 'example.com' });
static async upgrade(socket: Socket, opts: TlsConnectOptions = {}): Promise<TlsSocket>
Upgrade an existing connected Socket to TLS (client side). The original Socket should not be used after this call.
On failure, the original socket fd remains caller-owned. This allows the caller to decide whether to close or recover it.
const raw = await Socket.connect(addr);
const tls = await TlsSocket.upgrade(raw, { hostname: 'example.com' });
static async accept(fd: number, sslCtx: object): Promise<TlsSocket>
Accept a TLS connection (server side) on an already-accepted TCP fd.
sslCtx is borrowed from the server and is not freed by the returned
socket. On handshake failure, the SSL object is freed and the fd remains
caller-owned.
const tls = await TlsSocket.accept(fd, sslCtx);