From 045a128c2cecfd9b6026f0f54ed1fff734713c4e Mon Sep 17 00:00:00 2001 From: Arjan H Date: Sun, 4 Feb 2024 13:46:26 +0100 Subject: [PATCH] Option to allow public contact email addresses in lockdown mode When in lockdown mode, only those domains can be used to request certificates for, but it also only accepts email addresses in those domains. With this option in the GUI it is now possible to still allow all public domains in contact addresses. --- gui/apply-boulder | 9 ++++ gui/main.go | 11 +++++ gui/static/js/labca.js | 3 ++ gui/templates/views/manage.tmpl | 14 +++++- gui/templates/views/setup.tmpl | 11 ++++- patches/policy_pa.patch | 82 ++++++++++++++++++++++++--------- 6 files changed, 103 insertions(+), 27 deletions(-) diff --git a/gui/apply-boulder b/gui/apply-boulder index 92eba0a..dc43038 100755 --- a/gui/apply-boulder +++ b/gui/apply-boulder @@ -86,6 +86,15 @@ if [ "$PKI_DOMAIN_MODE" == "lockdown" ] && [ "$PKI_LOCKDOWN_DOMAINS" != "" ]; th for d in $(echo $PKI_LOCKDOWN_DOMAINS | sed -e "s/\\\r/ /g" | sed -e "s/\\\n/ /g" | tr '\r' ' '); do echo " - \"$d\"" >> hostname-policy.yaml done + + allow_public="false" + ld_public_contacts=$(grep ld_public_contacts $dataDir/config.json | grep true || echo "") + if [ "$ld_public_contacts" != "" ]; then + allow_public="true" + fi + + echo >> hostname-policy.yaml + echo "LockdownAllowPublicContacts: $allow_public" >> hostname-policy.yaml fi if [ "$PKI_DOMAIN_MODE" == "whitelist" ] && [ "$PKI_WHITELIST_DOMAINS" != "" ]; then echo >> hostname-policy.yaml diff --git a/gui/main.go b/gui/main.go index 8ca3982..d5213b3 100644 --- a/gui/main.go +++ b/gui/main.go @@ -176,6 +176,7 @@ type SetupConfig struct { DomainMode string LockdownDomains string WhitelistDomains string + LDPublicContacts bool ExtendedTimeout bool RequestBase string Errors map[string]string @@ -666,6 +667,7 @@ func _configUpdateHandler(w http.ResponseWriter, r *http.Request) { DomainMode: r.Form.Get("domain_mode"), LockdownDomains: r.Form.Get("lockdown_domains"), WhitelistDomains: r.Form.Get("whitelist_domains"), + LDPublicContacts: (r.Form.Get("ld_public_contacts") == "true"), ExtendedTimeout: (r.Form.Get("extended_timeout") == "true"), } @@ -715,6 +717,11 @@ func _configUpdateHandler(w http.ResponseWriter, r *http.Request) { delta = true viper.Set("labca.lockdown", cfg.LockdownDomains) } + + if cfg.LDPublicContacts != viper.GetBool("labca.ld_public_contacts") { + delta = true + viper.Set("labca.ld_public_contacts", cfg.LDPublicContacts) + } } if domainMode == "whitelist" { if cfg.WhitelistDomains != viper.GetString("labca.whitelist") { @@ -1585,6 +1592,7 @@ func _manageGet(w http.ResponseWriter, r *http.Request) { manageData["DomainMode"] = domainMode if domainMode == "lockdown" { manageData["LockdownDomains"] = viper.GetString("labca.lockdown") + manageData["LDPublicContacts"] = viper.GetBool("labca.ld_public_contacts") } if domainMode == "whitelist" { manageData["WhitelistDomains"] = viper.GetString("labca.whitelist") @@ -2536,6 +2544,7 @@ func _setupBaseConfig(w http.ResponseWriter, r *http.Request) bool { DomainMode: "lockdown", LockdownDomains: domain, WhitelistDomains: domain, + LDPublicContacts: true, RequestBase: r.Header.Get("X-Request-Base"), } @@ -2553,6 +2562,7 @@ func _setupBaseConfig(w http.ResponseWriter, r *http.Request) bool { DomainMode: r.Form.Get("domain_mode"), LockdownDomains: r.Form.Get("lockdown_domains"), WhitelistDomains: r.Form.Get("whitelist_domains"), + LDPublicContacts: (r.Form.Get("ld_public_contacts") == "true"), RequestBase: r.Header.Get("X-Request-Base"), } @@ -2571,6 +2581,7 @@ func _setupBaseConfig(w http.ResponseWriter, r *http.Request) bool { viper.Set("labca.domain_mode", cfg.DomainMode) if cfg.DomainMode == "lockdown" { viper.Set("labca.lockdown", cfg.LockdownDomains) + viper.Set("labca.ld_public_contacts", cfg.LDPublicContacts) } if cfg.DomainMode == "whitelist" { viper.Set("labca.whitelist", cfg.WhitelistDomains) diff --git a/gui/static/js/labca.js b/gui/static/js/labca.js index 326c0af..25f51de 100644 --- a/gui/static/js/labca.js +++ b/gui/static/js/labca.js @@ -165,8 +165,10 @@ $(function() { if ($("input[type=radio]#whitelist").prop('checked') || $("input[type=radio]#standard").prop('checked') ) { $("#domain_mode_warning").show(); + $("#ld_options").hide(); } else { $("#domain_mode_warning").hide(); + $("#ld_options").show(); } } @@ -175,6 +177,7 @@ $(function() { }); $("#domain_mode_warning").hide(); + $("#ld_options").show(); radioDisable(); diff --git a/gui/templates/views/manage.tmpl b/gui/templates/views/manage.tmpl index 04dd695..a4a0d18 100644 --- a/gui/templates/views/manage.tmpl +++ b/gui/templates/views/manage.tmpl @@ -295,17 +295,26 @@
- Standard - any official domains
+ Standard - any official domains + +
+
+ +  Still allow all public domain names in contact email addresses when creating new ACME accounts +
+

 If you see timeout related errors on the Dashboard / Audit Log, try checking this box.
+
+ Are you sure? This facilitates man-in-the-middle attacks! +
-   Are you sure? This facilitates man-in-the-middle attacks!
@@ -1001,6 +1010,7 @@ domain_mode: ($("#standard").prop('checked') ? 'standard' : ($("#whitelist").prop('checked') ? 'whitelist' : 'lockdown')), lockdown_domains: $("#lockdown_domains").val(), whitelist_domains: $("#whitelist_domains").val(), + ld_public_contacts: $("#ld_public_contacts").prop("checked"), extended_timeout: $("#extended_timeout").prop("checked"), }, }) diff --git a/gui/templates/views/setup.tmpl b/gui/templates/views/setup.tmpl index 44b5064..2fc610a 100644 --- a/gui/templates/views/setup.tmpl +++ b/gui/templates/views/setup.tmpl @@ -37,11 +37,18 @@ {{ . }}
{{ end }} - Standard - any official domains

+ Standard - any official domains
+ +
+
+ +  Still allow all public domain names in contact email addresses when creating new ACME accounts +
+
+ Are you sure? This facilitates man-in-the-middle attacks!
-   Are you sure? This facilitates man-in-the-middle attacks!
{{end}} diff --git a/patches/policy_pa.patch b/patches/policy_pa.patch index 39d26ca..4288a17 100644 --- a/patches/policy_pa.patch +++ b/patches/policy_pa.patch @@ -1,27 +1,29 @@ diff --git a/policy/pa.go b/policy/pa.go -index d872d5cbe..49daeb7d4 100644 +index d872d5cbe..faa052d6c 100644 --- a/policy/pa.go +++ b/policy/pa.go -@@ -32,6 +32,8 @@ type AuthorityImpl struct { +@@ -32,6 +32,9 @@ type AuthorityImpl struct { blocklist map[string]bool exactBlocklist map[string]bool wildcardExactBlocklist map[string]bool + whitelist map[string]bool + lockdown map[string]bool ++ ldPublicContacts bool blocklistMu sync.RWMutex enabledChallenges map[core.AcmeChallenge]bool -@@ -72,6 +74,9 @@ type blockedNamesPolicy struct { +@@ -72,6 +75,10 @@ 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"` ++ Whitelist []string `yaml:"Whitelist"` ++ Lockdown []string `yaml:"Lockdown"` ++ LockdownAllowPublicContacts bool `yaml:"LockdownAllowPublicContacts"` } // LoadHostnamePolicyFile will load the given policy file, returning an error if -@@ -131,10 +136,20 @@ func (pa *AuthorityImpl) processHostnamePolicy(policy blockedNamesPolicy) error +@@ -131,10 +138,21 @@ func (pa *AuthorityImpl) processHostnamePolicy(policy blockedNamesPolicy) error // wildcardNameMap to block issuance for `*.`+parts[1] wildcardNameMap[parts[1]] = true } @@ -39,23 +41,35 @@ index d872d5cbe..49daeb7d4 100644 pa.wildcardExactBlocklist = wildcardNameMap + pa.whitelist = whiteMap + pa.lockdown = lockMap ++ pa.ldPublicContacts = policy.LockdownAllowPublicContacts pa.blocklistMu.Unlock() return nil } -@@ -203,7 +218,7 @@ var ( +@@ -203,7 +221,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) error { ++func (pa *AuthorityImpl) ValidNonWildcardDomain(domain string, isContact bool) error { if domain == "" { return errEmptyName } -@@ -279,6 +294,14 @@ func ValidNonWildcardDomain(domain string) error { +@@ -235,7 +253,9 @@ func ValidNonWildcardDomain(domain string) error { + return errTooManyLabels + } + if len(labels) < 2 { +- return errTooFewLabels ++ if !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 +@@ -279,6 +299,14 @@ func ValidNonWildcardDomain(domain string) error { } } -+ ok, err := pa.checkWhitelist(domain) ++ ok, err := pa.checkWhitelist(domain, isContact) + if err != nil { + return err + } @@ -66,7 +80,7 @@ index d872d5cbe..49daeb7d4 100644 // 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 { -@@ -294,9 +317,9 @@ func ValidNonWildcardDomain(domain string) error { +@@ -294,9 +322,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. @@ -74,20 +88,29 @@ index d872d5cbe..49daeb7d4 100644 +func (pa *AuthorityImpl) ValidDomain(domain string) error { if strings.Count(domain, "*") <= 0 { - return ValidNonWildcardDomain(domain) -+ return pa.ValidNonWildcardDomain(domain) ++ return pa.ValidNonWildcardDomain(domain, false) } // Names containing more than one wildcard are invalid. -@@ -323,7 +346,7 @@ func ValidDomain(domain string) error { +@@ -315,7 +343,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 +@@ -323,7 +351,7 @@ func ValidDomain(domain string) error { if baseDomain == icannTLD { return errICANNTLDWildcard } - return ValidNonWildcardDomain(baseDomain) -+ return pa.ValidNonWildcardDomain(baseDomain) ++ return pa.ValidNonWildcardDomain(baseDomain, false) } // forbiddenMailDomains is a map of domain names we do not allow after the -@@ -341,7 +364,7 @@ var forbiddenMailDomains = map[string]bool{ +@@ -341,7 +369,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). @@ -96,16 +119,16 @@ index d872d5cbe..49daeb7d4 100644 email, err := mail.ParseAddress(address) if err != nil { if len(address) > 254 { -@@ -351,7 +374,7 @@ func ValidEmail(address string) error { +@@ -351,7 +379,7 @@ func ValidEmail(address string) error { } splitEmail := strings.SplitN(email.Address, "@", -1) domain := strings.ToLower(splitEmail[len(splitEmail)-1]) - err = ValidNonWildcardDomain(domain) -+ err = pa.ValidNonWildcardDomain(domain) ++ err = pa.ValidNonWildcardDomain(domain, true) if err != nil { return berrors.InvalidEmailError( "contact email %q has invalid domain : %s", -@@ -416,7 +439,7 @@ func (pa *AuthorityImpl) WillingToIssue(domains []string) error { +@@ -416,7 +444,7 @@ func (pa *AuthorityImpl) WillingToIssue(domains []string) error { for _, domain := range domains { if strings.Count(domain, "*") > 0 { // Domain contains a wildcard, check that it is valid. @@ -114,28 +137,28 @@ index d872d5cbe..49daeb7d4 100644 if err != nil { appendSubError(domain, err) continue -@@ -433,12 +456,15 @@ func (pa *AuthorityImpl) WillingToIssue(domains []string) error { +@@ -433,12 +461,15 @@ func (pa *AuthorityImpl) WillingToIssue(domains []string) error { } } else { // Validate that the domain is well-formed. - err := ValidNonWildcardDomain(domain) -+ err := pa.ValidNonWildcardDomain(domain) ++ err := pa.ValidNonWildcardDomain(domain, false) if err != nil { appendSubError(domain, err) continue } } -+ if ok, _ := pa.checkWhitelist(domain); ok { ++ if ok, _ := pa.checkWhitelist(domain, false); ok { + return nil + } // Require no match against hostname block lists err := pa.checkHostLists(domain) if err != nil { -@@ -471,6 +497,31 @@ func (pa *AuthorityImpl) WillingToIssue(domains []string) error { +@@ -471,6 +502,34 @@ func (pa *AuthorityImpl) WillingToIssue(domains []string) error { return nil } -+func (pa *AuthorityImpl) checkWhitelist(domain string) (bool, error) { ++func (pa *AuthorityImpl) checkWhitelist(domain string, isContact bool) (bool, error) { + pa.blocklistMu.RLock() + defer pa.blocklistMu.RUnlock() + @@ -152,6 +175,9 @@ index d872d5cbe..49daeb7d4 100644 + } + + 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 { @@ -163,3 +189,13 @@ index d872d5cbe..49daeb7d4 100644 // checkWildcardHostList checks the wildcardExactBlocklist for a given domain. // If the domain is not present on the list nil is returned, otherwise // errPolicyForbidden is returned. +@@ -500,6 +559,9 @@ func (pa *AuthorityImpl) checkHostLists(domain string) error { + labels := strings.Split(domain, ".") + for i := range labels { + joined := strings.Join(labels[i:], ".") ++ if pa.lockdown[domain] { ++ continue ++ } + if pa.blocklist[joined] { + return errPolicyForbidden + }