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
23
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
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
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
146 record.Tag = "issuewild"
147 record.Value = ";"
148 results = append(results, &record)
149 case "unsatisfiable-wildcard-override.com":
150
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
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
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
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
527
528 func TestIsCAAValidErrMessage(t *testing.T) {
529 va, _ := setup(nil, 0, "", nil)
530 va.dnsClient = caaMockDNS{}
531
532
533
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
542 test.AssertNotError(t, err, "Unexpected error calling IsCAAValidRequest")
543
544 test.AssertNotNil(t, resp, "Response to IsCAAValidRequest was nil")
545
546 test.AssertNotNil(t, resp.Problem, "Response Problem was nil")
547
548 test.AssertEquals(t, resp.Problem.Detail, fmt.Sprintf("While processing CAA for %s: error", domain))
549 }
550
551
552
553
554 func TestIsCAAValidParams(t *testing.T) {
555 va, _ := setup(nil, 0, "", nil)
556 va.dnsClient = caaMockDNS{}
557
558
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
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
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
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
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
694
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
705
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
717
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