package policy import ( "crypto/sha256" "encoding/hex" "errors" "fmt" "net" "net/mail" "os" "regexp" "slices" "strings" "sync" "golang.org/x/net/idna" "golang.org/x/text/unicode/norm" "github.com/letsencrypt/boulder/bdns" "github.com/letsencrypt/boulder/core" berrors "github.com/letsencrypt/boulder/errors" "github.com/letsencrypt/boulder/iana" "github.com/letsencrypt/boulder/identifier" blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/strictyaml" ) // AuthorityImpl enforces CA policy decisions. type AuthorityImpl struct { log blog.Logger blocklist map[string]bool exactBlocklist map[string]bool wildcardExactBlocklist map[string]bool blocklistMu sync.RWMutex enabledChallenges map[core.AcmeChallenge]bool enabledIdentifiers map[identifier.IdentifierType]bool } // New constructs a Policy Authority. func New(identifierTypes map[identifier.IdentifierType]bool, challengeTypes map[core.AcmeChallenge]bool, log blog.Logger) (*AuthorityImpl, error) { // If identifierTypes are not configured (i.e. nil), default to allowing DNS // identifiers. This default is temporary, to improve deployability. // // TODO(#8184): Remove this default. if identifierTypes == nil { identifierTypes = map[identifier.IdentifierType]bool{identifier.TypeDNS: true} } return &AuthorityImpl{ log: log, enabledChallenges: challengeTypes, enabledIdentifiers: identifierTypes, }, nil } // blockedNamesPolicy is a struct holding lists of blocked domain names. One for // exact blocks and one for blocks including all subdomains. type blockedNamesPolicy struct { // ExactBlockedNames is a list of domain names. Issuance for names exactly // matching an entry in the list will be forbidden. (e.g. `ExactBlockedNames` // containing `www.example.com` will not block `example.com` or // `mail.example.com`). ExactBlockedNames []string `yaml:"ExactBlockedNames"` // HighRiskBlockedNames is like ExactBlockedNames except that issuance is // blocked for subdomains as well. (e.g. BlockedNames containing `example.com` // will block `www.example.com`). // // This list typically doesn't change with much regularity. HighRiskBlockedNames []string `yaml:"HighRiskBlockedNames"` // AdminBlockedNames operates the same as BlockedNames but is changed with more // frequency based on administrative blocks/revocations that are added over // time above and beyond the high-risk domains. Managing these entries separately // from HighRiskBlockedNames makes it easier to vet changes accurately. AdminBlockedNames []string `yaml:"AdminBlockedNames"` } // LoadHostnamePolicyFile will load the given policy file, returning an error if // it fails. func (pa *AuthorityImpl) LoadHostnamePolicyFile(f string) error { configBytes, err := os.ReadFile(f) if err != nil { return err } hash := sha256.Sum256(configBytes) pa.log.Infof("loading hostname policy, sha256: %s", hex.EncodeToString(hash[:])) var policy blockedNamesPolicy err = strictyaml.Unmarshal(configBytes, &policy) if err != nil { return err } if len(policy.HighRiskBlockedNames) == 0 { return fmt.Errorf("No entries in HighRiskBlockedNames.") } if len(policy.ExactBlockedNames) == 0 { return fmt.Errorf("No entries in ExactBlockedNames.") } return pa.processHostnamePolicy(policy) } // processHostnamePolicy handles loading a new blockedNamesPolicy into the PA. // All of the policy.ExactBlockedNames will be added to the // wildcardExactBlocklist by processHostnamePolicy to ensure that wildcards for // exact blocked names entries are forbidden. func (pa *AuthorityImpl) processHostnamePolicy(policy blockedNamesPolicy) error { nameMap := make(map[string]bool) for _, v := range policy.HighRiskBlockedNames { nameMap[v] = true } for _, v := range policy.AdminBlockedNames { nameMap[v] = true } exactNameMap := make(map[string]bool) wildcardNameMap := make(map[string]bool) for _, v := range policy.ExactBlockedNames { exactNameMap[v] = true // Remove the leftmost label of the exact blocked names entry to make an exact // wildcard block list entry that will prevent issuing a wildcard that would // include the exact blocklist entry. e.g. if "highvalue.example.com" is on // the exact blocklist we want "example.com" to be in the // wildcardExactBlocklist so that "*.example.com" cannot be issued. // // First, split the domain into two parts: the first label and the rest of the domain. parts := strings.SplitN(v, ".", 2) // if there are less than 2 parts then this entry is malformed! There should // at least be a "something." and a TLD like "com" if len(parts) < 2 { return fmt.Errorf( "Malformed ExactBlockedNames entry, only one label: %q", v) } // Add the second part, the domain minus the first label, to the // wildcardNameMap to block issuance for `*.`+parts[1] wildcardNameMap[parts[1]] = true } pa.blocklistMu.Lock() pa.blocklist = nameMap pa.exactBlocklist = exactNameMap pa.wildcardExactBlocklist = wildcardNameMap pa.blocklistMu.Unlock() return nil } // The values of maxDNSIdentifierLength, maxLabelLength and maxLabels are hard coded // into the error messages errNameTooLong, errLabelTooLong and errTooManyLabels. // If their values change, the related error messages should be updated. const ( maxLabels = 10 // RFC 1034 says DNS labels have a max of 63 octets, and names have a max of 255 // octets: https://tools.ietf.org/html/rfc1035#page-10. Since two of those octets // are taken up by the leading length byte and the trailing root period the actual // max length becomes 253. maxLabelLength = 63 maxDNSIdentifierLength = 253 ) var dnsLabelCharacterRegexp = regexp.MustCompile("^[a-z0-9-]+$") func isDNSCharacter(ch byte) bool { return ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z') || ('0' <= ch && ch <= '9') || ch == '.' || ch == '-' } // In these error messages: // 253 is the value of maxDNSIdentifierLength // 63 is the value of maxLabelLength // 10 is the value of maxLabels // If these values change, the related error messages should be updated. var ( errNonPublic = berrors.MalformedError("Domain name does not end with a valid public suffix (TLD)") errICANNTLD = berrors.MalformedError("Domain name is an ICANN TLD") errPolicyForbidden = berrors.RejectedIdentifierError("The ACME server refuses to issue a certificate for this domain name, because it is forbidden by policy") errInvalidDNSCharacter = berrors.MalformedError("Domain name contains an invalid character") errNameTooLong = berrors.MalformedError("Domain name is longer than 253 bytes") errIPAddressInDNS = berrors.MalformedError("Identifier type is DNS but value is an IP address") errIPInvalid = berrors.MalformedError("IP address is invalid") errIPSpecialPurpose = berrors.MalformedError("IP address is in a special-purpose address block") errTooManyLabels = berrors.MalformedError("Domain name has more than 10 labels (parts)") errEmptyIdentifier = berrors.MalformedError("Identifier value (name) is empty") errNameEndsInDot = berrors.MalformedError("Domain name ends in a dot") errTooFewLabels = berrors.MalformedError("Domain name needs at least one dot") errLabelTooShort = berrors.MalformedError("Domain name can not have two dots in a row") errLabelTooLong = berrors.MalformedError("Domain has a label (component between dots) longer than 63 bytes") errMalformedIDN = berrors.MalformedError("Domain name contains malformed punycode") errInvalidRLDH = berrors.RejectedIdentifierError("Domain name contains an invalid label in a reserved format (R-LDH: '??--')") errTooManyWildcards = berrors.MalformedError("Domain name has more than one wildcard") errMalformedWildcard = berrors.MalformedError("Domain name contains an invalid wildcard. A wildcard is only permitted before the first dot in a domain name") errICANNTLDWildcard = berrors.MalformedError("Domain name is a wildcard for an ICANN TLD") errWildcardNotSupported = berrors.MalformedError("Wildcard domain names are not supported") errUnsupportedIdent = berrors.MalformedError("Invalid identifier type") ) // validNonWildcardDomain checks that a domain isn't: // - empty // - prefixed with the wildcard label `*.` // - made of invalid DNS characters // - longer than the maxDNSIdentifierLength // - an IPv4 or IPv6 address // - suffixed with just "." // - made of too many DNS labels // - made of any invalid DNS labels // - suffixed with something other than an IANA registered TLD // - exactly equal to an IANA registered TLD // // It does NOT ensure that the domain is absent from any PA blocked lists. func validNonWildcardDomain(domain string) error { if domain == "" { return errEmptyIdentifier } if strings.HasPrefix(domain, "*.") { return errWildcardNotSupported } for _, ch := range []byte(domain) { if !isDNSCharacter(ch) { return errInvalidDNSCharacter } } if len(domain) > maxDNSIdentifierLength { return errNameTooLong } if ip := net.ParseIP(domain); ip != nil { return errIPAddressInDNS } if strings.HasSuffix(domain, ".") { return errNameEndsInDot } labels := strings.Split(domain, ".") if len(labels) > maxLabels { return errTooManyLabels } if len(labels) < 2 { return errTooFewLabels } for _, label := range labels { // Check that this is a valid LDH Label: "A string consisting of ASCII // letters, digits, and the hyphen with the further restriction that the // hyphen cannot appear at the beginning or end of the string. Like all DNS // labels, its total length must not exceed 63 octets." (RFC 5890, 2.3.1) if len(label) < 1 { return errLabelTooShort } if len(label) > maxLabelLength { return errLabelTooLong } if !dnsLabelCharacterRegexp.MatchString(label) { return errInvalidDNSCharacter } if label[0] == '-' || label[len(label)-1] == '-' { return errInvalidDNSCharacter } // Check if this is a Reserved LDH Label: "[has] the property that they // contain "--" in the third and fourth characters but which otherwise // conform to LDH label rules." (RFC 5890, 2.3.1) if len(label) >= 4 && label[2:4] == "--" { // Check if this is an XN-Label: "labels that begin with the prefix "xn--" // (case independent), but otherwise conform to the rules for LDH labels." // (RFC 5890, 2.3.1) if label[0:2] != "xn" { return errInvalidRLDH } // Check if this is a P-Label: "A XN-Label that contains valid output of // the Punycode algorithm (as defined in RFC 3492, Section 6.3) from the // fifth and subsequent positions." (Baseline Requirements, 1.6.1) ulabel, err := idna.ToUnicode(label) if err != nil { return errMalformedIDN } if !norm.NFC.IsNormalString(ulabel) { return errMalformedIDN } } } // Names must end in an ICANN TLD, but they must not be equal to an ICANN TLD. icannTLD, err := iana.ExtractSuffix(domain) if err != nil { return errNonPublic } if icannTLD == domain { return errICANNTLD } return nil } // ValidDomain checks that a domain is valid and that it doesn't contain any // invalid wildcard characters. It does NOT ensure that the domain is absent // from any PA blocked lists. func ValidDomain(domain string) error { if strings.Count(domain, "*") <= 0 { return validNonWildcardDomain(domain) } // Names containing more than one wildcard are invalid. if strings.Count(domain, "*") > 1 { return errTooManyWildcards } // If the domain has a wildcard character, but it isn't the first most // label of the domain name then the wildcard domain is malformed if !strings.HasPrefix(domain, "*.") { return errMalformedWildcard } // The base domain is the wildcard request with the `*.` prefix removed baseDomain := strings.TrimPrefix(domain, "*.") // Names must end in an ICANN TLD, but they must not be equal to an ICANN TLD. icannTLD, err := iana.ExtractSuffix(baseDomain) if err != nil { return errNonPublic } // Names must have a non-wildcard label immediately adjacent to the ICANN // TLD. No `*.com`! if baseDomain == icannTLD { return errICANNTLDWildcard } return validNonWildcardDomain(baseDomain) } // validIP checks that an IP address: // - isn't empty // - is an IPv4 or IPv6 address // - isn't in an IANA special-purpose address registry // // It does NOT ensure that the IP address is absent from any PA blocked lists. func validIP(ip string) error { if ip == "" { return errEmptyIdentifier } // Check the output of net.IP.String(), to ensure the input complied with // RFC 8738, Sec. 3. ("The identifier value MUST contain the textual form of // the address as defined in RFC 1123, Sec. 2.1 for IPv4 and in RFC 5952, // Sec. 4 for IPv6.") ParseIP() will accept a non-compliant but otherwise // valid string; String() will output a compliant string. parsedIP := net.ParseIP(ip) if parsedIP == nil || parsedIP.String() != ip { return errIPInvalid } if bdns.IsReservedIP(parsedIP) { return errIPSpecialPurpose } return nil } // forbiddenMailDomains is a map of domain names we do not allow after the // @ symbol in contact mailto addresses. These are frequently used when // copy-pasting example configurations and would not result in expiration // messages and subscriber communications reaching the user that created the // registration if allowed. var forbiddenMailDomains = map[string]bool{ // https://tools.ietf.org/html/rfc2606#section-3 "example.com": true, "example.net": true, "example.org": true, } // ValidEmail returns an error if the input doesn't parse as an email address, // the domain isn't a valid hostname in Preferred Name Syntax, or its on the // list of domains forbidden for mail (because they are often used in examples). func ValidEmail(address string) error { email, err := mail.ParseAddress(address) if err != nil { return berrors.InvalidEmailError("unable to parse email address") } splitEmail := strings.SplitN(email.Address, "@", -1) domain := strings.ToLower(splitEmail[len(splitEmail)-1]) err = validNonWildcardDomain(domain) if err != nil { return berrors.InvalidEmailError("contact email has invalid domain: %s", err) } if forbiddenMailDomains[domain] { // We're okay including the domain in the error message here because this // case occurs only for a small block-list of domains listed above. return berrors.InvalidEmailError("contact email has forbidden domain %q", domain) } return nil } // subError returns an appropriately typed error based on the input error func subError(ident identifier.ACMEIdentifier, err error) berrors.SubBoulderError { var bErr *berrors.BoulderError if errors.As(err, &bErr) { return berrors.SubBoulderError{ Identifier: ident, BoulderError: bErr, } } else { return berrors.SubBoulderError{ Identifier: ident, BoulderError: &berrors.BoulderError{ Type: berrors.RejectedIdentifier, Detail: err.Error(), }, } } } // WillingToIssue determines whether the CA is willing to issue for the provided // identifiers. // // It checks the criteria checked by `WellFormedIdentifiers`, and additionally // checks whether any identifier is on a blocklist. // // If multiple identifiers are invalid, the error will contain suberrors // specific to each identifier. // // Precondition: all input identifier values must be in lowercase. func (pa *AuthorityImpl) WillingToIssue(idents identifier.ACMEIdentifiers) error { err := WellFormedIdentifiers(idents) if err != nil { return err } var subErrors []berrors.SubBoulderError for _, ident := range idents { if !pa.IdentifierTypeEnabled(ident.Type) { subErrors = append(subErrors, subError(ident, berrors.RejectedIdentifierError("The ACME server has disabled this identifier type"))) continue } // Only DNS identifiers are subject to wildcard and blocklist checks. // Unsupported identifier types will have been caught by // WellFormedIdentifiers(). // // TODO(#7311): We may want to implement IP address blocklists too. if ident.Type == identifier.TypeDNS { if strings.Count(ident.Value, "*") > 0 { // The base domain is the wildcard request with the `*.` prefix removed baseDomain := strings.TrimPrefix(ident.Value, "*.") // The base domain can't be in the wildcard exact blocklist err = pa.checkWildcardHostList(baseDomain) if err != nil { subErrors = append(subErrors, subError(ident, err)) continue } } // For both wildcard and non-wildcard domains, check whether any parent domain // name is on the regular blocklist. err := pa.checkHostLists(ident.Value) if err != nil { subErrors = append(subErrors, subError(ident, err)) continue } } } return combineSubErrors(subErrors) } // WellFormedIdentifiers returns an error if any of the provided identifiers do // not meet these criteria: // // For DNS identifiers: // - MUST contains only lowercase characters, numbers, hyphens, and dots // - MUST NOT have more than maxLabels labels // - MUST follow the DNS hostname syntax rules in RFC 1035 and RFC 2181 // // In particular, DNS identifiers: // - MUST NOT contain underscores // - MUST NOT match the syntax of an IP address // - MUST end in a public suffix // - MUST have at least one label in addition to the public suffix // - MUST NOT be a label-wise suffix match for a name on the block list, // where comparison is case-independent (normalized to lower case) // // If a DNS identifier contains a *, we additionally require: // - There is at most one `*` wildcard character // - That the wildcard character is the leftmost label // - That the wildcard label is not immediately adjacent to a top level ICANN // TLD // // For IP identifiers: // - MUST match the syntax of an IP address // - MUST NOT be in an IANA special-purpose address registry // // If multiple identifiers are invalid, the error will contain suberrors // specific to each identifier. func WellFormedIdentifiers(idents identifier.ACMEIdentifiers) error { var subErrors []berrors.SubBoulderError for _, ident := range idents { switch ident.Type { case identifier.TypeDNS: err := ValidDomain(ident.Value) if err != nil { subErrors = append(subErrors, subError(ident, err)) } case identifier.TypeIP: err := validIP(ident.Value) if err != nil { subErrors = append(subErrors, subError(ident, err)) } default: subErrors = append(subErrors, subError(ident, errUnsupportedIdent)) } } return combineSubErrors(subErrors) } func combineSubErrors(subErrors []berrors.SubBoulderError) error { if len(subErrors) > 0 { // If there was only one error, then use it as the top level error that is // returned. if len(subErrors) == 1 { return berrors.RejectedIdentifierError( "Cannot issue for %q: %s", subErrors[0].Identifier.Value, subErrors[0].BoulderError.Detail, ) } detail := fmt.Sprintf( "Cannot issue for %q: %s (and %d more problems. Refer to sub-problems for more information.)", subErrors[0].Identifier.Value, subErrors[0].BoulderError.Detail, len(subErrors)-1, ) return (&berrors.BoulderError{ Type: berrors.RejectedIdentifier, Detail: detail, }).WithSubErrors(subErrors) } return nil } // checkWildcardHostList checks the wildcardExactBlocklist for a given domain. // If the domain is not present on the list nil is returned, otherwise // errPolicyForbidden is returned. func (pa *AuthorityImpl) checkWildcardHostList(domain string) error { pa.blocklistMu.RLock() defer pa.blocklistMu.RUnlock() if pa.wildcardExactBlocklist == nil { return fmt.Errorf("Hostname policy not yet loaded.") } if pa.wildcardExactBlocklist[domain] { return errPolicyForbidden } return nil } func (pa *AuthorityImpl) checkHostLists(domain string) error { pa.blocklistMu.RLock() defer pa.blocklistMu.RUnlock() if pa.blocklist == nil { return fmt.Errorf("Hostname policy not yet loaded.") } labels := strings.Split(domain, ".") for i := range labels { joined := strings.Join(labels[i:], ".") if pa.blocklist[joined] { return errPolicyForbidden } } if pa.exactBlocklist[domain] { return errPolicyForbidden } return nil } // ChallengeTypesFor determines which challenge types are acceptable for the // given identifier. This determination is made purely based on the identifier, // and not based on which challenge types are enabled, so that challenge type // filtering can happen dynamically at request rather than being set in stone // at creation time. func (pa *AuthorityImpl) ChallengeTypesFor(ident identifier.ACMEIdentifier) ([]core.AcmeChallenge, error) { switch ident.Type { case identifier.TypeDNS: // If the identifier is for a DNS wildcard name we only provide a DNS-01 // challenge, to comply with the BRs Sections 3.2.2.4.19 and 3.2.2.4.20 // stating that ACME HTTP-01 and TLS-ALPN-01 are not suitable for validating // Wildcard Domains. if strings.HasPrefix(ident.Value, "*.") { return []core.AcmeChallenge{core.ChallengeTypeDNS01}, nil } // Return all challenge types we support for non-wildcard DNS identifiers. return []core.AcmeChallenge{ core.ChallengeTypeHTTP01, core.ChallengeTypeDNS01, core.ChallengeTypeTLSALPN01, }, nil case identifier.TypeIP: // Only HTTP-01 and TLS-ALPN-01 are suitable for IP address identifiers // per RFC 8738, Sec. 4. return []core.AcmeChallenge{ core.ChallengeTypeHTTP01, core.ChallengeTypeTLSALPN01, }, nil default: // Otherwise return an error because we don't support any challenges for this // identifier type. return nil, fmt.Errorf("unrecognized identifier type %q", ident.Type) } } // ChallengeTypeEnabled returns whether the specified challenge type is enabled func (pa *AuthorityImpl) ChallengeTypeEnabled(t core.AcmeChallenge) bool { pa.blocklistMu.RLock() defer pa.blocklistMu.RUnlock() return pa.enabledChallenges[t] } // CheckAuthzChallenges determines that an authorization was fulfilled by a // challenge that is currently enabled and was appropriate for the kind of // identifier in the authorization. func (pa *AuthorityImpl) CheckAuthzChallenges(authz *core.Authorization) error { chall, err := authz.SolvedBy() if err != nil { return err } if !pa.ChallengeTypeEnabled(chall) { return errors.New("authorization fulfilled by disabled challenge type") } challTypes, err := pa.ChallengeTypesFor(authz.Identifier) if err != nil { return err } if !slices.Contains(challTypes, chall) { return errors.New("authorization fulfilled by inapplicable challenge type") } return nil } // IdentifierTypeEnabled returns whether the specified identifier type is enabled func (pa *AuthorityImpl) IdentifierTypeEnabled(t identifier.IdentifierType) bool { pa.blocklistMu.RLock() defer pa.blocklistMu.RUnlock() return pa.enabledIdentifiers[t] }