From 88eeb0822b24e5cbf6bfd7e3246bb4cd6a1968dc Mon Sep 17 00:00:00 2001 From: zhaoyifan Date: Fri, 14 Nov 2025 10:35:12 +0800 Subject: [PATCH] [backport]fix CVE-2025-58187,CVE-2025-61723 --- ...509-improve-domain-name-verification.patch | 509 ++++++++++++++++ ...o-x509-rework-fix-for-CVE-2025-58187.patch | 571 ++++++++++++++++++ ...ng-pem-make-Decode-complexity-linear.patch | 219 +++++++ golang.spec | 11 +- 4 files changed, 1309 insertions(+), 1 deletion(-) create mode 100644 0136-CVE-2025-58187-crypto-x509-improve-domain-name-verification.patch create mode 100644 0137-CVE-2025-58187-crypto-x509-rework-fix-for-CVE-2025-58187.patch create mode 100644 0138-CVE-2025-61723-encoding-pem-make-Decode-complexity-linear.patch diff --git a/0136-CVE-2025-58187-crypto-x509-improve-domain-name-verification.patch b/0136-CVE-2025-58187-crypto-x509-improve-domain-name-verification.patch new file mode 100644 index 0000000..725de5a --- /dev/null +++ b/0136-CVE-2025-58187-crypto-x509-improve-domain-name-verification.patch @@ -0,0 +1,509 @@ +From 3fc4c79fdbb17b9b29ea9f8c29dd780df075d4c4 Mon Sep 17 00:00:00 2001 +From: Neal Patel +Date: Mon, 15 Sep 2025 16:31:22 -0400 +Subject: [PATCH] crypto/x509: improve domain name verification + +Don't use domainToReverseLabels to check if domain names are valid, +since it is not particularly performant, and can contribute to DoS +vectors. Instead just iterate over the name and enforce the properties +we care about. + +This also enforces that DNS names, both in SANs and name constraints, +are valid. We previously allowed invalid SANs, because some +intermediates had these weird names (see #23995), but there are +currently no trusted intermediates that have this property, and since we +target the web PKI, supporting this particular case is not a high +priority. + +Thank you to Jakub Ciolek for reporting this issue. + +Fixes CVE-2025-58187 +Fixes #75681 + +Change-Id: I6ebce847dcbe5fc63ef2f9a74f53f11c4c56d3d1 +Reviewed-on: https://go-internal-review.googlesource.com/c/go/+/2820 +Reviewed-by: Damien Neil +Reviewed-by: Roland Shoemaker +Reviewed-on: https://go-review.googlesource.com/c/go/+/709854 +Auto-Submit: Michael Pratt +Reviewed-by: Carlos Amedee +LUCI-TryBot-Result: Go LUCI + +Backported-by: zhaoyifan +Reference: https://go-review.googlesource.com/c/go/+/709854 +Conflict: x509.go +--- + src/crypto/x509/name_constraints_test.go | 70 +------- + src/crypto/x509/verify.go | 1 + + src/crypto/x509/x509.go | 207 ++++++++++++++--------- + src/crypto/x509/x509_test.go | 36 ++++ + 4 files changed, 167 insertions(+), 147 deletions(-) + +diff --git a/src/crypto/x509/name_constraints_test.go b/src/crypto/x509/name_constraints_test.go +index 0ef3375..105057f 100644 +--- a/src/crypto/x509/name_constraints_test.go ++++ b/src/crypto/x509/name_constraints_test.go +@@ -1453,63 +1453,7 @@ var nameConstraintsTests = []nameConstraintsTest{ + requestedEKUs: []ExtKeyUsage{ExtKeyUsageServerAuth}, + }, + +- // An invalid DNS SAN should be detected only at validation time so +- // that we can process CA certificates in the wild that have invalid SANs. +- // See https://github.com/golang/go/issues/23995 +- +- // #77: an invalid DNS or mail SAN will not be detected if name constraint +- // checking is not triggered. +- { +- roots: make([]constraintsSpec, 1), +- intermediates: [][]constraintsSpec{ +- { +- {}, +- }, +- }, +- leaf: leafSpec{ +- sans: []string{"dns:this is invalid", "email:this @ is invalid"}, +- }, +- }, +- +- // #78: an invalid DNS SAN will be detected if any name constraint checking +- // is triggered. +- { +- roots: []constraintsSpec{ +- { +- bad: []string{"uri:"}, +- }, +- }, +- intermediates: [][]constraintsSpec{ +- { +- {}, +- }, +- }, +- leaf: leafSpec{ +- sans: []string{"dns:this is invalid"}, +- }, +- expectedError: "cannot parse dnsName", +- }, +- +- // #79: an invalid email SAN will be detected if any name constraint +- // checking is triggered. +- { +- roots: []constraintsSpec{ +- { +- bad: []string{"uri:"}, +- }, +- }, +- intermediates: [][]constraintsSpec{ +- { +- {}, +- }, +- }, +- leaf: leafSpec{ +- sans: []string{"email:this @ is invalid"}, +- }, +- expectedError: "cannot parse rfc822Name", +- }, +- +- // #80: if several EKUs are requested, satisfying any of them is sufficient. ++ // #77: if several EKUs are requested, satisfying any of them is sufficient. + { + roots: make([]constraintsSpec, 1), + intermediates: [][]constraintsSpec{ +@@ -1524,7 +1468,7 @@ var nameConstraintsTests = []nameConstraintsTest{ + requestedEKUs: []ExtKeyUsage{ExtKeyUsageClientAuth, ExtKeyUsageEmailProtection}, + }, + +- // #81: EKUs that are not asserted in VerifyOpts are not required to be ++ // #78: EKUs that are not asserted in VerifyOpts are not required to be + // nested. + { + roots: make([]constraintsSpec, 1), +@@ -1543,7 +1487,7 @@ var nameConstraintsTests = []nameConstraintsTest{ + }, + }, + +- // #82: a certificate without SANs and CN is accepted in a constrained chain. ++ // #79: a certificate without SANs and CN is accepted in a constrained chain. + { + roots: []constraintsSpec{ + { +@@ -1560,7 +1504,7 @@ var nameConstraintsTests = []nameConstraintsTest{ + }, + }, + +- // #83: a certificate without SANs and with a CN that does not parse as a ++ // #80: a certificate without SANs and with a CN that does not parse as a + // hostname is accepted in a constrained chain. + { + roots: []constraintsSpec{ +@@ -1579,7 +1523,7 @@ var nameConstraintsTests = []nameConstraintsTest{ + }, + }, + +- // #84: a certificate with SANs and CN is accepted in a constrained chain. ++ // #81: a certificate with SANs and CN is accepted in a constrained chain. + { + roots: []constraintsSpec{ + { +@@ -1597,7 +1541,7 @@ var nameConstraintsTests = []nameConstraintsTest{ + }, + }, + +- // #85: without SANs, a certificate with a valid CN is accepted in a ++ // #82: without SANs, a certificate with a valid CN is accepted in a + // constrained chain if x509ignoreCN is set. + { + roots: []constraintsSpec{ +@@ -1617,7 +1561,7 @@ var nameConstraintsTests = []nameConstraintsTest{ + ignoreCN: true, + }, + +- // #86: URIs with IPv6 addresses with zones and ports are rejected ++ // #83: URIs with IPv6 addresses with zones and ports are rejected + { + roots: []constraintsSpec{ + { +diff --git a/src/crypto/x509/verify.go b/src/crypto/x509/verify.go +index 1f3565d..9e87fc0 100644 +--- a/src/crypto/x509/verify.go ++++ b/src/crypto/x509/verify.go +@@ -379,6 +379,7 @@ func parseRFC2821Mailbox(in string) (mailbox rfc2821Mailbox, ok bool) { + // domainToReverseLabels converts a textual domain name like foo.example.com to + // the list of labels in reverse order, e.g. ["com", "example", "foo"]. + func domainToReverseLabels(domain string) (reverseLabels []string, ok bool) { ++ reverseLabels = make([]string, 0, strings.Count(domain, ".")+1) + for len(domain) > 0 { + if i := strings.LastIndexByte(domain, '.'); i == -1 { + reverseLabels = append(reverseLabels, domain) +diff --git a/src/crypto/x509/x509.go b/src/crypto/x509/x509.go +index 8ce57fb..7ac7c25 100644 +--- a/src/crypto/x509/x509.go ++++ b/src/crypto/x509/x509.go +@@ -468,17 +468,19 @@ func getSignatureAlgorithmFromAI(ai pkix.AlgorithmIdentifier) SignatureAlgorithm + // RFC 3279, 2.3 Public Key Algorithms + // + // pkcs-1 OBJECT IDENTIFIER ::== { iso(1) member-body(2) us(840) +-// rsadsi(113549) pkcs(1) 1 } ++// ++// rsadsi(113549) pkcs(1) 1 } + // + // rsaEncryption OBJECT IDENTIFIER ::== { pkcs1-1 1 } + // + // id-dsa OBJECT IDENTIFIER ::== { iso(1) member-body(2) us(840) +-// x9-57(10040) x9cm(4) 1 } + // +-// RFC 5480, 2.1.1 Unrestricted Algorithm Identifier and Parameters ++// x9-57(10040) x9cm(4) 1 } ++// ++// # RFC 5480, 2.1.1 Unrestricted Algorithm Identifier and Parameters + // +-// id-ecPublicKey OBJECT IDENTIFIER ::= { +-// iso(1) member-body(2) us(840) ansi-X9-62(10045) keyType(2) 1 } ++// id-ecPublicKey OBJECT IDENTIFIER ::= { ++// iso(1) member-body(2) us(840) ansi-X9-62(10045) keyType(2) 1 } + var ( + oidPublicKeyRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 1} + oidPublicKeyDSA = asn1.ObjectIdentifier{1, 2, 840, 10040, 4, 1} +@@ -502,18 +504,18 @@ func getPublicKeyAlgorithmFromOID(oid asn1.ObjectIdentifier) PublicKeyAlgorithm + + // RFC 5480, 2.1.1.1. Named Curve + // +-// secp224r1 OBJECT IDENTIFIER ::= { +-// iso(1) identified-organization(3) certicom(132) curve(0) 33 } ++// secp224r1 OBJECT IDENTIFIER ::= { ++// iso(1) identified-organization(3) certicom(132) curve(0) 33 } + // +-// secp256r1 OBJECT IDENTIFIER ::= { +-// iso(1) member-body(2) us(840) ansi-X9-62(10045) curves(3) +-// prime(1) 7 } ++// secp256r1 OBJECT IDENTIFIER ::= { ++// iso(1) member-body(2) us(840) ansi-X9-62(10045) curves(3) ++// prime(1) 7 } + // +-// secp384r1 OBJECT IDENTIFIER ::= { +-// iso(1) identified-organization(3) certicom(132) curve(0) 34 } ++// secp384r1 OBJECT IDENTIFIER ::= { ++// iso(1) identified-organization(3) certicom(132) curve(0) 34 } + // +-// secp521r1 OBJECT IDENTIFIER ::= { +-// iso(1) identified-organization(3) certicom(132) curve(0) 35 } ++// secp521r1 OBJECT IDENTIFIER ::= { ++// iso(1) identified-organization(3) certicom(132) curve(0) 35 } + // + // NB: secp256r1 is equivalent to prime256v1 + var ( +@@ -1114,23 +1116,80 @@ func forEachSAN(extension []byte, callback func(tag int, data []byte) error) err + return nil + } + ++// domainNameValid checks the structure of a DNS name or similar name from a ++// certificate. If constraint is true, it checks additional properties required ++// for name constraints. ++// ++// This enforces the property that each label of a domain has a maximum length ++// of 63 characters, and so on. See ++// https://datatracker.ietf.org/doc/html/rfc1035#section-2.3.4. ++func domainNameValid(s string, constraint bool) bool { ++ if len(s) == 0 { ++ return false ++ } ++ ++ // There must not be a trailing period, although a trailing period during ++ // matching is handled by trimming it in matchDomainConstraint and ++ // matchHostnames. ++ if s[len(s)-1] == '.' { ++ return false ++ } ++ ++ // https://datatracker.ietf.org/doc/html/rfc1035#section-2.3.4 ++ // 255 octets or less. ++ if len(s) > 253 { ++ return false ++ } ++ ++ labelStart := 0 ++ for i := 0; i < len(s); i++ { ++ c := s[i] ++ if c == '.' { ++ labelLen := i - labelStart ++ if labelLen == 0 { ++ // Empty label, invalid unless this is the first character and ++ // constraint is true, in which case it's used to match the ++ // domain and all subdomains. ++ if i != 0 || !constraint { ++ return false ++ } ++ } ++ // https://datatracker.ietf.org/doc/html/rfc1035#section-2.3.4 ++ // labels 63 octets or less. ++ if labelLen > 63 { ++ return false ++ } ++ labelStart = i + 1 ++ } ++ } ++ // Check the last label. ++ if labelLen := len(s) - labelStart; labelLen == 0 || labelLen > 63 { ++ return false ++ } ++ ++ return true ++} ++ + func parseSANExtension(value []byte) (dnsNames, emailAddresses []string, ipAddresses []net.IP, uris []*url.URL, err error) { + err = forEachSAN(value, func(tag int, data []byte) error { + switch tag { + case nameTypeEmail: +- emailAddresses = append(emailAddresses, string(data)) ++ email := string(data) ++ if _, ok := parseRFC2821Mailbox(email); !ok { ++ return fmt.Errorf("x509: cannot parse rfc822Name %q", email) ++ } ++ emailAddresses = append(emailAddresses, email) + case nameTypeDNS: +- dnsNames = append(dnsNames, string(data)) ++ name := string(data) ++ if !domainNameValid(name, false) { ++ return fmt.Errorf("x509: cannot parse dnsName %q", name) ++ } ++ dnsNames = append(dnsNames, name) + case nameTypeURI: + uri, err := url.Parse(string(data)) + if err != nil { + return fmt.Errorf("x509: cannot parse URI %q: %s", string(data), err) + } +- if len(uri.Host) > 0 { +- if _, ok := domainToReverseLabels(uri.Host); !ok { +- return fmt.Errorf("x509: cannot parse URI %q: invalid domain", string(data)) +- } +- } + uris = append(uris, uri) + case nameTypeIP: + switch len(data) { +@@ -1230,15 +1289,7 @@ func parseNameConstraintsExtension(out *Certificate, e pkix.Extension) (unhandle + return nil, nil, nil, nil, errors.New("x509: invalid constraint value: " + err.Error()) + } + +- trimmedDomain := domain +- if len(trimmedDomain) > 0 && trimmedDomain[0] == '.' { +- // constraints can have a leading +- // period to exclude the domain +- // itself, but that's not valid in a +- // normal domain name. +- trimmedDomain = trimmedDomain[1:] +- } +- if _, ok := domainToReverseLabels(trimmedDomain); !ok { ++ if !domainNameValid(domain, true) { + return nil, nil, nil, nil, fmt.Errorf("x509: failed to parse dnsName constraint %q", domain) + } + dnsNames = append(dnsNames, domain) +@@ -1280,11 +1331,7 @@ func parseNameConstraintsExtension(out *Certificate, e pkix.Extension) (unhandle + } + } else { + // Otherwise it's a domain name. +- domain := constraint +- if len(domain) > 0 && domain[0] == '.' { +- domain = domain[1:] +- } +- if _, ok := domainToReverseLabels(domain); !ok { ++ if !domainNameValid(constraint, true) { + return nil, nil, nil, nil, fmt.Errorf("x509: failed to parse rfc822Name constraint %q", constraint) + } + } +@@ -1300,15 +1347,7 @@ func parseNameConstraintsExtension(out *Certificate, e pkix.Extension) (unhandle + return nil, nil, nil, nil, fmt.Errorf("x509: failed to parse URI constraint %q: cannot be IP address", domain) + } + +- trimmedDomain := domain +- if len(trimmedDomain) > 0 && trimmedDomain[0] == '.' { +- // constraints can have a leading +- // period to exclude the domain itself, +- // but that's not valid in a normal +- // domain name. +- trimmedDomain = trimmedDomain[1:] +- } +- if _, ok := domainToReverseLabels(trimmedDomain); !ok { ++ if !domainNameValid(domain, true) { + return nil, nil, nil, nil, fmt.Errorf("x509: failed to parse URI constraint %q", domain) + } + uriDomains = append(uriDomains, domain) +@@ -2037,38 +2076,38 @@ var emptyASN1Subject = []byte{0x30, 0} + // CreateCertificate creates a new X.509v3 certificate based on a template. + // The following members of template are used: + // +-// - AuthorityKeyId +-// - BasicConstraintsValid +-// - CRLDistributionPoints +-// - DNSNames +-// - EmailAddresses +-// - ExcludedDNSDomains +-// - ExcludedEmailAddresses +-// - ExcludedIPRanges +-// - ExcludedURIDomains +-// - ExtKeyUsage +-// - ExtraExtensions +-// - IPAddresses +-// - IsCA +-// - IssuingCertificateURL +-// - KeyUsage +-// - MaxPathLen +-// - MaxPathLenZero +-// - NotAfter +-// - NotBefore +-// - OCSPServer +-// - PermittedDNSDomains +-// - PermittedDNSDomainsCritical +-// - PermittedEmailAddresses +-// - PermittedIPRanges +-// - PermittedURIDomains +-// - PolicyIdentifiers +-// - SerialNumber +-// - SignatureAlgorithm +-// - Subject +-// - SubjectKeyId +-// - URIs +-// - UnknownExtKeyUsage ++// - AuthorityKeyId ++// - BasicConstraintsValid ++// - CRLDistributionPoints ++// - DNSNames ++// - EmailAddresses ++// - ExcludedDNSDomains ++// - ExcludedEmailAddresses ++// - ExcludedIPRanges ++// - ExcludedURIDomains ++// - ExtKeyUsage ++// - ExtraExtensions ++// - IPAddresses ++// - IsCA ++// - IssuingCertificateURL ++// - KeyUsage ++// - MaxPathLen ++// - MaxPathLenZero ++// - NotAfter ++// - NotBefore ++// - OCSPServer ++// - PermittedDNSDomains ++// - PermittedDNSDomainsCritical ++// - PermittedEmailAddresses ++// - PermittedIPRanges ++// - PermittedURIDomains ++// - PolicyIdentifiers ++// - SerialNumber ++// - SignatureAlgorithm ++// - Subject ++// - SubjectKeyId ++// - URIs ++// - UnknownExtKeyUsage + // + // The certificate is signed by parent. If parent is equal to template then the + // certificate is self-signed. The parameter pub is the public key of the +@@ -2420,14 +2459,14 @@ func parseCSRExtensions(rawAttributes []asn1.RawValue) ([]pkix.Extension, error) + // CreateCertificateRequest creates a new certificate request based on a + // template. The following members of template are used: + // +-// - SignatureAlgorithm +-// - Subject +-// - DNSNames +-// - EmailAddresses +-// - IPAddresses +-// - URIs +-// - ExtraExtensions +-// - Attributes (deprecated) ++// - SignatureAlgorithm ++// - Subject ++// - DNSNames ++// - EmailAddresses ++// - IPAddresses ++// - URIs ++// - ExtraExtensions ++// - Attributes (deprecated) + // + // priv is the private key to sign the CSR with, and the corresponding public + // key will be included in the CSR. It must implement crypto.Signer and its +diff --git a/src/crypto/x509/x509_test.go b/src/crypto/x509/x509_test.go +index 840f535..0c57663 100644 +--- a/src/crypto/x509/x509_test.go ++++ b/src/crypto/x509/x509_test.go +@@ -2702,3 +2702,39 @@ func TestCreateRevocationList(t *testing.T) { + }) + } + } ++ ++func TestDomainNameValid(t *testing.T) { ++ tests := []struct { ++ name string ++ domain string ++ constraint bool ++ want bool ++ }{ ++ {"empty string", "", false, false}, ++ {"valid domain", "example.com", false, true}, ++ {"valid subdomain", "sub.example.com", false, true}, ++ {"trailing period", "example.com.", false, false}, ++ {"leading period non-constraint", ".example.com", false, false}, ++ {"leading period constraint", ".example.com", true, true}, ++ {"double period", "example..com", false, false}, ++ {"too long", strings.Repeat("a", 254), false, false}, ++ {"exactly 253 chars", strings.Repeat("a", 240) + ".example.com", false, true}, ++ {"label too long", strings.Repeat("a", 64) + ".example.com", false, false}, ++ {"label exactly 63 chars", strings.Repeat("a", 63) + ".example.com", false, true}, ++ {"single label", "localhost", false, true}, ++ {"only period non-constraint", ".", false, false}, ++ {"only period constraint", ".", true, false}, ++ {"multiple labels", "a.b.c.d.e.f.g", false, true}, ++ {"empty label at start", ".example", false, false}, ++ {"empty label at end", "example.", false, false}, ++ {"hyphen at start", "-example.com", false, true}, ++ } ++ ++ for _, tt := range tests { ++ t.Run(tt.name, func(t *testing.T) { ++ if got := domainNameValid(tt.domain, tt.constraint); got != tt.want { ++ t.Errorf("domainNameValid(%q, %v) = %v, want %v", tt.domain, tt.constraint, got, tt.want) ++ } ++ }) ++ } ++} +-- +2.33.0 + diff --git a/0137-CVE-2025-58187-crypto-x509-rework-fix-for-CVE-2025-58187.patch b/0137-CVE-2025-58187-crypto-x509-rework-fix-for-CVE-2025-58187.patch new file mode 100644 index 0000000..1f95f01 --- /dev/null +++ b/0137-CVE-2025-58187-crypto-x509-rework-fix-for-CVE-2025-58187.patch @@ -0,0 +1,571 @@ +From 1cd71689f2ed8f07031a0cc58fc3586ca501839f Mon Sep 17 00:00:00 2001 +From: Roland Shoemaker +Date: Thu, 9 Oct 2025 13:35:24 -0700 +Subject: [PATCH] crypto/x509: rework fix for CVE-2025-58187 + +In CL 709854 we enabled strict validation for a number of properties of +domain names (and their constraints). This caused significant breakage, +since we didn't previously disallow the creation of certificates which +contained these malformed domains. + +Rollback a number of the properties we enforced, making domainNameValid +only enforce the same properties that domainToReverseLabels does. Since +this also undoes some of the DoS protections our initial fix enabled, +this change also adds caching of constraints in isValid (which perhaps +is the fix we should've initially chosen). + +Updates #75835 +Fixes #75828 + +Change-Id: Ie6ca6b4f30e9b8a143692b64757f7bbf4671ed0e +Reviewed-on: https://go-review.googlesource.com/c/go/+/710735 +LUCI-TryBot-Result: Go LUCI +Reviewed-by: Damien Neil + +Backported-by: zhaoyifan +Reference: https://go-review.googlesource.com/c/go/+/710735 +Conflict: x509.go verify.go x509_test.go +--- + src/crypto/x509/name_constraints_test.go | 90 ++++++++++++++++------ + src/crypto/x509/verify.go | 54 ++++++++++---- + src/crypto/x509/verify_test.go | 2 +- + src/crypto/x509/x509.go | 95 ++++++++++++------------ + src/crypto/x509/x509_test.go | 95 +++++++++++++++--------- + 5 files changed, 215 insertions(+), 121 deletions(-) + +diff --git a/src/crypto/x509/name_constraints_test.go b/src/crypto/x509/name_constraints_test.go +index 105057f..92d9a64 100644 +--- a/src/crypto/x509/name_constraints_test.go ++++ b/src/crypto/x509/name_constraints_test.go +@@ -1453,7 +1453,63 @@ var nameConstraintsTests = []nameConstraintsTest{ + requestedEKUs: []ExtKeyUsage{ExtKeyUsageServerAuth}, + }, + +- // #77: if several EKUs are requested, satisfying any of them is sufficient. ++ // An invalid DNS SAN should be detected only at validation time so ++ // that we can process CA certificates in the wild that have invalid SANs. ++ // See https://github.com/golang/go/issues/23995 ++ ++ // #77: an invalid DNS or mail SAN will not be detected if name constraint ++ // checking is not triggered. ++ { ++ roots: make([]constraintsSpec, 1), ++ intermediates: [][]constraintsSpec{ ++ { ++ {}, ++ }, ++ }, ++ leaf: leafSpec{ ++ sans: []string{"dns:this is invalid", "email:this @ is invalid"}, ++ }, ++ }, ++ ++ // #78: an invalid DNS SAN will be detected if any name constraint checking ++ // is triggered. ++ { ++ roots: []constraintsSpec{ ++ { ++ bad: []string{"uri:"}, ++ }, ++ }, ++ intermediates: [][]constraintsSpec{ ++ { ++ {}, ++ }, ++ }, ++ leaf: leafSpec{ ++ sans: []string{"dns:this is invalid"}, ++ }, ++ expectedError: "cannot parse dnsName", ++ }, ++ ++ // #79: an invalid email SAN will be detected if any name constraint ++ // checking is triggered. ++ { ++ roots: []constraintsSpec{ ++ { ++ bad: []string{"uri:"}, ++ }, ++ }, ++ intermediates: [][]constraintsSpec{ ++ { ++ {}, ++ }, ++ }, ++ leaf: leafSpec{ ++ sans: []string{"email:this @ is invalid"}, ++ }, ++ expectedError: "cannot parse rfc822Name", ++ }, ++ ++ // #80: if several EKUs are requested, satisfying any of them is sufficient. + { + roots: make([]constraintsSpec, 1), + intermediates: [][]constraintsSpec{ +@@ -1468,7 +1524,7 @@ var nameConstraintsTests = []nameConstraintsTest{ + requestedEKUs: []ExtKeyUsage{ExtKeyUsageClientAuth, ExtKeyUsageEmailProtection}, + }, + +- // #78: EKUs that are not asserted in VerifyOpts are not required to be ++ // #81: EKUs that are not asserted in VerifyOpts are not required to be + // nested. + { + roots: make([]constraintsSpec, 1), +@@ -1487,7 +1543,7 @@ var nameConstraintsTests = []nameConstraintsTest{ + }, + }, + +- // #79: a certificate without SANs and CN is accepted in a constrained chain. ++ // #82: a certificate without SANs and CN is accepted in a constrained chain. + { + roots: []constraintsSpec{ + { +@@ -1504,7 +1560,7 @@ var nameConstraintsTests = []nameConstraintsTest{ + }, + }, + +- // #80: a certificate without SANs and with a CN that does not parse as a ++ // #83: a certificate without SANs and with a CN that does not parse as a + // hostname is accepted in a constrained chain. + { + roots: []constraintsSpec{ +@@ -1523,7 +1579,7 @@ var nameConstraintsTests = []nameConstraintsTest{ + }, + }, + +- // #81: a certificate with SANs and CN is accepted in a constrained chain. ++ // #84: a certificate with SANs and CN is accepted in a constrained chain. + { + roots: []constraintsSpec{ + { +@@ -1541,27 +1597,15 @@ var nameConstraintsTests = []nameConstraintsTest{ + }, + }, + +- // #82: without SANs, a certificate with a valid CN is accepted in a +- // constrained chain if x509ignoreCN is set. ++ // #85: .example.com is an invalid DNS name, it should not match the ++ // constraint example.com. + { +- roots: []constraintsSpec{ +- { +- ok: []string{"dns:foo.com", "dns:.foo.com"}, +- }, +- }, +- intermediates: [][]constraintsSpec{ +- { +- {}, +- }, +- }, +- leaf: leafSpec{ +- sans: []string{}, +- cn: "foo.com", +- }, +- ignoreCN: true, ++ roots: []constraintsSpec{{ok: []string{"dns:example.com"}}}, ++ leaf: leafSpec{sans: []string{"dns:.example.com"}}, ++ expectedError: "cannot parse dnsName \".example.com\"", + }, + +- // #83: URIs with IPv6 addresses with zones and ports are rejected ++ // #86: URIs with IPv6 addresses with zones and ports are rejected + { + roots: []constraintsSpec{ + { +diff --git a/src/crypto/x509/verify.go b/src/crypto/x509/verify.go +index 9e87fc0..8d6e1fa 100644 +--- a/src/crypto/x509/verify.go ++++ b/src/crypto/x509/verify.go +@@ -412,7 +412,7 @@ func domainToReverseLabels(domain string) (reverseLabels []string, ok bool) { + return reverseLabels, true + } + +-func matchEmailConstraint(mailbox rfc2821Mailbox, constraint string) (bool, error) { ++func matchEmailConstraint(mailbox rfc2821Mailbox, constraint string, reversedDomainsCache map[string][]string, reversedConstraintsCache map[string][]string) (bool, error) { + // If the constraint contains an @, then it specifies an exact mailbox + // name. + if strings.Contains(constraint, "@") { +@@ -425,10 +425,10 @@ func matchEmailConstraint(mailbox rfc2821Mailbox, constraint string) (bool, erro + + // Otherwise the constraint is like a DNS constraint of the domain part + // of the mailbox. +- return matchDomainConstraint(mailbox.domain, constraint) ++ return matchDomainConstraint(mailbox.domain, constraint, reversedDomainsCache, reversedConstraintsCache) + } + +-func matchURIConstraint(uri *url.URL, constraint string) (bool, error) { ++func matchURIConstraint(uri *url.URL, constraint string, reversedDomainsCache map[string][]string, reversedConstraintsCache map[string][]string) (bool, error) { + // From RFC 5280, Section 4.2.1.10: + // “a uniformResourceIdentifier that does not include an authority + // component with a host name specified as a fully qualified domain +@@ -458,7 +458,7 @@ func matchURIConstraint(uri *url.URL, constraint string) (bool, error) { + return false, fmt.Errorf("URI with IP (%q) cannot be matched against constraints", uri.String()) + } + +- return matchDomainConstraint(host, constraint) ++ return matchDomainConstraint(host, constraint, reversedDomainsCache, reversedConstraintsCache) + } + + //go:linkname parseIPZone net.parseIPZone +@@ -478,16 +478,21 @@ func matchIPConstraint(ip net.IP, constraint *net.IPNet) (bool, error) { + return true, nil + } + +-func matchDomainConstraint(domain, constraint string) (bool, error) { ++func matchDomainConstraint(domain, constraint string, reversedDomainsCache map[string][]string, reversedConstraintsCache map[string][]string) (bool, error) { + // The meaning of zero length constraints is not specified, but this + // code follows NSS and accepts them as matching everything. + if len(constraint) == 0 { + return true, nil + } + +- domainLabels, ok := domainToReverseLabels(domain) +- if !ok { +- return false, fmt.Errorf("x509: internal error: cannot parse domain %q", domain) ++ domainLabels, found := reversedDomainsCache[domain] ++ if !found { ++ var ok bool ++ domainLabels, ok = domainToReverseLabels(domain) ++ if !ok { ++ return false, fmt.Errorf("x509: internal error: cannot parse domain %q", domain) ++ } ++ reversedDomainsCache[domain] = domainLabels + } + + // RFC 5280 says that a leading period in a domain name means that at +@@ -501,9 +506,14 @@ func matchDomainConstraint(domain, constraint string) (bool, error) { + constraint = constraint[1:] + } + +- constraintLabels, ok := domainToReverseLabels(constraint) +- if !ok { +- return false, fmt.Errorf("x509: internal error: cannot parse domain %q", constraint) ++ constraintLabels, found := reversedConstraintsCache[constraint] ++ if !found { ++ var ok bool ++ constraintLabels, ok = domainToReverseLabels(constraint) ++ if !ok { ++ return false, fmt.Errorf("x509: internal error: cannot parse domain %q", constraint) ++ } ++ reversedConstraintsCache[constraint] = constraintLabels + } + + if len(domainLabels) < len(constraintLabels) || +@@ -627,6 +637,20 @@ func (c *Certificate) isValid(certType int, currentChain []*Certificate, opts *V + } + + checkNameConstraints := (certType == intermediateCertificate || certType == rootCertificate) && c.hasNameConstraints() ++ ++ // Each time we do constraint checking, we need to check the constraints in ++ // the current certificate against all of the names that preceded it. We ++ // reverse these names using domainToReverseLabels, which is a relatively ++ // expensive operation. Since we check each name against each constraint, ++ // this requires us to do N*C calls to domainToReverseLabels (where N is the ++ // total number of names that preceed the certificate, and C is the total ++ // number of constraints in the certificate). By caching the results of ++ // calling domainToReverseLabels, we can reduce that to N+C calls at the ++ // cost of keeping all of the parsed names and constraints in memory until ++ // we return from isValid. ++ reversedDomainsCache := map[string][]string{} ++ reversedConstraintsCache := map[string][]string{} ++ + if checkNameConstraints && leaf.commonNameAsHostname() { + // This is the deprecated, legacy case of depending on the commonName as + // a hostname. We don't enforce name constraints against the CN, but +@@ -646,20 +670,20 @@ func (c *Certificate) isValid(certType int, currentChain []*Certificate, opts *V + + if err := c.checkNameConstraints(&comparisonCount, maxConstraintComparisons, "email address", name, mailbox, + func(parsedName, constraint interface{}) (bool, error) { +- return matchEmailConstraint(parsedName.(rfc2821Mailbox), constraint.(string)) ++ return matchEmailConstraint(parsedName.(rfc2821Mailbox), constraint.(string), reversedDomainsCache, reversedConstraintsCache) + }, c.PermittedEmailAddresses, c.ExcludedEmailAddresses); err != nil { + return err + } + + case nameTypeDNS: + name := string(data) +- if _, ok := domainToReverseLabels(name); !ok { ++ if !domainNameValid(name, false) { + return fmt.Errorf("x509: cannot parse dnsName %q", name) + } + + if err := c.checkNameConstraints(&comparisonCount, maxConstraintComparisons, "DNS name", name, name, + func(parsedName, constraint interface{}) (bool, error) { +- return matchDomainConstraint(parsedName.(string), constraint.(string)) ++ return matchDomainConstraint(parsedName.(string), constraint.(string), reversedDomainsCache, reversedConstraintsCache) + }, c.PermittedDNSDomains, c.ExcludedDNSDomains); err != nil { + return err + } +@@ -673,7 +697,7 @@ func (c *Certificate) isValid(certType int, currentChain []*Certificate, opts *V + + if err := c.checkNameConstraints(&comparisonCount, maxConstraintComparisons, "URI", name, uri, + func(parsedName, constraint interface{}) (bool, error) { +- return matchURIConstraint(parsedName.(*url.URL), constraint.(string)) ++ return matchURIConstraint(parsedName.(*url.URL), constraint.(string), reversedDomainsCache, reversedConstraintsCache) + }, c.PermittedURIDomains, c.ExcludedURIDomains); err != nil { + return err + } +diff --git a/src/crypto/x509/verify_test.go b/src/crypto/x509/verify_test.go +index 0606c0b..dc4e85f 100644 +--- a/src/crypto/x509/verify_test.go ++++ b/src/crypto/x509/verify_test.go +@@ -1667,7 +1667,7 @@ var nameConstraintTests = []struct { + + func TestNameConstraints(t *testing.T) { + for i, test := range nameConstraintTests { +- result, err := matchDomainConstraint(test.domain, test.constraint) ++ result, err := matchDomainConstraint(test.domain, test.constraint, map[string][]string{}, map[string][]string{}) + + if err != nil && !test.expectError { + t.Errorf("unexpected error for test #%d: domain=%s, constraint=%s, err=%s", i, test.domain, test.constraint, err) +diff --git a/src/crypto/x509/x509.go b/src/crypto/x509/x509.go +index 7ac7c25..4169b09 100644 +--- a/src/crypto/x509/x509.go ++++ b/src/crypto/x509/x509.go +@@ -1116,56 +1116,61 @@ func forEachSAN(extension []byte, callback func(tag int, data []byte) error) err + return nil + } + +-// domainNameValid checks the structure of a DNS name or similar name from a +-// certificate. If constraint is true, it checks additional properties required +-// for name constraints. +-// +-// This enforces the property that each label of a domain has a maximum length +-// of 63 characters, and so on. See +-// https://datatracker.ietf.org/doc/html/rfc1035#section-2.3.4. ++// domainNameValid is an alloc-less version of the checks that ++// domainToReverseLabels does. + func domainNameValid(s string, constraint bool) bool { ++ // TODO(#75835): This function omits a number of checks which we ++ // really should be doing to enforce that domain names are valid names per ++ // RFC 1034. We previously enabled these checks, but this broke a ++ // significant number of certificates we previously considered valid, and we ++ // happily create via CreateCertificate (et al). We should enable these ++ // checks, but will need to gate them behind a GODEBUG. ++ // ++ // I have left the checks we previously enabled, noted with "TODO(#75835)" so ++ // that we can easily re-enable them once we unbreak everyone. ++ ++ // TODO(#75835): this should only be true for constraints. + if len(s) == 0 { +- return false ++ return true + } + +- // There must not be a trailing period, although a trailing period during +- // matching is handled by trimming it in matchDomainConstraint and +- // matchHostnames. ++ // Do not allow trailing period (FQDN format is not allowed in SANs or ++ // constraints). + if s[len(s)-1] == '.' { + return false + } + +- // https://datatracker.ietf.org/doc/html/rfc1035#section-2.3.4 +- // 255 octets or less. +- if len(s) > 253 { +- return false ++ // TODO(#75835): domains must have at least one label, cannot have ++ // a leading empty label, and cannot be longer than 253 characters. ++ // if len(s) == 0 || (!constraint && s[0] == '.') || len(s) > 253 { ++ // return false ++ // } ++ ++ lastDot := -1 ++ if constraint && s[0] == '.' { ++ s = s[1:] + } + +- labelStart := 0 +- for i := 0; i < len(s); i++ { +- c := s[i] +- if c == '.' { +- labelLen := i - labelStart +- if labelLen == 0 { +- // Empty label, invalid unless this is the first character and +- // constraint is true, in which case it's used to match the +- // domain and all subdomains. +- if i != 0 || !constraint { +- return false +- } ++ for i := 0; i <= len(s); i++ { ++ if i < len(s) && (s[i] < 33 || s[i] > 126) { ++ // Invalid character. ++ return false ++ } ++ if i == len(s) || s[i] == '.' { ++ labelLen := i ++ if lastDot >= 0 { ++ labelLen -= lastDot + 1 + } +- // https://datatracker.ietf.org/doc/html/rfc1035#section-2.3.4 +- // labels 63 octets or less. +- if labelLen > 63 { ++ if labelLen == 0 { + return false + } +- labelStart = i + 1 ++ // TODO(#75835): labels cannot be longer than 63 characters. ++ // if labelLen > 63 { ++ // return false ++ // } ++ lastDot = i + } + } +- // Check the last label. +- if labelLen := len(s) - labelStart; labelLen == 0 || labelLen > 63 { +- return false +- } + + return true + } +@@ -1174,21 +1179,17 @@ func parseSANExtension(value []byte) (dnsNames, emailAddresses []string, ipAddre + err = forEachSAN(value, func(tag int, data []byte) error { + switch tag { + case nameTypeEmail: +- email := string(data) +- if _, ok := parseRFC2821Mailbox(email); !ok { +- return fmt.Errorf("x509: cannot parse rfc822Name %q", email) +- } +- emailAddresses = append(emailAddresses, email) ++ emailAddresses = append(emailAddresses, string(data)) + case nameTypeDNS: +- name := string(data) +- if !domainNameValid(name, false) { +- return fmt.Errorf("x509: cannot parse dnsName %q", name) +- } +- dnsNames = append(dnsNames, name) ++ dnsNames = append(dnsNames, string(data)) + case nameTypeURI: +- uri, err := url.Parse(string(data)) ++ uriStr := string(data) ++ uri, err := url.Parse(uriStr) + if err != nil { +- return fmt.Errorf("x509: cannot parse URI %q: %s", string(data), err) ++ return fmt.Errorf("x509: cannot parse URI %q: %s", uriStr, err) ++ } ++ if len(uri.Host) > 0 && !domainNameValid(uri.Host, false) { ++ return fmt.Errorf("x509: cannot parse URI %q: invalid domain", uriStr) + } + uris = append(uris, uri) + case nameTypeIP: +diff --git a/src/crypto/x509/x509_test.go b/src/crypto/x509/x509_test.go +index 0c57663..7f2c931 100644 +--- a/src/crypto/x509/x509_test.go ++++ b/src/crypto/x509/x509_test.go +@@ -1838,16 +1838,18 @@ func TestInsecureAlgorithmErrorString(t *testing.T) { + } + + // These CSR was generated with OpenSSL: +-// openssl req -out CSR.csr -new -sha256 -nodes -keyout privateKey.key -config openssl.cnf ++// ++// openssl req -out CSR.csr -new -sha256 -nodes -keyout privateKey.key -config openssl.cnf + // + // With openssl.cnf containing the following sections: +-// [ v3_req ] +-// basicConstraints = CA:FALSE +-// keyUsage = nonRepudiation, digitalSignature, keyEncipherment +-// subjectAltName = email:gopher@golang.org,DNS:test.example.com +-// [ req_attributes ] +-// challengePassword = ignored challenge +-// unstructuredName = ignored unstructured name ++// ++// [ v3_req ] ++// basicConstraints = CA:FALSE ++// keyUsage = nonRepudiation, digitalSignature, keyEncipherment ++// subjectAltName = email:gopher@golang.org,DNS:test.example.com ++// [ req_attributes ] ++// challengePassword = ignored challenge ++// unstructuredName = ignored unstructured name + var csrBase64Array = [...]string{ + // Just [ v3_req ] + "MIIDHDCCAgQCAQAwfjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEUMBIGA1UEAwwLQ29tbW9uIE5hbWUxITAfBgkqhkiG9w0BCQEWEnRlc3RAZW1haWwuYWRkcmVzczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK1GY4YFx2ujlZEOJxQVYmsjUnLsd5nFVnNpLE4cV+77sgv9NPNlB8uhn3MXt5leD34rm/2BisCHOifPucYlSrszo2beuKhvwn4+2FxDmWtBEMu/QA16L5IvoOfYZm/gJTsPwKDqvaR0tTU67a9OtxwNTBMI56YKtmwd/o8d3hYv9cg+9ZGAZ/gKONcg/OWYx/XRh6bd0g8DMbCikpWgXKDsvvK1Nk+VtkDO1JxuBaj4Lz/p/MifTfnHoqHxWOWl4EaTs4Ychxsv34/rSj1KD1tJqorIv5Xv2aqv4sjxfbrYzX4kvS5SC1goIovLnhj5UjmQ3Qy8u65eow/LLWw+YFcCAwEAAaBZMFcGCSqGSIb3DQEJDjFKMEgwCQYDVR0TBAIwADALBgNVHQ8EBAMCBeAwLgYDVR0RBCcwJYERZ29waGVyQGdvbGFuZy5vcmeCEHRlc3QuZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADggEBAB6VPMRrchvNW61Tokyq3ZvO6/NoGIbuwUn54q6l5VZW0Ep5Nq8juhegSSnaJ0jrovmUgKDN9vEo2KxuAtwG6udS6Ami3zP+hRd4k9Q8djJPb78nrjzWiindLK5Fps9U5mMoi1ER8ViveyAOTfnZt/jsKUaRsscY2FzE9t9/o5moE6LTcHUS4Ap1eheR+J72WOnQYn3cifYaemsA9MJuLko+kQ6xseqttbh9zjqd9fiCSh/LNkzos9c+mg2yMADitaZinAh+HZi50ooEbjaT3erNq9O6RqwJlgD00g6MQdoz9bTAryCUhCQfkIaepmQ7BxS0pqWNW3MMwfDwx/Snz6g=", +@@ -2704,36 +2706,59 @@ func TestCreateRevocationList(t *testing.T) { + } + + func TestDomainNameValid(t *testing.T) { +- tests := []struct { ++ for _, tc := range []struct { + name string +- domain string ++ dnsName string + constraint bool +- want bool ++ valid bool + }{ +- {"empty string", "", false, false}, +- {"valid domain", "example.com", false, true}, +- {"valid subdomain", "sub.example.com", false, true}, +- {"trailing period", "example.com.", false, false}, +- {"leading period non-constraint", ".example.com", false, false}, +- {"leading period constraint", ".example.com", true, true}, +- {"double period", "example..com", false, false}, +- {"too long", strings.Repeat("a", 254), false, false}, +- {"exactly 253 chars", strings.Repeat("a", 240) + ".example.com", false, true}, +- {"label too long", strings.Repeat("a", 64) + ".example.com", false, false}, +- {"label exactly 63 chars", strings.Repeat("a", 63) + ".example.com", false, true}, +- {"single label", "localhost", false, true}, +- {"only period non-constraint", ".", false, false}, +- {"only period constraint", ".", true, false}, +- {"multiple labels", "a.b.c.d.e.f.g", false, true}, +- {"empty label at start", ".example", false, false}, +- {"empty label at end", "example.", false, false}, +- {"hyphen at start", "-example.com", false, true}, +- } +- +- for _, tt := range tests { +- t.Run(tt.name, func(t *testing.T) { +- if got := domainNameValid(tt.domain, tt.constraint); got != tt.want { +- t.Errorf("domainNameValid(%q, %v) = %v, want %v", tt.domain, tt.constraint, got, tt.want) ++ // TODO(#75835): these tests are for stricter name validation, which we ++ // had to disable. Once we reenable these strict checks, behind a ++ // GODEBUG, we should add them back in. ++ // {"empty name, name", "", false, false}, ++ // {"254 char label, name", strings.Repeat("a.a", 84) + "aaa", false, false}, ++ // {"254 char label, constraint", strings.Repeat("a.a", 84) + "aaa", true, false}, ++ // {"253 char label, name", strings.Repeat("a.a", 84) + "aa", false, false}, ++ // {"253 char label, constraint", strings.Repeat("a.a", 84) + "aa", true, false}, ++ // {"64 char single label, name", strings.Repeat("a", 64), false, false}, ++ // {"64 char single label, constraint", strings.Repeat("a", 64), true, false}, ++ // {"64 char label, name", "a." + strings.Repeat("a", 64), false, false}, ++ // {"64 char label, constraint", "a." + strings.Repeat("a", 64), true, false}, ++ ++ // TODO(#75835): these are the inverse of the tests above, they should be removed ++ // once the strict checking is enabled. ++ {"254 char label, name", strings.Repeat("a.a", 84) + "aaa", false, true}, ++ {"254 char label, constraint", strings.Repeat("a.a", 84) + "aaa", true, true}, ++ {"253 char label, name", strings.Repeat("a.a", 84) + "aa", false, true}, ++ {"253 char label, constraint", strings.Repeat("a.a", 84) + "aa", true, true}, ++ {"64 char single label, name", strings.Repeat("a", 64), false, true}, ++ {"64 char single label, constraint", strings.Repeat("a", 64), true, true}, ++ {"64 char label, name", "a." + strings.Repeat("a", 64), false, true}, ++ {"64 char label, constraint", "a." + strings.Repeat("a", 64), true, true}, ++ ++ // Check we properly enforce properties of domain names. ++ {"empty name, constraint", "", true, true}, ++ {"empty label, name", "a..a", false, false}, ++ {"empty label, constraint", "a..a", true, false}, ++ {"period, name", ".", false, false}, ++ {"period, constraint", ".", true, false}, // TODO(roland): not entirely clear if this is a valid constraint (require at least one label?) ++ {"valid, name", "a.b.c", false, true}, ++ {"valid, constraint", "a.b.c", true, true}, ++ {"leading period, name", ".a.b.c", false, false}, ++ {"leading period, constraint", ".a.b.c", true, true}, ++ {"trailing period, name", "a.", false, false}, ++ {"trailing period, constraint", "a.", true, false}, ++ {"bare label, name", "a", false, true}, ++ {"bare label, constraint", "a", true, true}, ++ {"63 char single label, name", strings.Repeat("a", 63), false, true}, ++ {"63 char single label, constraint", strings.Repeat("a", 63), true, true}, ++ {"63 char label, name", "a." + strings.Repeat("a", 63), false, true}, ++ {"63 char label, constraint", "a." + strings.Repeat("a", 63), true, true}, ++ } { ++ t.Run(tc.name, func(t *testing.T) { ++ valid := domainNameValid(tc.dnsName, tc.constraint) ++ if tc.valid != valid { ++ t.Errorf("domainNameValid(%q, %t) = %v; want %v", tc.dnsName, tc.constraint, !tc.valid, tc.valid) + } + }) + } +-- +2.33.0 \ No newline at end of file diff --git a/0138-CVE-2025-61723-encoding-pem-make-Decode-complexity-linear.patch b/0138-CVE-2025-61723-encoding-pem-make-Decode-complexity-linear.patch new file mode 100644 index 0000000..88e7473 --- /dev/null +++ b/0138-CVE-2025-61723-encoding-pem-make-Decode-complexity-linear.patch @@ -0,0 +1,219 @@ +From 5ce8cd16f3859ec5ac4106ad8ec15d6236f4501b Mon Sep 17 00:00:00 2001 +From: Roland Shoemaker +Date: Tue, 30 Sep 2025 11:16:56 -0700 +Subject: [PATCH] encoding/pem: make Decode complexity linear + +Because Decode scanned the input first for the first BEGIN line, and +then the first END line, the complexity of Decode is quadratic. If the +input contained a large number of BEGINs and then a single END right at +the end of the input, we would find the first BEGIN, and then scan the +entire input for the END, and fail to parse the block, so move onto the +next BEGIN, scan the entire input for the END, etc. + +Instead, look for the first END in the input, and then the first BEGIN +that precedes the found END. We then process the bytes between the BEGIN +and END, and move onto the bytes after the END for further processing. +This gives us linear complexity. + +Fixes CVE-2025-61723 +Fixes #75676 + +Change-Id: I813c4f63e78bca4054226c53e13865c781564ccf +Reviewed-on: https://go-internal-review.googlesource.com/c/go/+/2921 +Reviewed-by: Nicholas Husin +Reviewed-by: Damien Neil +Reviewed-on: https://go-review.googlesource.com/c/go/+/709858 +TryBot-Bypass: Michael Pratt +Auto-Submit: Michael Pratt +Reviewed-by: Carlos Amedee + +Backported-by: zhaoyifan +Reference: https://go-review.googlesource.com/c/go/+/709858 +Conflict: no +--- + src/encoding/pem/pem.go | 67 ++++++++++++++++++++---------------- + src/encoding/pem/pem_test.go | 13 +++---- + 2 files changed, 44 insertions(+), 36 deletions(-) + +diff --git a/src/encoding/pem/pem.go b/src/encoding/pem/pem.go +index 1bee1c1..1790127 100644 +--- a/src/encoding/pem/pem.go ++++ b/src/encoding/pem/pem.go +@@ -35,7 +35,7 @@ type Block struct { + // line bytes. The remainder of the byte array (also not including the new line + // bytes) is also returned and this will always be smaller than the original + // argument. +-func getLine(data []byte) (line, rest []byte) { ++func getLine(data []byte) (line, rest []byte, consumed int) { + i := bytes.IndexByte(data, '\n') + var j int + if i < 0 { +@@ -47,7 +47,7 @@ func getLine(data []byte) (line, rest []byte) { + i-- + } + } +- return bytes.TrimRight(data[0:i], " \t"), data[j:] ++ return bytes.TrimRight(data[0:i], " \t"), data[j:], j + } + + // removeSpacesAndTabs returns a copy of its input with all spaces and tabs +@@ -87,20 +87,32 @@ func Decode(data []byte) (p *Block, rest []byte) { + // pemStart begins with a newline. However, at the very beginning of + // the byte array, we'll accept the start string without it. + rest = data ++ + for { +- if bytes.HasPrefix(rest, pemStart[1:]) { +- rest = rest[len(pemStart)-1:] +- } else if i := bytes.Index(rest, pemStart); i >= 0 { +- rest = rest[i+len(pemStart) : len(rest)] +- } else { ++ // Find the first END line, and then find the last BEGIN line before ++ // the end line. This lets us skip any repeated BEGIN lines that don't ++ // have a matching END. ++ endIndex := bytes.Index(rest, pemEnd) ++ if endIndex < 0 { ++ return nil, data ++ } ++ endTrailerIndex := endIndex + len(pemEnd) ++ beginIndex := bytes.LastIndex(rest[:endIndex], pemStart[1:]) ++ if beginIndex < 0 || beginIndex > 0 && rest[beginIndex-1] != '\n' { + return nil, data + } ++ rest = rest[beginIndex+len(pemStart)-1:] ++ endIndex -= beginIndex + len(pemStart) - 1 ++ endTrailerIndex -= beginIndex + len(pemStart) - 1 + + var typeLine []byte +- typeLine, rest = getLine(rest) ++ var consumed int ++ typeLine, rest, consumed = getLine(rest) + if !bytes.HasSuffix(typeLine, pemEndOfLine) { + continue + } ++ endIndex -= consumed ++ endTrailerIndex -= consumed + typeLine = typeLine[0 : len(typeLine)-len(pemEndOfLine)] + + p = &Block{ +@@ -114,7 +126,7 @@ func Decode(data []byte) (p *Block, rest []byte) { + if len(rest) == 0 { + return nil, data + } +- line, next := getLine(rest) ++ line, next, consumed := getLine(rest) + + i := bytes.IndexByte(line, ':') + if i == -1 { +@@ -127,21 +139,13 @@ func Decode(data []byte) (p *Block, rest []byte) { + val = bytes.TrimSpace(val) + p.Headers[string(key)] = string(val) + rest = next ++ endIndex -= consumed ++ endTrailerIndex -= consumed + } + +- var endIndex, endTrailerIndex int +- +- // If there were no headers, the END line might occur +- // immediately, without a leading newline. +- if len(p.Headers) == 0 && bytes.HasPrefix(rest, pemEnd[1:]) { +- endIndex = 0 +- endTrailerIndex = len(pemEnd) - 1 +- } else { +- endIndex = bytes.Index(rest, pemEnd) +- endTrailerIndex = endIndex + len(pemEnd) +- } +- +- if endIndex < 0 { ++ // If there were headers, there must be a newline between the headers ++ // and the END line, so endIndex should be >= 0. ++ if len(p.Headers) > 0 && endIndex < 0 { + continue + } + +@@ -161,21 +165,24 @@ func Decode(data []byte) (p *Block, rest []byte) { + } + + // The line must end with only whitespace. +- if s, _ := getLine(restOfEndLine); len(s) != 0 { ++ if s, _, _ := getLine(restOfEndLine); len(s) != 0 { + continue + } + +- base64Data := removeSpacesAndTabs(rest[:endIndex]) +- p.Bytes = make([]byte, base64.StdEncoding.DecodedLen(len(base64Data))) +- n, err := base64.StdEncoding.Decode(p.Bytes, base64Data) +- if err != nil { +- continue ++ p.Bytes = []byte{} ++ if endIndex > 0 { ++ base64Data := removeSpacesAndTabs(rest[:endIndex]) ++ p.Bytes = make([]byte, base64.StdEncoding.DecodedLen(len(base64Data))) ++ n, err := base64.StdEncoding.Decode(p.Bytes, base64Data) ++ if err != nil { ++ continue ++ } ++ p.Bytes = p.Bytes[:n] + } +- p.Bytes = p.Bytes[:n] + + // the -1 is because we might have only matched pemEnd without the + // leading newline if the PEM block was empty. +- _, rest = getLine(rest[endIndex+len(pemEnd)-1:]) ++ _, rest, _ = getLine(rest[endIndex+len(pemEnd)-1:]) + return p, rest + } + } +diff --git a/src/encoding/pem/pem_test.go b/src/encoding/pem/pem_test.go +index 4485581..cecb8ea 100644 +--- a/src/encoding/pem/pem_test.go ++++ b/src/encoding/pem/pem_test.go +@@ -34,7 +34,7 @@ var getLineTests = []GetLineTest{ + + func TestGetLine(t *testing.T) { + for i, test := range getLineTests { +- x, y := getLine([]byte(test.in)) ++ x, y, _ := getLine([]byte(test.in)) + if string(x) != test.out1 || string(y) != test.out2 { + t.Errorf("#%d got:%+v,%+v want:%s,%s", i, x, y, test.out1, test.out2) + } +@@ -46,6 +46,7 @@ func TestDecode(t *testing.T) { + if !reflect.DeepEqual(result, certificate) { + t.Errorf("#0 got:%#v want:%#v", result, certificate) + } ++ + result, remainder = Decode(remainder) + if !reflect.DeepEqual(result, privateKey) { + t.Errorf("#1 got:%#v want:%#v", result, privateKey) +@@ -68,7 +69,7 @@ func TestDecode(t *testing.T) { + } + + result, remainder = Decode(remainder) +- if result == nil || result.Type != "HEADERS" || len(result.Headers) != 1 { ++ if result == nil || result.Type != "VALID HEADERS" || len(result.Headers) != 1 { + t.Errorf("#5 expected single header block but got :%v", result) + } + +@@ -381,15 +382,15 @@ ZWAaUoVtWIQ52aKS0p19G99hhb+IVANC4akkdHV4SP8i7MVNZhfUmg== + + # This shouldn't be recognised because of the missing newline after the + headers. +------BEGIN HEADERS----- ++-----BEGIN INVALID HEADERS----- + Header: 1 +------END HEADERS----- ++-----END INVALID HEADERS----- + + # This should be valid, however. +------BEGIN HEADERS----- ++-----BEGIN VALID HEADERS----- + Header: 1 + +------END HEADERS-----`) ++-----END VALID HEADERS-----`) + + var certificate = &Block{Type: "CERTIFICATE", + Headers: map[string]string{}, +-- +2.33.0 diff --git a/golang.spec b/golang.spec index bb1ceab..19a9f0e 100644 --- a/golang.spec +++ b/golang.spec @@ -58,7 +58,7 @@ Name: golang Version: 1.15.7 -Release: 55 +Release: 56 Summary: The Go Programming Language License: BSD and Public Domain URL: https://golang.org/ @@ -277,6 +277,9 @@ Patch6132: 0132-backport-CVE-2025-22871-net-http-reject-newlines-in-chunk-siz.p Patch6133: 0133-CVE-2025-58183-archive-tar-set-a-limit-on-the.patch Patch6134: 0134-CVE-2025-61724-net-textproto-avoid-quadratic-complexity.patch Patch6135: 0135-CVE-2025-58185-encoding-asn1-prevent-memory-exhaustion.patch +Patch6136: 0136-CVE-2025-58187-crypto-x509-improve-domain-name-verification.patch +Patch6137: 0137-CVE-2025-58187-crypto-x509-rework-fix-for-CVE-2025-58187.patch +Patch6138: 0138-CVE-2025-61723-encoding-pem-make-Decode-complexity-linear.patch Patch9002: 0002-fix-patch-cmd-go-internal-modfetch-do-not-sho.patch @@ -515,6 +518,12 @@ fi %files devel -f go-tests.list -f go-misc.list -f go-src.list %changelog +* Fri Nov 14 2025 zhaoyifan - 1.15.7-56 +- Type:CVE +- CVE:CVE-2025-58187,CVE-2025-61723 +- SUG:NA +- DESC:fix CVE-2025-58187,CVE-2025-61723 + * Sat Nov 8 2025 huzhangying - 1.15.7-55 - Type:CVE - CVE:CVE-2025-58183, CVE-2025-58185, CVE-2025-61724 -- Gitee