From c1e7eb738934399737b3a8452ad9b68bb26805d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Anic=CC=81?= Date: Mon, 8 Jul 2024 23:37:44 +0200 Subject: [PATCH] crypto.Certificate: case insensitive host name check This makes comparing host name with dns name from certificate case insensitive. I found a few domains (from the [cloudflare](https://radar.cloudflare.com/domains) list of top domains) for which tls.Client fails to connect. Error is: ```zig error: TlsInitializationFailed Code/zig/lib/std/crypto/Certificate.zig:336:9: 0x1177b1f in verifyHostName (http_get_std) return error.CertificateHostMismatch; Code/zig/lib/std/crypto/tls23/handshake_client.zig:461:25: 0x11752bd in parseServerCertificate (http_get_std) try subject.verifyHostName(opt.host); ``` In its certificate this domains have host names which are not strictly lower case. This is what checkHostName is comparing: |host_name | dns_name | |------------------------------------------------| |ey.com | EY.COM | |truist.com | Truist.com | |wscampanhas.bradesco | WSCAMPANHAS.BRADESCO | |dell.com | Dell.com | From [RFC2818](https://datatracker.ietf.org/doc/html/rfc2818#section-2.4): > Matching is performed using the matching rules specified by [RFC2459]. From [RFC2459](https://datatracker.ietf.org/doc/html/rfc2459#section-4.2.1.7): > When comparing URIs, conforming implementations > MUST compare the scheme and host without regard to case, but assume > the remainder of the scheme-specific-part is case sensitive. Testing with: ``` const std = @import("std"); pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; const allocator = gpa.allocator(); const args = try std.process.argsAlloc(allocator); defer std.process.argsFree(allocator, args); if (args.len > 1) { const domain = args[1]; var client: std.http.Client = .{ .allocator = allocator }; defer client.deinit(); // Add https:// prefix if needed const url = brk: { const scheme = "https://"; if (domain.len >= scheme.len and std.mem.eql(u8, domain[0..scheme.len], scheme)) break :brk domain; var url_buf: [128]u8 = undefined; break :brk try std.fmt.bufPrint(&url_buf, "https://{s}", .{domain}); }; const uri = try std.Uri.parse(url); var server_header_buffer: [16 * 1024]u8 = undefined; var req = try client.open(.GET, uri, .{ .server_header_buffer = &server_header_buffer }); defer req.deinit(); try req.send(); try req.wait(); } } ``` `$ zig run example/main.zig -- truist.com ` --- lib/std/crypto/Certificate.zig | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/std/crypto/Certificate.zig b/lib/std/crypto/Certificate.zig index 1e3bb9ca0b..9ddd587a19 100644 --- a/lib/std/crypto/Certificate.zig +++ b/lib/std/crypto/Certificate.zig @@ -345,7 +345,7 @@ pub const Parsed = struct { // component or component fragment. E.g., *.a.com matches foo.a.com but // not bar.foo.a.com. f*.com matches foo.com but not bar.com. fn checkHostName(host_name: []const u8, dns_name: []const u8) bool { - if (mem.eql(u8, dns_name, host_name)) { + if (std.ascii.eqlIgnoreCase(dns_name, host_name)) { return true; // exact match } @@ -362,7 +362,7 @@ pub const Parsed = struct { // If not a wildcard and they dont // match then there is no match. - if (mem.eql(u8, dns.?, "*") == false and mem.eql(u8, dns.?, host.?) == false) { + if (mem.eql(u8, dns.?, "*") == false and std.ascii.eqlIgnoreCase(dns.?, host.?) == false) { return false; } }; @@ -381,6 +381,9 @@ test "Parsed.checkHostName" { try expectEqual(false, Parsed.checkHostName("foo.bar.ziglang.org", "*.ziglang.org")); try expectEqual(false, Parsed.checkHostName("ziglang.org", "zig*.org")); try expectEqual(false, Parsed.checkHostName("lang.org", "zig*.org")); + // host name check should be case insensitive + try expectEqual(true, Parsed.checkHostName("ziglang.org", "Ziglang.org")); + try expectEqual(true, Parsed.checkHostName("bar.ziglang.org", "*.Ziglang.ORG")); } pub const ParseError = der.Element.ParseElementError || ParseVersionError || ParseTimeError || ParseEnumError || ParseBitStringError;