authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-02-18 23:33:27-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-07-01 16:35:26-07:00
log6c7b103122f7394f2596830e77c8194a0d33770e
treee3d69b803edb4bb069a04bb07ec8022eff7bc5ce
parentd87b59f5a59bbdc96c617ac90f2e1a3cd39bd7b9

std.crypto.tls.Client: upgrade to `std.io.BufferedWriter`

This is pretty clearly a better API.

1 files changed, 122 insertions(+), 117 deletions(-)

lib/std/crypto/tls/Client.zig+122-117
......@@ -44,6 +44,8 @@ application_cipher: tls.ApplicationCipher,
4444/// The fields `partial_cleartext_idx`, `partial_ciphertext_idx`, and
4545/// `partial_ciphertext_end` describe the span of the segments.
4646partially_read_buffer: [tls.max_ciphertext_record_len]u8,
47/// Encrypted bytes sent to the server here.
48output: *std.io.BufferedWriter,
4749/// If non-null, ssl secrets are logged to a file. Creating such a log file allows other
4850/// programs with access to that file to decrypt all traffic over this connection.
4951ssl_key_log: ?struct {
......@@ -84,24 +86,6 @@ pub const StreamInterface = struct {
8486 _ = .{ this, iovecs };
8587 @panic("unimplemented");
8688 }
87
88 /// Can be any error set.
89 pub const WriteError = error{};
90
91 /// Returns the number of bytes read, which may be less than the buffer
92 /// space provided. A short read does not indicate end-of-stream.
93 pub fn writev(this: @This(), iovecs: []const std.posix.iovec_const) WriteError!usize {
94 _ = .{ this, iovecs };
95 @panic("unimplemented");
96 }
97
98 /// The `iovecs` parameter is mutable in case this function needs to mutate
99 /// the fields in order to handle partial writes from the underlying layer.
100 pub fn writevAll(this: @This(), iovecs: []std.posix.iovec_const) WriteError!void {
101 // This can be implemented in terms of writev, or specialized if desired.
102 _ = .{ this, iovecs };
103 @panic("unimplemented");
104 }
10589};
10690
10791pub const Options = struct {
......@@ -129,61 +113,92 @@ pub const Options = struct {
129113 /// other programs with access to that file to decrypt all traffic over this connection.
130114 /// TODO `std.crypto` should have no dependencies on `std.fs`.
131115 ssl_key_log_file: ?std.fs.File = null,
116 diagnostics: ?*Diagnostics = null,
117
118 pub const Diagnostics = union {
119 /// Populated on `error.WriteFailure` and `error.ReadFailure`.
120 err: anyerror,
121 /// Populated on `error.TlsAlert`.
122 ///
123 /// If this isn't a error alert, then it's a closure alert, which makes
124 /// no sense in a handshake.
125 alert: tls.AlertDescription,
126 };
132127};
133128
134pub fn InitError(comptime Stream: type) type {
135 return std.mem.Allocator.Error || Stream.WriteError || Stream.ReadError || tls.AlertDescription.Error || error{
136 InsufficientEntropy,
137 DiskQuota,
138 LockViolation,
139 NotOpenForWriting,
140 TlsUnexpectedMessage,
141 TlsIllegalParameter,
142 TlsDecryptFailure,
143 TlsRecordOverflow,
144 TlsBadRecordMac,
145 CertificateFieldHasInvalidLength,
146 CertificateHostMismatch,
147 CertificatePublicKeyInvalid,
148 CertificateExpired,
149 CertificateFieldHasWrongDataType,
150 CertificateIssuerMismatch,
151 CertificateNotYetValid,
152 CertificateSignatureAlgorithmMismatch,
153 CertificateSignatureAlgorithmUnsupported,
154 CertificateSignatureInvalid,
155 CertificateSignatureInvalidLength,
156 CertificateSignatureNamedCurveUnsupported,
157 CertificateSignatureUnsupportedBitCount,
158 TlsCertificateNotVerified,
159 TlsBadSignatureScheme,
160 TlsBadRsaSignatureBitCount,
161 InvalidEncoding,
162 IdentityElement,
163 SignatureVerificationFailed,
164 TlsDecryptError,
165 TlsConnectionTruncated,
166 TlsDecodeError,
167 UnsupportedCertificateVersion,
168 CertificateTimeInvalid,
169 CertificateHasUnrecognizedObjectId,
170 CertificateHasInvalidBitString,
171 MessageTooLong,
172 NegativeIntoUnsigned,
173 TargetTooSmall,
174 BufferTooSmall,
175 InvalidSignature,
176 NotSquare,
177 NonCanonical,
178 WeakPublicKey,
129/// TODO I wish this could be a method of Diagnostics
130fn wrapWrite(opt_diags: ?*Options.Diagnostics, returned: anyerror!void) error{WriteFailure}!void {
131 returned catch |err| {
132 if (opt_diags) |diags| diags.* = .{ .err = err };
133 return error.WriteFailure;
134 };
135}
136
137/// TODO I wish this could be a method of Diagnostics
138fn wrapRead(opt_diags: ?*Options.Diagnostics, returned: anyerror!void) error{ReadFailure}!void {
139 returned catch |err| {
140 if (opt_diags) |diags| diags.* = .{ .err = err };
141 return error.ReadFailure;
179142 };
180143}
181144
182/// Initiates a TLS handshake and establishes a TLSv1.2 or TLSv1.3 session with `stream`, which
145const InitError = error{
146 //OutOfMemory,
147 WriteFailure,
148 ReadFailure,
149 InsufficientEntropy,
150 DiskQuota,
151 LockViolation,
152 NotOpenForWriting,
153 /// The alert description will be stored in `Options.Diagnostics.alert`.
154 TlsAlert,
155 TlsUnexpectedMessage,
156 TlsIllegalParameter,
157 TlsDecryptFailure,
158 TlsRecordOverflow,
159 TlsBadRecordMac,
160 CertificateFieldHasInvalidLength,
161 CertificateHostMismatch,
162 CertificatePublicKeyInvalid,
163 CertificateExpired,
164 CertificateFieldHasWrongDataType,
165 CertificateIssuerMismatch,
166 CertificateNotYetValid,
167 CertificateSignatureAlgorithmMismatch,
168 CertificateSignatureAlgorithmUnsupported,
169 CertificateSignatureInvalid,
170 CertificateSignatureInvalidLength,
171 CertificateSignatureNamedCurveUnsupported,
172 CertificateSignatureUnsupportedBitCount,
173 TlsCertificateNotVerified,
174 TlsBadSignatureScheme,
175 TlsBadRsaSignatureBitCount,
176 InvalidEncoding,
177 IdentityElement,
178 SignatureVerificationFailed,
179 TlsDecryptError,
180 TlsConnectionTruncated,
181 TlsDecodeError,
182 UnsupportedCertificateVersion,
183 CertificateTimeInvalid,
184 CertificateHasUnrecognizedObjectId,
185 CertificateHasInvalidBitString,
186 MessageTooLong,
187 NegativeIntoUnsigned,
188 TargetTooSmall,
189 BufferTooSmall,
190 InvalidSignature,
191 NotSquare,
192 NonCanonical,
193 WeakPublicKey,
194};
195
196/// Initiates a TLS handshake and establishes a TLSv1.2 or TLSv1.3 session with `input`, which
183197/// must conform to `StreamInterface`.
184198///
185199/// `host` is only borrowed during this function call.
186pub fn init(stream: anytype, options: Options) InitError(@TypeOf(stream))!Client {
200pub fn init(input: anytype, output: *std.io.BufferedWriter, options: Options) InitError!Client {
201 const diags = options.diagnostics;
187202 const host = switch (options.host) {
188203 .no_verification => "",
189204 .explicit => |host| host,
......@@ -275,11 +290,8 @@ pub fn init(stream: anytype, options: Options) InitError(@TypeOf(stream))!Client
275290 };
276291
277292 {
278 var iovecs = [_]std.posix.iovec_const{
279 .{ .base = cleartext_header.ptr, .len = cleartext_header.len },
280 .{ .base = host.ptr, .len = host.len },
281 };
282 try stream.writevAll(iovecs[0..if (host.len == 0) 1 else 2]);
293 var iovecs: [2][]const u8 = .{ cleartext_header, host };
294 try wrapWrite(diags, output.writevAll(iovecs[0..if (host.len == 0) 1 else 2]));
283295 }
284296
285297 var tls_version: tls.ProtocolVersion = undefined;
......@@ -331,12 +343,12 @@ pub fn init(stream: anytype, options: Options) InitError(@TypeOf(stream))!Client
331343 var handshake_buffer: [tls.max_ciphertext_record_len]u8 = undefined;
332344 var d: tls.Decoder = .{ .buf = &handshake_buffer };
333345 fragment: while (true) {
334 try d.readAtLeastOurAmt(stream, tls.record_header_len);
346 try wrapRead(diags, d.readAtLeastOurAmt(input, tls.record_header_len));
335347 const record_header = d.buf[d.idx..][0..tls.record_header_len];
336348 const record_ct = d.decode(tls.ContentType);
337349 d.skip(2); // legacy_version
338350 const record_len = d.decode(u16);
339 try d.readAtLeast(stream, record_len);
351 try wrapRead(diags, d.readAtLeast(input, record_len));
340352 var record_decoder = try d.sub(record_len);
341353 var ctd, const ct = content: switch (cipher_state) {
342354 .cleartext => .{ record_decoder, record_ct },
......@@ -414,11 +426,8 @@ pub fn init(stream: anytype, options: Options) InitError(@TypeOf(stream))!Client
414426 const level = ctd.decode(tls.AlertLevel);
415427 const desc = ctd.decode(tls.AlertDescription);
416428 _ = level;
417
418 // if this isn't a error alert, then it's a closure alert, which makes no sense in a handshake
419 try desc.toError();
420 // TODO: handle server-side closures
421 return error.TlsUnexpectedMessage;
429 if (diags) |x| x.* = .{ .alert = desc };
430 return error.TlsAlert;
422431 },
423432 .change_cipher_spec => {
424433 ctd.ensure(1) catch continue :fragment;
......@@ -754,11 +763,12 @@ pub fn init(stream: anytype, options: Options) InitError(@TypeOf(stream))!Client
754763 nonce,
755764 pv.app_cipher.client_write_key,
756765 );
757 const all_msgs = client_key_exchange_msg ++ client_change_cipher_spec_msg ++ client_verify_msg;
758 var all_msgs_vec = [_]std.posix.iovec_const{
759 .{ .base = &all_msgs, .len = all_msgs.len },
766 var all_msgs_vec: [3][]const u8 = .{
767 &client_key_exchange_msg,
768 &client_change_cipher_spec_msg,
769 &client_verify_msg,
760770 };
761 try stream.writevAll(&all_msgs_vec);
771 try wrapWrite(diags, output.writevAll(&all_msgs_vec));
762772 },
763773 }
764774 write_seq += 1;
......@@ -819,11 +829,11 @@ pub fn init(stream: anytype, options: Options) InitError(@TypeOf(stream))!Client
819829 const nonce = pv.client_handshake_iv;
820830 P.AEAD.encrypt(ciphertext, auth_tag, &out_cleartext, ad, nonce, pv.client_handshake_key);
821831
822 const all_msgs = client_change_cipher_spec_msg ++ finished_msg;
823 var all_msgs_vec = [_]std.posix.iovec_const{
824 .{ .base = &all_msgs, .len = all_msgs.len },
832 var all_msgs_vec: [2][]const u8 = .{
833 &client_change_cipher_spec_msg,
834 &finished_msg,
825835 };
826 try stream.writevAll(&all_msgs_vec);
836 try wrapWrite(diags, output.writevAll(&all_msgs_vec));
827837
828838 const client_secret = hkdfExpandLabel(P.Hkdf, pv.master_secret, "c ap traffic", &handshake_hash, P.Hash.digest_length);
829839 const server_secret = hkdfExpandLabel(P.Hkdf, pv.master_secret, "s ap traffic", &handshake_hash, P.Hash.digest_length);
......@@ -873,6 +883,7 @@ pub fn init(stream: anytype, options: Options) InitError(@TypeOf(stream))!Client
873883 .received_close_notify = false,
874884 .allow_truncation_attacks = false,
875885 .application_cipher = app_cipher,
886 .output = output,
876887 .partially_read_buffer = undefined,
877888 .ssl_key_log = if (options.ssl_key_log_file) |key_log_file| .{
878889 .client_key_seq = key_seq,
......@@ -896,39 +907,39 @@ pub fn init(stream: anytype, options: Options) InitError(@TypeOf(stream))!Client
896907 }
897908}
898909
899/// Sends TLS-encrypted data to `stream`, which must conform to `StreamInterface`.
900/// Returns the number of cleartext bytes sent, which may be fewer than `bytes.len`.
901pub fn write(c: *Client, stream: anytype, bytes: []const u8) !usize {
902 return writeEnd(c, stream, bytes, false);
910pub fn writer(c: *Client) std.io.Writer {
911 return .{
912 .context = c,
913 .vtable = &.{
914 .writeSplat = writeSplat,
915 .writeFile = std.io.Writer.unimplemented_writeFile,
916 },
917 };
903918}
904919
905/// Sends TLS-encrypted data to `stream`, which must conform to `StreamInterface`.
906pub fn writeAll(c: *Client, stream: anytype, bytes: []const u8) !void {
907 var index: usize = 0;
908 while (index < bytes.len) {
909 index += try c.write(stream, bytes[index..]);
910 }
920fn writeSplat(context: *anyopaque, data: []const []const u8, splat: usize) anyerror!usize {
921 const c: *Client = @alignCast(@ptrCast(context));
922 assert(data.len > 1 or splat > 0);
923 return writeEnd(c, data[0], false);
911924}
912925
913/// Sends TLS-encrypted data to `stream`, which must conform to `StreamInterface`.
914/// If `end` is true, then this function additionally sends a `close_notify` alert,
915/// which is necessary for the server to distinguish between a properly finished
916/// TLS session, or a truncation attack.
917pub fn writeAllEnd(c: *Client, stream: anytype, bytes: []const u8, end: bool) !void {
926/// If `end` is true, then this function additionally sends a `close_notify`
927/// alert, which is necessary for the server to distinguish between a properly
928/// finished TLS session, or a truncation attack.
929pub fn writeAllEnd(c: *Client, bytes: []const u8, end: bool) anyerror!void {
918930 var index: usize = 0;
919931 while (index < bytes.len) {
920 index += try c.writeEnd(stream, bytes[index..], end);
932 index += try c.writeEnd(bytes[index..], end);
921933 }
922934}
923935
924/// Sends TLS-encrypted data to `stream`, which must conform to `StreamInterface`.
925936/// Returns the number of cleartext bytes sent, which may be fewer than `bytes.len`.
926937/// If `end` is true, then this function additionally sends a `close_notify` alert,
927938/// which is necessary for the server to distinguish between a properly finished
928939/// TLS session, or a truncation attack.
929pub fn writeEnd(c: *Client, stream: anytype, bytes: []const u8, end: bool) !usize {
940pub fn writeEnd(c: *Client, bytes: []const u8, end: bool) anyerror!usize {
930941 var ciphertext_buf: [tls.max_ciphertext_record_len * 4]u8 = undefined;
931 var iovecs_buf: [6]std.posix.iovec_const = undefined;
942 var iovecs_buf: [6][]const u8 = undefined;
932943 var prepared = prepareCiphertextRecord(c, &iovecs_buf, &ciphertext_buf, bytes, .application_data);
933944 if (end) {
934945 prepared.iovec_end += prepareCiphertextRecord(
......@@ -948,7 +959,7 @@ pub fn writeEnd(c: *Client, stream: anytype, bytes: []const u8, end: bool) !usiz
948959 var i: usize = 0;
949960 var total_amt: usize = 0;
950961 while (true) {
951 var amt = try stream.writev(iovecs_buf[i..iovec_end]);
962 var amt = try c.output.writev(iovecs_buf[i..iovec_end]);
952963 while (amt >= iovecs_buf[i].len) {
953964 const encrypted_amt = iovecs_buf[i].len;
954965 total_amt += encrypted_amt - overhead_len;
......@@ -962,14 +973,13 @@ pub fn writeEnd(c: *Client, stream: anytype, bytes: []const u8, end: bool) !usiz
962973 // not sent; otherwise the caller would not know to retry the call.
963974 if (amt == 0 and (!end or i < iovec_end - 1)) return total_amt;
964975 }
965 iovecs_buf[i].base += amt;
966 iovecs_buf[i].len -= amt;
976 iovecs_buf[i] = iovecs_buf[i][amt..];
967977 }
968978}
969979
970980fn prepareCiphertextRecord(
971981 c: *Client,
972 iovecs: []std.posix.iovec_const,
982 iovecs: [][]const u8,
973983 ciphertext_buf: []u8,
974984 bytes: []const u8,
975985 inner_content_type: tls.ContentType,
......@@ -1031,10 +1041,7 @@ fn prepareCiphertextRecord(
10311041 c.write_seq += 1; // TODO send key_update on overflow
10321042
10331043 const record = ciphertext_buf[record_start..ciphertext_end];
1034 iovecs[iovec_end] = .{
1035 .base = record.ptr,
1036 .len = record.len,
1037 };
1044 iovecs[iovec_end] = record;
10381045 iovec_end += 1;
10391046 }
10401047 },
......@@ -1084,10 +1091,7 @@ fn prepareCiphertextRecord(
10841091 c.write_seq += 1; // TODO send key_update on overflow
10851092
10861093 const record = ciphertext_buf[record_start..ciphertext_end];
1087 iovecs[iovec_end] = .{
1088 .base = record.ptr,
1089 .len = record.len,
1090 };
1094 iovecs[iovec_end] = record;
10911095 iovec_end += 1;
10921096 }
10931097 },
......@@ -1511,7 +1515,8 @@ fn logSecrets(key_log_file: std.fs.File, context: anytype, secrets: anytype) voi
15111515 const locked = if (key_log_file.lock(.exclusive)) |_| true else |_| false;
15121516 defer if (locked) key_log_file.unlock();
15131517 key_log_file.seekFromEnd(0) catch {};
1514 inline for (@typeInfo(@TypeOf(secrets)).@"struct".fields) |field| key_log_file.writer().print("{s}" ++
1518 var w = key_log_file.writer().unbuffered();
1519 inline for (@typeInfo(@TypeOf(secrets)).@"struct".fields) |field| w.print("{s}" ++
15151520 (if (@hasField(@TypeOf(context), "counter")) "_{d}" else "") ++ " {x} {x}\n", .{field.name} ++
15161521 (if (@hasField(@TypeOf(context), "counter")) .{context.counter} else .{}) ++ .{
15171522 context.client_random,