An SMTP client and server library for Zig implementing RFC 5321.
0

Configure Feed

Select the types of activity you want to include in your feed.

zig-smtp / src / Tls.zig
6.4 kB 183 lines
1// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us> 2// SPDX-License-Identifier: MIT 3 4//! Client-side TLS transport layered over an existing stream, wrapping 5//! `std.crypto.tls.Client` with sensible defaults (system trust store, 6//! entropy, and clock wiring). Works for both implicit TLS (connect, then 7//! `init` before any SMTP traffic) and STARTTLS (after `Client.starttls` 8//! succeeds, `init` over the same stream, then `Client.setTransport` and a 9//! fresh `hello`). 10//! 11//! The underlying stream reader and writer must each have a buffer of at 12//! least `min_buffer_len` bytes. 13 14const Tls = @This(); 15 16const std = @import("std"); 17const Io = std.Io; 18 19tls_client: std.crypto.tls.Client, 20read_buffer: []u8, 21write_buffer: []u8, 22/// Pass-through writer handed out by `writer`. `std.crypto.tls.Client`'s own 23/// writer encrypts on flush but leaves the records in the underlying stream 24/// writer's buffer; this wrapper's flush pushes them all the way to the 25/// stream, which line-oriented protocols like SMTP depend on (each command 26/// must reach the server before its reply can arrive). 27writer_state: Io.Writer, 28 29/// Minimum buffer size for the underlying stream reader and writer. 30pub const min_buffer_len = std.crypto.tls.Client.min_buffer_len; 31 32pub const Options = struct { 33 /// Host name the server's certificate must be valid for (also sent via 34 /// SNI). Ignored with `ca = .insecure`. 35 host: []const u8, 36 ca: Ca = .system, 37 38 pub const Ca = union(enum) { 39 /// Verify the server certificate against the system trust store, 40 /// loaded fresh for this connection. 41 system, 42 /// Verify against a caller-managed bundle (reusable across 43 /// connections; see `std.crypto.Certificate.Bundle.rescan`). 44 bundle: struct { 45 lock: *Io.RwLock, 46 bundle: *std.crypto.Certificate.Bundle, 47 }, 48 /// No certificate verification at all. This provides encryption but 49 /// no authentication — fine for tests, unsafe on real networks. 50 insecure, 51 }; 52}; 53 54pub const InitError = std.crypto.tls.Client.InitError || error{ 55 OutOfMemory, 56 CertificateBundleLoadFailure, 57}; 58 59/// Performs the TLS handshake over `input`/`output` (the stream's 60/// reader/writer, each with a buffer of at least `min_buffer_len` bytes). 61/// Allocates the TLS record buffers from `gpa`; free them with `deinit`. 62pub fn init( 63 gpa: std.mem.Allocator, 64 io: Io, 65 input: *Io.Reader, 66 output: *Io.Writer, 67 options: Options, 68) InitError!Tls { 69 const read_buffer = try gpa.alloc(u8, min_buffer_len); 70 errdefer gpa.free(read_buffer); 71 const write_buffer = try gpa.alloc(u8, min_buffer_len); 72 errdefer gpa.free(write_buffer); 73 74 var entropy: [std.crypto.tls.Client.Options.entropy_len]u8 = undefined; 75 io.random(&entropy); 76 const now = Io.Clock.real.now(io); 77 78 const tls_client = switch (options.ca) { 79 .system => system: { 80 var lock: Io.RwLock = .init; 81 var bundle: std.crypto.Certificate.Bundle = .empty; 82 defer bundle.deinit(gpa); 83 bundle.rescan(gpa, io, now) catch return error.CertificateBundleLoadFailure; 84 break :system try std.crypto.tls.Client.init(input, output, .{ 85 .host = .{ .explicit = options.host }, 86 .ca = .{ .bundle = .{ 87 .gpa = gpa, 88 .io = io, 89 .lock = &lock, 90 .bundle = &bundle, 91 } }, 92 .read_buffer = read_buffer, 93 .write_buffer = write_buffer, 94 .entropy = &entropy, 95 .realtime_now = now, 96 }); 97 }, 98 .bundle => |ca| try std.crypto.tls.Client.init(input, output, .{ 99 .host = .{ .explicit = options.host }, 100 .ca = .{ .bundle = .{ 101 .gpa = gpa, 102 .io = io, 103 .lock = ca.lock, 104 .bundle = ca.bundle, 105 } }, 106 .read_buffer = read_buffer, 107 .write_buffer = write_buffer, 108 .entropy = &entropy, 109 .realtime_now = now, 110 }), 111 .insecure => try std.crypto.tls.Client.init(input, output, .{ 112 .host = .no_verification, 113 .ca = .no_verification, 114 .read_buffer = read_buffer, 115 .write_buffer = write_buffer, 116 .entropy = &entropy, 117 .realtime_now = now, 118 }), 119 }; 120 121 return .{ 122 .tls_client = tls_client, 123 .read_buffer = read_buffer, 124 .write_buffer = write_buffer, 125 .writer_state = .{ 126 .vtable = &.{ .drain = drain, .flush = flushThrough }, 127 .buffer = &.{}, 128 }, 129 }; 130} 131 132fn drain(w: *Io.Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize { 133 const t: *Tls = @alignCast(@fieldParentPtr("writer_state", w)); 134 if (data.len == 0) return 0; 135 var n: usize = 0; 136 for (data[0 .. data.len - 1]) |bytes| { 137 try t.tls_client.writer.writeAll(bytes); 138 n += bytes.len; 139 } 140 const pattern = data[data.len - 1]; 141 for (0..splat) |_| { 142 try t.tls_client.writer.writeAll(pattern); 143 n += pattern.len; 144 } 145 return n; 146} 147 148fn flushThrough(w: *Io.Writer) Io.Writer.Error!void { 149 const t: *Tls = @alignCast(@fieldParentPtr("writer_state", w)); 150 try t.tls_client.writer.flush(); 151 try t.tls_client.output.flush(); 152} 153 154/// The decrypted stream from the server. 155pub fn reader(t: *Tls) *Io.Reader { 156 return &t.tls_client.reader; 157} 158 159/// The plaintext stream to the server. Flushing encrypts and pushes the 160/// records through to the underlying stream. 161pub fn writer(t: *Tls) *Io.Writer { 162 return &t.writer_state; 163} 164 165/// Flushes pending data and sends a TLS close_notify alert, letting the 166/// server distinguish a clean shutdown from a truncation attack. Call before 167/// closing the underlying stream. 168pub fn end(t: *Tls) Io.Writer.Error!void { 169 try t.tls_client.end(); 170 try t.tls_client.output.flush(); 171} 172 173pub fn deinit(t: *Tls, gpa: std.mem.Allocator) void { 174 gpa.free(t.read_buffer); 175 gpa.free(t.write_buffer); 176 t.* = undefined; 177} 178 179test { 180 // The handshake needs a live peer, so tests only force full semantic 181 // analysis here; end-to-end coverage comes from the demo CLI. 182 std.testing.refAllDecls(Tls); 183}