github.com/letsencrypt/boulder@v0.20251208.0/policy/pa_test.go (about)

     1  package policy
     2  
     3  import (
     4  	"fmt"
     5  	"net/netip"
     6  	"os"
     7  	"strings"
     8  	"testing"
     9  
    10  	"gopkg.in/yaml.v3"
    11  
    12  	"github.com/letsencrypt/boulder/core"
    13  	berrors "github.com/letsencrypt/boulder/errors"
    14  	"github.com/letsencrypt/boulder/features"
    15  	"github.com/letsencrypt/boulder/identifier"
    16  	blog "github.com/letsencrypt/boulder/log"
    17  	"github.com/letsencrypt/boulder/test"
    18  )
    19  
    20  func paImpl(t *testing.T) *AuthorityImpl {
    21  	enabledChallenges := map[core.AcmeChallenge]bool{
    22  		core.ChallengeTypeHTTP01:       true,
    23  		core.ChallengeTypeDNS01:        true,
    24  		core.ChallengeTypeTLSALPN01:    true,
    25  		core.ChallengeTypeDNSAccount01: true,
    26  	}
    27  
    28  	enabledIdentifiers := map[identifier.IdentifierType]bool{
    29  		identifier.TypeDNS: true,
    30  		identifier.TypeIP:  true,
    31  	}
    32  
    33  	pa, err := New(enabledIdentifiers, enabledChallenges, blog.NewMock())
    34  	if err != nil {
    35  		t.Fatalf("Couldn't create policy implementation: %s", err)
    36  	}
    37  	return pa
    38  }
    39  
    40  func TestWellFormedIdentifiers(t *testing.T) {
    41  	testCases := []struct {
    42  		ident identifier.ACMEIdentifier
    43  		err   error
    44  	}{
    45  		// Invalid identifier types
    46  		{identifier.ACMEIdentifier{}, errUnsupportedIdent}, // Empty identifier type
    47  		{identifier.ACMEIdentifier{Type: "fnord", Value: "uh-oh, Spaghetti-Os[tm]"}, errUnsupportedIdent},
    48  
    49  		// Empty identifier values
    50  		{identifier.NewDNS(``), errEmptyIdentifier},                 // Empty DNS identifier
    51  		{identifier.ACMEIdentifier{Type: "ip"}, errEmptyIdentifier}, // Empty IP identifier
    52  
    53  		// DNS follies
    54  
    55  		{identifier.NewDNS(`zomb!.com`), errInvalidDNSCharacter}, // ASCII character out of range
    56  		{identifier.NewDNS(`emailaddress@myseriously.present.com`), errInvalidDNSCharacter},
    57  		{identifier.NewDNS(`user:pass@myseriously.present.com`), errInvalidDNSCharacter},
    58  		{identifier.NewDNS(`zömbo.com`), errInvalidDNSCharacter},                              // non-ASCII character
    59  		{identifier.NewDNS(`127.0.0.1`), errIPAddressInDNS},                                   // IPv4 address
    60  		{identifier.NewDNS(`fe80::1:1`), errInvalidDNSCharacter},                              // IPv6 address
    61  		{identifier.NewDNS(`[2001:db8:85a3:8d3:1319:8a2e:370:7348]`), errInvalidDNSCharacter}, // unexpected IPv6 variants
    62  		{identifier.NewDNS(`[2001:db8:85a3:8d3:1319:8a2e:370:7348]:443`), errInvalidDNSCharacter},
    63  		{identifier.NewDNS(`2001:db8::/32`), errInvalidDNSCharacter},
    64  		{identifier.NewDNS(`a.b.c.d.e.f.g.h.i.j.k`), errTooManyLabels}, // Too many labels (>10)
    65  
    66  		{identifier.NewDNS(`www.0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef012345.com`), errNameTooLong}, // Too long (254 characters)
    67  
    68  		{identifier.NewDNS(`www.ef0123456789abcdef013456789abcdef012345.789abcdef012345679abcdef0123456789abcdef01234.6789abcdef0123456789abcdef0.23456789abcdef0123456789a.cdef0123456789abcdef0123456789ab.def0123456789abcdef0123456789.bcdef0123456789abcdef012345.com`), nil}, // OK, not too long (240 characters)
    69  
    70  		{identifier.NewDNS(`www.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz.com`), errLabelTooLong}, // Label too long (>63 characters)
    71  
    72  		{identifier.NewDNS(`www.-ombo.com`), errInvalidDNSCharacter}, // Label starts with '-'
    73  		{identifier.NewDNS(`www.zomb-.com`), errInvalidDNSCharacter}, // Label ends with '-'
    74  		{identifier.NewDNS(`xn--.net`), errInvalidDNSCharacter},      // Label ends with '-'
    75  		{identifier.NewDNS(`-0b.net`), errInvalidDNSCharacter},       // First label begins with '-'
    76  		{identifier.NewDNS(`-0.net`), errInvalidDNSCharacter},        // First label begins with '-'
    77  		{identifier.NewDNS(`-.net`), errInvalidDNSCharacter},         // First label is only '-'
    78  		{identifier.NewDNS(`---.net`), errInvalidDNSCharacter},       // First label is only hyphens
    79  		{identifier.NewDNS(`0`), errTooFewLabels},
    80  		{identifier.NewDNS(`1`), errTooFewLabels},
    81  		{identifier.NewDNS(`*`), errMalformedWildcard},
    82  		{identifier.NewDNS(`**`), errTooManyWildcards},
    83  		{identifier.NewDNS(`*.*`), errTooManyWildcards},
    84  		{identifier.NewDNS(`zombo*com`), errMalformedWildcard},
    85  		{identifier.NewDNS(`*.com`), errICANNTLDWildcard},
    86  		{identifier.NewDNS(`..a`), errLabelTooShort},
    87  		{identifier.NewDNS(`a..a`), errLabelTooShort},
    88  		{identifier.NewDNS(`.a..a`), errLabelTooShort},
    89  		{identifier.NewDNS(`..foo.com`), errLabelTooShort},
    90  		{identifier.NewDNS(`.`), errNameEndsInDot},
    91  		{identifier.NewDNS(`..`), errNameEndsInDot},
    92  		{identifier.NewDNS(`a..`), errNameEndsInDot},
    93  		{identifier.NewDNS(`.....`), errNameEndsInDot},
    94  		{identifier.NewDNS(`.a.`), errNameEndsInDot},
    95  		{identifier.NewDNS(`www.zombo.com.`), errNameEndsInDot},
    96  		{identifier.NewDNS(`www.zombo_com.com`), errInvalidDNSCharacter},
    97  		{identifier.NewDNS(`\uFEFF`), errInvalidDNSCharacter}, // Byte order mark
    98  		{identifier.NewDNS(`\uFEFFwww.zombo.com`), errInvalidDNSCharacter},
    99  		{identifier.NewDNS(`www.zom\u202Ebo.com`), errInvalidDNSCharacter}, // Right-to-Left Override
   100  		{identifier.NewDNS(`\u202Ewww.zombo.com`), errInvalidDNSCharacter},
   101  		{identifier.NewDNS(`www.zom\u200Fbo.com`), errInvalidDNSCharacter}, // Right-to-Left Mark
   102  		{identifier.NewDNS(`\u200Fwww.zombo.com`), errInvalidDNSCharacter},
   103  		// Underscores are technically disallowed in DNS. Some DNS
   104  		// implementations accept them but we will be conservative.
   105  		{identifier.NewDNS(`www.zom_bo.com`), errInvalidDNSCharacter},
   106  		{identifier.NewDNS(`zombocom`), errTooFewLabels},
   107  		{identifier.NewDNS(`localhost`), errTooFewLabels},
   108  		{identifier.NewDNS(`mail`), errTooFewLabels},
   109  
   110  		// disallow capitalized letters for #927
   111  		{identifier.NewDNS(`CapitalizedLetters.com`), errInvalidDNSCharacter},
   112  
   113  		{identifier.NewDNS(`example.acting`), errNonPublic},
   114  		{identifier.NewDNS(`example.internal`), errNonPublic},
   115  		// All-numeric final label not okay.
   116  		{identifier.NewDNS(`www.zombo.163`), errNonPublic},
   117  		{identifier.NewDNS(`xn--109-3veba6djs1bfxlfmx6c9g.xn--f1awi.xn--p1ai`), errMalformedIDN}, // Not in Unicode NFC
   118  		{identifier.NewDNS(`bq--abwhky3f6fxq.jakacomo.com`), errInvalidRLDH},
   119  		// Three hyphens starting at third second char of first label.
   120  		{identifier.NewDNS(`bq---abwhky3f6fxq.jakacomo.com`), errInvalidRLDH},
   121  		// Three hyphens starting at second char of first label.
   122  		{identifier.NewDNS(`h---test.hk2yz.org`), errInvalidRLDH},
   123  		{identifier.NewDNS(`co.uk`), errICANNTLD},
   124  		{identifier.NewDNS(`foo.er`), errICANNTLD},
   125  
   126  		// IP oopsies
   127  
   128  		{identifier.ACMEIdentifier{Type: "ip", Value: `zombo.com`}, errIPInvalid}, // That's DNS!
   129  
   130  		// Unexpected IPv4 variants
   131  		{identifier.ACMEIdentifier{Type: "ip", Value: `192.168.1.1.1`}, errIPInvalid},            // extra octet
   132  		{identifier.ACMEIdentifier{Type: "ip", Value: `192.168.1.256`}, errIPInvalid},            // octet out of range
   133  		{identifier.ACMEIdentifier{Type: "ip", Value: `192.168.1.a1`}, errIPInvalid},             // character out of range
   134  		{identifier.ACMEIdentifier{Type: "ip", Value: `192.168.1.0/24`}, errIPInvalid},           // with CIDR
   135  		{identifier.ACMEIdentifier{Type: "ip", Value: `192.168.1.1:443`}, errIPInvalid},          // with port
   136  		{identifier.ACMEIdentifier{Type: "ip", Value: `0xc0a80101`}, errIPInvalid},               // as hex
   137  		{identifier.ACMEIdentifier{Type: "ip", Value: `1.1.168.192.in-addr.arpa`}, errIPInvalid}, // reverse DNS
   138  
   139  		// Unexpected IPv6 variants
   140  		{identifier.ACMEIdentifier{Type: "ip", Value: `2602:80a:6000:abad:cafe::1%lo`}, errIPInvalid},                                             // scope zone (RFC 4007)
   141  		{identifier.ACMEIdentifier{Type: "ip", Value: `2602:80a:6000:abad:cafe::1%`}, errIPInvalid},                                               // empty scope zone (RFC 4007)
   142  		{identifier.ACMEIdentifier{Type: "ip", Value: `3fff:aaa:a:c0ff:ee:a:bad:deed:ffff`}, errIPInvalid},                                        // extra octet
   143  		{identifier.ACMEIdentifier{Type: "ip", Value: `3fff:aaa:a:c0ff:ee:a:bad:mead`}, errIPInvalid},                                             // character out of range
   144  		{identifier.ACMEIdentifier{Type: "ip", Value: `2001:db8::/32`}, errIPInvalid},                                                             // with CIDR
   145  		{identifier.ACMEIdentifier{Type: "ip", Value: `[3fff:aaa:a:c0ff:ee:a:bad:deed]`}, errIPInvalid},                                           // in brackets
   146  		{identifier.ACMEIdentifier{Type: "ip", Value: `[3fff:aaa:a:c0ff:ee:a:bad:deed]:443`}, errIPInvalid},                                       // in brackets, with port
   147  		{identifier.ACMEIdentifier{Type: "ip", Value: `0x3fff0aaa000ac0ff00ee000a0baddeed`}, errIPInvalid},                                        // as hex
   148  		{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
   149  		{identifier.ACMEIdentifier{Type: "ip", Value: `3fff:0aaa:a:c0ff:ee:a:bad:deed`}, errIPInvalid},                                            // leading 0 in 2nd octet (RFC 5952, Sec. 4.1)
   150  		{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)
   151  		{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)
   152  		{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)
   153  		{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)
   154  		{identifier.ACMEIdentifier{Type: "ip", Value: `3fff:aaa:a:C0FF:EE:a:bad:deed`}, errIPInvalid},                                             // alpha characters capitalized (RFC 5952, Sec. 4.3)
   155  		{identifier.ACMEIdentifier{Type: "ip", Value: `::ffff:192.168.1.1`}, berrors.MalformedError("IP address is in a reserved address block")}, // IPv6-encapsulated IPv4
   156  
   157  		// IANA special-purpose address blocks
   158  		{identifier.NewIP(netip.MustParseAddr("192.0.2.129")), berrors.MalformedError("IP address is in a reserved address block")},                        // Documentation (TEST-NET-1)
   159  		{identifier.NewIP(netip.MustParseAddr("2001:db8:eee:eeee:eeee:eeee:d01:f1")), berrors.MalformedError("IP address is in a reserved address block")}, // Documentation
   160  	}
   161  
   162  	// Test syntax errors
   163  	for _, tc := range testCases {
   164  		err := WellFormedIdentifiers(identifier.ACMEIdentifiers{tc.ident})
   165  		if tc.err == nil {
   166  			test.AssertNil(t, err, fmt.Sprintf("Unexpected error for %q identifier %q, got %s", tc.ident.Type, tc.ident.Value, err))
   167  		} else {
   168  			test.AssertError(t, err, fmt.Sprintf("Expected error for %q identifier %q, but got none", tc.ident.Type, tc.ident.Value))
   169  			var berr *berrors.BoulderError
   170  			test.AssertErrorWraps(t, err, &berr)
   171  			test.AssertContains(t, berr.Error(), tc.err.Error())
   172  		}
   173  	}
   174  }
   175  
   176  func TestWillingToIssue(t *testing.T) {
   177  	shouldBeBlocked := identifier.ACMEIdentifiers{
   178  		identifier.NewDNS(`highvalue.website1.org`),
   179  		identifier.NewDNS(`website2.co.uk`),
   180  		identifier.NewDNS(`www.website3.com`),
   181  		identifier.NewDNS(`lots.of.labels.website4.com`),
   182  		identifier.NewDNS(`banned.in.dc.com`),
   183  		identifier.NewDNS(`bad.brains.banned.in.dc.com`),
   184  		identifier.NewIP(netip.MustParseAddr(`64.112.117.66`)),
   185  		identifier.NewIP(netip.MustParseAddr(`2602:80a:6000:666::1`)),
   186  		identifier.NewIP(netip.MustParseAddr(`2602:80a:6000:666::1%lo`)),
   187  		identifier.NewIP(netip.MustParseAddr(`ff00::1`)),
   188  		identifier.NewIP(netip.MustParseAddr(`ff10::1`)),
   189  		identifier.NewIP(netip.MustParseAddr(`ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff`)),
   190  	}
   191  	blocklistContents := []string{
   192  		`website2.com`,
   193  		`website2.org`,
   194  		`website2.co.uk`,
   195  		`website3.com`,
   196  		`website4.com`,
   197  	}
   198  	exactBlocklistContents := []string{
   199  		`www.website1.org`,
   200  		`highvalue.website1.org`,
   201  		`dl.website1.org`,
   202  	}
   203  	adminBlockedNamesContents := []string{
   204  		`banned.in.dc.com`,
   205  	}
   206  	adminBlockedPrefixesContents := []string{
   207  		`64.112.117.66/32`,
   208  		`224.0.0.0/4`,
   209  		`2602:80a:6000:666::/64`,
   210  		`ff00::/8`,
   211  	}
   212  
   213  	shouldBeAccepted := identifier.ACMEIdentifiers{
   214  		identifier.NewDNS(`lowvalue.website1.org`),
   215  		identifier.NewDNS(`website4.sucks`),
   216  		identifier.NewDNS(`www.unrelated.com`),
   217  		identifier.NewDNS(`unrelated.com`),
   218  		identifier.NewDNS(`www.8675309.com`),
   219  		identifier.NewDNS(`8675309.com`),
   220  		identifier.NewDNS(`web5ite2.com`),
   221  		identifier.NewDNS(`www.web-site2.com`),
   222  		identifier.NewDNS(`www.highvalue.website1.org`),
   223  		identifier.NewIP(netip.MustParseAddr(`64.112.117.67`)),
   224  		identifier.NewIP(netip.MustParseAddr(`2620:fe::fe`)),
   225  		identifier.NewIP(netip.MustParseAddr(`2602:80a:6000:667::`)),
   226  	}
   227  
   228  	policy := blockedIdentsPolicy{
   229  		HighRiskBlockedNames: blocklistContents,
   230  		ExactBlockedNames:    exactBlocklistContents,
   231  		AdminBlockedNames:    adminBlockedNamesContents,
   232  		AdminBlockedPrefixes: adminBlockedPrefixesContents,
   233  	}
   234  
   235  	yamlPolicyBytes, err := yaml.Marshal(policy)
   236  	test.AssertNotError(t, err, "Couldn't YAML serialize blocklist")
   237  	yamlPolicyFile, _ := os.CreateTemp("", "test-blocklist.*.yaml")
   238  	defer os.Remove(yamlPolicyFile.Name())
   239  	err = os.WriteFile(yamlPolicyFile.Name(), yamlPolicyBytes, 0640)
   240  	test.AssertNotError(t, err, "Couldn't write YAML blocklist")
   241  
   242  	pa := paImpl(t)
   243  
   244  	err = pa.LoadIdentPolicyFile(yamlPolicyFile.Name())
   245  	test.AssertNotError(t, err, "Couldn't load rules")
   246  
   247  	// Invalid encoding
   248  	err = pa.WillingToIssue(identifier.ACMEIdentifiers{identifier.NewDNS("www.xn--m.com")})
   249  	test.AssertError(t, err, "WillingToIssue didn't fail on a malformed IDN")
   250  	// Invalid identifier type
   251  	err = pa.WillingToIssue(identifier.ACMEIdentifiers{identifier.ACMEIdentifier{Type: "fnord", Value: "uh-oh, Spaghetti-Os[tm]"}})
   252  	test.AssertError(t, err, "WillingToIssue didn't fail on an invalid identifier type")
   253  	// Valid encoding
   254  	err = pa.WillingToIssue(identifier.ACMEIdentifiers{identifier.NewDNS("www.xn--mnich-kva.com")})
   255  	test.AssertNotError(t, err, "WillingToIssue failed on a properly formed IDN")
   256  	// IDN TLD
   257  	err = pa.WillingToIssue(identifier.ACMEIdentifiers{identifier.NewDNS("xn--example--3bhk5a.xn--p1ai")})
   258  	test.AssertNotError(t, err, "WillingToIssue failed on a properly formed domain with IDN TLD")
   259  	features.Reset()
   260  
   261  	// Test expected blocked identifiers
   262  	for _, ident := range shouldBeBlocked {
   263  		err := pa.WillingToIssue(identifier.ACMEIdentifiers{ident})
   264  		test.AssertError(t, err, "identifier was not correctly forbidden")
   265  		var berr *berrors.BoulderError
   266  		test.AssertErrorWraps(t, err, &berr)
   267  		test.AssertContains(t, berr.Detail, errPolicyForbidden.Error())
   268  	}
   269  
   270  	// Test acceptance of good identifiers
   271  	for _, ident := range shouldBeAccepted {
   272  		err := pa.WillingToIssue(identifier.ACMEIdentifiers{ident})
   273  		test.AssertNotError(t, err, "identifier was incorrectly forbidden")
   274  	}
   275  }
   276  
   277  func TestWillingToIssue_Wildcards(t *testing.T) {
   278  	bannedDomains := []string{
   279  		"zombo.gov.us",
   280  	}
   281  	exactBannedDomains := []string{
   282  		"highvalue.letsdecrypt.org",
   283  	}
   284  	pa := paImpl(t)
   285  
   286  	bannedBytes, err := yaml.Marshal(blockedIdentsPolicy{
   287  		HighRiskBlockedNames: bannedDomains,
   288  		ExactBlockedNames:    exactBannedDomains,
   289  	})
   290  	test.AssertNotError(t, err, "Couldn't serialize banned list")
   291  	f, _ := os.CreateTemp("", "test-wildcard-banlist.*.yaml")
   292  	defer os.Remove(f.Name())
   293  	err = os.WriteFile(f.Name(), bannedBytes, 0640)
   294  	test.AssertNotError(t, err, "Couldn't write serialized banned list to file")
   295  	err = pa.LoadIdentPolicyFile(f.Name())
   296  	test.AssertNotError(t, err, "Couldn't load policy contents from file")
   297  
   298  	testCases := []struct {
   299  		Name        string
   300  		Domain      string
   301  		ExpectedErr error
   302  	}{
   303  		{
   304  			Name:        "Too many wildcards",
   305  			Domain:      "ok.*.whatever.*.example.com",
   306  			ExpectedErr: errTooManyWildcards,
   307  		},
   308  		{
   309  			Name:        "Misplaced wildcard",
   310  			Domain:      "ok.*.whatever.example.com",
   311  			ExpectedErr: errMalformedWildcard,
   312  		},
   313  		{
   314  			Name:        "Missing ICANN TLD",
   315  			Domain:      "*.ok.madeup",
   316  			ExpectedErr: errNonPublic,
   317  		},
   318  		{
   319  			Name:        "Wildcard for ICANN TLD",
   320  			Domain:      "*.com",
   321  			ExpectedErr: errICANNTLDWildcard,
   322  		},
   323  		{
   324  			Name:        "Forbidden base domain",
   325  			Domain:      "*.zombo.gov.us",
   326  			ExpectedErr: errPolicyForbidden,
   327  		},
   328  		// We should not allow getting a wildcard for that would cover an exact
   329  		// blocklist domain
   330  		{
   331  			Name:        "Wildcard for ExactBlocklist base domain",
   332  			Domain:      "*.letsdecrypt.org",
   333  			ExpectedErr: errPolicyForbidden,
   334  		},
   335  		// We should allow a wildcard for a domain that doesn't match the exact
   336  		// blocklist domain
   337  		{
   338  			Name:        "Wildcard for non-matching subdomain of ExactBlocklist domain",
   339  			Domain:      "*.lowvalue.letsdecrypt.org",
   340  			ExpectedErr: nil,
   341  		},
   342  		// We should allow getting a wildcard for an exact blocklist domain since it
   343  		// only covers subdomains, not the exact name.
   344  		{
   345  			Name:        "Wildcard for ExactBlocklist domain",
   346  			Domain:      "*.highvalue.letsdecrypt.org",
   347  			ExpectedErr: nil,
   348  		},
   349  		{
   350  			Name:        "Valid wildcard domain",
   351  			Domain:      "*.everything.is.possible.at.zombo.com",
   352  			ExpectedErr: nil,
   353  		},
   354  	}
   355  
   356  	for _, tc := range testCases {
   357  		t.Run(tc.Name, func(t *testing.T) {
   358  			err := pa.WillingToIssue(identifier.ACMEIdentifiers{identifier.NewDNS(tc.Domain)})
   359  			if tc.ExpectedErr == nil {
   360  				test.AssertNil(t, err, fmt.Sprintf("Unexpected error for domain %q, got %s", tc.Domain, err))
   361  			} else {
   362  				test.AssertError(t, err, fmt.Sprintf("Expected error for domain %q, but got none", tc.Domain))
   363  				var berr *berrors.BoulderError
   364  				test.AssertErrorWraps(t, err, &berr)
   365  				test.AssertContains(t, berr.Error(), tc.ExpectedErr.Error())
   366  			}
   367  		})
   368  	}
   369  }
   370  
   371  // TestWillingToIssue_SubErrors tests that more than one rejected identifier
   372  // results in an error with suberrors.
   373  func TestWillingToIssue_SubErrors(t *testing.T) {
   374  	banned := []string{
   375  		"letsdecrypt.org",
   376  		"example.com",
   377  	}
   378  	pa := paImpl(t)
   379  
   380  	bannedBytes, err := yaml.Marshal(blockedIdentsPolicy{
   381  		HighRiskBlockedNames: banned,
   382  		ExactBlockedNames:    banned,
   383  	})
   384  	test.AssertNotError(t, err, "Couldn't serialize banned list")
   385  	f, _ := os.CreateTemp("", "test-wildcard-banlist.*.yaml")
   386  	defer os.Remove(f.Name())
   387  	err = os.WriteFile(f.Name(), bannedBytes, 0640)
   388  	test.AssertNotError(t, err, "Couldn't write serialized banned list to file")
   389  	err = pa.LoadIdentPolicyFile(f.Name())
   390  	test.AssertNotError(t, err, "Couldn't load policy contents from file")
   391  
   392  	// Test multiple malformed domains and one banned domain; only the malformed ones will generate errors
   393  	err = pa.WillingToIssue(identifier.ACMEIdentifiers{
   394  		identifier.NewDNS("perfectly-fine.com"),      // fine
   395  		identifier.NewDNS("letsdecrypt_org"),         // malformed
   396  		identifier.NewDNS("example.comm"),            // malformed
   397  		identifier.NewDNS("letsdecrypt.org"),         // banned
   398  		identifier.NewDNS("also-perfectly-fine.com"), // fine
   399  	})
   400  	test.AssertDeepEquals(t, err,
   401  		&berrors.BoulderError{
   402  			Type:   berrors.RejectedIdentifier,
   403  			Detail: "Cannot issue for \"letsdecrypt_org\": Domain name contains an invalid character (and 1 more problems. Refer to sub-problems for more information.)",
   404  			SubErrors: []berrors.SubBoulderError{
   405  				{
   406  					BoulderError: &berrors.BoulderError{
   407  						Type:   berrors.Malformed,
   408  						Detail: "Domain name contains an invalid character",
   409  					},
   410  					Identifier: identifier.NewDNS("letsdecrypt_org"),
   411  				},
   412  				{
   413  					BoulderError: &berrors.BoulderError{
   414  						Type:   berrors.Malformed,
   415  						Detail: "Domain name does not end with a valid public suffix (TLD)",
   416  					},
   417  					Identifier: identifier.NewDNS("example.comm"),
   418  				},
   419  			},
   420  		})
   421  
   422  	// Test multiple banned domains.
   423  	err = pa.WillingToIssue(identifier.ACMEIdentifiers{
   424  		identifier.NewDNS("perfectly-fine.com"),      // fine
   425  		identifier.NewDNS("letsdecrypt.org"),         // banned
   426  		identifier.NewDNS("example.com"),             // banned
   427  		identifier.NewDNS("also-perfectly-fine.com"), // fine
   428  	})
   429  	test.AssertError(t, err, "Expected err from WillingToIssueWildcards")
   430  
   431  	test.AssertDeepEquals(t, err,
   432  		&berrors.BoulderError{
   433  			Type:   berrors.RejectedIdentifier,
   434  			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.)",
   435  			SubErrors: []berrors.SubBoulderError{
   436  				{
   437  					BoulderError: &berrors.BoulderError{
   438  						Type:   berrors.RejectedIdentifier,
   439  						Detail: "The ACME server refuses to issue a certificate for this domain name, because it is forbidden by policy",
   440  					},
   441  					Identifier: identifier.NewDNS("letsdecrypt.org"),
   442  				},
   443  				{
   444  					BoulderError: &berrors.BoulderError{
   445  						Type:   berrors.RejectedIdentifier,
   446  						Detail: "The ACME server refuses to issue a certificate for this domain name, because it is forbidden by policy",
   447  					},
   448  					Identifier: identifier.NewDNS("example.com"),
   449  				},
   450  			},
   451  		})
   452  
   453  	// Test willing to issue with only *one* bad identifier.
   454  	err = pa.WillingToIssue(identifier.ACMEIdentifiers{identifier.NewDNS("letsdecrypt.org")})
   455  	test.AssertDeepEquals(t, err,
   456  		&berrors.BoulderError{
   457  			Type:   berrors.RejectedIdentifier,
   458  			Detail: "Cannot issue for \"letsdecrypt.org\": The ACME server refuses to issue a certificate for this domain name, because it is forbidden by policy",
   459  		})
   460  }
   461  
   462  func TestChallengeTypesFor(t *testing.T) {
   463  	t.Parallel()
   464  	pa := paImpl(t)
   465  
   466  	t.Run("DNSAccount01Enabled=true", func(t *testing.T) {
   467  		features.Set(features.Config{DNSAccount01Enabled: true})
   468  		t.Cleanup(features.Reset)
   469  
   470  		testCases := []struct {
   471  			name       string
   472  			ident      identifier.ACMEIdentifier
   473  			wantChalls []core.AcmeChallenge
   474  			wantErr    string
   475  		}{
   476  			{
   477  				name:  "dns",
   478  				ident: identifier.NewDNS("example.com"),
   479  				wantChalls: []core.AcmeChallenge{
   480  					core.ChallengeTypeHTTP01,
   481  					core.ChallengeTypeDNS01,
   482  					core.ChallengeTypeTLSALPN01,
   483  					core.ChallengeTypeDNSAccount01,
   484  				},
   485  			},
   486  			{
   487  				name:  "dns wildcard",
   488  				ident: identifier.NewDNS("*.example.com"),
   489  				wantChalls: []core.AcmeChallenge{
   490  					core.ChallengeTypeDNS01,
   491  					core.ChallengeTypeDNSAccount01,
   492  				},
   493  			},
   494  			{
   495  				name:  "ip",
   496  				ident: identifier.NewIP(netip.MustParseAddr("1.2.3.4")),
   497  				wantChalls: []core.AcmeChallenge{
   498  					core.ChallengeTypeHTTP01, core.ChallengeTypeTLSALPN01,
   499  				},
   500  			},
   501  			{
   502  				name:    "invalid",
   503  				ident:   identifier.ACMEIdentifier{Type: "fnord", Value: "uh-oh, Spaghetti-Os[tm]"},
   504  				wantErr: "unrecognized identifier type",
   505  			},
   506  		}
   507  
   508  		for _, tc := range testCases {
   509  			tc := tc // Capture range variable
   510  			t.Run(tc.name, func(t *testing.T) {
   511  				t.Parallel()
   512  				challs, err := pa.ChallengeTypesFor(tc.ident)
   513  
   514  				if len(tc.wantChalls) != 0 {
   515  					test.AssertNotError(t, err, "should have succeeded")
   516  					test.AssertDeepEquals(t, challs, tc.wantChalls)
   517  				}
   518  
   519  				if tc.wantErr != "" {
   520  					test.AssertError(t, err, "should have errored")
   521  					test.AssertContains(t, err.Error(), tc.wantErr)
   522  				}
   523  			})
   524  		}
   525  	})
   526  
   527  	t.Run("DNSAccount01Enabled=false", func(t *testing.T) {
   528  		features.Set(features.Config{DNSAccount01Enabled: false})
   529  		t.Cleanup(features.Reset)
   530  
   531  		testCases := []struct {
   532  			name       string
   533  			ident      identifier.ACMEIdentifier
   534  			wantChalls []core.AcmeChallenge
   535  			wantErr    string
   536  		}{
   537  			{
   538  				name:  "dns",
   539  				ident: identifier.NewDNS("example.com"),
   540  				wantChalls: []core.AcmeChallenge{
   541  					core.ChallengeTypeHTTP01,
   542  					core.ChallengeTypeDNS01,
   543  					core.ChallengeTypeTLSALPN01,
   544  					// DNSAccount01 excluded
   545  				},
   546  			},
   547  			{
   548  				name:  "wildcard",
   549  				ident: identifier.NewDNS("*.example.com"),
   550  				wantChalls: []core.AcmeChallenge{
   551  					core.ChallengeTypeDNS01,
   552  					// DNSAccount01 excluded
   553  				},
   554  			},
   555  			{
   556  				name:  "ip",
   557  				ident: identifier.NewIP(netip.MustParseAddr("1.2.3.4")),
   558  				wantChalls: []core.AcmeChallenge{
   559  					core.ChallengeTypeHTTP01, core.ChallengeTypeTLSALPN01,
   560  				},
   561  			},
   562  		}
   563  
   564  		for _, tc := range testCases {
   565  			tc := tc // Capture range variable
   566  			t.Run(tc.name, func(t *testing.T) {
   567  				t.Parallel()
   568  				challs, err := pa.ChallengeTypesFor(tc.ident)
   569  
   570  				if len(tc.wantChalls) != 0 {
   571  					test.AssertNotError(t, err, "should have succeeded")
   572  					test.AssertDeepEquals(t, challs, tc.wantChalls)
   573  				}
   574  
   575  				if tc.wantErr != "" {
   576  					test.AssertError(t, err, "should have errored")
   577  					test.AssertContains(t, err.Error(), tc.wantErr)
   578  				}
   579  			})
   580  		}
   581  	})
   582  }
   583  
   584  // TestMalformedExactBlocklist tests that loading a YAML policy file with an
   585  // invalid exact blocklist entry will fail as expected.
   586  func TestMalformedExactBlocklist(t *testing.T) {
   587  	pa := paImpl(t)
   588  
   589  	exactBannedDomains := []string{
   590  		// Only one label - not valid
   591  		"com",
   592  	}
   593  	bannedDomains := []string{
   594  		"placeholder.domain.not.important.for.this.test.com",
   595  	}
   596  
   597  	// Create YAML for the exactBannedDomains
   598  	bannedBytes, err := yaml.Marshal(blockedIdentsPolicy{
   599  		HighRiskBlockedNames: bannedDomains,
   600  		ExactBlockedNames:    exactBannedDomains,
   601  	})
   602  	test.AssertNotError(t, err, "Couldn't serialize banned list")
   603  
   604  	// Create a temp file for the YAML contents
   605  	f, _ := os.CreateTemp("", "test-invalid-exactblocklist.*.yaml")
   606  	defer os.Remove(f.Name())
   607  	// Write the YAML to the temp file
   608  	err = os.WriteFile(f.Name(), bannedBytes, 0640)
   609  	test.AssertNotError(t, err, "Couldn't write serialized banned list to file")
   610  
   611  	// Try to use the YAML tempfile as the ident policy. It should produce an
   612  	// error since the exact blocklist contents are malformed.
   613  	err = pa.LoadIdentPolicyFile(f.Name())
   614  	test.AssertError(t, err, "Loaded invalid exact blocklist content without error")
   615  	test.AssertEquals(t, err.Error(), "malformed ExactBlockedNames entry, only one label: \"com\"")
   616  }
   617  
   618  func TestValidEmailError(t *testing.T) {
   619  	err := ValidEmail("(๑•́ ω •̀๑)")
   620  	test.AssertEquals(t, err.Error(), "unable to parse email address")
   621  
   622  	err = ValidEmail("john.smith@gmail.com #replace with real email")
   623  	test.AssertEquals(t, err.Error(), "unable to parse email address")
   624  
   625  	err = ValidEmail("example@example.com")
   626  	test.AssertEquals(t, err.Error(), "contact email has forbidden domain \"example.com\"")
   627  
   628  	err = ValidEmail("example@-foobar.com")
   629  	test.AssertEquals(t, err.Error(), "contact email has invalid domain: Domain name contains an invalid character")
   630  }
   631  
   632  func TestCheckAuthzChallenges(t *testing.T) {
   633  	t.Parallel()
   634  
   635  	testCases := []struct {
   636  		name    string
   637  		authz   core.Authorization
   638  		enabled map[core.AcmeChallenge]bool
   639  		wantErr string
   640  	}{
   641  		{
   642  			name: "unrecognized identifier",
   643  			authz: core.Authorization{
   644  				Identifier: identifier.ACMEIdentifier{Type: "oops", Value: "example.com"},
   645  				Challenges: []core.Challenge{{Type: core.ChallengeTypeDNS01, Status: core.StatusValid}},
   646  			},
   647  			wantErr: "unrecognized identifier type",
   648  		},
   649  		{
   650  			name: "no challenges",
   651  			authz: core.Authorization{
   652  				Identifier: identifier.NewDNS("example.com"),
   653  				Challenges: []core.Challenge{},
   654  			},
   655  			wantErr: "has no challenges",
   656  		},
   657  		{
   658  			name: "no valid challenges",
   659  			authz: core.Authorization{
   660  				Identifier: identifier.NewDNS("example.com"),
   661  				Challenges: []core.Challenge{{Type: core.ChallengeTypeDNS01, Status: core.StatusPending}},
   662  			},
   663  			wantErr: "not solved by any challenge",
   664  		},
   665  		{
   666  			name: "solved by disabled challenge",
   667  			authz: core.Authorization{
   668  				Identifier: identifier.NewDNS("example.com"),
   669  				Challenges: []core.Challenge{{Type: core.ChallengeTypeDNS01, Status: core.StatusValid}},
   670  			},
   671  			enabled: map[core.AcmeChallenge]bool{core.ChallengeTypeHTTP01: true},
   672  			wantErr: "disabled challenge type",
   673  		},
   674  		{
   675  			name: "solved by wrong kind of challenge",
   676  			authz: core.Authorization{
   677  				Identifier: identifier.NewDNS("*.example.com"),
   678  				Challenges: []core.Challenge{{Type: core.ChallengeTypeHTTP01, Status: core.StatusValid}},
   679  			},
   680  			wantErr: "inapplicable challenge type",
   681  		},
   682  		{
   683  			name: "valid authz",
   684  			authz: core.Authorization{
   685  				Identifier: identifier.NewDNS("example.com"),
   686  				Challenges: []core.Challenge{{Type: core.ChallengeTypeTLSALPN01, Status: core.StatusValid}},
   687  			},
   688  		},
   689  	}
   690  
   691  	for _, tc := range testCases {
   692  		t.Run(tc.name, func(t *testing.T) {
   693  			t.Parallel()
   694  			pa := paImpl(t)
   695  
   696  			if tc.enabled != nil {
   697  				pa.enabledChallenges = tc.enabled
   698  			}
   699  
   700  			err := pa.CheckAuthzChallenges(&tc.authz)
   701  
   702  			if tc.wantErr == "" {
   703  				test.AssertNotError(t, err, "should have succeeded")
   704  			} else {
   705  				test.AssertError(t, err, "should have errored")
   706  				test.AssertContains(t, err.Error(), tc.wantErr)
   707  			}
   708  		})
   709  	}
   710  }
   711  
   712  func TestWillingToIssue_IdentifierType(t *testing.T) {
   713  	t.Parallel()
   714  
   715  	testCases := []struct {
   716  		name    string
   717  		ident   identifier.ACMEIdentifier
   718  		enabled map[identifier.IdentifierType]bool
   719  		wantErr string
   720  	}{
   721  		{
   722  			name:    "DNS identifier, none enabled",
   723  			ident:   identifier.NewDNS("example.com"),
   724  			enabled: nil,
   725  			wantErr: "The ACME server has disabled this identifier type",
   726  		},
   727  		{
   728  			name:    "DNS identifier, DNS enabled",
   729  			ident:   identifier.NewDNS("example.com"),
   730  			enabled: map[identifier.IdentifierType]bool{identifier.TypeDNS: true},
   731  			wantErr: "",
   732  		},
   733  		{
   734  			name:    "DNS identifier, DNS & IP enabled",
   735  			ident:   identifier.NewDNS("example.com"),
   736  			enabled: map[identifier.IdentifierType]bool{identifier.TypeDNS: true, identifier.TypeIP: true},
   737  			wantErr: "",
   738  		},
   739  		{
   740  			name:    "DNS identifier, IP enabled",
   741  			ident:   identifier.NewDNS("example.com"),
   742  			enabled: map[identifier.IdentifierType]bool{identifier.TypeIP: true},
   743  			wantErr: "The ACME server has disabled this identifier type",
   744  		},
   745  		{
   746  			name:    "IP identifier, none enabled",
   747  			ident:   identifier.NewIP(netip.MustParseAddr("9.9.9.9")),
   748  			enabled: nil,
   749  			wantErr: "The ACME server has disabled this identifier type",
   750  		},
   751  		{
   752  			name:    "IP identifier, DNS enabled",
   753  			ident:   identifier.NewIP(netip.MustParseAddr("9.9.9.9")),
   754  			enabled: map[identifier.IdentifierType]bool{identifier.TypeDNS: true},
   755  			wantErr: "The ACME server has disabled this identifier type",
   756  		},
   757  		{
   758  			name:    "IP identifier, DNS & IP enabled",
   759  			ident:   identifier.NewIP(netip.MustParseAddr("9.9.9.9")),
   760  			enabled: map[identifier.IdentifierType]bool{identifier.TypeDNS: true, identifier.TypeIP: true},
   761  			wantErr: "",
   762  		},
   763  		{
   764  			name:    "IP identifier, IP enabled",
   765  			ident:   identifier.NewIP(netip.MustParseAddr("9.9.9.9")),
   766  			enabled: map[identifier.IdentifierType]bool{identifier.TypeIP: true},
   767  			wantErr: "",
   768  		},
   769  		{
   770  			name:    "invalid identifier type",
   771  			ident:   identifier.ACMEIdentifier{Type: "drywall", Value: "oh yeah!"},
   772  			enabled: map[identifier.IdentifierType]bool{"drywall": true},
   773  			wantErr: "Invalid identifier type",
   774  		},
   775  	}
   776  
   777  	for _, tc := range testCases {
   778  		t.Run(tc.name, func(t *testing.T) {
   779  			t.Parallel()
   780  
   781  			policy := blockedIdentsPolicy{
   782  				HighRiskBlockedNames: []string{"zombo.gov.us"},
   783  				ExactBlockedNames:    []string{`highvalue.website1.org`},
   784  				AdminBlockedNames:    []string{`banned.in.dc.com`},
   785  			}
   786  
   787  			yamlPolicyBytes, err := yaml.Marshal(policy)
   788  			test.AssertNotError(t, err, "Couldn't YAML serialize blocklist")
   789  			yamlPolicyFile, _ := os.CreateTemp("", "test-blocklist.*.yaml")
   790  			defer os.Remove(yamlPolicyFile.Name())
   791  			err = os.WriteFile(yamlPolicyFile.Name(), yamlPolicyBytes, 0640)
   792  			test.AssertNotError(t, err, "Couldn't write YAML blocklist")
   793  
   794  			pa := paImpl(t)
   795  
   796  			err = pa.LoadIdentPolicyFile(yamlPolicyFile.Name())
   797  			test.AssertNotError(t, err, "Couldn't load rules")
   798  
   799  			pa.enabledIdentifiers = tc.enabled
   800  
   801  			err = pa.WillingToIssue(identifier.ACMEIdentifiers{tc.ident})
   802  
   803  			if tc.wantErr == "" {
   804  				if err != nil {
   805  					t.Errorf("should have succeeded, but got error: %s", err.Error())
   806  				}
   807  			} else {
   808  				if err == nil {
   809  					t.Errorf("should have failed")
   810  				} else if !strings.Contains(err.Error(), tc.wantErr) {
   811  					t.Errorf("wrong error; wanted '%s', but got '%s'", tc.wantErr, err.Error())
   812  				}
   813  			}
   814  		})
   815  	}
   816  }