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 }