x509roots/fallback/bundle: fix bundle test with Go 1.27+

In Go 1.27 we've updated crypto/x509/pkix to avoid hex-encoding
attribute values that are string-typed. However, in TestBundle() we
assert the parsed certificate subject CN matches expected and now the
parsed value differs based on Go version.

This commit introduces some small helpers that on Go 1.25/1.26 replicate
the Go 1.27 behavior, decoding hex-encoded attribute values before
making the comparison.

In this way the test continues to pass without losing any coverage, or
introducing duplicated per-version bundles. In the future when only Go
1.27+ are supported we can revert this extra machinery.

Change-Id: I66bf6439e421169c0f9c750f88116b73ec5188fe
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/775760
Reviewed-by: Roland Shoemaker <roland@golang.org>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Auto-Submit: Daniel McCarney <daniel@binaryparadox.net>
diff --git a/x509roots/fallback/bundle/bundle_test.go b/x509roots/fallback/bundle/bundle_test.go
index 3eafe15..222e76c 100644
--- a/x509roots/fallback/bundle/bundle_test.go
+++ b/x509roots/fallback/bundle/bundle_test.go
@@ -7,7 +7,9 @@
 import (
 	"crypto/sha256"
 	"crypto/x509"
+	"encoding/asn1"
 	"encoding/hex"
+	"regexp"
 	"testing"
 )
 
@@ -19,7 +21,7 @@
 			continue
 		}
 
-		if unparsed.cn != cert.Subject.String() {
+		if !subjectsEqual(unparsed.cn, cert.Subject.String()) {
 			t.Errorf("unparsedCertificates[%v].cn = %q; want = %q", i, unparsed.cn, cert.Subject.String())
 		}
 
@@ -30,3 +32,34 @@
 		}
 	}
 }
+
+// subjectsEqual reports whether two RFC 2253 DN strings match.
+//
+// It tolerates the rendering difference introduced in Go 1.27, where
+// string-typed attribute values for OIDs outside attributeTypeNames are
+// rendered as strings rather than hex-encoded DER (see Go CL 773800).
+//
+// This can be removed when Go 1.25/1.26 are no longer supported.
+func subjectsEqual(a, b string) bool {
+	return a == b || normalizeHexValues(a) == normalizeHexValues(b)
+}
+
+// normalizeHexValues rewrites any "oid=#hex" to the equivalent "oid=value"
+// rendering produced by Go 1.27+.
+func normalizeHexValues(s string) string {
+	return hexAttrRE.ReplaceAllStringFunc(s, func(match string) string {
+		m := hexAttrRE.FindStringSubmatch(match)
+		der, err := hex.DecodeString(m[2])
+		if err != nil {
+			return match
+		}
+		var v string
+		if rest, err := asn1.Unmarshal(der, &v); err != nil || len(rest) != 0 {
+			return match
+		}
+		return m[1] + "=" + v
+	})
+}
+
+// hexAttrRE matches a "oid=#hex" attribute value in an RFC 2253 DN string.
+var hexAttrRE = regexp.MustCompile(`([\d.]+)=#([[:xdigit:]]+)`)