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  }