| ... | ... | @@ -33,7 +33,7 @@ received_close_notify: bool, |
| 33 | 33 | /// This makes the application vulnerable to truncation attacks unless the |
| 34 | 34 | /// application layer itself verifies that the amount of data received equals |
| 35 | 35 | /// the amount of data expected, such as HTTP with the Content-Length header. |
| 36 | | allow_truncation_attacks: bool = false, |
| 36 | allow_truncation_attacks: bool, |
| 37 | 37 | application_cipher: tls.ApplicationCipher, |
| 38 | 38 | /// The size is enough to contain exactly one TLSCiphertext record. |
| 39 | 39 | /// This buffer is segmented into four parts: |
| ... | ... | @@ -44,6 +44,24 @@ 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 | /// 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. |
| 49 | ssl_key_log: ?struct { |
| 50 | client_key_seq: u64, |
| 51 | server_key_seq: u64, |
| 52 | client_random: [32]u8, |
| 53 | file: std.fs.File, |
| 54 | |
| 55 | fn clientCounter(key_log: *@This()) u64 { |
| 56 | defer key_log.client_key_seq += 1; |
| 57 | return key_log.client_key_seq; |
| 58 | } |
| 59 | |
| 60 | fn serverCounter(key_log: *@This()) u64 { |
| 61 | defer key_log.server_key_seq += 1; |
| 62 | return key_log.server_key_seq; |
| 63 | } |
| 64 | }, |
| 47 | 65 | |
| 48 | 66 | /// This is an example of the type that is needed by the read and write |
| 49 | 67 | /// functions. It can have any fields but it must at least have these |
| ... | ... | @@ -88,6 +106,32 @@ pub const StreamInterface = struct { |
| 88 | 106 | } |
| 89 | 107 | }; |
| 90 | 108 | |
| 109 | pub const Options = struct { |
| 110 | /// How to perform host verification of server certificates. |
| 111 | host: union(enum) { |
| 112 | /// No host verification is performed, which prevents a trusted connection from |
| 113 | /// being established. |
| 114 | no_verification, |
| 115 | /// Verify that the server certificate was issues for a given host. |
| 116 | explicit: []const u8, |
| 117 | }, |
| 118 | /// How to verify the authenticity of server certificates. |
| 119 | ca: union(enum) { |
| 120 | /// No ca verification is performed, which prevents a trusted connection from |
| 121 | /// being established. |
| 122 | no_verification, |
| 123 | /// Verify that the server certificate is a valid self-signed certificate. |
| 124 | /// This provides no authorization guarantees, as anyone can create a |
| 125 | /// self-signed certificate. |
| 126 | self_signed, |
| 127 | /// Verify that the server certificate is authorized by a given ca bundle. |
| 128 | bundle: Certificate.Bundle, |
| 129 | }, |
| 130 | /// If non-null, ssl secrets are logged to this file. Creating such a log file allows |
| 131 | /// other programs with access to that file to decrypt all traffic over this connection. |
| 132 | ssl_key_log_file: ?std.fs.File = null, |
| 133 | }; |
| 134 | |
| 91 | 135 | pub fn InitError(comptime Stream: type) type { |
| 92 | 136 | return std.mem.Allocator.Error || Stream.WriteError || Stream.ReadError || tls.AlertDescription.Error || error{ |
| 93 | 137 | InsufficientEntropy, |
| ... | ... | @@ -140,12 +184,17 @@ pub fn InitError(comptime Stream: type) type { |
| 140 | 184 | /// must conform to `StreamInterface`. |
| 141 | 185 | /// |
| 142 | 186 | /// `host` is only borrowed during this function call. |
| 143 | | pub fn init(stream: anytype, ca_bundle: Certificate.Bundle, host: []const u8) InitError(@TypeOf(stream))!Client { |
| 187 | pub fn init(stream: anytype, options: Options) InitError(@TypeOf(stream))!Client { |
| 188 | const host = switch (options.host) { |
| 189 | .no_verification => "", |
| 190 | .explicit => |host| host, |
| 191 | }; |
| 144 | 192 | const host_len: u16 = @intCast(host.len); |
| 145 | 193 | |
| 146 | 194 | var random_buffer: [128]u8 = undefined; |
| 147 | 195 | crypto.random.bytes(&random_buffer); |
| 148 | 196 | const client_hello_rand = random_buffer[0..32].*; |
| 197 | var key_seq: u64 = 0; |
| 149 | 198 | var server_hello_rand: [32]u8 = undefined; |
| 150 | 199 | const legacy_session_id = random_buffer[32..64].*; |
| 151 | 200 | |
| ... | ... | @@ -179,15 +228,21 @@ pub fn init(stream: anytype, ca_bundle: Certificate.Bundle, host: []const u8) In |
| 179 | 228 | array(u16, u8, key_share.secp256r1_kp.public_key.toUncompressedSec1()) ++ |
| 180 | 229 | int(u16, @intFromEnum(tls.NamedGroup.x25519)) ++ |
| 181 | 230 | array(u16, u8, key_share.x25519_kp.public_key), |
| 182 | | )) ++ int(u16, @intFromEnum(tls.ExtensionType.server_name)) ++ |
| 231 | )); |
| 232 | const server_name_extension = int(u16, @intFromEnum(tls.ExtensionType.server_name)) ++ |
| 183 | 233 | int(u16, 2 + 1 + 2 + host_len) ++ // byte length of this extension payload |
| 184 | 234 | int(u16, 1 + 2 + host_len) ++ // server_name_list byte count |
| 185 | 235 | .{0x00} ++ // name_type |
| 186 | 236 | int(u16, host_len); |
| 237 | const server_name_extension_len = switch (options.host) { |
| 238 | .no_verification => 0, |
| 239 | .explicit => server_name_extension.len + host_len, |
| 240 | }; |
| 187 | 241 | |
| 188 | 242 | const extensions_header = |
| 189 | | int(u16, @intCast(extensions_payload.len + host_len)) ++ |
| 190 | | extensions_payload; |
| 243 | int(u16, @intCast(extensions_payload.len + server_name_extension_len)) ++ |
| 244 | extensions_payload ++ |
| 245 | server_name_extension; |
| 191 | 246 | |
| 192 | 247 | const client_hello = |
| 193 | 248 | int(u16, @intFromEnum(tls.ProtocolVersion.tls_1_2)) ++ |
| ... | ... | @@ -198,20 +253,24 @@ pub fn init(stream: anytype, ca_bundle: Certificate.Bundle, host: []const u8) In |
| 198 | 253 | extensions_header; |
| 199 | 254 | |
| 200 | 255 | const out_handshake = .{@intFromEnum(tls.HandshakeType.client_hello)} ++ |
| 201 | | int(u24, @intCast(client_hello.len + host_len)) ++ |
| 256 | int(u24, @intCast(client_hello.len - server_name_extension.len + server_name_extension_len)) ++ |
| 202 | 257 | client_hello; |
| 203 | 258 | |
| 204 | | const cleartext_header = .{@intFromEnum(tls.ContentType.handshake)} ++ |
| 259 | const cleartext_header_buf = .{@intFromEnum(tls.ContentType.handshake)} ++ |
| 205 | 260 | int(u16, @intFromEnum(tls.ProtocolVersion.tls_1_0)) ++ |
| 206 | | int(u16, @intCast(out_handshake.len + host_len)) ++ |
| 261 | int(u16, @intCast(out_handshake.len - server_name_extension.len + server_name_extension_len)) ++ |
| 207 | 262 | out_handshake; |
| 263 | const cleartext_header = switch (options.host) { |
| 264 | .no_verification => cleartext_header_buf[0 .. cleartext_header_buf.len - server_name_extension.len], |
| 265 | .explicit => &cleartext_header_buf, |
| 266 | }; |
| 208 | 267 | |
| 209 | 268 | { |
| 210 | 269 | var iovecs = [_]std.posix.iovec_const{ |
| 211 | | .{ .base = &cleartext_header, .len = cleartext_header.len }, |
| 270 | .{ .base = cleartext_header.ptr, .len = cleartext_header.len }, |
| 212 | 271 | .{ .base = host.ptr, .len = host.len }, |
| 213 | 272 | }; |
| 214 | | try stream.writevAll(&iovecs); |
| 273 | try stream.writevAll(iovecs[0..if (host.len == 0) 1 else 2]); |
| 215 | 274 | } |
| 216 | 275 | |
| 217 | 276 | var tls_version: tls.ProtocolVersion = undefined; |
| ... | ... | @@ -472,6 +531,12 @@ pub fn init(stream: anytype, ca_bundle: Certificate.Bundle, host: []const u8) In |
| 472 | 531 | pv.master_secret = P.Hkdf.extract(&ap_derived_secret, &zeroes); |
| 473 | 532 | const client_secret = hkdfExpandLabel(P.Hkdf, pv.handshake_secret, "c hs traffic", &hello_hash, P.Hash.digest_length); |
| 474 | 533 | const server_secret = hkdfExpandLabel(P.Hkdf, pv.handshake_secret, "s hs traffic", &hello_hash, P.Hash.digest_length); |
| 534 | if (options.ssl_key_log_file) |key_log_file| logSecrets(key_log_file, .{ |
| 535 | .client_random = &client_hello_rand, |
| 536 | }, .{ |
| 537 | .SERVER_HANDSHAKE_TRAFFIC_SECRET = &server_secret, |
| 538 | .CLIENT_HANDSHAKE_TRAFFIC_SECRET = &client_secret, |
| 539 | }); |
| 475 | 540 | pv.client_finished_key = hkdfExpandLabel(P.Hkdf, client_secret, "finished", "", P.Hmac.key_length); |
| 476 | 541 | pv.server_finished_key = hkdfExpandLabel(P.Hkdf, server_secret, "finished", "", P.Hmac.key_length); |
| 477 | 542 | pv.client_handshake_key = hkdfExpandLabel(P.Hkdf, client_secret, "key", "", P.AEAD.key_length); |
| ... | ... | @@ -544,6 +609,13 @@ pub fn init(stream: anytype, ca_bundle: Certificate.Bundle, host: []const u8) In |
| 544 | 609 | const cert_size = certs_decoder.decode(u24); |
| 545 | 610 | const certd = try certs_decoder.sub(cert_size); |
| 546 | 611 | |
| 612 | if (tls_version == .tls_1_3) { |
| 613 | try certs_decoder.ensure(2); |
| 614 | const total_ext_size = certs_decoder.decode(u16); |
| 615 | const all_extd = try certs_decoder.sub(total_ext_size); |
| 616 | _ = all_extd; |
| 617 | } |
| 618 | |
| 547 | 619 | const subject_cert: Certificate = .{ |
| 548 | 620 | .buffer = certd.buf, |
| 549 | 621 | .index = @intCast(certd.idx), |
| ... | ... | @@ -551,7 +623,10 @@ pub fn init(stream: anytype, ca_bundle: Certificate.Bundle, host: []const u8) In |
| 551 | 623 | const subject = try subject_cert.parse(); |
| 552 | 624 | if (cert_index == 0) { |
| 553 | 625 | // Verify the host on the first certificate. |
| 554 | | try subject.verifyHostName(host); |
| 626 | switch (options.host) { |
| 627 | .no_verification => {}, |
| 628 | .explicit => try subject.verifyHostName(host), |
| 629 | } |
| 555 | 630 | |
| 556 | 631 | // Keep track of the public key for the |
| 557 | 632 | // certificate_verify message later. |
| ... | ... | @@ -560,23 +635,27 @@ pub fn init(stream: anytype, ca_bundle: Certificate.Bundle, host: []const u8) In |
| 560 | 635 | try prev_cert.verify(subject, now_sec); |
| 561 | 636 | } |
| 562 | 637 | |
| 563 | | if (ca_bundle.verify(subject, now_sec)) |_| { |
| 564 | | handshake_state = .trust_chain_established; |
| 565 | | break :cert; |
| 566 | | } else |err| switch (err) { |
| 567 | | error.CertificateIssuerNotFound => {}, |
| 568 | | else => |e| return e, |
| 638 | switch (options.ca) { |
| 639 | .no_verification => { |
| 640 | handshake_state = .trust_chain_established; |
| 641 | break :cert; |
| 642 | }, |
| 643 | .self_signed => { |
| 644 | try subject.verify(subject, now_sec); |
| 645 | handshake_state = .trust_chain_established; |
| 646 | break :cert; |
| 647 | }, |
| 648 | .bundle => |ca_bundle| if (ca_bundle.verify(subject, now_sec)) |_| { |
| 649 | handshake_state = .trust_chain_established; |
| 650 | break :cert; |
| 651 | } else |err| switch (err) { |
| 652 | error.CertificateIssuerNotFound => {}, |
| 653 | else => |e| return e, |
| 654 | }, |
| 569 | 655 | } |
| 570 | 656 | |
| 571 | 657 | prev_cert = subject; |
| 572 | 658 | cert_index += 1; |
| 573 | | |
| 574 | | if (tls_version == .tls_1_3) { |
| 575 | | try certs_decoder.ensure(2); |
| 576 | | const total_ext_size = certs_decoder.decode(u16); |
| 577 | | const all_extd = try certs_decoder.sub(total_ext_size); |
| 578 | | _ = all_extd; |
| 579 | | } |
| 580 | 659 | } |
| 581 | 660 | }, |
| 582 | 661 | .server_key_exchange => { |
| ... | ... | @@ -625,6 +704,11 @@ pub fn init(stream: anytype, ca_bundle: Certificate.Bundle, host: []const u8) In |
| 625 | 704 | &client_hello_rand, |
| 626 | 705 | &server_hello_rand, |
| 627 | 706 | }, 48); |
| 707 | if (options.ssl_key_log_file) |key_log_file| logSecrets(key_log_file, .{ |
| 708 | .client_random = &client_hello_rand, |
| 709 | }, .{ |
| 710 | .CLIENT_RANDOM = &master_secret, |
| 711 | }); |
| 628 | 712 | const key_block = hmacExpandLabel( |
| 629 | 713 | P.Hmac, |
| 630 | 714 | &master_secret, |
| ... | ... | @@ -748,6 +832,14 @@ pub fn init(stream: anytype, ca_bundle: Certificate.Bundle, host: []const u8) In |
| 748 | 832 | |
| 749 | 833 | const client_secret = hkdfExpandLabel(P.Hkdf, pv.master_secret, "c ap traffic", &handshake_hash, P.Hash.digest_length); |
| 750 | 834 | const server_secret = hkdfExpandLabel(P.Hkdf, pv.master_secret, "s ap traffic", &handshake_hash, P.Hash.digest_length); |
| 835 | if (options.ssl_key_log_file) |key_log_file| logSecrets(key_log_file, .{ |
| 836 | .counter = key_seq, |
| 837 | .client_random = &client_hello_rand, |
| 838 | }, .{ |
| 839 | .SERVER_TRAFFIC_SECRET = &server_secret, |
| 840 | .CLIENT_TRAFFIC_SECRET = &client_secret, |
| 841 | }); |
| 842 | key_seq += 1; |
| 751 | 843 | break :app_cipher @unionInit(tls.ApplicationCipher, @tagName(tag), .{ .tls_1_3 = .{ |
| 752 | 844 | .client_secret = client_secret, |
| 753 | 845 | .server_secret = server_secret, |
| ... | ... | @@ -784,8 +876,15 @@ pub fn init(stream: anytype, ca_bundle: Certificate.Bundle, host: []const u8) In |
| 784 | 876 | .partial_ciphertext_idx = 0, |
| 785 | 877 | .partial_ciphertext_end = @intCast(leftover.len), |
| 786 | 878 | .received_close_notify = false, |
| 879 | .allow_truncation_attacks = false, |
| 787 | 880 | .application_cipher = app_cipher, |
| 788 | 881 | .partially_read_buffer = undefined, |
| 882 | .ssl_key_log = if (options.ssl_key_log_file) |key_log_file| .{ |
| 883 | .client_key_seq = key_seq, |
| 884 | .server_key_seq = key_seq, |
| 885 | .client_random = client_hello_rand, |
| 886 | .file = key_log_file, |
| 887 | } else null, |
| 789 | 888 | }; |
| 790 | 889 | @memcpy(client.partially_read_buffer[0..leftover.len], leftover); |
| 791 | 890 | return client; |
| ... | ... | @@ -1358,6 +1457,12 @@ pub fn readvAdvanced(c: *Client, stream: anytype, iovecs: []const std.posix.iove |
| 1358 | 1457 | const pv = &p.tls_1_3; |
| 1359 | 1458 | const P = @TypeOf(p.*); |
| 1360 | 1459 | const server_secret = hkdfExpandLabel(P.Hkdf, pv.server_secret, "traffic upd", "", P.Hash.digest_length); |
| 1460 | if (c.ssl_key_log) |*key_log| logSecrets(key_log.file, .{ |
| 1461 | .counter = key_log.serverCounter(), |
| 1462 | .client_random = &key_log.client_random, |
| 1463 | }, .{ |
| 1464 | .SERVER_TRAFFIC_SECRET = &server_secret, |
| 1465 | }); |
| 1361 | 1466 | pv.server_secret = server_secret; |
| 1362 | 1467 | pv.server_key = hkdfExpandLabel(P.Hkdf, server_secret, "key", "", P.AEAD.key_length); |
| 1363 | 1468 | pv.server_iv = hkdfExpandLabel(P.Hkdf, server_secret, "iv", "", P.AEAD.nonce_length); |
| ... | ... | @@ -1372,6 +1477,12 @@ pub fn readvAdvanced(c: *Client, stream: anytype, iovecs: []const std.posix.iove |
| 1372 | 1477 | const pv = &p.tls_1_3; |
| 1373 | 1478 | const P = @TypeOf(p.*); |
| 1374 | 1479 | const client_secret = hkdfExpandLabel(P.Hkdf, pv.client_secret, "traffic upd", "", P.Hash.digest_length); |
| 1480 | if (c.ssl_key_log) |*key_log| logSecrets(key_log.file, .{ |
| 1481 | .counter = key_log.clientCounter(), |
| 1482 | .client_random = &key_log.client_random, |
| 1483 | }, .{ |
| 1484 | .CLIENT_TRAFFIC_SECRET = &client_secret, |
| 1485 | }); |
| 1375 | 1486 | pv.client_secret = client_secret; |
| 1376 | 1487 | pv.client_key = hkdfExpandLabel(P.Hkdf, client_secret, "key", "", P.AEAD.key_length); |
| 1377 | 1488 | pv.client_iv = hkdfExpandLabel(P.Hkdf, client_secret, "iv", "", P.AEAD.nonce_length); |
| ... | ... | @@ -1426,6 +1537,18 @@ pub fn readvAdvanced(c: *Client, stream: anytype, iovecs: []const std.posix.iove |
| 1426 | 1537 | } |
| 1427 | 1538 | } |
| 1428 | 1539 | |
| 1540 | fn logSecrets(key_log_file: std.fs.File, context: anytype, secrets: anytype) void { |
| 1541 | const locked = if (key_log_file.lock(.exclusive)) |_| true else |_| false; |
| 1542 | defer if (locked) key_log_file.unlock(); |
| 1543 | key_log_file.seekFromEnd(0) catch {}; |
| 1544 | inline for (@typeInfo(@TypeOf(secrets)).@"struct".fields) |field| key_log_file.writer().print("{s}" ++ |
| 1545 | (if (@hasField(@TypeOf(context), "counter")) "_{d}" else "") ++ " {} {}\n", .{field.name} ++ |
| 1546 | (if (@hasField(@TypeOf(context), "counter")) .{context.counter} else .{}) ++ .{ |
| 1547 | std.fmt.fmtSliceHexLower(context.client_random), |
| 1548 | std.fmt.fmtSliceHexLower(@field(secrets, field.name)), |
| 1549 | }) catch {}; |
| 1550 | } |
| 1551 | |
| 1429 | 1552 | fn finishRead(c: *Client, frag: []const u8, in: usize, out: usize) usize { |
| 1430 | 1553 | const saved_buf = frag[in..]; |
| 1431 | 1554 | if (c.partial_ciphertext_idx > c.partial_cleartext_idx) { |