| ... | ... | @@ -175,6 +175,8 @@ pub const GeneralNameTag = enum(u5) { |
| 175 | 175 | _, |
| 176 | 176 | }; |
| 177 | 177 | |
| 178 | const net = @import("../Io/net.zig"); |
| 179 | |
| 178 | 180 | pub const Parsed = struct { |
| 179 | 181 | certificate: Certificate, |
| 180 | 182 | issuer_slice: Slice, |
| ... | ... | @@ -315,6 +317,7 @@ pub const Parsed = struct { |
| 315 | 317 | // what to check. Otherwise, only the common name is checked. |
| 316 | 318 | const subject_alt_name = parsed_subject.subjectAltName(); |
| 317 | 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 | 321 | if (checkHostName(host_name, parsed_subject.commonName())) { |
| 319 | 322 | return; |
| 320 | 323 | } else { |
| ... | ... | @@ -332,6 +335,10 @@ pub const Parsed = struct { |
| 332 | 335 | const dns_name = subject_alt_name[general_name.slice.start..general_name.slice.end]; |
| 333 | 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 | 342 | else => {}, |
| 336 | 343 | } |
| 337 | 344 | } |
| ... | ... | @@ -376,6 +383,22 @@ pub const Parsed = struct { |
| 376 | 383 | |
| 377 | 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 | 404 | test "Parsed.checkHostName RFC 6125 compliance" { |
| ... | ... | @@ -417,6 +440,39 @@ test "Parsed.checkHostName RFC 6125 compliance" { |
| 417 | 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 | 476 | pub const ParseError = der.Element.ParseError || ParseVersionError || ParseTimeError || ParseEnumError || ParseBitStringError; |
| 421 | 477 | |
| 422 | 478 | pub fn parse(cert: Certificate) ParseError!Parsed { |