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,...@@ -44,6 +44,8 @@ application_cipher: tls.ApplicationCipher,
44/// The fields `partial_cleartext_idx`, `partial_ciphertext_idx`, and44/// The fields `partial_cleartext_idx`, `partial_ciphertext_idx`, and
45/// `partial_ciphertext_end` describe the span of the segments.45/// `partial_ciphertext_end` describe the span of the segments.
46partially_read_buffer: [tls.max_ciphertext_record_len]u8,46partially_read_buffer: [tls.max_ciphertext_record_len]u8,
47/// Encrypted bytes sent to the server here.
48output: *std.io.BufferedWriter,
47/// If non-null, ssl secrets are logged to a file. Creating such a log file allows other49/// If non-null, ssl secrets are logged to a file. Creating such a log file allows other
48/// programs with access to that file to decrypt all traffic over this connection.50/// programs with access to that file to decrypt all traffic over this connection.
49ssl_key_log: ?struct {51ssl_key_log: ?struct {
...@@ -84,24 +86,6 @@ pub const StreamInterface = struct {...@@ -84,24 +86,6 @@ pub const StreamInterface = struct {
84 _ = .{ this, iovecs };86 _ = .{ this, iovecs };
85 @panic("unimplemented");87 @panic("unimplemented");
86 }88 }
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 }
105};89};
10690
107pub const Options = struct {91pub const Options = struct {
...@@ -129,61 +113,92 @@ pub const Options = struct {...@@ -129,61 +113,92 @@ pub const Options = struct {
129 /// other programs with access to that file to decrypt all traffic over this connection.113 /// other programs with access to that file to decrypt all traffic over this connection.
130 /// TODO `std.crypto` should have no dependencies on `std.fs`.114 /// TODO `std.crypto` should have no dependencies on `std.fs`.
131 ssl_key_log_file: ?std.fs.File = null,115 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 };
132};127};
133128
134pub fn InitError(comptime Stream: type) type {129/// TODO I wish this could be a method of Diagnostics
135 return std.mem.Allocator.Error || Stream.WriteError || Stream.ReadError || tls.AlertDescription.Error || error{130fn wrapWrite(opt_diags: ?*Options.Diagnostics, returned: anyerror!void) error{WriteFailure}!void {
136 InsufficientEntropy,131 returned catch |err| {
137 DiskQuota,132 if (opt_diags) |diags| diags.* = .{ .err = err };
138 LockViolation,133 return error.WriteFailure;
139 NotOpenForWriting,134 };
140 TlsUnexpectedMessage,135}
141 TlsIllegalParameter,136
142 TlsDecryptFailure,137/// TODO I wish this could be a method of Diagnostics
143 TlsRecordOverflow,138fn wrapRead(opt_diags: ?*Options.Diagnostics, returned: anyerror!void) error{ReadFailure}!void {
144 TlsBadRecordMac,139 returned catch |err| {
145 CertificateFieldHasInvalidLength,140 if (opt_diags) |diags| diags.* = .{ .err = err };
146 CertificateHostMismatch,141 return error.ReadFailure;
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,
179 };142 };
180}143}
181144
182/// Initiates a TLS handshake and establishes a TLSv1.2 or TLSv1.3 session with `stream`, which145const 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
183/// must conform to `StreamInterface`.197/// must conform to `StreamInterface`.
184///198///
185/// `host` is only borrowed during this function call.199/// `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;
187 const host = switch (options.host) {202 const host = switch (options.host) {
188 .no_verification => "",203 .no_verification => "",
189 .explicit => |host| host,204 .explicit => |host| host,
...@@ -275,11 +290,8 @@ pub fn init(stream: anytype, options: Options) InitError(@TypeOf(stream))!Client...@@ -275,11 +290,8 @@ pub fn init(stream: anytype, options: Options) InitError(@TypeOf(stream))!Client
275 };290 };
276291
277 {292 {
278 var iovecs = [_]std.posix.iovec_const{293 var iovecs: [2][]const u8 = .{ cleartext_header, host };
279 .{ .base = cleartext_header.ptr, .len = cleartext_header.len },294 try wrapWrite(diags, output.writevAll(iovecs[0..if (host.len == 0) 1 else 2]));
280 .{ .base = host.ptr, .len = host.len },
281 };
282 try stream.writevAll(iovecs[0..if (host.len == 0) 1 else 2]);
283 }295 }
284296
285 var tls_version: tls.ProtocolVersion = undefined;297 var tls_version: tls.ProtocolVersion = undefined;
...@@ -331,12 +343,12 @@ pub fn init(stream: anytype, options: Options) InitError(@TypeOf(stream))!Client...@@ -331,12 +343,12 @@ pub fn init(stream: anytype, options: Options) InitError(@TypeOf(stream))!Client
331 var handshake_buffer: [tls.max_ciphertext_record_len]u8 = undefined;343 var handshake_buffer: [tls.max_ciphertext_record_len]u8 = undefined;
332 var d: tls.Decoder = .{ .buf = &handshake_buffer };344 var d: tls.Decoder = .{ .buf = &handshake_buffer };
333 fragment: while (true) {345 fragment: while (true) {
334 try d.readAtLeastOurAmt(stream, tls.record_header_len);346 try wrapRead(diags, d.readAtLeastOurAmt(input, tls.record_header_len));
335 const record_header = d.buf[d.idx..][0..tls.record_header_len];347 const record_header = d.buf[d.idx..][0..tls.record_header_len];
336 const record_ct = d.decode(tls.ContentType);348 const record_ct = d.decode(tls.ContentType);
337 d.skip(2); // legacy_version349 d.skip(2); // legacy_version
338 const record_len = d.decode(u16);350 const record_len = d.decode(u16);
339 try d.readAtLeast(stream, record_len);351 try wrapRead(diags, d.readAtLeast(input, record_len));
340 var record_decoder = try d.sub(record_len);352 var record_decoder = try d.sub(record_len);
341 var ctd, const ct = content: switch (cipher_state) {353 var ctd, const ct = content: switch (cipher_state) {
342 .cleartext => .{ record_decoder, record_ct },354 .cleartext => .{ record_decoder, record_ct },
...@@ -414,11 +426,8 @@ pub fn init(stream: anytype, options: Options) InitError(@TypeOf(stream))!Client...@@ -414,11 +426,8 @@ pub fn init(stream: anytype, options: Options) InitError(@TypeOf(stream))!Client
414 const level = ctd.decode(tls.AlertLevel);426 const level = ctd.decode(tls.AlertLevel);
415 const desc = ctd.decode(tls.AlertDescription);427 const desc = ctd.decode(tls.AlertDescription);
416 _ = level;428 _ = level;
417429 if (diags) |x| x.* = .{ .alert = desc };
418 // if this isn't a error alert, then it's a closure alert, which makes no sense in a handshake430 return error.TlsAlert;
419 try desc.toError();
420 // TODO: handle server-side closures
421 return error.TlsUnexpectedMessage;
422 },431 },
423 .change_cipher_spec => {432 .change_cipher_spec => {
424 ctd.ensure(1) catch continue :fragment;433 ctd.ensure(1) catch continue :fragment;
...@@ -754,11 +763,12 @@ pub fn init(stream: anytype, options: Options) InitError(@TypeOf(stream))!Client...@@ -754,11 +763,12 @@ pub fn init(stream: anytype, options: Options) InitError(@TypeOf(stream))!Client
754 nonce,763 nonce,
755 pv.app_cipher.client_write_key,764 pv.app_cipher.client_write_key,
756 );765 );
757 const all_msgs = client_key_exchange_msg ++ client_change_cipher_spec_msg ++ client_verify_msg;766 var all_msgs_vec: [3][]const u8 = .{
758 var all_msgs_vec = [_]std.posix.iovec_const{767 &client_key_exchange_msg,
759 .{ .base = &all_msgs, .len = all_msgs.len },768 &client_change_cipher_spec_msg,
769 &client_verify_msg,
760 };770 };
761 try stream.writevAll(&all_msgs_vec);771 try wrapWrite(diags, output.writevAll(&all_msgs_vec));
762 },772 },
763 }773 }
764 write_seq += 1;774 write_seq += 1;
...@@ -819,11 +829,11 @@ pub fn init(stream: anytype, options: Options) InitError(@TypeOf(stream))!Client...@@ -819,11 +829,11 @@ pub fn init(stream: anytype, options: Options) InitError(@TypeOf(stream))!Client
819 const nonce = pv.client_handshake_iv;829 const nonce = pv.client_handshake_iv;
820 P.AEAD.encrypt(ciphertext, auth_tag, &out_cleartext, ad, nonce, pv.client_handshake_key);830 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;832 var all_msgs_vec: [2][]const u8 = .{
823 var all_msgs_vec = [_]std.posix.iovec_const{833 &client_change_cipher_spec_msg,
824 .{ .base = &all_msgs, .len = all_msgs.len },834 &finished_msg,
825 };835 };
826 try stream.writevAll(&all_msgs_vec);836 try wrapWrite(diags, output.writevAll(&all_msgs_vec));
827837
828 const client_secret = hkdfExpandLabel(P.Hkdf, pv.master_secret, "c ap traffic", &handshake_hash, P.Hash.digest_length);838 const client_secret = hkdfExpandLabel(P.Hkdf, pv.master_secret, "c ap traffic", &handshake_hash, P.Hash.digest_length);
829 const server_secret = hkdfExpandLabel(P.Hkdf, pv.master_secret, "s ap traffic", &handshake_hash, P.Hash.digest_length);839 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...@@ -873,6 +883,7 @@ pub fn init(stream: anytype, options: Options) InitError(@TypeOf(stream))!Client
873 .received_close_notify = false,883 .received_close_notify = false,
874 .allow_truncation_attacks = false,884 .allow_truncation_attacks = false,
875 .application_cipher = app_cipher,885 .application_cipher = app_cipher,
886 .output = output,
876 .partially_read_buffer = undefined,887 .partially_read_buffer = undefined,
877 .ssl_key_log = if (options.ssl_key_log_file) |key_log_file| .{888 .ssl_key_log = if (options.ssl_key_log_file) |key_log_file| .{
878 .client_key_seq = key_seq,889 .client_key_seq = key_seq,
...@@ -896,39 +907,39 @@ pub fn init(stream: anytype, options: Options) InitError(@TypeOf(stream))!Client...@@ -896,39 +907,39 @@ pub fn init(stream: anytype, options: Options) InitError(@TypeOf(stream))!Client
896 }907 }
897}908}
898909
899/// Sends TLS-encrypted data to `stream`, which must conform to `StreamInterface`.910pub fn writer(c: *Client) std.io.Writer {
900/// Returns the number of cleartext bytes sent, which may be fewer than `bytes.len`.911 return .{
901pub fn write(c: *Client, stream: anytype, bytes: []const u8) !usize {912 .context = c,
902 return writeEnd(c, stream, bytes, false);913 .vtable = &.{
914 .writeSplat = writeSplat,
915 .writeFile = std.io.Writer.unimplemented_writeFile,
916 },
917 };
903}918}
904919
905/// Sends TLS-encrypted data to `stream`, which must conform to `StreamInterface`.920fn writeSplat(context: *anyopaque, data: []const []const u8, splat: usize) anyerror!usize {
906pub fn writeAll(c: *Client, stream: anytype, bytes: []const u8) !void {921 const c: *Client = @alignCast(@ptrCast(context));
907 var index: usize = 0;922 assert(data.len > 1 or splat > 0);
908 while (index < bytes.len) {923 return writeEnd(c, data[0], false);
909 index += try c.write(stream, bytes[index..]);
910 }
911}924}
912925
913/// Sends TLS-encrypted data to `stream`, which must conform to `StreamInterface`.926/// If `end` is true, then this function additionally sends a `close_notify`
914/// If `end` is true, then this function additionally sends a `close_notify` alert,927/// alert, which is necessary for the server to distinguish between a properly
915/// which is necessary for the server to distinguish between a properly finished928/// finished TLS session, or a truncation attack.
916/// TLS session, or a truncation attack.929pub fn writeAllEnd(c: *Client, bytes: []const u8, end: bool) anyerror!void {
917pub fn writeAllEnd(c: *Client, stream: anytype, bytes: []const u8, end: bool) !void {
918 var index: usize = 0;930 var index: usize = 0;
919 while (index < bytes.len) {931 while (index < bytes.len) {
920 index += try c.writeEnd(stream, bytes[index..], end);932 index += try c.writeEnd(bytes[index..], end);
921 }933 }
922}934}
923935
924/// Sends TLS-encrypted data to `stream`, which must conform to `StreamInterface`.
925/// Returns the number of cleartext bytes sent, which may be fewer than `bytes.len`.936/// Returns the number of cleartext bytes sent, which may be fewer than `bytes.len`.
926/// If `end` is true, then this function additionally sends a `close_notify` alert,937/// If `end` is true, then this function additionally sends a `close_notify` alert,
927/// which is necessary for the server to distinguish between a properly finished938/// which is necessary for the server to distinguish between a properly finished
928/// TLS session, or a truncation attack.939/// 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 {
930 var ciphertext_buf: [tls.max_ciphertext_record_len * 4]u8 = undefined;941 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;
932 var prepared = prepareCiphertextRecord(c, &iovecs_buf, &ciphertext_buf, bytes, .application_data);943 var prepared = prepareCiphertextRecord(c, &iovecs_buf, &ciphertext_buf, bytes, .application_data);
933 if (end) {944 if (end) {
934 prepared.iovec_end += prepareCiphertextRecord(945 prepared.iovec_end += prepareCiphertextRecord(
...@@ -948,7 +959,7 @@ pub fn writeEnd(c: *Client, stream: anytype, bytes: []const u8, end: bool) !usiz...@@ -948,7 +959,7 @@ pub fn writeEnd(c: *Client, stream: anytype, bytes: []const u8, end: bool) !usiz
948 var i: usize = 0;959 var i: usize = 0;
949 var total_amt: usize = 0;960 var total_amt: usize = 0;
950 while (true) {961 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]);
952 while (amt >= iovecs_buf[i].len) {963 while (amt >= iovecs_buf[i].len) {
953 const encrypted_amt = iovecs_buf[i].len;964 const encrypted_amt = iovecs_buf[i].len;
954 total_amt += encrypted_amt - overhead_len;965 total_amt += encrypted_amt - overhead_len;
...@@ -962,14 +973,13 @@ pub fn writeEnd(c: *Client, stream: anytype, bytes: []const u8, end: bool) !usiz...@@ -962,14 +973,13 @@ pub fn writeEnd(c: *Client, stream: anytype, bytes: []const u8, end: bool) !usiz
962 // not sent; otherwise the caller would not know to retry the call.973 // not sent; otherwise the caller would not know to retry the call.
963 if (amt == 0 and (!end or i < iovec_end - 1)) return total_amt;974 if (amt == 0 and (!end or i < iovec_end - 1)) return total_amt;
964 }975 }
965 iovecs_buf[i].base += amt;976 iovecs_buf[i] = iovecs_buf[i][amt..];
966 iovecs_buf[i].len -= amt;
967 }977 }
968}978}
969979
970fn prepareCiphertextRecord(980fn prepareCiphertextRecord(
971 c: *Client,981 c: *Client,
972 iovecs: []std.posix.iovec_const,982 iovecs: [][]const u8,
973 ciphertext_buf: []u8,983 ciphertext_buf: []u8,
974 bytes: []const u8,984 bytes: []const u8,
975 inner_content_type: tls.ContentType,985 inner_content_type: tls.ContentType,
...@@ -1031,10 +1041,7 @@ fn prepareCiphertextRecord(...@@ -1031,10 +1041,7 @@ fn prepareCiphertextRecord(
1031 c.write_seq += 1; // TODO send key_update on overflow1041 c.write_seq += 1; // TODO send key_update on overflow
10321042
1033 const record = ciphertext_buf[record_start..ciphertext_end];1043 const record = ciphertext_buf[record_start..ciphertext_end];
1034 iovecs[iovec_end] = .{1044 iovecs[iovec_end] = record;
1035 .base = record.ptr,
1036 .len = record.len,
1037 };
1038 iovec_end += 1;1045 iovec_end += 1;
1039 }1046 }
1040 },1047 },
...@@ -1084,10 +1091,7 @@ fn prepareCiphertextRecord(...@@ -1084,10 +1091,7 @@ fn prepareCiphertextRecord(
1084 c.write_seq += 1; // TODO send key_update on overflow1091 c.write_seq += 1; // TODO send key_update on overflow
10851092
1086 const record = ciphertext_buf[record_start..ciphertext_end];1093 const record = ciphertext_buf[record_start..ciphertext_end];
1087 iovecs[iovec_end] = .{1094 iovecs[iovec_end] = record;
1088 .base = record.ptr,
1089 .len = record.len,
1090 };
1091 iovec_end += 1;1095 iovec_end += 1;
1092 }1096 }
1093 },1097 },
...@@ -1511,7 +1515,8 @@ fn logSecrets(key_log_file: std.fs.File, context: anytype, secrets: anytype) voi...@@ -1511,7 +1515,8 @@ fn logSecrets(key_log_file: std.fs.File, context: anytype, secrets: anytype) voi
1511 const locked = if (key_log_file.lock(.exclusive)) |_| true else |_| false;1515 const locked = if (key_log_file.lock(.exclusive)) |_| true else |_| false;
1512 defer if (locked) key_log_file.unlock();1516 defer if (locked) key_log_file.unlock();
1513 key_log_file.seekFromEnd(0) catch {};1517 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}" ++
1515 (if (@hasField(@TypeOf(context), "counter")) "_{d}" else "") ++ " {x} {x}\n", .{field.name} ++1520 (if (@hasField(@TypeOf(context), "counter")) "_{d}" else "") ++ " {x} {x}\n", .{field.name} ++
1516 (if (@hasField(@TypeOf(context), "counter")) .{context.counter} else .{}) ++ .{1521 (if (@hasField(@TypeOf(context), "counter")) .{context.counter} else .{}) ++ .{
1517 context.client_random,1522 context.client_random,