// Copyright 2025 The Go Authors. All rights reserved.// Use of this source code is governed by a BSD-style// license that can be found in the LICENSE file.package x509import ()// This file contains the data structures and functions necessary for// efficiently checking X.509 name constraints. The method for constraint// checking implemented in this file is based on a technique originally// described by davidben@google.com.//// The basic concept is based on the fact that constraints describe possibly// overlapping subtrees that we need to match against. If sorted in lexicographic// order, and then pruned, removing any subtrees that overlap with preceding// subtrees, a simple binary search can be used to find the nearest matching// prefix. This reduces the complexity of name constraint checking from// quadratic to log linear complexity.//// A close reading of RFC 5280 may suggest that constraints could also be// implemented as a trie (or radix tree), which would present the possibility of// doing construction and matching in linear time, but the memory cost of// implementing them is actually quite high, and in the worst case (where each// node has a high number of children) can be abused to require a program to use// significant amounts of memory. The log linear approach taken here is// extremely cheap in terms of memory because we directly alias the already// parsed constraints, thus avoiding the need to do significant additional// allocations.//// The basic data structure is nameConstraintsSet, which implements the sorting,// pruning, and querying of the prefix sets.//// In order to check IP, DNS, URI, and email constraints, we need to use two// different techniques, one for IP addresses, which is quite simple, and one// for DNS names, which additionally compose the portions of URIs and emails we// care about (technically we also need some special logic for email addresses// as well for when constraints comprise of full email addresses) which is// slightly more complex.//// IP addresses use two nameConstraintsSets, one for IPv4 addresses and one for// IPv6 addresses, with no additional logic.//// DNS names require some extra logic in order to handle the distinctions// between permitted and excluded subtrees, as well as for wildcards, and the// semantics of leading period constraints (i.e. '.example.com'). This logic is// implemented in the dnsConstraints type.//// Email addresses also require some additional logic, which does not make use// of nameConstraintsSet, to handle constraints which define full email// addresses (i.e. 'test@example.com'). For bare domain constraints, we use the// dnsConstraints type described above, querying the domain portion of the email// address. For full email addresses, we also hold a map of email addresses with// the domain portion of the email lowercased, since it is case insensitive. When// looking up an email address in the constraint set, we first check the full// email address map, and if we don't find anything, we check the domain portion// of the email address against the dnsConstraints.typenameConstraintsSet[ *net.IPNet | string, net.IP | string] struct {set []}// sortAndPrune sorts the constraints using the provided comparison function, and then// prunes any constraints that are subsets of preceding constraints using the// provided subset function.func ( *nameConstraintsSet[, ]) ( func(, ) int, func(, ) bool) {iflen(.set) < 2 {return }slices.SortFunc(.set, )iflen(.set) < 2 {return } := 1for := 1; < len(.set); ++ {if !(.set[-1], .set[]) { .set[] = .set[] ++ } } .set = .set[:]}// search does a binary search over the constraints set for the provided value// s, using the provided comparison function cmp to find the lower bound, and// the match function to determine if the found constraint is a prefix of s. If// a matching constraint is found, it is returned along with true. If no// matching constraint is found, the zero value of T and false are returned.func ( *nameConstraintsSet[, ]) ( , func(, ) int, func(, ) bool) ( , bool) {iflen(.set) == 0 {return , false }// Look for the lower bound of s in the set. , := slices.BinarySearchFunc(.set, , )// If we found an exact match, return itif {return .set[], true }if < 0 {return , false }varif == 0 { = .set[0] } else { = .set[-1] }if (, ) {return , true }return , false}func (, *net.IPNet) bool {if !.Contains(.IP) {returnfalse } := make(net.IP, len(.IP))for := range .IP { [] = .IP[] | (^.Mask[]) }return .Contains()}func (, *net.IPNet) int { := bytes.Compare(.IP, .IP)if != 0 {return }returnbytes.Compare(.Mask, .Mask)}func ( *net.IPNet, net.IP) int {returnbytes.Compare(.IP, )}func ( *net.IPNet, net.IP) bool {return .Contains()}typeipConstraintsstruct {// NOTE: we could store IP network prefixes as a pre-processed byte slice // (i.e. by masking the IP) and doing the byte prefix checking using faster // techniques, but this would require allocating new byte slices, which is // likely significantly more expensive than just operating on the // pre-allocated *net.IPNet and net.IP objects directly.ipv4 *nameConstraintsSet[*net.IPNet, net.IP]ipv6 *nameConstraintsSet[*net.IPNet, net.IP]}func ( []*net.IPNet) interface { (net.IP) (*net.IPNet, bool)} {iflen() == 0 {returnnil }var , []*net.IPNetfor , := range {iflen(.IP) == net.IPv4len { = append(, ) } else { = append(, ) } }var , *nameConstraintsSet[*net.IPNet, net.IP]iflen() > 0 { = &nameConstraintsSet[*net.IPNet, net.IP]{set: , } .sortAndPrune(ipNetworkCompare, ipNetworkSubset) }iflen() > 0 { = &nameConstraintsSet[*net.IPNet, net.IP]{set: , } .sortAndPrune(ipNetworkCompare, ipNetworkSubset) }return &ipConstraints{ipv4: , ipv6: }}func ( *ipConstraints) ( net.IP) (*net.IPNet, bool) {var *nameConstraintsSet[*net.IPNet, net.IP]iflen() == net.IPv4len { = .ipv4 } else { = .ipv6 }if == nil {returnnil, false }return .search(, ipBinarySearch, ipMatch)}// dnsHasSuffix case-insensitively checks if DNS name b is a label suffix of DNS// name a, meaning that example.com is not considered a suffix of// testexample.com, but is a suffix of test.example.com.//// dnsHasSuffix supports the URI "leading period" constraint semantics, which// while not explicitly defined for dNSNames in RFC 5280, are widely supported// (see errata 5997). In particular, a constraint of ".example.com" is// considered to only match subdomains of example.com, but not example.com// itself.//// a and b must both be non-empty strings representing (mostly) valid DNS names.func (, string) bool { := len() := len()if > {returnfalse } := - 1 := - for ; >= 0; -- { , := [], [-()]if == {continue }if < { , = , }if'A' <= && <= 'Z' && == +'a'-'A' {continue }returnfalse }if [0] != '.' && > && [--1] != '.' {returnfalse }returntrue}// dnsCompareTable contains the ASCII alphabet mapped from a characters index in// the table to its lowercased form.vardnsCompareTable [256]bytefunc () {// NOTE: we don't actually need the // full alphabet, but calculating offsets would be more expensive than just // having redundant characters.for := 0; < 256; ++ { := byte()if'A' <= && <= 'Z' {// Lowercase uppercase characters A-Z. += 'a' - 'A' }dnsCompareTable[] = }// Set the period character to 0 so that we get the right sorting behavior. // // In particular, we need the period character to sort before the only // other valid DNS name character which isn't a-z or 0-9, the hyphen, // otherwise a name with a dash would be incorrectly sorted into the middle // of another tree. // // For example, imagine a certificate with the constraints "a.com", "a.a.com", and // "a-a.com". These would sort as "a.com", "a-a.com", "a.a.com", which would break // the pruning step since we wouldn't see that "a.a.com" is a subset of "a.com". // Sorting the period before the hyphen ensures that "a.a.com" sorts before "a-a.com".dnsCompareTable['.'] = 0}// dnsCompare is a case-insensitive reversed implementation of strings.Compare// that operates from the end to the start of the strings. This is more// efficient that allocating reversed version of a and b and using// strings.Compare directly (even though it is highly optimized).//// NOTE: this function treats the period character ('.') as sorting above every// other character, which is necessary for us to properly sort names into their// correct order. This is further discussed in the init function above.func (, string) int { := len() - 1 := len() - 1for >= 0 && >= 0 { := dnsCompareTable[[]] := dnsCompareTable[[]]if == { -- --continue } := 1if < { = -1 }return } := 0if < { = -1 } elseif < { = 1 }return}typednsConstraintsstruct {// all lets us short circuit the query logic if we see a zero length // constraint which permits or excludes everything.allbool// permitted indicates if these constraints are for permitted or excluded // names.permittedboolconstraints *nameConstraintsSet[string, string]// parentConstraints contains a subset of constraints which are used for // wildcard SAN queries, which are constructed by removing the first label // from the constraints in constraints. parentConstraints is only populated // if permitted is false.parentConstraintsmap[string]string}func ( []string, bool) interface{ (string) (string, bool) } {iflen() == 0 {returnnil }for , := range {iflen() == 0 {return &dnsConstraints{all: true} } } := slices.Clone() := &dnsConstraints{constraints: &nameConstraintsSet[string, string]{set: , },permitted: , } .constraints.sortAndPrune(dnsCompare, dnsHasSuffix)if ! { := map[string]string{}for , := range .constraints.set { = strings.ToLower() := trimFirstLabel()if == "" {continue } [] = }iflen() > 0 { .parentConstraints = } }return}func ( *dnsConstraints) ( string) (string, bool) {if .all {return"", true } , := .constraints.search(, dnsCompare, dnsHasSuffix)if {return , true }if !.permitted && len() > 0 && [0] == '*' { = strings.ToLower() := trimFirstLabel()if , := .parentConstraints[]; {return , true } }return"", false}typeemailConstraintsstruct {dnsConstraintsinterface{ query(string) (string, bool) }// fullEmails is map of rfc2821Mailboxs that are fully specified in the // constraints, which we need to check for separately since they don't // follow the same matching rules as the domain-based constraints. The // domain portion of the rfc2821Mailbox has been lowercased, since the // domain portion is case insensitive. When checking the map for an email, // the domain portion of the query should also be lowercased.fullEmailsmap[rfc2821Mailbox]struct{}}func ( []string, bool) interface { (rfc2821Mailbox) (string, bool)} {iflen() == 0 {returnnil } := map[rfc2821Mailbox]struct{}{}var []stringfor , := range {if !strings.ContainsRune(, '@') { = append(, )continue } , := parseRFC2821Mailbox()if ! {// We've already parsed these addresses in parseCertificate, and // treat failures as a hard failure for parsing. The only way we can // get a parse failure here is if the caller has mutated the // certificate since parsing.continue } .domain = strings.ToLower(.domain) [] = struct{}{} } := &emailConstraints{fullEmails: , }iflen() > 0 { .dnsConstraints = newDNSConstraints(, ) }return}func ( *emailConstraints) ( rfc2821Mailbox) (string, bool) {iflen(.fullEmails) > 0 {if , := .fullEmails[]; {returnfmt.Sprintf("%s@%s", .local, .domain), true } }if .dnsConstraints == nil {return"", false } , := .dnsConstraints.query(.domain)return , }typeconstraints[ any, any] struct {constraintTypestringpermittedinterface{ query() (, bool) }excludedinterface{ query() (, bool) }}func [ string | *net.IPNet, any, string | net.IP | parsedURI | rfc2821Mailbox]( constraints[, ], , ) error {if .permitted != nil {if , := .permitted.query(); ! {returnfmt.Errorf("%s %q is not permitted by any constraint", .constraintType, ) } }if .excluded != nil {if , := .excluded.query(); {returnfmt.Errorf("%s %q is excluded by constraint %q", .constraintType, , ) } }returnnil}typechainConstraintsstruct {ipconstraints[*net.IPNet, net.IP]dnsconstraints[string, string]uriconstraints[string, string]emailconstraints[string, rfc2821Mailbox]indexintnext *chainConstraints}func ( *chainConstraints) ( []string, []parsedURI, []rfc2821Mailbox, []net.IP) error {for , := range {if := checkConstraints(.ip, , ); != nil {return } }for , := range {if !domainNameValid(, false) {returnfmt.Errorf("x509: cannot parse dnsName %q", ) }if := checkConstraints(.dns, , ); != nil {return } }for , := range {if !domainNameValid(.domain, false) {returnfmt.Errorf("x509: internal error: URI SAN %q failed to parse", ) }if := checkConstraints(.uri, .domain, ); != nil {return } }for , := range {if !domainNameValid(.domain, false) {returnfmt.Errorf("x509: cannot parse rfc822Name %q", ) }if := checkConstraints(.email, , ); != nil {return } }returnnil}func ( []*Certificate) error {var *chainConstraintsvar *chainConstraintsfor , := range {if !.hasNameConstraints() {continue } := &chainConstraints{ip: constraints[*net.IPNet, net.IP]{"IP address", newIPNetConstraints(.PermittedIPRanges), newIPNetConstraints(.ExcludedIPRanges)},dns: constraints[string, string]{"DNS name", newDNSConstraints(.PermittedDNSDomains, true), newDNSConstraints(.ExcludedDNSDomains, false)},uri: constraints[string, string]{"URI", newDNSConstraints(.PermittedURIDomains, true), newDNSConstraints(.ExcludedURIDomains, false)},email: constraints[string, rfc2821Mailbox]{"email address", newEmailConstraints(.PermittedEmailAddresses, true), newEmailConstraints(.ExcludedEmailAddresses, false)},index: , }if == nil { = = } elseif != nil { .next = = } }if == nil {returnnil }for , := range {if !.hasSANExtension() {continue }if >= .index {for .index <= {if .next == nil {returnnil } = .next } } , := parseURIs(.URIs)if != nil {return } , := parseMailboxes(.EmailAddresses)if != nil {return }for := ; != nil; = .next {if := .check(.DNSNames, , , .IPAddresses); != nil {return } } }returnnil}typeparsedURIstruct {uri *url.URLdomainstring}func ( parsedURI) () string {return .uri.String()}func ( []*url.URL) ([]parsedURI, error) { := make([]parsedURI, 0, len())for , := range { := strings.ToLower(.Host)iflen() == 0 {returnnil, fmt.Errorf("URI with empty host (%q) cannot be matched against constraints", .String()) }ifstrings.Contains(, ":") && !strings.HasSuffix(, "]") {varerror , _, = net.SplitHostPort(.Host)if != nil {returnnil, fmt.Errorf("cannot parse URI host %q: %v", .Host, ) } }// netip.ParseAddr will reject the URI IPv6 literal form "[...]", so we // check if _either_ the string parses as an IP, or if it is enclosed in // square brackets.if , := netip.ParseAddr(); == nil || (strings.HasPrefix(, "[") && strings.HasSuffix(, "]")) {returnnil, fmt.Errorf("URI with IP (%q) cannot be matched against constraints", .String()) } = append(, parsedURI{, }) }return , nil}func ( []string) ([]rfc2821Mailbox, error) { := make([]rfc2821Mailbox, 0, len())for , := range { , := parseRFC2821Mailbox()if ! {returnnil, fmt.Errorf("cannot parse rfc822Name %q", ) } .domain = strings.ToLower(.domain) = append(, ) }return , nil}func ( string) string { := strings.IndexByte(, '.')if < 0 {// Constraint is a single label, we cannot trim it.return"" }return [:]}
The pages are generated with Goldsv0.8.4. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds.