github.com/letsencrypt/boulder@v0.20251208.0/test/integration/validation_test.go (about)

     1  //go:build integration
     2  
     3  package integration
     4  
     5  import (
     6  	"crypto/ecdsa"
     7  	"crypto/elliptic"
     8  	"crypto/rand"
     9  	"database/sql"
    10  	"fmt"
    11  	"slices"
    12  	"strings"
    13  	"testing"
    14  	"time"
    15  
    16  	"github.com/eggsampler/acme/v3"
    17  	"github.com/miekg/dns"
    18  
    19  	challtestsrvclient "github.com/letsencrypt/boulder/test/chall-test-srv-client"
    20  	"github.com/letsencrypt/boulder/test/vars"
    21  )
    22  
    23  var expectedUserAgents = []string{"boulder", "remoteva-a", "remoteva-b", "remoteva-c"}
    24  
    25  func collectUserAgentsFromDNSRequests(requests []challtestsrvclient.DNSRequest) []string {
    26  	userAgents := make([]string, len(requests))
    27  	for i, request := range requests {
    28  		userAgents[i] = request.UserAgent
    29  	}
    30  	return userAgents
    31  }
    32  
    33  func assertUserAgentsLength(t *testing.T, got []string, checkType string) {
    34  	t.Helper()
    35  
    36  	if len(got) != 4 {
    37  		t.Errorf("During %s, expected 4 User-Agents, got %d", checkType, len(got))
    38  	}
    39  }
    40  
    41  func assertExpectedUserAgents(t *testing.T, got []string, checkType string) {
    42  	t.Helper()
    43  
    44  	for _, ua := range expectedUserAgents {
    45  		if !slices.Contains(got, ua) {
    46  			t.Errorf("During %s, expected User-Agent %q in %s (got %v)", checkType, ua, expectedUserAgents, got)
    47  		}
    48  	}
    49  }
    50  
    51  func TestMPICTLSALPN01(t *testing.T) {
    52  	t.Parallel()
    53  
    54  	client, err := makeClient()
    55  	if err != nil {
    56  		t.Fatalf("creating acme client: %s", err)
    57  	}
    58  
    59  	domain := randomDomain(t)
    60  
    61  	order, err := client.Client.NewOrder(client.Account, []acme.Identifier{{Type: "dns", Value: domain}})
    62  	if err != nil {
    63  		t.Fatalf("creating order: %s", err)
    64  	}
    65  
    66  	authz, err := client.Client.FetchAuthorization(client.Account, order.Authorizations[0])
    67  	if err != nil {
    68  		t.Fatalf("fetching authorization: %s", err)
    69  	}
    70  
    71  	chal, ok := authz.ChallengeMap[acme.ChallengeTypeTLSALPN01]
    72  	if !ok {
    73  		t.Fatalf("no TLS-ALPN-01 challenge found in %#v", authz)
    74  	}
    75  
    76  	_, err = testSrvClient.AddARecord(domain, []string{"64.112.117.134"})
    77  	if err != nil {
    78  		t.Fatalf("adding A record: %s", err)
    79  	}
    80  	defer func() {
    81  		testSrvClient.RemoveARecord(domain)
    82  	}()
    83  
    84  	_, err = testSrvClient.AddTLSALPN01Response(domain, chal.KeyAuthorization)
    85  	if err != nil {
    86  		t.Fatal(err)
    87  	}
    88  	defer func() {
    89  		_, err = testSrvClient.RemoveTLSALPN01Response(domain)
    90  		if err != nil {
    91  			t.Fatal(err)
    92  		}
    93  	}()
    94  
    95  	chal, err = client.Client.UpdateChallenge(client.Account, chal)
    96  	if err != nil {
    97  		t.Fatalf("completing TLS-ALPN-01 validation: %s", err)
    98  	}
    99  
   100  	validationEvents, err := testSrvClient.TLSALPN01RequestHistory(domain)
   101  	if err != nil {
   102  		t.Fatal(err)
   103  	}
   104  	if len(validationEvents) != 4 {
   105  		t.Errorf("expected 4 validation events got %d", len(validationEvents))
   106  	}
   107  
   108  	dnsEvents, err := testSrvClient.DNSRequestHistory(domain)
   109  	if err != nil {
   110  		t.Fatal(err)
   111  	}
   112  
   113  	var caaEvents []challtestsrvclient.DNSRequest
   114  	for _, event := range dnsEvents {
   115  		if event.Question.Qtype == dns.TypeCAA {
   116  			caaEvents = append(caaEvents, event)
   117  		}
   118  	}
   119  	assertUserAgentsLength(t, collectUserAgentsFromDNSRequests(caaEvents), "CAA check")
   120  	assertExpectedUserAgents(t, collectUserAgentsFromDNSRequests(caaEvents), "CAA check")
   121  }
   122  
   123  func TestMPICDNS01(t *testing.T) {
   124  	t.Parallel()
   125  
   126  	client, err := makeClient()
   127  	if err != nil {
   128  		t.Fatalf("creating acme client: %s", err)
   129  	}
   130  
   131  	domain := randomDomain(t)
   132  
   133  	order, err := client.Client.NewOrder(client.Account, []acme.Identifier{{Type: "dns", Value: domain}})
   134  	if err != nil {
   135  		t.Fatalf("creating order: %s", err)
   136  	}
   137  
   138  	authz, err := client.Client.FetchAuthorization(client.Account, order.Authorizations[0])
   139  	if err != nil {
   140  		t.Fatalf("fetching authorization: %s", err)
   141  	}
   142  
   143  	chal, ok := authz.ChallengeMap[acme.ChallengeTypeDNS01]
   144  	if !ok {
   145  		t.Fatalf("no DNS challenge found in %#v", authz)
   146  	}
   147  
   148  	_, err = testSrvClient.AddDNS01Response(domain, chal.KeyAuthorization)
   149  	if err != nil {
   150  		t.Fatal(err)
   151  	}
   152  	defer func() {
   153  		_, err = testSrvClient.RemoveDNS01Response(domain)
   154  		if err != nil {
   155  			t.Fatal(err)
   156  		}
   157  	}()
   158  
   159  	chal, err = client.Client.UpdateChallenge(client.Account, chal)
   160  	if err != nil {
   161  		t.Fatalf("completing DNS-01 validation: %s", err)
   162  	}
   163  
   164  	challDomainDNSEvents, err := testSrvClient.DNSRequestHistory("_acme-challenge." + domain)
   165  	if err != nil {
   166  		t.Fatal(err)
   167  	}
   168  
   169  	var validationEvents []challtestsrvclient.DNSRequest
   170  	for _, event := range challDomainDNSEvents {
   171  		if event.Question.Qtype == dns.TypeTXT && event.Question.Name == "_acme-challenge."+domain+"." {
   172  			validationEvents = append(validationEvents, event)
   173  		}
   174  	}
   175  	assertUserAgentsLength(t, collectUserAgentsFromDNSRequests(validationEvents), "DNS-01 validation")
   176  	assertExpectedUserAgents(t, collectUserAgentsFromDNSRequests(validationEvents), "DNS-01 validation")
   177  
   178  	domainDNSEvents, err := testSrvClient.DNSRequestHistory(domain)
   179  	if err != nil {
   180  		t.Fatal(err)
   181  	}
   182  
   183  	var caaEvents []challtestsrvclient.DNSRequest
   184  	for _, event := range domainDNSEvents {
   185  		if event.Question.Qtype == dns.TypeCAA {
   186  			caaEvents = append(caaEvents, event)
   187  		}
   188  	}
   189  	assertUserAgentsLength(t, collectUserAgentsFromDNSRequests(caaEvents), "CAA check")
   190  	assertExpectedUserAgents(t, collectUserAgentsFromDNSRequests(caaEvents), "CAA check")
   191  }
   192  
   193  func TestMPICHTTP01(t *testing.T) {
   194  	t.Parallel()
   195  
   196  	client, err := makeClient()
   197  	if err != nil {
   198  		t.Fatalf("creating acme client: %s", err)
   199  	}
   200  
   201  	domain := randomDomain(t)
   202  
   203  	order, err := client.Client.NewOrder(client.Account, []acme.Identifier{{Type: "dns", Value: domain}})
   204  	if err != nil {
   205  		t.Fatalf("creating order: %s", err)
   206  	}
   207  
   208  	authz, err := client.Client.FetchAuthorization(client.Account, order.Authorizations[0])
   209  	if err != nil {
   210  		t.Fatalf("fetching authorization: %s", err)
   211  	}
   212  
   213  	chal, ok := authz.ChallengeMap[acme.ChallengeTypeHTTP01]
   214  	if !ok {
   215  		t.Fatalf("no HTTP challenge found in %#v", authz)
   216  	}
   217  
   218  	_, err = testSrvClient.AddHTTP01Response(chal.Token, chal.KeyAuthorization)
   219  	if err != nil {
   220  		t.Fatal(err)
   221  	}
   222  	defer func() {
   223  		_, err = testSrvClient.RemoveHTTP01Response(chal.Token)
   224  		if err != nil {
   225  			t.Fatal(err)
   226  		}
   227  	}()
   228  
   229  	chal, err = client.Client.UpdateChallenge(client.Account, chal)
   230  	if err != nil {
   231  		t.Fatalf("completing HTTP-01 validation: %s", err)
   232  	}
   233  
   234  	validationEvents, err := testSrvClient.HTTPRequestHistory(domain)
   235  	if err != nil {
   236  		t.Fatal(err)
   237  	}
   238  
   239  	var validationUAs []string
   240  	for _, event := range validationEvents {
   241  		if event.URL == "/.well-known/acme-challenge/"+chal.Token {
   242  			validationUAs = append(validationUAs, event.UserAgent)
   243  		}
   244  	}
   245  	assertUserAgentsLength(t, validationUAs, "HTTP-01 validation")
   246  	assertExpectedUserAgents(t, validationUAs, "HTTP-01 validation")
   247  
   248  	dnsEvents, err := testSrvClient.DNSRequestHistory(domain)
   249  	if err != nil {
   250  		t.Fatal(err)
   251  	}
   252  
   253  	var caaEvents []challtestsrvclient.DNSRequest
   254  	for _, event := range dnsEvents {
   255  		if event.Question.Qtype == dns.TypeCAA {
   256  			caaEvents = append(caaEvents, event)
   257  		}
   258  	}
   259  
   260  	assertUserAgentsLength(t, collectUserAgentsFromDNSRequests(caaEvents), "CAA check")
   261  	assertExpectedUserAgents(t, collectUserAgentsFromDNSRequests(caaEvents), "CAA check")
   262  }
   263  
   264  func TestCAARechecking(t *testing.T) {
   265  	t.Parallel()
   266  
   267  	domain := randomDomain(t)
   268  	idents := []acme.Identifier{{Type: "dns", Value: domain}}
   269  
   270  	// Create an order and authorization, and fulfill the associated challenge.
   271  	// This should put the authz into the "valid" state, since CAA checks passed.
   272  	client, err := makeClient()
   273  	if err != nil {
   274  		t.Fatalf("creating acme client: %s", err)
   275  	}
   276  
   277  	order, err := client.Client.NewOrder(client.Account, idents)
   278  	if err != nil {
   279  		t.Fatalf("creating order: %s", err)
   280  	}
   281  
   282  	authz, err := client.Client.FetchAuthorization(client.Account, order.Authorizations[0])
   283  	if err != nil {
   284  		t.Fatalf("fetching authorization: %s", err)
   285  	}
   286  
   287  	chal, ok := authz.ChallengeMap[acme.ChallengeTypeHTTP01]
   288  	if !ok {
   289  		t.Fatalf("no HTTP challenge found in %#v", authz)
   290  	}
   291  
   292  	_, err = testSrvClient.AddHTTP01Response(chal.Token, chal.KeyAuthorization)
   293  	if err != nil {
   294  		t.Fatal(err)
   295  	}
   296  	defer func() {
   297  		_, err = testSrvClient.RemoveHTTP01Response(chal.Token)
   298  		if err != nil {
   299  			t.Fatal(err)
   300  		}
   301  	}()
   302  
   303  	chal, err = client.Client.UpdateChallenge(client.Account, chal)
   304  	if err != nil {
   305  		t.Fatalf("completing HTTP-01 validation: %s", err)
   306  	}
   307  
   308  	// Manipulate the database so that it looks like the authz was validated
   309  	// more than 8 hours ago.
   310  	db, err := sql.Open("mysql", vars.DBConnSAIntegrationFullPerms)
   311  	if err != nil {
   312  		t.Fatalf("sql.Open: %s", err)
   313  	}
   314  
   315  	_, err = db.Exec(`UPDATE authz2 SET attemptedAt = ? WHERE identifierValue = ?`, time.Now().Add(-24*time.Hour).Format(time.DateTime), domain)
   316  	if err != nil {
   317  		t.Fatalf("updating authz attemptedAt timestamp: %s", err)
   318  	}
   319  
   320  	// Change the CAA record to now forbid issuance.
   321  	_, err = testSrvClient.AddCAAIssue(domain, ";")
   322  	if err != nil {
   323  		t.Fatal(err)
   324  	}
   325  
   326  	// Try to finalize the order created above. Due to our db manipulation, this
   327  	// should trigger a CAA recheck. And due to our challtestsrv manipulation,
   328  	// that CAA recheck should fail. Therefore the whole finalize should fail.
   329  	key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
   330  	if err != nil {
   331  		t.Fatalf("generating cert key: %s", err)
   332  	}
   333  
   334  	csr, err := makeCSR(key, idents, false)
   335  	if err != nil {
   336  		t.Fatalf("generating finalize csr: %s", err)
   337  	}
   338  
   339  	_, err = client.Client.FinalizeOrder(client.Account, order, csr)
   340  	if err == nil {
   341  		t.Errorf("expected finalize to fail, but got success")
   342  	}
   343  	if !strings.Contains(err.Error(), "CAA") {
   344  		t.Errorf("expected finalize to fail due to CAA, but got: %s", err)
   345  	}
   346  }
   347  
   348  func TestCAAAccountURI(t *testing.T) {
   349  	t.Parallel()
   350  	client, err := makeClient()
   351  	if err != nil {
   352  		t.Fatalf("creating acme client: %s", err)
   353  	}
   354  
   355  	domain := random_domain()
   356  	idents := []acme.Identifier{{Type: "dns", Value: domain}}
   357  	record := fmt.Sprintf("happy-hacker-ca.invalid; accounturi=%s", client.Account.URL)
   358  	_, err = testSrvClient.AddCAAIssue(domain, record)
   359  	if err != nil {
   360  		t.Fatal(err)
   361  	}
   362  	_, err = authAndIssue(client, nil, idents, true, "")
   363  	if err != nil {
   364  		t.Fatalf("authAndIssue: %s", err)
   365  	}
   366  
   367  	newClient, err := makeClient()
   368  	if err != nil {
   369  		t.Fatalf("creating acme client: %s", err)
   370  	}
   371  
   372  	// New domain to avoid rate limiting; same old CAA record to trigger CAA error.
   373  	domain = random_domain()
   374  	idents = []acme.Identifier{{Type: "dns", Value: domain}}
   375  	_, err = testSrvClient.AddCAAIssue(domain, record)
   376  	if err != nil {
   377  		t.Fatal(err)
   378  	}
   379  	_, err = authAndIssue(newClient, nil, idents, true, "")
   380  	if err == nil {
   381  		t.Errorf("expected error with mismatched CAA account URI, but got success")
   382  	}
   383  	if err != nil && !strings.Contains(err.Error(), "CAA") {
   384  		t.Errorf("expected CAA error with mismatched CAA account URI, but got: %s", err)
   385  	}
   386  }
   387  
   388  func TestCAAValidationMethods(t *testing.T) {
   389  	t.Parallel()
   390  
   391  	testCases := []struct {
   392  		caa         string
   393  		challType   string
   394  		expectError bool
   395  	}{
   396  		{"dns-01", acme.ChallengeTypeDNS01, false},
   397  		{"dns-01", acme.ChallengeTypeHTTP01, true},
   398  		{"http-01", acme.ChallengeTypeHTTP01, false},
   399  		{"http-01", acme.ChallengeTypeDNS01, true},
   400  		{"dns-01,http-01", acme.ChallengeTypeDNS01, false},
   401  		{"dns-01,http-01", acme.ChallengeTypeHTTP01, false},
   402  	}
   403  
   404  	client, err := makeClient()
   405  	if err != nil {
   406  		t.Fatalf("creating acme client: %s", err)
   407  	}
   408  
   409  	for _, tc := range testCases {
   410  		t.Run(fmt.Sprintf("%s-with-caa-allowing-%s", tc.challType, tc.caa), func(t *testing.T) {
   411  			domain := random_domain()
   412  			record := fmt.Sprintf("happy-hacker-ca.invalid; validationmethods=%s", tc.caa)
   413  			_, err = testSrvClient.AddCAAIssue(domain, record)
   414  			if err != nil {
   415  				t.Fatal(err)
   416  			}
   417  
   418  			idents := []acme.Identifier{{Type: "dns", Value: domain}}
   419  			order, err := client.Client.NewOrder(client.Account, idents)
   420  			if err != nil {
   421  				t.Fatalf("creating order: %s", err)
   422  			}
   423  
   424  			authz, err := client.Client.FetchAuthorization(client.Account, order.Authorizations[0])
   425  			if err != nil {
   426  				t.Fatalf("fetching authorization: %s", err)
   427  			}
   428  
   429  			var chal acme.Challenge
   430  			var ok bool
   431  			switch tc.challType {
   432  			case acme.ChallengeTypeDNS01:
   433  				chal, ok = authz.ChallengeMap[acme.ChallengeTypeDNS01]
   434  				if !ok {
   435  					t.Fatalf("no DNS challenge found in %#v", authz)
   436  				}
   437  				_, err = testSrvClient.AddDNS01Response(domain, chal.KeyAuthorization)
   438  				if err != nil {
   439  					t.Fatalf("adding DNS-01 response: %s", err)
   440  				}
   441  			case acme.ChallengeTypeHTTP01:
   442  				chal, ok = authz.ChallengeMap[acme.ChallengeTypeHTTP01]
   443  				if !ok {
   444  					t.Fatalf("no HTTP challenge found in %#v", authz)
   445  				}
   446  				_, err = testSrvClient.AddHTTP01Response(chal.Token, chal.KeyAuthorization)
   447  				if err != nil {
   448  					t.Fatalf("adding HTTP-01 response: %s", err)
   449  				}
   450  			default:
   451  				t.Fatalf("unknown challenge type: %q", tc.challType)
   452  			}
   453  
   454  			chal, err = client.Client.UpdateChallenge(client.Account, chal)
   455  			if err != nil {
   456  				if tc.expectError && !strings.Contains(err.Error(), "CAA") {
   457  					t.Errorf("expected validation to fail due to CAA, but got: %s", err)
   458  				} else if !tc.expectError {
   459  					t.Errorf("issuing with challenge type %q and CAA %q failed: %s", tc.challType, tc.caa, err)
   460  				}
   461  			}
   462  			if err == nil && tc.expectError {
   463  				t.Errorf("issuing with challenge type %q and CAA %q succeeded, but should have failed", tc.challType, tc.caa)
   464  			}
   465  		})
   466  	}
   467  }