1731 lines
57 KiB
Go
1731 lines
57 KiB
Go
package va
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"regexp"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/miekg/dns"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
|
|
"github.com/letsencrypt/boulder/bdns"
|
|
"github.com/letsencrypt/boulder/core"
|
|
berrors "github.com/letsencrypt/boulder/errors"
|
|
"github.com/letsencrypt/boulder/features"
|
|
"github.com/letsencrypt/boulder/identifier"
|
|
"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, bdns.ResolverAddrs, error) {
|
|
return nil, bdns.ResolverAddrs{"caaMockDNS"}, nil
|
|
}
|
|
|
|
func (mock caaMockDNS) LookupHost(_ context.Context, hostname string) ([]net.IP, bdns.ResolverAddrs, error) {
|
|
ip := net.ParseIP("127.0.0.1")
|
|
return []net.IP{ip}, bdns.ResolverAddrs{"caaMockDNS"}, nil
|
|
}
|
|
|
|
func (mock caaMockDNS) LookupCAA(_ context.Context, domain string) ([]*dns.CAA, string, bdns.ResolverAddrs, error) {
|
|
var results []*dns.CAA
|
|
var record dns.CAA
|
|
switch strings.TrimRight(domain, ".") {
|
|
case "caa-timeout.com":
|
|
return nil, "", bdns.ResolverAddrs{"caaMockDNS"}, 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, "", bdns.ResolverAddrs{"caaMockDNS"}, nil
|
|
case "gonetld":
|
|
return nil, "", bdns.ResolverAddrs{"caaMockDNS"}, fmt.Errorf("NXDOMAIN")
|
|
case "servfail.com", "servfail.present.com":
|
|
return results, "", bdns.ResolverAddrs{"caaMockDNS"}, 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)
|
|
}
|
|
var response string
|
|
if len(results) > 0 {
|
|
response = "foo"
|
|
}
|
|
return results, response, bdns.ResolverAddrs{"caaMockDNS"}, nil
|
|
}
|
|
|
|
func TestCAATimeout(t *testing.T) {
|
|
va, _ := setup(nil, "", nil, caaMockDNS{})
|
|
|
|
params := &caaParams{
|
|
accountURIID: 12345,
|
|
validationMethod: core.ChallengeTypeHTTP01,
|
|
}
|
|
|
|
err := va.checkCAA(ctx, identifier.NewDNS("caa-timeout.com"), params)
|
|
test.AssertErrorIs(t, err, berrors.DNS)
|
|
test.AssertContains(t, err.Error(), "error")
|
|
}
|
|
|
|
func TestCAAChecking(t *testing.T) {
|
|
testCases := []struct {
|
|
Name string
|
|
Domain string
|
|
FoundAt string
|
|
Valid bool
|
|
}{
|
|
{
|
|
Name: "Bad (Reserved)",
|
|
Domain: "reserved.com",
|
|
FoundAt: "reserved.com",
|
|
Valid: false,
|
|
},
|
|
{
|
|
Name: "Bad (Reserved, Mixed case Issue)",
|
|
Domain: "mixedcase.com",
|
|
FoundAt: "mixedcase.com",
|
|
Valid: false,
|
|
},
|
|
{
|
|
Name: "Bad (Critical)",
|
|
Domain: "critical.com",
|
|
FoundAt: "critical.com",
|
|
Valid: false,
|
|
},
|
|
{
|
|
Name: "Bad (NX Critical)",
|
|
Domain: "nx.critical.com",
|
|
FoundAt: "critical.com",
|
|
Valid: false,
|
|
},
|
|
{
|
|
Name: "Good (absent)",
|
|
Domain: "absent.com",
|
|
FoundAt: "",
|
|
Valid: true,
|
|
},
|
|
{
|
|
Name: "Good (example.co.uk, absent)",
|
|
Domain: "example.co.uk",
|
|
FoundAt: "",
|
|
Valid: true,
|
|
},
|
|
{
|
|
Name: "Good (present and valid)",
|
|
Domain: "present.com",
|
|
FoundAt: "present.com",
|
|
Valid: true,
|
|
},
|
|
{
|
|
Name: "Good (present on parent)",
|
|
Domain: "child.present.com",
|
|
FoundAt: "present.com",
|
|
Valid: true,
|
|
},
|
|
{
|
|
Name: "Good (present w/ servfail exception?)",
|
|
Domain: "present.servfail.com",
|
|
FoundAt: "present.servfail.com",
|
|
Valid: true,
|
|
},
|
|
{
|
|
Name: "Good (multiple critical, one matching)",
|
|
Domain: "multi-crit-present.com",
|
|
FoundAt: "multi-crit-present.com",
|
|
Valid: true,
|
|
},
|
|
{
|
|
Name: "Bad (unknown critical)",
|
|
Domain: "unknown-critical.com",
|
|
FoundAt: "unknown-critical.com",
|
|
Valid: false,
|
|
},
|
|
{
|
|
Name: "Bad (unknown critical 2)",
|
|
Domain: "unknown-critical2.com",
|
|
FoundAt: "unknown-critical2.com",
|
|
Valid: false,
|
|
},
|
|
{
|
|
Name: "Good (unknown non-critical, no issue)",
|
|
Domain: "unknown-noncritical.com",
|
|
FoundAt: "unknown-noncritical.com",
|
|
Valid: true,
|
|
},
|
|
{
|
|
Name: "Good (unknown non-critical, no issuewild)",
|
|
Domain: "*.unknown-noncritical.com",
|
|
FoundAt: "unknown-noncritical.com",
|
|
Valid: true,
|
|
},
|
|
{
|
|
Name: "Good (issue rec with unknown params)",
|
|
Domain: "present-with-parameter.com",
|
|
FoundAt: "present-with-parameter.com",
|
|
Valid: true,
|
|
},
|
|
{
|
|
Name: "Bad (issue rec with invalid tag)",
|
|
Domain: "present-with-invalid-tag.com",
|
|
FoundAt: "present-with-invalid-tag.com",
|
|
Valid: false,
|
|
},
|
|
{
|
|
Name: "Bad (issue rec with invalid value)",
|
|
Domain: "present-with-invalid-value.com",
|
|
FoundAt: "present-with-invalid-value.com",
|
|
Valid: false,
|
|
},
|
|
{
|
|
Name: "Bad (restricts to dns-01, but tested with http-01)",
|
|
Domain: "present-dns-only.com",
|
|
FoundAt: "present-dns-only.com",
|
|
Valid: false,
|
|
},
|
|
{
|
|
Name: "Good (restricts to http-01, tested with http-01)",
|
|
Domain: "present-http-only.com",
|
|
FoundAt: "present-http-only.com",
|
|
Valid: true,
|
|
},
|
|
{
|
|
Name: "Good (restricts to http-01 or dns-01, tested with http-01)",
|
|
Domain: "present-http-or-dns.com",
|
|
FoundAt: "present-http-or-dns.com",
|
|
Valid: true,
|
|
},
|
|
{
|
|
Name: "Good (restricts to accounturi, tested with correct account)",
|
|
Domain: "present-correct-accounturi.com",
|
|
FoundAt: "present-correct-accounturi.com",
|
|
Valid: true,
|
|
},
|
|
{
|
|
Name: "Good (restricts to http-01 and accounturi, tested with correct account)",
|
|
Domain: "present-http-only-correct-accounturi.com",
|
|
FoundAt: "present-http-only-correct-accounturi.com",
|
|
Valid: true,
|
|
},
|
|
{
|
|
Name: "Bad (restricts to dns-01 and accounturi, tested with http-01)",
|
|
Domain: "present-dns-only-correct-accounturi.com",
|
|
FoundAt: "present-dns-only-correct-accounturi.com",
|
|
Valid: false,
|
|
},
|
|
{
|
|
Name: "Bad (restricts to http-01 and accounturi, tested with incorrect account)",
|
|
Domain: "present-http-only-incorrect-accounturi.com",
|
|
FoundAt: "present-http-only-incorrect-accounturi.com",
|
|
Valid: false,
|
|
},
|
|
{
|
|
Name: "Bad (restricts to accounturi, tested with incorrect account)",
|
|
Domain: "present-incorrect-accounturi.com",
|
|
FoundAt: "present-incorrect-accounturi.com",
|
|
Valid: false,
|
|
},
|
|
{
|
|
Name: "Good (restricts to multiple accounturi, tested with a correct account)",
|
|
Domain: "present-multiple-accounturi.com",
|
|
FoundAt: "present-multiple-accounturi.com",
|
|
Valid: true,
|
|
},
|
|
{
|
|
Name: "Bad (unsatisfiable issue record)",
|
|
Domain: "unsatisfiable.com",
|
|
FoundAt: "unsatisfiable.com",
|
|
Valid: false,
|
|
},
|
|
{
|
|
Name: "Bad (unsatisfiable issue, wildcard)",
|
|
Domain: "*.unsatisfiable.com",
|
|
FoundAt: "unsatisfiable.com",
|
|
Valid: false,
|
|
},
|
|
{
|
|
Name: "Bad (unsatisfiable wildcard)",
|
|
Domain: "*.unsatisfiable-wildcard.com",
|
|
FoundAt: "unsatisfiable-wildcard.com",
|
|
Valid: false,
|
|
},
|
|
{
|
|
Name: "Bad (unsatisfiable wildcard override)",
|
|
Domain: "*.unsatisfiable-wildcard-override.com",
|
|
FoundAt: "unsatisfiable-wildcard-override.com",
|
|
Valid: false,
|
|
},
|
|
{
|
|
Name: "Good (satisfiable wildcard)",
|
|
Domain: "*.satisfiable-wildcard.com",
|
|
FoundAt: "satisfiable-wildcard.com",
|
|
Valid: true,
|
|
},
|
|
{
|
|
Name: "Good (multiple issuewild, one satisfiable)",
|
|
Domain: "*.satisfiable-multi-wildcard.com",
|
|
FoundAt: "satisfiable-multi-wildcard.com",
|
|
Valid: true,
|
|
},
|
|
{
|
|
Name: "Good (satisfiable wildcard override)",
|
|
Domain: "*.satisfiable-wildcard-override.com",
|
|
FoundAt: "satisfiable-wildcard-override.com",
|
|
Valid: true,
|
|
},
|
|
}
|
|
|
|
accountURIID := int64(123)
|
|
method := core.ChallengeTypeHTTP01
|
|
params := &caaParams{accountURIID: accountURIID, validationMethod: method}
|
|
|
|
va, _ := setup(nil, "", nil, caaMockDNS{})
|
|
va.accountURIPrefixes = []string{"https://letsencrypt.org/acct/reg/"}
|
|
|
|
for _, caaTest := range testCases {
|
|
mockLog := va.log.(*blog.Mock)
|
|
defer mockLog.Clear()
|
|
t.Run(caaTest.Name, func(t *testing.T) {
|
|
ident := identifier.NewDNS(caaTest.Domain)
|
|
foundAt, valid, _, err := va.checkCAARecords(ctx, ident, params)
|
|
if err != nil {
|
|
t.Errorf("checkCAARecords error for %s: %s", caaTest.Domain, err)
|
|
}
|
|
if foundAt != caaTest.FoundAt {
|
|
t.Errorf("checkCAARecords presence mismatch for %s: got %q expected %q", caaTest.Domain, foundAt, caaTest.FoundAt)
|
|
}
|
|
if valid != caaTest.Valid {
|
|
t.Errorf("checkCAARecords validity mismatch for %s: got %t expected %t", caaTest.Domain, valid, caaTest.Valid)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCAALogging(t *testing.T) {
|
|
va, _ := setup(nil, "", nil, caaMockDNS{})
|
|
|
|
testCases := []struct {
|
|
Name string
|
|
Domain string
|
|
AccountURIID int64
|
|
ChallengeType core.AcmeChallenge
|
|
ExpectedLogline string
|
|
}{
|
|
{
|
|
Domain: "reserved.com",
|
|
AccountURIID: 12345,
|
|
ChallengeType: core.ChallengeTypeHTTP01,
|
|
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\"",
|
|
},
|
|
{
|
|
Domain: "reserved.com",
|
|
AccountURIID: 12345,
|
|
ChallengeType: core.ChallengeTypeDNS01,
|
|
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\"",
|
|
},
|
|
{
|
|
Domain: "mixedcase.com",
|
|
AccountURIID: 12345,
|
|
ChallengeType: core.ChallengeTypeHTTP01,
|
|
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\"",
|
|
},
|
|
{
|
|
Domain: "critical.com",
|
|
AccountURIID: 12345,
|
|
ChallengeType: core.ChallengeTypeHTTP01,
|
|
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\"",
|
|
},
|
|
{
|
|
Domain: "present.com",
|
|
AccountURIID: 12345,
|
|
ChallengeType: core.ChallengeTypeHTTP01,
|
|
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\"",
|
|
},
|
|
{
|
|
Domain: "not.here.but.still.present.com",
|
|
AccountURIID: 12345,
|
|
ChallengeType: core.ChallengeTypeHTTP01,
|
|
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\"",
|
|
},
|
|
{
|
|
Domain: "multi-crit-present.com",
|
|
AccountURIID: 12345,
|
|
ChallengeType: core.ChallengeTypeHTTP01,
|
|
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\"",
|
|
},
|
|
{
|
|
Domain: "present-with-parameter.com",
|
|
AccountURIID: 12345,
|
|
ChallengeType: core.ChallengeTypeHTTP01,
|
|
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\"",
|
|
},
|
|
{
|
|
Domain: "satisfiable-wildcard-override.com",
|
|
AccountURIID: 12345,
|
|
ChallengeType: core.ChallengeTypeHTTP01,
|
|
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\"",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.Domain, func(t *testing.T) {
|
|
mockLog := va.log.(*blog.Mock)
|
|
defer mockLog.Clear()
|
|
|
|
params := &caaParams{
|
|
accountURIID: tc.AccountURIID,
|
|
validationMethod: tc.ChallengeType,
|
|
}
|
|
_ = va.checkCAA(ctx, identifier.NewDNS(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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDoCAAErrMessage tests that an error result from `va.IsCAAValid`
|
|
// includes the domain name that was being checked in the failure detail.
|
|
func TestDoCAAErrMessage(t *testing.T) {
|
|
t.Parallel()
|
|
va, _ := setup(nil, "", nil, caaMockDNS{})
|
|
|
|
// Call the operation with a domain we know fails with a generic error from the
|
|
// caaMockDNS.
|
|
domain := "caa-timeout.com"
|
|
resp, err := va.DoCAA(ctx, &vapb.IsCAAValidRequest{
|
|
Identifier: identifier.NewDNS(domain).ToProto(),
|
|
ValidationMethod: string(core.ChallengeTypeHTTP01),
|
|
AccountURIID: 12345,
|
|
})
|
|
|
|
// 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))
|
|
}
|
|
|
|
// TestDoCAAParams tests that the IsCAAValid method rejects any requests
|
|
// which do not have the necessary parameters to do CAA Account and Method
|
|
// Binding checks.
|
|
func TestDoCAAParams(t *testing.T) {
|
|
t.Parallel()
|
|
va, _ := setup(nil, "", nil, caaMockDNS{})
|
|
|
|
// Calling IsCAAValid without a ValidationMethod should fail.
|
|
_, err := va.DoCAA(ctx, &vapb.IsCAAValidRequest{
|
|
Identifier: identifier.NewDNS("present.com").ToProto(),
|
|
AccountURIID: 12345,
|
|
})
|
|
test.AssertError(t, err, "calling IsCAAValid without a ValidationMethod")
|
|
|
|
// Calling IsCAAValid with an invalid ValidationMethod should fail.
|
|
_, err = va.DoCAA(ctx, &vapb.IsCAAValidRequest{
|
|
Identifier: identifier.NewDNS("present.com").ToProto(),
|
|
ValidationMethod: "tls-sni-01",
|
|
AccountURIID: 12345,
|
|
})
|
|
test.AssertError(t, err, "calling IsCAAValid with a bad ValidationMethod")
|
|
|
|
// Calling IsCAAValid without an AccountURIID should fail.
|
|
_, err = va.DoCAA(ctx, &vapb.IsCAAValidRequest{
|
|
Identifier: identifier.NewDNS("present.com").ToProto(),
|
|
ValidationMethod: string(core.ChallengeTypeHTTP01),
|
|
})
|
|
test.AssertError(t, err, "calling IsCAAValid without an AccountURIID")
|
|
|
|
// Calling IsCAAValid with a non-DNS identifier type should fail.
|
|
_, err = va.DoCAA(ctx, &vapb.IsCAAValidRequest{
|
|
Identifier: identifier.NewIP(netip.MustParseAddr("127.0.0.1")).ToProto(),
|
|
ValidationMethod: string(core.ChallengeTypeHTTP01),
|
|
AccountURIID: 12345,
|
|
})
|
|
test.AssertError(t, err, "calling IsCAAValid with a non-DNS identifier type")
|
|
}
|
|
|
|
var errCAABrokenDNSClient = errors.New("dnsClient is broken")
|
|
|
|
// caaBrokenDNS implements the `dns.DNSClient` interface, but always returns
|
|
// errors.
|
|
type caaBrokenDNS struct{}
|
|
|
|
func (b caaBrokenDNS) LookupTXT(_ context.Context, hostname string) ([]string, bdns.ResolverAddrs, error) {
|
|
return nil, bdns.ResolverAddrs{"caaBrokenDNS"}, errCAABrokenDNSClient
|
|
}
|
|
|
|
func (b caaBrokenDNS) LookupHost(_ context.Context, hostname string) ([]net.IP, bdns.ResolverAddrs, error) {
|
|
return nil, bdns.ResolverAddrs{"caaBrokenDNS"}, errCAABrokenDNSClient
|
|
}
|
|
|
|
func (b caaBrokenDNS) LookupCAA(_ context.Context, domain string) ([]*dns.CAA, string, bdns.ResolverAddrs, error) {
|
|
return nil, "", bdns.ResolverAddrs{"caaBrokenDNS"}, errCAABrokenDNSClient
|
|
}
|
|
|
|
// caaHijackedDNS implements the `dns.DNSClient` interface with a set of useful
|
|
// test answers for CAA queries. It returns alternate CAA records than what
|
|
// caaMockDNS returns simulating either a BGP hijack or DNS records that have
|
|
// changed while queries were inflight.
|
|
type caaHijackedDNS struct{}
|
|
|
|
func (h caaHijackedDNS) LookupTXT(_ context.Context, hostname string) ([]string, bdns.ResolverAddrs, error) {
|
|
return nil, bdns.ResolverAddrs{"caaHijackedDNS"}, nil
|
|
}
|
|
|
|
func (h caaHijackedDNS) LookupHost(_ context.Context, hostname string) ([]net.IP, bdns.ResolverAddrs, error) {
|
|
ip := net.ParseIP("127.0.0.1")
|
|
return []net.IP{ip}, bdns.ResolverAddrs{"caaHijackedDNS"}, nil
|
|
}
|
|
func (h caaHijackedDNS) LookupCAA(_ context.Context, domain string) ([]*dns.CAA, string, bdns.ResolverAddrs, error) {
|
|
// These records are altered from their caaMockDNS counterparts. Use this to
|
|
// tickle remoteValidationFailures.
|
|
var results []*dns.CAA
|
|
var record dns.CAA
|
|
switch strings.TrimRight(domain, ".") {
|
|
case "present.com", "present.servfail.com":
|
|
record.Tag = "issue"
|
|
record.Value = "other-ca.com"
|
|
results = append(results, &record)
|
|
case "present-dns-only.com":
|
|
return results, "", bdns.ResolverAddrs{"caaHijackedDNS"}, fmt.Errorf("SERVFAIL")
|
|
case "satisfiable-wildcard.com":
|
|
record.Tag = "issuewild"
|
|
record.Value = ";"
|
|
results = append(results, &record)
|
|
secondRecord := record
|
|
secondRecord.Tag = "issue"
|
|
secondRecord.Value = ";"
|
|
results = append(results, &secondRecord)
|
|
}
|
|
var response string
|
|
if len(results) > 0 {
|
|
response = "foo"
|
|
}
|
|
return results, response, bdns.ResolverAddrs{"caaHijackedDNS"}, nil
|
|
}
|
|
|
|
// parseValidationLogEvent extracts ... from JSON={ ... } in a ValidateChallenge
|
|
// audit log and returns it as a validationLogEvent struct.
|
|
func parseValidationLogEvent(t *testing.T, log []string) validationLogEvent {
|
|
re := regexp.MustCompile(`JSON=\{.*\}`)
|
|
var audit validationLogEvent
|
|
for _, line := range log {
|
|
match := re.FindString(line)
|
|
if match != "" {
|
|
jsonStr := match[len(`JSON=`):]
|
|
if err := json.Unmarshal([]byte(jsonStr), &audit); err != nil {
|
|
t.Fatalf("Failed to parse JSON: %v", err)
|
|
}
|
|
return audit
|
|
}
|
|
}
|
|
t.Fatal("JSON not found in log")
|
|
return audit
|
|
}
|
|
|
|
func TestMultiCAARechecking(t *testing.T) {
|
|
// The remote differential log order is non-deterministic, so let's use
|
|
// the same UA for all applicable RVAs.
|
|
const (
|
|
localUA = "local"
|
|
remoteUA = "remote"
|
|
brokenUA = "broken"
|
|
hijackedUA = "hijacked"
|
|
)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
ident identifier.ACMEIdentifier
|
|
remoteVAs []remoteConf
|
|
expectedProbSubstring string
|
|
expectedProbType probs.ProblemType
|
|
expectedDiffLogSubstring string
|
|
expectedSummary *mpicSummary
|
|
expectedLabels prometheus.Labels
|
|
localDNSClient bdns.Client
|
|
}{
|
|
{
|
|
name: "all VAs functional, no CAA records",
|
|
ident: identifier.NewDNS("present-dns-only.com"),
|
|
localDNSClient: caaMockDNS{},
|
|
remoteVAs: []remoteConf{
|
|
{ua: remoteUA, rir: arin},
|
|
{ua: remoteUA, rir: ripe},
|
|
{ua: remoteUA, rir: apnic},
|
|
},
|
|
expectedLabels: prometheus.Labels{
|
|
"operation": opCAA,
|
|
"perspective": allPerspectives,
|
|
"challenge_type": string(core.ChallengeTypeDNS01),
|
|
"problem_type": "",
|
|
"result": pass,
|
|
},
|
|
},
|
|
{
|
|
name: "broken localVA, RVAs functional, no CAA records",
|
|
ident: identifier.NewDNS("present-dns-only.com"),
|
|
localDNSClient: caaBrokenDNS{},
|
|
expectedProbSubstring: "While processing CAA for present-dns-only.com: dnsClient is broken",
|
|
expectedProbType: probs.DNSProblem,
|
|
remoteVAs: []remoteConf{
|
|
{ua: remoteUA, rir: arin},
|
|
{ua: remoteUA, rir: ripe},
|
|
{ua: remoteUA, rir: apnic},
|
|
},
|
|
expectedLabels: prometheus.Labels{
|
|
"operation": opCAA,
|
|
"perspective": allPerspectives,
|
|
"challenge_type": string(core.ChallengeTypeDNS01),
|
|
"problem_type": string(probs.DNSProblem),
|
|
"result": fail,
|
|
},
|
|
},
|
|
{
|
|
name: "functional localVA, 1 broken RVA, no CAA records",
|
|
ident: identifier.NewDNS("present-dns-only.com"),
|
|
localDNSClient: caaMockDNS{},
|
|
expectedDiffLogSubstring: `"RemoteSuccesses":2,"RemoteFailures":1`,
|
|
expectedSummary: &mpicSummary{
|
|
Passed: []string{"dc-1-RIPE", "dc-2-APNIC"},
|
|
Failed: []string{"dc-0-ARIN"},
|
|
PassedRIRs: []string{ripe, apnic},
|
|
QuorumResult: "2/3",
|
|
},
|
|
remoteVAs: []remoteConf{
|
|
{ua: brokenUA, rir: arin, dns: caaBrokenDNS{}},
|
|
{ua: remoteUA, rir: ripe},
|
|
{ua: remoteUA, rir: apnic},
|
|
},
|
|
expectedLabels: prometheus.Labels{
|
|
"operation": opCAA,
|
|
"perspective": allPerspectives,
|
|
"challenge_type": string(core.ChallengeTypeDNS01),
|
|
"problem_type": "",
|
|
"result": pass,
|
|
},
|
|
},
|
|
{
|
|
name: "functional localVA, 2 broken RVA, no CAA records",
|
|
ident: identifier.NewDNS("present-dns-only.com"),
|
|
expectedProbSubstring: "During secondary validation: While processing CAA",
|
|
expectedProbType: probs.DNSProblem,
|
|
expectedDiffLogSubstring: `"RemoteSuccesses":1,"RemoteFailures":2`,
|
|
expectedSummary: &mpicSummary{
|
|
Passed: []string{"dc-2-APNIC"},
|
|
Failed: []string{"dc-0-ARIN", "dc-1-RIPE"},
|
|
PassedRIRs: []string{apnic},
|
|
QuorumResult: "1/3",
|
|
},
|
|
localDNSClient: caaMockDNS{},
|
|
remoteVAs: []remoteConf{
|
|
{ua: brokenUA, rir: arin, dns: caaBrokenDNS{}},
|
|
{ua: brokenUA, rir: ripe, dns: caaBrokenDNS{}},
|
|
{ua: remoteUA, rir: apnic},
|
|
},
|
|
expectedLabels: prometheus.Labels{
|
|
"operation": opCAA,
|
|
"perspective": allPerspectives,
|
|
"challenge_type": string(core.ChallengeTypeDNS01),
|
|
"problem_type": string(probs.DNSProblem),
|
|
"result": fail,
|
|
},
|
|
},
|
|
{
|
|
name: "functional localVA, all broken RVAs, no CAA records",
|
|
ident: identifier.NewDNS("present-dns-only.com"),
|
|
expectedProbSubstring: "During secondary validation: While processing CAA",
|
|
expectedProbType: probs.DNSProblem,
|
|
expectedDiffLogSubstring: `"RemoteSuccesses":0,"RemoteFailures":3`,
|
|
expectedSummary: &mpicSummary{
|
|
Passed: []string{},
|
|
Failed: []string{"dc-0-ARIN", "dc-1-RIPE", "dc-2-APNIC"},
|
|
PassedRIRs: []string{},
|
|
QuorumResult: "0/3",
|
|
},
|
|
localDNSClient: caaMockDNS{},
|
|
remoteVAs: []remoteConf{
|
|
{ua: brokenUA, rir: arin, dns: caaBrokenDNS{}},
|
|
{ua: brokenUA, rir: ripe, dns: caaBrokenDNS{}},
|
|
{ua: brokenUA, rir: apnic, dns: caaBrokenDNS{}},
|
|
},
|
|
expectedLabels: prometheus.Labels{
|
|
"operation": opCAA,
|
|
"perspective": allPerspectives,
|
|
"challenge_type": string(core.ChallengeTypeDNS01),
|
|
"problem_type": string(probs.DNSProblem),
|
|
"result": fail,
|
|
},
|
|
},
|
|
{
|
|
name: "all VAs functional, CAA issue type present",
|
|
ident: identifier.NewDNS("present.com"),
|
|
localDNSClient: caaMockDNS{},
|
|
remoteVAs: []remoteConf{
|
|
{ua: remoteUA, rir: arin},
|
|
{ua: remoteUA, rir: ripe},
|
|
{ua: remoteUA, rir: apnic},
|
|
},
|
|
expectedLabels: prometheus.Labels{
|
|
"operation": opCAA,
|
|
"perspective": allPerspectives,
|
|
"challenge_type": string(core.ChallengeTypeDNS01),
|
|
"problem_type": "",
|
|
"result": pass,
|
|
},
|
|
},
|
|
{
|
|
name: "functional localVA, 1 broken RVA, CAA issue type present",
|
|
ident: identifier.NewDNS("present.com"),
|
|
expectedDiffLogSubstring: `"RemoteSuccesses":2,"RemoteFailures":1`,
|
|
expectedSummary: &mpicSummary{
|
|
Passed: []string{"dc-1-RIPE", "dc-2-APNIC"},
|
|
Failed: []string{"dc-0-ARIN"},
|
|
PassedRIRs: []string{ripe, apnic},
|
|
QuorumResult: "2/3",
|
|
},
|
|
localDNSClient: caaMockDNS{},
|
|
remoteVAs: []remoteConf{
|
|
{ua: brokenUA, rir: arin, dns: caaBrokenDNS{}},
|
|
{ua: remoteUA, rir: ripe},
|
|
{ua: remoteUA, rir: apnic},
|
|
},
|
|
expectedLabels: prometheus.Labels{
|
|
"operation": opCAA,
|
|
"perspective": allPerspectives,
|
|
"challenge_type": string(core.ChallengeTypeDNS01),
|
|
"problem_type": "",
|
|
"result": pass,
|
|
},
|
|
},
|
|
{
|
|
name: "functional localVA, 2 broken RVA, CAA issue type present",
|
|
ident: identifier.NewDNS("present.com"),
|
|
expectedProbSubstring: "During secondary validation: While processing CAA",
|
|
expectedProbType: probs.DNSProblem,
|
|
expectedDiffLogSubstring: `"RemoteSuccesses":1,"RemoteFailures":2`,
|
|
expectedSummary: &mpicSummary{
|
|
Passed: []string{"dc-2-APNIC"},
|
|
Failed: []string{"dc-0-ARIN", "dc-1-RIPE"},
|
|
PassedRIRs: []string{apnic},
|
|
QuorumResult: "1/3",
|
|
},
|
|
localDNSClient: caaMockDNS{},
|
|
remoteVAs: []remoteConf{
|
|
{ua: brokenUA, rir: arin, dns: caaBrokenDNS{}},
|
|
{ua: brokenUA, rir: ripe, dns: caaBrokenDNS{}},
|
|
{ua: remoteUA, rir: apnic},
|
|
},
|
|
expectedLabels: prometheus.Labels{
|
|
"operation": opCAA,
|
|
"perspective": allPerspectives,
|
|
"challenge_type": string(core.ChallengeTypeDNS01),
|
|
"problem_type": string(probs.DNSProblem),
|
|
"result": fail,
|
|
},
|
|
},
|
|
{
|
|
name: "functional localVA, all broken RVAs, CAA issue type present",
|
|
ident: identifier.NewDNS("present.com"),
|
|
expectedProbSubstring: "During secondary validation: While processing CAA",
|
|
expectedProbType: probs.DNSProblem,
|
|
expectedDiffLogSubstring: `"RemoteSuccesses":0,"RemoteFailures":3`,
|
|
expectedSummary: &mpicSummary{
|
|
Passed: []string{},
|
|
Failed: []string{"dc-0-ARIN", "dc-1-RIPE", "dc-2-APNIC"},
|
|
PassedRIRs: []string{},
|
|
QuorumResult: "0/3",
|
|
},
|
|
localDNSClient: caaMockDNS{},
|
|
remoteVAs: []remoteConf{
|
|
{ua: brokenUA, rir: arin, dns: caaBrokenDNS{}},
|
|
{ua: brokenUA, rir: ripe, dns: caaBrokenDNS{}},
|
|
{ua: brokenUA, rir: apnic, dns: caaBrokenDNS{}},
|
|
},
|
|
expectedLabels: prometheus.Labels{
|
|
"operation": opCAA,
|
|
"perspective": allPerspectives,
|
|
"challenge_type": string(core.ChallengeTypeDNS01),
|
|
"problem_type": string(probs.DNSProblem),
|
|
"result": fail,
|
|
},
|
|
},
|
|
{
|
|
// The localVA returns early with a problem before kicking off the
|
|
// remote checks.
|
|
name: "all VAs functional, CAA issue type forbids issuance",
|
|
ident: identifier.NewDNS("unsatisfiable.com"),
|
|
expectedProbSubstring: "CAA record for unsatisfiable.com prevents issuance",
|
|
expectedProbType: probs.CAAProblem,
|
|
localDNSClient: caaMockDNS{},
|
|
remoteVAs: []remoteConf{
|
|
{ua: remoteUA, rir: arin},
|
|
{ua: remoteUA, rir: ripe},
|
|
{ua: remoteUA, rir: apnic},
|
|
},
|
|
},
|
|
{
|
|
name: "1 hijacked RVA, CAA issue type present",
|
|
ident: identifier.NewDNS("present.com"),
|
|
expectedDiffLogSubstring: `"RemoteSuccesses":2,"RemoteFailures":1`,
|
|
expectedSummary: &mpicSummary{
|
|
Passed: []string{"dc-1-RIPE", "dc-2-APNIC"},
|
|
Failed: []string{"dc-0-ARIN"},
|
|
PassedRIRs: []string{ripe, apnic},
|
|
QuorumResult: "2/3",
|
|
},
|
|
localDNSClient: caaMockDNS{},
|
|
remoteVAs: []remoteConf{
|
|
{ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}},
|
|
{ua: remoteUA, rir: ripe},
|
|
{ua: remoteUA, rir: apnic},
|
|
},
|
|
},
|
|
{
|
|
name: "2 hijacked RVAs, CAA issue type present",
|
|
ident: identifier.NewDNS("present.com"),
|
|
expectedProbSubstring: "During secondary validation: While processing CAA",
|
|
expectedProbType: probs.CAAProblem,
|
|
expectedDiffLogSubstring: `"RemoteSuccesses":1,"RemoteFailures":2`,
|
|
expectedSummary: &mpicSummary{
|
|
Passed: []string{"dc-2-APNIC"},
|
|
Failed: []string{"dc-0-ARIN", "dc-1-RIPE"},
|
|
PassedRIRs: []string{apnic},
|
|
QuorumResult: "1/3",
|
|
},
|
|
localDNSClient: caaMockDNS{},
|
|
remoteVAs: []remoteConf{
|
|
{ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}},
|
|
{ua: hijackedUA, rir: ripe, dns: caaHijackedDNS{}},
|
|
{ua: remoteUA, rir: apnic},
|
|
},
|
|
},
|
|
{
|
|
name: "3 hijacked RVAs, CAA issue type present",
|
|
ident: identifier.NewDNS("present.com"),
|
|
expectedProbSubstring: "During secondary validation: While processing CAA",
|
|
expectedProbType: probs.CAAProblem,
|
|
expectedDiffLogSubstring: `"RemoteSuccesses":0,"RemoteFailures":3`,
|
|
expectedSummary: &mpicSummary{
|
|
Passed: []string{},
|
|
Failed: []string{"dc-0-ARIN", "dc-1-RIPE", "dc-2-APNIC"},
|
|
PassedRIRs: []string{},
|
|
QuorumResult: "0/3",
|
|
},
|
|
localDNSClient: caaMockDNS{},
|
|
remoteVAs: []remoteConf{
|
|
{ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}},
|
|
{ua: hijackedUA, rir: ripe, dns: caaHijackedDNS{}},
|
|
{ua: hijackedUA, rir: apnic, dns: caaHijackedDNS{}},
|
|
},
|
|
},
|
|
{
|
|
name: "1 hijacked RVA, CAA issuewild type present",
|
|
ident: identifier.NewDNS("satisfiable-wildcard.com"),
|
|
expectedDiffLogSubstring: `"RemoteSuccesses":2,"RemoteFailures":1`,
|
|
expectedSummary: &mpicSummary{
|
|
Passed: []string{"dc-1-RIPE", "dc-2-APNIC"},
|
|
Failed: []string{"dc-0-ARIN"},
|
|
PassedRIRs: []string{ripe, apnic},
|
|
QuorumResult: "2/3",
|
|
},
|
|
localDNSClient: caaMockDNS{},
|
|
remoteVAs: []remoteConf{
|
|
{ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}},
|
|
{ua: remoteUA, rir: ripe},
|
|
{ua: remoteUA, rir: apnic},
|
|
},
|
|
},
|
|
{
|
|
name: "2 hijacked RVAs, CAA issuewild type present",
|
|
ident: identifier.NewDNS("satisfiable-wildcard.com"),
|
|
expectedProbSubstring: "During secondary validation: While processing CAA",
|
|
expectedProbType: probs.CAAProblem,
|
|
expectedDiffLogSubstring: `"RemoteSuccesses":1,"RemoteFailures":2`,
|
|
expectedSummary: &mpicSummary{
|
|
Passed: []string{"dc-2-APNIC"},
|
|
Failed: []string{"dc-0-ARIN", "dc-1-RIPE"},
|
|
PassedRIRs: []string{apnic},
|
|
QuorumResult: "1/3",
|
|
},
|
|
localDNSClient: caaMockDNS{},
|
|
remoteVAs: []remoteConf{
|
|
{ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}},
|
|
{ua: hijackedUA, rir: ripe, dns: caaHijackedDNS{}},
|
|
{ua: remoteUA, rir: apnic},
|
|
},
|
|
},
|
|
{
|
|
name: "3 hijacked RVAs, CAA issuewild type present",
|
|
ident: identifier.NewDNS("satisfiable-wildcard.com"),
|
|
expectedProbSubstring: "During secondary validation: While processing CAA",
|
|
expectedProbType: probs.CAAProblem,
|
|
expectedDiffLogSubstring: `"RemoteSuccesses":0,"RemoteFailures":3`,
|
|
expectedSummary: &mpicSummary{
|
|
Passed: []string{},
|
|
Failed: []string{"dc-0-ARIN", "dc-1-RIPE", "dc-2-APNIC"},
|
|
PassedRIRs: []string{},
|
|
QuorumResult: "0/3",
|
|
},
|
|
localDNSClient: caaMockDNS{},
|
|
remoteVAs: []remoteConf{
|
|
{ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}},
|
|
{ua: hijackedUA, rir: ripe, dns: caaHijackedDNS{}},
|
|
{ua: hijackedUA, rir: apnic, dns: caaHijackedDNS{}},
|
|
},
|
|
},
|
|
{
|
|
name: "1 hijacked RVA, CAA issuewild type present, 1 failure allowed",
|
|
ident: identifier.NewDNS("satisfiable-wildcard.com"),
|
|
expectedDiffLogSubstring: `"RemoteSuccesses":2,"RemoteFailures":1`,
|
|
expectedSummary: &mpicSummary{
|
|
Passed: []string{"dc-1-RIPE", "dc-2-APNIC"},
|
|
Failed: []string{"dc-0-ARIN"},
|
|
PassedRIRs: []string{ripe, apnic},
|
|
QuorumResult: "2/3",
|
|
},
|
|
localDNSClient: caaMockDNS{},
|
|
remoteVAs: []remoteConf{
|
|
{ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}},
|
|
{ua: remoteUA, rir: ripe},
|
|
{ua: remoteUA, rir: apnic},
|
|
},
|
|
},
|
|
{
|
|
name: "2 hijacked RVAs, CAA issuewild type present, 1 failure allowed",
|
|
ident: identifier.NewDNS("satisfiable-wildcard.com"),
|
|
expectedProbSubstring: "During secondary validation: While processing CAA",
|
|
expectedProbType: probs.CAAProblem,
|
|
expectedDiffLogSubstring: `"RemoteSuccesses":1,"RemoteFailures":2`,
|
|
expectedSummary: &mpicSummary{
|
|
Passed: []string{"dc-2-APNIC"},
|
|
Failed: []string{"dc-0-ARIN", "dc-1-RIPE"},
|
|
PassedRIRs: []string{apnic},
|
|
QuorumResult: "1/3",
|
|
},
|
|
localDNSClient: caaMockDNS{},
|
|
remoteVAs: []remoteConf{
|
|
{ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}},
|
|
{ua: hijackedUA, rir: ripe, dns: caaHijackedDNS{}},
|
|
{ua: remoteUA, rir: apnic},
|
|
},
|
|
},
|
|
{
|
|
name: "3 hijacked RVAs, CAA issuewild type present, 1 failure allowed",
|
|
ident: identifier.NewDNS("satisfiable-wildcard.com"),
|
|
expectedProbSubstring: "During secondary validation: While processing CAA",
|
|
expectedProbType: probs.CAAProblem,
|
|
expectedDiffLogSubstring: `"RemoteSuccesses":0,"RemoteFailures":3`,
|
|
expectedSummary: &mpicSummary{
|
|
Passed: []string{},
|
|
Failed: []string{"dc-0-ARIN", "dc-1-RIPE", "dc-2-APNIC"},
|
|
PassedRIRs: []string{},
|
|
QuorumResult: "0/3",
|
|
},
|
|
localDNSClient: caaMockDNS{},
|
|
remoteVAs: []remoteConf{
|
|
{ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}},
|
|
{ua: hijackedUA, rir: ripe, dns: caaHijackedDNS{}},
|
|
{ua: hijackedUA, rir: apnic, dns: caaHijackedDNS{}},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
va, mockLog := setupWithRemotes(nil, localUA, tc.remoteVAs, tc.localDNSClient)
|
|
defer mockLog.Clear()
|
|
|
|
features.Set(features.Config{
|
|
EnforceMultiCAA: true,
|
|
})
|
|
defer features.Reset()
|
|
|
|
isValidRes, err := va.DoCAA(context.TODO(), &vapb.IsCAAValidRequest{
|
|
Identifier: tc.ident.ToProto(),
|
|
ValidationMethod: string(core.ChallengeTypeDNS01),
|
|
AccountURIID: 1,
|
|
})
|
|
test.AssertNotError(t, err, "Should not have errored, but did")
|
|
|
|
if tc.expectedProbSubstring != "" {
|
|
test.AssertNotNil(t, isValidRes.Problem, "IsCAAValidRequest returned nil problem, but should not have")
|
|
test.AssertContains(t, isValidRes.Problem.Detail, tc.expectedProbSubstring)
|
|
} else if isValidRes.Problem != nil {
|
|
test.AssertBoxedNil(t, isValidRes.Problem, "IsCAAValidRequest returned a problem, but should not have")
|
|
}
|
|
|
|
if tc.expectedProbType != "" {
|
|
test.AssertNotNil(t, isValidRes.Problem, "IsCAAValidRequest returned nil problem, but should not have")
|
|
test.AssertEquals(t, string(tc.expectedProbType), isValidRes.Problem.ProblemType)
|
|
}
|
|
|
|
if tc.expectedSummary != nil {
|
|
gotAuditLog := parseValidationLogEvent(t, mockLog.GetAllMatching("JSON=.*"))
|
|
slices.Sort(tc.expectedSummary.Passed)
|
|
slices.Sort(tc.expectedSummary.Failed)
|
|
slices.Sort(tc.expectedSummary.PassedRIRs)
|
|
test.AssertDeepEquals(t, gotAuditLog.Summary, tc.expectedSummary)
|
|
}
|
|
|
|
gotAnyRemoteFailures := mockLog.GetAllMatching("CAA check failed due to remote failures:")
|
|
if len(gotAnyRemoteFailures) >= 1 {
|
|
// The primary VA only emits this line once.
|
|
test.AssertEquals(t, len(gotAnyRemoteFailures), 1)
|
|
} else {
|
|
test.AssertEquals(t, len(gotAnyRemoteFailures), 0)
|
|
}
|
|
|
|
if tc.expectedLabels != nil {
|
|
test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, tc.expectedLabels, 1)
|
|
}
|
|
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCAAFailure(t *testing.T) {
|
|
hs := httpSrv(t, expectedToken, false)
|
|
defer hs.Close()
|
|
|
|
va, _ := setup(hs, "", nil, caaMockDNS{})
|
|
|
|
err := va.checkCAA(ctx, identifier.NewDNS("reserved.com"), &caaParams{1, core.ChallengeTypeHTTP01})
|
|
if err == nil {
|
|
t.Fatalf("Expected CAA rejection for reserved.com, got success")
|
|
}
|
|
test.AssertErrorIs(t, err, berrors.CAA)
|
|
|
|
err = va.checkCAA(ctx, identifier.NewDNS("example.gonetld"), &caaParams{1, core.ChallengeTypeHTTP01})
|
|
if err == nil {
|
|
t.Fatalf("Expected CAA rejection for gonetld, got success")
|
|
}
|
|
prob := detailedError(err)
|
|
test.AssertEquals(t, prob.Type, probs.DNSProblem)
|
|
test.AssertContains(t, prob.String(), "NXDOMAIN")
|
|
}
|
|
|
|
func TestFilterCAA(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
input []*dns.CAA
|
|
expectedIssueVals []string
|
|
expectedWildVals []string
|
|
expectedCU bool
|
|
}{
|
|
{
|
|
name: "recognized non-critical",
|
|
input: []*dns.CAA{
|
|
{Tag: "issue", Value: "a"},
|
|
{Tag: "issuewild", Value: "b"},
|
|
{Tag: "iodef", Value: "c"},
|
|
{Tag: "issuemail", Value: "c"},
|
|
},
|
|
expectedIssueVals: []string{"a"},
|
|
expectedWildVals: []string{"b"},
|
|
},
|
|
{
|
|
name: "recognized critical",
|
|
input: []*dns.CAA{
|
|
{Tag: "issue", Value: "a", Flag: 128},
|
|
{Tag: "issuewild", Value: "b", Flag: 128},
|
|
{Tag: "iodef", Value: "c", Flag: 128},
|
|
{Tag: "issuemail", Value: "c", Flag: 128},
|
|
},
|
|
expectedIssueVals: []string{"a"},
|
|
expectedWildVals: []string{"b"},
|
|
},
|
|
{
|
|
name: "unrecognized non-critical",
|
|
input: []*dns.CAA{
|
|
{Tag: "unknown", Flag: 2},
|
|
},
|
|
},
|
|
{
|
|
name: "unrecognized critical",
|
|
input: []*dns.CAA{
|
|
{Tag: "unknown", Flag: 128},
|
|
},
|
|
expectedCU: true,
|
|
},
|
|
{
|
|
name: "unrecognized improper critical",
|
|
input: []*dns.CAA{
|
|
{Tag: "unknown", Flag: 1},
|
|
},
|
|
expectedCU: true,
|
|
},
|
|
{
|
|
name: "unrecognized very improper critical",
|
|
input: []*dns.CAA{
|
|
{Tag: "unknown", Flag: 9},
|
|
},
|
|
expectedCU: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
issue, wild, cu := filterCAA(tc.input)
|
|
for _, tag := range issue {
|
|
test.AssertSliceContains(t, tc.expectedIssueVals, tag.Value)
|
|
}
|
|
for _, tag := range wild {
|
|
test.AssertSliceContains(t, tc.expectedWildVals, tag.Value)
|
|
}
|
|
test.AssertEquals(t, tc.expectedCU, cu)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSelectCAA(t *testing.T) {
|
|
expected := dns.CAA{Tag: "issue", Value: "foo"}
|
|
|
|
// An empty slice of caaResults should return nil, nil
|
|
r := []caaResult{}
|
|
s, err := selectCAA(r)
|
|
test.Assert(t, s == nil, "set is not nil")
|
|
test.AssertNotError(t, err, "error is not nil")
|
|
|
|
// A slice of empty caaResults should return nil, "", nil
|
|
r = []caaResult{
|
|
{"", false, nil, nil, false, "", nil, nil},
|
|
{"", false, nil, nil, false, "", nil, nil},
|
|
{"", false, nil, nil, false, "", nil, nil},
|
|
}
|
|
s, err = selectCAA(r)
|
|
test.Assert(t, s == nil, "set is not nil")
|
|
test.AssertNotError(t, err, "error is not nil")
|
|
|
|
// A slice of caaResults containing an error followed by a CAA
|
|
// record should return the error
|
|
r = []caaResult{
|
|
{"foo.com", false, nil, nil, false, "", nil, errors.New("oops")},
|
|
{"com", true, []*dns.CAA{&expected}, nil, false, "foo", nil, nil},
|
|
}
|
|
s, err = selectCAA(r)
|
|
test.Assert(t, s == nil, "set is not nil")
|
|
test.AssertError(t, err, "error is nil")
|
|
test.AssertEquals(t, err.Error(), "oops")
|
|
|
|
// A slice of caaResults containing a good record that precedes an
|
|
// error, should return that good record, not the error
|
|
r = []caaResult{
|
|
{"foo.com", true, []*dns.CAA{&expected}, nil, false, "foo", nil, nil},
|
|
{"com", false, nil, nil, false, "", nil, errors.New("")},
|
|
}
|
|
s, err = selectCAA(r)
|
|
test.AssertEquals(t, len(s.issue), 1)
|
|
test.Assert(t, s.issue[0] == &expected, "Incorrect record returned")
|
|
test.AssertEquals(t, s.dig, "foo")
|
|
test.Assert(t, err == nil, "error is not nil")
|
|
|
|
// A slice of caaResults containing multiple CAA records should
|
|
// return the first non-empty CAA record
|
|
r = []caaResult{
|
|
{"bar.foo.com", false, []*dns.CAA{}, []*dns.CAA{}, false, "", nil, nil},
|
|
{"foo.com", true, []*dns.CAA{&expected}, nil, false, "foo", nil, nil},
|
|
{"com", true, []*dns.CAA{&expected}, nil, false, "bar", nil, nil},
|
|
}
|
|
s, err = selectCAA(r)
|
|
test.AssertEquals(t, len(s.issue), 1)
|
|
test.Assert(t, s.issue[0] == &expected, "Incorrect record returned")
|
|
test.AssertEquals(t, s.dig, "foo")
|
|
test.AssertNotError(t, err, "expect nil error")
|
|
}
|
|
|
|
func TestAccountURIMatches(t *testing.T) {
|
|
t.Parallel()
|
|
tests := []struct {
|
|
name string
|
|
params []caaParameter
|
|
prefixes []string
|
|
id int64
|
|
want bool
|
|
}{
|
|
{
|
|
name: "empty accounturi",
|
|
params: nil,
|
|
prefixes: []string{
|
|
"https://acme-v01.api.letsencrypt.org/acme/reg/",
|
|
},
|
|
id: 123456,
|
|
want: true,
|
|
},
|
|
{
|
|
name: "no accounturi in rr, but other parameters exist",
|
|
params: []caaParameter{{tag: "validationmethods", val: "tls-alpn-01"}},
|
|
prefixes: []string{
|
|
"https://acme-v02.api.letsencrypt.org/acme/reg/",
|
|
},
|
|
id: 123456,
|
|
want: true,
|
|
},
|
|
{
|
|
name: "non-uri accounturi",
|
|
params: []caaParameter{{tag: "accounturi", val: "\\invalid 😎/123456"}},
|
|
prefixes: []string{
|
|
"\\invalid 😎",
|
|
},
|
|
id: 123456,
|
|
want: false,
|
|
},
|
|
{
|
|
name: "simple match",
|
|
params: []caaParameter{{tag: "accounturi", val: "https://acme-v01.api.letsencrypt.org/acme/reg/123456"}},
|
|
prefixes: []string{
|
|
"https://acme-v01.api.letsencrypt.org/acme/reg/",
|
|
},
|
|
id: 123456,
|
|
want: true,
|
|
},
|
|
{
|
|
name: "simple match, but has a friend",
|
|
params: []caaParameter{{tag: "validationmethods", val: "dns-01"}, {tag: "accounturi", val: "https://acme-v01.api.letsencrypt.org/acme/reg/123456"}},
|
|
prefixes: []string{
|
|
"https://acme-v01.api.letsencrypt.org/acme/reg/",
|
|
},
|
|
id: 123456,
|
|
want: true,
|
|
},
|
|
{
|
|
name: "accountid mismatch",
|
|
params: []caaParameter{{tag: "accounturi", val: "https://acme-v01.api.letsencrypt.org/acme/reg/123456"}},
|
|
prefixes: []string{
|
|
"https://acme-v01.api.letsencrypt.org/acme/reg/",
|
|
},
|
|
id: 123457,
|
|
want: false,
|
|
},
|
|
{
|
|
name: "single parameter, no value",
|
|
params: []caaParameter{{tag: "accounturi", val: ""}},
|
|
prefixes: []string{
|
|
"https://acme-v02.api.letsencrypt.org/acme/reg/",
|
|
},
|
|
id: 123456,
|
|
want: false,
|
|
},
|
|
{
|
|
name: "multiple parameters, each with no value",
|
|
params: []caaParameter{{tag: "accounturi", val: ""}, {tag: "accounturi", val: ""}},
|
|
prefixes: []string{
|
|
"https://acme-v02.api.letsencrypt.org/acme/reg/",
|
|
},
|
|
id: 123456,
|
|
want: false,
|
|
},
|
|
{
|
|
name: "multiple parameters, one with no value",
|
|
params: []caaParameter{{tag: "accounturi", val: ""}, {tag: "accounturi", val: "https://acme-v02.api.letsencrypt.org/acme/reg/123456"}},
|
|
prefixes: []string{
|
|
"https://acme-v02.api.letsencrypt.org/acme/reg/",
|
|
},
|
|
id: 123456,
|
|
want: false,
|
|
},
|
|
{
|
|
name: "multiple parameters, each with an identical value",
|
|
params: []caaParameter{{tag: "accounturi", val: "https://acme-v02.api.letsencrypt.org/acme/reg/123456"}, {tag: "accounturi", val: "https://acme-v02.api.letsencrypt.org/acme/reg/123456"}},
|
|
prefixes: []string{
|
|
"https://acme-v02.api.letsencrypt.org/acme/reg/",
|
|
},
|
|
id: 123456,
|
|
want: false,
|
|
},
|
|
{
|
|
name: "multiple parameters, each with a different value",
|
|
params: []caaParameter{{tag: "accounturi", val: "https://acme-v02.api.letsencrypt.org/acme/reg/69"}, {tag: "accounturi", val: "https://acme-v02.api.letsencrypt.org/acme/reg/420"}},
|
|
prefixes: []string{
|
|
"https://acme-v02.api.letsencrypt.org/acme/reg/",
|
|
},
|
|
id: 69,
|
|
want: false,
|
|
},
|
|
{
|
|
name: "multiple prefixes, match first",
|
|
params: []caaParameter{{tag: "accounturi", val: "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,
|
|
},
|
|
{
|
|
name: "multiple prefixes, match second",
|
|
params: []caaParameter{{tag: "accounturi", val: "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,
|
|
},
|
|
{
|
|
name: "multiple prefixes, match none",
|
|
params: []caaParameter{{tag: "accounturi", val: "https://acme-v02.api.letsencrypt.org/acme/acct/123456"}},
|
|
prefixes: []string{
|
|
"https://acme-v01.api.letsencrypt.org/acme/acct/",
|
|
"https://acme-v03.api.letsencrypt.org/acme/acct/",
|
|
},
|
|
id: 123456,
|
|
want: false,
|
|
},
|
|
{
|
|
name: "three prefixes",
|
|
params: []caaParameter{{tag: "accounturi", val: "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,
|
|
},
|
|
{
|
|
name: "multiple prefixes, wrong accountid",
|
|
params: []caaParameter{{tag: "accounturi", val: "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 _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
got := caaAccountURIMatches(tc.params, tc.prefixes, tc.id)
|
|
test.AssertEquals(t, got, tc.want)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidationMethodMatches(t *testing.T) {
|
|
t.Parallel()
|
|
tests := []struct {
|
|
name string
|
|
params []caaParameter
|
|
method core.AcmeChallenge
|
|
want bool
|
|
}{
|
|
{
|
|
name: "empty validationmethods",
|
|
params: nil,
|
|
method: core.ChallengeTypeHTTP01,
|
|
want: true,
|
|
},
|
|
{
|
|
name: "no validationmethods in rr, but other parameters exist", // validationmethods is not mandatory
|
|
params: []caaParameter{{tag: "accounturi", val: "ph1LwuzHere"}},
|
|
method: core.ChallengeTypeHTTP01,
|
|
want: true,
|
|
},
|
|
{
|
|
name: "no value",
|
|
params: []caaParameter{{tag: "validationmethods", val: ""}}, // equivalent to forbidding issuance
|
|
method: core.ChallengeTypeHTTP01,
|
|
want: false,
|
|
},
|
|
{
|
|
name: "only comma",
|
|
params: []caaParameter{{tag: "validationmethods", val: ","}},
|
|
method: core.ChallengeTypeHTTP01,
|
|
want: false,
|
|
},
|
|
{
|
|
name: "malformed method",
|
|
params: []caaParameter{{tag: "validationmethods", val: "howdy !"}},
|
|
method: core.ChallengeTypeHTTP01,
|
|
want: false,
|
|
},
|
|
{
|
|
name: "invalid method",
|
|
params: []caaParameter{{tag: "validationmethods", val: "tls-sni-01"}},
|
|
method: core.ChallengeTypeHTTP01,
|
|
want: false,
|
|
},
|
|
{
|
|
name: "simple match",
|
|
params: []caaParameter{{tag: "validationmethods", val: "http-01"}},
|
|
method: core.ChallengeTypeHTTP01,
|
|
want: true,
|
|
},
|
|
{
|
|
name: "simple match, but has a friend",
|
|
params: []caaParameter{{tag: "accounturi", val: "https://example.org"}, {tag: "validationmethods", val: "http-01"}},
|
|
method: core.ChallengeTypeHTTP01,
|
|
want: true,
|
|
},
|
|
{
|
|
name: "multiple validationmethods, each with no value",
|
|
params: []caaParameter{{tag: "validationmethods", val: ""}, {tag: "validationmethods", val: ""}},
|
|
method: core.ChallengeTypeHTTP01,
|
|
want: false,
|
|
},
|
|
{
|
|
name: "multiple validationmethods, one with no value",
|
|
params: []caaParameter{{tag: "validationmethods", val: ""}, {tag: "validationmethods", val: "http-01"}},
|
|
method: core.ChallengeTypeHTTP01,
|
|
want: false,
|
|
},
|
|
{
|
|
name: "multiple validationmethods, each with an identical value",
|
|
params: []caaParameter{{tag: "validationmethods", val: "http-01"}, {tag: "validationmethods", val: "http-01"}},
|
|
method: core.ChallengeTypeHTTP01,
|
|
want: false,
|
|
},
|
|
{
|
|
name: "multiple validationmethods, each with a different value",
|
|
params: []caaParameter{{tag: "validationmethods", val: "http-01"}, {tag: "validationmethods", val: "dns-01"}},
|
|
method: core.ChallengeTypeHTTP01,
|
|
want: false,
|
|
},
|
|
{
|
|
name: "simple mismatch",
|
|
params: []caaParameter{{tag: "validationmethods", val: "dns-01"}},
|
|
method: core.ChallengeTypeHTTP01,
|
|
want: false,
|
|
},
|
|
{
|
|
name: "multiple choices, match first",
|
|
params: []caaParameter{{tag: "validationmethods", val: "http-01,dns-01"}},
|
|
method: core.ChallengeTypeHTTP01,
|
|
want: true,
|
|
},
|
|
{
|
|
name: "multiple choices, match second",
|
|
params: []caaParameter{{tag: "validationmethods", val: "http-01,dns-01"}},
|
|
method: core.ChallengeTypeDNS01,
|
|
want: true,
|
|
},
|
|
{
|
|
name: "multiple choices, match none",
|
|
params: []caaParameter{{tag: "validationmethods", val: "http-01,dns-01"}},
|
|
method: core.ChallengeTypeTLSALPN01,
|
|
want: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
got := caaValidationMethodMatches(tc.params, tc.method)
|
|
test.AssertEquals(t, got, tc.want)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExtractIssuerDomainAndParameters(t *testing.T) {
|
|
t.Parallel()
|
|
tests := []struct {
|
|
name string
|
|
value string
|
|
wantDomain string
|
|
wantParameters []caaParameter
|
|
expectErrSubstr string
|
|
}{
|
|
{
|
|
name: "empty record is valid",
|
|
value: "",
|
|
wantDomain: "",
|
|
wantParameters: nil,
|
|
expectErrSubstr: "",
|
|
},
|
|
{
|
|
name: "only semicolon is valid",
|
|
value: ";",
|
|
wantDomain: "",
|
|
wantParameters: nil,
|
|
expectErrSubstr: "",
|
|
},
|
|
{
|
|
name: "only semicolon and whitespace is valid",
|
|
value: " ; ",
|
|
wantDomain: "",
|
|
wantParameters: nil,
|
|
expectErrSubstr: "",
|
|
},
|
|
{
|
|
name: "only domain is valid",
|
|
value: "letsencrypt.org",
|
|
wantDomain: "letsencrypt.org",
|
|
wantParameters: nil,
|
|
expectErrSubstr: "",
|
|
},
|
|
{
|
|
name: "only domain with trailing semicolon is valid",
|
|
value: "letsencrypt.org;",
|
|
wantDomain: "letsencrypt.org",
|
|
wantParameters: nil,
|
|
expectErrSubstr: "",
|
|
},
|
|
{
|
|
name: "only domain with semicolon and trailing whitespace is valid",
|
|
value: "letsencrypt.org; ",
|
|
wantDomain: "letsencrypt.org",
|
|
wantParameters: nil,
|
|
expectErrSubstr: "",
|
|
},
|
|
{
|
|
name: "domain with params and whitespace is valid",
|
|
value: " letsencrypt.org ;foo=bar;baz=bar",
|
|
wantDomain: "letsencrypt.org",
|
|
wantParameters: []caaParameter{{tag: "foo", val: "bar"}, {tag: "baz", val: "bar"}},
|
|
expectErrSubstr: "",
|
|
},
|
|
{
|
|
name: "domain with params and different whitespace is valid",
|
|
value: " letsencrypt.org ;foo=bar;baz=bar",
|
|
wantDomain: "letsencrypt.org",
|
|
wantParameters: []caaParameter{{tag: "foo", val: "bar"}, {tag: "baz", val: "bar"}},
|
|
expectErrSubstr: "",
|
|
},
|
|
{
|
|
name: "empty params are valid",
|
|
value: "letsencrypt.org; foo=; baz = bar",
|
|
wantDomain: "letsencrypt.org",
|
|
wantParameters: []caaParameter{{tag: "foo", val: ""}, {tag: "baz", val: "bar"}},
|
|
expectErrSubstr: "",
|
|
},
|
|
{
|
|
name: "whitespace around params is valid",
|
|
value: "letsencrypt.org; foo= ; baz = bar",
|
|
wantDomain: "letsencrypt.org",
|
|
wantParameters: []caaParameter{{tag: "foo", val: ""}, {tag: "baz", val: "bar"}},
|
|
expectErrSubstr: "",
|
|
},
|
|
{
|
|
name: "comma-separated param values are valid",
|
|
value: "letsencrypt.org; foo=b1,b2,b3 ; baz = a=b ",
|
|
wantDomain: "letsencrypt.org",
|
|
wantParameters: []caaParameter{{tag: "foo", val: "b1,b2,b3"}, {tag: "baz", val: "a=b"}},
|
|
expectErrSubstr: "",
|
|
},
|
|
{
|
|
name: "duplicate tags are valid",
|
|
value: "letsencrypt.org; foo=b1,b2,b3 ; foo= b1,b2,b3 ",
|
|
wantDomain: "letsencrypt.org",
|
|
wantParameters: []caaParameter{{tag: "foo", val: "b1,b2,b3"}, {tag: "foo", val: "b1,b2,b3"}},
|
|
expectErrSubstr: "",
|
|
},
|
|
{
|
|
name: "spaces in param values are invalid",
|
|
value: "letsencrypt.org; foo=b1,b2,b3 ; baz = a = b ",
|
|
expectErrSubstr: "value contains disallowed character",
|
|
},
|
|
{
|
|
name: "spaces in param values are still invalid",
|
|
value: "letsencrypt.org; foo=b1,b2,b3 ; baz=a= b",
|
|
expectErrSubstr: "value contains disallowed character",
|
|
},
|
|
{
|
|
name: "param without equals sign is invalid",
|
|
value: "letsencrypt.org; foo=b1,b2,b3 ; baz = a;b ",
|
|
expectErrSubstr: "parameter not formatted as tag=value",
|
|
},
|
|
{
|
|
name: "hyphens in param values are valid",
|
|
value: "letsencrypt.org; 1=2; baz=a-b",
|
|
wantDomain: "letsencrypt.org",
|
|
wantParameters: []caaParameter{{tag: "1", val: "2"}, {tag: "baz", val: "a-b"}},
|
|
expectErrSubstr: "",
|
|
},
|
|
{
|
|
name: "underscores in param tags are invalid",
|
|
value: "letsencrypt.org; a_b=123",
|
|
expectErrSubstr: "tag contains disallowed character",
|
|
},
|
|
{
|
|
name: "multiple spaces in param values are extra invalid",
|
|
value: "letsencrypt.org; ab=1 2 3",
|
|
expectErrSubstr: "value contains disallowed character",
|
|
},
|
|
{
|
|
name: "hyphens in param tags are invalid",
|
|
value: "letsencrypt.org; 1=2; a-b=c",
|
|
expectErrSubstr: "tag contains disallowed character",
|
|
},
|
|
{
|
|
name: "high codepoints in params are invalid",
|
|
value: "letsencrypt.org; foo=a\u2615b",
|
|
expectErrSubstr: "value contains disallowed character",
|
|
},
|
|
{
|
|
name: "missing semicolons between params are invalid",
|
|
value: "letsencrypt.org; foo=b1,b2,b3 baz=a",
|
|
expectErrSubstr: "value contains disallowed character",
|
|
},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
gotDomain, gotParameters, gotErr := parseCAARecord(&dns.CAA{Value: tc.value})
|
|
|
|
if tc.expectErrSubstr == "" {
|
|
test.AssertNotError(t, gotErr, "")
|
|
} else {
|
|
test.AssertError(t, gotErr, "")
|
|
test.AssertContains(t, gotErr.Error(), tc.expectErrSubstr)
|
|
}
|
|
|
|
if tc.wantDomain != "" {
|
|
test.AssertEquals(t, gotDomain, tc.wantDomain)
|
|
}
|
|
|
|
if tc.wantParameters != nil {
|
|
test.AssertDeepEquals(t, gotParameters, tc.wantParameters)
|
|
}
|
|
})
|
|
}
|
|
}
|