github.com/letsencrypt/boulder@v0.20251208.0/va/va_test.go (about) 1 package va 2 3 import ( 4 "context" 5 "crypto/rsa" 6 "encoding/base64" 7 "errors" 8 "fmt" 9 "math/big" 10 "net" 11 "net/http" 12 "net/http/httptest" 13 "net/netip" 14 "os" 15 "strings" 16 "sync" 17 "syscall" 18 "testing" 19 "time" 20 21 "github.com/go-jose/go-jose/v4" 22 "github.com/jmhodges/clock" 23 "github.com/prometheus/client_golang/prometheus" 24 "google.golang.org/grpc" 25 26 "github.com/letsencrypt/boulder/bdns" 27 "github.com/letsencrypt/boulder/core" 28 corepb "github.com/letsencrypt/boulder/core/proto" 29 "github.com/letsencrypt/boulder/features" 30 "github.com/letsencrypt/boulder/iana" 31 "github.com/letsencrypt/boulder/identifier" 32 blog "github.com/letsencrypt/boulder/log" 33 "github.com/letsencrypt/boulder/metrics" 34 "github.com/letsencrypt/boulder/probs" 35 "github.com/letsencrypt/boulder/test" 36 vapb "github.com/letsencrypt/boulder/va/proto" 37 ) 38 39 func ka(token string) string { 40 return token + "." + expectedThumbprint 41 } 42 43 func bigIntFromB64(b64 string) *big.Int { 44 bytes, _ := base64.URLEncoding.DecodeString(b64) 45 x := big.NewInt(0) 46 x.SetBytes(bytes) 47 return x 48 } 49 50 func intFromB64(b64 string) int { 51 return int(bigIntFromB64(b64).Int64()) 52 } 53 54 // Any changes to this key must be reflected in //bdns/mocks.go, where values 55 // derived from it are hardcoded as the "correct" responses for DNS challenges. 56 // This key should not be used for anything other than computing Key 57 // Authorizations, i.e. it should not be used as the key to create a self-signed 58 // TLS-ALPN-01 certificate. 59 var n = bigIntFromB64("n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT-O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqVwGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuCLqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5gHdrNP5zw==") 60 var e = intFromB64("AQAB") 61 var d = bigIntFromB64("bWUC9B-EFRIo8kpGfh0ZuyGPvMNKvYWNtB_ikiH9k20eT-O1q_I78eiZkpXxXQ0UTEs2LsNRS-8uJbvQ-A1irkwMSMkK1J3XTGgdrhCku9gRldY7sNA_AKZGh-Q661_42rINLRCe8W-nZ34ui_qOfkLnK9QWDDqpaIsA-bMwWWSDFu2MUBYwkHTMEzLYGqOe04noqeq1hExBTHBOBdkMXiuFhUq1BU6l-DqEiWxqg82sXt2h-LMnT3046AOYJoRioz75tSUQfGCshWTBnP5uDjd18kKhyv07lhfSJdrPdM5Plyl21hsFf4L_mHCuoFau7gdsPfHPxxjVOcOpBrQzwQ==") 62 var p = bigIntFromB64("uKE2dh-cTf6ERF4k4e_jy78GfPYUIaUyoSSJuBzp3Cubk3OCqs6grT8bR_cu0Dm1MZwWmtdqDyI95HrUeq3MP15vMMON8lHTeZu2lmKvwqW7anV5UzhM1iZ7z4yMkuUwFWoBvyY898EXvRD-hdqRxHlSqAZ192zB3pVFJ0s7pFc=") 63 var q = bigIntFromB64("uKE2dh-cTf6ERF4k4e_jy78GfPYUIaUyoSSJuBzp3Cubk3OCqs6grT8bR_cu0Dm1MZwWmtdqDyI95HrUeq3MP15vMMON8lHTeZu2lmKvwqW7anV5UzhM1iZ7z4yMkuUwFWoBvyY898EXvRD-hdqRxHlSqAZ192zB3pVFJ0s7pFc=") 64 var TheKey = rsa.PrivateKey{ 65 PublicKey: rsa.PublicKey{N: n, E: e}, 66 D: d, 67 Primes: []*big.Int{p, q}, 68 } 69 var accountKey = &jose.JSONWebKey{Key: TheKey.Public()} 70 var expectedToken = "LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0" 71 var expectedThumbprint = "9jg46WB3rR_AHD-EBXdN7cBkH1WOu0tA3M9fm21mqTI" 72 var expectedKeyAuthorization = ka(expectedToken) 73 74 var ctx context.Context 75 76 func TestMain(m *testing.M) { 77 var cancel context.CancelFunc 78 ctx, cancel = context.WithTimeout(context.Background(), 10*time.Minute) 79 ret := m.Run() 80 cancel() 81 os.Exit(ret) 82 } 83 84 var accountURIPrefixes = []string{"http://boulder.service.consul:4000/acme/reg/"} 85 86 func createValidationRequest(ident identifier.ACMEIdentifier, challengeType core.AcmeChallenge) *vapb.PerformValidationRequest { 87 return &vapb.PerformValidationRequest{ 88 Identifier: ident.ToProto(), 89 Challenge: &corepb.Challenge{ 90 Type: string(challengeType), 91 Status: string(core.StatusPending), 92 Token: expectedToken, 93 Validationrecords: nil, 94 }, 95 Authz: &vapb.AuthzMeta{ 96 Id: "", 97 RegID: 1, 98 }, 99 ExpectedKeyAuthorization: expectedKeyAuthorization, 100 } 101 } 102 103 // isNonLoopbackReservedIP is a mock reserved IP checker that permits loopback 104 // networks. 105 func isNonLoopbackReservedIP(ip netip.Addr) error { 106 loopbackV4 := netip.MustParsePrefix("127.0.0.0/8") 107 loopbackV6 := netip.MustParsePrefix("::1/128") 108 if loopbackV4.Contains(ip) || loopbackV6.Contains(ip) { 109 return nil 110 } 111 return iana.IsReservedAddr(ip) 112 } 113 114 // setup returns an in-memory VA and a mock logger. The default resolver client 115 // is MockClient{}, but can be overridden. 116 // 117 // If remoteVAs is nil, this builds a VA that acts like a remote (and does not 118 // perform multi-perspective validation). Otherwise it acts like a primary. 119 func setup(srv *httptest.Server, userAgent string, remoteVAs []RemoteVA, mockDNSClientOverride bdns.Client) (*ValidationAuthorityImpl, *blog.Mock) { 120 features.Reset() 121 fc := clock.NewFake() 122 123 logger := blog.NewMock() 124 125 if userAgent == "" { 126 userAgent = "user agent 1.0" 127 } 128 129 perspective := PrimaryPerspective 130 if len(remoteVAs) == 0 { 131 // We're being set up as a remote. Use a distinct perspective from other remotes 132 // to better simulate what prod will be like. 133 perspective = "example perspective " + core.RandomString(4) 134 } 135 136 va, err := NewValidationAuthorityImpl( 137 &bdns.MockClient{Log: logger}, 138 remoteVAs, 139 userAgent, 140 "letsencrypt.org", 141 metrics.NoopRegisterer, 142 fc, 143 logger, 144 accountURIPrefixes, 145 perspective, 146 "", 147 isNonLoopbackReservedIP, 148 time.Second, 149 ) 150 if err != nil { 151 panic(fmt.Sprintf("Failed to create validation authority: %v", err)) 152 } 153 154 if mockDNSClientOverride != nil { 155 va.dnsClient = mockDNSClientOverride 156 } 157 158 // Adjusting industry regulated ACME challenge port settings is fine during 159 // testing 160 if srv != nil { 161 port := getPort(srv) 162 va.httpPort = port 163 va.tlsPort = port 164 } 165 166 return va, logger 167 } 168 169 func setupRemote(srv *httptest.Server, userAgent string, mockDNSClientOverride bdns.Client, perspective, rir string) RemoteClients { 170 rva, _ := setup(srv, userAgent, nil, mockDNSClientOverride) 171 rva.perspective = perspective 172 rva.rir = rir 173 174 return RemoteClients{VAClient: &inMemVA{rva}, CAAClient: &inMemVA{rva}} 175 } 176 177 // RIRs 178 const ( 179 arin = "ARIN" 180 ripe = "RIPE" 181 apnic = "APNIC" 182 lacnic = "LACNIC" 183 afrinic = "AFRINIC" 184 ) 185 186 // remoteConf is used in conjunction with setupRemotes/withRemotes to configure 187 // a remote VA. 188 type remoteConf struct { 189 // ua is optional, will default to "user agent 1.0". When set to "broken" or 190 // "hijacked", the Address field of the resulting RemoteVA will be set to 191 // match. This is a bit hacky, but it's the easiest way to satisfy some of 192 // our existing TestMultiCAARechecking tests. 193 ua string 194 // rir is required. 195 rir string 196 // dns is optional. 197 dns bdns.Client 198 // impl is optional. 199 impl RemoteClients 200 } 201 202 func setupRemotes(confs []remoteConf, srv *httptest.Server) []RemoteVA { 203 remoteVAs := make([]RemoteVA, 0, len(confs)) 204 for i, c := range confs { 205 if c.rir == "" { 206 panic("rir is required") 207 } 208 // perspective MUST be unique for each remote VA, otherwise the VA will 209 // fail to start. 210 perspective := fmt.Sprintf("dc-%d-%s", i, c.rir) 211 clients := setupRemote(srv, c.ua, c.dns, perspective, c.rir) 212 if c.impl != (RemoteClients{}) { 213 clients = c.impl 214 } 215 remoteVAs = append(remoteVAs, RemoteVA{ 216 RemoteClients: clients, 217 Perspective: perspective, 218 RIR: c.rir, 219 }) 220 } 221 222 return remoteVAs 223 } 224 225 func setupWithRemotes(srv *httptest.Server, userAgent string, remotes []remoteConf, mockDNSClientOverride bdns.Client) (*ValidationAuthorityImpl, *blog.Mock) { 226 remoteVAs := setupRemotes(remotes, srv) 227 return setup(srv, userAgent, remoteVAs, mockDNSClientOverride) 228 } 229 230 type multiSrv struct { 231 *httptest.Server 232 233 mu sync.Mutex 234 allowedUAs map[string]bool 235 } 236 237 func httpMultiSrv(t *testing.T, token string, allowedUAs map[string]bool) *multiSrv { 238 t.Helper() 239 m := http.NewServeMux() 240 241 server := httptest.NewUnstartedServer(m) 242 ms := &multiSrv{server, sync.Mutex{}, allowedUAs} 243 244 m.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 245 ms.mu.Lock() 246 defer ms.mu.Unlock() 247 if ms.allowedUAs[r.UserAgent()] { 248 ch := core.Challenge{Token: token} 249 keyAuthz, _ := ch.ExpectedKeyAuthorization(accountKey) 250 fmt.Fprint(w, keyAuthz, "\n\r \t") 251 } else { 252 fmt.Fprint(w, "???") 253 } 254 }) 255 256 ms.Start() 257 return ms 258 } 259 260 // cancelledVA is a mock that always returns context.Canceled for 261 // PerformValidation calls 262 type cancelledVA struct{} 263 264 func (v cancelledVA) DoDCV(_ context.Context, _ *vapb.PerformValidationRequest, _ ...grpc.CallOption) (*vapb.ValidationResult, error) { 265 return nil, context.Canceled 266 } 267 268 func (v cancelledVA) DoCAA(_ context.Context, _ *vapb.IsCAAValidRequest, _ ...grpc.CallOption) (*vapb.IsCAAValidResponse, error) { 269 return nil, context.Canceled 270 } 271 272 // brokenRemoteVA is a mock for the VAClient and CAAClient interfaces that always return 273 // errors. 274 type brokenRemoteVA struct{} 275 276 // errBrokenRemoteVA is the error returned by a brokenRemoteVA's 277 // PerformValidation and IsSafeDomain functions. 278 var errBrokenRemoteVA = errors.New("brokenRemoteVA is broken") 279 280 // DoDCV returns errBrokenRemoteVA unconditionally 281 func (b brokenRemoteVA) DoDCV(_ context.Context, _ *vapb.PerformValidationRequest, _ ...grpc.CallOption) (*vapb.ValidationResult, error) { 282 return nil, errBrokenRemoteVA 283 } 284 285 func (b brokenRemoteVA) DoCAA(_ context.Context, _ *vapb.IsCAAValidRequest, _ ...grpc.CallOption) (*vapb.IsCAAValidResponse, error) { 286 return nil, errBrokenRemoteVA 287 } 288 289 // inMemVA is a wrapper which fulfills the VAClient and CAAClient 290 // interfaces, but then forwards requests directly to its inner 291 // ValidationAuthorityImpl rather than over the network. This lets a local 292 // in-memory mock VA act like a remote VA. 293 type inMemVA struct { 294 rva *ValidationAuthorityImpl 295 } 296 297 func (inmem *inMemVA) DoDCV(ctx context.Context, req *vapb.PerformValidationRequest, _ ...grpc.CallOption) (*vapb.ValidationResult, error) { 298 return inmem.rva.DoDCV(ctx, req) 299 } 300 301 func (inmem *inMemVA) DoCAA(ctx context.Context, req *vapb.IsCAAValidRequest, _ ...grpc.CallOption) (*vapb.IsCAAValidResponse, error) { 302 return inmem.rva.DoCAA(ctx, req) 303 } 304 305 func TestNewValidationAuthorityImplWithDuplicateRemotes(t *testing.T) { 306 var remoteVAs []RemoteVA 307 for range 3 { 308 remoteVAs = append(remoteVAs, RemoteVA{ 309 RemoteClients: setupRemote(nil, "", nil, "dadaist", arin), 310 Perspective: "dadaist", 311 RIR: arin, 312 }) 313 } 314 315 _, err := NewValidationAuthorityImpl( 316 &bdns.MockClient{Log: blog.NewMock()}, 317 remoteVAs, 318 "user agent 1.0", 319 "letsencrypt.org", 320 metrics.NoopRegisterer, 321 clock.NewFake(), 322 blog.NewMock(), 323 accountURIPrefixes, 324 "example perspective", 325 "", 326 isNonLoopbackReservedIP, 327 time.Second, 328 ) 329 test.AssertError(t, err, "NewValidationAuthorityImpl allowed duplicate remote perspectives") 330 test.AssertContains(t, err.Error(), "duplicate remote VA perspective \"dadaist\"") 331 } 332 333 func TestPerformValidationWithMismatchedRemoteVAPerspectives(t *testing.T) { 334 t.Parallel() 335 336 mismatched1 := RemoteVA{ 337 RemoteClients: setupRemote(nil, "", nil, "dadaist", arin), 338 Perspective: "baroque", 339 RIR: arin, 340 } 341 mismatched2 := RemoteVA{ 342 RemoteClients: setupRemote(nil, "", nil, "impressionist", ripe), 343 Perspective: "minimalist", 344 RIR: ripe, 345 } 346 remoteVAs := setupRemotes([]remoteConf{{rir: ripe}}, nil) 347 remoteVAs = append(remoteVAs, mismatched1, mismatched2) 348 349 va, mockLog := setup(nil, "", remoteVAs, nil) 350 req := createValidationRequest(identifier.NewDNS("good-dns01.com"), core.ChallengeTypeDNS01) 351 res, _ := va.DoDCV(context.Background(), req) 352 test.AssertNotNil(t, res.GetProblem(), "validation succeeded with mismatched remote VA perspectives") 353 test.AssertEquals(t, len(mockLog.GetAllMatching("Expected perspective")), 2) 354 } 355 356 func TestPerformValidationWithMismatchedRemoteVARIRs(t *testing.T) { 357 t.Parallel() 358 359 mismatched1 := RemoteVA{ 360 RemoteClients: setupRemote(nil, "", nil, "dadaist", arin), 361 Perspective: "dadaist", 362 RIR: ripe, 363 } 364 mismatched2 := RemoteVA{ 365 RemoteClients: setupRemote(nil, "", nil, "impressionist", ripe), 366 Perspective: "impressionist", 367 RIR: arin, 368 } 369 remoteVAs := setupRemotes([]remoteConf{{rir: ripe}}, nil) 370 remoteVAs = append(remoteVAs, mismatched1, mismatched2) 371 372 va, mockLog := setup(nil, "", remoteVAs, nil) 373 req := createValidationRequest(identifier.NewDNS("good-dns01.com"), core.ChallengeTypeDNS01) 374 res, _ := va.DoDCV(context.Background(), req) 375 test.AssertNotNil(t, res.GetProblem(), "validation succeeded with mismatched remote VA perspectives") 376 test.AssertEquals(t, len(mockLog.GetAllMatching("Expected perspective")), 2) 377 } 378 379 func TestValidateMalformedChallenge(t *testing.T) { 380 va, _ := setup(nil, "", nil, nil) 381 382 _, err := va.validateChallenge(ctx, identifier.NewDNS("example.com"), "fake-type-01", expectedToken, expectedKeyAuthorization, testAccountURI) 383 384 prob := detailedError(err) 385 test.AssertEquals(t, prob.Type, probs.MalformedProblem) 386 } 387 388 func TestPerformValidationInvalid(t *testing.T) { 389 t.Parallel() 390 va, _ := setup(nil, "", nil, nil) 391 392 req := createValidationRequest(identifier.NewDNS("foo.com"), core.ChallengeTypeDNS01) 393 res, _ := va.DoDCV(context.Background(), req) 394 test.Assert(t, res.Problem != nil, "validation succeeded") 395 test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, prometheus.Labels{ 396 "operation": opDCV, 397 "perspective": va.perspective, 398 "challenge_type": string(core.ChallengeTypeDNS01), 399 "problem_type": string(probs.UnauthorizedProblem), 400 "result": fail, 401 }, 1) 402 } 403 404 func TestInternalErrorLogged(t *testing.T) { 405 t.Parallel() 406 407 va, mockLog := setup(nil, "", nil, nil) 408 409 ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond) 410 defer cancel() 411 req := createValidationRequest(identifier.NewDNS("nonexistent.com"), core.ChallengeTypeHTTP01) 412 _, err := va.DoDCV(ctx, req) 413 test.AssertNotError(t, err, "failed validation should not be an error") 414 matchingLogs := mockLog.GetAllMatching( 415 `Validation result JSON=.*"InternalError":"127.0.0.1: Get.*nonexistent.com/\.well-known.*: context deadline exceeded`) 416 test.AssertEquals(t, len(matchingLogs), 1) 417 } 418 419 func TestPerformValidationValid(t *testing.T) { 420 t.Parallel() 421 422 va, mockLog := setup(nil, "", nil, nil) 423 424 // create a challenge with well known token 425 req := createValidationRequest(identifier.NewDNS("good-dns01.com"), core.ChallengeTypeDNS01) 426 res, _ := va.DoDCV(context.Background(), req) 427 test.Assert(t, res.Problem == nil, fmt.Sprintf("validation failed: %#v", res.Problem)) 428 test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, prometheus.Labels{ 429 "operation": opDCV, 430 "perspective": va.perspective, 431 "challenge_type": string(core.ChallengeTypeDNS01), 432 "problem_type": "", 433 "result": pass, 434 }, 1) 435 resultLog := mockLog.GetAllMatching(`Validation result`) 436 if len(resultLog) != 1 { 437 t.Fatalf("Wrong number of matching lines for 'Validation result'") 438 } 439 440 if !strings.Contains(resultLog[0], `"Identifier":{"type":"dns","value":"good-dns01.com"}`) { 441 t.Error("PerformValidation didn't log validation identifier.") 442 } 443 } 444 445 // TestPerformValidationWildcard tests that the VA properly strips the `*.` 446 // prefix from a wildcard name provided to the PerformValidation function. 447 func TestPerformValidationWildcard(t *testing.T) { 448 t.Parallel() 449 450 va, mockLog := setup(nil, "", nil, nil) 451 452 // create a challenge with well known token 453 req := createValidationRequest(identifier.NewDNS("*.good-dns01.com"), core.ChallengeTypeDNS01) 454 // perform a validation for a wildcard name 455 res, _ := va.DoDCV(context.Background(), req) 456 test.Assert(t, res.Problem == nil, fmt.Sprintf("validation failed: %#v", res.Problem)) 457 test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, prometheus.Labels{ 458 "operation": opDCV, 459 "perspective": va.perspective, 460 "challenge_type": string(core.ChallengeTypeDNS01), 461 "problem_type": "", 462 "result": pass, 463 }, 1) 464 resultLog := mockLog.GetAllMatching(`Validation result`) 465 if len(resultLog) != 1 { 466 t.Fatalf("Wrong number of matching lines for 'Validation result'") 467 } 468 469 // We expect that the top level Identifier reflect the wildcard name 470 if !strings.Contains(resultLog[0], `"Identifier":{"type":"dns","value":"*.good-dns01.com"}`) { 471 t.Errorf("PerformValidation didn't log correct validation identifier.") 472 } 473 // We expect that the ValidationRecord contain the correct non-wildcard 474 // hostname that was validated 475 if !strings.Contains(resultLog[0], `"hostname":"good-dns01.com"`) { 476 t.Errorf("PerformValidation didn't log correct validation record hostname.") 477 } 478 } 479 480 func TestMultiVA(t *testing.T) { 481 t.Parallel() 482 483 // Create a new challenge to use for the httpSrv 484 req := createValidationRequest(identifier.NewDNS("localhost"), core.ChallengeTypeHTTP01) 485 486 brokenVA := RemoteClients{ 487 VAClient: brokenRemoteVA{}, 488 CAAClient: brokenRemoteVA{}, 489 } 490 cancelledVA := RemoteClients{ 491 VAClient: cancelledVA{}, 492 CAAClient: cancelledVA{}, 493 } 494 495 testCases := []struct { 496 Name string 497 Remotes []remoteConf 498 PrimaryUA string 499 ExpectedProbType string 500 ExpectedLogContains string 501 }{ 502 { 503 // With local and all remote VAs working there should be no problem. 504 Name: "Local and remote VAs OK", 505 Remotes: []remoteConf{ 506 {ua: pass, rir: arin}, 507 {ua: pass, rir: ripe}, 508 {ua: pass, rir: apnic}, 509 }, 510 PrimaryUA: pass, 511 }, 512 { 513 // If the local VA fails everything should fail 514 Name: "Local VA bad, remote VAs OK", 515 Remotes: []remoteConf{ 516 {ua: pass, rir: arin}, 517 {ua: pass, rir: ripe}, 518 {ua: pass, rir: apnic}, 519 }, 520 PrimaryUA: fail, 521 ExpectedProbType: string(probs.UnauthorizedProblem), 522 }, 523 { 524 // If one out of three remote VAs fails with an internal err it should succeed 525 Name: "Local VA ok, 1/3 remote VA internal err", 526 Remotes: []remoteConf{ 527 {ua: pass, rir: arin}, 528 {ua: pass, rir: ripe}, 529 {ua: pass, rir: apnic, impl: brokenVA}, 530 }, 531 PrimaryUA: pass, 532 }, 533 { 534 // If two out of three remote VAs fail with an internal err it should fail 535 Name: "Local VA ok, 2/3 remote VAs internal err", 536 Remotes: []remoteConf{ 537 {ua: pass, rir: arin}, 538 {ua: pass, rir: ripe, impl: brokenVA}, 539 {ua: pass, rir: apnic, impl: brokenVA}, 540 }, 541 PrimaryUA: pass, 542 ExpectedProbType: string(probs.ServerInternalProblem), 543 // The real failure cause should be logged 544 ExpectedLogContains: errBrokenRemoteVA.Error(), 545 }, 546 { 547 // If one out of five remote VAs fail with an internal err it should succeed 548 Name: "Local VA ok, 1/5 remote VAs internal err", 549 Remotes: []remoteConf{ 550 {ua: pass, rir: arin}, 551 {ua: pass, rir: ripe}, 552 {ua: pass, rir: apnic}, 553 {ua: pass, rir: lacnic}, 554 {ua: pass, rir: afrinic, impl: brokenVA}, 555 }, 556 PrimaryUA: pass, 557 }, 558 { 559 // If two out of five remote VAs fail with an internal err it should fail 560 Name: "Local VA ok, 2/5 remote VAs internal err", 561 Remotes: []remoteConf{ 562 {ua: pass, rir: arin}, 563 {ua: pass, rir: ripe}, 564 {ua: pass, rir: apnic}, 565 {ua: pass, rir: arin, impl: brokenVA}, 566 {ua: pass, rir: ripe, impl: brokenVA}, 567 }, 568 PrimaryUA: pass, 569 ExpectedProbType: string(probs.ServerInternalProblem), 570 // The real failure cause should be logged 571 ExpectedLogContains: errBrokenRemoteVA.Error(), 572 }, 573 { 574 // If two out of six remote VAs fail with an internal err it should succeed 575 Name: "Local VA ok, 2/6 remote VAs internal err", 576 Remotes: []remoteConf{ 577 {ua: pass, rir: arin}, 578 {ua: pass, rir: ripe}, 579 {ua: pass, rir: apnic}, 580 {ua: pass, rir: lacnic}, 581 {ua: pass, rir: afrinic, impl: brokenVA}, 582 {ua: pass, rir: arin, impl: brokenVA}, 583 }, 584 PrimaryUA: pass, 585 }, 586 { 587 // If three out of six remote VAs fail with an internal err it should fail 588 Name: "Local VA ok, 4/6 remote VAs internal err", 589 Remotes: []remoteConf{ 590 {ua: pass, rir: arin}, 591 {ua: pass, rir: ripe}, 592 {ua: pass, rir: apnic}, 593 {ua: pass, rir: lacnic, impl: brokenVA}, 594 {ua: pass, rir: afrinic, impl: brokenVA}, 595 {ua: pass, rir: arin, impl: brokenVA}, 596 }, 597 PrimaryUA: pass, 598 ExpectedProbType: string(probs.ServerInternalProblem), 599 // The real failure cause should be logged 600 ExpectedLogContains: errBrokenRemoteVA.Error(), 601 }, 602 { 603 // With only one working remote VA there should be a validation failure 604 Name: "Local VA and one remote VA OK", 605 Remotes: []remoteConf{ 606 {ua: pass, rir: arin}, 607 {ua: fail, rir: ripe}, 608 {ua: fail, rir: apnic}, 609 }, 610 PrimaryUA: pass, 611 ExpectedProbType: string(probs.UnauthorizedProblem), 612 ExpectedLogContains: "During secondary validation: The key authorization file from the server", 613 }, 614 { 615 // If one remote VA cancels, it should succeed 616 Name: "Local VA and one remote VA OK, one cancelled VA", 617 Remotes: []remoteConf{ 618 {ua: pass, rir: arin}, 619 {ua: pass, rir: ripe, impl: cancelledVA}, 620 {ua: pass, rir: apnic}, 621 }, 622 PrimaryUA: pass, 623 }, 624 { 625 // If all remote VAs cancel, it should fail 626 Name: "Local VA OK, three cancelled remote VAs", 627 Remotes: []remoteConf{ 628 {ua: pass, rir: arin, impl: cancelledVA}, 629 {ua: pass, rir: ripe, impl: cancelledVA}, 630 {ua: pass, rir: apnic, impl: cancelledVA}, 631 }, 632 PrimaryUA: pass, 633 ExpectedProbType: string(probs.ServerInternalProblem), 634 ExpectedLogContains: "During secondary validation: Secondary validation RPC canceled", 635 }, 636 { 637 // With the local and remote VAs seeing diff problems, we expect a problem. 638 Name: "Local and remote VA differential", 639 Remotes: []remoteConf{ 640 {ua: fail, rir: arin}, 641 {ua: fail, rir: ripe}, 642 {ua: fail, rir: apnic}, 643 }, 644 PrimaryUA: pass, 645 ExpectedProbType: string(probs.UnauthorizedProblem), 646 ExpectedLogContains: "During secondary validation: The key authorization file from the server", 647 }, 648 } 649 650 for _, tc := range testCases { 651 t.Run(tc.Name, func(t *testing.T) { 652 t.Parallel() 653 654 // Configure one test server per test case so that all tests can run in parallel. 655 ms := httpMultiSrv(t, expectedToken, map[string]bool{pass: true, fail: false}) 656 defer ms.Close() 657 658 // Configure a primary VA with testcase remote VAs. 659 localVA, mockLog := setupWithRemotes(ms.Server, tc.PrimaryUA, tc.Remotes, nil) 660 661 // Perform all validations 662 res, _ := localVA.DoDCV(ctx, req) 663 if res.Problem == nil && tc.ExpectedProbType != "" { 664 t.Errorf("expected prob %v, got nil", tc.ExpectedProbType) 665 } else if res.Problem != nil && tc.ExpectedProbType == "" { 666 t.Errorf("expected no prob, got %v", res.Problem) 667 } else if res.Problem != nil && tc.ExpectedProbType != "" { 668 // That result should match expected. 669 test.AssertEquals(t, res.Problem.ProblemType, tc.ExpectedProbType) 670 } 671 672 if tc.ExpectedLogContains != "" { 673 lines := mockLog.GetAllMatching(tc.ExpectedLogContains) 674 if len(lines) == 0 { 675 t.Fatalf("Got log %v; expected %q", mockLog.GetAll(), tc.ExpectedLogContains) 676 } 677 } 678 }) 679 } 680 } 681 682 func TestMultiVAPolicy(t *testing.T) { 683 t.Parallel() 684 685 remoteConfs := []remoteConf{ 686 {ua: fail, rir: arin}, 687 {ua: fail, rir: ripe}, 688 {ua: fail, rir: apnic}, 689 } 690 691 ms := httpMultiSrv(t, expectedToken, map[string]bool{pass: true, fail: false}) 692 defer ms.Close() 693 694 // Create a local test VA with the remote VAs 695 localVA, _ := setupWithRemotes(ms.Server, pass, remoteConfs, nil) 696 697 // Perform validation for a domain not in the disabledDomains list 698 req := createValidationRequest(identifier.NewDNS("letsencrypt.org"), core.ChallengeTypeHTTP01) 699 res, _ := localVA.DoDCV(ctx, req) 700 // It should fail 701 if res.Problem == nil { 702 t.Error("expected prob from PerformValidation, got nil") 703 } 704 } 705 706 func TestMultiVALogging(t *testing.T) { 707 t.Parallel() 708 709 remoteConfs := []remoteConf{ 710 {ua: pass, rir: arin}, 711 {ua: pass, rir: ripe}, 712 {ua: pass, rir: apnic}, 713 } 714 715 ms := httpMultiSrv(t, expectedToken, map[string]bool{pass: true, fail: false}) 716 defer ms.Close() 717 718 va, _ := setupWithRemotes(ms.Server, pass, remoteConfs, nil) 719 req := createValidationRequest(identifier.NewDNS("letsencrypt.org"), core.ChallengeTypeHTTP01) 720 res, err := va.DoDCV(ctx, req) 721 test.Assert(t, res.Problem == nil, fmt.Sprintf("validation failed with: %#v", res.Problem)) 722 test.AssertNotError(t, err, "performing validation") 723 } 724 725 func TestDetailedError(t *testing.T) { 726 cases := []struct { 727 err error 728 ip netip.Addr 729 expected string 730 }{ 731 { 732 err: ipError{ 733 ip: netip.MustParseAddr("192.168.1.1"), 734 err: &net.OpError{ 735 Op: "dial", 736 Net: "tcp", 737 Err: &os.SyscallError{ 738 Syscall: "getsockopt", 739 Err: syscall.ECONNREFUSED, 740 }, 741 }, 742 }, 743 expected: "192.168.1.1: Connection refused", 744 }, 745 { 746 err: &net.OpError{ 747 Op: "dial", 748 Net: "tcp", 749 Err: &os.SyscallError{ 750 Syscall: "getsockopt", 751 Err: syscall.ECONNREFUSED, 752 }, 753 }, 754 expected: "Connection refused", 755 }, 756 { 757 err: &net.OpError{ 758 Op: "dial", 759 Net: "tcp", 760 Err: &os.SyscallError{ 761 Syscall: "getsockopt", 762 Err: syscall.ECONNRESET, 763 }, 764 }, 765 ip: netip.Addr{}, 766 expected: "Connection reset by peer", 767 }, 768 } 769 for _, tc := range cases { 770 actual := detailedError(tc.err).Detail 771 if actual != tc.expected { 772 t.Errorf("Wrong detail for %v. Got %q, expected %q", tc.err, actual, tc.expected) 773 } 774 } 775 }