authorgravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2026-04-20 16:57:41+02:00
committergravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2026-04-20 16:57:41+02:00
logc51e26887e839ba39f684a63f13dca4051030700
treea7f0a1ed1a233f3bd977b7b3bfe256fe3a338279
parent259e4cf9fd07c4384e4ced614cc7bf26e2631e46
parentac7e895df0f7e21d0b0dbda1d3e27467d4efe8d3

Merge pull request 'tls.Client: reject empty TLS 1.3 inner plaintext and short records' (#31972) from jedisct1/zig:empty-plaintext-underflows-content-type-parsing into master

Reviewed-on: https://codeberg.org/ziglang/zig/pulls/31972 Reviewed-by: Andrew Kelley <andrew@ziglang.org>

2 files changed, 77 insertions(+), 1 deletions(-)

lib/std/crypto.zig+1
......@@ -367,6 +367,7 @@ test {
367367 _ = ff;
368368 _ = errors;
369369 _ = tls;
370 _ = tls.Client;
370371 _ = Certificate;
371372 _ = codecs;
372373}
lib/std/crypto/tls/Client.zig+76-1
......@@ -382,7 +382,9 @@ pub fn init(input: *Reader, output: *Writer, options: Options) InitError!Client
382382 P.AEAD.decrypt(cleartext, ciphertext, auth_tag, record_header, nonce, pv.server_handshake_key) catch
383383 return error.TlsBadRecordMac;
384384 // TODO use scalar, non-slice version
385 cleartext_fragment_end += mem.trimEnd(u8, cleartext, "\x00").len;
385 const trimmed_len = mem.trimEnd(u8, cleartext, "\x00").len;
386 if (trimmed_len == 0) return error.TlsDecodeError;
387 cleartext_fragment_end += trimmed_len;
386388 },
387389 }
388390 read_seq += 1;
......@@ -1176,6 +1178,7 @@ fn readIndirect(c: *Client) Reader.Error!usize {
11761178 .tls_1_3 => {
11771179 const pv = &p.tls_1_3;
11781180 const P = @TypeOf(p.*);
1181 if (record_len < P.AEAD.tag_length) return failRead(c, error.TlsRecordOverflow);
11791182 const ad = input.take(tls.record_header_len) catch unreachable; // already peeked
11801183 const ciphertext_len = record_len - P.AEAD.tag_length;
11811184 const ciphertext = input.take(ciphertext_len) catch unreachable; // already peeked
......@@ -1192,6 +1195,7 @@ fn readIndirect(c: *Client) Reader.Error!usize {
11921195 return failRead(c, error.TlsBadRecordMac);
11931196 // TODO use scalar, non-slice version
11941197 const msg = mem.trimEnd(u8, cleartext, "\x00");
1198 if (msg.len == 0) return failRead(c, error.TlsDecodeError);
11951199 break :cleartext .{ msg.len - 1, @enumFromInt(msg[msg.len - 1]) };
11961200 },
11971201 .tls_1_2 => {
......@@ -1668,3 +1672,74 @@ else
16681672 .AES_256_GCM_SHA384,
16691673 .ECDHE_RSA_WITH_AES_256_GCM_SHA384,
16701674 });
1675
1676fn testReadError(input_buf: []const u8, cipher: tls.ApplicationCipher) ReadError {
1677 var input_reader: Reader = .fixed(input_buf);
1678 var read_buf: [tls.max_ciphertext_record_len]u8 = undefined;
1679 var c: Client = .{
1680 .input = &input_reader,
1681 .reader = .{
1682 .buffer = &read_buf,
1683 .vtable = &.{ .stream = stream, .readVec = readVec },
1684 .seek = 0,
1685 .end = 0,
1686 },
1687 .output = undefined,
1688 .writer = undefined,
1689 .tls_version = .tls_1_3,
1690 .read_seq = 0,
1691 .write_seq = 0,
1692 .received_close_notify = false,
1693 .allow_truncation_attacks = false,
1694 .application_cipher = cipher,
1695 .ssl_key_log = null,
1696 };
1697 var w: Writer = .failing;
1698 std.testing.expectError(error.ReadFailed, c.reader.stream(&w, .unlimited)) catch
1699 @panic("expected ReadFailed");
1700 return c.read_err.?;
1701}
1702
1703test "empty inner plaintext" {
1704 const AEAD = crypto.aead.chacha_poly.ChaCha20Poly1305;
1705 const key: [AEAD.key_length]u8 = @splat(0);
1706 const iv: [AEAD.nonce_length]u8 = @splat(0);
1707
1708 const plaintext = [1]u8{0x00};
1709 var ciphertext: [plaintext.len]u8 = undefined;
1710 var tag: [AEAD.tag_length]u8 = undefined;
1711 const content_len: u16 = plaintext.len + AEAD.tag_length;
1712 const record_header = [_]u8{ 0x17, 0x03, 0x03 } ++ mem.toBytes(big(content_len));
1713 AEAD.encrypt(&ciphertext, &tag, &plaintext, &record_header, iv, key);
1714
1715 try std.testing.expectEqual(error.TlsDecodeError, testReadError(
1716 &record_header ++ ciphertext ++ tag,
1717 .{ .CHACHA20_POLY1305_SHA256 = .{ .tls_1_3 = .{
1718 .server_key = key,
1719 .server_iv = iv,
1720 .client_secret = undefined,
1721 .server_secret = undefined,
1722 .client_key = undefined,
1723 .client_iv = undefined,
1724 } } },
1725 ));
1726}
1727
1728test "record shorter than tag" {
1729 const AEAD = crypto.aead.chacha_poly.ChaCha20Poly1305;
1730 const record_len: u16 = AEAD.tag_length - 1;
1731 const header = [_]u8{ 0x17, 0x03, 0x03 } ++ mem.toBytes(big(record_len));
1732 const wire = header ++ @as([record_len]u8, @splat(0));
1733
1734 try std.testing.expectEqual(error.TlsRecordOverflow, testReadError(
1735 &wire,
1736 .{ .CHACHA20_POLY1305_SHA256 = .{ .tls_1_3 = .{
1737 .server_key = undefined,
1738 .server_iv = undefined,
1739 .client_secret = undefined,
1740 .server_secret = undefined,
1741 .client_key = undefined,
1742 .client_iv = undefined,
1743 } } },
1744 ));
1745}