diff --git a/policy/pa.go b/policy/pa.go index ab17bd89d..0a71a962d 100644 --- a/policy/pa.go +++ b/policy/pa.go @@ -32,6 +32,9 @@ type AuthorityImpl struct { domainBlocklist map[string]bool fqdnBlocklist map[string]bool wildcardFqdnBlocklist map[string]bool + whitelist map[string]bool + lockdown map[string]bool + ldPublicContacts bool ipPrefixBlocklist []netip.Prefix blocklistMu sync.RWMutex @@ -73,6 +76,10 @@ type blockedIdentsPolicy struct { // AdminBlockedPrefixes is a list of IP address prefixes. All IP addresses // contained within the prefix are blocked. AdminBlockedPrefixes []string `yaml:"AdminBlockedPrefixes"` + + Whitelist []string `yaml:"Whitelist"` + Lockdown []string `yaml:"Lockdown"` + LockdownAllowPublicContacts bool `yaml:"LockdownAllowPublicContacts"` } // LoadIdentPolicyFile will load the given policy file, returning an error if it @@ -144,11 +151,23 @@ func (pa *AuthorityImpl) processIdentPolicy(policy blockedIdentsPolicy) error { prefixes = append(prefixes, prefix) } + whiteMap := make(map[string]bool) + for _, v := range policy.Whitelist { + whiteMap[v] = true + } + lockMap := make(map[string]bool) + for _, v := range policy.Lockdown { + lockMap[v] = true + } + pa.blocklistMu.Lock() pa.domainBlocklist = nameMap pa.fqdnBlocklist = exactNameMap pa.wildcardFqdnBlocklist = wildcardNameMap pa.ipPrefixBlocklist = prefixes + pa.whitelist = whiteMap + pa.lockdown = lockMap + pa.ldPublicContacts = policy.LockdownAllowPublicContacts pa.blocklistMu.Unlock() return nil } @@ -219,7 +238,7 @@ var ( // - 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 { +func (pa *AuthorityImpl) ValidNonWildcardDomain(domain string, isContact bool) error { if domain == "" { return errEmptyIdentifier } @@ -252,7 +271,9 @@ func validNonWildcardDomain(domain string) error { return errTooManyLabels } if len(labels) < 2 { - return errTooFewLabels + if (len(pa.lockdown) > 0 || len(pa.whitelist) > 0) && !pa.lockdown[domain] && !pa.whitelist[domain] { + return errTooFewLabels + } } for _, label := range labels { // Check that this is a valid LDH Label: "A string consisting of ASCII @@ -296,12 +317,17 @@ func validNonWildcardDomain(domain string) error { } } - // Names must end in an ICANN TLD, but they must not be equal to an ICANN TLD. - icannTLD, err := iana.ExtractSuffix(domain) + ok, err := pa.checkWhitelist(domain, isContact) if err != nil { - return errNonPublic + return err + } + if ok { + return nil } - if icannTLD == domain { + + // Names must not be equal to an ICANN TLD. + icannTLD, err := iana.ExtractSuffix(domain) + if err == nil && icannTLD == domain { return errICANNTLD } @@ -311,9 +337,9 @@ func validNonWildcardDomain(domain string) error { // 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 { +func (pa *AuthorityImpl) ValidDomain(domain string) error { if strings.Count(domain, "*") <= 0 { - return validNonWildcardDomain(domain) + return pa.ValidNonWildcardDomain(domain, false) } // Names containing more than one wildcard are invalid. @@ -332,7 +358,7 @@ func ValidDomain(domain string) error { // 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 { + if err != nil && !pa.lockdown[baseDomain] && !pa.whitelist[baseDomain] { return errNonPublic } // Names must have a non-wildcard label immediately adjacent to the ICANN @@ -340,7 +366,7 @@ func ValidDomain(domain string) error { if baseDomain == icannTLD { return errICANNTLDWildcard } - return validNonWildcardDomain(baseDomain) + return pa.ValidNonWildcardDomain(baseDomain, false) } // ValidIP checks that an IP address: @@ -383,14 +409,14 @@ var forbiddenMailDomains = map[string]bool{ // 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 { +func (pa *AuthorityImpl) 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) + err = pa.ValidNonWildcardDomain(domain, true) if err != nil { return berrors.InvalidEmailError("contact email has invalid domain: %s", err) } @@ -432,7 +458,7 @@ func subError(ident identifier.ACMEIdentifier, err error) berrors.SubBoulderErro // // Precondition: all input identifier values must be in lowercase. func (pa *AuthorityImpl) WillingToIssue(idents identifier.ACMEIdentifiers) error { - err := WellFormedIdentifiers(idents) + err := pa.WellFormedIdentifiers(idents) if err != nil { return err } @@ -449,6 +475,10 @@ func (pa *AuthorityImpl) WillingToIssue(idents identifier.ACMEIdentifiers) error // The base domain is the wildcard request with the `*.` prefix removed baseDomain := strings.TrimPrefix(ident.Value, "*.") + if ok, _ := pa.checkWhitelist(ident.Value, false); ok { + return nil + } + // The base domain can't be in the wildcard exact blocklist err = pa.checkWildcardBlocklist(baseDomain) if err != nil { @@ -497,12 +527,12 @@ func (pa *AuthorityImpl) WillingToIssue(idents identifier.ACMEIdentifiers) error // // If multiple identifiers are invalid, the error will contain suberrors // specific to each identifier. -func WellFormedIdentifiers(idents identifier.ACMEIdentifiers) error { +func (pa *AuthorityImpl) WellFormedIdentifiers(idents identifier.ACMEIdentifiers) error { var subErrors []berrors.SubBoulderError for _, ident := range idents { switch ident.Type { case identifier.TypeDNS: - err := ValidDomain(ident.Value) + err := pa.ValidDomain(ident.Value) if err != nil { subErrors = append(subErrors, subError(ident, err)) } @@ -544,6 +574,34 @@ func combineSubErrors(subErrors []berrors.SubBoulderError) error { return nil } +func (pa *AuthorityImpl) checkWhitelist(domain string, isContact bool) (bool, error) { + pa.blocklistMu.RLock() + defer pa.blocklistMu.RUnlock() + + if (pa.whitelist == nil) || (pa.lockdown == nil) { + return false, fmt.Errorf("Hostname policy not yet loaded.") + } + + labels := strings.Split(domain, ".") + for i := range labels { + joined := strings.Join(labels[i:], ".") + if pa.whitelist[joined] || pa.lockdown[joined] { + return true, nil + } + } + + if len(pa.lockdown) > 0 { + if isContact && pa.ldPublicContacts { + return false, nil + } + // In Lockdown mode, the domain MUST be in the list, so return an error if not found + return false, errPolicyForbidden + } else { + // In Whitelist mode, if the domain is not in the list, continue with the other checks + return false, nil + } +} + // checkWildcardBlocklist checks the wildcardExactBlocklist for a given domain. // If the domain is not present on the list nil is returned, otherwise // errPolicyForbidden is returned. @@ -575,6 +633,9 @@ func (pa *AuthorityImpl) checkBlocklists(ident identifier.ACMEIdentifier) error labels := strings.Split(ident.Value, ".") for i := range labels { joined := strings.Join(labels[i:], ".") + if pa.lockdown[joined] || pa.whitelist[joined] { + return nil + } if pa.domainBlocklist[joined] { return errPolicyForbidden }