boulder/policy/pa_test.go

499 lines
17 KiB
Go

package policy
import (
"encoding/json"
"io/ioutil"
"os"
"testing"
"github.com/letsencrypt/boulder/core"
"github.com/letsencrypt/boulder/features"
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/test"
)
var log = blog.UseMock()
var enabledChallenges = map[string]bool{
core.ChallengeTypeHTTP01: true,
core.ChallengeTypeTLSSNI01: true,
core.ChallengeTypeDNS01: true,
}
const (
testRegID = 1234
testRegIDWhitelisted = 1000
)
func paImpl(t *testing.T) *AuthorityImpl {
pa, err := New(enabledChallenges)
if err != nil {
t.Fatalf("Couldn't create policy implementation: %s", err)
}
return pa
}
func TestWillingToIssue(t *testing.T) {
testCases := []struct {
domain string
err error
}{
{``, errEmptyName}, // Empty name
{`zomb!.com`, errInvalidDNSCharacter}, // ASCII character out of range
{`emailaddress@myseriously.present.com`, errInvalidDNSCharacter},
{`user:pass@myseriously.present.com`, errInvalidDNSCharacter},
{`zömbo.com`, errInvalidDNSCharacter}, // non-ASCII character
{`127.0.0.1`, errIPAddress}, // IPv4 address
{`fe80::1:1`, errInvalidDNSCharacter}, // IPv6 addresses
{`[2001:db8:85a3:8d3:1319:8a2e:370:7348]`, errInvalidDNSCharacter}, // unexpected IPv6 variants
{`[2001:db8:85a3:8d3:1319:8a2e:370:7348]:443`, errInvalidDNSCharacter},
{`2001:db8::/32`, errInvalidDNSCharacter},
{`a.b.c.d.e.f.g.h.i.j.k`, errTooManyLabels}, // Too many labels (>10)
{`www.0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef012345.com`, errNameTooLong}, // Too long (254 characters)
{`www.0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef012345.com`, errNameTooLong}, // Too long (240 characters)
{`www.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz.com`, errLabelTooLong}, // Label too long (>63 characters)
{`www.-ombo.com`, errInvalidDNSCharacter}, // Label starts with '-'
{`www.zomb-.com`, errInvalidDNSCharacter}, // Label ends with '-'
{`xn--.net`, errInvalidDNSCharacter}, // Label ends with '-'
{`0`, errTooFewLabels},
{`1`, errTooFewLabels},
{`*`, errInvalidDNSCharacter},
{`**`, errInvalidDNSCharacter},
{`*.*`, errWildcardNotSupported},
{`zombo*com`, errInvalidDNSCharacter},
{`*.com`, errWildcardNotSupported},
{`*.zombo.com`, errWildcardNotSupported},
{`..a`, errLabelTooShort},
{`a..a`, errLabelTooShort},
{`.a..a`, errLabelTooShort},
{`..foo.com`, errLabelTooShort},
{`.`, errNameEndsInDot},
{`..`, errNameEndsInDot},
{`a..`, errNameEndsInDot},
{`.....`, errNameEndsInDot},
{`.a.`, errNameEndsInDot},
{`www.zombo.com.`, errNameEndsInDot},
{`www.zombo_com.com`, errInvalidDNSCharacter},
{`\uFEFF`, errInvalidDNSCharacter}, // Byte order mark
{`\uFEFFwww.zombo.com`, errInvalidDNSCharacter},
{`www.zom\u202Ebo.com`, errInvalidDNSCharacter}, // Right-to-Left Override
{`\u202Ewww.zombo.com`, errInvalidDNSCharacter},
{`www.zom\u200Fbo.com`, errInvalidDNSCharacter}, // Right-to-Left Mark
{`\u200Fwww.zombo.com`, errInvalidDNSCharacter},
// Underscores are technically disallowed in DNS. Some DNS
// implementations accept them but we will be conservative.
{`www.zom_bo.com`, errInvalidDNSCharacter},
{`zombocom`, errTooFewLabels},
{`localhost`, errTooFewLabels},
{`mail`, errTooFewLabels},
// disallow capitalized letters for #927
{`CapitalizedLetters.com`, errInvalidDNSCharacter},
{`example.acting`, errNonPublic},
{`example.internal`, errNonPublic},
// All-numeric final label not okay.
{`www.zombo.163`, errNonPublic},
{`xn--109-3veba6djs1bfxlfmx6c9g.xn--f1awi.xn--p1ai`, errMalformedIDN}, // Not in Unicode NFKC
{`bq--abwhky3f6fxq.jakacomo.com`, errInvalidRLDH},
}
shouldBeTLDError := []string{
`co.uk`,
`foo.bn`,
}
shouldBeBlacklisted := []string{
`highvalue.website1.org`,
`website2.co.uk`,
`www.website3.com`,
`lots.of.labels.website4.com`,
}
blacklistContents := []string{
`website2.com`,
`website2.org`,
`website2.co.uk`,
`website3.com`,
`website4.com`,
}
exactBlacklistContents := []string{
`www.website1.org`,
`highvalue.website1.org`,
`dl.website1.org`,
}
shouldBeAccepted := []string{
`lowvalue.website1.org`,
`website4.sucks`,
"www.unrelated.com",
"unrelated.com",
"www.8675309.com",
"8675309.com",
"web5ite2.com",
"www.web-site2.com",
}
pa := paImpl(t)
blacklistBytes, err := json.Marshal(blacklistJSON{
Blacklist: blacklistContents,
ExactBlacklist: exactBlacklistContents,
})
test.AssertNotError(t, err, "Couldn't serialize blacklist")
f, _ := ioutil.TempFile("", "test-blacklist.txt")
defer os.Remove(f.Name())
err = ioutil.WriteFile(f.Name(), blacklistBytes, 0640)
test.AssertNotError(t, err, "Couldn't write blacklist")
err = pa.SetHostnamePolicyFile(f.Name())
test.AssertNotError(t, err, "Couldn't load rules")
// Test for invalid identifier type
identifier := core.AcmeIdentifier{Type: "ip", Value: "example.com"}
err = pa.WillingToIssue(identifier)
if err != errInvalidIdentifier {
t.Error("Identifier was not correctly forbidden: ", identifier)
}
// Test syntax errors
for _, tc := range testCases {
identifier := core.AcmeIdentifier{Type: core.IdentifierDNS, Value: tc.domain}
err := pa.WillingToIssue(identifier)
if err != tc.err {
t.Errorf("WillingToIssue(%q) = %q, expected %q", tc.domain, err, tc.err)
}
}
// Invalid encoding
err = pa.WillingToIssue(core.AcmeIdentifier{Type: core.IdentifierDNS, Value: "www.xn--m.com"})
test.AssertError(t, err, "WillingToIssue didn't fail on a malformed IDN")
// Valid encoding
err = pa.WillingToIssue(core.AcmeIdentifier{Type: core.IdentifierDNS, Value: "www.xn--mnich-kva.com"})
test.AssertNotError(t, err, "WillingToIssue failed on a properly formed IDN")
// IDN TLD
err = pa.WillingToIssue(core.AcmeIdentifier{Type: core.IdentifierDNS, Value: "xn--example--3bhk5a.xn--p1ai"})
test.AssertNotError(t, err, "WillingToIssue failed on a properly formed domain with IDN TLD")
features.Reset()
// Test domains that are equal to public suffixes
for _, domain := range shouldBeTLDError {
identifier := core.AcmeIdentifier{Type: core.IdentifierDNS, Value: domain}
err := pa.WillingToIssue(identifier)
if err != errICANNTLD {
t.Error("Identifier was not correctly forbidden: ", identifier, err)
}
}
// Test blacklisting
for _, domain := range shouldBeBlacklisted {
identifier := core.AcmeIdentifier{Type: core.IdentifierDNS, Value: domain}
err := pa.WillingToIssue(identifier)
if err != errBlacklisted {
t.Error("Identifier was not correctly forbidden: ", identifier, err)
}
}
// Test acceptance of good names
for _, domain := range shouldBeAccepted {
identifier := core.AcmeIdentifier{Type: core.IdentifierDNS, Value: domain}
if err := pa.WillingToIssue(identifier); err != nil {
t.Error("Identifier was incorrectly forbidden: ", identifier, err)
}
}
}
func TestWillingToIssueWildcard(t *testing.T) {
bannedDomains := []string{
"zombo.gov.us",
}
exactBannedDomains := []string{
"highvalue.letsdecrypt.org",
}
pa := paImpl(t)
bannedBytes, err := json.Marshal(blacklistJSON{
Blacklist: bannedDomains,
ExactBlacklist: exactBannedDomains,
})
test.AssertNotError(t, err, "Couldn't serialize banned list")
f, _ := ioutil.TempFile("", "test-wildcard-banlist.txt")
defer os.Remove(f.Name())
err = ioutil.WriteFile(f.Name(), bannedBytes, 0640)
test.AssertNotError(t, err, "Couldn't write serialized banned list to file")
err = pa.SetHostnamePolicyFile(f.Name())
test.AssertNotError(t, err, "Couldn't load policy contents from file")
makeDNSIdent := func(domain string) core.AcmeIdentifier {
return core.AcmeIdentifier{
Type: core.IdentifierDNS,
Value: domain,
}
}
testCases := []struct {
Name string
Ident core.AcmeIdentifier
ExpectedErr error
}{
{
Name: "Non-DNS identifier",
Ident: core.AcmeIdentifier{Type: "nickname", Value: "cpu"},
ExpectedErr: errInvalidIdentifier,
},
{
Name: "Too many wildcards",
Ident: makeDNSIdent("ok.*.whatever.*.example.com"),
ExpectedErr: errTooManyWildcards,
},
{
Name: "Misplaced wildcard",
Ident: makeDNSIdent("ok.*.whatever.example.com"),
ExpectedErr: errMalformedWildcard,
},
{
Name: "Missing ICANN TLD",
Ident: makeDNSIdent("*.ok.madeup"),
ExpectedErr: errNonPublic,
},
{
Name: "Wildcard for ICANN TLD",
Ident: makeDNSIdent("*.com"),
ExpectedErr: errICANNTLDWildcard,
},
{
Name: "Forbidden base domain",
Ident: makeDNSIdent("*.zombo.gov.us"),
ExpectedErr: errBlacklisted,
},
// We should not allow getting a wildcard for that would cover an exact
// blocklist domain
{
Name: "Wildcard for ExactBlacklist base domain",
Ident: makeDNSIdent("*.letsdecrypt.org"),
ExpectedErr: errBlacklisted,
},
// We should allow a wildcard for a domain that doesn't match the exact
// blacklist domain
{
Name: "Wildcard for non-matching subdomain of ExactBlacklist domain",
Ident: makeDNSIdent("*.lowvalue.letsdecrypt.org"),
ExpectedErr: nil,
},
// We should allow getting a wildcard for an exact blacklist domain since it
// only covers subdomains, not the exact name.
{
Name: "Wildcard for ExactBlacklist domain",
Ident: makeDNSIdent("*.highvalue.letsdecrypt.org"),
ExpectedErr: nil,
},
{
Name: "Valid wildcard domain",
Ident: makeDNSIdent("*.everything.is.possible.at.zombo.com"),
ExpectedErr: nil,
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
result := pa.WillingToIssueWildcard(tc.Ident)
test.AssertEquals(t, result, tc.ExpectedErr)
})
}
}
var accountKeyJSON = `{
"kty":"RSA",
"n":"yNWVhtYEKJR21y9xsHV-PD_bYwbXSeNuFal46xYxVfRL5mqha7vttvjB_vc7Xg2RvgCxHPCqoxgMPTzHrZT75LjCwIW2K_klBYN8oYvTwwmeSkAz6ut7ZxPv-nZaT5TJhGk0NT2kh_zSpdriEJ_3vW-mqxYbbBmpvHqsa1_zx9fSuHYctAZJWzxzUZXykbWMWQZpEiE0J4ajj51fInEzVn7VxV-mzfMyboQjujPh7aNJxAWSq4oQEJJDgWwSh9leyoJoPpONHxh5nEE5AjE01FkGICSxjpZsF-w8hOTI3XXohUdu29Se26k2B0PolDSuj0GIQU6-W9TdLXSjBb2SpQ",
"e":"AQAB"
}`
func TestChallengesFor(t *testing.T) {
pa := paImpl(t)
challenges, combinations, err := pa.ChallengesFor(core.AcmeIdentifier{}, testRegID, false)
test.AssertNotError(t, err, "ChallengesFor failed")
test.Assert(t, len(challenges) == len(enabledChallenges), "Wrong number of challenges returned")
test.Assert(t, len(combinations) == len(enabledChallenges), "Wrong number of combinations returned")
seenChalls := make(map[string]bool)
// Expected only if the pseudo-RNG is seeded with 99.
expectedCombos := [][]int{{1}, {2}, {0}}
for _, challenge := range challenges {
test.Assert(t, !seenChalls[challenge.Type], "should not already have seen this type")
seenChalls[challenge.Type] = true
test.Assert(t, enabledChallenges[challenge.Type], "Unsupported challenge returned")
}
test.AssertEquals(t, len(seenChalls), len(enabledChallenges))
test.AssertDeepEquals(t, expectedCombos, combinations)
}
func TestChallengesForWhitelist(t *testing.T) {
enabledChallenges[core.ChallengeTypeTLSSNI01] = false
var enabledChallengesWhitelist = map[string][]int64{
core.ChallengeTypeHTTP01: []int64{},
core.ChallengeTypeTLSSNI01: []int64{testRegIDWhitelisted},
core.ChallengeTypeDNS01: []int64{},
}
pa := paImpl(t)
wlBytes, err := json.Marshal(enabledChallengesWhitelist)
test.AssertNotError(t, err, "Couldn't serialize whitelist")
f, _ := ioutil.TempFile("", "test-challenges-whitelist.json")
defer os.Remove(f.Name())
err = ioutil.WriteFile(f.Name(), wlBytes, 0640)
test.AssertNotError(t, err, "Couldn't write serialized whitelist to file")
err = pa.SetChallengesWhitelistFile(f.Name())
test.AssertNotError(t, err, "Couldn't load policy contents from file")
challenges, _, err := pa.ChallengesFor(core.AcmeIdentifier{}, testRegID, false)
test.AssertNotError(t, err, "ChallengesFor failed")
test.Assert(t, len(challenges) == len(enabledChallenges)-1, "Wrong number of challenges returned")
challenges, _, err = pa.ChallengesFor(core.AcmeIdentifier{}, testRegIDWhitelisted, false)
test.AssertNotError(t, err, "ChallengesFor failed")
test.Assert(t, len(challenges) == len(enabledChallenges), "Wrong number of challenges returned")
}
func TestChallengesForWildcard(t *testing.T) {
// wildcardIdent is an identifier for a wildcard domain name
wildcardIdent := core.AcmeIdentifier{
Type: core.IdentifierDNS,
Value: "*.zombo.com",
}
mustConstructPA := func(t *testing.T, enabledChallenges map[string]bool) *AuthorityImpl {
pa, err := New(enabledChallenges)
test.AssertNotError(t, err, "Couldn't create policy implementation")
return pa
}
// First try to get a challenge for the wildcard ident without the
// DNS-01 challenge type enabled. This should produce an error
var enabledChallenges = map[string]bool{
core.ChallengeTypeHTTP01: true,
core.ChallengeTypeTLSSNI01: true,
core.ChallengeTypeDNS01: false,
}
pa := mustConstructPA(t, enabledChallenges)
_, _, err := pa.ChallengesFor(wildcardIdent, testRegID, false)
test.AssertError(t, err, "ChallengesFor did not error for a wildcard ident "+
"when DNS-01 was disabled")
test.AssertEquals(t, err.Error(), "Challenges requested for wildcard "+
"identifier but DNS-01 challenge type is not enabled")
// Try again with DNS-01 enabled. It should not error and
// should return only one DNS-01 type challenge
enabledChallenges[core.ChallengeTypeDNS01] = true
pa = mustConstructPA(t, enabledChallenges)
challenges, combinations, err := pa.ChallengesFor(wildcardIdent, testRegID, false)
test.AssertNotError(t, err, "ChallengesFor errored for a wildcard ident "+
"unexpectedly")
test.AssertEquals(t, len(combinations), 1)
test.AssertEquals(t, len(challenges), 1)
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
// invalid exact blacklist entry will fail as expected.
func TestMalformedExactBlacklist(t *testing.T) {
pa := paImpl(t)
exactBannedDomains := []string{
// Only one label - not valid
"com",
}
bannedDomains := []string{
"placeholder.domain.not.important.for.this.test.com",
}
// Create JSON for the exactBannedDomains
bannedBytes, err := json.Marshal(blacklistJSON{
Blacklist: bannedDomains,
ExactBlacklist: exactBannedDomains,
})
test.AssertNotError(t, err, "Couldn't serialize banned list")
// Create a temp file for the JSON contents
f, _ := ioutil.TempFile("", "test-invalid-exactblacklist.json")
defer os.Remove(f.Name())
// Write the JSON to the temp file
err = ioutil.WriteFile(f.Name(), bannedBytes, 0640)
test.AssertNotError(t, err, "Couldn't write serialized banned list to file")
// Try to use the JSON tempfile as the hostname policy. It should produce an
// error since the exact blacklist contents are malformed.
err = pa.SetHostnamePolicyFile(f.Name())
test.AssertError(t, err, "Loaded invalid exact blacklist content without error")
test.AssertEquals(t, err.Error(), "Malformed exact blacklist entry, only one label: \"com\"")
}