authorgravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2025-08-14 15:57:00+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2025-08-14 13:57:00+00:00
log96e4825fbba714322ad750ee179da3d5f8463bb5
tree56bc3618efb9502667c2872d73bf2939814301f0
parent6bcdcf85c7a26f03926bdb35120901545dc19206
signaturebadge-check Signed by PGP key B5690EEEBB952194

Validate wildcard TLS certificates correctly (#24829)

Validate wildcard certificates as specified in RFC 6125. In particular, `*.example.com` should match `foo.example.com` but NOT `bar.foo.example.com` as it previously did.

1 files changed, 50 insertions(+), 22 deletions(-)

lib/std/crypto/Certificate.zig+50-22
......@@ -344,45 +344,73 @@ pub const Parsed = struct {
344344 // component or component fragment. E.g., *.a.com matches foo.a.com but
345345 // not bar.foo.a.com. f*.com matches foo.com but not bar.com.
346346 fn checkHostName(host_name: []const u8, dns_name: []const u8) bool {
347 // Empty strings should not match
348 if (host_name.len == 0 or dns_name.len == 0) return false;
349
350 // RFC 6125 Section 6.4.1: Exact match (case-insensitive)
347351 if (std.ascii.eqlIgnoreCase(dns_name, host_name)) {
348352 return true; // exact match
349353 }
350354
351 var it_host = std.mem.splitScalar(u8, host_name, '.');
352 var it_dns = std.mem.splitScalar(u8, dns_name, '.');
355 // RFC 6125 Section 6.4.3: Wildcard certificates
356 // Wildcard must be leftmost label and in the form "*.rest.of.domain"
357 if (dns_name.len >= 3 and mem.startsWith(u8, dns_name, "*.")) {
358 const wildcard_suffix = dns_name[2..];
353359
354 const len_match = while (true) {
355 const host = it_host.next();
356 const dns = it_dns.next();
360 // No additional wildcards allowed in the suffix
361 if (mem.indexOf(u8, wildcard_suffix, "*") != null) return false;
357362
358 if (host == null or dns == null) {
359 break host == null and dns == null;
360 }
363 // Find the first dot in hostname to split first label from rest
364 const dot_pos = mem.indexOf(u8, host_name, ".") orelse return false;
361365
362 // If not a wildcard and they dont
363 // match then there is no match.
364 if (mem.eql(u8, dns.?, "*") == false and std.ascii.eqlIgnoreCase(dns.?, host.?) == false) {
365 return false;
366 }
367 };
366 // Wildcard matches exactly one label, so compare the rest
367 const host_suffix = host_name[dot_pos + 1 ..];
368368
369 // If the components are not the same
370 // length then there is no match.
371 return len_match;
369 // Match suffixes (case-insensitive per RFC 6125)
370 return std.ascii.eqlIgnoreCase(wildcard_suffix, host_suffix);
371 }
372
373 return false;
372374 }
373375};
374376
375test "Parsed.checkHostName" {
377test "Parsed.checkHostName RFC 6125 compliance" {
376378 const expectEqual = std.testing.expectEqual;
377379
380 // Exact match tests
378381 try expectEqual(true, Parsed.checkHostName("ziglang.org", "ziglang.org"));
382 try expectEqual(true, Parsed.checkHostName("ziglang.org", "Ziglang.org")); // case insensitive
383 try expectEqual(true, Parsed.checkHostName("ZIGLANG.ORG", "ziglang.org")); // case insensitive
384
385 // Valid wildcard matches
379386 try expectEqual(true, Parsed.checkHostName("bar.ziglang.org", "*.ziglang.org"));
387 try expectEqual(true, Parsed.checkHostName("BAR.ziglang.org", "*.Ziglang.ORG")); // case insensitive
388
389 // RFC 6125: Wildcard matches exactly one label
380390 try expectEqual(false, Parsed.checkHostName("foo.bar.ziglang.org", "*.ziglang.org"));
391 try expectEqual(false, Parsed.checkHostName("ziglang.org", "*.ziglang.org")); // no empty match
392
393 // RFC 6125: No partial wildcards allowed
381394 try expectEqual(false, Parsed.checkHostName("ziglang.org", "zig*.org"));
382 try expectEqual(false, Parsed.checkHostName("lang.org", "zig*.org"));
383 // host name check should be case insensitive
384 try expectEqual(true, Parsed.checkHostName("ziglang.org", "Ziglang.org"));
385 try expectEqual(true, Parsed.checkHostName("bar.ziglang.org", "*.Ziglang.ORG"));
395 try expectEqual(false, Parsed.checkHostName("ziglang.org", "*lang.org"));
396 try expectEqual(false, Parsed.checkHostName("ziglang.org", "zi*ng.org"));
397
398 // RFC 6125: No multiple wildcards
399 try expectEqual(false, Parsed.checkHostName("foo.bar.org", "*.*.org"));
400
401 // RFC 6125: Wildcard must be in leftmost label
402 try expectEqual(false, Parsed.checkHostName("foo.bar.org", "foo.*.org"));
403
404 // Single label hostnames should not match wildcards
405 try expectEqual(false, Parsed.checkHostName("localhost", "*.local"));
406 try expectEqual(false, Parsed.checkHostName("localhost", "*.localhost"));
407
408 // Edge cases
409 try expectEqual(false, Parsed.checkHostName("", ""));
410 try expectEqual(false, Parsed.checkHostName("example.com", ""));
411 try expectEqual(false, Parsed.checkHostName("", "*.example.com"));
412 try expectEqual(false, Parsed.checkHostName("example.com", "*"));
413 try expectEqual(false, Parsed.checkHostName("example.com", "*."));
386414}
387415
388416pub const ParseError = der.Element.ParseError || ParseVersionError || ParseTimeError || ParseEnumError || ParseBitStringError;