From c05fff546635a17d46b44f9db846254c259a4ae8 Mon Sep 17 00:00:00 2001 From: Alan Hoffmeister Date: Tue, 17 Mar 2026 17:43:25 -0300 Subject: [PATCH 1/2] fix(tls): bound PSK identity length before decrypt Reject oversized TLS 1.3 PSK ticket identities before copying or decrypting them into fixed-size stack buffers, and add a regression test covering the oversized identity case. Co-authored-by: Codex --- src/quic/tls13.zig | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/quic/tls13.zig b/src/quic/tls13.zig index 33229fe..2e12734 100644 --- a/src/quic/tls13.zig +++ b/src/quic/tls13.zig @@ -1828,6 +1828,10 @@ pub const Tls13Handshake = struct { // Decrypt ticket identity to get PSK if (identity_len < 16 + 1) return; // at least tag + 1 byte const ciphertext_len = identity_len - 16; + const max_ticket_plaintext_len = 64; + const max_ticket_identity_len = max_ticket_plaintext_len + 16; + if (identity_len > max_ticket_identity_len) return; + if (ciphertext_len > max_ticket_plaintext_len) return; // Reconstruct nonce from ticket (we use the last 4 bytes of identity as hint) var ticket_nonce: [12]u8 = .{0} ** 12; @@ -3104,6 +3108,33 @@ test "loopback PSK resumption: two handshakes with session ticket" { ); } +test "tryProcessPsk rejects oversized ticket identity before decrypt" { + const tp = transport_params.TransportParams{ + .initial_max_data = 1048576, + .initial_max_streams_bidi = 100, + }; + + const server_config = TlsConfig{ + .cert_chain_der = &.{}, + .private_key_bytes = &.{}, + .alpn = &[_][]const u8{"h3"}, + .ticket_key = .{0xA5} ** 16, + }; + + var handshake = Tls13Handshake.initServer(server_config, tp); + + var psk_ext: [124]u8 = .{0} ** 124; + std.mem.writeInt(u16, psk_ext[0..2], 87, .big); + std.mem.writeInt(u16, psk_ext[2..4], 81, .big); + std.mem.writeInt(u32, psk_ext[85..89], 0, .big); + std.mem.writeInt(u16, psk_ext[89..91], 33, .big); + psk_ext[91] = 32; + + handshake.tryProcessPsk("", "", 0, &psk_ext, 0, psk_ext.len); + + try std.testing.expect(!handshake.using_psk); +} + // NewSessionTicket roundtrip test test "NewSessionTicket: build and parse roundtrip" { const psk: [32]u8 = .{0x55} ** 32; From 8066dde499461cea2f245bed0759e846befc6449 Mon Sep 17 00:00:00 2001 From: Alan Hoffmeister Date: Tue, 17 Mar 2026 17:48:29 -0300 Subject: [PATCH 2/2] refactor(tls): derive PSK bounds from stack buffers Derive the oversized PSK identity rejection limits from the actual decrypt and ticket buffers used in tryProcessPsk so the guard stays aligned with the implementation. Co-authored-by: Codex --- src/quic/tls13.zig | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/quic/tls13.zig b/src/quic/tls13.zig index 2e12734..e788353 100644 --- a/src/quic/tls13.zig +++ b/src/quic/tls13.zig @@ -1827,23 +1827,21 @@ pub const Tls13Handshake = struct { // Decrypt ticket identity to get PSK if (identity_len < 16 + 1) return; // at least tag + 1 byte + var decrypted: [64]u8 = undefined; + var ct_copy: [decrypted.len + 16]u8 = undefined; + if (identity_len > ct_copy.len) return; const ciphertext_len = identity_len - 16; - const max_ticket_plaintext_len = 64; - const max_ticket_identity_len = max_ticket_plaintext_len + 16; - if (identity_len > max_ticket_identity_len) return; - if (ciphertext_len > max_ticket_plaintext_len) return; + if (ciphertext_len > decrypted.len) return; // Reconstruct nonce from ticket (we use the last 4 bytes of identity as hint) var ticket_nonce: [12]u8 = .{0} ** 12; // Use zeros as nonce — server encrypts with incrementing nonce_buf in [8..12] // We need to try nonce counter values. For simplicity, try a few. - var decrypted: [64]u8 = undefined; var psk_found = false; var found_psk: [32]u8 = undefined; for (0..256) |nonce_try| { std.mem.writeInt(u32, ticket_nonce[8..12], @intCast(nonce_try), .big); - var ct_copy: [80]u8 = undefined; @memcpy(ct_copy[0..identity_len], identity[0..identity_len]); const tag_start = ciphertext_len; const tag: [16]u8 = ct_copy[tag_start..][0..16].*;