| ... | ... | @@ -44,6 +44,8 @@ application_cipher: tls.ApplicationCipher, |
| 44 | 44 | /// The fields `partial_cleartext_idx`, `partial_ciphertext_idx`, and |
| 45 | 45 | /// `partial_ciphertext_end` describe the span of the segments. |
| 46 | 46 | partially_read_buffer: [tls.max_ciphertext_record_len]u8, |
| 47 | /// Encrypted bytes sent to the server here. |
| 48 | output: *std.io.BufferedWriter, |
| 47 | 49 | /// If non-null, ssl secrets are logged to a file. Creating such a log file allows other |
| 48 | 50 | /// programs with access to that file to decrypt all traffic over this connection. |
| 49 | 51 | ssl_key_log: ?struct { |
| ... | ... | @@ -84,24 +86,6 @@ pub const StreamInterface = struct { |
| 84 | 86 | _ = .{ this, iovecs }; |
| 85 | 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 | }; |
| 106 | 90 | |
| 107 | 91 | pub const Options = struct { |
| ... | ... | @@ -129,61 +113,92 @@ pub const Options = struct { |
| 129 | 113 | /// other programs with access to that file to decrypt all traffic over this connection. |
| 130 | 114 | /// TODO `std.crypto` should have no dependencies on `std.fs`. |
| 131 | 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 | }; |
| 133 | 128 | |
| 134 | | pub 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 |
| 130 | fn 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 |
| 138 | fn 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; |
| 179 | 142 | }; |
| 180 | 143 | } |
| 181 | 144 | |
| 182 | | /// Initiates a TLS handshake and establishes a TLSv1.2 or TLSv1.3 session with `stream`, which |
| 145 | const 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 | 197 | /// must conform to `StreamInterface`. |
| 184 | 198 | /// |
| 185 | 199 | /// `host` is only borrowed during this function call. |
| 186 | | pub fn init(stream: anytype, options: Options) InitError(@TypeOf(stream))!Client { |
| 200 | pub fn init(input: anytype, output: *std.io.BufferedWriter, options: Options) InitError!Client { |
| 201 | const diags = options.diagnostics; |
| 187 | 202 | const host = switch (options.host) { |
| 188 | 203 | .no_verification => "", |
| 189 | 204 | .explicit => |host| host, |
| ... | ... | @@ -275,11 +290,8 @@ pub fn init(stream: anytype, options: Options) InitError(@TypeOf(stream))!Client |
| 275 | 290 | }; |
| 276 | 291 | |
| 277 | 292 | { |
| 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])); |
| 283 | 295 | } |
| 284 | 296 | |
| 285 | 297 | var tls_version: tls.ProtocolVersion = undefined; |
| ... | ... | @@ -331,12 +343,12 @@ pub fn init(stream: anytype, options: Options) InitError(@TypeOf(stream))!Client |
| 331 | 343 | var handshake_buffer: [tls.max_ciphertext_record_len]u8 = undefined; |
| 332 | 344 | var d: tls.Decoder = .{ .buf = &handshake_buffer }; |
| 333 | 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 | 347 | const record_header = d.buf[d.idx..][0..tls.record_header_len]; |
| 336 | 348 | const record_ct = d.decode(tls.ContentType); |
| 337 | 349 | d.skip(2); // legacy_version |
| 338 | 350 | const record_len = d.decode(u16); |
| 339 | | try d.readAtLeast(stream, record_len); |
| 351 | try wrapRead(diags, d.readAtLeast(input, record_len)); |
| 340 | 352 | var record_decoder = try d.sub(record_len); |
| 341 | 353 | var ctd, const ct = content: switch (cipher_state) { |
| 342 | 354 | .cleartext => .{ record_decoder, record_ct }, |
| ... | ... | @@ -414,11 +426,8 @@ pub fn init(stream: anytype, options: Options) InitError(@TypeOf(stream))!Client |
| 414 | 426 | const level = ctd.decode(tls.AlertLevel); |
| 415 | 427 | const desc = ctd.decode(tls.AlertDescription); |
| 416 | 428 | _ = 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; |
| 422 | 431 | }, |
| 423 | 432 | .change_cipher_spec => { |
| 424 | 433 | ctd.ensure(1) catch continue :fragment; |
| ... | ... | @@ -754,11 +763,12 @@ pub fn init(stream: anytype, options: Options) InitError(@TypeOf(stream))!Client |
| 754 | 763 | nonce, |
| 755 | 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; |
| 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, |
| 760 | 770 | }; |
| 761 | | try stream.writevAll(&all_msgs_vec); |
| 771 | try wrapWrite(diags, output.writevAll(&all_msgs_vec)); |
| 762 | 772 | }, |
| 763 | 773 | } |
| 764 | 774 | write_seq += 1; |
| ... | ... | @@ -819,11 +829,11 @@ pub fn init(stream: anytype, options: Options) InitError(@TypeOf(stream))!Client |
| 819 | 829 | const nonce = pv.client_handshake_iv; |
| 820 | 830 | P.AEAD.encrypt(ciphertext, auth_tag, &out_cleartext, ad, nonce, pv.client_handshake_key); |
| 821 | 831 | |
| 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, |
| 825 | 835 | }; |
| 826 | | try stream.writevAll(&all_msgs_vec); |
| 836 | try wrapWrite(diags, output.writevAll(&all_msgs_vec)); |
| 827 | 837 | |
| 828 | 838 | const client_secret = hkdfExpandLabel(P.Hkdf, pv.master_secret, "c ap traffic", &handshake_hash, P.Hash.digest_length); |
| 829 | 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 | 883 | .received_close_notify = false, |
| 874 | 884 | .allow_truncation_attacks = false, |
| 875 | 885 | .application_cipher = app_cipher, |
| 886 | .output = output, |
| 876 | 887 | .partially_read_buffer = undefined, |
| 877 | 888 | .ssl_key_log = if (options.ssl_key_log_file) |key_log_file| .{ |
| 878 | 889 | .client_key_seq = key_seq, |
| ... | ... | @@ -896,39 +907,39 @@ pub fn init(stream: anytype, options: Options) InitError(@TypeOf(stream))!Client |
| 896 | 907 | } |
| 897 | 908 | } |
| 898 | 909 | |
| 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`. |
| 901 | | pub fn write(c: *Client, stream: anytype, bytes: []const u8) !usize { |
| 902 | | return writeEnd(c, stream, bytes, false); |
| 910 | pub 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 | }; |
| 903 | 918 | } |
| 904 | 919 | |
| 905 | | /// Sends TLS-encrypted data to `stream`, which must conform to `StreamInterface`. |
| 906 | | pub 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 | | } |
| 920 | fn 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); |
| 911 | 924 | } |
| 912 | 925 | |
| 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. |
| 917 | | pub 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. |
| 929 | pub fn writeAllEnd(c: *Client, bytes: []const u8, end: bool) anyerror!void { |
| 918 | 930 | var index: usize = 0; |
| 919 | 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 | } |
| 923 | 935 | |
| 924 | | /// Sends TLS-encrypted data to `stream`, which must conform to `StreamInterface`. |
| 925 | 936 | /// Returns the number of cleartext bytes sent, which may be fewer than `bytes.len`. |
| 926 | 937 | /// If `end` is true, then this function additionally sends a `close_notify` alert, |
| 927 | 938 | /// which is necessary for the server to distinguish between a properly finished |
| 928 | 939 | /// TLS session, or a truncation attack. |
| 929 | | pub fn writeEnd(c: *Client, stream: anytype, bytes: []const u8, end: bool) !usize { |
| 940 | pub fn writeEnd(c: *Client, bytes: []const u8, end: bool) anyerror!usize { |
| 930 | 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 | 943 | var prepared = prepareCiphertextRecord(c, &iovecs_buf, &ciphertext_buf, bytes, .application_data); |
| 933 | 944 | if (end) { |
| 934 | 945 | prepared.iovec_end += prepareCiphertextRecord( |
| ... | ... | @@ -948,7 +959,7 @@ pub fn writeEnd(c: *Client, stream: anytype, bytes: []const u8, end: bool) !usiz |
| 948 | 959 | var i: usize = 0; |
| 949 | 960 | var total_amt: usize = 0; |
| 950 | 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 | 963 | while (amt >= iovecs_buf[i].len) { |
| 953 | 964 | const encrypted_amt = iovecs_buf[i].len; |
| 954 | 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 | 973 | // not sent; otherwise the caller would not know to retry the call. |
| 963 | 974 | if (amt == 0 and (!end or i < iovec_end - 1)) return total_amt; |
| 964 | 975 | } |
| 965 | | iovecs_buf[i].base += amt; |
| 966 | | iovecs_buf[i].len -= amt; |
| 976 | iovecs_buf[i] = iovecs_buf[i][amt..]; |
| 967 | 977 | } |
| 968 | 978 | } |
| 969 | 979 | |
| 970 | 980 | fn prepareCiphertextRecord( |
| 971 | 981 | c: *Client, |
| 972 | | iovecs: []std.posix.iovec_const, |
| 982 | iovecs: [][]const u8, |
| 973 | 983 | ciphertext_buf: []u8, |
| 974 | 984 | bytes: []const u8, |
| 975 | 985 | inner_content_type: tls.ContentType, |
| ... | ... | @@ -1031,10 +1041,7 @@ fn prepareCiphertextRecord( |
| 1031 | 1041 | c.write_seq += 1; // TODO send key_update on overflow |
| 1032 | 1042 | |
| 1033 | 1043 | 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; |
| 1038 | 1045 | iovec_end += 1; |
| 1039 | 1046 | } |
| 1040 | 1047 | }, |
| ... | ... | @@ -1084,10 +1091,7 @@ fn prepareCiphertextRecord( |
| 1084 | 1091 | c.write_seq += 1; // TODO send key_update on overflow |
| 1085 | 1092 | |
| 1086 | 1093 | 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; |
| 1091 | 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 | 1515 | const locked = if (key_log_file.lock(.exclusive)) |_| true else |_| false; |
| 1512 | 1516 | defer if (locked) key_log_file.unlock(); |
| 1513 | 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 | 1520 | (if (@hasField(@TypeOf(context), "counter")) "_{d}" else "") ++ " {x} {x}\n", .{field.name} ++ |
| 1516 | 1521 | (if (@hasField(@TypeOf(context), "counter")) .{context.counter} else .{}) ++ .{ |
| 1517 | 1522 | context.client_random, |