...

Source file src/github.com/letsencrypt/boulder/va/caa_test.go

Documentation: github.com/letsencrypt/boulder/va

     1  package va
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"net"
     8  	"strings"
     9  	"testing"
    10  
    11  	"github.com/miekg/dns"
    12  
    13  	"github.com/letsencrypt/boulder/core"
    14  	"github.com/letsencrypt/boulder/identifier"
    15  	"github.com/letsencrypt/boulder/probs"
    16  	"github.com/letsencrypt/boulder/test"
    17  
    18  	blog "github.com/letsencrypt/boulder/log"
    19  	vapb "github.com/letsencrypt/boulder/va/proto"
    20  )
    21  
    22  // caaMockDNS implements the `dns.DNSClient` interface with a set of useful test
    23  // answers for CAA queries.
    24  type caaMockDNS struct{}
    25  
    26  func (mock caaMockDNS) LookupTXT(_ context.Context, hostname string) ([]string, error) {
    27  	return nil, nil
    28  }
    29  
    30  func (mock caaMockDNS) LookupHost(_ context.Context, hostname string) ([]net.IP, error) {
    31  	ip := net.ParseIP("127.0.0.1")
    32  	return []net.IP{ip}, nil
    33  }
    34  
    35  func (mock caaMockDNS) LookupCAA(_ context.Context, domain string) ([]*dns.CAA, string, error) {
    36  	var results []*dns.CAA
    37  	var record dns.CAA
    38  	switch strings.TrimRight(domain, ".") {
    39  	case "caa-timeout.com":
    40  		return nil, "", fmt.Errorf("error")
    41  	case "reserved.com":
    42  		record.Tag = "issue"
    43  		record.Value = "ca.com"
    44  		results = append(results, &record)
    45  	case "mixedcase.com":
    46  		record.Tag = "iSsUe"
    47  		record.Value = "ca.com"
    48  		results = append(results, &record)
    49  	case "critical.com":
    50  		record.Flag = 1
    51  		record.Tag = "issue"
    52  		record.Value = "ca.com"
    53  		results = append(results, &record)
    54  	case "present.com", "present.servfail.com":
    55  		record.Tag = "issue"
    56  		record.Value = "letsencrypt.org"
    57  		results = append(results, &record)
    58  	case "com":
    59  		// com has no CAA records.
    60  		return nil, "", nil
    61  	case "gonetld":
    62  		return nil, "", fmt.Errorf("NXDOMAIN")
    63  	case "servfail.com", "servfail.present.com":
    64  		return results, "", fmt.Errorf("SERVFAIL")
    65  	case "multi-crit-present.com":
    66  		record.Flag = 1
    67  		record.Tag = "issue"
    68  		record.Value = "ca.com"
    69  		results = append(results, &record)
    70  		secondRecord := record
    71  		secondRecord.Value = "letsencrypt.org"
    72  		results = append(results, &secondRecord)
    73  	case "unknown-critical.com":
    74  		record.Flag = 128
    75  		record.Tag = "foo"
    76  		record.Value = "bar"
    77  		results = append(results, &record)
    78  	case "unknown-critical2.com":
    79  		record.Flag = 1
    80  		record.Tag = "foo"
    81  		record.Value = "bar"
    82  		results = append(results, &record)
    83  	case "unknown-noncritical.com":
    84  		record.Flag = 0x7E // all bits we don't treat as meaning "critical"
    85  		record.Tag = "foo"
    86  		record.Value = "bar"
    87  		results = append(results, &record)
    88  	case "present-with-parameter.com":
    89  		record.Tag = "issue"
    90  		record.Value = "  letsencrypt.org  ;foo=bar;baz=bar"
    91  		results = append(results, &record)
    92  	case "present-with-invalid-tag.com":
    93  		record.Tag = "issue"
    94  		record.Value = "letsencrypt.org; a_b=123"
    95  		results = append(results, &record)
    96  	case "present-with-invalid-value.com":
    97  		record.Tag = "issue"
    98  		record.Value = "letsencrypt.org; ab=1 2 3"
    99  		results = append(results, &record)
   100  	case "present-dns-only.com":
   101  		record.Tag = "issue"
   102  		record.Value = "letsencrypt.org; validationmethods=dns-01"
   103  		results = append(results, &record)
   104  	case "present-http-only.com":
   105  		record.Tag = "issue"
   106  		record.Value = "letsencrypt.org; validationmethods=http-01"
   107  		results = append(results, &record)
   108  	case "present-http-or-dns.com":
   109  		record.Tag = "issue"
   110  		record.Value = "letsencrypt.org; validationmethods=http-01,dns-01"
   111  		results = append(results, &record)
   112  	case "present-dns-only-correct-accounturi.com":
   113  		record.Tag = "issue"
   114  		record.Value = "letsencrypt.org; accounturi=https://letsencrypt.org/acct/reg/123; validationmethods=dns-01"
   115  		results = append(results, &record)
   116  	case "present-http-only-correct-accounturi.com":
   117  		record.Tag = "issue"
   118  		record.Value = "letsencrypt.org; accounturi=https://letsencrypt.org/acct/reg/123; validationmethods=http-01"
   119  		results = append(results, &record)
   120  	case "present-http-only-incorrect-accounturi.com":
   121  		record.Tag = "issue"
   122  		record.Value = "letsencrypt.org; accounturi=https://letsencrypt.org/acct/reg/321; validationmethods=http-01"
   123  		results = append(results, &record)
   124  	case "present-correct-accounturi.com":
   125  		record.Tag = "issue"
   126  		record.Value = "letsencrypt.org; accounturi=https://letsencrypt.org/acct/reg/123"
   127  		results = append(results, &record)
   128  	case "present-incorrect-accounturi.com":
   129  		record.Tag = "issue"
   130  		record.Value = "letsencrypt.org; accounturi=https://letsencrypt.org/acct/reg/321"
   131  		results = append(results, &record)
   132  	case "present-multiple-accounturi.com":
   133  		record.Tag = "issue"
   134  		record.Value = "letsencrypt.org; accounturi=https://letsencrypt.org/acct/reg/321"
   135  		results = append(results, &record)
   136  		secondRecord := record
   137  		secondRecord.Tag = "issue"
   138  		secondRecord.Value = "letsencrypt.org; accounturi=https://letsencrypt.org/acct/reg/123"
   139  		results = append(results, &secondRecord)
   140  	case "unsatisfiable.com":
   141  		record.Tag = "issue"
   142  		record.Value = ";"
   143  		results = append(results, &record)
   144  	case "unsatisfiable-wildcard.com":
   145  		// Forbidden issuance - issuewild doesn't contain LE
   146  		record.Tag = "issuewild"
   147  		record.Value = ";"
   148  		results = append(results, &record)
   149  	case "unsatisfiable-wildcard-override.com":
   150  		// Forbidden issuance - issue allows LE, issuewild overrides and does not
   151  		record.Tag = "issue"
   152  		record.Value = "letsencrypt.org"
   153  		results = append(results, &record)
   154  		secondRecord := record
   155  		secondRecord.Tag = "issuewild"
   156  		secondRecord.Value = "ca.com"
   157  		results = append(results, &secondRecord)
   158  	case "satisfiable-wildcard-override.com":
   159  		// Ok issuance - issue doesn't allow LE, issuewild overrides and does
   160  		record.Tag = "issue"
   161  		record.Value = "ca.com"
   162  		results = append(results, &record)
   163  		secondRecord := record
   164  		secondRecord.Tag = "issuewild"
   165  		secondRecord.Value = "letsencrypt.org"
   166  		results = append(results, &secondRecord)
   167  	case "satisfiable-multi-wildcard.com":
   168  		// Ok issuance - first issuewild doesn't permit LE but second does
   169  		record.Tag = "issuewild"
   170  		record.Value = "ca.com"
   171  		results = append(results, &record)
   172  		secondRecord := record
   173  		secondRecord.Tag = "issuewild"
   174  		secondRecord.Value = "letsencrypt.org"
   175  		results = append(results, &secondRecord)
   176  	case "satisfiable-wildcard.com":
   177  		// Ok issuance - issuewild allows LE
   178  		record.Tag = "issuewild"
   179  		record.Value = "letsencrypt.org"
   180  		results = append(results, &record)
   181  	}
   182  	var response string
   183  	if len(results) > 0 {
   184  		response = "foo"
   185  	}
   186  	return results, response, nil
   187  }
   188  
   189  func TestCAATimeout(t *testing.T) {
   190  	va, _ := setup(nil, 0, "", nil)
   191  	va.dnsClient = caaMockDNS{}
   192  
   193  	params := &caaParams{
   194  		accountURIID:     12345,
   195  		validationMethod: core.ChallengeTypeHTTP01,
   196  	}
   197  
   198  	err := va.checkCAA(ctx, identifier.DNSIdentifier("caa-timeout.com"), params)
   199  	if err.Type != probs.DNSProblem {
   200  		t.Errorf("Expected timeout error type %s, got %s", probs.DNSProblem, err.Type)
   201  	}
   202  
   203  	expected := "error"
   204  	if err.Detail != expected {
   205  		t.Errorf("checkCAA: got %#v, expected %#v", err.Detail, expected)
   206  	}
   207  }
   208  
   209  func TestCAAChecking(t *testing.T) {
   210  	testCases := []struct {
   211  		Name    string
   212  		Domain  string
   213  		FoundAt string
   214  		Valid   bool
   215  	}{
   216  		{
   217  			Name:    "Bad (Reserved)",
   218  			Domain:  "reserved.com",
   219  			FoundAt: "reserved.com",
   220  			Valid:   false,
   221  		},
   222  		{
   223  			Name:    "Bad (Reserved, Mixed case Issue)",
   224  			Domain:  "mixedcase.com",
   225  			FoundAt: "mixedcase.com",
   226  			Valid:   false,
   227  		},
   228  		{
   229  			Name:    "Bad (Critical)",
   230  			Domain:  "critical.com",
   231  			FoundAt: "critical.com",
   232  			Valid:   false,
   233  		},
   234  		{
   235  			Name:    "Bad (NX Critical)",
   236  			Domain:  "nx.critical.com",
   237  			FoundAt: "critical.com",
   238  			Valid:   false,
   239  		},
   240  		{
   241  			Name:    "Good (absent)",
   242  			Domain:  "absent.com",
   243  			FoundAt: "",
   244  			Valid:   true,
   245  		},
   246  		{
   247  			Name:    "Good (example.co.uk, absent)",
   248  			Domain:  "example.co.uk",
   249  			FoundAt: "",
   250  			Valid:   true,
   251  		},
   252  		{
   253  			Name:    "Good (present and valid)",
   254  			Domain:  "present.com",
   255  			FoundAt: "present.com",
   256  			Valid:   true,
   257  		},
   258  		{
   259  			Name:    "Good (present on parent)",
   260  			Domain:  "child.present.com",
   261  			FoundAt: "present.com",
   262  			Valid:   true,
   263  		},
   264  		{
   265  			Name:    "Good (present w/ servfail exception?)",
   266  			Domain:  "present.servfail.com",
   267  			FoundAt: "present.servfail.com",
   268  			Valid:   true,
   269  		},
   270  		{
   271  			Name:    "Good (multiple critical, one matching)",
   272  			Domain:  "multi-crit-present.com",
   273  			FoundAt: "multi-crit-present.com",
   274  			Valid:   true,
   275  		},
   276  		{
   277  			Name:    "Bad (unknown critical)",
   278  			Domain:  "unknown-critical.com",
   279  			FoundAt: "unknown-critical.com",
   280  			Valid:   false,
   281  		},
   282  		{
   283  			Name:    "Bad (unknown critical 2)",
   284  			Domain:  "unknown-critical2.com",
   285  			FoundAt: "unknown-critical2.com",
   286  			Valid:   false,
   287  		},
   288  		{
   289  			Name:    "Good (unknown non-critical, no issue/issuewild)",
   290  			Domain:  "unknown-noncritical.com",
   291  			FoundAt: "unknown-noncritical.com",
   292  			Valid:   true,
   293  		},
   294  		{
   295  			Name:    "Good (issue rec with unknown params)",
   296  			Domain:  "present-with-parameter.com",
   297  			FoundAt: "present-with-parameter.com",
   298  			Valid:   true,
   299  		},
   300  		{
   301  			Name:    "Bad (issue rec with invalid tag)",
   302  			Domain:  "present-with-invalid-tag.com",
   303  			FoundAt: "present-with-invalid-tag.com",
   304  			Valid:   false,
   305  		},
   306  		{
   307  			Name:    "Bad (issue rec with invalid value)",
   308  			Domain:  "present-with-invalid-value.com",
   309  			FoundAt: "present-with-invalid-value.com",
   310  			Valid:   false,
   311  		},
   312  		{
   313  			Name:    "Bad (restricts to dns-01, but tested with http-01)",
   314  			Domain:  "present-dns-only.com",
   315  			FoundAt: "present-dns-only.com",
   316  			Valid:   false,
   317  		},
   318  		{
   319  			Name:    "Good (restricts to http-01, tested with http-01)",
   320  			Domain:  "present-http-only.com",
   321  			FoundAt: "present-http-only.com",
   322  			Valid:   true,
   323  		},
   324  		{
   325  			Name:    "Good (restricts to http-01 or dns-01, tested with http-01)",
   326  			Domain:  "present-http-or-dns.com",
   327  			FoundAt: "present-http-or-dns.com",
   328  			Valid:   true,
   329  		},
   330  		{
   331  			Name:    "Good (restricts to accounturi, tested with correct account)",
   332  			Domain:  "present-correct-accounturi.com",
   333  			FoundAt: "present-correct-accounturi.com",
   334  			Valid:   true,
   335  		},
   336  		{
   337  			Name:    "Good (restricts to http-01 and accounturi, tested with correct account)",
   338  			Domain:  "present-http-only-correct-accounturi.com",
   339  			FoundAt: "present-http-only-correct-accounturi.com",
   340  			Valid:   true,
   341  		},
   342  		{
   343  			Name:    "Bad (restricts to dns-01 and accounturi, tested with http-01)",
   344  			Domain:  "present-dns-only-correct-accounturi.com",
   345  			FoundAt: "present-dns-only-correct-accounturi.com",
   346  			Valid:   false,
   347  		},
   348  		{
   349  			Name:    "Bad (restricts to http-01 and accounturi, tested with incorrect account)",
   350  			Domain:  "present-http-only-incorrect-accounturi.com",
   351  			FoundAt: "present-http-only-incorrect-accounturi.com",
   352  			Valid:   false,
   353  		},
   354  		{
   355  			Name:    "Bad (restricts to accounturi, tested with incorrect account)",
   356  			Domain:  "present-incorrect-accounturi.com",
   357  			FoundAt: "present-incorrect-accounturi.com",
   358  			Valid:   false,
   359  		},
   360  		{
   361  			Name:    "Good (restricts to multiple accounturi, tested with a correct account)",
   362  			Domain:  "present-multiple-accounturi.com",
   363  			FoundAt: "present-multiple-accounturi.com",
   364  			Valid:   true,
   365  		},
   366  		{
   367  			Name:    "Bad (unsatisfiable issue record)",
   368  			Domain:  "unsatisfiable.com",
   369  			FoundAt: "unsatisfiable.com",
   370  			Valid:   false,
   371  		},
   372  		{
   373  			Name:    "Bad (unsatisfiable issue, wildcard)",
   374  			Domain:  "*.unsatisfiable.com",
   375  			FoundAt: "unsatisfiable.com",
   376  			Valid:   false,
   377  		},
   378  		{
   379  			Name:    "Bad (unsatisfiable wildcard)",
   380  			Domain:  "*.unsatisfiable-wildcard.com",
   381  			FoundAt: "unsatisfiable-wildcard.com",
   382  			Valid:   false,
   383  		},
   384  		{
   385  			Name:    "Bad (unsatisfiable wildcard override)",
   386  			Domain:  "*.unsatisfiable-wildcard-override.com",
   387  			FoundAt: "unsatisfiable-wildcard-override.com",
   388  			Valid:   false,
   389  		},
   390  		{
   391  			Name:    "Good (satisfiable wildcard)",
   392  			Domain:  "*.satisfiable-wildcard.com",
   393  			FoundAt: "satisfiable-wildcard.com",
   394  			Valid:   true,
   395  		},
   396  		{
   397  			Name:    "Good (multiple issuewild, one satisfiable)",
   398  			Domain:  "*.satisfiable-multi-wildcard.com",
   399  			FoundAt: "satisfiable-multi-wildcard.com",
   400  			Valid:   true,
   401  		},
   402  		{
   403  			Name:    "Good (satisfiable wildcard override)",
   404  			Domain:  "*.satisfiable-wildcard-override.com",
   405  			FoundAt: "satisfiable-wildcard-override.com",
   406  			Valid:   true,
   407  		},
   408  	}
   409  
   410  	accountURIID := int64(123)
   411  	method := core.ChallengeTypeHTTP01
   412  	params := &caaParams{accountURIID: accountURIID, validationMethod: method}
   413  
   414  	va, _ := setup(nil, 0, "", nil)
   415  	va.dnsClient = caaMockDNS{}
   416  	va.accountURIPrefixes = []string{"https://letsencrypt.org/acct/reg/"}
   417  
   418  	for _, caaTest := range testCases {
   419  		mockLog := va.log.(*blog.Mock)
   420  		mockLog.Clear()
   421  		t.Run(caaTest.Name, func(t *testing.T) {
   422  			ident := identifier.DNSIdentifier(caaTest.Domain)
   423  			foundAt, valid, _, err := va.checkCAARecords(ctx, ident, params)
   424  			if err != nil {
   425  				t.Errorf("checkCAARecords error for %s: %s", caaTest.Domain, err)
   426  			}
   427  			if foundAt != caaTest.FoundAt {
   428  				t.Errorf("checkCAARecords presence mismatch for %s: got %q expected %q", caaTest.Domain, foundAt, caaTest.FoundAt)
   429  			}
   430  			if valid != caaTest.Valid {
   431  				t.Errorf("checkCAARecords validity mismatch for %s: got %t expected %t", caaTest.Domain, valid, caaTest.Valid)
   432  			}
   433  		})
   434  	}
   435  }
   436  
   437  func TestCAALogging(t *testing.T) {
   438  	va, _ := setup(nil, 0, "", nil)
   439  	va.dnsClient = caaMockDNS{}
   440  
   441  	testCases := []struct {
   442  		Name            string
   443  		Domain          string
   444  		AccountURIID    int64
   445  		ChallengeType   core.AcmeChallenge
   446  		ExpectedLogline string
   447  	}{
   448  		{
   449  			Domain:          "reserved.com",
   450  			AccountURIID:    12345,
   451  			ChallengeType:   core.ChallengeTypeHTTP01,
   452  			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\"",
   453  		},
   454  		{
   455  			Domain:          "reserved.com",
   456  			AccountURIID:    12345,
   457  			ChallengeType:   core.ChallengeTypeDNS01,
   458  			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\"",
   459  		},
   460  		{
   461  			Domain:          "mixedcase.com",
   462  			AccountURIID:    12345,
   463  			ChallengeType:   core.ChallengeTypeHTTP01,
   464  			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\"",
   465  		},
   466  		{
   467  			Domain:          "critical.com",
   468  			AccountURIID:    12345,
   469  			ChallengeType:   core.ChallengeTypeHTTP01,
   470  			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\"",
   471  		},
   472  		{
   473  			Domain:          "present.com",
   474  			AccountURIID:    12345,
   475  			ChallengeType:   core.ChallengeTypeHTTP01,
   476  			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\"",
   477  		},
   478  		{
   479  			Domain:          "not.here.but.still.present.com",
   480  			AccountURIID:    12345,
   481  			ChallengeType:   core.ChallengeTypeHTTP01,
   482  			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\"",
   483  		},
   484  		{
   485  			Domain:          "multi-crit-present.com",
   486  			AccountURIID:    12345,
   487  			ChallengeType:   core.ChallengeTypeHTTP01,
   488  			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\"",
   489  		},
   490  		{
   491  			Domain:          "present-with-parameter.com",
   492  			AccountURIID:    12345,
   493  			ChallengeType:   core.ChallengeTypeHTTP01,
   494  			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\"",
   495  		},
   496  		{
   497  			Domain:          "satisfiable-wildcard-override.com",
   498  			AccountURIID:    12345,
   499  			ChallengeType:   core.ChallengeTypeHTTP01,
   500  			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\"",
   501  		},
   502  	}
   503  
   504  	for _, tc := range testCases {
   505  		t.Run(tc.Domain, func(t *testing.T) {
   506  			mockLog := va.log.(*blog.Mock)
   507  			mockLog.Clear()
   508  
   509  			params := &caaParams{
   510  				accountURIID:     tc.AccountURIID,
   511  				validationMethod: tc.ChallengeType,
   512  			}
   513  			_ = va.checkCAA(ctx, identifier.ACMEIdentifier{Type: identifier.DNS, Value: tc.Domain}, params)
   514  
   515  			caaLogLines := mockLog.GetAllMatching(`Checked CAA records for`)
   516  			if len(caaLogLines) != 1 {
   517  				t.Errorf("checkCAARecords didn't audit log CAA record info. Instead got:\n%s\n",
   518  					strings.Join(mockLog.GetAllMatching(`.*`), "\n"))
   519  			} else {
   520  				test.AssertEquals(t, caaLogLines[0], tc.ExpectedLogline)
   521  			}
   522  		})
   523  	}
   524  }
   525  
   526  // TestIsCAAValidErrMessage tests that an error result from `va.IsCAAValid`
   527  // includes the domain name that was being checked in the failure detail.
   528  func TestIsCAAValidErrMessage(t *testing.T) {
   529  	va, _ := setup(nil, 0, "", nil)
   530  	va.dnsClient = caaMockDNS{}
   531  
   532  	// Call IsCAAValid with a domain we know fails with a generic error from the
   533  	// caaMockDNS.
   534  	domain := "caa-timeout.com"
   535  	resp, err := va.IsCAAValid(ctx, &vapb.IsCAAValidRequest{
   536  		Domain:           domain,
   537  		ValidationMethod: string(core.ChallengeTypeHTTP01),
   538  		AccountURIID:     12345,
   539  	})
   540  
   541  	// The lookup itself should not return an error
   542  	test.AssertNotError(t, err, "Unexpected error calling IsCAAValidRequest")
   543  	// The result should not be nil
   544  	test.AssertNotNil(t, resp, "Response to IsCAAValidRequest was nil")
   545  	// The result's Problem should not be nil
   546  	test.AssertNotNil(t, resp.Problem, "Response Problem was nil")
   547  	// The result's Problem should be an error message that includes the domain.
   548  	test.AssertEquals(t, resp.Problem.Detail, fmt.Sprintf("While processing CAA for %s: error", domain))
   549  }
   550  
   551  // TestIsCAAValidParams tests that the IsCAAValid method rejects any requests
   552  // which do not have the necessary parameters to do CAA Account and Method
   553  // Binding checks.
   554  func TestIsCAAValidParams(t *testing.T) {
   555  	va, _ := setup(nil, 0, "", nil)
   556  	va.dnsClient = caaMockDNS{}
   557  
   558  	// Calling IsCAAValid without a ValidationMethod should fail.
   559  	_, err := va.IsCAAValid(ctx, &vapb.IsCAAValidRequest{
   560  		Domain:       "present.com",
   561  		AccountURIID: 12345,
   562  	})
   563  	test.AssertError(t, err, "calling IsCAAValid without a ValidationMethod")
   564  
   565  	// Calling IsCAAValid with an invalid ValidationMethod should fail.
   566  	_, err = va.IsCAAValid(ctx, &vapb.IsCAAValidRequest{
   567  		Domain:           "present.com",
   568  		ValidationMethod: "tls-sni-01",
   569  		AccountURIID:     12345,
   570  	})
   571  	test.AssertError(t, err, "calling IsCAAValid with a bad ValidationMethod")
   572  
   573  	// Calling IsCAAValid without an AccountURIID should fail.
   574  	_, err = va.IsCAAValid(ctx, &vapb.IsCAAValidRequest{
   575  		Domain:           "present.com",
   576  		ValidationMethod: string(core.ChallengeTypeHTTP01),
   577  	})
   578  	test.AssertError(t, err, "calling IsCAAValid without an AccountURIID")
   579  }
   580  
   581  func TestCAAFailure(t *testing.T) {
   582  	chall := createChallenge(core.ChallengeTypeHTTP01)
   583  	hs := httpSrv(t, chall.Token)
   584  	defer hs.Close()
   585  
   586  	va, _ := setup(hs, 0, "", nil)
   587  	va.dnsClient = caaMockDNS{}
   588  
   589  	_, prob := va.validate(ctx, dnsi("reserved.com"), 1, chall)
   590  	if prob == nil {
   591  		t.Fatalf("Expected CAA rejection for reserved.com, got success")
   592  	}
   593  	test.AssertEquals(t, prob.Type, probs.CAAProblem)
   594  
   595  	_, prob = va.validate(ctx, dnsi("example.gonetld"), 1, chall)
   596  	if prob == nil {
   597  		t.Fatalf("Expected CAA rejection for gonetld, got success")
   598  	}
   599  	test.AssertEquals(t, prob.Type, probs.DNSProblem)
   600  	test.AssertContains(t, prob.Error(), "NXDOMAIN")
   601  }
   602  
   603  func TestFilterCAA(t *testing.T) {
   604  	testCases := []struct {
   605  		name              string
   606  		input             []*dns.CAA
   607  		expectedIssueVals []string
   608  		expectedWildVals  []string
   609  		expectedCU        bool
   610  	}{
   611  		{
   612  			name: "recognized non-critical",
   613  			input: []*dns.CAA{
   614  				{Tag: "issue", Value: "a"},
   615  				{Tag: "issuewild", Value: "b"},
   616  				{Tag: "iodef", Value: "c"},
   617  			},
   618  			expectedIssueVals: []string{"a"},
   619  			expectedWildVals:  []string{"b"},
   620  		},
   621  		{
   622  			name: "recognized critical",
   623  			input: []*dns.CAA{
   624  				{Tag: "issue", Value: "a", Flag: 128},
   625  				{Tag: "issuewild", Value: "b", Flag: 128},
   626  				{Tag: "iodef", Value: "c", Flag: 128},
   627  			},
   628  			expectedIssueVals: []string{"a"},
   629  			expectedWildVals:  []string{"b"},
   630  		},
   631  		{
   632  			name: "unrecognized non-critical",
   633  			input: []*dns.CAA{
   634  				{Tag: "unknown", Flag: 2},
   635  			},
   636  		},
   637  		{
   638  			name: "unrecognized critical",
   639  			input: []*dns.CAA{
   640  				{Tag: "unknown", Flag: 128},
   641  			},
   642  			expectedCU: true,
   643  		},
   644  		{
   645  			name: "unrecognized improper critical",
   646  			input: []*dns.CAA{
   647  				{Tag: "unknown", Flag: 1},
   648  			},
   649  			expectedCU: true,
   650  		},
   651  		{
   652  			name: "unrecognized very improper critical",
   653  			input: []*dns.CAA{
   654  				{Tag: "unknown", Flag: 9},
   655  			},
   656  			expectedCU: true,
   657  		},
   658  	}
   659  
   660  	for _, tc := range testCases {
   661  		t.Run(tc.name, func(t *testing.T) {
   662  			issue, wild, cu := filterCAA(tc.input)
   663  			for _, tag := range issue {
   664  				test.AssertSliceContains(t, tc.expectedIssueVals, tag.Value)
   665  			}
   666  			for _, tag := range wild {
   667  				test.AssertSliceContains(t, tc.expectedWildVals, tag.Value)
   668  			}
   669  			test.AssertEquals(t, tc.expectedCU, cu)
   670  		})
   671  	}
   672  }
   673  
   674  func TestSelectCAA(t *testing.T) {
   675  	expected := dns.CAA{Tag: "issue", Value: "foo"}
   676  
   677  	// An empty slice of caaResults should return nil, nil
   678  	r := []caaResult{}
   679  	s, err := selectCAA(r)
   680  	test.Assert(t, s == nil, "set is not nil")
   681  	test.AssertNotError(t, err, "error is not nil")
   682  
   683  	// A slice of empty caaResults should return nil, "", nil
   684  	r = []caaResult{
   685  		{"", false, nil, nil, false, "", nil},
   686  		{"", false, nil, nil, false, "", nil},
   687  		{"", false, nil, nil, false, "", nil},
   688  	}
   689  	s, err = selectCAA(r)
   690  	test.Assert(t, s == nil, "set is not nil")
   691  	test.AssertNotError(t, err, "error is not nil")
   692  
   693  	// A slice of caaResults containing an error followed by a CAA
   694  	// record should return the error
   695  	r = []caaResult{
   696  		{"foo.com", false, nil, nil, false, "", errors.New("oops")},
   697  		{"com", true, []*dns.CAA{&expected}, nil, false, "foo", nil},
   698  	}
   699  	s, err = selectCAA(r)
   700  	test.Assert(t, s == nil, "set is not nil")
   701  	test.AssertError(t, err, "error is nil")
   702  	test.AssertEquals(t, err.Error(), "oops")
   703  
   704  	//  A slice of caaResults containing a good record that precedes an
   705  	//  error, should return that good record, not the error
   706  	r = []caaResult{
   707  		{"foo.com", true, []*dns.CAA{&expected}, nil, false, "foo", nil},
   708  		{"com", false, nil, nil, false, "", errors.New("")},
   709  	}
   710  	s, err = selectCAA(r)
   711  	test.AssertEquals(t, len(s.issue), 1)
   712  	test.Assert(t, s.issue[0] == &expected, "Incorrect record returned")
   713  	test.AssertEquals(t, s.dig, "foo")
   714  	test.Assert(t, err == nil, "error is not nil")
   715  
   716  	// A slice of caaResults containing multiple CAA records should
   717  	// return the first non-empty CAA record
   718  	r = []caaResult{
   719  		{"bar.foo.com", false, []*dns.CAA{}, []*dns.CAA{}, false, "", nil},
   720  		{"foo.com", true, []*dns.CAA{&expected}, nil, false, "foo", nil},
   721  		{"com", true, []*dns.CAA{&expected}, nil, false, "bar", nil},
   722  	}
   723  	s, err = selectCAA(r)
   724  	test.AssertEquals(t, len(s.issue), 1)
   725  	test.Assert(t, s.issue[0] == &expected, "Incorrect record returned")
   726  	test.AssertEquals(t, s.dig, "foo")
   727  	test.AssertNotError(t, err, "expect nil error")
   728  }
   729  
   730  func TestAccountURIMatches(t *testing.T) {
   731  	tests := []struct {
   732  		name     string
   733  		params   map[string]string
   734  		prefixes []string
   735  		id       int64
   736  		want     bool
   737  	}{
   738  		{
   739  			name:   "empty accounturi",
   740  			params: map[string]string{},
   741  			prefixes: []string{
   742  				"https://acme-v01.api.letsencrypt.org/acme/reg/",
   743  			},
   744  			id:   123456,
   745  			want: true,
   746  		},
   747  		{
   748  			name: "non-uri accounturi",
   749  			params: map[string]string{
   750  				"accounturi": "\\invalid 😎/123456",
   751  			},
   752  			prefixes: []string{
   753  				"\\invalid 😎",
   754  			},
   755  			id:   123456,
   756  			want: false,
   757  		},
   758  		{
   759  			name: "simple match",
   760  			params: map[string]string{
   761  				"accounturi": "https://acme-v01.api.letsencrypt.org/acme/reg/123456",
   762  			},
   763  			prefixes: []string{
   764  				"https://acme-v01.api.letsencrypt.org/acme/reg/",
   765  			},
   766  			id:   123456,
   767  			want: true,
   768  		},
   769  		{
   770  			name: "accountid mismatch",
   771  			params: map[string]string{
   772  				"accounturi": "https://acme-v01.api.letsencrypt.org/acme/reg/123456",
   773  			},
   774  			prefixes: []string{
   775  				"https://acme-v01.api.letsencrypt.org/acme/reg/",
   776  			},
   777  			id:   123457,
   778  			want: false,
   779  		},
   780  		{
   781  			name: "multiple prefixes, match first",
   782  			params: map[string]string{
   783  				"accounturi": "https://acme-staging.api.letsencrypt.org/acme/reg/123456",
   784  			},
   785  			prefixes: []string{
   786  				"https://acme-staging.api.letsencrypt.org/acme/reg/",
   787  				"https://acme-staging-v02.api.letsencrypt.org/acme/acct/",
   788  			},
   789  			id:   123456,
   790  			want: true,
   791  		},
   792  		{
   793  			name: "multiple prefixes, match second",
   794  			params: map[string]string{
   795  				"accounturi": "https://acme-v02.api.letsencrypt.org/acme/acct/123456",
   796  			},
   797  			prefixes: []string{
   798  				"https://acme-v01.api.letsencrypt.org/acme/reg/",
   799  				"https://acme-v02.api.letsencrypt.org/acme/acct/",
   800  			},
   801  			id:   123456,
   802  			want: true,
   803  		},
   804  		{
   805  			name: "multiple prefixes, match none",
   806  			params: map[string]string{
   807  				"accounturi": "https://acme-v02.api.letsencrypt.org/acme/acct/123456",
   808  			},
   809  			prefixes: []string{
   810  				"https://acme-v01.api.letsencrypt.org/acme/acct/",
   811  				"https://acme-v03.api.letsencrypt.org/acme/acct/",
   812  			},
   813  			id:   123456,
   814  			want: false,
   815  		},
   816  		{
   817  			name: "three prefixes",
   818  			params: map[string]string{
   819  				"accounturi": "https://acme-v02.api.letsencrypt.org/acme/acct/123456",
   820  			},
   821  			prefixes: []string{
   822  				"https://acme-v01.api.letsencrypt.org/acme/reg/",
   823  				"https://acme-v02.api.letsencrypt.org/acme/acct/",
   824  				"https://acme-v03.api.letsencrypt.org/acme/acct/",
   825  			},
   826  			id:   123456,
   827  			want: true,
   828  		},
   829  		{
   830  			name: "multiple prefixes, wrong accountid",
   831  			params: map[string]string{
   832  				"accounturi": "https://acme-v02.api.letsencrypt.org/acme/acct/123456",
   833  			},
   834  			prefixes: []string{
   835  				"https://acme-v01.api.letsencrypt.org/acme/reg/",
   836  				"https://acme-v02.api.letsencrypt.org/acme/acct/",
   837  			},
   838  			id:   654321,
   839  			want: false,
   840  		},
   841  	}
   842  
   843  	for _, tc := range tests {
   844  		t.Run(tc.name, func(t *testing.T) {
   845  			got := caaAccountURIMatches(tc.params, tc.prefixes, tc.id)
   846  			test.AssertEquals(t, got, tc.want)
   847  		})
   848  	}
   849  }
   850  
   851  func TestValidationMethodMatches(t *testing.T) {
   852  	tests := []struct {
   853  		name   string
   854  		params map[string]string
   855  		method core.AcmeChallenge
   856  		want   bool
   857  	}{
   858  		{
   859  			name:   "empty validationmethods",
   860  			params: map[string]string{},
   861  			method: core.ChallengeTypeHTTP01,
   862  			want:   true,
   863  		},
   864  		{
   865  			name: "only comma",
   866  			params: map[string]string{
   867  				"validationmethods": ",",
   868  			},
   869  			method: core.ChallengeTypeHTTP01,
   870  			want:   false,
   871  		},
   872  		{
   873  			name: "malformed method",
   874  			params: map[string]string{
   875  				"validationmethods": "howdy !",
   876  			},
   877  			method: core.ChallengeTypeHTTP01,
   878  			want:   false,
   879  		},
   880  		{
   881  			name: "invalid method",
   882  			params: map[string]string{
   883  				"validationmethods": "tls-sni-01",
   884  			},
   885  			method: core.ChallengeTypeHTTP01,
   886  			want:   false,
   887  		},
   888  		{
   889  			name: "simple match",
   890  			params: map[string]string{
   891  				"validationmethods": "http-01",
   892  			},
   893  			method: core.ChallengeTypeHTTP01,
   894  			want:   true,
   895  		},
   896  		{
   897  			name: "simple mismatch",
   898  			params: map[string]string{
   899  				"validationmethods": "dns-01",
   900  			},
   901  			method: core.ChallengeTypeHTTP01,
   902  			want:   false,
   903  		},
   904  		{
   905  			name: "multiple choices, match first",
   906  			params: map[string]string{
   907  				"validationmethods": "http-01,dns-01",
   908  			},
   909  			method: core.ChallengeTypeHTTP01,
   910  			want:   true,
   911  		},
   912  		{
   913  			name: "multiple choices, match second",
   914  			params: map[string]string{
   915  				"validationmethods": "http-01,dns-01",
   916  			},
   917  			method: core.ChallengeTypeDNS01,
   918  			want:   true,
   919  		},
   920  		{
   921  			name: "multiple choices, match none",
   922  			params: map[string]string{
   923  				"validationmethods": "http-01,dns-01",
   924  			},
   925  			method: core.ChallengeTypeTLSALPN01,
   926  			want:   false,
   927  		},
   928  	}
   929  
   930  	for _, tc := range tests {
   931  		t.Run(tc.name, func(t *testing.T) {
   932  			got := caaValidationMethodMatches(tc.params, tc.method)
   933  			test.AssertEquals(t, got, tc.want)
   934  		})
   935  	}
   936  }
   937  
   938  func TestExtractIssuerDomainAndParameters(t *testing.T) {
   939  	tests := []struct {
   940  		name            string
   941  		value           string
   942  		wantDomain      string
   943  		wantParameters  map[string]string
   944  		expectErrSubstr string
   945  	}{
   946  		{
   947  			name:            "empty record is valid",
   948  			value:           "",
   949  			wantDomain:      "",
   950  			wantParameters:  map[string]string{},
   951  			expectErrSubstr: "",
   952  		},
   953  		{
   954  			name:            "only semicolon is valid",
   955  			value:           ";",
   956  			wantDomain:      "",
   957  			wantParameters:  map[string]string{},
   958  			expectErrSubstr: "",
   959  		},
   960  		{
   961  			name:            "only semicolon and whitespace is valid",
   962  			value:           " ; ",
   963  			wantDomain:      "",
   964  			wantParameters:  map[string]string{},
   965  			expectErrSubstr: "",
   966  		},
   967  		{
   968  			name:            "only domain is valid",
   969  			value:           "letsencrypt.org",
   970  			wantDomain:      "letsencrypt.org",
   971  			wantParameters:  map[string]string{},
   972  			expectErrSubstr: "",
   973  		},
   974  		{
   975  			name:            "only domain with trailing semicolon is valid",
   976  			value:           "letsencrypt.org;",
   977  			wantDomain:      "letsencrypt.org",
   978  			wantParameters:  map[string]string{},
   979  			expectErrSubstr: "",
   980  		},
   981  		{
   982  			name:            "domain with params and whitespace is valid",
   983  			value:           "  letsencrypt.org	;foo=bar;baz=bar",
   984  			wantDomain:      "letsencrypt.org",
   985  			wantParameters:  map[string]string{"foo": "bar", "baz": "bar"},
   986  			expectErrSubstr: "",
   987  		},
   988  		{
   989  			name:            "domain with params and different whitespace is valid",
   990  			value:           "	letsencrypt.org ;foo=bar;baz=bar",
   991  			wantDomain:      "letsencrypt.org",
   992  			wantParameters:  map[string]string{"foo": "bar", "baz": "bar"},
   993  			expectErrSubstr: "",
   994  		},
   995  		{
   996  			name:            "empty params are valid",
   997  			value:           "letsencrypt.org; foo=; baz =	bar",
   998  			wantDomain:      "letsencrypt.org",
   999  			wantParameters:  map[string]string{"foo": "", "baz": "bar"},
  1000  			expectErrSubstr: "",
  1001  		},
  1002  		{
  1003  			name:            "whitespace around params is valid",
  1004  			value:           "letsencrypt.org; foo=	; baz =	bar",
  1005  			wantDomain:      "letsencrypt.org",
  1006  			wantParameters:  map[string]string{"foo": "", "baz": "bar"},
  1007  			expectErrSubstr: "",
  1008  		},
  1009  		{
  1010  			name:            "comma-separated param values are valid",
  1011  			value:           "letsencrypt.org; foo=b1,b2,b3	; baz =		a=b	",
  1012  			wantDomain:      "letsencrypt.org",
  1013  			wantParameters:  map[string]string{"foo": "b1,b2,b3", "baz": "a=b"},
  1014  			expectErrSubstr: "",
  1015  		},
  1016  		{
  1017  			name:            "spaces in param values are invalid",
  1018  			value:           "letsencrypt.org; foo=b1,b2,b3	; baz =		a = b	",
  1019  			expectErrSubstr: "value contains disallowed character",
  1020  		},
  1021  		{
  1022  			name:            "spaces in param values are still invalid",
  1023  			value:           "letsencrypt.org; foo=b1,b2,b3	; baz=a=	b",
  1024  			expectErrSubstr: "value contains disallowed character",
  1025  		},
  1026  		{
  1027  			name:            "param without equals sign is invalid",
  1028  			value:           "letsencrypt.org; foo=b1,b2,b3	; baz =		a;b	",
  1029  			expectErrSubstr: "parameter not formatted as tag=value",
  1030  		},
  1031  		{
  1032  			name:            "hyphens in param values are valid",
  1033  			value:           "letsencrypt.org; 1=2; baz=a-b",
  1034  			wantDomain:      "letsencrypt.org",
  1035  			wantParameters:  map[string]string{"1": "2", "baz": "a-b"},
  1036  			expectErrSubstr: "",
  1037  		},
  1038  		{
  1039  			name:            "underscores in param tags are invalid",
  1040  			value:           "letsencrypt.org; a_b=123",
  1041  			expectErrSubstr: "tag contains disallowed character",
  1042  		},
  1043  		{
  1044  			name:            "multiple spaces in param values are extra invalid",
  1045  			value:           "letsencrypt.org; ab=1 2 3",
  1046  			expectErrSubstr: "value contains disallowed character",
  1047  		},
  1048  		{
  1049  			name:            "hyphens in param tags are invalid",
  1050  			value:           "letsencrypt.org; 1=2; a-b=c",
  1051  			expectErrSubstr: "tag contains disallowed character",
  1052  		},
  1053  		{
  1054  			name:            "high codepoints in params are invalid",
  1055  			value:           "letsencrypt.org; foo=a\u2615b",
  1056  			expectErrSubstr: "value contains disallowed character",
  1057  		},
  1058  		{
  1059  			name:            "missing semicolons between params are invalid",
  1060  			value:           "letsencrypt.org; foo=b1,b2,b3 baz=a",
  1061  			expectErrSubstr: "value contains disallowed character",
  1062  		},
  1063  	}
  1064  	for _, tc := range tests {
  1065  		t.Run(tc.name, func(t *testing.T) {
  1066  			gotDomain, gotParameters, gotErr := parseCAARecord(&dns.CAA{Value: tc.value})
  1067  
  1068  			if tc.expectErrSubstr == "" {
  1069  				test.AssertNotError(t, gotErr, "")
  1070  			} else {
  1071  				test.AssertError(t, gotErr, "")
  1072  				test.AssertContains(t, gotErr.Error(), tc.expectErrSubstr)
  1073  			}
  1074  
  1075  			if tc.wantDomain != "" {
  1076  				test.AssertEquals(t, gotDomain, tc.wantDomain)
  1077  			}
  1078  
  1079  			if tc.wantParameters != nil {
  1080  				test.AssertDeepEquals(t, gotParameters, tc.wantParameters)
  1081  			}
  1082  		})
  1083  	}
  1084  }
  1085  

View as plain text