Files
labca/patches/policy_pa.patch
2025-12-27 16:40:48 +01:00

219 lines
7.8 KiB
Diff

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
}