crypto/x509: use netip.ParseAddr in VerifyHostname for IPv6 zones net.ParseIP returns nil for zone-scoped IPv6 addresses (e.g. fe80::1%eth0), causing VerifyHostname to fall through to DNS name matching instead of IP matching. Switch to netip.ParseAddr which handles zone IDs correctly; AsSlice() returns the zone-free byte representation for comparison against certificate SANs. Fixes #79719 Change-Id: I47fa43838273d225b8e70eb6c68d5d5466b75e5f Reviewed-on: https://go-review.googlesource.com/c/go/+/785280 Reviewed-by: Sravani Bhamidipaty <sravanibhamidipaty@gmail.com> Reviewed-by: Roland Shoemaker <roland@golang.org> Auto-Submit: Daniel McCarney <daniel@binaryparadox.net> Reviewed-by: Daniel McCarney <daniel@binaryparadox.net> LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Mark Freeman <markfreeman@google.com>
diff --git a/src/crypto/x509/verify.go b/src/crypto/x509/verify.go index f97393f..cba05a1 100644 --- a/src/crypto/x509/verify.go +++ b/src/crypto/x509/verify.go
@@ -13,6 +13,7 @@ "iter" "maps" "net" + "net/netip" "runtime" "slices" "strings" @@ -944,15 +945,17 @@ if len(h) >= 3 && h[0] == '[' && h[len(h)-1] == ']' { candidateIP = h[1 : len(h)-1] } - if ip := net.ParseIP(candidateIP); ip != nil { + // We use netip.ParseAddr() to allow IPv6 scoped addresses. + if addr, err := netip.ParseAddr(candidateIP); err == nil { // We only match IP addresses against IP SANs. // See RFC 6125, Appendix B.2. + ip := net.IP(addr.AsSlice()) for _, candidate := range c.IPAddresses { if ip.Equal(candidate) { return nil } } - return HostnameError{c, candidateIP} + return HostnameError{c, ip.String()} } candidateName := toLowerCaseASCII(h) // Save allocations inside the loop.
diff --git a/src/crypto/x509/verify_test.go b/src/crypto/x509/verify_test.go index 1d3e845..d32d815 100644 --- a/src/crypto/x509/verify_test.go +++ b/src/crypto/x509/verify_test.go
@@ -3183,3 +3183,39 @@ } return dsaDER } + +func TestVerifyHostnameIPAddresses(t *testing.T) { + cert := &Certificate{ + IPAddresses: []net.IP{ + net.ParseIP("192.0.2.1"), + net.ParseIP("fe80::1"), + }, + } + + tests := []struct { + name string + host string + match bool + }{ + {name: "IPv4", host: "192.0.2.1", match: true}, + {name: "IPv4 bracketed", host: "[192.0.2.1]", match: true}, + {name: "IPv4 mismatch", host: "192.0.2.2"}, + {name: "IPv6", host: "fe80::1", match: true}, + {name: "IPv6 bracketed", host: "[fe80::1]", match: true}, + {name: "IPv6 with zone", host: "fe80::1%eth0", match: true}, + {name: "IPv6 with zone bracketed", host: "[fe80::1%eth0]", match: true}, + {name: "IPv6 mismatch", host: "fe80::2"}, + {name: "IPv6 with zone mismatch", host: "fe80::2%eth0"}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := cert.VerifyHostname(tc.host) + if tc.match && err != nil { + t.Errorf("VerifyHostname(%q) = %v, want nil", tc.host, err) + } + if !tc.match && err == nil { + t.Errorf("VerifyHostname(%q) = nil, want error", tc.host) + } + }) + } +}