// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT //! Client-side TLS transport layered over an existing stream, using //! ianic/tls.zig (the same library that backs server-side STARTTLS). Works //! for both implicit TLS (connect, then `init` before any SMTP traffic) and //! STARTTLS (after `Client.starttls` succeeds, `init` over the same stream, //! then `Client.setTransport` and a fresh `hello`). //! //! The standard library's TLS client is not used because it requires the //! optional TLS 1.3 middlebox-compatibility ChangeCipherSpec record from the //! server; servers that disable it (e.g. Exim) break its handshake. //! //! The underlying stream reader and writer must each have a buffer of at //! least `min_buffer_len` bytes. `init` must be called on the `Tls` at its //! final resting address (the connection holds interior pointers), e.g.: //! //! ``` //! var tls_transport: Tls = undefined; //! try tls_transport.init(io, gpa, &stream_reader.interface, //! &stream_writer.interface, .{ .host = "smtp.example.com" }); //! ``` const Tls = @This(); const std = @import("std"); const Io = std.Io; const tls = @import("tls"); connection: tls.Connection, reader_state: tls.Connection.Reader, writer_state: tls.Connection.Writer, read_buffer: []u8, write_buffer: []u8, /// Minimum buffer size for the underlying stream reader and writer. pub const min_buffer_len = tls.input_buffer_len; pub const Options = struct { /// Host name the server's certificate must be valid for (also sent via /// SNI). Ignored with `ca = .insecure`. host: []const u8, ca: Ca = .system, pub const Ca = union(enum) { /// Verify the server certificate against the system trust store, /// loaded fresh for this connection. system, /// Verify against a caller-provided bundle (reusable across /// connections; see `tls.config.cert.fromSystem`). Not deinitialized /// by this transport. bundle: std.crypto.Certificate.Bundle, /// No certificate verification at all. This provides encryption but /// no authentication — fine for tests, unsafe on real networks. insecure, }; }; /// Performs the TLS handshake over `input`/`output` (the stream's /// reader/writer, each with a buffer of at least `min_buffer_len` bytes). /// Allocates plaintext buffers from `gpa`; free them with `deinit`. pub fn init( t: *Tls, io: Io, gpa: std.mem.Allocator, input: *Io.Reader, output: *Io.Writer, options: Options, ) !void { const read_buffer = try gpa.alloc(u8, 4096); errdefer gpa.free(read_buffer); const write_buffer = try gpa.alloc(u8, 4096); errdefer gpa.free(write_buffer); var rng_source: std.Random.IoSource = .{ .io = io }; var system_ca: ?std.crypto.Certificate.Bundle = null; defer if (system_ca) |*bundle| bundle.deinit(gpa); if (options.ca == .system) system_ca = try tls.config.cert.fromSystem(gpa, io); t.connection = try tls.client(input, output, .{ .rng = rng_source.interface(), .now = Io.Clock.real.now(io), .host = if (options.ca == .insecure) "" else options.host, .root_ca = switch (options.ca) { .system => system_ca.?, .bundle => |bundle| bundle, .insecure => .empty, }, .insecure_skip_verify = options.ca == .insecure, }); t.read_buffer = read_buffer; t.write_buffer = write_buffer; t.reader_state = t.connection.reader(read_buffer); t.writer_state = t.connection.writer(write_buffer); } /// The decrypted stream from the server. pub fn reader(t: *Tls) *Io.Reader { return &t.reader_state.interface; } /// The plaintext stream to the server; each flush encrypts and pushes the /// records through to the underlying stream. pub fn writer(t: *Tls) *Io.Writer { return &t.writer_state.interface; } /// Flushes pending data and sends a TLS close_notify alert, letting the /// server distinguish a clean shutdown from a truncation attack. Call before /// closing the underlying stream. pub fn end(t: *Tls) error{WriteFailed}!void { try t.writer_state.interface.flush(); t.connection.close() catch return error.WriteFailed; } pub fn deinit(t: *Tls, gpa: std.mem.Allocator) void { gpa.free(t.read_buffer); gpa.free(t.write_buffer); t.* = undefined; } test { // The handshake needs a live peer, so tests only force full semantic // analysis here; end-to-end coverage comes from the NixOS interop test. std.testing.refAllDecls(Tls); }