From 9c41f1e736b5dc120aaf82073ff22361c60db133 Mon Sep 17 00:00:00 2001 From: Alan Hoffmeister Date: Tue, 17 Mar 2026 17:53:27 -0300 Subject: [PATCH] fix(tls): require a verified leaf key for CertificateVerify Fail closed when certificate verification is enabled but the server Certificate message is empty or the extracted leaf public key does not fit the verification buffer, and add regression tests for the empty-list, oversized-key, and missing-leaf-key paths. Co-authored-by: Codex --- src/quic/tls13.zig | 93 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 87 insertions(+), 6 deletions(-) diff --git a/src/quic/tls13.zig b/src/quic/tls13.zig index 33229fe..db5e78a 100644 --- a/src/quic/tls13.zig +++ b/src/quic/tls13.zig @@ -1115,11 +1115,7 @@ pub const Tls13Handshake = struct { if (cert_index == 0) { // Leaf cert: extract public key for CertificateVerify const pub_key = parsed.pubKey(); - if (pub_key.len <= self.leaf_pub_key_buf.len) { - @memcpy(self.leaf_pub_key_buf[0..pub_key.len], pub_key); - self.leaf_pub_key_len = @intCast(pub_key.len); - self.leaf_pub_key_algo = std.meta.activeTag(parsed.pub_key_algo); - } + try self.storeLeafPublicKey(pub_key, std.meta.activeTag(parsed.pub_key_algo)); // Verify hostname if SNI was set if (self.config.server_name) |server_name| { @@ -1162,6 +1158,8 @@ pub const Tls13Handshake = struct { cert_index += 1; } + if (!self.config.skip_cert_verify and cert_index == 0) return error.BadCertificate; + self.transcript.update(msg); self.state = .client_wait_certificate_verify; return ._continue; @@ -1171,8 +1169,9 @@ pub const Tls13Handshake = struct { const msg = self.readHandshakeMsg() orelse return .wait_for_data; if (msg[0] != @intFromEnum(MsgType.certificate_verify)) return error.UnexpectedMessage; + if (!self.config.skip_cert_verify and self.leaf_pub_key_len == 0) return error.BadCertificateVerify; - if (!self.config.skip_cert_verify and self.leaf_pub_key_len > 0) { + if (!self.config.skip_cert_verify) { // Get transcript hash BEFORE updating with CertificateVerify const transcript_hash = self.transcript.current(); @@ -1206,6 +1205,17 @@ pub const Tls13Handshake = struct { return ._continue; } + fn storeLeafPublicKey( + self: *Tls13Handshake, + pub_key: []const u8, + pub_key_algo: Certificate.AlgorithmCategory, + ) HandshakeError!void { + if (pub_key.len == 0 or pub_key.len > self.leaf_pub_key_buf.len) return error.BadCertificate; + @memcpy(self.leaf_pub_key_buf[0..pub_key.len], pub_key); + self.leaf_pub_key_len = @intCast(pub_key.len); + self.leaf_pub_key_algo = pub_key_algo; + } + fn clientProcessFinished(self: *Tls13Handshake) !Action { const msg = self.readHandshakeMsg() orelse return .wait_for_data; @@ -3104,6 +3114,77 @@ test "loopback PSK resumption: two handshakes with session ticket" { ); } +test "clientProcessCertificate rejects empty certificate list when verification enabled" { + const tp = transport_params.TransportParams{ + .initial_max_data = 1048576, + .initial_max_streams_bidi = 100, + }; + + const client_config = TlsConfig{ + .cert_chain_der = &.{}, + .private_key_bytes = &.{}, + .alpn = &[_][]const u8{"h3"}, + .server_name = "localhost", + .skip_cert_verify = false, + }; + + var handshake = Tls13Handshake.initClient(client_config, tp); + + var buf: [64]u8 = undefined; + const cert_msg = try buildCertificate(&buf, &.{}); + handshake.provideData(cert_msg); + + try std.testing.expectError(error.BadCertificate, handshake.clientProcessCertificate()); +} + +test "storeLeafPublicKey rejects oversized verification keys" { + const tp = transport_params.TransportParams{ + .initial_max_data = 1048576, + .initial_max_streams_bidi = 100, + }; + + const client_config = TlsConfig{ + .cert_chain_der = &.{}, + .private_key_bytes = &.{}, + .alpn = &[_][]const u8{"h3"}, + .server_name = "localhost", + .skip_cert_verify = false, + }; + + var handshake = Tls13Handshake.initClient(client_config, tp); + var oversized: [601]u8 = .{0x42} ** 601; + + try std.testing.expectError( + error.BadCertificate, + handshake.storeLeafPublicKey(&oversized, .X9_62_id_ecPublicKey), + ); +} + +test "clientProcessCertificateVerify rejects missing leaf key when verification enabled" { + const tp = transport_params.TransportParams{ + .initial_max_data = 1048576, + .initial_max_streams_bidi = 100, + }; + + const client_config = TlsConfig{ + .cert_chain_der = &.{}, + .private_key_bytes = &.{}, + .alpn = &[_][]const u8{"h3"}, + .server_name = "localhost", + .skip_cert_verify = false, + }; + + var handshake = Tls13Handshake.initClient(client_config, tp); + handshake.provideData(&[_]u8{ + @intFromEnum(MsgType.certificate_verify), + 0, + 0, + 0, + }); + + try std.testing.expectError(error.BadCertificateVerify, handshake.clientProcessCertificateVerify()); +} + // NewSessionTicket roundtrip test test "NewSessionTicket: build and parse roundtrip" { const psk: [32]u8 = .{0x55} ** 32;