827 lines
27 KiB
Go
827 lines
27 KiB
Go
package va
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/miekg/dns"
|
|
|
|
"github.com/letsencrypt/boulder/core"
|
|
"github.com/letsencrypt/boulder/features"
|
|
"github.com/letsencrypt/boulder/probs"
|
|
"github.com/letsencrypt/boulder/test"
|
|
|
|
blog "github.com/letsencrypt/boulder/log"
|
|
vapb "github.com/letsencrypt/boulder/va/proto"
|
|
)
|
|
|
|
// caaMockDNS implements the `dns.DNSClient` interface with a set of useful test
|
|
// answers for CAA queries.
|
|
type caaMockDNS struct{}
|
|
|
|
func (mock caaMockDNS) LookupTXT(_ context.Context, hostname string) ([]string, []string, error) {
|
|
return nil, nil, nil
|
|
}
|
|
|
|
func (mock caaMockDNS) LookupHost(_ context.Context, hostname string) ([]net.IP, error) {
|
|
ip := net.ParseIP("127.0.0.1")
|
|
return []net.IP{ip}, nil
|
|
}
|
|
|
|
func (mock caaMockDNS) LookupCAA(_ context.Context, domain string) ([]*dns.CAA, error) {
|
|
var results []*dns.CAA
|
|
var record dns.CAA
|
|
switch strings.TrimRight(domain, ".") {
|
|
case "caa-timeout.com":
|
|
return nil, fmt.Errorf("error")
|
|
case "reserved.com":
|
|
record.Tag = "issue"
|
|
record.Value = "ca.com"
|
|
results = append(results, &record)
|
|
case "mixedcase.com":
|
|
record.Tag = "iSsUe"
|
|
record.Value = "ca.com"
|
|
results = append(results, &record)
|
|
case "critical.com":
|
|
record.Flag = 1
|
|
record.Tag = "issue"
|
|
record.Value = "ca.com"
|
|
results = append(results, &record)
|
|
case "present.com", "present.servfail.com":
|
|
record.Tag = "issue"
|
|
record.Value = "letsencrypt.org"
|
|
results = append(results, &record)
|
|
case "com":
|
|
// com has no CAA records.
|
|
return nil, nil
|
|
case "servfail.com", "servfail.present.com":
|
|
return results, fmt.Errorf("SERVFAIL")
|
|
case "multi-crit-present.com":
|
|
record.Flag = 1
|
|
record.Tag = "issue"
|
|
record.Value = "ca.com"
|
|
results = append(results, &record)
|
|
secondRecord := record
|
|
secondRecord.Value = "letsencrypt.org"
|
|
results = append(results, &secondRecord)
|
|
case "unknown-critical.com":
|
|
record.Flag = 128
|
|
record.Tag = "foo"
|
|
record.Value = "bar"
|
|
results = append(results, &record)
|
|
case "unknown-critical2.com":
|
|
record.Flag = 1
|
|
record.Tag = "foo"
|
|
record.Value = "bar"
|
|
results = append(results, &record)
|
|
case "unknown-noncritical.com":
|
|
record.Flag = 0x7E // all bits we don't treat as meaning "critical"
|
|
record.Tag = "foo"
|
|
record.Value = "bar"
|
|
results = append(results, &record)
|
|
case "present-with-parameter.com":
|
|
record.Tag = "issue"
|
|
record.Value = " letsencrypt.org ;foo=bar;baz=bar"
|
|
results = append(results, &record)
|
|
case "present-with-invalid-tag.com":
|
|
record.Tag = "issue"
|
|
record.Value = "letsencrypt.org; a_b=123"
|
|
results = append(results, &record)
|
|
case "present-with-invalid-value.com":
|
|
record.Tag = "issue"
|
|
record.Value = "letsencrypt.org; ab=1 2 3"
|
|
results = append(results, &record)
|
|
case "present-dns-only.com":
|
|
record.Tag = "issue"
|
|
record.Value = "letsencrypt.org; validationmethods=dns-01"
|
|
results = append(results, &record)
|
|
case "present-http-only.com":
|
|
record.Tag = "issue"
|
|
record.Value = "letsencrypt.org; validationmethods=http-01"
|
|
results = append(results, &record)
|
|
case "present-http-or-dns.com":
|
|
record.Tag = "issue"
|
|
record.Value = "letsencrypt.org; validationmethods=http-01,dns-01"
|
|
results = append(results, &record)
|
|
case "present-dns-only-correct-accounturi.com":
|
|
record.Tag = "issue"
|
|
record.Value = "letsencrypt.org; accounturi=https://letsencrypt.org/acct/reg/123; validationmethods=dns-01"
|
|
results = append(results, &record)
|
|
case "present-http-only-correct-accounturi.com":
|
|
record.Tag = "issue"
|
|
record.Value = "letsencrypt.org; accounturi=https://letsencrypt.org/acct/reg/123; validationmethods=http-01"
|
|
results = append(results, &record)
|
|
case "present-http-only-incorrect-accounturi.com":
|
|
record.Tag = "issue"
|
|
record.Value = "letsencrypt.org; accounturi=https://letsencrypt.org/acct/reg/321; validationmethods=http-01"
|
|
results = append(results, &record)
|
|
case "present-correct-accounturi.com":
|
|
record.Tag = "issue"
|
|
record.Value = "letsencrypt.org; accounturi=https://letsencrypt.org/acct/reg/123"
|
|
results = append(results, &record)
|
|
case "present-incorrect-accounturi.com":
|
|
record.Tag = "issue"
|
|
record.Value = "letsencrypt.org; accounturi=https://letsencrypt.org/acct/reg/321"
|
|
results = append(results, &record)
|
|
case "present-multiple-accounturi.com":
|
|
record.Tag = "issue"
|
|
record.Value = "letsencrypt.org; accounturi=https://letsencrypt.org/acct/reg/321"
|
|
results = append(results, &record)
|
|
secondRecord := record
|
|
secondRecord.Tag = "issue"
|
|
secondRecord.Value = "letsencrypt.org; accounturi=https://letsencrypt.org/acct/reg/123"
|
|
results = append(results, &secondRecord)
|
|
case "unsatisfiable.com":
|
|
record.Tag = "issue"
|
|
record.Value = ";"
|
|
results = append(results, &record)
|
|
case "unsatisfiable-wildcard.com":
|
|
// Forbidden issuance - issuewild doesn't contain LE
|
|
record.Tag = "issuewild"
|
|
record.Value = ";"
|
|
results = append(results, &record)
|
|
case "unsatisfiable-wildcard-override.com":
|
|
// Forbidden issuance - issue allows LE, issuewild overrides and does not
|
|
record.Tag = "issue"
|
|
record.Value = "letsencrypt.org"
|
|
results = append(results, &record)
|
|
secondRecord := record
|
|
secondRecord.Tag = "issuewild"
|
|
secondRecord.Value = "ca.com"
|
|
results = append(results, &secondRecord)
|
|
case "satisfiable-wildcard-override.com":
|
|
// Ok issuance - issue doesn't allow LE, issuewild overrides and does
|
|
record.Tag = "issue"
|
|
record.Value = "ca.com"
|
|
results = append(results, &record)
|
|
secondRecord := record
|
|
secondRecord.Tag = "issuewild"
|
|
secondRecord.Value = "letsencrypt.org"
|
|
results = append(results, &secondRecord)
|
|
case "satisfiable-multi-wildcard.com":
|
|
// Ok issuance - first issuewild doesn't permit LE but second does
|
|
record.Tag = "issuewild"
|
|
record.Value = "ca.com"
|
|
results = append(results, &record)
|
|
secondRecord := record
|
|
secondRecord.Tag = "issuewild"
|
|
secondRecord.Value = "letsencrypt.org"
|
|
results = append(results, &secondRecord)
|
|
case "satisfiable-wildcard.com":
|
|
// Ok issuance - issuewild allows LE
|
|
record.Tag = "issuewild"
|
|
record.Value = "letsencrypt.org"
|
|
results = append(results, &record)
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
func TestCAATimeout(t *testing.T) {
|
|
va, _ := setup(nil, 0, "", nil)
|
|
va.dnsClient = caaMockDNS{}
|
|
err := va.checkCAA(ctx, core.AcmeIdentifier{Type: core.IdentifierDNS, Value: "caa-timeout.com"}, nil)
|
|
if err.Type != probs.DNSProblem {
|
|
t.Errorf("Expected timeout error type %s, got %s", probs.DNSProblem, err.Type)
|
|
}
|
|
expected := "error"
|
|
if err.Detail != expected {
|
|
t.Errorf("checkCAA: got %#v, expected %#v", err.Detail, expected)
|
|
}
|
|
}
|
|
|
|
func TestCAAChecking(t *testing.T) {
|
|
testCases := []struct {
|
|
Name string
|
|
Domain string
|
|
Present bool
|
|
Valid bool
|
|
}{
|
|
{
|
|
Name: "Bad (Reserved)",
|
|
Domain: "reserved.com",
|
|
Present: true,
|
|
Valid: false,
|
|
},
|
|
{
|
|
Name: "Bad (Reserved, Mixed case Issue)",
|
|
Domain: "mixedcase.com",
|
|
Present: true,
|
|
Valid: false,
|
|
},
|
|
{
|
|
Name: "Bad (Critical)",
|
|
Domain: "critical.com",
|
|
Present: true,
|
|
Valid: false,
|
|
},
|
|
{
|
|
Name: "Bad (NX Critical)",
|
|
Domain: "nx.critical.com",
|
|
Present: true,
|
|
Valid: false,
|
|
},
|
|
{
|
|
Name: "Good (absent)",
|
|
Domain: "absent.com",
|
|
Present: false,
|
|
Valid: true,
|
|
},
|
|
{
|
|
Name: "Good (example.co.uk, absent)",
|
|
Domain: "example.co.uk",
|
|
Present: false,
|
|
Valid: true,
|
|
},
|
|
{
|
|
Name: "Good (present and valid)",
|
|
Domain: "present.com",
|
|
Present: true,
|
|
Valid: true,
|
|
},
|
|
{
|
|
Name: "Good (present w/ servfail exception?)",
|
|
Domain: "present.servfail.com",
|
|
Present: true,
|
|
Valid: true,
|
|
},
|
|
{
|
|
Name: "Good (multiple critical, one matching)",
|
|
Domain: "multi-crit-present.com",
|
|
Present: true,
|
|
Valid: true,
|
|
},
|
|
{
|
|
Name: "Bad (unknown critical)",
|
|
Domain: "unknown-critical.com",
|
|
Present: true,
|
|
Valid: false,
|
|
},
|
|
{
|
|
Name: "Bad (unknown critical 2)",
|
|
Domain: "unknown-critical2.com",
|
|
Present: true,
|
|
Valid: false,
|
|
},
|
|
{
|
|
Name: "Good (unknown non-critical, no issue/issuewild)",
|
|
Domain: "unknown-noncritical.com",
|
|
Present: true,
|
|
Valid: true,
|
|
},
|
|
{
|
|
Name: "Good (issue rec with unknown params)",
|
|
Domain: "present-with-parameter.com",
|
|
Present: true,
|
|
Valid: true,
|
|
},
|
|
{
|
|
Name: "Bad (issue rec with invalid tag)",
|
|
Domain: "present-with-invalid-tag.com",
|
|
Present: true,
|
|
Valid: false,
|
|
},
|
|
{
|
|
Name: "Bad (issue rec with invalid value)",
|
|
Domain: "present-with-invalid-value.com",
|
|
Present: true,
|
|
Valid: false,
|
|
},
|
|
{
|
|
Name: "Bad (restricts to dns-01, but tested with http-01)",
|
|
Domain: "present-dns-only.com",
|
|
Present: true,
|
|
Valid: false,
|
|
},
|
|
{
|
|
Name: "Good (restricts to http-01, tested with http-01)",
|
|
Domain: "present-http-only.com",
|
|
Present: true,
|
|
Valid: true,
|
|
},
|
|
{
|
|
Name: "Good (restricts to http-01 or dns-01, tested with http-01)",
|
|
Domain: "present-http-or-dns.com",
|
|
Present: true,
|
|
Valid: true,
|
|
},
|
|
{
|
|
Name: "Good (restricts to accounturi, tested with correct account)",
|
|
Domain: "present-correct-accounturi.com",
|
|
Present: true,
|
|
Valid: true,
|
|
},
|
|
{
|
|
Name: "Good (restricts to http-01 and accounturi, tested with correct account)",
|
|
Domain: "present-http-only-correct-accounturi.com",
|
|
Present: true,
|
|
Valid: true,
|
|
},
|
|
{
|
|
Name: "Bad (restricts to dns-01 and accounturi, tested with http-01)",
|
|
Domain: "present-dns-only-correct-accounturi.com",
|
|
Present: true,
|
|
Valid: false,
|
|
},
|
|
{
|
|
Name: "Bad (restricts to http-01 and accounturi, tested with incorrect account)",
|
|
Domain: "present-http-only-incorrect-accounturi.com",
|
|
Present: true,
|
|
Valid: false,
|
|
},
|
|
{
|
|
Name: "Bad (restricts to accounturi, tested with incorrect account)",
|
|
Domain: "present-incorrect-accounturi.com",
|
|
Present: true,
|
|
Valid: false,
|
|
},
|
|
{
|
|
Name: "Good (restricts to multiple accounturi, tested with a correct account)",
|
|
Domain: "present-multiple-accounturi.com",
|
|
Present: true,
|
|
Valid: true,
|
|
},
|
|
{
|
|
Name: "Bad (unsatisfiable issue record)",
|
|
Domain: "unsatisfiable.com",
|
|
Present: true,
|
|
Valid: false,
|
|
},
|
|
{
|
|
Name: "Bad (unsatisfiable issue, wildcard)",
|
|
Domain: "*.unsatisfiable.com",
|
|
Present: true,
|
|
Valid: false,
|
|
},
|
|
{
|
|
Name: "Bad (unsatisfiable wildcard)",
|
|
Domain: "*.unsatisfiable-wildcard.com",
|
|
Present: true,
|
|
Valid: false,
|
|
},
|
|
{
|
|
Name: "Bad (unsatisfiable wildcard override)",
|
|
Domain: "*.unsatisfiable-wildcard-override.com",
|
|
Present: true,
|
|
Valid: false,
|
|
},
|
|
{
|
|
Name: "Good (satisfiable wildcard)",
|
|
Domain: "*.satisfiable-wildcard.com",
|
|
Present: true,
|
|
Valid: true,
|
|
},
|
|
{
|
|
Name: "Good (multiple issuewild, one satisfiable)",
|
|
Domain: "*.satisfiable-multi-wildcard.com",
|
|
Present: true,
|
|
Valid: true,
|
|
},
|
|
{
|
|
Name: "Good (satisfiable wildcard override)",
|
|
Domain: "*.satisfiable-wildcard-override.com",
|
|
Present: true,
|
|
Valid: true,
|
|
},
|
|
}
|
|
|
|
accountURIID := int64(123)
|
|
method := "http-01"
|
|
params := &caaParams{accountURIID: &accountURIID, validationMethod: &method}
|
|
|
|
va, _ := setup(nil, 0, "", nil)
|
|
if err := features.Set(map[string]bool{"CAAValidationMethods": true, "CAAAccountURI": true}); err != nil {
|
|
t.Fatalf("Failed to enable feature: %v", err)
|
|
}
|
|
|
|
va.dnsClient = caaMockDNS{}
|
|
va.accountURIPrefixes = []string{"https://letsencrypt.org/acct/reg/"}
|
|
for _, caaTest := range testCases {
|
|
mockLog := va.log.(*blog.Mock)
|
|
mockLog.Clear()
|
|
t.Run(caaTest.Name, func(t *testing.T) {
|
|
ident := core.AcmeIdentifier{Type: "dns", Value: caaTest.Domain}
|
|
present, valid, _, err := va.checkCAARecords(ctx, ident, params)
|
|
if err != nil {
|
|
t.Errorf("checkCAARecords error for %s: %s", caaTest.Domain, err)
|
|
}
|
|
if present != caaTest.Present {
|
|
t.Errorf("checkCAARecords presence mismatch for %s: got %t expected %t", caaTest.Domain, present, caaTest.Present)
|
|
}
|
|
if valid != caaTest.Valid {
|
|
t.Errorf("checkCAARecords validity mismatch for %s: got %t expected %t", caaTest.Domain, valid, caaTest.Valid)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Reset to disable CAAValidationMethods/CAAAccountURI.
|
|
features.Reset()
|
|
|
|
// present-dns-only.com should now be valid even with http-01
|
|
ident := core.AcmeIdentifier{Type: "dns", Value: "present-dns-only.com"}
|
|
present, valid, _, err := va.checkCAARecords(ctx, ident, params)
|
|
test.AssertNotError(t, err, "present-dns-only.com")
|
|
test.Assert(t, present, "Present should be true")
|
|
test.Assert(t, valid, "Valid should be true")
|
|
|
|
// present-incorrect-accounturi.com should now be also be valid
|
|
ident = core.AcmeIdentifier{Type: "dns", Value: "present-incorrect-accounturi.com"}
|
|
present, valid, _, err = va.checkCAARecords(ctx, ident, params)
|
|
test.AssertNotError(t, err, "present-incorrect-accounturi.com")
|
|
test.Assert(t, present, "Present should be true")
|
|
test.Assert(t, valid, "Valid should be true")
|
|
|
|
// nil params should be valid, too
|
|
present, valid, _, err = va.checkCAARecords(ctx, ident, nil)
|
|
test.AssertNotError(t, err, "present-dns-only.com")
|
|
test.Assert(t, present, "Present should be true")
|
|
test.Assert(t, valid, "Valid should be true")
|
|
|
|
ident.Value = "servfail.com"
|
|
present, valid, _, err = va.checkCAARecords(ctx, ident, nil)
|
|
test.AssertError(t, err, "servfail.com")
|
|
test.Assert(t, !present, "Present should be false")
|
|
test.Assert(t, !valid, "Valid should be false")
|
|
|
|
if _, _, _, err := va.checkCAARecords(ctx, ident, nil); err == nil {
|
|
t.Errorf("Should have returned error on CAA lookup, but did not: %s", ident.Value)
|
|
}
|
|
|
|
ident.Value = "servfail.present.com"
|
|
present, valid, _, err = va.checkCAARecords(ctx, ident, nil)
|
|
test.AssertError(t, err, "servfail.present.com")
|
|
test.Assert(t, !present, "Present should be false")
|
|
test.Assert(t, !valid, "Valid should be false")
|
|
|
|
if _, _, _, err := va.checkCAARecords(ctx, ident, nil); err == nil {
|
|
t.Errorf("Should have returned error on CAA lookup, but did not: %s", ident.Value)
|
|
}
|
|
}
|
|
|
|
func TestCAALogging(t *testing.T) {
|
|
va, _ := setup(nil, 0, "", nil)
|
|
va.dnsClient = caaMockDNS{}
|
|
|
|
httpChal := core.ChallengeTypeHTTP01
|
|
dnsChal := core.ChallengeTypeDNS01
|
|
acctID := int64(12345)
|
|
|
|
testCases := []struct {
|
|
Name string
|
|
Domain string
|
|
AccountURIID *int64
|
|
ChallengeType *string
|
|
ExpectedLogline string
|
|
}{
|
|
{
|
|
Domain: "reserved.com",
|
|
ChallengeType: nil,
|
|
ExpectedLogline: "INFO: [AUDIT] Checked CAA records for reserved.com, [Present: true, Account ID: unknown, Challenge: unknown, Valid for issuance: false] Records=[{\"Hdr\":{\"Name\":\"\",\"Rrtype\":0,\"Class\":0,\"Ttl\":0,\"Rdlength\":0},\"Flag\":0,\"Tag\":\"issue\",\"Value\":\"ca.com\"}]",
|
|
},
|
|
{
|
|
Domain: "reserved.com",
|
|
AccountURIID: &acctID,
|
|
ChallengeType: &httpChal,
|
|
ExpectedLogline: "INFO: [AUDIT] Checked CAA records for reserved.com, [Present: true, Account ID: 12345, Challenge: http-01, Valid for issuance: false] Records=[{\"Hdr\":{\"Name\":\"\",\"Rrtype\":0,\"Class\":0,\"Ttl\":0,\"Rdlength\":0},\"Flag\":0,\"Tag\":\"issue\",\"Value\":\"ca.com\"}]",
|
|
},
|
|
{
|
|
Domain: "reserved.com",
|
|
AccountURIID: &acctID,
|
|
ChallengeType: &dnsChal,
|
|
ExpectedLogline: "INFO: [AUDIT] Checked CAA records for reserved.com, [Present: true, Account ID: 12345, Challenge: dns-01, Valid for issuance: false] Records=[{\"Hdr\":{\"Name\":\"\",\"Rrtype\":0,\"Class\":0,\"Ttl\":0,\"Rdlength\":0},\"Flag\":0,\"Tag\":\"issue\",\"Value\":\"ca.com\"}]",
|
|
},
|
|
{
|
|
Domain: "mixedcase.com",
|
|
AccountURIID: &acctID,
|
|
ChallengeType: &httpChal,
|
|
ExpectedLogline: "INFO: [AUDIT] Checked CAA records for mixedcase.com, [Present: true, Account ID: 12345, Challenge: http-01, Valid for issuance: false] Records=[{\"Hdr\":{\"Name\":\"\",\"Rrtype\":0,\"Class\":0,\"Ttl\":0,\"Rdlength\":0},\"Flag\":0,\"Tag\":\"iSsUe\",\"Value\":\"ca.com\"}]",
|
|
},
|
|
{
|
|
Domain: "critical.com",
|
|
AccountURIID: &acctID,
|
|
ChallengeType: &httpChal,
|
|
ExpectedLogline: "INFO: [AUDIT] Checked CAA records for critical.com, [Present: true, Account ID: 12345, Challenge: http-01, Valid for issuance: false] Records=[{\"Hdr\":{\"Name\":\"\",\"Rrtype\":0,\"Class\":0,\"Ttl\":0,\"Rdlength\":0},\"Flag\":1,\"Tag\":\"issue\",\"Value\":\"ca.com\"}]",
|
|
},
|
|
{
|
|
Domain: "present.com",
|
|
AccountURIID: &acctID,
|
|
ChallengeType: &httpChal,
|
|
ExpectedLogline: "INFO: [AUDIT] Checked CAA records for present.com, [Present: true, Account ID: 12345, Challenge: http-01, Valid for issuance: true] Records=[{\"Hdr\":{\"Name\":\"\",\"Rrtype\":0,\"Class\":0,\"Ttl\":0,\"Rdlength\":0},\"Flag\":0,\"Tag\":\"issue\",\"Value\":\"letsencrypt.org\"}]",
|
|
},
|
|
{
|
|
Domain: "multi-crit-present.com",
|
|
AccountURIID: &acctID,
|
|
ChallengeType: &httpChal,
|
|
ExpectedLogline: "INFO: [AUDIT] Checked CAA records for multi-crit-present.com, [Present: true, Account ID: 12345, Challenge: http-01, Valid for issuance: true] Records=[{\"Hdr\":{\"Name\":\"\",\"Rrtype\":0,\"Class\":0,\"Ttl\":0,\"Rdlength\":0},\"Flag\":1,\"Tag\":\"issue\",\"Value\":\"ca.com\"},{\"Hdr\":{\"Name\":\"\",\"Rrtype\":0,\"Class\":0,\"Ttl\":0,\"Rdlength\":0},\"Flag\":1,\"Tag\":\"issue\",\"Value\":\"letsencrypt.org\"}]",
|
|
},
|
|
{
|
|
Domain: "present-with-parameter.com",
|
|
AccountURIID: &acctID,
|
|
ChallengeType: &httpChal,
|
|
ExpectedLogline: "INFO: [AUDIT] Checked CAA records for present-with-parameter.com, [Present: true, Account ID: 12345, Challenge: http-01, Valid for issuance: true] Records=[{\"Hdr\":{\"Name\":\"\",\"Rrtype\":0,\"Class\":0,\"Ttl\":0,\"Rdlength\":0},\"Flag\":0,\"Tag\":\"issue\",\"Value\":\" letsencrypt.org ;foo=bar;baz=bar\"}]",
|
|
},
|
|
{
|
|
Domain: "satisfiable-wildcard-override.com",
|
|
AccountURIID: &acctID,
|
|
ChallengeType: &httpChal,
|
|
ExpectedLogline: "INFO: [AUDIT] Checked CAA records for satisfiable-wildcard-override.com, [Present: true, Account ID: 12345, Challenge: http-01, Valid for issuance: false] Records=[{\"Hdr\":{\"Name\":\"\",\"Rrtype\":0,\"Class\":0,\"Ttl\":0,\"Rdlength\":0},\"Flag\":0,\"Tag\":\"issue\",\"Value\":\"ca.com\"},{\"Hdr\":{\"Name\":\"\",\"Rrtype\":0,\"Class\":0,\"Ttl\":0,\"Rdlength\":0},\"Flag\":0,\"Tag\":\"issuewild\",\"Value\":\"letsencrypt.org\"}]",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.Domain, func(t *testing.T) {
|
|
mockLog := va.log.(*blog.Mock)
|
|
mockLog.Clear()
|
|
|
|
params := &caaParams{
|
|
accountURIID: tc.AccountURIID,
|
|
validationMethod: tc.ChallengeType,
|
|
}
|
|
_ = va.checkCAA(ctx, core.AcmeIdentifier{Type: core.IdentifierDNS, Value: tc.Domain}, params)
|
|
|
|
caaLogLines := mockLog.GetAllMatching(`Checked CAA records for`)
|
|
if len(caaLogLines) != 1 {
|
|
t.Errorf("checkCAARecords didn't audit log CAA record info. Instead got:\n%s\n",
|
|
strings.Join(mockLog.GetAllMatching(`.*`), "\n"))
|
|
} else {
|
|
test.AssertEquals(t, caaLogLines[0], tc.ExpectedLogline)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestIsCAAValidErrMessage tests that an error result from `va.IsCAAValid`
|
|
// includes the domain name that was being checked in the failure detail.
|
|
func TestIsCAAValidErrMessage(t *testing.T) {
|
|
va, _ := setup(nil, 0, "", nil)
|
|
va.dnsClient = caaMockDNS{}
|
|
|
|
// Call IsCAAValid with a domain we know fails with a generic error from the
|
|
// caaMockDNS.
|
|
domain := "caa-timeout.com"
|
|
resp, err := va.IsCAAValid(ctx, &vapb.IsCAAValidRequest{
|
|
Domain: &domain,
|
|
})
|
|
|
|
// The lookup itself should not return an error
|
|
test.AssertNotError(t, err, "Unexpected error calling IsCAAValidRequest")
|
|
// The result should not be nil
|
|
test.AssertNotNil(t, resp, "Response to IsCAAValidRequest was nil")
|
|
// The result's Problem should not be nil
|
|
test.AssertNotNil(t, resp.Problem, "Response Problem was nil")
|
|
// The result's Problem should be an error message that includes the domain.
|
|
test.AssertEquals(t, *resp.Problem.Detail, fmt.Sprintf("While processing CAA for %s: error", domain))
|
|
}
|
|
|
|
func TestCAAFailure(t *testing.T) {
|
|
chall := createChallenge(core.ChallengeTypeHTTP01)
|
|
hs := httpSrv(t, chall.Token)
|
|
defer hs.Close()
|
|
|
|
va, _ := setup(hs, 0, "", nil)
|
|
va.dnsClient = caaMockDNS{}
|
|
|
|
_, prob := va.validate(ctx, dnsi("reserved.com"), chall, core.Authorization{})
|
|
if prob == nil {
|
|
t.Fatalf("Expected CAA rejection for reserved.com, got success")
|
|
}
|
|
test.AssertEquals(t, prob.Type, probs.CAAProblem)
|
|
}
|
|
|
|
func TestParseResults(t *testing.T) {
|
|
r := []caaResult{}
|
|
s, records, err := parseResults(r)
|
|
test.Assert(t, s == nil, "set is not nil")
|
|
test.Assert(t, err == nil, "error is not nil")
|
|
test.Assert(t, records == nil, "records is not nil")
|
|
test.AssertNotError(t, err, "no error should be returned")
|
|
r = []caaResult{{nil, errors.New("")}, {[]*dns.CAA{{Value: "test"}}, nil}}
|
|
s, records, err = parseResults(r)
|
|
test.Assert(t, s == nil, "set is not nil")
|
|
test.AssertEquals(t, err.Error(), "")
|
|
expected := dns.CAA{Value: "other-test"}
|
|
test.AssertEquals(t, len(records), 0)
|
|
r = []caaResult{{[]*dns.CAA{&expected}, nil}, {[]*dns.CAA{{Value: "test"}}, nil}}
|
|
s, records, err = parseResults(r)
|
|
test.AssertEquals(t, len(s.Unknown), 1)
|
|
test.Assert(t, s.Unknown[0] == &expected, "Incorrect record returned")
|
|
test.AssertNotError(t, err, "no error should be returned")
|
|
test.AssertEquals(t, len(records), len(r[0].records))
|
|
for i, rec := range records {
|
|
test.AssertEquals(t, rec.String(), r[0].records[i].String())
|
|
}
|
|
}
|
|
|
|
func TestCheckAccountURI(t *testing.T) {
|
|
tests := []struct {
|
|
uri string
|
|
prefixes []string
|
|
id int64
|
|
want bool
|
|
}{
|
|
{
|
|
uri: "https://acme-v01.api.letsencrypt.org/acme/reg/123456",
|
|
prefixes: []string{
|
|
"https://acme-v01.api.letsencrypt.org/acme/reg/",
|
|
},
|
|
id: 123456,
|
|
want: true,
|
|
},
|
|
{
|
|
uri: "https://acme-v01.api.letsencrypt.org/acme/reg/123456",
|
|
prefixes: []string{
|
|
"https://acme-v01.api.letsencrypt.org/acme/reg/",
|
|
},
|
|
id: 123457,
|
|
want: false,
|
|
},
|
|
{
|
|
uri: "https://acme-staging.api.letsencrypt.org/acme/reg/123456",
|
|
prefixes: []string{
|
|
"https://acme-staging.api.letsencrypt.org/acme/reg/",
|
|
"https://acme-staging-v02.api.letsencrypt.org/acme/acct/",
|
|
},
|
|
id: 123456,
|
|
want: true,
|
|
},
|
|
{
|
|
uri: "https://acme-v02.api.letsencrypt.org/acme/acct/123456",
|
|
prefixes: []string{
|
|
"https://acme-v01.api.letsencrypt.org/acme/reg/",
|
|
"https://acme-v02.api.letsencrypt.org/acme/acct/",
|
|
},
|
|
id: 123456,
|
|
want: true,
|
|
},
|
|
{
|
|
uri: "https://acme-v02.api.letsencrypt.org/acme/acct/123456",
|
|
prefixes: []string{
|
|
"https://acme-v01.api.letsencrypt.org/acme/reg/",
|
|
"https://acme-v02.api.letsencrypt.org/acme/acct/",
|
|
"https://acme-v03.api.letsencrypt.org/acme/acct/",
|
|
},
|
|
id: 123456,
|
|
want: true,
|
|
},
|
|
{
|
|
uri: "https://acme-v02.api.letsencrypt.org/acme/acct/123456",
|
|
prefixes: []string{
|
|
"https://acme-v01.api.letsencrypt.org/acme/reg/",
|
|
"https://acme-v02.api.letsencrypt.org/acme/acct/",
|
|
},
|
|
id: 654321,
|
|
want: false,
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
if got, want := checkAccountURI(test.uri, test.prefixes, test.id), test.want; got != want {
|
|
t.Errorf("checkAccountURI(%q, %v, %d) = %v, want %v", test.uri, test.prefixes, test.id, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestContainsMethod(t *testing.T) {
|
|
test.AssertEquals(t, containsMethod("abc,123,xyz", "123"), true)
|
|
test.AssertEquals(t, containsMethod("abc,xyz", "abc"), true)
|
|
test.AssertEquals(t, containsMethod("abc,xyz", "xyz"), true)
|
|
test.AssertEquals(t, containsMethod("abc", "abc"), true)
|
|
|
|
test.AssertEquals(t, containsMethod("abc,xyz,123", "456"), false)
|
|
test.AssertEquals(t, containsMethod("abc", "123"), false)
|
|
}
|
|
|
|
func TestExtractIssuerDomainAndParameters(t *testing.T) {
|
|
tests := []struct {
|
|
value string
|
|
wantDomain string
|
|
wantParameters map[string]string
|
|
wantValid bool
|
|
}{
|
|
{
|
|
value: "",
|
|
wantDomain: "",
|
|
wantParameters: map[string]string{},
|
|
wantValid: true,
|
|
},
|
|
{
|
|
value: ";",
|
|
wantDomain: "",
|
|
wantParameters: map[string]string{},
|
|
wantValid: true,
|
|
},
|
|
{
|
|
value: " ; ",
|
|
wantDomain: "",
|
|
wantParameters: map[string]string{},
|
|
wantValid: true,
|
|
},
|
|
{
|
|
value: "letsencrypt.org",
|
|
wantDomain: "letsencrypt.org",
|
|
wantParameters: map[string]string{},
|
|
wantValid: true,
|
|
},
|
|
{
|
|
value: "letsencrypt.org;",
|
|
wantDomain: "letsencrypt.org",
|
|
wantParameters: map[string]string{},
|
|
wantValid: true,
|
|
},
|
|
{
|
|
value: " letsencrypt.org ;foo=bar;baz=bar",
|
|
wantDomain: "letsencrypt.org",
|
|
wantParameters: map[string]string{"foo": "bar", "baz": "bar"},
|
|
wantValid: true,
|
|
},
|
|
{
|
|
value: " letsencrypt.org ;foo=bar;baz=bar",
|
|
wantDomain: "letsencrypt.org",
|
|
wantParameters: map[string]string{"foo": "bar", "baz": "bar"},
|
|
wantValid: true,
|
|
},
|
|
{
|
|
value: "letsencrypt.org; foo=; baz = bar",
|
|
wantDomain: "letsencrypt.org",
|
|
wantParameters: map[string]string{"foo": "", "baz": "bar"},
|
|
wantValid: true,
|
|
},
|
|
{
|
|
value: "letsencrypt.org; foo= ; baz = bar",
|
|
wantDomain: "letsencrypt.org",
|
|
wantParameters: map[string]string{"foo": "", "baz": "bar"},
|
|
wantValid: true,
|
|
},
|
|
{
|
|
value: "letsencrypt.org; foo=b1,b2,b3 ; baz = a=b ",
|
|
wantDomain: "letsencrypt.org",
|
|
wantParameters: map[string]string{"foo": "b1,b2,b3", "baz": "a=b"},
|
|
wantValid: true,
|
|
},
|
|
{
|
|
value: "letsencrypt.org; foo=b1,b2,b3 ; baz = a = b ",
|
|
wantDomain: "letsencrypt.org",
|
|
wantValid: false,
|
|
},
|
|
{
|
|
value: "letsencrypt.org; foo=b1,b2,b3 ; baz=a= b",
|
|
wantDomain: "letsencrypt.org",
|
|
wantValid: false,
|
|
},
|
|
{
|
|
value: "letsencrypt.org; foo=b1,b2,b3 ; baz = a;b ",
|
|
wantDomain: "letsencrypt.org",
|
|
wantValid: false,
|
|
},
|
|
{
|
|
value: "letsencrypt.org; 1=2; baz=a-b",
|
|
wantDomain: "letsencrypt.org",
|
|
wantParameters: map[string]string{"1": "2", "baz": "a-b"},
|
|
wantValid: true,
|
|
},
|
|
{
|
|
value: "letsencrypt.org; a_b=123",
|
|
wantDomain: "letsencrypt.org",
|
|
wantValid: false,
|
|
},
|
|
{
|
|
value: "letsencrypt.org; ab=1 2 3",
|
|
wantDomain: "letsencrypt.org",
|
|
wantValid: false,
|
|
},
|
|
{
|
|
value: "letsencrypt.org; 1=2; a-b=c",
|
|
wantDomain: "letsencrypt.org",
|
|
wantValid: false,
|
|
},
|
|
{
|
|
value: "letsencrypt.org; foo=a\u2615b",
|
|
wantDomain: "letsencrypt.org",
|
|
wantValid: false,
|
|
},
|
|
{
|
|
value: "letsencrypt.org; foo=b1,b2,b3 baz=a",
|
|
wantDomain: "letsencrypt.org",
|
|
wantValid: false,
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run("", func(t *testing.T) {
|
|
caa := &dns.CAA{Value: test.value}
|
|
gotDomain, gotParameters, gotValid := extractIssuerDomainAndParameters(caa)
|
|
if got, want := gotValid, test.wantValid; got != want {
|
|
t.Errorf("CAA value %q - got valid %v, want %v", test.value, got, want)
|
|
}
|
|
if got, want := gotDomain, test.wantDomain; got != want {
|
|
t.Errorf("CAA value %q - got domain %q, want %q", test.value, got, want)
|
|
}
|
|
if got, want := gotParameters, test.wantParameters; !reflect.DeepEqual(got, want) {
|
|
t.Errorf("CAA value %q - got parameters %v, want %v", test.value, got, want)
|
|
}
|
|
})
|
|
}
|
|
}
|