diff --git a/policy/pa.go b/policy/pa.go index 599dcdb10..084cb3ba8 100644 --- a/policy/pa.go +++ b/policy/pa.go @@ -30,6 +30,8 @@ type AuthorityImpl struct { blocklist map[string]bool exactBlocklist map[string]bool wildcardExactBlocklist map[string]bool + whitelist map[string]bool + lockdown map[string]bool blocklistMu sync.RWMutex enabledChallenges map[core.AcmeChallenge]bool @@ -70,6 +72,9 @@ type blockedNamesPolicy struct { // 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"` + + Whitelist []string `yaml:"Whitelist"` + Lockdown []string `yaml:"Lockdown"` } // SetHostnamePolicyFile will load the given policy file, returning error if it @@ -138,10 +143,20 @@ func (pa *AuthorityImpl) processHostnamePolicy(policy blockedNamesPolicy) error // wildcardNameMap to block issuance for `*.`+parts[1] wildcardNameMap[parts[1]] = true } + 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.blocklist = nameMap pa.exactBlocklist = exactNameMap pa.wildcardExactBlocklist = wildcardNameMap + pa.whitelist = whiteMap + pa.lockdown = lockMap pa.blocklistMu.Unlock() return nil } @@ -214,7 +229,7 @@ var ( // * exactly equal to an IANA registered TLD // // It does _not_ check that the domain isn't on any PA blocked lists. -func ValidDomain(domain string) error { +func (pa *AuthorityImpl) ValidDomain(domain string) error { if domain == "" { return errEmptyName } @@ -281,6 +296,14 @@ func ValidDomain(domain string) error { } } + ok, err := pa.checkWhitelist(domain) + if err != nil { + return err + } + if ok { + return nil + } + // 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 { @@ -308,7 +331,7 @@ 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 { if len(address) > 254 { @@ -318,7 +341,7 @@ func ValidEmail(address string) error { } splitEmail := strings.SplitN(email.Address, "@", -1) domain := strings.ToLower(splitEmail[len(splitEmail)-1]) - if err := ValidDomain(domain); err != nil { + if err := pa.ValidDomain(domain); err != nil { return berrors.InvalidEmailError( "contact email %q has invalid domain : %s", email.Address, err) @@ -357,10 +380,14 @@ func (pa *AuthorityImpl) WillingToIssue(id identifier.ACMEIdentifier) error { } domain := id.Value - if err := ValidDomain(domain); err != nil { + if err := pa.ValidDomain(domain); err != nil { return err } + if ok, _ := pa.checkWhitelist(domain); ok { + return nil + } + // Require no match against hostname block lists if err := pa.checkHostLists(domain); err != nil { return err @@ -369,6 +396,31 @@ func (pa *AuthorityImpl) WillingToIssue(id identifier.ACMEIdentifier) error { return nil } +func (pa *AuthorityImpl) checkWhitelist(domain string) (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 { + // 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 + } +} + // WillingToIssueWildcards is an extension of WillingToIssue that accepts DNS // identifiers for well formed wildcard domains in addition to regular // identifiers.