package policy import ( "fmt" "net/netip" "os" "strings" "testing" "gopkg.in/yaml.v3" "github.com/letsencrypt/boulder/core" berrors "github.com/letsencrypt/boulder/errors" "github.com/letsencrypt/boulder/features" "github.com/letsencrypt/boulder/identifier" blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/test" ) func paImpl(t *testing.T) *AuthorityImpl { enabledChallenges := map[core.AcmeChallenge]bool{ core.ChallengeTypeHTTP01: true, core.ChallengeTypeDNS01: true, core.ChallengeTypeTLSALPN01: true, } enabledIdentifiers := map[identifier.IdentifierType]bool{ identifier.TypeDNS: true, identifier.TypeIP: true, } pa, err := New(enabledIdentifiers, enabledChallenges, blog.NewMock()) if err != nil { t.Fatalf("Couldn't create policy implementation: %s", err) } return pa } func TestWellFormedIdentifiers(t *testing.T) { testCases := []struct { ident identifier.ACMEIdentifier err error }{ // Invalid identifier types {identifier.ACMEIdentifier{}, errUnsupportedIdent}, // Empty identifier type {identifier.ACMEIdentifier{Type: "fnord", Value: "uh-oh, Spaghetti-Os[tm]"}, errUnsupportedIdent}, // Empty identifier values {identifier.NewDNS(``), errEmptyIdentifier}, // Empty DNS identifier {identifier.ACMEIdentifier{Type: "ip"}, errEmptyIdentifier}, // Empty IP identifier // DNS follies {identifier.NewDNS(`zomb!.com`), errInvalidDNSCharacter}, // ASCII character out of range {identifier.NewDNS(`emailaddress@myseriously.present.com`), errInvalidDNSCharacter}, {identifier.NewDNS(`user:pass@myseriously.present.com`), errInvalidDNSCharacter}, {identifier.NewDNS(`zömbo.com`), errInvalidDNSCharacter}, // non-ASCII character {identifier.NewDNS(`127.0.0.1`), errIPAddressInDNS}, // IPv4 address {identifier.NewDNS(`fe80::1:1`), errInvalidDNSCharacter}, // IPv6 address {identifier.NewDNS(`[2001:db8:85a3:8d3:1319:8a2e:370:7348]`), errInvalidDNSCharacter}, // unexpected IPv6 variants {identifier.NewDNS(`[2001:db8:85a3:8d3:1319:8a2e:370:7348]:443`), errInvalidDNSCharacter}, {identifier.NewDNS(`2001:db8::/32`), errInvalidDNSCharacter}, {identifier.NewDNS(`a.b.c.d.e.f.g.h.i.j.k`), errTooManyLabels}, // Too many labels (>10) {identifier.NewDNS(`www.0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef012345.com`), errNameTooLong}, // Too long (254 characters) {identifier.NewDNS(`www.ef0123456789abcdef013456789abcdef012345.789abcdef012345679abcdef0123456789abcdef01234.6789abcdef0123456789abcdef0.23456789abcdef0123456789a.cdef0123456789abcdef0123456789ab.def0123456789abcdef0123456789.bcdef0123456789abcdef012345.com`), nil}, // OK, not too long (240 characters) {identifier.NewDNS(`www.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz.com`), errLabelTooLong}, // Label too long (>63 characters) {identifier.NewDNS(`www.-ombo.com`), errInvalidDNSCharacter}, // Label starts with '-' {identifier.NewDNS(`www.zomb-.com`), errInvalidDNSCharacter}, // Label ends with '-' {identifier.NewDNS(`xn--.net`), errInvalidDNSCharacter}, // Label ends with '-' {identifier.NewDNS(`-0b.net`), errInvalidDNSCharacter}, // First label begins with '-' {identifier.NewDNS(`-0.net`), errInvalidDNSCharacter}, // First label begins with '-' {identifier.NewDNS(`-.net`), errInvalidDNSCharacter}, // First label is only '-' {identifier.NewDNS(`---.net`), errInvalidDNSCharacter}, // First label is only hyphens {identifier.NewDNS(`0`), errTooFewLabels}, {identifier.NewDNS(`1`), errTooFewLabels}, {identifier.NewDNS(`*`), errMalformedWildcard}, {identifier.NewDNS(`**`), errTooManyWildcards}, {identifier.NewDNS(`*.*`), errTooManyWildcards}, {identifier.NewDNS(`zombo*com`), errMalformedWildcard}, {identifier.NewDNS(`*.com`), errICANNTLDWildcard}, {identifier.NewDNS(`..a`), errLabelTooShort}, {identifier.NewDNS(`a..a`), errLabelTooShort}, {identifier.NewDNS(`.a..a`), errLabelTooShort}, {identifier.NewDNS(`..foo.com`), errLabelTooShort}, {identifier.NewDNS(`.`), errNameEndsInDot}, {identifier.NewDNS(`..`), errNameEndsInDot}, {identifier.NewDNS(`a..`), errNameEndsInDot}, {identifier.NewDNS(`.....`), errNameEndsInDot}, {identifier.NewDNS(`.a.`), errNameEndsInDot}, {identifier.NewDNS(`www.zombo.com.`), errNameEndsInDot}, {identifier.NewDNS(`www.zombo_com.com`), errInvalidDNSCharacter}, {identifier.NewDNS(`\uFEFF`), errInvalidDNSCharacter}, // Byte order mark {identifier.NewDNS(`\uFEFFwww.zombo.com`), errInvalidDNSCharacter}, {identifier.NewDNS(`www.zom\u202Ebo.com`), errInvalidDNSCharacter}, // Right-to-Left Override {identifier.NewDNS(`\u202Ewww.zombo.com`), errInvalidDNSCharacter}, {identifier.NewDNS(`www.zom\u200Fbo.com`), errInvalidDNSCharacter}, // Right-to-Left Mark {identifier.NewDNS(`\u200Fwww.zombo.com`), errInvalidDNSCharacter}, // Underscores are technically disallowed in DNS. Some DNS // implementations accept them but we will be conservative. {identifier.NewDNS(`www.zom_bo.com`), errInvalidDNSCharacter}, {identifier.NewDNS(`zombocom`), errTooFewLabels}, {identifier.NewDNS(`localhost`), errTooFewLabels}, {identifier.NewDNS(`mail`), errTooFewLabels}, // disallow capitalized letters for #927 {identifier.NewDNS(`CapitalizedLetters.com`), errInvalidDNSCharacter}, {identifier.NewDNS(`example.acting`), errNonPublic}, {identifier.NewDNS(`example.internal`), errNonPublic}, // All-numeric final label not okay. {identifier.NewDNS(`www.zombo.163`), errNonPublic}, {identifier.NewDNS(`xn--109-3veba6djs1bfxlfmx6c9g.xn--f1awi.xn--p1ai`), errMalformedIDN}, // Not in Unicode NFC {identifier.NewDNS(`bq--abwhky3f6fxq.jakacomo.com`), errInvalidRLDH}, // Three hyphens starting at third second char of first label. {identifier.NewDNS(`bq---abwhky3f6fxq.jakacomo.com`), errInvalidRLDH}, // Three hyphens starting at second char of first label. {identifier.NewDNS(`h---test.hk2yz.org`), errInvalidRLDH}, {identifier.NewDNS(`co.uk`), errICANNTLD}, {identifier.NewDNS(`foo.bd`), errICANNTLD}, // IP oopsies {identifier.ACMEIdentifier{Type: "ip", Value: `zombo.com`}, errIPInvalid}, // That's DNS! // Unexpected IPv4 variants {identifier.ACMEIdentifier{Type: "ip", Value: `192.168.1.1.1`}, errIPInvalid}, // extra octet {identifier.ACMEIdentifier{Type: "ip", Value: `192.168.1.256`}, errIPInvalid}, // octet out of range {identifier.ACMEIdentifier{Type: "ip", Value: `192.168.1.a1`}, errIPInvalid}, // character out of range {identifier.ACMEIdentifier{Type: "ip", Value: `192.168.1.0/24`}, errIPInvalid}, // with CIDR {identifier.ACMEIdentifier{Type: "ip", Value: `192.168.1.1:443`}, errIPInvalid}, // with port {identifier.ACMEIdentifier{Type: "ip", Value: `0xc0a80101`}, errIPInvalid}, // as hex {identifier.ACMEIdentifier{Type: "ip", Value: `1.1.168.192.in-addr.arpa`}, errIPInvalid}, // reverse DNS // Unexpected IPv6 variants {identifier.ACMEIdentifier{Type: "ip", Value: `3fff:aaa:a:c0ff:ee:a:bad:deed:ffff`}, errIPInvalid}, // extra octet {identifier.ACMEIdentifier{Type: "ip", Value: `3fff:aaa:a:c0ff:ee:a:bad:mead`}, errIPInvalid}, // character out of range {identifier.ACMEIdentifier{Type: "ip", Value: `2001:db8::/32`}, errIPInvalid}, // with CIDR {identifier.ACMEIdentifier{Type: "ip", Value: `[3fff:aaa:a:c0ff:ee:a:bad:deed]`}, errIPInvalid}, // in brackets {identifier.ACMEIdentifier{Type: "ip", Value: `[3fff:aaa:a:c0ff:ee:a:bad:deed]:443`}, errIPInvalid}, // in brackets, with port {identifier.ACMEIdentifier{Type: "ip", Value: `0x3fff0aaa000ac0ff00ee000a0baddeed`}, errIPInvalid}, // as hex {identifier.ACMEIdentifier{Type: "ip", Value: `d.e.e.d.d.a.b.0.a.0.0.0.e.e.0.0.f.f.0.c.a.0.0.0.a.a.a.0.f.f.f.3.ip6.arpa`}, errIPInvalid}, // reverse DNS {identifier.ACMEIdentifier{Type: "ip", Value: `3fff:0aaa:a:c0ff:ee:a:bad:deed`}, errIPInvalid}, // leading 0 in 2nd octet (RFC 5952, Sec. 4.1) {identifier.ACMEIdentifier{Type: "ip", Value: `3fff:aaa:0:0:0:a:bad:deed`}, errIPInvalid}, // lone 0s in 3rd-5th octets, :: not used (RFC 5952, Sec. 4.2.1) {identifier.ACMEIdentifier{Type: "ip", Value: `3fff:aaa::c0ff:ee:a:bad:deed`}, errIPInvalid}, // :: used for just one empty octet (RFC 5952, Sec. 4.2.2) {identifier.ACMEIdentifier{Type: "ip", Value: `3fff:aaa::ee:0:0:0`}, errIPInvalid}, // :: used for the shorter of two possible collapses (RFC 5952, Sec. 4.2.3) {identifier.ACMEIdentifier{Type: "ip", Value: `fe80:0:0:0:a::`}, errIPInvalid}, // :: used for the last of two possible equal-length collapses (RFC 5952, Sec. 4.2.3) {identifier.ACMEIdentifier{Type: "ip", Value: `3fff:aaa:a:C0FF:EE:a:bad:deed`}, errIPInvalid}, // alpha characters capitalized (RFC 5952, Sec. 4.3) {identifier.ACMEIdentifier{Type: "ip", Value: `::ffff:192.168.1.1`}, errIPInvalid}, // IPv6-encapsulated IPv4 // IANA special-purpose address blocks {identifier.NewIP(netip.MustParseAddr("192.0.2.129")), errIPSpecialPurpose}, // Documentation (TEST-NET-1) {identifier.NewIP(netip.MustParseAddr("2001:db8:eee:eeee:eeee:eeee:d01:f1")), errIPSpecialPurpose}, // Documentation } // Test syntax errors for _, tc := range testCases { err := WellFormedIdentifiers(identifier.ACMEIdentifiers{tc.ident}) if tc.err == nil { test.AssertNil(t, err, fmt.Sprintf("Unexpected error for %q identifier %q, got %s", tc.ident.Type, tc.ident.Value, err)) } else { test.AssertError(t, err, fmt.Sprintf("Expected error for %q identifier %q, but got none", tc.ident.Type, tc.ident.Value)) var berr *berrors.BoulderError test.AssertErrorWraps(t, err, &berr) test.AssertContains(t, berr.Error(), tc.err.Error()) } } } func TestWillingToIssue(t *testing.T) { shouldBeBlocked := identifier.ACMEIdentifiers{ identifier.NewDNS(`highvalue.website1.org`), identifier.NewDNS(`website2.co.uk`), identifier.NewDNS(`www.website3.com`), identifier.NewDNS(`lots.of.labels.website4.com`), identifier.NewDNS(`banned.in.dc.com`), identifier.NewDNS(`bad.brains.banned.in.dc.com`), } blocklistContents := []string{ `website2.com`, `website2.org`, `website2.co.uk`, `website3.com`, `website4.com`, } exactBlocklistContents := []string{ `www.website1.org`, `highvalue.website1.org`, `dl.website1.org`, } adminBlockedContents := []string{ `banned.in.dc.com`, } shouldBeAccepted := identifier.ACMEIdentifiers{ identifier.NewDNS(`lowvalue.website1.org`), identifier.NewDNS(`website4.sucks`), identifier.NewDNS(`www.unrelated.com`), identifier.NewDNS(`unrelated.com`), identifier.NewDNS(`www.8675309.com`), identifier.NewDNS(`8675309.com`), identifier.NewDNS(`web5ite2.com`), identifier.NewDNS(`www.web-site2.com`), identifier.NewIP(netip.MustParseAddr(`9.9.9.9`)), identifier.NewIP(netip.MustParseAddr(`2620:fe::fe`)), } policy := blockedNamesPolicy{ HighRiskBlockedNames: blocklistContents, ExactBlockedNames: exactBlocklistContents, AdminBlockedNames: adminBlockedContents, } yamlPolicyBytes, err := yaml.Marshal(policy) test.AssertNotError(t, err, "Couldn't YAML serialize blocklist") yamlPolicyFile, _ := os.CreateTemp("", "test-blocklist.*.yaml") defer os.Remove(yamlPolicyFile.Name()) err = os.WriteFile(yamlPolicyFile.Name(), yamlPolicyBytes, 0640) test.AssertNotError(t, err, "Couldn't write YAML blocklist") pa := paImpl(t) err = pa.LoadHostnamePolicyFile(yamlPolicyFile.Name()) test.AssertNotError(t, err, "Couldn't load rules") // Invalid encoding err = pa.WillingToIssue(identifier.ACMEIdentifiers{identifier.NewDNS("www.xn--m.com")}) test.AssertError(t, err, "WillingToIssue didn't fail on a malformed IDN") // Invalid identifier type err = pa.WillingToIssue(identifier.ACMEIdentifiers{identifier.ACMEIdentifier{Type: "fnord", Value: "uh-oh, Spaghetti-Os[tm]"}}) test.AssertError(t, err, "WillingToIssue didn't fail on an invalid identifier type") // Valid encoding err = pa.WillingToIssue(identifier.ACMEIdentifiers{identifier.NewDNS("www.xn--mnich-kva.com")}) test.AssertNotError(t, err, "WillingToIssue failed on a properly formed IDN") // IDN TLD err = pa.WillingToIssue(identifier.ACMEIdentifiers{identifier.NewDNS("xn--example--3bhk5a.xn--p1ai")}) test.AssertNotError(t, err, "WillingToIssue failed on a properly formed domain with IDN TLD") features.Reset() // Test expected blocked domains for _, ident := range shouldBeBlocked { err := pa.WillingToIssue(identifier.ACMEIdentifiers{ident}) test.AssertError(t, err, "identifier was not correctly forbidden") var berr *berrors.BoulderError test.AssertErrorWraps(t, err, &berr) test.AssertContains(t, berr.Detail, errPolicyForbidden.Error()) } // Test acceptance of good names for _, ident := range shouldBeAccepted { err := pa.WillingToIssue(identifier.ACMEIdentifiers{ident}) test.AssertNotError(t, err, "identifier was incorrectly forbidden") } } func TestWillingToIssue_Wildcards(t *testing.T) { bannedDomains := []string{ "zombo.gov.us", } exactBannedDomains := []string{ "highvalue.letsdecrypt.org", } pa := paImpl(t) bannedBytes, err := yaml.Marshal(blockedNamesPolicy{ HighRiskBlockedNames: bannedDomains, ExactBlockedNames: exactBannedDomains, }) test.AssertNotError(t, err, "Couldn't serialize banned list") f, _ := os.CreateTemp("", "test-wildcard-banlist.*.yaml") defer os.Remove(f.Name()) err = os.WriteFile(f.Name(), bannedBytes, 0640) test.AssertNotError(t, err, "Couldn't write serialized banned list to file") err = pa.LoadHostnamePolicyFile(f.Name()) test.AssertNotError(t, err, "Couldn't load policy contents from file") testCases := []struct { Name string Domain string ExpectedErr error }{ { Name: "Too many wildcards", Domain: "ok.*.whatever.*.example.com", ExpectedErr: errTooManyWildcards, }, { Name: "Misplaced wildcard", Domain: "ok.*.whatever.example.com", ExpectedErr: errMalformedWildcard, }, { Name: "Missing ICANN TLD", Domain: "*.ok.madeup", ExpectedErr: errNonPublic, }, { Name: "Wildcard for ICANN TLD", Domain: "*.com", ExpectedErr: errICANNTLDWildcard, }, { Name: "Forbidden base domain", Domain: "*.zombo.gov.us", ExpectedErr: errPolicyForbidden, }, // We should not allow getting a wildcard for that would cover an exact // blocklist domain { Name: "Wildcard for ExactBlocklist base domain", Domain: "*.letsdecrypt.org", ExpectedErr: errPolicyForbidden, }, // We should allow a wildcard for a domain that doesn't match the exact // blocklist domain { Name: "Wildcard for non-matching subdomain of ExactBlocklist domain", Domain: "*.lowvalue.letsdecrypt.org", ExpectedErr: nil, }, // We should allow getting a wildcard for an exact blocklist domain since it // only covers subdomains, not the exact name. { Name: "Wildcard for ExactBlocklist domain", Domain: "*.highvalue.letsdecrypt.org", ExpectedErr: nil, }, { Name: "Valid wildcard domain", Domain: "*.everything.is.possible.at.zombo.com", ExpectedErr: nil, }, } for _, tc := range testCases { t.Run(tc.Name, func(t *testing.T) { err := pa.WillingToIssue(identifier.ACMEIdentifiers{identifier.NewDNS(tc.Domain)}) if tc.ExpectedErr == nil { test.AssertNil(t, err, fmt.Sprintf("Unexpected error for domain %q, got %s", tc.Domain, err)) } else { test.AssertError(t, err, fmt.Sprintf("Expected error for domain %q, but got none", tc.Domain)) var berr *berrors.BoulderError test.AssertErrorWraps(t, err, &berr) test.AssertContains(t, berr.Error(), tc.ExpectedErr.Error()) } }) } } // TestWillingToIssue_SubErrors tests that more than one rejected identifier // results in an error with suberrors. func TestWillingToIssue_SubErrors(t *testing.T) { banned := []string{ "letsdecrypt.org", "example.com", } pa := paImpl(t) bannedBytes, err := yaml.Marshal(blockedNamesPolicy{ HighRiskBlockedNames: banned, ExactBlockedNames: banned, }) test.AssertNotError(t, err, "Couldn't serialize banned list") f, _ := os.CreateTemp("", "test-wildcard-banlist.*.yaml") defer os.Remove(f.Name()) err = os.WriteFile(f.Name(), bannedBytes, 0640) test.AssertNotError(t, err, "Couldn't write serialized banned list to file") err = pa.LoadHostnamePolicyFile(f.Name()) test.AssertNotError(t, err, "Couldn't load policy contents from file") // Test multiple malformed domains and one banned domain; only the malformed ones will generate errors err = pa.WillingToIssue(identifier.ACMEIdentifiers{ identifier.NewDNS("perfectly-fine.com"), // fine identifier.NewDNS("letsdecrypt_org"), // malformed identifier.NewDNS("example.comm"), // malformed identifier.NewDNS("letsdecrypt.org"), // banned identifier.NewDNS("also-perfectly-fine.com"), // fine }) test.AssertDeepEquals(t, err, &berrors.BoulderError{ Type: berrors.RejectedIdentifier, Detail: "Cannot issue for \"letsdecrypt_org\": Domain name contains an invalid character (and 1 more problems. Refer to sub-problems for more information.)", SubErrors: []berrors.SubBoulderError{ { BoulderError: &berrors.BoulderError{ Type: berrors.Malformed, Detail: "Domain name contains an invalid character", }, Identifier: identifier.NewDNS("letsdecrypt_org"), }, { BoulderError: &berrors.BoulderError{ Type: berrors.Malformed, Detail: "Domain name does not end with a valid public suffix (TLD)", }, Identifier: identifier.NewDNS("example.comm"), }, }, }) // Test multiple banned domains. err = pa.WillingToIssue(identifier.ACMEIdentifiers{ identifier.NewDNS("perfectly-fine.com"), // fine identifier.NewDNS("letsdecrypt.org"), // banned identifier.NewDNS("example.com"), // banned identifier.NewDNS("also-perfectly-fine.com"), // fine }) test.AssertError(t, err, "Expected err from WillingToIssueWildcards") test.AssertDeepEquals(t, err, &berrors.BoulderError{ Type: berrors.RejectedIdentifier, Detail: "Cannot issue for \"letsdecrypt.org\": The ACME server refuses to issue a certificate for this domain name, because it is forbidden by policy (and 1 more problems. Refer to sub-problems for more information.)", SubErrors: []berrors.SubBoulderError{ { BoulderError: &berrors.BoulderError{ Type: berrors.RejectedIdentifier, Detail: "The ACME server refuses to issue a certificate for this domain name, because it is forbidden by policy", }, Identifier: identifier.NewDNS("letsdecrypt.org"), }, { BoulderError: &berrors.BoulderError{ Type: berrors.RejectedIdentifier, Detail: "The ACME server refuses to issue a certificate for this domain name, because it is forbidden by policy", }, Identifier: identifier.NewDNS("example.com"), }, }, }) // Test willing to issue with only *one* bad identifier. err = pa.WillingToIssue(identifier.ACMEIdentifiers{identifier.NewDNS("letsdecrypt.org")}) test.AssertDeepEquals(t, err, &berrors.BoulderError{ Type: berrors.RejectedIdentifier, Detail: "Cannot issue for \"letsdecrypt.org\": The ACME server refuses to issue a certificate for this domain name, because it is forbidden by policy", }) } func TestChallengeTypesFor(t *testing.T) { t.Parallel() pa := paImpl(t) testCases := []struct { name string ident identifier.ACMEIdentifier wantChalls []core.AcmeChallenge wantErr string }{ { name: "dns", ident: identifier.NewDNS("example.com"), wantChalls: []core.AcmeChallenge{ core.ChallengeTypeHTTP01, core.ChallengeTypeDNS01, core.ChallengeTypeTLSALPN01, }, }, { name: "dns wildcard", ident: identifier.NewDNS("*.example.com"), wantChalls: []core.AcmeChallenge{ core.ChallengeTypeDNS01, }, }, { name: "ip", ident: identifier.NewIP(netip.MustParseAddr("1.2.3.4")), wantChalls: []core.AcmeChallenge{ core.ChallengeTypeHTTP01, core.ChallengeTypeTLSALPN01, }, }, { name: "invalid", ident: identifier.ACMEIdentifier{Type: "fnord", Value: "uh-oh, Spaghetti-Os[tm]"}, wantErr: "unrecognized identifier type", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { t.Parallel() challs, err := pa.ChallengeTypesFor(tc.ident) if len(tc.wantChalls) != 0 { test.AssertNotError(t, err, "should have succeeded") test.AssertDeepEquals(t, challs, tc.wantChalls) } if tc.wantErr != "" { test.AssertError(t, err, "should have errored") test.AssertContains(t, err.Error(), tc.wantErr) } }) } } // TestMalformedExactBlocklist tests that loading a YAML policy file with an // invalid exact blocklist entry will fail as expected. func TestMalformedExactBlocklist(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 YAML for the exactBannedDomains bannedBytes, err := yaml.Marshal(blockedNamesPolicy{ HighRiskBlockedNames: bannedDomains, ExactBlockedNames: exactBannedDomains, }) test.AssertNotError(t, err, "Couldn't serialize banned list") // Create a temp file for the YAML contents f, _ := os.CreateTemp("", "test-invalid-exactblocklist.*.yaml") defer os.Remove(f.Name()) // Write the YAML to the temp file err = os.WriteFile(f.Name(), bannedBytes, 0640) test.AssertNotError(t, err, "Couldn't write serialized banned list to file") // Try to use the YAML tempfile as the hostname policy. It should produce an // error since the exact blocklist contents are malformed. err = pa.LoadHostnamePolicyFile(f.Name()) test.AssertError(t, err, "Loaded invalid exact blocklist content without error") test.AssertEquals(t, err.Error(), "Malformed ExactBlockedNames entry, only one label: \"com\"") } func TestValidEmailError(t *testing.T) { err := ValidEmail("(๑•́ ω •̀๑)") test.AssertEquals(t, err.Error(), "unable to parse email address") err = ValidEmail("john.smith@gmail.com #replace with real email") test.AssertEquals(t, err.Error(), "unable to parse email address") err = ValidEmail("example@example.com") test.AssertEquals(t, err.Error(), "contact email has forbidden domain \"example.com\"") err = ValidEmail("example@-foobar.com") test.AssertEquals(t, err.Error(), "contact email has invalid domain: Domain name contains an invalid character") } func TestCheckAuthzChallenges(t *testing.T) { t.Parallel() testCases := []struct { name string authz core.Authorization enabled map[core.AcmeChallenge]bool wantErr string }{ { name: "unrecognized identifier", authz: core.Authorization{ Identifier: identifier.ACMEIdentifier{Type: "oops", Value: "example.com"}, Challenges: []core.Challenge{{Type: core.ChallengeTypeDNS01, Status: core.StatusValid}}, }, wantErr: "unrecognized identifier type", }, { name: "no challenges", authz: core.Authorization{ Identifier: identifier.NewDNS("example.com"), Challenges: []core.Challenge{}, }, wantErr: "has no challenges", }, { name: "no valid challenges", authz: core.Authorization{ Identifier: identifier.NewDNS("example.com"), Challenges: []core.Challenge{{Type: core.ChallengeTypeDNS01, Status: core.StatusPending}}, }, wantErr: "not solved by any challenge", }, { name: "solved by disabled challenge", authz: core.Authorization{ Identifier: identifier.NewDNS("example.com"), Challenges: []core.Challenge{{Type: core.ChallengeTypeDNS01, Status: core.StatusValid}}, }, enabled: map[core.AcmeChallenge]bool{core.ChallengeTypeHTTP01: true}, wantErr: "disabled challenge type", }, { name: "solved by wrong kind of challenge", authz: core.Authorization{ Identifier: identifier.NewDNS("*.example.com"), Challenges: []core.Challenge{{Type: core.ChallengeTypeHTTP01, Status: core.StatusValid}}, }, wantErr: "inapplicable challenge type", }, { name: "valid authz", authz: core.Authorization{ Identifier: identifier.NewDNS("example.com"), Challenges: []core.Challenge{{Type: core.ChallengeTypeTLSALPN01, Status: core.StatusValid}}, }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { t.Parallel() pa := paImpl(t) if tc.enabled != nil { pa.enabledChallenges = tc.enabled } err := pa.CheckAuthzChallenges(&tc.authz) if tc.wantErr == "" { test.AssertNotError(t, err, "should have succeeded") } else { test.AssertError(t, err, "should have errored") test.AssertContains(t, err.Error(), tc.wantErr) } }) } } func TestWillingToIssue_IdentifierType(t *testing.T) { t.Parallel() testCases := []struct { name string ident identifier.ACMEIdentifier enabled map[identifier.IdentifierType]bool wantErr string }{ { name: "DNS identifier, none enabled", ident: identifier.NewDNS("example.com"), enabled: nil, wantErr: "The ACME server has disabled this identifier type", }, { name: "DNS identifier, DNS enabled", ident: identifier.NewDNS("example.com"), enabled: map[identifier.IdentifierType]bool{identifier.TypeDNS: true}, wantErr: "", }, { name: "DNS identifier, DNS & IP enabled", ident: identifier.NewDNS("example.com"), enabled: map[identifier.IdentifierType]bool{identifier.TypeDNS: true, identifier.TypeIP: true}, wantErr: "", }, { name: "DNS identifier, IP enabled", ident: identifier.NewDNS("example.com"), enabled: map[identifier.IdentifierType]bool{identifier.TypeIP: true}, wantErr: "The ACME server has disabled this identifier type", }, { name: "IP identifier, none enabled", ident: identifier.NewIP(netip.MustParseAddr("9.9.9.9")), enabled: nil, wantErr: "The ACME server has disabled this identifier type", }, { name: "IP identifier, DNS enabled", ident: identifier.NewIP(netip.MustParseAddr("9.9.9.9")), enabled: map[identifier.IdentifierType]bool{identifier.TypeDNS: true}, wantErr: "The ACME server has disabled this identifier type", }, { name: "IP identifier, DNS & IP enabled", ident: identifier.NewIP(netip.MustParseAddr("9.9.9.9")), enabled: map[identifier.IdentifierType]bool{identifier.TypeDNS: true, identifier.TypeIP: true}, wantErr: "", }, { name: "IP identifier, IP enabled", ident: identifier.NewIP(netip.MustParseAddr("9.9.9.9")), enabled: map[identifier.IdentifierType]bool{identifier.TypeIP: true}, wantErr: "", }, { name: "invalid identifier type", ident: identifier.ACMEIdentifier{Type: "drywall", Value: "oh yeah!"}, enabled: map[identifier.IdentifierType]bool{"drywall": true}, wantErr: "Invalid identifier type", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { t.Parallel() policy := blockedNamesPolicy{ HighRiskBlockedNames: []string{"zombo.gov.us"}, ExactBlockedNames: []string{`highvalue.website1.org`}, AdminBlockedNames: []string{`banned.in.dc.com`}, } yamlPolicyBytes, err := yaml.Marshal(policy) test.AssertNotError(t, err, "Couldn't YAML serialize blocklist") yamlPolicyFile, _ := os.CreateTemp("", "test-blocklist.*.yaml") defer os.Remove(yamlPolicyFile.Name()) err = os.WriteFile(yamlPolicyFile.Name(), yamlPolicyBytes, 0640) test.AssertNotError(t, err, "Couldn't write YAML blocklist") pa := paImpl(t) err = pa.LoadHostnamePolicyFile(yamlPolicyFile.Name()) test.AssertNotError(t, err, "Couldn't load rules") pa.enabledIdentifiers = tc.enabled err = pa.WillingToIssue(identifier.ACMEIdentifiers{tc.ident}) if tc.wantErr == "" { if err != nil { t.Errorf("should have succeeded, but got error: %s", err.Error()) } } else { if err == nil { t.Errorf("should have failed") } else if !strings.Contains(err.Error(), tc.wantErr) { t.Errorf("wrong error; wanted '%s', but got '%s'", tc.wantErr, err.Error()) } } }) } }