authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2022-12-30 20:06:42-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2023-01-02 16:57:16-07:00
log611a1fdd6df81a95a74162a0ebdd5afba94d29d4
tree7ce072a7c78bafe6c795cded942a9a32b3169e40
parentb3c8c383bbba05d9a9d28073e8e8ceba3f089ae8

std.crypto.tls: add API for sending close_notify

This commit adds `writeEnd` and `writeAllEnd` in order to send data and also notify the server that there will be no more data written. Unfortunately, it seems most TLS implementations in the wild get this wrong and immediately close the socket when they see a close_notify, rather than only ending the data stream on the application layer.

3 files changed, 160 insertions(+), 41 deletions(-)

lib/std/crypto/tls.zig+5
...@@ -47,6 +47,11 @@ pub const hello_retry_request_sequence = [32]u8{...@@ -47,6 +47,11 @@ pub const hello_retry_request_sequence = [32]u8{
47 0xC2, 0xA2, 0x11, 0x16, 0x7A, 0xBB, 0x8C, 0x5E, 0x07, 0x9E, 0x09, 0xE2, 0xC8, 0xA8, 0x33, 0x9C,47 0xC2, 0xA2, 0x11, 0x16, 0x7A, 0xBB, 0x8C, 0x5E, 0x07, 0x9E, 0x09, 0xE2, 0xC8, 0xA8, 0x33, 0x9C,
48};48};
4949
50pub const close_notify_alert = [_]u8{
51 @enumToInt(AlertLevel.warning),
52 @enumToInt(AlertDescription.close_notify),
53};
54
50pub const ProtocolVersion = enum(u16) {55pub const ProtocolVersion = enum(u16) {
51 tls_1_2 = 0x0303,56 tls_1_2 = 0x0303,
52 tls_1_3 = 0x0304,57 tls_1_3 = 0x0304,
lib/std/crypto/tls/Client.zig+154-40
...@@ -37,8 +37,54 @@ application_cipher: tls.ApplicationCipher,...@@ -37,8 +37,54 @@ application_cipher: tls.ApplicationCipher,
37/// `partial_ciphertext_end` describe the span of the segments.37/// `partial_ciphertext_end` describe the span of the segments.
38partially_read_buffer: [tls.max_ciphertext_record_len]u8,38partially_read_buffer: [tls.max_ciphertext_record_len]u8,
3939
40/// This is an example of the type that is needed by the read and write
41/// functions. It can have any fields but it must at least have these
42/// functions.
43///
44/// Note that `std.net.Stream` conforms to this interface.
45///
46/// This declaration serves as documentation only.
47pub const StreamInterface = struct {
48 /// Can be any error set.
49 pub const ReadError = error{};
50
51 /// Returns the number of bytes read. The number read may be less than the
52 /// buffer space provided. End-of-stream is indicated by a return value of 0.
53 ///
54 /// The `iovecs` parameter is mutable because so that function may to
55 /// mutate the fields in order to handle partial reads from the underlying
56 /// stream layer.
57 pub fn readv(this: @This(), iovecs: []std.os.iovec) ReadError!usize {
58 _ = .{ this, iovecs };
59 @panic("unimplemented");
60 }
61
62 /// Can be any error set.
63 pub const WriteError = error{};
64
65 /// Returns the number of bytes read, which may be less than the buffer
66 /// space provided. A short read does not indicate end-of-stream.
67 pub fn writev(this: @This(), iovecs: []const std.os.iovec_const) WriteError!usize {
68 _ = .{ this, iovecs };
69 @panic("unimplemented");
70 }
71
72 /// Returns the number of bytes read, which may be less than the buffer
73 /// space provided, indicating end-of-stream.
74 /// The `iovecs` parameter is mutable in case this function needs to mutate
75 /// the fields in order to handle partial writes from the underlying layer.
76 pub fn writevAll(this: @This(), iovecs: []std.os.iovec_const) WriteError!usize {
77 // This can be implemented in terms of writev, or specialized if desired.
78 _ = .{ this, iovecs };
79 @panic("unimplemented");
80 }
81};
82
83/// Initiates a TLS handshake and establishes a TLSv1.3 session with `stream`, which
84/// must conform to `StreamInterface`.
85///
40/// `host` is only borrowed during this function call.86/// `host` is only borrowed during this function call.
41pub fn init(stream: net.Stream, ca_bundle: Certificate.Bundle, host: []const u8) !Client {87pub fn init(stream: anytype, ca_bundle: Certificate.Bundle, host: []const u8) !Client {
42 const host_len = @intCast(u16, host.len);88 const host_len = @intCast(u16, host.len);
4389
44 var random_buffer: [128]u8 = undefined;90 var random_buffer: [128]u8 = undefined;
...@@ -579,31 +625,115 @@ pub fn init(stream: net.Stream, ca_bundle: Certificate.Bundle, host: []const u8)...@@ -579,31 +625,115 @@ pub fn init(stream: net.Stream, ca_bundle: Certificate.Bundle, host: []const u8)
579 }625 }
580}626}
581627
582pub fn write(c: *Client, stream: net.Stream, bytes: []const u8) !usize {628/// Sends TLS-encrypted data to `stream`, which must conform to `StreamInterface`.
629/// Returns the number of plaintext bytes sent, which may be fewer than `bytes.len`.
630pub fn write(c: *Client, stream: anytype, bytes: []const u8) !usize {
631 return writeEnd(c, stream, bytes, false);
632}
633
634/// Sends TLS-encrypted data to `stream`, which must conform to `StreamInterface`.
635pub fn writeAll(c: *Client, stream: anytype, bytes: []const u8) !void {
636 var index: usize = 0;
637 while (index < bytes.len) {
638 index += try c.write(stream, bytes[index..]);
639 }
640}
641
642/// Sends TLS-encrypted data to `stream`, which must conform to `StreamInterface`.
643/// If `end` is true, then this function additionally sends a `close_notify` alert,
644/// which is necessary for the server to distinguish between a properly finished
645/// TLS session, or a truncation attack.
646pub fn writeAllEnd(c: *Client, stream: anytype, bytes: []const u8, end: bool) !void {
647 var index: usize = 0;
648 while (index < bytes.len) {
649 index += try c.writeEnd(stream, bytes[index..], end);
650 }
651}
652
653/// Sends TLS-encrypted data to `stream`, which must conform to `StreamInterface`.
654/// Returns the number of plaintext bytes sent, which may be fewer than `bytes.len`.
655/// If `end` is true, then this function additionally sends a `close_notify` alert,
656/// which is necessary for the server to distinguish between a properly finished
657/// TLS session, or a truncation attack.
658pub fn writeEnd(c: *Client, stream: anytype, bytes: []const u8, end: bool) !usize {
583 var ciphertext_buf: [tls.max_ciphertext_record_len * 4]u8 = undefined;659 var ciphertext_buf: [tls.max_ciphertext_record_len * 4]u8 = undefined;
660 var iovecs_buf: [6]std.os.iovec_const = undefined;
661 var prepared = prepareCiphertextRecord(c, &iovecs_buf, &ciphertext_buf, bytes, .application_data);
662 if (end) {
663 prepared.iovec_end += prepareCiphertextRecord(
664 c,
665 iovecs_buf[prepared.iovec_end..],
666 ciphertext_buf[prepared.ciphertext_end..],
667 &tls.close_notify_alert,
668 .alert,
669 ).iovec_end;
670 }
671
672 const iovec_end = prepared.iovec_end;
673 const overhead_len = prepared.overhead_len;
674
675 // Ideally we would call writev exactly once here, however, we must ensure
676 // that we don't return with a record partially written.
677 var i: usize = 0;
678 var total_amt: usize = 0;
679 while (true) {
680 var amt = try stream.writev(iovecs_buf[i..iovec_end]);
681 while (amt >= iovecs_buf[i].iov_len) {
682 const encrypted_amt = iovecs_buf[i].iov_len;
683 total_amt += encrypted_amt - overhead_len;
684 amt -= encrypted_amt;
685 i += 1;
686 // Rely on the property that iovecs delineate records, meaning that
687 // if amt equals zero here, we have fortunately found ourselves
688 // with a short read that aligns at the record boundary.
689 if (i >= iovec_end) return total_amt;
690 // We also cannot return on a vector boundary if the final close_notify is
691 // not sent; otherwise the caller would not know to retry the call.
692 if (amt == 0 and (!end or i < iovec_end - 1)) return total_amt;
693 }
694 iovecs_buf[i].iov_base += amt;
695 iovecs_buf[i].iov_len -= amt;
696 }
697}
698
699fn prepareCiphertextRecord(
700 c: *Client,
701 iovecs: []std.os.iovec_const,
702 ciphertext_buf: []u8,
703 bytes: []const u8,
704 inner_content_type: tls.ContentType,
705) struct {
706 iovec_end: usize,
707 ciphertext_end: usize,
708 /// How many bytes are taken up by overhead per record.
709 overhead_len: usize,
710} {
584 // Due to the trailing inner content type byte in the ciphertext, we need711 // Due to the trailing inner content type byte in the ciphertext, we need
585 // an additional buffer for storing the cleartext into before encrypting.712 // an additional buffer for storing the cleartext into before encrypting.
586 var cleartext_buf: [max_ciphertext_len]u8 = undefined;713 var cleartext_buf: [max_ciphertext_len]u8 = undefined;
587 var iovecs_buf: [5]std.os.iovec_const = undefined;
588 var ciphertext_end: usize = 0;714 var ciphertext_end: usize = 0;
589 var iovec_end: usize = 0;715 var iovec_end: usize = 0;
590 var bytes_i: usize = 0;716 var bytes_i: usize = 0;
591 // How many bytes are taken up by overhead per record.717 switch (c.application_cipher) {
592 const overhead_len: usize = switch (c.application_cipher) {718 inline else => |*p| {
593 inline else => |*p| l: {
594 const P = @TypeOf(p.*);719 const P = @TypeOf(p.*);
595 const V = @Vector(P.AEAD.nonce_length, u8);720 const V = @Vector(P.AEAD.nonce_length, u8);
596 const overhead_len = tls.record_header_len + P.AEAD.tag_length + 1;721 const overhead_len = tls.record_header_len + P.AEAD.tag_length + 1;
722 const close_notify_alert_reserved = tls.close_notify_alert.len + overhead_len;
597 while (true) {723 while (true) {
598 const encrypted_content_len = @intCast(u16, @min(724 const encrypted_content_len = @intCast(u16, @min(
599 @min(bytes.len - bytes_i, max_ciphertext_len - 1),725 @min(bytes.len - bytes_i, max_ciphertext_len - 1),
600 ciphertext_buf.len -726 ciphertext_buf.len - close_notify_alert_reserved -
601 tls.record_header_len - P.AEAD.tag_length - ciphertext_end - 1,727 overhead_len - ciphertext_end,
602 ));728 ));
603 if (encrypted_content_len == 0) break :l overhead_len;729 if (encrypted_content_len == 0) return .{
730 .iovec_end = iovec_end,
731 .ciphertext_end = ciphertext_end,
732 .overhead_len = overhead_len,
733 };
604734
605 mem.copy(u8, &cleartext_buf, bytes[bytes_i..][0..encrypted_content_len]);735 mem.copy(u8, &cleartext_buf, bytes[bytes_i..][0..encrypted_content_len]);
606 cleartext_buf[encrypted_content_len] = @enumToInt(tls.ContentType.application_data);736 cleartext_buf[encrypted_content_len] = @enumToInt(inner_content_type);
607 bytes_i += encrypted_content_len;737 bytes_i += encrypted_content_len;
608 const ciphertext_len = encrypted_content_len + 1;738 const ciphertext_len = encrypted_content_len + 1;
609 const cleartext = cleartext_buf[0..ciphertext_len];739 const cleartext = cleartext_buf[0..ciphertext_len];
...@@ -626,40 +756,13 @@ pub fn write(c: *Client, stream: net.Stream, bytes: []const u8) !usize {...@@ -626,40 +756,13 @@ pub fn write(c: *Client, stream: net.Stream, bytes: []const u8) !usize {
626 P.AEAD.encrypt(ciphertext, auth_tag, cleartext, ad, nonce, p.client_key);756 P.AEAD.encrypt(ciphertext, auth_tag, cleartext, ad, nonce, p.client_key);
627757
628 const record = ciphertext_buf[record_start..ciphertext_end];758 const record = ciphertext_buf[record_start..ciphertext_end];
629 iovecs_buf[iovec_end] = .{759 iovecs[iovec_end] = .{
630 .iov_base = record.ptr,760 .iov_base = record.ptr,
631 .iov_len = record.len,761 .iov_len = record.len,
632 };762 };
633 iovec_end += 1;763 iovec_end += 1;
634 }764 }
635 },765 },
636 };
637
638 // Ideally we would call writev exactly once here, however, we must ensure
639 // that we don't return with a record partially written.
640 var i: usize = 0;
641 var total_amt: usize = 0;
642 while (true) {
643 var amt = try stream.writev(iovecs_buf[i..iovec_end]);
644 while (amt >= iovecs_buf[i].iov_len) {
645 const encrypted_amt = iovecs_buf[i].iov_len;
646 total_amt += encrypted_amt - overhead_len;
647 amt -= encrypted_amt;
648 i += 1;
649 // Rely on the property that iovecs delineate records, meaning that
650 // if amt equals zero here, we have fortunately found ourselves
651 // with a short read that aligns at the record boundary.
652 if (i >= iovec_end or amt == 0) return total_amt;
653 }
654 iovecs_buf[i].iov_base += amt;
655 iovecs_buf[i].iov_len -= amt;
656 }
657}
658
659pub fn writeAll(c: *Client, stream: net.Stream, bytes: []const u8) !void {
660 var index: usize = 0;
661 while (index < bytes.len) {
662 index += try c.write(stream, bytes[index..]);
663 }766 }
664}767}
665768
...@@ -669,6 +772,7 @@ pub fn eof(c: Client) bool {...@@ -669,6 +772,7 @@ pub fn eof(c: Client) bool {
669 c.partial_ciphertext_idx >= c.partial_ciphertext_end;772 c.partial_ciphertext_idx >= c.partial_ciphertext_end;
670}773}
671774
775/// Receives TLS-encrypted data from `stream`, which must conform to `StreamInterface`.
672/// Returns the number of bytes read, calling the underlying read function the776/// Returns the number of bytes read, calling the underlying read function the
673/// minimal number of times until the buffer has at least `len` bytes filled.777/// minimal number of times until the buffer has at least `len` bytes filled.
674/// If the number read is less than `len` it means the stream reached the end.778/// If the number read is less than `len` it means the stream reached the end.
...@@ -678,10 +782,12 @@ pub fn readAtLeast(c: *Client, stream: anytype, buffer: []u8, len: usize) !usize...@@ -678,10 +782,12 @@ pub fn readAtLeast(c: *Client, stream: anytype, buffer: []u8, len: usize) !usize
678 return readvAtLeast(c, stream, &iovecs, len);782 return readvAtLeast(c, stream, &iovecs, len);
679}783}
680784
785/// Receives TLS-encrypted data from `stream`, which must conform to `StreamInterface`.
681pub fn read(c: *Client, stream: anytype, buffer: []u8) !usize {786pub fn read(c: *Client, stream: anytype, buffer: []u8) !usize {
682 return readAtLeast(c, stream, buffer, 1);787 return readAtLeast(c, stream, buffer, 1);
683}788}
684789
790/// Receives TLS-encrypted data from `stream`, which must conform to `StreamInterface`.
685/// Returns the number of bytes read. If the number read is smaller than791/// Returns the number of bytes read. If the number read is smaller than
686/// `buffer.len`, it means the stream reached the end. Reaching the end of the792/// `buffer.len`, it means the stream reached the end. Reaching the end of the
687/// stream is not an error condition.793/// stream is not an error condition.
...@@ -689,6 +795,7 @@ pub fn readAll(c: *Client, stream: anytype, buffer: []u8) !usize {...@@ -689,6 +795,7 @@ pub fn readAll(c: *Client, stream: anytype, buffer: []u8) !usize {
689 return readAtLeast(c, stream, buffer, buffer.len);795 return readAtLeast(c, stream, buffer, buffer.len);
690}796}
691797
798/// Receives TLS-encrypted data from `stream`, which must conform to `StreamInterface`.
692/// Returns the number of bytes read. If the number read is less than the space799/// Returns the number of bytes read. If the number read is less than the space
693/// provided it means the stream reached the end. Reaching the end of the800/// provided it means the stream reached the end. Reaching the end of the
694/// stream is not an error condition.801/// stream is not an error condition.
...@@ -698,6 +805,7 @@ pub fn readv(c: *Client, stream: anytype, iovecs: []std.os.iovec) !usize {...@@ -698,6 +805,7 @@ pub fn readv(c: *Client, stream: anytype, iovecs: []std.os.iovec) !usize {
698 return readvAtLeast(c, stream, iovecs);805 return readvAtLeast(c, stream, iovecs);
699}806}
700807
808/// Receives TLS-encrypted data from `stream`, which must conform to `StreamInterface`.
701/// Returns the number of bytes read, calling the underlying read function the809/// Returns the number of bytes read, calling the underlying read function the
702/// minimal number of times until the iovecs have at least `len` bytes filled.810/// minimal number of times until the iovecs have at least `len` bytes filled.
703/// If the number read is less than `len` it means the stream reached the end.811/// If the number read is less than `len` it means the stream reached the end.
...@@ -722,6 +830,7 @@ pub fn readvAtLeast(c: *Client, stream: anytype, iovecs: []std.os.iovec, len: us...@@ -722,6 +830,7 @@ pub fn readvAtLeast(c: *Client, stream: anytype, iovecs: []std.os.iovec, len: us
722 }830 }
723}831}
724832
833/// Receives TLS-encrypted data from `stream`, which must conform to `StreamInterface`.
725/// Returns number of bytes that have been read, populated inside `iovecs`. A834/// Returns number of bytes that have been read, populated inside `iovecs`. A
726/// return value of zero bytes does not mean end of stream. Instead, check the `eof()`835/// return value of zero bytes does not mean end of stream. Instead, check the `eof()`
727/// for the end of stream. The `eof()` may be true after any call to836/// for the end of stream. The `eof()` may be true after any call to
...@@ -729,7 +838,7 @@ pub fn readvAtLeast(c: *Client, stream: anytype, iovecs: []std.os.iovec, len: us...@@ -729,7 +838,7 @@ pub fn readvAtLeast(c: *Client, stream: anytype, iovecs: []std.os.iovec, len: us
729/// function asserts that `eof()` is `false`.838/// function asserts that `eof()` is `false`.
730/// See `readv` for a higher level function that has the same, familiar API as839/// See `readv` for a higher level function that has the same, familiar API as
731/// other read functions, such as `std.fs.File.read`.840/// other read functions, such as `std.fs.File.read`.
732pub fn readvAdvanced(c: *Client, stream: net.Stream, iovecs: []const std.os.iovec) !usize {841pub fn readvAdvanced(c: *Client, stream: anytype, iovecs: []const std.os.iovec) !usize {
733 var vp: VecPut = .{ .iovecs = iovecs };842 var vp: VecPut = .{ .iovecs = iovecs };
734843
735 // Give away the buffered cleartext we have, if any.844 // Give away the buffered cleartext we have, if any.
...@@ -905,7 +1014,8 @@ pub fn readvAdvanced(c: *Client, stream: net.Stream, iovecs: []const std.os.iove...@@ -905,7 +1014,8 @@ pub fn readvAdvanced(c: *Client, stream: net.Stream, iovecs: []const std.os.iove
905 break :c cleartext;1014 break :c cleartext;
906 },1015 },
907 };1016 };
908 c.read_seq += 1;1017
1018 c.read_seq = try std.math.add(u64, c.read_seq, 1);
9091019
910 const inner_ct = @intToEnum(tls.ContentType, cleartext[cleartext.len - 1]);1020 const inner_ct = @intToEnum(tls.ContentType, cleartext[cleartext.len - 1]);
911 switch (inner_ct) {1021 switch (inner_ct) {
...@@ -1196,3 +1306,7 @@ const cipher_suites = enum_array(tls.CipherSuite, &.{...@@ -1196,3 +1306,7 @@ const cipher_suites = enum_array(tls.CipherSuite, &.{
1196 .AES_256_GCM_SHA384,1306 .AES_256_GCM_SHA384,
1197 .CHACHA20_POLY1305_SHA256,1307 .CHACHA20_POLY1305_SHA256,
1198});1308});
1309
1310test {
1311 _ = StreamInterface;
1312}
lib/std/http/Client.zig+1-1
...@@ -47,7 +47,7 @@ pub const Request = struct {...@@ -47,7 +47,7 @@ pub const Request = struct {
47 try req.stream.writeAll(req.headers.items);47 try req.stream.writeAll(req.headers.items);
48 },48 },
49 .https => {49 .https => {
50 try req.tls_client.writeAll(req.stream, req.headers.items);50 try req.tls_client.writeAllEnd(req.stream, req.headers.items, true);
51 },51 },
52 }52 }
53 }53 }