| ... | @@ -175,6 +175,8 @@ pub const GeneralNameTag = enum(u5) { | ... | @@ -175,6 +175,8 @@ pub const GeneralNameTag = enum(u5) { |
| 175 | _, | 175 | _, |
| 176 | }; | 176 | }; |
| 177 | | 177 | |
| | 178 | const net = @import("../Io/net.zig"); |
| | 179 | |
| 178 | pub const Parsed = struct { | 180 | pub const Parsed = struct { |
| 179 | certificate: Certificate, | 181 | certificate: Certificate, |
| 180 | issuer_slice: Slice, | 182 | issuer_slice: Slice, |
| ... | @@ -315,6 +317,7 @@ pub const Parsed = struct { | ... | @@ -315,6 +317,7 @@ pub const Parsed = struct { |
| 315 | // what to check. Otherwise, only the common name is checked. | 317 | // what to check. Otherwise, only the common name is checked. |
| 316 | const subject_alt_name = parsed_subject.subjectAltName(); | 318 | const subject_alt_name = parsed_subject.subjectAltName(); |
| 317 | if (subject_alt_name.len == 0) { | 319 | if (subject_alt_name.len == 0) { |
| | 320 | // note: checkIpAddress is intentionally omitted, as it is not permitted in the common name field anyway. |
| 318 | if (checkHostName(host_name, parsed_subject.commonName())) { | 321 | if (checkHostName(host_name, parsed_subject.commonName())) { |
| 319 | return; | 322 | return; |
| 320 | } else { | 323 | } else { |
| ... | @@ -332,6 +335,10 @@ pub const Parsed = struct { | ... | @@ -332,6 +335,10 @@ pub const Parsed = struct { |
| 332 | const dns_name = subject_alt_name[general_name.slice.start..general_name.slice.end]; | 335 | const dns_name = subject_alt_name[general_name.slice.start..general_name.slice.end]; |
| 333 | if (checkHostName(host_name, dns_name)) return; | 336 | if (checkHostName(host_name, dns_name)) return; |
| 334 | }, | 337 | }, |
| | 338 | .iPAddress => { |
| | 339 | const ip_address = subject_alt_name[general_name.slice.start..general_name.slice.end]; |
| | 340 | if (checkIpAddress(host_name, ip_address)) return; |
| | 341 | }, |
| 335 | else => {}, | 342 | else => {}, |
| 336 | } | 343 | } |
| 337 | } | 344 | } |
| ... | @@ -376,6 +383,22 @@ pub const Parsed = struct { | ... | @@ -376,6 +383,22 @@ pub const Parsed = struct { |
| 376 | | 383 | |
| 377 | return false; | 384 | return false; |
| 378 | } | 385 | } |
| | 386 | |
| | 387 | // Check IP address according to RFC 5280 §4.2.1.6. |
| | 388 | fn checkIpAddress(host_name: []const u8, ip_address: []const u8) bool { |
| | 389 | switch (ip_address.len) { |
| | 390 | 4 => { |
| | 391 | // port is irrelevant to SAN matching, so 0 is a harmless placeholder. |
| | 392 | const address = net.Ip4Address.parse(host_name, 0) catch return false; |
| | 393 | return mem.eql(u8, &address.bytes, ip_address); |
| | 394 | }, |
| | 395 | 16 => { |
| | 396 | const address = net.Ip6Address.parse(host_name, 0) catch return false; |
| | 397 | return mem.eql(u8, &address.bytes, ip_address); |
| | 398 | }, |
| | 399 | else => return false, // a malformed certificate, neither 4 nor 16 octets |
| | 400 | } |
| | 401 | } |
| 379 | }; | 402 | }; |
| 380 | | 403 | |
| 381 | test "Parsed.checkHostName RFC 6125 compliance" { | 404 | test "Parsed.checkHostName RFC 6125 compliance" { |
| ... | @@ -417,6 +440,39 @@ test "Parsed.checkHostName RFC 6125 compliance" { | ... | @@ -417,6 +440,39 @@ test "Parsed.checkHostName RFC 6125 compliance" { |
| 417 | try expectEqual(false, Parsed.checkHostName("example.com", "*.")); | 440 | try expectEqual(false, Parsed.checkHostName("example.com", "*.")); |
| 418 | } | 441 | } |
| 419 | | 442 | |
| | 443 | test "Parsed.checkIpAddress RFC 5280 4.2.1.6 compliance" { |
| | 444 | const expectEqual = std.testing.expectEqual; |
| | 445 | |
| | 446 | // Exact match positive tests |
| | 447 | try expectEqual(true, Parsed.checkIpAddress("127.0.0.1", &[4]u8{ 127, 0, 0, 1 })); |
| | 448 | try expectEqual(true, Parsed.checkIpAddress("0:0:0:0:0:0:0:1", &[16]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 })); |
| | 449 | |
| | 450 | // Mismatches should not pass |
| | 451 | try expectEqual(false, Parsed.checkIpAddress("1.2.3.4", &[4]u8{ 5, 6, 7, 8 })); |
| | 452 | try expectEqual(false, Parsed.checkIpAddress("0:0:0:0:0:0:0:1", &[16]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2 })); |
| | 453 | |
| | 454 | // IPv6: the hostname may be in short-form and should match the exact 16 octets specified in the SAN |
| | 455 | try expectEqual(true, Parsed.checkIpAddress("::1", &[16]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 })); |
| | 456 | |
| | 457 | // IPv6: do not match when using DNS64 / NAT64 (i.e. 64:ff9b::/96) |
| | 458 | // the RFC requires exact octet matches, so this is likely surprising and wrong. The decision here is to fail-safe out of an abundance of caution. |
| | 459 | // The test assertions are included not to harden on this behavior, but to show that this use-case was considered. |
| | 460 | // This check may become more lenient in the future if a valid use-case is found. |
| | 461 | try expectEqual(false, Parsed.checkIpAddress("64:ff9b::192.0.2.10", &[4]u8{ 192, 0, 2, 10 })); |
| | 462 | try expectEqual(false, Parsed.checkIpAddress("::ffff:127.0.0.1", &[4]u8{ 127, 0, 0, 1 })); |
| | 463 | |
| | 464 | // Malformed SAN lengths (not 4 or 16 octets) never match. |
| | 465 | try expectEqual(false, Parsed.checkIpAddress("127.0.0", &[_]u8{ 127, 0, 0 })); |
| | 466 | try expectEqual(false, Parsed.checkIpAddress("127.0.0.1.0", &[_]u8{ 127, 0, 0, 1, 0 })); |
| | 467 | |
| | 468 | // A non-parseable host_name never matches. |
| | 469 | try expectEqual(false, Parsed.checkIpAddress("not-an-ip", &[4]u8{ 127, 0, 0, 1 })); |
| | 470 | |
| | 471 | // Edge cases - empty strings |
| | 472 | try expectEqual(false, Parsed.checkIpAddress("", "")); |
| | 473 | try expectEqual(false, Parsed.checkIpAddress("127.0.0.1", "")); |
| | 474 | } |
| | 475 | |
| 420 | pub const ParseError = der.Element.ParseError || ParseVersionError || ParseTimeError || ParseEnumError || ParseBitStringError; | 476 | pub const ParseError = der.Element.ParseError || ParseVersionError || ParseTimeError || ParseEnumError || ParseBitStringError; |
| 421 | | 477 | |
| 422 | pub fn parse(cert: Certificate) ParseError!Parsed { | 478 | pub fn parse(cert: Certificate) ParseError!Parsed { |