Require email domains end in a IANA suffix (#4037)
This commit is contained in:
parent
fb7b60f42c
commit
3129c57bb8
|
@ -0,0 +1,32 @@
|
||||||
|
package iana
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/weppos/publicsuffix-go/publicsuffix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExtractSuffix returns the public suffix of the domain using only the "ICANN"
|
||||||
|
// section of the Public Suffix List database.
|
||||||
|
// If the domain does not end in a suffix that belongs to an IANA-assigned
|
||||||
|
// domain, ExtractSuffix returns an error.
|
||||||
|
func ExtractSuffix(name string) (string, error) {
|
||||||
|
if name == "" {
|
||||||
|
return "", fmt.Errorf("Blank name argument passed to ExtractSuffix")
|
||||||
|
}
|
||||||
|
|
||||||
|
rule := publicsuffix.DefaultList.Find(name, &publicsuffix.FindOptions{IgnorePrivate: true, DefaultRule: nil})
|
||||||
|
if rule == nil {
|
||||||
|
return "", fmt.Errorf("Domain %s has no IANA TLD", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
suffix := rule.Decompose(name)[1]
|
||||||
|
|
||||||
|
// If the TLD is empty, it means name is actually a suffix.
|
||||||
|
// In fact, decompose returns an array of empty strings in this case.
|
||||||
|
if suffix == "" {
|
||||||
|
suffix = name
|
||||||
|
}
|
||||||
|
|
||||||
|
return suffix, nil
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
package iana
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestExtractSuffix_Valid(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
domain, want string
|
||||||
|
}{
|
||||||
|
// TLD with only 1 rule.
|
||||||
|
{"biz", "biz"},
|
||||||
|
{"domain.biz", "biz"},
|
||||||
|
{"b.domain.biz", "biz"},
|
||||||
|
|
||||||
|
// The relevant {kobe,kyoto}.jp rules are:
|
||||||
|
// jp
|
||||||
|
// *.kobe.jp
|
||||||
|
// !city.kobe.jp
|
||||||
|
// kyoto.jp
|
||||||
|
// ide.kyoto.jp
|
||||||
|
{"jp", "jp"},
|
||||||
|
{"kobe.jp", "jp"},
|
||||||
|
{"c.kobe.jp", "c.kobe.jp"},
|
||||||
|
{"b.c.kobe.jp", "c.kobe.jp"},
|
||||||
|
{"a.b.c.kobe.jp", "c.kobe.jp"},
|
||||||
|
{"city.kobe.jp", "kobe.jp"},
|
||||||
|
{"www.city.kobe.jp", "kobe.jp"},
|
||||||
|
{"kyoto.jp", "kyoto.jp"},
|
||||||
|
{"test.kyoto.jp", "kyoto.jp"},
|
||||||
|
{"ide.kyoto.jp", "ide.kyoto.jp"},
|
||||||
|
{"b.ide.kyoto.jp", "ide.kyoto.jp"},
|
||||||
|
{"a.b.ide.kyoto.jp", "ide.kyoto.jp"},
|
||||||
|
|
||||||
|
// Domain with a private public suffix should return the ICANN public suffix.
|
||||||
|
{"foo.compute-1.amazonaws.com", "com"},
|
||||||
|
// Domain equal to a private public suffix should return the ICANN public
|
||||||
|
// suffix.
|
||||||
|
{"cloudapp.net", "net"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
got, err := ExtractSuffix(tc.domain)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%q: returned error", tc.domain)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if got != tc.want {
|
||||||
|
t.Errorf("%q: got %q, want %q", tc.domain, got, tc.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractSuffix_Invalid(t *testing.T) {
|
||||||
|
testCases := []string{
|
||||||
|
"",
|
||||||
|
"example",
|
||||||
|
"example.example",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
_, err := ExtractSuffix(tc)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("%q: expected err, got none", tc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
31
policy/pa.go
31
policy/pa.go
|
@ -11,13 +11,13 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/weppos/publicsuffix-go/publicsuffix"
|
|
||||||
"golang.org/x/net/idna"
|
"golang.org/x/net/idna"
|
||||||
"golang.org/x/text/unicode/norm"
|
"golang.org/x/text/unicode/norm"
|
||||||
|
|
||||||
"github.com/letsencrypt/boulder/core"
|
"github.com/letsencrypt/boulder/core"
|
||||||
berrors "github.com/letsencrypt/boulder/errors"
|
berrors "github.com/letsencrypt/boulder/errors"
|
||||||
"github.com/letsencrypt/boulder/features"
|
"github.com/letsencrypt/boulder/features"
|
||||||
|
"github.com/letsencrypt/boulder/iana"
|
||||||
blog "github.com/letsencrypt/boulder/log"
|
blog "github.com/letsencrypt/boulder/log"
|
||||||
"github.com/letsencrypt/boulder/reloader"
|
"github.com/letsencrypt/boulder/reloader"
|
||||||
)
|
)
|
||||||
|
@ -289,7 +289,7 @@ func (pa *AuthorityImpl) WillingToIssue(id core.AcmeIdentifier) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Names must end in an ICANN TLD, but they must not be equal to an ICANN TLD.
|
// Names must end in an ICANN TLD, but they must not be equal to an ICANN TLD.
|
||||||
icannTLD, err := extractDomainIANASuffix(domain)
|
icannTLD, err := iana.ExtractSuffix(domain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errNonPublic
|
return errNonPublic
|
||||||
}
|
}
|
||||||
|
@ -342,7 +342,7 @@ func (pa *AuthorityImpl) WillingToIssueWildcard(ident core.AcmeIdentifier) error
|
||||||
// The base domain is the wildcard request with the `*.` prefix removed
|
// The base domain is the wildcard request with the `*.` prefix removed
|
||||||
baseDomain := strings.TrimPrefix(rawDomain, "*.")
|
baseDomain := strings.TrimPrefix(rawDomain, "*.")
|
||||||
// Names must end in an ICANN TLD, but they must not be equal to an ICANN TLD.
|
// Names must end in an ICANN TLD, but they must not be equal to an ICANN TLD.
|
||||||
icannTLD, err := extractDomainIANASuffix(baseDomain)
|
icannTLD, err := iana.ExtractSuffix(baseDomain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errNonPublic
|
return errNonPublic
|
||||||
}
|
}
|
||||||
|
@ -481,31 +481,6 @@ func (pa *AuthorityImpl) ChallengesFor(identifier core.AcmeIdentifier, regID int
|
||||||
return shuffled, shuffledCombos, nil
|
return shuffled, shuffledCombos, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExtractDomainIANASuffix returns the public suffix of the domain using only the "ICANN"
|
|
||||||
// section of the Public Suffix List database.
|
|
||||||
// If the domain does not end in a suffix that belongs to an IANA-assigned
|
|
||||||
// domain, ExtractDomainIANASuffix returns an error.
|
|
||||||
func extractDomainIANASuffix(name string) (string, error) {
|
|
||||||
if name == "" {
|
|
||||||
return "", fmt.Errorf("Blank name argument passed to ExtractDomainIANASuffix")
|
|
||||||
}
|
|
||||||
|
|
||||||
rule := publicsuffix.DefaultList.Find(name, &publicsuffix.FindOptions{IgnorePrivate: true, DefaultRule: nil})
|
|
||||||
if rule == nil {
|
|
||||||
return "", fmt.Errorf("Domain %s has no IANA TLD", name)
|
|
||||||
}
|
|
||||||
|
|
||||||
suffix := rule.Decompose(name)[1]
|
|
||||||
|
|
||||||
// If the TLD is empty, it means name is actually a suffix.
|
|
||||||
// In fact, decompose returns an array of empty strings in this case.
|
|
||||||
if suffix == "" {
|
|
||||||
suffix = name
|
|
||||||
}
|
|
||||||
|
|
||||||
return suffix, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChallengeTypeEnabled returns whether the specified challenge type is enabled
|
// ChallengeTypeEnabled returns whether the specified challenge type is enabled
|
||||||
func (pa *AuthorityImpl) ChallengeTypeEnabled(t string, regID int64) bool {
|
func (pa *AuthorityImpl) ChallengeTypeEnabled(t string, regID int64) bool {
|
||||||
pa.blacklistMu.RLock()
|
pa.blacklistMu.RLock()
|
||||||
|
|
|
@ -401,68 +401,6 @@ func TestChallengesForWildcard(t *testing.T) {
|
||||||
test.AssertEquals(t, challenges[0].Type, core.ChallengeTypeDNS01)
|
test.AssertEquals(t, challenges[0].Type, core.ChallengeTypeDNS01)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExtractDomainIANASuffix_Valid(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
domain, want string
|
|
||||||
}{
|
|
||||||
// TLD with only 1 rule.
|
|
||||||
{"biz", "biz"},
|
|
||||||
{"domain.biz", "biz"},
|
|
||||||
{"b.domain.biz", "biz"},
|
|
||||||
|
|
||||||
// The relevant {kobe,kyoto}.jp rules are:
|
|
||||||
// jp
|
|
||||||
// *.kobe.jp
|
|
||||||
// !city.kobe.jp
|
|
||||||
// kyoto.jp
|
|
||||||
// ide.kyoto.jp
|
|
||||||
{"jp", "jp"},
|
|
||||||
{"kobe.jp", "jp"},
|
|
||||||
{"c.kobe.jp", "c.kobe.jp"},
|
|
||||||
{"b.c.kobe.jp", "c.kobe.jp"},
|
|
||||||
{"a.b.c.kobe.jp", "c.kobe.jp"},
|
|
||||||
{"city.kobe.jp", "kobe.jp"},
|
|
||||||
{"www.city.kobe.jp", "kobe.jp"},
|
|
||||||
{"kyoto.jp", "kyoto.jp"},
|
|
||||||
{"test.kyoto.jp", "kyoto.jp"},
|
|
||||||
{"ide.kyoto.jp", "ide.kyoto.jp"},
|
|
||||||
{"b.ide.kyoto.jp", "ide.kyoto.jp"},
|
|
||||||
{"a.b.ide.kyoto.jp", "ide.kyoto.jp"},
|
|
||||||
|
|
||||||
// Domain with a private public suffix should return the ICANN public suffix.
|
|
||||||
{"foo.compute-1.amazonaws.com", "com"},
|
|
||||||
// Domain equal to a private public suffix should return the ICANN public
|
|
||||||
// suffix.
|
|
||||||
{"cloudapp.net", "net"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
got, err := extractDomainIANASuffix(tc.domain)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("%q: returned error", tc.domain)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if got != tc.want {
|
|
||||||
t.Errorf("%q: got %q, want %q", tc.domain, got, tc.want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExtractDomainIANASuffix_Invalid(t *testing.T) {
|
|
||||||
testCases := []string{
|
|
||||||
"",
|
|
||||||
"example",
|
|
||||||
"example.example",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
_, err := extractDomainIANASuffix(tc)
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("%q: expected err, got none", tc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestMalformedExactBlacklist tests that loading a JSON policy file with an
|
// TestMalformedExactBlacklist tests that loading a JSON policy file with an
|
||||||
// invalid exact blacklist entry will fail as expected.
|
// invalid exact blacklist entry will fail as expected.
|
||||||
func TestMalformedExactBlacklist(t *testing.T) {
|
func TestMalformedExactBlacklist(t *testing.T) {
|
||||||
|
|
4
ra/ra.go
4
ra/ra.go
|
@ -23,6 +23,7 @@ import (
|
||||||
"github.com/letsencrypt/boulder/features"
|
"github.com/letsencrypt/boulder/features"
|
||||||
"github.com/letsencrypt/boulder/goodkey"
|
"github.com/letsencrypt/boulder/goodkey"
|
||||||
bgrpc "github.com/letsencrypt/boulder/grpc"
|
bgrpc "github.com/letsencrypt/boulder/grpc"
|
||||||
|
"github.com/letsencrypt/boulder/iana"
|
||||||
blog "github.com/letsencrypt/boulder/log"
|
blog "github.com/letsencrypt/boulder/log"
|
||||||
"github.com/letsencrypt/boulder/metrics"
|
"github.com/letsencrypt/boulder/metrics"
|
||||||
"github.com/letsencrypt/boulder/probs"
|
"github.com/letsencrypt/boulder/probs"
|
||||||
|
@ -385,6 +386,9 @@ func validateEmail(address string) error {
|
||||||
"invalid contact domain. Contact emails @%s are forbidden",
|
"invalid contact domain. Contact emails @%s are forbidden",
|
||||||
domain)
|
domain)
|
||||||
}
|
}
|
||||||
|
if _, err := iana.ExtractSuffix(domain); err != nil {
|
||||||
|
return berrors.InvalidEmailError("email domain name does not end in a IANA suffix")
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -365,6 +365,18 @@ func TestValidateContacts(t *testing.T) {
|
||||||
|
|
||||||
err = ra.validateContacts(context.Background(), &[]string{forbidden})
|
err = ra.validateContacts(context.Background(), &[]string{forbidden})
|
||||||
test.AssertError(t, err, "Forbidden email")
|
test.AssertError(t, err, "Forbidden email")
|
||||||
|
|
||||||
|
err = ra.validateContacts(context.Background(), &[]string{"mailto:admin@localhost"})
|
||||||
|
test.AssertError(t, err, "Forbidden email")
|
||||||
|
|
||||||
|
err = ra.validateContacts(context.Background(), &[]string{"mailto:admin@example.not.a.iana.suffix"})
|
||||||
|
test.AssertError(t, err, "Forbidden email")
|
||||||
|
|
||||||
|
err = ra.validateContacts(context.Background(), &[]string{"mailto:admin@1.2.3.4"})
|
||||||
|
test.AssertError(t, err, "Forbidden email")
|
||||||
|
|
||||||
|
err = ra.validateContacts(context.Background(), &[]string{"mailto:admin@[1.2.3.4]"})
|
||||||
|
test.AssertError(t, err, "Forbidden email")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewRegistration(t *testing.T) {
|
func TestNewRegistration(t *testing.T) {
|
||||||
|
|
|
@ -418,7 +418,7 @@ def random_domain():
|
||||||
return "rand.%x.xyz" % random.randrange(2**32)
|
return "rand.%x.xyz" % random.randrange(2**32)
|
||||||
|
|
||||||
def test_expiration_mailer():
|
def test_expiration_mailer():
|
||||||
email_addr = "integration.%x@boulder" % random.randrange(2**16)
|
email_addr = "integration.%x@letsencrypt.org" % random.randrange(2**16)
|
||||||
cert, _ = auth_and_issue([random_domain()], email=email_addr)
|
cert, _ = auth_and_issue([random_domain()], email=email_addr)
|
||||||
# Check that the expiration mailer sends a reminder
|
# Check that the expiration mailer sends a reminder
|
||||||
expiry = datetime.datetime.strptime(cert.body.get_notAfter(), '%Y%m%d%H%M%SZ')
|
expiry = datetime.datetime.strptime(cert.body.get_notAfter(), '%Y%m%d%H%M%SZ')
|
||||||
|
|
Loading…
Reference in New Issue