github.com/letsencrypt/boulder@v0.20251208.0/va/caa_test.go (about)

     1  package va
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"net/netip"
     9  	"regexp"
    10  	"slices"
    11  	"strings"
    12  	"testing"
    13  
    14  	"github.com/miekg/dns"
    15  	"github.com/prometheus/client_golang/prometheus"
    16  
    17  	"github.com/letsencrypt/boulder/bdns"
    18  	"github.com/letsencrypt/boulder/core"
    19  	berrors "github.com/letsencrypt/boulder/errors"
    20  	"github.com/letsencrypt/boulder/features"
    21  	"github.com/letsencrypt/boulder/identifier"
    22  	"github.com/letsencrypt/boulder/probs"
    23  	"github.com/letsencrypt/boulder/test"
    24  
    25  	blog "github.com/letsencrypt/boulder/log"
    26  	vapb "github.com/letsencrypt/boulder/va/proto"
    27  )
    28  
    29  // caaMockDNS implements the `dns.DNSClient` interface with a set of useful test
    30  // answers for CAA queries.
    31  type caaMockDNS struct{}
    32  
    33  func (mock caaMockDNS) LookupTXT(_ context.Context, hostname string) ([]string, bdns.ResolverAddrs, error) {
    34  	return nil, bdns.ResolverAddrs{"caaMockDNS"}, nil
    35  }
    36  
    37  func (mock caaMockDNS) LookupHost(_ context.Context, hostname string) ([]netip.Addr, bdns.ResolverAddrs, error) {
    38  	return []netip.Addr{netip.MustParseAddr("127.0.0.1")}, bdns.ResolverAddrs{"caaMockDNS"}, nil
    39  }
    40  
    41  func (mock caaMockDNS) LookupCAA(_ context.Context, domain string) ([]*dns.CAA, string, bdns.ResolverAddrs, error) {
    42  	var results []*dns.CAA
    43  	var record dns.CAA
    44  	switch strings.TrimRight(domain, ".") {
    45  	case "caa-timeout.com":
    46  		return nil, "", bdns.ResolverAddrs{"caaMockDNS"}, fmt.Errorf("error")
    47  	case "reserved.com":
    48  		record.Tag = "issue"
    49  		record.Value = "ca.com"
    50  		results = append(results, &record)
    51  	case "mixedcase.com":
    52  		record.Tag = "iSsUe"
    53  		record.Value = "ca.com"
    54  		results = append(results, &record)
    55  	case "critical.com":
    56  		record.Flag = 1
    57  		record.Tag = "issue"
    58  		record.Value = "ca.com"
    59  		results = append(results, &record)
    60  	case "present.com", "present.servfail.com":
    61  		record.Tag = "issue"
    62  		record.Value = "letsencrypt.org"
    63  		results = append(results, &record)
    64  	case "com":
    65  		// com has no CAA records.
    66  		return nil, "", bdns.ResolverAddrs{"caaMockDNS"}, nil
    67  	case "gonetld":
    68  		return nil, "", bdns.ResolverAddrs{"caaMockDNS"}, fmt.Errorf("NXDOMAIN")
    69  	case "servfail.com", "servfail.present.com":
    70  		return results, "", bdns.ResolverAddrs{"caaMockDNS"}, fmt.Errorf("SERVFAIL")
    71  	case "multi-crit-present.com":
    72  		record.Flag = 1
    73  		record.Tag = "issue"
    74  		record.Value = "ca.com"
    75  		results = append(results, &record)
    76  		secondRecord := record
    77  		secondRecord.Value = "letsencrypt.org"
    78  		results = append(results, &secondRecord)
    79  	case "unknown-critical.com":
    80  		record.Flag = 128
    81  		record.Tag = "foo"
    82  		record.Value = "bar"
    83  		results = append(results, &record)
    84  	case "unknown-critical2.com":
    85  		record.Flag = 1
    86  		record.Tag = "foo"
    87  		record.Value = "bar"
    88  		results = append(results, &record)
    89  	case "unknown-noncritical.com":
    90  		record.Flag = 0x7E // all bits we don't treat as meaning "critical"
    91  		record.Tag = "foo"
    92  		record.Value = "bar"
    93  		results = append(results, &record)
    94  	case "present-with-parameter.com":
    95  		record.Tag = "issue"
    96  		record.Value = "  letsencrypt.org  ;foo=bar;baz=bar"
    97  		results = append(results, &record)
    98  	case "present-with-invalid-tag.com":
    99  		record.Tag = "issue"
   100  		record.Value = "letsencrypt.org; a_b=123"
   101  		results = append(results, &record)
   102  	case "present-with-invalid-value.com":
   103  		record.Tag = "issue"
   104  		record.Value = "letsencrypt.org; ab=1 2 3"
   105  		results = append(results, &record)
   106  	case "present-dns-only.com":
   107  		record.Tag = "issue"
   108  		record.Value = "letsencrypt.org; validationmethods=dns-01"
   109  		results = append(results, &record)
   110  	case "present-http-only.com":
   111  		record.Tag = "issue"
   112  		record.Value = "letsencrypt.org; validationmethods=http-01"
   113  		results = append(results, &record)
   114  	case "present-http-or-dns.com":
   115  		record.Tag = "issue"
   116  		record.Value = "letsencrypt.org; validationmethods=http-01,dns-01"
   117  		results = append(results, &record)
   118  	case "present-dns-only-correct-accounturi.com":
   119  		record.Tag = "issue"
   120  		record.Value = "letsencrypt.org; accounturi=https://letsencrypt.org/acct/reg/123; validationmethods=dns-01"
   121  		results = append(results, &record)
   122  	case "present-http-only-correct-accounturi.com":
   123  		record.Tag = "issue"
   124  		record.Value = "letsencrypt.org; accounturi=https://letsencrypt.org/acct/reg/123; validationmethods=http-01"
   125  		results = append(results, &record)
   126  	case "present-http-only-incorrect-accounturi.com":
   127  		record.Tag = "issue"
   128  		record.Value = "letsencrypt.org; accounturi=https://letsencrypt.org/acct/reg/321; validationmethods=http-01"
   129  		results = append(results, &record)
   130  	case "present-correct-accounturi.com":
   131  		record.Tag = "issue"
   132  		record.Value = "letsencrypt.org; accounturi=https://letsencrypt.org/acct/reg/123"
   133  		results = append(results, &record)
   134  	case "present-incorrect-accounturi.com":
   135  		record.Tag = "issue"
   136  		record.Value = "letsencrypt.org; accounturi=https://letsencrypt.org/acct/reg/321"
   137  		results = append(results, &record)
   138  	case "present-multiple-accounturi.com":
   139  		record.Tag = "issue"
   140  		record.Value = "letsencrypt.org; accounturi=https://letsencrypt.org/acct/reg/321"
   141  		results = append(results, &record)
   142  		secondRecord := record
   143  		secondRecord.Tag = "issue"
   144  		secondRecord.Value = "letsencrypt.org; accounturi=https://letsencrypt.org/acct/reg/123"
   145  		results = append(results, &secondRecord)
   146  	case "unsatisfiable.com":
   147  		record.Tag = "issue"
   148  		record.Value = ";"
   149  		results = append(results, &record)
   150  	case "unsatisfiable-wildcard.com":
   151  		// Forbidden issuance - issuewild doesn't contain LE
   152  		record.Tag = "issuewild"
   153  		record.Value = ";"
   154  		results = append(results, &record)
   155  	case "unsatisfiable-wildcard-override.com":
   156  		// Forbidden issuance - issue allows LE, issuewild overrides and does not
   157  		record.Tag = "issue"
   158  		record.Value = "letsencrypt.org"
   159  		results = append(results, &record)
   160  		secondRecord := record
   161  		secondRecord.Tag = "issuewild"
   162  		secondRecord.Value = "ca.com"
   163  		results = append(results, &secondRecord)
   164  	case "satisfiable-wildcard-override.com":
   165  		// Ok issuance - issue doesn't allow LE, issuewild overrides and does
   166  		record.Tag = "issue"
   167  		record.Value = "ca.com"
   168  		results = append(results, &record)
   169  		secondRecord := record
   170  		secondRecord.Tag = "issuewild"
   171  		secondRecord.Value = "letsencrypt.org"
   172  		results = append(results, &secondRecord)
   173  	case "satisfiable-multi-wildcard.com":
   174  		// Ok issuance - first issuewild doesn't permit LE but second does
   175  		record.Tag = "issuewild"
   176  		record.Value = "ca.com"
   177  		results = append(results, &record)
   178  		secondRecord := record
   179  		secondRecord.Tag = "issuewild"
   180  		secondRecord.Value = "letsencrypt.org"
   181  		results = append(results, &secondRecord)
   182  	case "satisfiable-wildcard.com":
   183  		// Ok issuance - issuewild allows LE
   184  		record.Tag = "issuewild"
   185  		record.Value = "letsencrypt.org"
   186  		results = append(results, &record)
   187  	}
   188  	var response string
   189  	if len(results) > 0 {
   190  		response = "foo"
   191  	}
   192  	return results, response, bdns.ResolverAddrs{"caaMockDNS"}, nil
   193  }
   194  
   195  func TestCAATimeout(t *testing.T) {
   196  	va, _ := setup(nil, "", nil, caaMockDNS{})
   197  
   198  	params := &caaParams{
   199  		accountURIID:     12345,
   200  		validationMethod: core.ChallengeTypeHTTP01,
   201  	}
   202  
   203  	err := va.checkCAA(ctx, identifier.NewDNS("caa-timeout.com"), params)
   204  	test.AssertErrorIs(t, err, berrors.DNS)
   205  	test.AssertContains(t, err.Error(), "error")
   206  }
   207  
   208  func TestCAAChecking(t *testing.T) {
   209  	testCases := []struct {
   210  		Name    string
   211  		Domain  string
   212  		FoundAt string
   213  		Valid   bool
   214  	}{
   215  		{
   216  			Name:    "Bad (Reserved)",
   217  			Domain:  "reserved.com",
   218  			FoundAt: "reserved.com",
   219  			Valid:   false,
   220  		},
   221  		{
   222  			Name:    "Bad (Reserved, Mixed case Issue)",
   223  			Domain:  "mixedcase.com",
   224  			FoundAt: "mixedcase.com",
   225  			Valid:   false,
   226  		},
   227  		{
   228  			Name:    "Bad (Critical)",
   229  			Domain:  "critical.com",
   230  			FoundAt: "critical.com",
   231  			Valid:   false,
   232  		},
   233  		{
   234  			Name:    "Bad (NX Critical)",
   235  			Domain:  "nx.critical.com",
   236  			FoundAt: "critical.com",
   237  			Valid:   false,
   238  		},
   239  		{
   240  			Name:    "Good (absent)",
   241  			Domain:  "absent.com",
   242  			FoundAt: "",
   243  			Valid:   true,
   244  		},
   245  		{
   246  			Name:    "Good (example.co.uk, absent)",
   247  			Domain:  "example.co.uk",
   248  			FoundAt: "",
   249  			Valid:   true,
   250  		},
   251  		{
   252  			Name:    "Good (present and valid)",
   253  			Domain:  "present.com",
   254  			FoundAt: "present.com",
   255  			Valid:   true,
   256  		},
   257  		{
   258  			Name:    "Good (present on parent)",
   259  			Domain:  "child.present.com",
   260  			FoundAt: "present.com",
   261  			Valid:   true,
   262  		},
   263  		{
   264  			Name:    "Good (present w/ servfail exception?)",
   265  			Domain:  "present.servfail.com",
   266  			FoundAt: "present.servfail.com",
   267  			Valid:   true,
   268  		},
   269  		{
   270  			Name:    "Good (multiple critical, one matching)",
   271  			Domain:  "multi-crit-present.com",
   272  			FoundAt: "multi-crit-present.com",
   273  			Valid:   true,
   274  		},
   275  		{
   276  			Name:    "Bad (unknown critical)",
   277  			Domain:  "unknown-critical.com",
   278  			FoundAt: "unknown-critical.com",
   279  			Valid:   false,
   280  		},
   281  		{
   282  			Name:    "Bad (unknown critical 2)",
   283  			Domain:  "unknown-critical2.com",
   284  			FoundAt: "unknown-critical2.com",
   285  			Valid:   false,
   286  		},
   287  		{
   288  			Name:    "Good (unknown non-critical, no issue)",
   289  			Domain:  "unknown-noncritical.com",
   290  			FoundAt: "unknown-noncritical.com",
   291  			Valid:   true,
   292  		},
   293  		{
   294  			Name:    "Good (unknown non-critical, no issuewild)",
   295  			Domain:  "*.unknown-noncritical.com",
   296  			FoundAt: "unknown-noncritical.com",
   297  			Valid:   true,
   298  		},
   299  		{
   300  			Name:    "Good (issue rec with unknown params)",
   301  			Domain:  "present-with-parameter.com",
   302  			FoundAt: "present-with-parameter.com",
   303  			Valid:   true,
   304  		},
   305  		{
   306  			Name:    "Bad (issue rec with invalid tag)",
   307  			Domain:  "present-with-invalid-tag.com",
   308  			FoundAt: "present-with-invalid-tag.com",
   309  			Valid:   false,
   310  		},
   311  		{
   312  			Name:    "Bad (issue rec with invalid value)",
   313  			Domain:  "present-with-invalid-value.com",
   314  			FoundAt: "present-with-invalid-value.com",
   315  			Valid:   false,
   316  		},
   317  		{
   318  			Name:    "Bad (restricts to dns-01, but tested with http-01)",
   319  			Domain:  "present-dns-only.com",
   320  			FoundAt: "present-dns-only.com",
   321  			Valid:   false,
   322  		},
   323  		{
   324  			Name:    "Good (restricts to http-01, tested with http-01)",
   325  			Domain:  "present-http-only.com",
   326  			FoundAt: "present-http-only.com",
   327  			Valid:   true,
   328  		},
   329  		{
   330  			Name:    "Good (restricts to http-01 or dns-01, tested with http-01)",
   331  			Domain:  "present-http-or-dns.com",
   332  			FoundAt: "present-http-or-dns.com",
   333  			Valid:   true,
   334  		},
   335  		{
   336  			Name:    "Good (restricts to accounturi, tested with correct account)",
   337  			Domain:  "present-correct-accounturi.com",
   338  			FoundAt: "present-correct-accounturi.com",
   339  			Valid:   true,
   340  		},
   341  		{
   342  			Name:    "Good (restricts to http-01 and accounturi, tested with correct account)",
   343  			Domain:  "present-http-only-correct-accounturi.com",
   344  			FoundAt: "present-http-only-correct-accounturi.com",
   345  			Valid:   true,
   346  		},
   347  		{
   348  			Name:    "Bad (restricts to dns-01 and accounturi, tested with http-01)",
   349  			Domain:  "present-dns-only-correct-accounturi.com",
   350  			FoundAt: "present-dns-only-correct-accounturi.com",
   351  			Valid:   false,
   352  		},
   353  		{
   354  			Name:    "Bad (restricts to http-01 and accounturi, tested with incorrect account)",
   355  			Domain:  "present-http-only-incorrect-accounturi.com",
   356  			FoundAt: "present-http-only-incorrect-accounturi.com",
   357  			Valid:   false,
   358  		},
   359  		{
   360  			Name:    "Bad (restricts to accounturi, tested with incorrect account)",
   361  			Domain:  "present-incorrect-accounturi.com",
   362  			FoundAt: "present-incorrect-accounturi.com",
   363  			Valid:   false,
   364  		},
   365  		{
   366  			Name:    "Good (restricts to multiple accounturi, tested with a correct account)",
   367  			Domain:  "present-multiple-accounturi.com",
   368  			FoundAt: "present-multiple-accounturi.com",
   369  			Valid:   true,
   370  		},
   371  		{
   372  			Name:    "Bad (unsatisfiable issue record)",
   373  			Domain:  "unsatisfiable.com",
   374  			FoundAt: "unsatisfiable.com",
   375  			Valid:   false,
   376  		},
   377  		{
   378  			Name:    "Bad (unsatisfiable issue, wildcard)",
   379  			Domain:  "*.unsatisfiable.com",
   380  			FoundAt: "unsatisfiable.com",
   381  			Valid:   false,
   382  		},
   383  		{
   384  			Name:    "Bad (unsatisfiable wildcard)",
   385  			Domain:  "*.unsatisfiable-wildcard.com",
   386  			FoundAt: "unsatisfiable-wildcard.com",
   387  			Valid:   false,
   388  		},
   389  		{
   390  			Name:    "Bad (unsatisfiable wildcard override)",
   391  			Domain:  "*.unsatisfiable-wildcard-override.com",
   392  			FoundAt: "unsatisfiable-wildcard-override.com",
   393  			Valid:   false,
   394  		},
   395  		{
   396  			Name:    "Good (satisfiable wildcard)",
   397  			Domain:  "*.satisfiable-wildcard.com",
   398  			FoundAt: "satisfiable-wildcard.com",
   399  			Valid:   true,
   400  		},
   401  		{
   402  			Name:    "Good (multiple issuewild, one satisfiable)",
   403  			Domain:  "*.satisfiable-multi-wildcard.com",
   404  			FoundAt: "satisfiable-multi-wildcard.com",
   405  			Valid:   true,
   406  		},
   407  		{
   408  			Name:    "Good (satisfiable wildcard override)",
   409  			Domain:  "*.satisfiable-wildcard-override.com",
   410  			FoundAt: "satisfiable-wildcard-override.com",
   411  			Valid:   true,
   412  		},
   413  	}
   414  
   415  	accountURIID := int64(123)
   416  	method := core.ChallengeTypeHTTP01
   417  	params := &caaParams{accountURIID: accountURIID, validationMethod: method}
   418  
   419  	va, _ := setup(nil, "", nil, caaMockDNS{})
   420  	va.accountURIPrefixes = []string{"https://letsencrypt.org/acct/reg/"}
   421  
   422  	for _, caaTest := range testCases {
   423  		mockLog := va.log.(*blog.Mock)
   424  		defer mockLog.Clear()
   425  		t.Run(caaTest.Name, func(t *testing.T) {
   426  			ident := identifier.NewDNS(caaTest.Domain)
   427  			foundAt, valid, _, err := va.checkCAARecords(ctx, ident, params)
   428  			if err != nil {
   429  				t.Errorf("checkCAARecords error for %s: %s", caaTest.Domain, err)
   430  			}
   431  			if foundAt != caaTest.FoundAt {
   432  				t.Errorf("checkCAARecords presence mismatch for %s: got %q expected %q", caaTest.Domain, foundAt, caaTest.FoundAt)
   433  			}
   434  			if valid != caaTest.Valid {
   435  				t.Errorf("checkCAARecords validity mismatch for %s: got %t expected %t", caaTest.Domain, valid, caaTest.Valid)
   436  			}
   437  		})
   438  	}
   439  }
   440  
   441  func TestCAALogging(t *testing.T) {
   442  	va, _ := setup(nil, "", nil, caaMockDNS{})
   443  
   444  	testCases := []struct {
   445  		Name            string
   446  		Domain          string
   447  		AccountURIID    int64
   448  		ChallengeType   core.AcmeChallenge
   449  		ExpectedLogline string
   450  	}{
   451  		{
   452  			Domain:          "reserved.com",
   453  			AccountURIID:    12345,
   454  			ChallengeType:   core.ChallengeTypeHTTP01,
   455  			ExpectedLogline: "INFO: [AUDIT] Checked CAA records for reserved.com, [Present: true, Account ID: 12345, Challenge: http-01, Valid for issuance: false, Found at: \"reserved.com\"] Response=\"foo\"",
   456  		},
   457  		{
   458  			Domain:          "reserved.com",
   459  			AccountURIID:    12345,
   460  			ChallengeType:   core.ChallengeTypeDNS01,
   461  			ExpectedLogline: "INFO: [AUDIT] Checked CAA records for reserved.com, [Present: true, Account ID: 12345, Challenge: dns-01, Valid for issuance: false, Found at: \"reserved.com\"] Response=\"foo\"",
   462  		},
   463  		{
   464  			Domain:          "mixedcase.com",
   465  			AccountURIID:    12345,
   466  			ChallengeType:   core.ChallengeTypeHTTP01,
   467  			ExpectedLogline: "INFO: [AUDIT] Checked CAA records for mixedcase.com, [Present: true, Account ID: 12345, Challenge: http-01, Valid for issuance: false, Found at: \"mixedcase.com\"] Response=\"foo\"",
   468  		},
   469  		{
   470  			Domain:          "critical.com",
   471  			AccountURIID:    12345,
   472  			ChallengeType:   core.ChallengeTypeHTTP01,
   473  			ExpectedLogline: "INFO: [AUDIT] Checked CAA records for critical.com, [Present: true, Account ID: 12345, Challenge: http-01, Valid for issuance: false, Found at: \"critical.com\"] Response=\"foo\"",
   474  		},
   475  		{
   476  			Domain:          "present.com",
   477  			AccountURIID:    12345,
   478  			ChallengeType:   core.ChallengeTypeHTTP01,
   479  			ExpectedLogline: "INFO: [AUDIT] Checked CAA records for present.com, [Present: true, Account ID: 12345, Challenge: http-01, Valid for issuance: true, Found at: \"present.com\"] Response=\"foo\"",
   480  		},
   481  		{
   482  			Domain:          "not.here.but.still.present.com",
   483  			AccountURIID:    12345,
   484  			ChallengeType:   core.ChallengeTypeHTTP01,
   485  			ExpectedLogline: "INFO: [AUDIT] Checked CAA records for not.here.but.still.present.com, [Present: true, Account ID: 12345, Challenge: http-01, Valid for issuance: true, Found at: \"present.com\"] Response=\"foo\"",
   486  		},
   487  		{
   488  			Domain:          "multi-crit-present.com",
   489  			AccountURIID:    12345,
   490  			ChallengeType:   core.ChallengeTypeHTTP01,
   491  			ExpectedLogline: "INFO: [AUDIT] Checked CAA records for multi-crit-present.com, [Present: true, Account ID: 12345, Challenge: http-01, Valid for issuance: true, Found at: \"multi-crit-present.com\"] Response=\"foo\"",
   492  		},
   493  		{
   494  			Domain:          "present-with-parameter.com",
   495  			AccountURIID:    12345,
   496  			ChallengeType:   core.ChallengeTypeHTTP01,
   497  			ExpectedLogline: "INFO: [AUDIT] Checked CAA records for present-with-parameter.com, [Present: true, Account ID: 12345, Challenge: http-01, Valid for issuance: true, Found at: \"present-with-parameter.com\"] Response=\"foo\"",
   498  		},
   499  		{
   500  			Domain:          "satisfiable-wildcard-override.com",
   501  			AccountURIID:    12345,
   502  			ChallengeType:   core.ChallengeTypeHTTP01,
   503  			ExpectedLogline: "INFO: [AUDIT] Checked CAA records for satisfiable-wildcard-override.com, [Present: true, Account ID: 12345, Challenge: http-01, Valid for issuance: false, Found at: \"satisfiable-wildcard-override.com\"] Response=\"foo\"",
   504  		},
   505  	}
   506  
   507  	for _, tc := range testCases {
   508  		t.Run(tc.Domain, func(t *testing.T) {
   509  			mockLog := va.log.(*blog.Mock)
   510  			defer mockLog.Clear()
   511  
   512  			params := &caaParams{
   513  				accountURIID:     tc.AccountURIID,
   514  				validationMethod: tc.ChallengeType,
   515  			}
   516  			_ = va.checkCAA(ctx, identifier.NewDNS(tc.Domain), params)
   517  
   518  			caaLogLines := mockLog.GetAllMatching(`Checked CAA records for`)
   519  			if len(caaLogLines) != 1 {
   520  				t.Errorf("checkCAARecords didn't audit log CAA record info. Instead got:\n%s\n",
   521  					strings.Join(mockLog.GetAllMatching(`.*`), "\n"))
   522  			} else {
   523  				test.AssertEquals(t, caaLogLines[0], tc.ExpectedLogline)
   524  			}
   525  		})
   526  	}
   527  }
   528  
   529  // TestDoCAAErrMessage tests that an error result from `va.IsCAAValid`
   530  // includes the domain name that was being checked in the failure detail.
   531  func TestDoCAAErrMessage(t *testing.T) {
   532  	t.Parallel()
   533  	va, _ := setup(nil, "", nil, caaMockDNS{})
   534  
   535  	// Call the operation with a domain we know fails with a generic error from the
   536  	// caaMockDNS.
   537  	domain := "caa-timeout.com"
   538  	resp, err := va.DoCAA(ctx, &vapb.IsCAAValidRequest{
   539  		Identifier:       identifier.NewDNS(domain).ToProto(),
   540  		ValidationMethod: string(core.ChallengeTypeHTTP01),
   541  		AccountURIID:     12345,
   542  	})
   543  
   544  	// The lookup itself should not return an error
   545  	test.AssertNotError(t, err, "Unexpected error calling IsCAAValidRequest")
   546  	// The result should not be nil
   547  	test.AssertNotNil(t, resp, "Response to IsCAAValidRequest was nil")
   548  	// The result's Problem should not be nil
   549  	test.AssertNotNil(t, resp.Problem, "Response Problem was nil")
   550  	// The result's Problem should be an error message that includes the domain.
   551  	test.AssertEquals(t, resp.Problem.Detail, fmt.Sprintf("While processing CAA for %s: error", domain))
   552  }
   553  
   554  // TestDoCAAParams tests that the IsCAAValid method rejects any requests
   555  // which do not have the necessary parameters to do CAA Account and Method
   556  // Binding checks.
   557  func TestDoCAAParams(t *testing.T) {
   558  	t.Parallel()
   559  	va, _ := setup(nil, "", nil, caaMockDNS{})
   560  
   561  	// Calling IsCAAValid without a ValidationMethod should fail.
   562  	_, err := va.DoCAA(ctx, &vapb.IsCAAValidRequest{
   563  		Identifier:   identifier.NewDNS("present.com").ToProto(),
   564  		AccountURIID: 12345,
   565  	})
   566  	test.AssertError(t, err, "calling IsCAAValid without a ValidationMethod")
   567  
   568  	// Calling IsCAAValid with an invalid ValidationMethod should fail.
   569  	_, err = va.DoCAA(ctx, &vapb.IsCAAValidRequest{
   570  		Identifier:       identifier.NewDNS("present.com").ToProto(),
   571  		ValidationMethod: "tls-sni-01",
   572  		AccountURIID:     12345,
   573  	})
   574  	test.AssertError(t, err, "calling IsCAAValid with a bad ValidationMethod")
   575  
   576  	// Calling IsCAAValid without an AccountURIID should fail.
   577  	_, err = va.DoCAA(ctx, &vapb.IsCAAValidRequest{
   578  		Identifier:       identifier.NewDNS("present.com").ToProto(),
   579  		ValidationMethod: string(core.ChallengeTypeHTTP01),
   580  	})
   581  	test.AssertError(t, err, "calling IsCAAValid without an AccountURIID")
   582  
   583  	// Calling IsCAAValid with a non-DNS identifier type should fail.
   584  	_, err = va.DoCAA(ctx, &vapb.IsCAAValidRequest{
   585  		Identifier:       identifier.NewIP(netip.MustParseAddr("127.0.0.1")).ToProto(),
   586  		ValidationMethod: string(core.ChallengeTypeHTTP01),
   587  		AccountURIID:     12345,
   588  	})
   589  	test.AssertError(t, err, "calling IsCAAValid with a non-DNS identifier type")
   590  }
   591  
   592  var errCAABrokenDNSClient = errors.New("dnsClient is broken")
   593  
   594  // caaBrokenDNS implements the `dns.DNSClient` interface, but always returns
   595  // errors.
   596  type caaBrokenDNS struct{}
   597  
   598  func (b caaBrokenDNS) LookupTXT(_ context.Context, hostname string) ([]string, bdns.ResolverAddrs, error) {
   599  	return nil, bdns.ResolverAddrs{"caaBrokenDNS"}, errCAABrokenDNSClient
   600  }
   601  
   602  func (b caaBrokenDNS) LookupHost(_ context.Context, hostname string) ([]netip.Addr, bdns.ResolverAddrs, error) {
   603  	return nil, bdns.ResolverAddrs{"caaBrokenDNS"}, errCAABrokenDNSClient
   604  }
   605  
   606  func (b caaBrokenDNS) LookupCAA(_ context.Context, domain string) ([]*dns.CAA, string, bdns.ResolverAddrs, error) {
   607  	return nil, "", bdns.ResolverAddrs{"caaBrokenDNS"}, errCAABrokenDNSClient
   608  }
   609  
   610  // caaHijackedDNS implements the `dns.DNSClient` interface with a set of useful
   611  // test answers for CAA queries. It returns alternate CAA records than what
   612  // caaMockDNS returns simulating either a BGP hijack or DNS records that have
   613  // changed while queries were inflight.
   614  type caaHijackedDNS struct{}
   615  
   616  func (h caaHijackedDNS) LookupTXT(_ context.Context, hostname string) ([]string, bdns.ResolverAddrs, error) {
   617  	return nil, bdns.ResolverAddrs{"caaHijackedDNS"}, nil
   618  }
   619  
   620  func (h caaHijackedDNS) LookupHost(_ context.Context, hostname string) ([]netip.Addr, bdns.ResolverAddrs, error) {
   621  	return []netip.Addr{netip.MustParseAddr("127.0.0.1")}, bdns.ResolverAddrs{"caaHijackedDNS"}, nil
   622  }
   623  func (h caaHijackedDNS) LookupCAA(_ context.Context, domain string) ([]*dns.CAA, string, bdns.ResolverAddrs, error) {
   624  	// These records are altered from their caaMockDNS counterparts. Use this to
   625  	// tickle remoteValidationFailures.
   626  	var results []*dns.CAA
   627  	var record dns.CAA
   628  	switch strings.TrimRight(domain, ".") {
   629  	case "present.com", "present.servfail.com":
   630  		record.Tag = "issue"
   631  		record.Value = "other-ca.com"
   632  		results = append(results, &record)
   633  	case "present-dns-only.com":
   634  		return results, "", bdns.ResolverAddrs{"caaHijackedDNS"}, fmt.Errorf("SERVFAIL")
   635  	case "satisfiable-wildcard.com":
   636  		record.Tag = "issuewild"
   637  		record.Value = ";"
   638  		results = append(results, &record)
   639  		secondRecord := record
   640  		secondRecord.Tag = "issue"
   641  		secondRecord.Value = ";"
   642  		results = append(results, &secondRecord)
   643  	}
   644  	var response string
   645  	if len(results) > 0 {
   646  		response = "foo"
   647  	}
   648  	return results, response, bdns.ResolverAddrs{"caaHijackedDNS"}, nil
   649  }
   650  
   651  // parseValidationLogEvent extracts ... from JSON={ ... } in a ValidateChallenge
   652  // audit log and returns it as a validationLogEvent struct.
   653  func parseValidationLogEvent(t *testing.T, log []string) validationLogEvent {
   654  	re := regexp.MustCompile(`JSON=\{.*\}`)
   655  	var audit validationLogEvent
   656  	for _, line := range log {
   657  		match := re.FindString(line)
   658  		if match != "" {
   659  			jsonStr := match[len(`JSON=`):]
   660  			if err := json.Unmarshal([]byte(jsonStr), &audit); err != nil {
   661  				t.Fatalf("Failed to parse JSON: %v", err)
   662  			}
   663  			return audit
   664  		}
   665  	}
   666  	t.Fatal("JSON not found in log")
   667  	return audit
   668  }
   669  
   670  func TestMultiCAARechecking(t *testing.T) {
   671  	// The remote differential log order is non-deterministic, so let's use
   672  	// the same UA for all applicable RVAs.
   673  	const (
   674  		localUA    = "local"
   675  		remoteUA   = "remote"
   676  		brokenUA   = "broken"
   677  		hijackedUA = "hijacked"
   678  	)
   679  
   680  	testCases := []struct {
   681  		name                     string
   682  		ident                    identifier.ACMEIdentifier
   683  		remoteVAs                []remoteConf
   684  		expectedProbSubstring    string
   685  		expectedProbType         probs.ProblemType
   686  		expectedDiffLogSubstring string
   687  		expectedSummary          *mpicSummary
   688  		expectedLabels           prometheus.Labels
   689  		localDNSClient           bdns.Client
   690  	}{
   691  		{
   692  			name:           "all VAs functional, no CAA records",
   693  			ident:          identifier.NewDNS("present-dns-only.com"),
   694  			localDNSClient: caaMockDNS{},
   695  			remoteVAs: []remoteConf{
   696  				{ua: remoteUA, rir: arin},
   697  				{ua: remoteUA, rir: ripe},
   698  				{ua: remoteUA, rir: apnic},
   699  			},
   700  			expectedLabels: prometheus.Labels{
   701  				"operation":      opCAA,
   702  				"perspective":    allPerspectives,
   703  				"challenge_type": string(core.ChallengeTypeDNS01),
   704  				"problem_type":   "",
   705  				"result":         pass,
   706  			},
   707  		},
   708  		{
   709  			name:                  "broken localVA, RVAs functional, no CAA records",
   710  			ident:                 identifier.NewDNS("present-dns-only.com"),
   711  			localDNSClient:        caaBrokenDNS{},
   712  			expectedProbSubstring: "While processing CAA for present-dns-only.com: dnsClient is broken",
   713  			expectedProbType:      probs.DNSProblem,
   714  			remoteVAs: []remoteConf{
   715  				{ua: remoteUA, rir: arin},
   716  				{ua: remoteUA, rir: ripe},
   717  				{ua: remoteUA, rir: apnic},
   718  			},
   719  			expectedLabels: prometheus.Labels{
   720  				"operation":      opCAA,
   721  				"perspective":    allPerspectives,
   722  				"challenge_type": string(core.ChallengeTypeDNS01),
   723  				"problem_type":   string(probs.DNSProblem),
   724  				"result":         fail,
   725  			},
   726  		},
   727  		{
   728  			name:                     "functional localVA, 1 broken RVA, no CAA records",
   729  			ident:                    identifier.NewDNS("present-dns-only.com"),
   730  			localDNSClient:           caaMockDNS{},
   731  			expectedDiffLogSubstring: `"RemoteSuccesses":2,"RemoteFailures":1`,
   732  			expectedSummary: &mpicSummary{
   733  				Passed:       []string{"dc-1-RIPE", "dc-2-APNIC"},
   734  				Failed:       []string{"dc-0-ARIN"},
   735  				PassedRIRs:   []string{ripe, apnic},
   736  				QuorumResult: "2/3",
   737  			},
   738  			remoteVAs: []remoteConf{
   739  				{ua: brokenUA, rir: arin, dns: caaBrokenDNS{}},
   740  				{ua: remoteUA, rir: ripe},
   741  				{ua: remoteUA, rir: apnic},
   742  			},
   743  			expectedLabels: prometheus.Labels{
   744  				"operation":      opCAA,
   745  				"perspective":    allPerspectives,
   746  				"challenge_type": string(core.ChallengeTypeDNS01),
   747  				"problem_type":   "",
   748  				"result":         pass,
   749  			},
   750  		},
   751  		{
   752  			name:                     "functional localVA, 2 broken RVA, no CAA records",
   753  			ident:                    identifier.NewDNS("present-dns-only.com"),
   754  			expectedProbSubstring:    "During secondary validation: While processing CAA",
   755  			expectedProbType:         probs.DNSProblem,
   756  			expectedDiffLogSubstring: `"RemoteSuccesses":1,"RemoteFailures":2`,
   757  			expectedSummary: &mpicSummary{
   758  				Passed:       []string{"dc-2-APNIC"},
   759  				Failed:       []string{"dc-0-ARIN", "dc-1-RIPE"},
   760  				PassedRIRs:   []string{apnic},
   761  				QuorumResult: "1/3",
   762  			},
   763  			localDNSClient: caaMockDNS{},
   764  			remoteVAs: []remoteConf{
   765  				{ua: brokenUA, rir: arin, dns: caaBrokenDNS{}},
   766  				{ua: brokenUA, rir: ripe, dns: caaBrokenDNS{}},
   767  				{ua: remoteUA, rir: apnic},
   768  			},
   769  			expectedLabels: prometheus.Labels{
   770  				"operation":      opCAA,
   771  				"perspective":    allPerspectives,
   772  				"challenge_type": string(core.ChallengeTypeDNS01),
   773  				"problem_type":   string(probs.DNSProblem),
   774  				"result":         fail,
   775  			},
   776  		},
   777  		{
   778  			name:                     "functional localVA, all broken RVAs, no CAA records",
   779  			ident:                    identifier.NewDNS("present-dns-only.com"),
   780  			expectedProbSubstring:    "During secondary validation: While processing CAA",
   781  			expectedProbType:         probs.DNSProblem,
   782  			expectedDiffLogSubstring: `"RemoteSuccesses":0,"RemoteFailures":3`,
   783  			expectedSummary: &mpicSummary{
   784  				Passed:       []string{},
   785  				Failed:       []string{"dc-0-ARIN", "dc-1-RIPE", "dc-2-APNIC"},
   786  				PassedRIRs:   []string{},
   787  				QuorumResult: "0/3",
   788  			},
   789  			localDNSClient: caaMockDNS{},
   790  			remoteVAs: []remoteConf{
   791  				{ua: brokenUA, rir: arin, dns: caaBrokenDNS{}},
   792  				{ua: brokenUA, rir: ripe, dns: caaBrokenDNS{}},
   793  				{ua: brokenUA, rir: apnic, dns: caaBrokenDNS{}},
   794  			},
   795  			expectedLabels: prometheus.Labels{
   796  				"operation":      opCAA,
   797  				"perspective":    allPerspectives,
   798  				"challenge_type": string(core.ChallengeTypeDNS01),
   799  				"problem_type":   string(probs.DNSProblem),
   800  				"result":         fail,
   801  			},
   802  		},
   803  		{
   804  			name:           "all VAs functional, CAA issue type present",
   805  			ident:          identifier.NewDNS("present.com"),
   806  			localDNSClient: caaMockDNS{},
   807  			remoteVAs: []remoteConf{
   808  				{ua: remoteUA, rir: arin},
   809  				{ua: remoteUA, rir: ripe},
   810  				{ua: remoteUA, rir: apnic},
   811  			},
   812  			expectedLabels: prometheus.Labels{
   813  				"operation":      opCAA,
   814  				"perspective":    allPerspectives,
   815  				"challenge_type": string(core.ChallengeTypeDNS01),
   816  				"problem_type":   "",
   817  				"result":         pass,
   818  			},
   819  		},
   820  		{
   821  			name:                     "functional localVA, 1 broken RVA, CAA issue type present",
   822  			ident:                    identifier.NewDNS("present.com"),
   823  			expectedDiffLogSubstring: `"RemoteSuccesses":2,"RemoteFailures":1`,
   824  			expectedSummary: &mpicSummary{
   825  				Passed:       []string{"dc-1-RIPE", "dc-2-APNIC"},
   826  				Failed:       []string{"dc-0-ARIN"},
   827  				PassedRIRs:   []string{ripe, apnic},
   828  				QuorumResult: "2/3",
   829  			},
   830  			localDNSClient: caaMockDNS{},
   831  			remoteVAs: []remoteConf{
   832  				{ua: brokenUA, rir: arin, dns: caaBrokenDNS{}},
   833  				{ua: remoteUA, rir: ripe},
   834  				{ua: remoteUA, rir: apnic},
   835  			},
   836  			expectedLabels: prometheus.Labels{
   837  				"operation":      opCAA,
   838  				"perspective":    allPerspectives,
   839  				"challenge_type": string(core.ChallengeTypeDNS01),
   840  				"problem_type":   "",
   841  				"result":         pass,
   842  			},
   843  		},
   844  		{
   845  			name:                     "functional localVA, 2 broken RVA, CAA issue type present",
   846  			ident:                    identifier.NewDNS("present.com"),
   847  			expectedProbSubstring:    "During secondary validation: While processing CAA",
   848  			expectedProbType:         probs.DNSProblem,
   849  			expectedDiffLogSubstring: `"RemoteSuccesses":1,"RemoteFailures":2`,
   850  			expectedSummary: &mpicSummary{
   851  				Passed:       []string{"dc-2-APNIC"},
   852  				Failed:       []string{"dc-0-ARIN", "dc-1-RIPE"},
   853  				PassedRIRs:   []string{apnic},
   854  				QuorumResult: "1/3",
   855  			},
   856  			localDNSClient: caaMockDNS{},
   857  			remoteVAs: []remoteConf{
   858  				{ua: brokenUA, rir: arin, dns: caaBrokenDNS{}},
   859  				{ua: brokenUA, rir: ripe, dns: caaBrokenDNS{}},
   860  				{ua: remoteUA, rir: apnic},
   861  			},
   862  			expectedLabels: prometheus.Labels{
   863  				"operation":      opCAA,
   864  				"perspective":    allPerspectives,
   865  				"challenge_type": string(core.ChallengeTypeDNS01),
   866  				"problem_type":   string(probs.DNSProblem),
   867  				"result":         fail,
   868  			},
   869  		},
   870  		{
   871  			name:                     "functional localVA, all broken RVAs, CAA issue type present",
   872  			ident:                    identifier.NewDNS("present.com"),
   873  			expectedProbSubstring:    "During secondary validation: While processing CAA",
   874  			expectedProbType:         probs.DNSProblem,
   875  			expectedDiffLogSubstring: `"RemoteSuccesses":0,"RemoteFailures":3`,
   876  			expectedSummary: &mpicSummary{
   877  				Passed:       []string{},
   878  				Failed:       []string{"dc-0-ARIN", "dc-1-RIPE", "dc-2-APNIC"},
   879  				PassedRIRs:   []string{},
   880  				QuorumResult: "0/3",
   881  			},
   882  			localDNSClient: caaMockDNS{},
   883  			remoteVAs: []remoteConf{
   884  				{ua: brokenUA, rir: arin, dns: caaBrokenDNS{}},
   885  				{ua: brokenUA, rir: ripe, dns: caaBrokenDNS{}},
   886  				{ua: brokenUA, rir: apnic, dns: caaBrokenDNS{}},
   887  			},
   888  			expectedLabels: prometheus.Labels{
   889  				"operation":      opCAA,
   890  				"perspective":    allPerspectives,
   891  				"challenge_type": string(core.ChallengeTypeDNS01),
   892  				"problem_type":   string(probs.DNSProblem),
   893  				"result":         fail,
   894  			},
   895  		},
   896  		{
   897  			// The localVA returns early with a problem before kicking off the
   898  			// remote checks.
   899  			name:                  "all VAs functional, CAA issue type forbids issuance",
   900  			ident:                 identifier.NewDNS("unsatisfiable.com"),
   901  			expectedProbSubstring: "CAA record for unsatisfiable.com prevents issuance",
   902  			expectedProbType:      probs.CAAProblem,
   903  			localDNSClient:        caaMockDNS{},
   904  			remoteVAs: []remoteConf{
   905  				{ua: remoteUA, rir: arin},
   906  				{ua: remoteUA, rir: ripe},
   907  				{ua: remoteUA, rir: apnic},
   908  			},
   909  		},
   910  		{
   911  			name:                     "1 hijacked RVA, CAA issue type present",
   912  			ident:                    identifier.NewDNS("present.com"),
   913  			expectedDiffLogSubstring: `"RemoteSuccesses":2,"RemoteFailures":1`,
   914  			expectedSummary: &mpicSummary{
   915  				Passed:       []string{"dc-1-RIPE", "dc-2-APNIC"},
   916  				Failed:       []string{"dc-0-ARIN"},
   917  				PassedRIRs:   []string{ripe, apnic},
   918  				QuorumResult: "2/3",
   919  			},
   920  			localDNSClient: caaMockDNS{},
   921  			remoteVAs: []remoteConf{
   922  				{ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}},
   923  				{ua: remoteUA, rir: ripe},
   924  				{ua: remoteUA, rir: apnic},
   925  			},
   926  		},
   927  		{
   928  			name:                     "2 hijacked RVAs, CAA issue type present",
   929  			ident:                    identifier.NewDNS("present.com"),
   930  			expectedProbSubstring:    "During secondary validation: While processing CAA",
   931  			expectedProbType:         probs.CAAProblem,
   932  			expectedDiffLogSubstring: `"RemoteSuccesses":1,"RemoteFailures":2`,
   933  			expectedSummary: &mpicSummary{
   934  				Passed:       []string{"dc-2-APNIC"},
   935  				Failed:       []string{"dc-0-ARIN", "dc-1-RIPE"},
   936  				PassedRIRs:   []string{apnic},
   937  				QuorumResult: "1/3",
   938  			},
   939  			localDNSClient: caaMockDNS{},
   940  			remoteVAs: []remoteConf{
   941  				{ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}},
   942  				{ua: hijackedUA, rir: ripe, dns: caaHijackedDNS{}},
   943  				{ua: remoteUA, rir: apnic},
   944  			},
   945  		},
   946  		{
   947  			name:                     "3 hijacked RVAs, CAA issue type present",
   948  			ident:                    identifier.NewDNS("present.com"),
   949  			expectedProbSubstring:    "During secondary validation: While processing CAA",
   950  			expectedProbType:         probs.CAAProblem,
   951  			expectedDiffLogSubstring: `"RemoteSuccesses":0,"RemoteFailures":3`,
   952  			expectedSummary: &mpicSummary{
   953  				Passed:       []string{},
   954  				Failed:       []string{"dc-0-ARIN", "dc-1-RIPE", "dc-2-APNIC"},
   955  				PassedRIRs:   []string{},
   956  				QuorumResult: "0/3",
   957  			},
   958  			localDNSClient: caaMockDNS{},
   959  			remoteVAs: []remoteConf{
   960  				{ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}},
   961  				{ua: hijackedUA, rir: ripe, dns: caaHijackedDNS{}},
   962  				{ua: hijackedUA, rir: apnic, dns: caaHijackedDNS{}},
   963  			},
   964  		},
   965  		{
   966  			name:                     "1 hijacked RVA, CAA issuewild type present",
   967  			ident:                    identifier.NewDNS("satisfiable-wildcard.com"),
   968  			expectedDiffLogSubstring: `"RemoteSuccesses":2,"RemoteFailures":1`,
   969  			expectedSummary: &mpicSummary{
   970  				Passed:       []string{"dc-1-RIPE", "dc-2-APNIC"},
   971  				Failed:       []string{"dc-0-ARIN"},
   972  				PassedRIRs:   []string{ripe, apnic},
   973  				QuorumResult: "2/3",
   974  			},
   975  			localDNSClient: caaMockDNS{},
   976  			remoteVAs: []remoteConf{
   977  				{ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}},
   978  				{ua: remoteUA, rir: ripe},
   979  				{ua: remoteUA, rir: apnic},
   980  			},
   981  		},
   982  		{
   983  			name:                     "2 hijacked RVAs, CAA issuewild type present",
   984  			ident:                    identifier.NewDNS("satisfiable-wildcard.com"),
   985  			expectedProbSubstring:    "During secondary validation: While processing CAA",
   986  			expectedProbType:         probs.CAAProblem,
   987  			expectedDiffLogSubstring: `"RemoteSuccesses":1,"RemoteFailures":2`,
   988  			expectedSummary: &mpicSummary{
   989  				Passed:       []string{"dc-2-APNIC"},
   990  				Failed:       []string{"dc-0-ARIN", "dc-1-RIPE"},
   991  				PassedRIRs:   []string{apnic},
   992  				QuorumResult: "1/3",
   993  			},
   994  			localDNSClient: caaMockDNS{},
   995  			remoteVAs: []remoteConf{
   996  				{ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}},
   997  				{ua: hijackedUA, rir: ripe, dns: caaHijackedDNS{}},
   998  				{ua: remoteUA, rir: apnic},
   999  			},
  1000  		},
  1001  		{
  1002  			name:                     "3 hijacked RVAs, CAA issuewild type present",
  1003  			ident:                    identifier.NewDNS("satisfiable-wildcard.com"),
  1004  			expectedProbSubstring:    "During secondary validation: While processing CAA",
  1005  			expectedProbType:         probs.CAAProblem,
  1006  			expectedDiffLogSubstring: `"RemoteSuccesses":0,"RemoteFailures":3`,
  1007  			expectedSummary: &mpicSummary{
  1008  				Passed:       []string{},
  1009  				Failed:       []string{"dc-0-ARIN", "dc-1-RIPE", "dc-2-APNIC"},
  1010  				PassedRIRs:   []string{},
  1011  				QuorumResult: "0/3",
  1012  			},
  1013  			localDNSClient: caaMockDNS{},
  1014  			remoteVAs: []remoteConf{
  1015  				{ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}},
  1016  				{ua: hijackedUA, rir: ripe, dns: caaHijackedDNS{}},
  1017  				{ua: hijackedUA, rir: apnic, dns: caaHijackedDNS{}},
  1018  			},
  1019  		},
  1020  		{
  1021  			name:                     "1 hijacked RVA, CAA issuewild type present, 1 failure allowed",
  1022  			ident:                    identifier.NewDNS("satisfiable-wildcard.com"),
  1023  			expectedDiffLogSubstring: `"RemoteSuccesses":2,"RemoteFailures":1`,
  1024  			expectedSummary: &mpicSummary{
  1025  				Passed:       []string{"dc-1-RIPE", "dc-2-APNIC"},
  1026  				Failed:       []string{"dc-0-ARIN"},
  1027  				PassedRIRs:   []string{ripe, apnic},
  1028  				QuorumResult: "2/3",
  1029  			},
  1030  			localDNSClient: caaMockDNS{},
  1031  			remoteVAs: []remoteConf{
  1032  				{ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}},
  1033  				{ua: remoteUA, rir: ripe},
  1034  				{ua: remoteUA, rir: apnic},
  1035  			},
  1036  		},
  1037  		{
  1038  			name:                     "2 hijacked RVAs, CAA issuewild type present, 1 failure allowed",
  1039  			ident:                    identifier.NewDNS("satisfiable-wildcard.com"),
  1040  			expectedProbSubstring:    "During secondary validation: While processing CAA",
  1041  			expectedProbType:         probs.CAAProblem,
  1042  			expectedDiffLogSubstring: `"RemoteSuccesses":1,"RemoteFailures":2`,
  1043  			expectedSummary: &mpicSummary{
  1044  				Passed:       []string{"dc-2-APNIC"},
  1045  				Failed:       []string{"dc-0-ARIN", "dc-1-RIPE"},
  1046  				PassedRIRs:   []string{apnic},
  1047  				QuorumResult: "1/3",
  1048  			},
  1049  			localDNSClient: caaMockDNS{},
  1050  			remoteVAs: []remoteConf{
  1051  				{ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}},
  1052  				{ua: hijackedUA, rir: ripe, dns: caaHijackedDNS{}},
  1053  				{ua: remoteUA, rir: apnic},
  1054  			},
  1055  		},
  1056  		{
  1057  			name:                     "3 hijacked RVAs, CAA issuewild type present, 1 failure allowed",
  1058  			ident:                    identifier.NewDNS("satisfiable-wildcard.com"),
  1059  			expectedProbSubstring:    "During secondary validation: While processing CAA",
  1060  			expectedProbType:         probs.CAAProblem,
  1061  			expectedDiffLogSubstring: `"RemoteSuccesses":0,"RemoteFailures":3`,
  1062  			expectedSummary: &mpicSummary{
  1063  				Passed:       []string{},
  1064  				Failed:       []string{"dc-0-ARIN", "dc-1-RIPE", "dc-2-APNIC"},
  1065  				PassedRIRs:   []string{},
  1066  				QuorumResult: "0/3",
  1067  			},
  1068  			localDNSClient: caaMockDNS{},
  1069  			remoteVAs: []remoteConf{
  1070  				{ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}},
  1071  				{ua: hijackedUA, rir: ripe, dns: caaHijackedDNS{}},
  1072  				{ua: hijackedUA, rir: apnic, dns: caaHijackedDNS{}},
  1073  			},
  1074  		},
  1075  	}
  1076  
  1077  	for _, tc := range testCases {
  1078  		t.Run(tc.name, func(t *testing.T) {
  1079  			va, mockLog := setupWithRemotes(nil, localUA, tc.remoteVAs, tc.localDNSClient)
  1080  			defer mockLog.Clear()
  1081  
  1082  			features.Set(features.Config{
  1083  				EnforceMultiCAA: true,
  1084  			})
  1085  			defer features.Reset()
  1086  
  1087  			isValidRes, err := va.DoCAA(context.TODO(), &vapb.IsCAAValidRequest{
  1088  				Identifier:       tc.ident.ToProto(),
  1089  				ValidationMethod: string(core.ChallengeTypeDNS01),
  1090  				AccountURIID:     1,
  1091  			})
  1092  			test.AssertNotError(t, err, "Should not have errored, but did")
  1093  
  1094  			if tc.expectedProbSubstring != "" {
  1095  				test.AssertNotNil(t, isValidRes.Problem, "IsCAAValidRequest returned nil problem, but should not have")
  1096  				test.AssertContains(t, isValidRes.Problem.Detail, tc.expectedProbSubstring)
  1097  			} else if isValidRes.Problem != nil {
  1098  				test.AssertBoxedNil(t, isValidRes.Problem, "IsCAAValidRequest returned a problem, but should not have")
  1099  			}
  1100  
  1101  			if tc.expectedProbType != "" {
  1102  				test.AssertNotNil(t, isValidRes.Problem, "IsCAAValidRequest returned nil problem, but should not have")
  1103  				test.AssertEquals(t, string(tc.expectedProbType), isValidRes.Problem.ProblemType)
  1104  			}
  1105  
  1106  			if tc.expectedSummary != nil {
  1107  				gotAuditLog := parseValidationLogEvent(t, mockLog.GetAllMatching("JSON=.*"))
  1108  				slices.Sort(tc.expectedSummary.Passed)
  1109  				slices.Sort(tc.expectedSummary.Failed)
  1110  				slices.Sort(tc.expectedSummary.PassedRIRs)
  1111  				test.AssertDeepEquals(t, gotAuditLog.Summary, tc.expectedSummary)
  1112  			}
  1113  
  1114  			gotAnyRemoteFailures := mockLog.GetAllMatching("CAA check failed due to remote failures:")
  1115  			if len(gotAnyRemoteFailures) >= 1 {
  1116  				// The primary VA only emits this line once.
  1117  				test.AssertEquals(t, len(gotAnyRemoteFailures), 1)
  1118  			} else {
  1119  				test.AssertEquals(t, len(gotAnyRemoteFailures), 0)
  1120  			}
  1121  
  1122  			if tc.expectedLabels != nil {
  1123  				test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, tc.expectedLabels, 1)
  1124  			}
  1125  
  1126  		})
  1127  	}
  1128  }
  1129  
  1130  func TestCAAFailure(t *testing.T) {
  1131  	hs := httpSrv(t, expectedToken, false)
  1132  	defer hs.Close()
  1133  
  1134  	va, _ := setup(hs, "", nil, caaMockDNS{})
  1135  
  1136  	err := va.checkCAA(ctx, identifier.NewDNS("reserved.com"), &caaParams{1, core.ChallengeTypeHTTP01})
  1137  	if err == nil {
  1138  		t.Fatalf("Expected CAA rejection for reserved.com, got success")
  1139  	}
  1140  	test.AssertErrorIs(t, err, berrors.CAA)
  1141  
  1142  	err = va.checkCAA(ctx, identifier.NewDNS("example.gonetld"), &caaParams{1, core.ChallengeTypeHTTP01})
  1143  	if err == nil {
  1144  		t.Fatalf("Expected CAA rejection for gonetld, got success")
  1145  	}
  1146  	prob := detailedError(err)
  1147  	test.AssertEquals(t, prob.Type, probs.DNSProblem)
  1148  	test.AssertContains(t, prob.String(), "NXDOMAIN")
  1149  }
  1150  
  1151  func TestFilterCAA(t *testing.T) {
  1152  	testCases := []struct {
  1153  		name              string
  1154  		input             []*dns.CAA
  1155  		expectedIssueVals []string
  1156  		expectedWildVals  []string
  1157  		expectedCU        bool
  1158  	}{
  1159  		{
  1160  			name: "recognized non-critical",
  1161  			input: []*dns.CAA{
  1162  				{Tag: "issue", Value: "a"},
  1163  				{Tag: "issuewild", Value: "b"},
  1164  				{Tag: "iodef", Value: "c"},
  1165  				{Tag: "issuemail", Value: "c"},
  1166  				{Tag: "issuevmc", Value: "c"},
  1167  			},
  1168  			expectedIssueVals: []string{"a"},
  1169  			expectedWildVals:  []string{"b"},
  1170  		},
  1171  		{
  1172  			name: "recognized critical",
  1173  			input: []*dns.CAA{
  1174  				{Tag: "issue", Value: "a", Flag: 128},
  1175  				{Tag: "issuewild", Value: "b", Flag: 128},
  1176  				{Tag: "iodef", Value: "c", Flag: 128},
  1177  				{Tag: "issuemail", Value: "c", Flag: 128},
  1178  				{Tag: "issuevmc", Value: "c", Flag: 128},
  1179  			},
  1180  			expectedIssueVals: []string{"a"},
  1181  			expectedWildVals:  []string{"b"},
  1182  		},
  1183  		{
  1184  			name: "unrecognized non-critical",
  1185  			input: []*dns.CAA{
  1186  				{Tag: "unknown", Flag: 2},
  1187  			},
  1188  		},
  1189  		{
  1190  			name: "unrecognized critical",
  1191  			input: []*dns.CAA{
  1192  				{Tag: "unknown", Flag: 128},
  1193  			},
  1194  			expectedCU: true,
  1195  		},
  1196  		{
  1197  			name: "unrecognized improper critical",
  1198  			input: []*dns.CAA{
  1199  				{Tag: "unknown", Flag: 1},
  1200  			},
  1201  			expectedCU: true,
  1202  		},
  1203  		{
  1204  			name: "unrecognized very improper critical",
  1205  			input: []*dns.CAA{
  1206  				{Tag: "unknown", Flag: 9},
  1207  			},
  1208  			expectedCU: true,
  1209  		},
  1210  	}
  1211  
  1212  	for _, tc := range testCases {
  1213  		t.Run(tc.name, func(t *testing.T) {
  1214  			issue, wild, cu := filterCAA(tc.input)
  1215  			for _, tag := range issue {
  1216  				test.AssertSliceContains(t, tc.expectedIssueVals, tag.Value)
  1217  			}
  1218  			for _, tag := range wild {
  1219  				test.AssertSliceContains(t, tc.expectedWildVals, tag.Value)
  1220  			}
  1221  			test.AssertEquals(t, tc.expectedCU, cu)
  1222  		})
  1223  	}
  1224  }
  1225  
  1226  func TestSelectCAA(t *testing.T) {
  1227  	expected := dns.CAA{Tag: "issue", Value: "foo"}
  1228  
  1229  	// An empty slice of caaResults should return nil, nil
  1230  	r := []caaResult{}
  1231  	s, err := selectCAA(r)
  1232  	test.Assert(t, s == nil, "set is not nil")
  1233  	test.AssertNotError(t, err, "error is not nil")
  1234  
  1235  	// A slice of empty caaResults should return nil, "", nil
  1236  	r = []caaResult{
  1237  		{"", false, nil, nil, false, "", nil, nil},
  1238  		{"", false, nil, nil, false, "", nil, nil},
  1239  		{"", false, nil, nil, false, "", nil, nil},
  1240  	}
  1241  	s, err = selectCAA(r)
  1242  	test.Assert(t, s == nil, "set is not nil")
  1243  	test.AssertNotError(t, err, "error is not nil")
  1244  
  1245  	// A slice of caaResults containing an error followed by a CAA
  1246  	// record should return the error
  1247  	r = []caaResult{
  1248  		{"foo.com", false, nil, nil, false, "", nil, errors.New("oops")},
  1249  		{"com", true, []*dns.CAA{&expected}, nil, false, "foo", nil, nil},
  1250  	}
  1251  	s, err = selectCAA(r)
  1252  	test.Assert(t, s == nil, "set is not nil")
  1253  	test.AssertError(t, err, "error is nil")
  1254  	test.AssertEquals(t, err.Error(), "oops")
  1255  
  1256  	//  A slice of caaResults containing a good record that precedes an
  1257  	//  error, should return that good record, not the error
  1258  	r = []caaResult{
  1259  		{"foo.com", true, []*dns.CAA{&expected}, nil, false, "foo", nil, nil},
  1260  		{"com", false, nil, nil, false, "", nil, errors.New("")},
  1261  	}
  1262  	s, err = selectCAA(r)
  1263  	test.AssertEquals(t, len(s.issue), 1)
  1264  	test.Assert(t, s.issue[0] == &expected, "Incorrect record returned")
  1265  	test.AssertEquals(t, s.dig, "foo")
  1266  	test.Assert(t, err == nil, "error is not nil")
  1267  
  1268  	// A slice of caaResults containing multiple CAA records should
  1269  	// return the first non-empty CAA record
  1270  	r = []caaResult{
  1271  		{"bar.foo.com", false, []*dns.CAA{}, []*dns.CAA{}, false, "", nil, nil},
  1272  		{"foo.com", true, []*dns.CAA{&expected}, nil, false, "foo", nil, nil},
  1273  		{"com", true, []*dns.CAA{&expected}, nil, false, "bar", nil, nil},
  1274  	}
  1275  	s, err = selectCAA(r)
  1276  	test.AssertEquals(t, len(s.issue), 1)
  1277  	test.Assert(t, s.issue[0] == &expected, "Incorrect record returned")
  1278  	test.AssertEquals(t, s.dig, "foo")
  1279  	test.AssertNotError(t, err, "expect nil error")
  1280  }
  1281  
  1282  func TestAccountURIMatches(t *testing.T) {
  1283  	t.Parallel()
  1284  	tests := []struct {
  1285  		name     string
  1286  		params   []caaParameter
  1287  		prefixes []string
  1288  		id       int64
  1289  		want     bool
  1290  	}{
  1291  		{
  1292  			name:   "empty accounturi",
  1293  			params: nil,
  1294  			prefixes: []string{
  1295  				"https://acme-v01.api.letsencrypt.org/acme/reg/",
  1296  			},
  1297  			id:   123456,
  1298  			want: true,
  1299  		},
  1300  		{
  1301  			name:   "no accounturi in rr, but other parameters exist",
  1302  			params: []caaParameter{{tag: "validationmethods", val: "tls-alpn-01"}},
  1303  			prefixes: []string{
  1304  				"https://acme-v02.api.letsencrypt.org/acme/reg/",
  1305  			},
  1306  			id:   123456,
  1307  			want: true,
  1308  		},
  1309  		{
  1310  			name:   "non-uri accounturi",
  1311  			params: []caaParameter{{tag: "accounturi", val: "\\invalid 😎/123456"}},
  1312  			prefixes: []string{
  1313  				"\\invalid 😎",
  1314  			},
  1315  			id:   123456,
  1316  			want: false,
  1317  		},
  1318  		{
  1319  			name:   "simple match",
  1320  			params: []caaParameter{{tag: "accounturi", val: "https://acme-v01.api.letsencrypt.org/acme/reg/123456"}},
  1321  			prefixes: []string{
  1322  				"https://acme-v01.api.letsencrypt.org/acme/reg/",
  1323  			},
  1324  			id:   123456,
  1325  			want: true,
  1326  		},
  1327  		{
  1328  			name:   "simple match, but has a friend",
  1329  			params: []caaParameter{{tag: "validationmethods", val: "dns-01"}, {tag: "accounturi", val: "https://acme-v01.api.letsencrypt.org/acme/reg/123456"}},
  1330  			prefixes: []string{
  1331  				"https://acme-v01.api.letsencrypt.org/acme/reg/",
  1332  			},
  1333  			id:   123456,
  1334  			want: true,
  1335  		},
  1336  		{
  1337  			name:   "accountid mismatch",
  1338  			params: []caaParameter{{tag: "accounturi", val: "https://acme-v01.api.letsencrypt.org/acme/reg/123456"}},
  1339  			prefixes: []string{
  1340  				"https://acme-v01.api.letsencrypt.org/acme/reg/",
  1341  			},
  1342  			id:   123457,
  1343  			want: false,
  1344  		},
  1345  		{
  1346  			name:   "single parameter, no value",
  1347  			params: []caaParameter{{tag: "accounturi", val: ""}},
  1348  			prefixes: []string{
  1349  				"https://acme-v02.api.letsencrypt.org/acme/reg/",
  1350  			},
  1351  			id:   123456,
  1352  			want: false,
  1353  		},
  1354  		{
  1355  			name:   "multiple parameters, each with no value",
  1356  			params: []caaParameter{{tag: "accounturi", val: ""}, {tag: "accounturi", val: ""}},
  1357  			prefixes: []string{
  1358  				"https://acme-v02.api.letsencrypt.org/acme/reg/",
  1359  			},
  1360  			id:   123456,
  1361  			want: false,
  1362  		},
  1363  		{
  1364  			name:   "multiple parameters, one with no value",
  1365  			params: []caaParameter{{tag: "accounturi", val: ""}, {tag: "accounturi", val: "https://acme-v02.api.letsencrypt.org/acme/reg/123456"}},
  1366  			prefixes: []string{
  1367  				"https://acme-v02.api.letsencrypt.org/acme/reg/",
  1368  			},
  1369  			id:   123456,
  1370  			want: false,
  1371  		},
  1372  		{
  1373  			name:   "multiple parameters, each with an identical value",
  1374  			params: []caaParameter{{tag: "accounturi", val: "https://acme-v02.api.letsencrypt.org/acme/reg/123456"}, {tag: "accounturi", val: "https://acme-v02.api.letsencrypt.org/acme/reg/123456"}},
  1375  			prefixes: []string{
  1376  				"https://acme-v02.api.letsencrypt.org/acme/reg/",
  1377  			},
  1378  			id:   123456,
  1379  			want: false,
  1380  		},
  1381  		{
  1382  			name:   "multiple parameters, each with a different value",
  1383  			params: []caaParameter{{tag: "accounturi", val: "https://acme-v02.api.letsencrypt.org/acme/reg/69"}, {tag: "accounturi", val: "https://acme-v02.api.letsencrypt.org/acme/reg/420"}},
  1384  			prefixes: []string{
  1385  				"https://acme-v02.api.letsencrypt.org/acme/reg/",
  1386  			},
  1387  			id:   69,
  1388  			want: false,
  1389  		},
  1390  		{
  1391  			name:   "multiple prefixes, match first",
  1392  			params: []caaParameter{{tag: "accounturi", val: "https://acme-staging.api.letsencrypt.org/acme/reg/123456"}},
  1393  			prefixes: []string{
  1394  				"https://acme-staging.api.letsencrypt.org/acme/reg/",
  1395  				"https://acme-staging-v02.api.letsencrypt.org/acme/acct/",
  1396  			},
  1397  			id:   123456,
  1398  			want: true,
  1399  		},
  1400  		{
  1401  			name:   "multiple prefixes, match second",
  1402  			params: []caaParameter{{tag: "accounturi", val: "https://acme-v02.api.letsencrypt.org/acme/acct/123456"}},
  1403  			prefixes: []string{
  1404  				"https://acme-v01.api.letsencrypt.org/acme/reg/",
  1405  				"https://acme-v02.api.letsencrypt.org/acme/acct/",
  1406  			},
  1407  			id:   123456,
  1408  			want: true,
  1409  		},
  1410  		{
  1411  			name:   "multiple prefixes, match none",
  1412  			params: []caaParameter{{tag: "accounturi", val: "https://acme-v02.api.letsencrypt.org/acme/acct/123456"}},
  1413  			prefixes: []string{
  1414  				"https://acme-v01.api.letsencrypt.org/acme/acct/",
  1415  				"https://acme-v03.api.letsencrypt.org/acme/acct/",
  1416  			},
  1417  			id:   123456,
  1418  			want: false,
  1419  		},
  1420  		{
  1421  			name:   "three prefixes",
  1422  			params: []caaParameter{{tag: "accounturi", val: "https://acme-v02.api.letsencrypt.org/acme/acct/123456"}},
  1423  			prefixes: []string{
  1424  				"https://acme-v01.api.letsencrypt.org/acme/reg/",
  1425  				"https://acme-v02.api.letsencrypt.org/acme/acct/",
  1426  				"https://acme-v03.api.letsencrypt.org/acme/acct/",
  1427  			},
  1428  			id:   123456,
  1429  			want: true,
  1430  		},
  1431  		{
  1432  			name:   "multiple prefixes, wrong accountid",
  1433  			params: []caaParameter{{tag: "accounturi", val: "https://acme-v02.api.letsencrypt.org/acme/acct/123456"}},
  1434  			prefixes: []string{
  1435  				"https://acme-v01.api.letsencrypt.org/acme/reg/",
  1436  				"https://acme-v02.api.letsencrypt.org/acme/acct/",
  1437  			},
  1438  			id:   654321,
  1439  			want: false,
  1440  		},
  1441  	}
  1442  
  1443  	for _, tc := range tests {
  1444  		t.Run(tc.name, func(t *testing.T) {
  1445  			t.Parallel()
  1446  			got := caaAccountURIMatches(tc.params, tc.prefixes, tc.id)
  1447  			test.AssertEquals(t, got, tc.want)
  1448  		})
  1449  	}
  1450  }
  1451  
  1452  func TestValidationMethodMatches(t *testing.T) {
  1453  	t.Parallel()
  1454  	tests := []struct {
  1455  		name   string
  1456  		params []caaParameter
  1457  		method core.AcmeChallenge
  1458  		want   bool
  1459  	}{
  1460  		{
  1461  			name:   "empty validationmethods",
  1462  			params: nil,
  1463  			method: core.ChallengeTypeHTTP01,
  1464  			want:   true,
  1465  		},
  1466  		{
  1467  			name:   "no validationmethods in rr, but other parameters exist", // validationmethods is not mandatory
  1468  			params: []caaParameter{{tag: "accounturi", val: "ph1LwuzHere"}},
  1469  			method: core.ChallengeTypeHTTP01,
  1470  			want:   true,
  1471  		},
  1472  		{
  1473  			name:   "no value",
  1474  			params: []caaParameter{{tag: "validationmethods", val: ""}}, // equivalent to forbidding issuance
  1475  			method: core.ChallengeTypeHTTP01,
  1476  			want:   false,
  1477  		},
  1478  		{
  1479  			name:   "only comma",
  1480  			params: []caaParameter{{tag: "validationmethods", val: ","}},
  1481  			method: core.ChallengeTypeHTTP01,
  1482  			want:   false,
  1483  		},
  1484  		{
  1485  			name:   "malformed method",
  1486  			params: []caaParameter{{tag: "validationmethods", val: "howdy !"}},
  1487  			method: core.ChallengeTypeHTTP01,
  1488  			want:   false,
  1489  		},
  1490  		{
  1491  			name:   "invalid method",
  1492  			params: []caaParameter{{tag: "validationmethods", val: "tls-sni-01"}},
  1493  			method: core.ChallengeTypeHTTP01,
  1494  			want:   false,
  1495  		},
  1496  		{
  1497  			name:   "simple match",
  1498  			params: []caaParameter{{tag: "validationmethods", val: "http-01"}},
  1499  			method: core.ChallengeTypeHTTP01,
  1500  			want:   true,
  1501  		},
  1502  		{
  1503  			name:   "simple match, but has a friend",
  1504  			params: []caaParameter{{tag: "accounturi", val: "https://example.org"}, {tag: "validationmethods", val: "http-01"}},
  1505  			method: core.ChallengeTypeHTTP01,
  1506  			want:   true,
  1507  		},
  1508  		{
  1509  			name:   "multiple validationmethods, each with no value",
  1510  			params: []caaParameter{{tag: "validationmethods", val: ""}, {tag: "validationmethods", val: ""}},
  1511  			method: core.ChallengeTypeHTTP01,
  1512  			want:   false,
  1513  		},
  1514  		{
  1515  			name:   "multiple validationmethods, one with no value",
  1516  			params: []caaParameter{{tag: "validationmethods", val: ""}, {tag: "validationmethods", val: "http-01"}},
  1517  			method: core.ChallengeTypeHTTP01,
  1518  			want:   false,
  1519  		},
  1520  		{
  1521  			name:   "multiple validationmethods, each with an identical value",
  1522  			params: []caaParameter{{tag: "validationmethods", val: "http-01"}, {tag: "validationmethods", val: "http-01"}},
  1523  			method: core.ChallengeTypeHTTP01,
  1524  			want:   false,
  1525  		},
  1526  		{
  1527  			name:   "multiple validationmethods, each with a different value",
  1528  			params: []caaParameter{{tag: "validationmethods", val: "http-01"}, {tag: "validationmethods", val: "dns-01"}},
  1529  			method: core.ChallengeTypeHTTP01,
  1530  			want:   false,
  1531  		},
  1532  		{
  1533  			name:   "simple mismatch",
  1534  			params: []caaParameter{{tag: "validationmethods", val: "dns-01"}},
  1535  			method: core.ChallengeTypeHTTP01,
  1536  			want:   false,
  1537  		},
  1538  		{
  1539  			name:   "multiple choices, match first",
  1540  			params: []caaParameter{{tag: "validationmethods", val: "http-01,dns-01"}},
  1541  			method: core.ChallengeTypeHTTP01,
  1542  			want:   true,
  1543  		},
  1544  		{
  1545  			name:   "multiple choices, match second",
  1546  			params: []caaParameter{{tag: "validationmethods", val: "http-01,dns-01"}},
  1547  			method: core.ChallengeTypeDNS01,
  1548  			want:   true,
  1549  		},
  1550  		{
  1551  			name:   "multiple choices, match none",
  1552  			params: []caaParameter{{tag: "validationmethods", val: "http-01,dns-01"}},
  1553  			method: core.ChallengeTypeTLSALPN01,
  1554  			want:   false,
  1555  		},
  1556  	}
  1557  
  1558  	for _, tc := range tests {
  1559  		t.Run(tc.name, func(t *testing.T) {
  1560  			t.Parallel()
  1561  			got := caaValidationMethodMatches(tc.params, tc.method)
  1562  			test.AssertEquals(t, got, tc.want)
  1563  		})
  1564  	}
  1565  }
  1566  
  1567  func TestExtractIssuerDomainAndParameters(t *testing.T) {
  1568  	t.Parallel()
  1569  	tests := []struct {
  1570  		name            string
  1571  		value           string
  1572  		wantDomain      string
  1573  		wantParameters  []caaParameter
  1574  		expectErrSubstr string
  1575  	}{
  1576  		{
  1577  			name:            "empty record is valid",
  1578  			value:           "",
  1579  			wantDomain:      "",
  1580  			wantParameters:  nil,
  1581  			expectErrSubstr: "",
  1582  		},
  1583  		{
  1584  			name:            "only semicolon is valid",
  1585  			value:           ";",
  1586  			wantDomain:      "",
  1587  			wantParameters:  nil,
  1588  			expectErrSubstr: "",
  1589  		},
  1590  		{
  1591  			name:            "only semicolon and whitespace is valid",
  1592  			value:           " ; ",
  1593  			wantDomain:      "",
  1594  			wantParameters:  nil,
  1595  			expectErrSubstr: "",
  1596  		},
  1597  		{
  1598  			name:            "only domain is valid",
  1599  			value:           "letsencrypt.org",
  1600  			wantDomain:      "letsencrypt.org",
  1601  			wantParameters:  nil,
  1602  			expectErrSubstr: "",
  1603  		},
  1604  		{
  1605  			name:            "only domain with trailing semicolon is valid",
  1606  			value:           "letsencrypt.org;",
  1607  			wantDomain:      "letsencrypt.org",
  1608  			wantParameters:  nil,
  1609  			expectErrSubstr: "",
  1610  		},
  1611  		{
  1612  			name:            "only domain with semicolon and trailing whitespace is valid",
  1613  			value:           "letsencrypt.org;   ",
  1614  			wantDomain:      "letsencrypt.org",
  1615  			wantParameters:  nil,
  1616  			expectErrSubstr: "",
  1617  		},
  1618  		{
  1619  			name:            "domain with params and whitespace is valid",
  1620  			value:           "  letsencrypt.org	;foo=bar;baz=bar",
  1621  			wantDomain:      "letsencrypt.org",
  1622  			wantParameters:  []caaParameter{{tag: "foo", val: "bar"}, {tag: "baz", val: "bar"}},
  1623  			expectErrSubstr: "",
  1624  		},
  1625  		{
  1626  			name:            "domain with params and different whitespace is valid",
  1627  			value:           "	letsencrypt.org ;foo=bar;baz=bar",
  1628  			wantDomain:      "letsencrypt.org",
  1629  			wantParameters:  []caaParameter{{tag: "foo", val: "bar"}, {tag: "baz", val: "bar"}},
  1630  			expectErrSubstr: "",
  1631  		},
  1632  		{
  1633  			name:            "empty params are valid",
  1634  			value:           "letsencrypt.org; foo=; baz =	bar",
  1635  			wantDomain:      "letsencrypt.org",
  1636  			wantParameters:  []caaParameter{{tag: "foo", val: ""}, {tag: "baz", val: "bar"}},
  1637  			expectErrSubstr: "",
  1638  		},
  1639  		{
  1640  			name:            "whitespace around params is valid",
  1641  			value:           "letsencrypt.org; foo=	; baz =	bar",
  1642  			wantDomain:      "letsencrypt.org",
  1643  			wantParameters:  []caaParameter{{tag: "foo", val: ""}, {tag: "baz", val: "bar"}},
  1644  			expectErrSubstr: "",
  1645  		},
  1646  		{
  1647  			name:            "comma-separated param values are valid",
  1648  			value:           "letsencrypt.org; foo=b1,b2,b3	; baz =		a=b	",
  1649  			wantDomain:      "letsencrypt.org",
  1650  			wantParameters:  []caaParameter{{tag: "foo", val: "b1,b2,b3"}, {tag: "baz", val: "a=b"}},
  1651  			expectErrSubstr: "",
  1652  		},
  1653  		{
  1654  			name:            "duplicate tags are valid",
  1655  			value:           "letsencrypt.org; foo=b1,b2,b3	; foo= b1,b2,b3	",
  1656  			wantDomain:      "letsencrypt.org",
  1657  			wantParameters:  []caaParameter{{tag: "foo", val: "b1,b2,b3"}, {tag: "foo", val: "b1,b2,b3"}},
  1658  			expectErrSubstr: "",
  1659  		},
  1660  		{
  1661  			name:            "spaces in param values are invalid",
  1662  			value:           "letsencrypt.org; foo=b1,b2,b3	; baz =		a = b	",
  1663  			expectErrSubstr: "value contains disallowed character",
  1664  		},
  1665  		{
  1666  			name:            "spaces in param values are still invalid",
  1667  			value:           "letsencrypt.org; foo=b1,b2,b3	; baz=a=	b",
  1668  			expectErrSubstr: "value contains disallowed character",
  1669  		},
  1670  		{
  1671  			name:            "param without equals sign is invalid",
  1672  			value:           "letsencrypt.org; foo=b1,b2,b3	; baz =		a;b	",
  1673  			expectErrSubstr: "parameter not formatted as tag=value",
  1674  		},
  1675  		{
  1676  			name:            "hyphens in param values are valid",
  1677  			value:           "letsencrypt.org; 1=2; baz=a-b",
  1678  			wantDomain:      "letsencrypt.org",
  1679  			wantParameters:  []caaParameter{{tag: "1", val: "2"}, {tag: "baz", val: "a-b"}},
  1680  			expectErrSubstr: "",
  1681  		},
  1682  		{
  1683  			name:            "underscores in param tags are invalid",
  1684  			value:           "letsencrypt.org; a_b=123",
  1685  			expectErrSubstr: "tag contains disallowed character",
  1686  		},
  1687  		{
  1688  			name:            "multiple spaces in param values are extra invalid",
  1689  			value:           "letsencrypt.org; ab=1 2 3",
  1690  			expectErrSubstr: "value contains disallowed character",
  1691  		},
  1692  		{
  1693  			name:            "hyphens in param tags are invalid",
  1694  			value:           "letsencrypt.org; 1=2; a-b=c",
  1695  			expectErrSubstr: "tag contains disallowed character",
  1696  		},
  1697  		{
  1698  			name:            "high codepoints in params are invalid",
  1699  			value:           "letsencrypt.org; foo=a\u2615b",
  1700  			expectErrSubstr: "value contains disallowed character",
  1701  		},
  1702  		{
  1703  			name:            "missing semicolons between params are invalid",
  1704  			value:           "letsencrypt.org; foo=b1,b2,b3 baz=a",
  1705  			expectErrSubstr: "value contains disallowed character",
  1706  		},
  1707  	}
  1708  	for _, tc := range tests {
  1709  		t.Run(tc.name, func(t *testing.T) {
  1710  			t.Parallel()
  1711  			gotDomain, gotParameters, gotErr := parseCAARecord(&dns.CAA{Value: tc.value})
  1712  
  1713  			if tc.expectErrSubstr == "" {
  1714  				test.AssertNotError(t, gotErr, "")
  1715  			} else {
  1716  				test.AssertError(t, gotErr, "")
  1717  				test.AssertContains(t, gotErr.Error(), tc.expectErrSubstr)
  1718  			}
  1719  
  1720  			if tc.wantDomain != "" {
  1721  				test.AssertEquals(t, gotDomain, tc.wantDomain)
  1722  			}
  1723  
  1724  			if tc.wantParameters != nil {
  1725  				test.AssertDeepEquals(t, gotParameters, tc.wantParameters)
  1726  			}
  1727  		})
  1728  	}
  1729  }