Multiple appends without pre-allocation; use make() with capacity when size is known
ipv4 = append(ipv4, n)
1// Copyright 2025 The Go Authors. All rights reserved.2// Use of this source code is governed by a BSD-style3// license that can be found in the LICENSE file.45package x50967import (8 "bytes"9 "fmt"10 "net"11 "net/netip"12 "net/url"13 "slices"14 "strings"15)1617// This file contains the data structures and functions necessary for18// efficiently checking X.509 name constraints. The method for constraint19// checking implemented in this file is based on a technique originally20// described by davidben@google.com.21//22// The basic concept is based on the fact that constraints describe possibly23// overlapping subtrees that we need to match against. If sorted in lexicographic24// order, and then pruned, removing any subtrees that overlap with preceding25// subtrees, a simple binary search can be used to find the nearest matching26// prefix. This reduces the complexity of name constraint checking from27// quadratic to log linear complexity.28//29// A close reading of RFC 5280 may suggest that constraints could also be30// implemented as a trie (or radix tree), which would present the possibility of31// doing construction and matching in linear time, but the memory cost of32// implementing them is actually quite high, and in the worst case (where each33// node has a high number of children) can be abused to require a program to use34// significant amounts of memory. The log linear approach taken here is35// extremely cheap in terms of memory because we directly alias the already36// parsed constraints, thus avoiding the need to do significant additional37// allocations.38//39// The basic data structure is nameConstraintsSet, which implements the sorting,40// pruning, and querying of the prefix sets.41//42// In order to check IP, DNS, URI, and email constraints, we need to use two43// different techniques, one for IP addresses, which is quite simple, and one44// for DNS names, which additionally compose the portions of URIs and emails we45// care about (technically we also need some special logic for email addresses46// as well for when constraints comprise of full email addresses) which is47// slightly more complex.48//49// IP addresses use two nameConstraintsSets, one for IPv4 addresses and one for50// IPv6 addresses, with no additional logic.51//52// DNS names require some extra logic in order to handle the distinctions53// between permitted and excluded subtrees, as well as for wildcards, and the54// semantics of leading period constraints (i.e. '.example.com'). This logic is55// implemented in the dnsConstraints type.56//57// Email addresses also require some additional logic, which does not make use58// of nameConstraintsSet, to handle constraints which define full email59// addresses (i.e. 'test@example.com'). For bare domain constraints, we use the60// dnsConstraints type described above, querying the domain portion of the email61// address. For full email addresses, we also hold a map of email addresses with62// the domain portion of the email lowercased, since it is case insensitive. When63// looking up an email address in the constraint set, we first check the full64// email address map, and if we don't find anything, we check the domain portion65// of the email address against the dnsConstraints.6667type nameConstraintsSet[T *net.IPNet | string, V net.IP | string] struct {68 set []T69}7071// sortAndPrune sorts the constraints using the provided comparison function, and then72// prunes any constraints that are subsets of preceding constraints using the73// provided subset function.74func (nc *nameConstraintsSet[T, V]) sortAndPrune(cmp func(T, T) int, subset func(T, T) bool) {75 if len(nc.set) < 2 {76 return77 }7879 slices.SortFunc(nc.set, cmp)8081 if len(nc.set) < 2 {82 return83 }84 writeIndex := 185 for readIndex := 1; readIndex < len(nc.set); readIndex++ {86 if !subset(nc.set[writeIndex-1], nc.set[readIndex]) {87 nc.set[writeIndex] = nc.set[readIndex]88 writeIndex++89 }90 }91 nc.set = nc.set[:writeIndex]92}9394// search does a binary search over the constraints set for the provided value95// s, using the provided comparison function cmp to find the lower bound, and96// the match function to determine if the found constraint is a prefix of s. If97// a matching constraint is found, it is returned along with true. If no98// matching constraint is found, the zero value of T and false are returned.99func (nc *nameConstraintsSet[T, V]) search(s V, cmp func(T, V) int, match func(T, V) bool) (lowerBound T, exactMatch bool) {100 if len(nc.set) == 0 {101 return lowerBound, false102 }103 // Look for the lower bound of s in the set.104 i, found := slices.BinarySearchFunc(nc.set, s, cmp)105 // If we found an exact match, return it106 if found {107 return nc.set[i], true108 }109110 if i < 0 {111 return lowerBound, false112 }113114 var constraint T115 if i == 0 {116 constraint = nc.set[0]117 } else {118 constraint = nc.set[i-1]119 }120 if match(constraint, s) {121 return constraint, true122 }123 return lowerBound, false124}125126func ipNetworkSubset(a, b *net.IPNet) bool {127 if !a.Contains(b.IP) {128 return false129 }130 broadcast := make(net.IP, len(b.IP))131 for i := range b.IP {132 broadcast[i] = b.IP[i] | (^b.Mask[i])133 }134 return a.Contains(broadcast)135}136137func ipNetworkCompare(a, b *net.IPNet) int {138 i := bytes.Compare(a.IP, b.IP)139 if i != 0 {140 return i141 }142 return bytes.Compare(a.Mask, b.Mask)143}144145func ipBinarySearch(constraint *net.IPNet, target net.IP) int {146 return bytes.Compare(constraint.IP, target)147}148149func ipMatch(constraint *net.IPNet, target net.IP) bool {150 return constraint.Contains(target)151}152153type ipConstraints struct {154 // NOTE: we could store IP network prefixes as a pre-processed byte slice155 // (i.e. by masking the IP) and doing the byte prefix checking using faster156 // techniques, but this would require allocating new byte slices, which is157 // likely significantly more expensive than just operating on the158 // pre-allocated *net.IPNet and net.IP objects directly.159160 ipv4 *nameConstraintsSet[*net.IPNet, net.IP]161 ipv6 *nameConstraintsSet[*net.IPNet, net.IP]162}163164func newIPNetConstraints(l []*net.IPNet) interface {165 query(net.IP) (*net.IPNet, bool)166} {167 if len(l) == 0 {168 return nil169 }170 var ipv4, ipv6 []*net.IPNet171 for _, n := range l {172 // Subtrees may carry non-zero host bits. Sort and search need the masked173 // network address, so use a copy and leave the parsed constraint as encoded.174 if masked := n.IP.Mask(n.Mask); masked != nil && !masked.Equal(n.IP) {175 n = &net.IPNet{IP: masked, Mask: n.Mask}176 }177 if len(n.IP) == net.IPv4len {178 ipv4 = append(ipv4, n)179 } else {180 ipv6 = append(ipv6, n)181 }182 }183 var v4c, v6c *nameConstraintsSet[*net.IPNet, net.IP]184 if len(ipv4) > 0 {185 v4c = &nameConstraintsSet[*net.IPNet, net.IP]{186 set: ipv4,187 }188 v4c.sortAndPrune(ipNetworkCompare, ipNetworkSubset)189 }190 if len(ipv6) > 0 {191 v6c = &nameConstraintsSet[*net.IPNet, net.IP]{192 set: ipv6,193 }194 v6c.sortAndPrune(ipNetworkCompare, ipNetworkSubset)195 }196 return &ipConstraints{ipv4: v4c, ipv6: v6c}197}198199func (ipc *ipConstraints) query(ip net.IP) (*net.IPNet, bool) {200 var c *nameConstraintsSet[*net.IPNet, net.IP]201 if len(ip) == net.IPv4len {202 c = ipc.ipv4203 } else {204 c = ipc.ipv6205 }206 if c == nil {207 return nil, false208 }209 return c.search(ip, ipBinarySearch, ipMatch)210}211212// dnsHasSuffix case-insensitively checks if DNS name b is a label suffix of DNS213// name a, meaning that example.com is not considered a suffix of214// testexample.com, but is a suffix of test.example.com.215//216// dnsHasSuffix supports the URI "leading period" constraint semantics, which217// while not explicitly defined for dNSNames in RFC 5280, are widely supported218// (see errata 5997). In particular, a constraint of ".example.com" is219// considered to only match subdomains of example.com, but not example.com220// itself.221//222// a and b must both be non-empty strings representing (mostly) valid DNS names.223func dnsHasSuffix(a, b string) bool {224 lenA := len(a)225 lenB := len(b)226 if lenA > lenB {227 return false228 }229 i := lenA - 1230 offset := lenA - lenB231 for ; i >= 0; i-- {232 ar, br := a[i], b[i-(offset)]233 if ar == br {234 continue235 }236 if br < ar {237 ar, br = br, ar238 }239 if 'A' <= ar && ar <= 'Z' && br == ar+'a'-'A' {240 continue241 }242 return false243 }244245 if a[0] != '.' && lenB > lenA && b[lenB-lenA-1] != '.' {246 return false247 }248249 return true250}251252// dnsCompareTable contains the ASCII alphabet mapped from a characters index in253// the table to its lowercased form.254var dnsCompareTable [256]byte255256func init() {257 // NOTE: we don't actually need the258 // full alphabet, but calculating offsets would be more expensive than just259 // having redundant characters.260 for i := 0; i < 256; i++ {261 c := byte(i)262 if 'A' <= c && c <= 'Z' {263 // Lowercase uppercase characters A-Z.264 c += 'a' - 'A'265 }266 dnsCompareTable[i] = c267 }268 // Set the period character to 0 so that we get the right sorting behavior.269 //270 // In particular, we need the period character to sort before the only271 // other valid DNS name character which isn't a-z or 0-9, the hyphen,272 // otherwise a name with a dash would be incorrectly sorted into the middle273 // of another tree.274 //275 // For example, imagine a certificate with the constraints "a.com", "a.a.com", and276 // "a-a.com". These would sort as "a.com", "a-a.com", "a.a.com", which would break277 // the pruning step since we wouldn't see that "a.a.com" is a subset of "a.com".278 // Sorting the period before the hyphen ensures that "a.a.com" sorts before "a-a.com".279 dnsCompareTable['.'] = 0280}281282// dnsCompare is a case-insensitive reversed implementation of strings.Compare283// that operates from the end to the start of the strings. This is more284// efficient that allocating reversed version of a and b and using285// strings.Compare directly (even though it is highly optimized).286//287// NOTE: this function treats the period character ('.') as sorting above every288// other character, which is necessary for us to properly sort names into their289// correct order. This is further discussed in the init function above.290func dnsCompare(a, b string) int {291 idxA := len(a) - 1292 idxB := len(b) - 1293294 for idxA >= 0 && idxB >= 0 {295 byteA := dnsCompareTable[a[idxA]]296 byteB := dnsCompareTable[b[idxB]]297 if byteA == byteB {298 idxA--299 idxB--300 continue301 }302 ret := 1303 if byteA < byteB {304 ret = -1305 }306 return ret307 }308309 ret := 0310 if idxA < idxB {311 ret = -1312 } else if idxB < idxA {313 ret = 1314 }315 return ret316}317318type dnsConstraints struct {319 // all lets us short circuit the query logic if we see a zero length320 // constraint which permits or excludes everything.321 all bool322323 // permitted indicates if these constraints are for permitted or excluded324 // names.325 permitted bool326327 constraints *nameConstraintsSet[string, string]328329 // parentConstraints contains a subset of constraints which are used for330 // wildcard SAN queries, which are constructed by removing the first label331 // from the constraints in constraints. parentConstraints is only populated332 // if permitted is false.333 parentConstraints map[string]string334}335336func newDNSConstraints(l []string, permitted bool) interface{ query(string) (string, bool) } {337 if len(l) == 0 {338 return nil339 }340 for _, n := range l {341 if len(n) == 0 {342 return &dnsConstraints{all: true}343 }344 }345 constraints := slices.Clone(l)346347 nc := &dnsConstraints{348 constraints: &nameConstraintsSet[string, string]{349 set: constraints,350 },351 permitted: permitted,352 }353354 nc.constraints.sortAndPrune(dnsCompare, dnsHasSuffix)355356 if !permitted {357 parentConstraints := map[string]string{}358 for _, name := range nc.constraints.set {359 name = strings.ToLower(name)360 trimmedName := trimFirstLabel(name)361 if trimmedName == "" {362 continue363 }364 parentConstraints[trimmedName] = name365 }366 if len(parentConstraints) > 0 {367 nc.parentConstraints = parentConstraints368 }369 }370371 return nc372}373374func (dnc *dnsConstraints) query(s string) (string, bool) {375 if dnc.all {376 return "", true377 }378379 constraint, match := dnc.constraints.search(s, dnsCompare, dnsHasSuffix)380 if match {381 return constraint, true382 }383384 if !dnc.permitted && len(s) > 0 && s[0] == '*' {385 s = strings.ToLower(s)386 trimmed := trimFirstLabel(s)387 if constraint, found := dnc.parentConstraints[trimmed]; found {388 return constraint, true389 }390 }391 return "", false392}393394type emailConstraints struct {395 dnsConstraints interface{ query(string) (string, bool) }396397 // fullEmails is map of rfc2821Mailboxs that are fully specified in the398 // constraints, which we need to check for separately since they don't399 // follow the same matching rules as the domain-based constraints. The400 // domain portion of the rfc2821Mailbox has been lowercased, since the401 // domain portion is case insensitive. When checking the map for an email,402 // the domain portion of the query should also be lowercased.403 fullEmails map[rfc2821Mailbox]struct{}404}405406func newEmailConstraints(l []string, permitted bool) interface {407 query(rfc2821Mailbox) (string, bool)408} {409 if len(l) == 0 {410 return nil411 }412 exactMap := map[rfc2821Mailbox]struct{}{}413 var domains []string414 for _, c := range l {415 if !strings.ContainsRune(c, '@') {416 domains = append(domains, c)417 continue418 }419 parsed, ok := parseRFC2821Mailbox(c)420 if !ok {421 // We've already parsed these addresses in parseCertificate, and422 // treat failures as a hard failure for parsing. The only way we can423 // get a parse failure here is if the caller has mutated the424 // certificate since parsing.425 continue426 }427 parsed.domain = strings.ToLower(parsed.domain)428 exactMap[parsed] = struct{}{}429 }430 ec := &emailConstraints{431 fullEmails: exactMap,432 }433 if len(domains) > 0 {434 ec.dnsConstraints = newDNSConstraints(domains, permitted)435 }436 return ec437}438439func (ec *emailConstraints) query(s rfc2821Mailbox) (string, bool) {440 if len(ec.fullEmails) > 0 {441 if _, ok := ec.fullEmails[s]; ok {442 return fmt.Sprintf("%s@%s", s.local, s.domain), true443 }444 }445 if ec.dnsConstraints == nil {446 return "", false447 }448 constraint, found := ec.dnsConstraints.query(s.domain)449 return constraint, found450}451452type constraints[T any, V any] struct {453 constraintType string454 permitted interface{ query(V) (T, bool) }455 excluded interface{ query(V) (T, bool) }456}457458func checkConstraints[T string | *net.IPNet, V any, P string | net.IP | parsedURI | rfc2821Mailbox](c constraints[T, V], s V, p P) error {459 if c.permitted != nil {460 if _, found := c.permitted.query(s); !found {461 return fmt.Errorf("%s %q is not permitted by any constraint", c.constraintType, p)462 }463 }464 if c.excluded != nil {465 if constraint, found := c.excluded.query(s); found {466 return fmt.Errorf("%s %q is excluded by constraint %q", c.constraintType, p, constraint)467 }468 }469 return nil470}471472type chainConstraints struct {473 ip constraints[*net.IPNet, net.IP]474 dns constraints[string, string]475 uri constraints[string, string]476 email constraints[string, rfc2821Mailbox]477478 index int479 next *chainConstraints480}481482func (cc *chainConstraints) check(dns []string, uris []parsedURI, emails []rfc2821Mailbox, ips []net.IP) error {483 for _, ip := range ips {484 if err := checkConstraints(cc.ip, ip, ip); err != nil {485 return err486 }487 }488 for _, d := range dns {489 if !domainNameValid(d, false) {490 return fmt.Errorf("x509: cannot parse dnsName %q", d)491 }492 if err := checkConstraints(cc.dns, d, d); err != nil {493 return err494 }495 }496 for _, u := range uris {497 if !domainNameValid(u.domain, false) {498 return fmt.Errorf("x509: internal error: URI SAN %q failed to parse", u)499 }500 if err := checkConstraints(cc.uri, u.domain, u); err != nil {501 return err502 }503 }504 for _, e := range emails {505 if !domainNameValid(e.domain, false) {506 return fmt.Errorf("x509: cannot parse rfc822Name %q", e)507 }508 if err := checkConstraints(cc.email, e, e); err != nil {509 return err510 }511 }512 return nil513}514515func checkChainConstraints(chain []*Certificate) error {516 var currentConstraints *chainConstraints517 var last *chainConstraints518 for i, c := range chain {519 if !c.hasNameConstraints() {520 continue521 }522 cc := &chainConstraints{523 ip: constraints[*net.IPNet, net.IP]{"IP address", newIPNetConstraints(c.PermittedIPRanges), newIPNetConstraints(c.ExcludedIPRanges)},524 dns: constraints[string, string]{"DNS name", newDNSConstraints(c.PermittedDNSDomains, true), newDNSConstraints(c.ExcludedDNSDomains, false)},525 uri: constraints[string, string]{"URI", newDNSConstraints(c.PermittedURIDomains, true), newDNSConstraints(c.ExcludedURIDomains, false)},526 email: constraints[string, rfc2821Mailbox]{"email address", newEmailConstraints(c.PermittedEmailAddresses, true), newEmailConstraints(c.ExcludedEmailAddresses, false)},527 index: i,528 }529 if currentConstraints == nil {530 currentConstraints = cc531 last = cc532 } else if last != nil {533 last.next = cc534 last = cc535 }536 }537 if currentConstraints == nil {538 return nil539 }540541 for i, c := range chain {542 if !c.hasSANExtension() {543 continue544 }545 if i >= currentConstraints.index {546 for currentConstraints.index <= i {547 if currentConstraints.next == nil {548 return nil549 }550 currentConstraints = currentConstraints.next551 }552 }553554 uris, err := parseURIs(c.URIs)555 if err != nil {556 return err557 }558 emails, err := parseMailboxes(c.EmailAddresses)559 if err != nil {560 return err561 }562563 for n := currentConstraints; n != nil; n = n.next {564 if err := n.check(c.DNSNames, uris, emails, c.IPAddresses); err != nil {565 return err566 }567 }568 }569570 return nil571}572573type parsedURI struct {574 uri *url.URL575 domain string576}577578func (u parsedURI) String() string {579 return u.uri.String()580}581582func parseURIs(uris []*url.URL) ([]parsedURI, error) {583 parsed := make([]parsedURI, 0, len(uris))584 for _, uri := range uris {585 host := strings.ToLower(uri.Host)586 if len(host) == 0 {587 return nil, fmt.Errorf("URI with empty host (%q) cannot be matched against constraints", uri.String())588 }589 if strings.Contains(host, ":") && !strings.HasSuffix(host, "]") {590 var err error591 host, _, err = net.SplitHostPort(uri.Host)592 if err != nil {593 return nil, fmt.Errorf("cannot parse URI host %q: %v", uri.Host, err)594 }595 }596597 // netip.ParseAddr will reject the URI IPv6 literal form "[...]", so we598 // check if _either_ the string parses as an IP, or if it is enclosed in599 // square brackets.600 if _, err := netip.ParseAddr(host); err == nil || (strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]")) {601 return nil, fmt.Errorf("URI with IP (%q) cannot be matched against constraints", uri.String())602 }603604 parsed = append(parsed, parsedURI{uri, host})605 }606 return parsed, nil607}608609func parseMailboxes(emails []string) ([]rfc2821Mailbox, error) {610 parsed := make([]rfc2821Mailbox, 0, len(emails))611 for _, email := range emails {612 mailbox, ok := parseRFC2821Mailbox(email)613 if !ok {614 return nil, fmt.Errorf("cannot parse rfc822Name %q", email)615 }616 mailbox.domain = strings.ToLower(mailbox.domain)617 parsed = append(parsed, mailbox)618 }619 return parsed, nil620}621622func trimFirstLabel(dnsName string) string {623 firstDotInd := strings.IndexByte(dnsName, '.')624 if firstDotInd < 0 {625 // Constraint is a single label, we cannot trim it.626 return ""627 }628 return dnsName[firstDotInd:]629}
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.