From acdb0587717fe7460513d57d9e868fac122e5bd9 Mon Sep 17 00:00:00 2001 From: Aaron Gable Date: Fri, 24 Oct 2025 16:20:37 -0700 Subject: [PATCH 1/2] Include AD bit in ValidationRecords --- bdns/dns.go | 37 +++++++++++++------------- bdns/dns_test.go | 62 +++++++++++++++++++++---------------------- bdns/mocks.go | 52 ++++++++++++++++++------------------ core/objects.go | 6 +++++ va/caa.go | 3 ++- va/caa_test.go | 66 +++++++++++++++++++++++----------------------- va/dns.go | 14 +++++----- va/http.go | 11 +++++--- va/http_test.go | 10 +++++-- va/tlsalpn.go | 7 ++--- va/tlsalpn_test.go | 4 +-- wfe2/wfe.go | 1 + 12 files changed, 147 insertions(+), 126 deletions(-) diff --git a/bdns/dns.go b/bdns/dns.go index 939d74280cd..2e4f4264ddc 100644 --- a/bdns/dns.go +++ b/bdns/dns.go @@ -34,9 +34,9 @@ type ResolverAddrs []string // Client queries for DNS records type Client interface { - LookupTXT(context.Context, string) (txts []string, resolver ResolverAddrs, err error) - LookupHost(context.Context, string) ([]netip.Addr, ResolverAddrs, error) - LookupCAA(context.Context, string) ([]*dns.CAA, string, ResolverAddrs, error) + LookupTXT(context.Context, string) (txts []string, resolver ResolverAddrs, ad bool, err error) + LookupHost(context.Context, string) ([]netip.Addr, ResolverAddrs, bool, error) + LookupCAA(context.Context, string) ([]*dns.CAA, string, ResolverAddrs, bool, error) } // impl represents a client that talks to an external resolver @@ -322,13 +322,13 @@ type dnsResp struct { // LookupTXT sends a DNS query to find all TXT records associated with // the provided hostname which it returns along with the returned // DNS authority section. -func (dnsClient *impl) LookupTXT(ctx context.Context, hostname string) ([]string, ResolverAddrs, error) { +func (dnsClient *impl) LookupTXT(ctx context.Context, hostname string) ([]string, ResolverAddrs, bool, error) { var txt []string dnsType := dns.TypeTXT r, resolver, err := dnsClient.exchangeOne(ctx, hostname, dnsType) errWrap := wrapErr(dnsType, hostname, r, err) if errWrap != nil { - return nil, ResolverAddrs{resolver}, errWrap + return nil, ResolverAddrs{resolver}, false, errWrap } for _, answer := range r.Answer { @@ -339,10 +339,10 @@ func (dnsClient *impl) LookupTXT(ctx context.Context, hostname string) ([]string } } - return txt, ResolverAddrs{resolver}, err + return txt, ResolverAddrs{resolver}, r.AuthenticatedData, err } -func (dnsClient *impl) lookupIP(ctx context.Context, hostname string, ipType uint16) ([]dns.RR, string, error) { +func (dnsClient *impl) lookupIP(ctx context.Context, hostname string, ipType uint16) ([]dns.RR, string, bool, error) { resp, resolver, err := dnsClient.exchangeOne(ctx, hostname, ipType) switch ipType { case dns.TypeA: @@ -356,9 +356,9 @@ func (dnsClient *impl) lookupIP(ctx context.Context, hostname string, ipType uin } errWrap := wrapErr(ipType, hostname, resp, err) if errWrap != nil { - return nil, resolver, errWrap + return nil, resolver, false, errWrap } - return resp.Answer, resolver, nil + return resp.Answer, resolver, resp.AuthenticatedData, nil } // LookupHost sends a DNS query to find all A and AAAA records associated with @@ -366,17 +366,18 @@ func (dnsClient *impl) lookupIP(ctx context.Context, hostname string, ipType uin // chase CNAME/DNAME aliases and return relevant records. It will retry // requests in the case of temporary network errors. It returns an error if // both the A and AAAA lookups fail or are empty, but succeeds otherwise. -func (dnsClient *impl) LookupHost(ctx context.Context, hostname string) ([]netip.Addr, ResolverAddrs, error) { +func (dnsClient *impl) LookupHost(ctx context.Context, hostname string) ([]netip.Addr, ResolverAddrs, bool, error) { var recordsA, recordsAAAA []dns.RR var errA, errAAAA error var resolverA, resolverAAAA string + var adA, adAAAA bool var wg sync.WaitGroup wg.Go(func() { - recordsA, resolverA, errA = dnsClient.lookupIP(ctx, hostname, dns.TypeA) + recordsA, resolverA, adA, errA = dnsClient.lookupIP(ctx, hostname, dns.TypeA) }) wg.Go(func() { - recordsAAAA, resolverAAAA, errAAAA = dnsClient.lookupIP(ctx, hostname, dns.TypeAAAA) + recordsAAAA, resolverAAAA, adAAAA, errAAAA = dnsClient.lookupIP(ctx, hostname, dns.TypeAAAA) }) wg.Wait() @@ -427,17 +428,17 @@ func (dnsClient *impl) LookupHost(ctx context.Context, hostname string) ([]netip // branching. We don't use ProblemDetails and SubProblemDetails here, because // this error will get wrapped in a DNSError and further munged by higher // layers in the stack. - return nil, resolvers, fmt.Errorf("%w; %s", errA, errAAAA) + return nil, resolvers, false, fmt.Errorf("%w; %s", errA, errAAAA) } - return append(addrsA, addrsAAAA...), resolvers, nil + return append(addrsA, addrsAAAA...), resolvers, adA && adAAAA, nil } // LookupCAA sends a DNS query to find all CAA records associated with // the provided hostname and the complete dig-style RR `response`. This // response is quite verbose, however it's only populated when the CAA // response is non-empty. -func (dnsClient *impl) LookupCAA(ctx context.Context, hostname string) ([]*dns.CAA, string, ResolverAddrs, error) { +func (dnsClient *impl) LookupCAA(ctx context.Context, hostname string) ([]*dns.CAA, string, ResolverAddrs, bool, error) { dnsType := dns.TypeCAA r, resolver, err := dnsClient.exchangeOne(ctx, hostname, dnsType) @@ -448,12 +449,12 @@ func (dnsClient *impl) LookupCAA(ctx context.Context, hostname string) ([]*dns.C // rechecking. But allow NXDOMAIN for TLDs to fall through to the error code // below, so we don't issue for gTLDs that have been removed by ICANN. if err == nil && r.Rcode == dns.RcodeNameError && strings.Contains(hostname, ".") { - return nil, "", ResolverAddrs{resolver}, nil + return nil, "", ResolverAddrs{resolver}, false, nil } errWrap := wrapErr(dnsType, hostname, r, err) if errWrap != nil { - return nil, "", ResolverAddrs{resolver}, errWrap + return nil, "", ResolverAddrs{resolver}, false, errWrap } var CAAs []*dns.CAA @@ -466,7 +467,7 @@ func (dnsClient *impl) LookupCAA(ctx context.Context, hostname string) ([]*dns.C if len(CAAs) > 0 { response = r.String() } - return CAAs, response, ResolverAddrs{resolver}, nil + return CAAs, response, ResolverAddrs{resolver}, r.AuthenticatedData, nil } // logDNSError logs the provided err result from making a query for hostname to diff --git a/bdns/dns_test.go b/bdns/dns_test.go index 563912133af..232aa3565ec 100644 --- a/bdns/dns_test.go +++ b/bdns/dns_test.go @@ -287,14 +287,14 @@ func TestDNSNoServers(t *testing.T) { obj := New(time.Hour, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 1, "", blog.UseMock(), tlsConfig) - _, resolvers, err := obj.LookupHost(context.Background(), "letsencrypt.org") + _, resolvers, _, err := obj.LookupHost(context.Background(), "letsencrypt.org") test.AssertEquals(t, len(resolvers), 0) test.AssertError(t, err, "No servers") - _, _, err = obj.LookupTXT(context.Background(), "letsencrypt.org") + _, _, _, err = obj.LookupTXT(context.Background(), "letsencrypt.org") test.AssertError(t, err, "No servers") - _, _, _, err = obj.LookupCAA(context.Background(), "letsencrypt.org") + _, _, _, _, err = obj.LookupCAA(context.Background(), "letsencrypt.org") test.AssertError(t, err, "No servers") } @@ -304,7 +304,7 @@ func TestDNSOneServer(t *testing.T) { obj := New(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 1, "", blog.UseMock(), tlsConfig) - _, resolvers, err := obj.LookupHost(context.Background(), "cps.letsencrypt.org") + _, resolvers, _, err := obj.LookupHost(context.Background(), "cps.letsencrypt.org") test.AssertEquals(t, len(resolvers), 2) slices.Sort(resolvers) test.AssertDeepEquals(t, resolvers, ResolverAddrs{"A:127.0.0.1:4053", "AAAA:127.0.0.1:4053"}) @@ -317,7 +317,7 @@ func TestDNSDuplicateServers(t *testing.T) { obj := New(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 1, "", blog.UseMock(), tlsConfig) - _, resolvers, err := obj.LookupHost(context.Background(), "cps.letsencrypt.org") + _, resolvers, _, err := obj.LookupHost(context.Background(), "cps.letsencrypt.org") test.AssertEquals(t, len(resolvers), 2) slices.Sort(resolvers) test.AssertDeepEquals(t, resolvers, ResolverAddrs{"A:127.0.0.1:4053", "AAAA:127.0.0.1:4053"}) @@ -331,13 +331,13 @@ func TestDNSServFail(t *testing.T) { obj := New(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 1, "", blog.UseMock(), tlsConfig) bad := "servfail.com" - _, _, err = obj.LookupTXT(context.Background(), bad) + _, _, _, err = obj.LookupTXT(context.Background(), bad) test.AssertError(t, err, "LookupTXT didn't return an error") - _, _, err = obj.LookupHost(context.Background(), bad) + _, _, _, err = obj.LookupHost(context.Background(), bad) test.AssertError(t, err, "LookupHost didn't return an error") - emptyCaa, _, _, err := obj.LookupCAA(context.Background(), bad) + emptyCaa, _, _, _, err := obj.LookupCAA(context.Background(), bad) test.Assert(t, len(emptyCaa) == 0, "Query returned non-empty list of CAA records") test.AssertError(t, err, "LookupCAA should have returned an error") } @@ -348,11 +348,11 @@ func TestDNSLookupTXT(t *testing.T) { obj := New(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 1, "", blog.UseMock(), tlsConfig) - a, _, err := obj.LookupTXT(context.Background(), "letsencrypt.org") + a, _, _, err := obj.LookupTXT(context.Background(), "letsencrypt.org") t.Logf("A: %v", a) test.AssertNotError(t, err, "No message") - a, _, err = obj.LookupTXT(context.Background(), "split-txt.letsencrypt.org") + a, _, _, err = obj.LookupTXT(context.Background(), "split-txt.letsencrypt.org") t.Logf("A: %v ", a) test.AssertNotError(t, err, "No message") test.AssertEquals(t, len(a), 1) @@ -366,14 +366,14 @@ func TestDNSLookupHost(t *testing.T) { obj := New(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 1, "", blog.UseMock(), tlsConfig) - ip, resolvers, err := obj.LookupHost(context.Background(), "servfail.com") + ip, resolvers, _, err := obj.LookupHost(context.Background(), "servfail.com") t.Logf("servfail.com - IP: %s, Err: %s", ip, err) test.AssertError(t, err, "Server failure") test.Assert(t, len(ip) == 0, "Should not have IPs") slices.Sort(resolvers) test.AssertDeepEquals(t, resolvers, ResolverAddrs{"A:127.0.0.1:4053", "AAAA:127.0.0.1:4053"}) - ip, resolvers, err = obj.LookupHost(context.Background(), "nonexistent.letsencrypt.org") + ip, resolvers, _, err = obj.LookupHost(context.Background(), "nonexistent.letsencrypt.org") t.Logf("nonexistent.letsencrypt.org - IP: %s, Err: %s", ip, err) test.AssertError(t, err, "No valid A or AAAA records should error") test.Assert(t, len(ip) == 0, "Should not have IPs") @@ -381,13 +381,13 @@ func TestDNSLookupHost(t *testing.T) { test.AssertDeepEquals(t, resolvers, ResolverAddrs{"A:127.0.0.1:4053", "AAAA:127.0.0.1:4053"}) // Single IPv4 address - ip, resolvers, err = obj.LookupHost(context.Background(), "cps.letsencrypt.org") + ip, resolvers, _, err = obj.LookupHost(context.Background(), "cps.letsencrypt.org") t.Logf("cps.letsencrypt.org - IP: %s, Err: %s", ip, err) test.AssertNotError(t, err, "Not an error to exist") test.Assert(t, len(ip) == 1, "Should have IP") slices.Sort(resolvers) test.AssertDeepEquals(t, resolvers, ResolverAddrs{"A:127.0.0.1:4053", "AAAA:127.0.0.1:4053"}) - ip, resolvers, err = obj.LookupHost(context.Background(), "cps.letsencrypt.org") + ip, resolvers, _, err = obj.LookupHost(context.Background(), "cps.letsencrypt.org") t.Logf("cps.letsencrypt.org - IP: %s, Err: %s", ip, err) test.AssertNotError(t, err, "Not an error to exist") test.Assert(t, len(ip) == 1, "Should have IP") @@ -395,7 +395,7 @@ func TestDNSLookupHost(t *testing.T) { test.AssertDeepEquals(t, resolvers, ResolverAddrs{"A:127.0.0.1:4053", "AAAA:127.0.0.1:4053"}) // Single IPv6 address - ip, resolvers, err = obj.LookupHost(context.Background(), "v6.letsencrypt.org") + ip, resolvers, _, err = obj.LookupHost(context.Background(), "v6.letsencrypt.org") t.Logf("v6.letsencrypt.org - IP: %s, Err: %s", ip, err) test.AssertNotError(t, err, "Not an error to exist") test.Assert(t, len(ip) == 1, "Should not have IPs") @@ -403,7 +403,7 @@ func TestDNSLookupHost(t *testing.T) { test.AssertDeepEquals(t, resolvers, ResolverAddrs{"A:127.0.0.1:4053", "AAAA:127.0.0.1:4053"}) // Both IPv6 and IPv4 address - ip, resolvers, err = obj.LookupHost(context.Background(), "dualstack.letsencrypt.org") + ip, resolvers, _, err = obj.LookupHost(context.Background(), "dualstack.letsencrypt.org") t.Logf("dualstack.letsencrypt.org - IP: %s, Err: %s", ip, err) test.AssertNotError(t, err, "Not an error to exist") test.Assert(t, len(ip) == 2, "Should have 2 IPs") @@ -415,7 +415,7 @@ func TestDNSLookupHost(t *testing.T) { test.AssertDeepEquals(t, resolvers, ResolverAddrs{"A:127.0.0.1:4053", "AAAA:127.0.0.1:4053"}) // IPv6 error, IPv4 success - ip, resolvers, err = obj.LookupHost(context.Background(), "v6error.letsencrypt.org") + ip, resolvers, _, err = obj.LookupHost(context.Background(), "v6error.letsencrypt.org") t.Logf("v6error.letsencrypt.org - IP: %s, Err: %s", ip, err) test.AssertNotError(t, err, "Not an error to exist") test.Assert(t, len(ip) == 1, "Should have 1 IP") @@ -425,7 +425,7 @@ func TestDNSLookupHost(t *testing.T) { test.AssertDeepEquals(t, resolvers, ResolverAddrs{"A:127.0.0.1:4053", "AAAA:127.0.0.1:4053"}) // IPv6 success, IPv4 error - ip, resolvers, err = obj.LookupHost(context.Background(), "v4error.letsencrypt.org") + ip, resolvers, _, err = obj.LookupHost(context.Background(), "v4error.letsencrypt.org") t.Logf("v4error.letsencrypt.org - IP: %s, Err: %s", ip, err) test.AssertNotError(t, err, "Not an error to exist") test.Assert(t, len(ip) == 1, "Should have 1 IP") @@ -437,7 +437,7 @@ func TestDNSLookupHost(t *testing.T) { // IPv6 error, IPv4 error // Should return both the IPv4 error (Refused) and the IPv6 error (NotImplemented) hostname := "dualstackerror.letsencrypt.org" - ip, resolvers, err = obj.LookupHost(context.Background(), hostname) + ip, resolvers, _, err = obj.LookupHost(context.Background(), hostname) t.Logf("%s - IP: %s, Err: %s", hostname, ip, err) test.AssertError(t, err, "Should be an error") test.AssertContains(t, err.Error(), "REFUSED looking up A for") @@ -453,11 +453,11 @@ func TestDNSNXDOMAIN(t *testing.T) { obj := New(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 1, "", blog.UseMock(), tlsConfig) hostname := "nxdomain.letsencrypt.org" - _, _, err = obj.LookupHost(context.Background(), hostname) + _, _, _, err = obj.LookupHost(context.Background(), hostname) test.AssertContains(t, err.Error(), "NXDOMAIN looking up A for") test.AssertContains(t, err.Error(), "NXDOMAIN looking up AAAA for") - _, _, err = obj.LookupTXT(context.Background(), hostname) + _, _, _, err = obj.LookupTXT(context.Background(), hostname) expected := Error{dns.TypeTXT, hostname, nil, dns.RcodeNameError, nil} test.AssertDeepEquals(t, err, expected) } @@ -469,7 +469,7 @@ func TestDNSLookupCAA(t *testing.T) { obj := New(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 1, "", blog.UseMock(), tlsConfig) removeIDExp := regexp.MustCompile(" id: [[:digit:]]+") - caas, resp, resolvers, err := obj.LookupCAA(context.Background(), "bracewel.net") + caas, resp, resolvers, _, err := obj.LookupCAA(context.Background(), "bracewel.net") test.AssertNotError(t, err, "CAA lookup failed") test.Assert(t, len(caas) > 0, "Should have CAA records") test.AssertEquals(t, len(resolvers), 1) @@ -485,14 +485,14 @@ bracewel.net. 0 IN CAA 1 issue "letsencrypt.org" ` test.AssertEquals(t, removeIDExp.ReplaceAllString(resp, " id: XXXX"), expectedResp) - caas, resp, resolvers, err = obj.LookupCAA(context.Background(), "nonexistent.letsencrypt.org") + caas, resp, resolvers, _, err = obj.LookupCAA(context.Background(), "nonexistent.letsencrypt.org") test.AssertNotError(t, err, "CAA lookup failed") test.Assert(t, len(caas) == 0, "Shouldn't have CAA records") test.AssertEquals(t, resolvers[0], "127.0.0.1:4053") expectedResp = "" test.AssertEquals(t, resp, expectedResp) - caas, resp, resolvers, err = obj.LookupCAA(context.Background(), "nxdomain.letsencrypt.org") + caas, resp, resolvers, _, err = obj.LookupCAA(context.Background(), "nxdomain.letsencrypt.org") slices.Sort(resolvers) test.AssertNotError(t, err, "CAA lookup failed") test.Assert(t, len(caas) == 0, "Shouldn't have CAA records") @@ -500,7 +500,7 @@ bracewel.net. 0 IN CAA 1 issue "letsencrypt.org" expectedResp = "" test.AssertEquals(t, resp, expectedResp) - caas, resp, resolvers, err = obj.LookupCAA(context.Background(), "cname.example.com") + caas, resp, resolvers, _, err = obj.LookupCAA(context.Background(), "cname.example.com") test.AssertNotError(t, err, "CAA lookup failed") test.Assert(t, len(caas) > 0, "Should follow CNAME to find CAA") test.AssertEquals(t, resolvers[0], "127.0.0.1:4053") @@ -515,7 +515,7 @@ caa.example.com. 0 IN CAA 1 issue "letsencrypt.org" ` test.AssertEquals(t, removeIDExp.ReplaceAllString(resp, " id: XXXX"), expectedResp) - _, _, resolvers, err = obj.LookupCAA(context.Background(), "gonetld") + _, _, resolvers, _, err = obj.LookupCAA(context.Background(), "gonetld") test.AssertError(t, err, "should fail for TLD NXDOMAIN") test.AssertContains(t, err.Error(), "NXDOMAIN") test.AssertEquals(t, resolvers[0], "127.0.0.1:4053") @@ -678,7 +678,7 @@ func TestRetry(t *testing.T) { testClient := New(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), tc.maxTries, "", blog.UseMock(), tlsConfig) dr := testClient.(*impl) dr.dnsClient = tc.te - _, _, err = dr.LookupTXT(context.Background(), "example.com") + _, _, _, err = dr.LookupTXT(context.Background(), "example.com") if err == errTooManyRequests { t.Errorf("#%d, sent more requests than the test case handles", i) } @@ -711,7 +711,7 @@ func TestRetry(t *testing.T) { dr.dnsClient = &testExchanger{errs: []error{isTempErr, isTempErr, nil}} ctx, cancel := context.WithCancel(context.Background()) cancel() - _, _, err = dr.LookupTXT(ctx, "example.com") + _, _, _, err = dr.LookupTXT(ctx, "example.com") if err == nil || err.Error() != "DNS problem: query timed out (and was canceled) looking up TXT for example.com" { t.Errorf("expected %s, got %s", context.Canceled, err) @@ -720,7 +720,7 @@ func TestRetry(t *testing.T) { dr.dnsClient = &testExchanger{errs: []error{isTempErr, isTempErr, nil}} ctx, cancel = context.WithTimeout(context.Background(), -10*time.Hour) defer cancel() - _, _, err = dr.LookupTXT(ctx, "example.com") + _, _, _, err = dr.LookupTXT(ctx, "example.com") if err == nil || err.Error() != "DNS problem: query timed out looking up TXT for example.com" { t.Errorf("expected %s, got %s", context.DeadlineExceeded, err) @@ -729,7 +729,7 @@ func TestRetry(t *testing.T) { dr.dnsClient = &testExchanger{errs: []error{isTempErr, isTempErr, nil}} ctx, deadlineCancel := context.WithTimeout(context.Background(), -10*time.Hour) deadlineCancel() - _, _, err = dr.LookupTXT(ctx, "example.com") + _, _, _, err = dr.LookupTXT(ctx, "example.com") if err == nil || err.Error() != "DNS problem: query timed out looking up TXT for example.com" { t.Errorf("expected %s, got %s", context.DeadlineExceeded, err) @@ -829,7 +829,7 @@ func TestRotateServerOnErr(t *testing.T) { // servers *all* queries should eventually succeed by being retried against // server "[2606:4700:4700::1111]:53". for range maxTries * 2 { - _, resolvers, err := client.LookupTXT(context.Background(), "example.com") + _, resolvers, _, err := client.LookupTXT(context.Background(), "example.com") test.AssertEquals(t, len(resolvers), 1) test.AssertEquals(t, resolvers[0], "[2606:4700:4700::1111]:53") // Any errors are unexpected - server "[2606:4700:4700::1111]:53" should diff --git a/bdns/mocks.go b/bdns/mocks.go index b72b674a283..53b663bc69e 100644 --- a/bdns/mocks.go +++ b/bdns/mocks.go @@ -19,75 +19,75 @@ type MockClient struct { } // LookupTXT is a mock -func (mock *MockClient) LookupTXT(_ context.Context, hostname string) ([]string, ResolverAddrs, error) { +func (mock *MockClient) LookupTXT(_ context.Context, hostname string) ([]string, ResolverAddrs, bool, error) { // Use the example account-specific label prefix derived from // "https://example.com/acme/acct/ExampleAccount" const accountLabelPrefix = "_ujmmovf2vn55tgye._acme-challenge" if hostname == accountLabelPrefix+".servfail.com" { // Mirror dns-01 servfail behaviour - return nil, ResolverAddrs{"MockClient"}, fmt.Errorf("SERVFAIL") + return nil, ResolverAddrs{"MockClient"}, false, fmt.Errorf("SERVFAIL") } if hostname == accountLabelPrefix+".good-dns01.com" { // Mirror dns-01 good record // base64(sha256("LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0" // + "." + "9jg46WB3rR_AHD-EBXdN7cBkH1WOu0tA3M9fm21mqTI")) - return []string{"LPsIwTo7o8BoG0-vjCyGQGBWSVIPxI-i_X336eUOQZo"}, ResolverAddrs{"MockClient"}, nil + return []string{"LPsIwTo7o8BoG0-vjCyGQGBWSVIPxI-i_X336eUOQZo"}, ResolverAddrs{"MockClient"}, true, nil } if hostname == accountLabelPrefix+".wrong-dns01.com" { // Mirror dns-01 wrong record - return []string{"a"}, ResolverAddrs{"MockClient"}, nil + return []string{"a"}, ResolverAddrs{"MockClient"}, true, nil } if hostname == accountLabelPrefix+".wrong-many-dns01.com" { // Mirror dns-01 wrong-many record - return []string{"a", "b", "c", "d", "e"}, ResolverAddrs{"MockClient"}, nil + return []string{"a", "b", "c", "d", "e"}, ResolverAddrs{"MockClient"}, true, nil } if hostname == accountLabelPrefix+".long-dns01.com" { // Mirror dns-01 long record - return []string{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, ResolverAddrs{"MockClient"}, nil + return []string{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, ResolverAddrs{"MockClient"}, true, nil } if hostname == accountLabelPrefix+".no-authority-dns01.com" { // Mirror dns-01 no-authority good record // base64(sha256("LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0" // + "." + "9jg46WB3rR_AHD-EBXdN7cBkH1WOu0tA3M9fm21mqTI")) - return []string{"LPsIwTo7o8BoG0-vjCyGQGBWSVIPxI-i_X336eUOQZo"}, ResolverAddrs{"MockClient"}, nil + return []string{"LPsIwTo7o8BoG0-vjCyGQGBWSVIPxI-i_X336eUOQZo"}, ResolverAddrs{"MockClient"}, true, nil } if hostname == accountLabelPrefix+".empty-txts.com" { // Mirror dns-01 zero TXT records - return []string{}, ResolverAddrs{"MockClient"}, nil + return []string{}, ResolverAddrs{"MockClient"}, true, nil } if hostname == "_acme-challenge.servfail.com" { - return nil, ResolverAddrs{"MockClient"}, fmt.Errorf("SERVFAIL") + return nil, ResolverAddrs{"MockClient"}, false, fmt.Errorf("SERVFAIL") } if hostname == "_acme-challenge.good-dns01.com" { // base64(sha256("LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0" // + "." + "9jg46WB3rR_AHD-EBXdN7cBkH1WOu0tA3M9fm21mqTI")) // expected token + test account jwk thumbprint - return []string{"LPsIwTo7o8BoG0-vjCyGQGBWSVIPxI-i_X336eUOQZo"}, ResolverAddrs{"MockClient"}, nil + return []string{"LPsIwTo7o8BoG0-vjCyGQGBWSVIPxI-i_X336eUOQZo"}, ResolverAddrs{"MockClient"}, true, nil } if hostname == "_acme-challenge.wrong-dns01.com" { - return []string{"a"}, ResolverAddrs{"MockClient"}, nil + return []string{"a"}, ResolverAddrs{"MockClient"}, true, nil } if hostname == "_acme-challenge.wrong-many-dns01.com" { - return []string{"a", "b", "c", "d", "e"}, ResolverAddrs{"MockClient"}, nil + return []string{"a", "b", "c", "d", "e"}, ResolverAddrs{"MockClient"}, true, nil } if hostname == "_acme-challenge.long-dns01.com" { - return []string{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, ResolverAddrs{"MockClient"}, nil + return []string{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, ResolverAddrs{"MockClient"}, true, nil } if hostname == "_acme-challenge.no-authority-dns01.com" { // base64(sha256("LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0" // + "." + "9jg46WB3rR_AHD-EBXdN7cBkH1WOu0tA3M9fm21mqTI")) // expected token + test account jwk thumbprint - return []string{"LPsIwTo7o8BoG0-vjCyGQGBWSVIPxI-i_X336eUOQZo"}, ResolverAddrs{"MockClient"}, nil + return []string{"LPsIwTo7o8BoG0-vjCyGQGBWSVIPxI-i_X336eUOQZo"}, ResolverAddrs{"MockClient"}, true, nil } // empty-txts.com always returns zero TXT records if hostname == "_acme-challenge.empty-txts.com" { - return []string{}, ResolverAddrs{"MockClient"}, nil + return []string{}, ResolverAddrs{"MockClient"}, true, nil } // Default fallback - return []string{"hostname"}, ResolverAddrs{"MockClient"}, nil + return []string{"hostname"}, ResolverAddrs{"MockClient"}, false, nil } // makeTimeoutError returns a a net.OpError for which Timeout() returns true. @@ -107,13 +107,13 @@ func (t timeoutError) Timeout() bool { } // LookupHost is a mock -func (mock *MockClient) LookupHost(_ context.Context, hostname string) ([]netip.Addr, ResolverAddrs, error) { +func (mock *MockClient) LookupHost(_ context.Context, hostname string) ([]netip.Addr, ResolverAddrs, bool, error) { if hostname == "always.invalid" || hostname == "invalid.invalid" { - return []netip.Addr{}, ResolverAddrs{"MockClient"}, nil + return []netip.Addr{}, ResolverAddrs{"MockClient"}, true, nil } if hostname == "always.timeout" { - return []netip.Addr{}, ResolverAddrs{"MockClient"}, &Error{dns.TypeA, "always.timeout", makeTimeoutError(), -1, nil} + return []netip.Addr{}, ResolverAddrs{"MockClient"}, false, &Error{dns.TypeA, "always.timeout", makeTimeoutError(), -1, nil} } if hostname == "always.error" { err := &net.OpError{ @@ -126,7 +126,7 @@ func (mock *MockClient) LookupHost(_ context.Context, hostname string) ([]netip. m.AuthenticatedData = true m.SetEdns0(4096, false) logDNSError(mock.Log, "mock.server", hostname, m, nil, err) - return []netip.Addr{}, ResolverAddrs{"MockClient"}, &Error{dns.TypeA, hostname, err, -1, nil} + return []netip.Addr{}, ResolverAddrs{"MockClient"}, true, &Error{dns.TypeA, hostname, err, -1, nil} } if hostname == "id.mismatch" { err := dns.ErrId @@ -140,24 +140,24 @@ func (mock *MockClient) LookupHost(_ context.Context, hostname string) ([]netip. record.A = net.ParseIP("127.0.0.1") r.Answer = append(r.Answer, record) logDNSError(mock.Log, "mock.server", hostname, m, r, err) - return []netip.Addr{}, ResolverAddrs{"MockClient"}, &Error{dns.TypeA, hostname, err, -1, nil} + return []netip.Addr{}, ResolverAddrs{"MockClient"}, true, &Error{dns.TypeA, hostname, err, -1, nil} } // dual-homed host with an IPv6 and an IPv4 address if hostname == "ipv4.and.ipv6.localhost" { return []netip.Addr{ netip.MustParseAddr("::1"), netip.MustParseAddr("127.0.0.1"), - }, ResolverAddrs{"MockClient"}, nil + }, ResolverAddrs{"MockClient"}, true, nil } if hostname == "ipv6.localhost" { return []netip.Addr{ netip.MustParseAddr("::1"), - }, ResolverAddrs{"MockClient"}, nil + }, ResolverAddrs{"MockClient"}, true, nil } - return []netip.Addr{netip.MustParseAddr("127.0.0.1")}, ResolverAddrs{"MockClient"}, nil + return []netip.Addr{netip.MustParseAddr("127.0.0.1")}, ResolverAddrs{"MockClient"}, false, nil } // LookupCAA returns mock records for use in tests. -func (mock *MockClient) LookupCAA(_ context.Context, domain string) ([]*dns.CAA, string, ResolverAddrs, error) { - return nil, "", ResolverAddrs{"MockClient"}, nil +func (mock *MockClient) LookupCAA(_ context.Context, domain string) ([]*dns.CAA, string, ResolverAddrs, bool, error) { + return nil, "", ResolverAddrs{"MockClient"}, true, nil } diff --git a/core/objects.go b/core/objects.go index 62e35191120..804d8b096a8 100644 --- a/core/objects.go +++ b/core/objects.go @@ -145,6 +145,12 @@ type ValidationRecord struct { // lookup for AddressUsed. During recursive A and AAAA lookups, a record may // instead look like A:host:port or AAAA:host:port ResolverAddrs []string `json:"resolverAddrs,omitempty"` + + // AD is equivalent to the Authenticated Data bit in DNS responses. It is true + // if the DNS response (either to the TXT query for DNS-based validation, or + // to the A/AAAA queries to lookup IPs for HTTP- and TLS-based validation) was + // DNSSEC-signed and the signature validated. + AD bool `json:"ad,omitempty"` } // Challenge is an aggregate of all data needed for any challenges. diff --git a/va/caa.go b/va/caa.go index 4fc9837db98..679a24ea6c7 100644 --- a/va/caa.go +++ b/va/caa.go @@ -179,6 +179,7 @@ type caaResult struct { criticalUnknown bool dig string resolvers bdns.ResolverAddrs + ad bool err error } @@ -239,7 +240,7 @@ func (va *ValidationAuthorityImpl) parallelCAALookup(ctx context.Context, name s go func(name string, r *caaResult) { r.name = name var records []*dns.CAA - records, r.dig, r.resolvers, r.err = va.dnsClient.LookupCAA(ctx, name) + records, r.dig, r.resolvers, r.ad, r.err = va.dnsClient.LookupCAA(ctx, name) if len(records) > 0 { r.present = true } diff --git a/va/caa_test.go b/va/caa_test.go index 3a83dd1553f..ba66bb2e0d9 100644 --- a/va/caa_test.go +++ b/va/caa_test.go @@ -30,20 +30,20 @@ import ( // 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) LookupTXT(_ context.Context, hostname string) ([]string, bdns.ResolverAddrs, bool, error) { + return nil, bdns.ResolverAddrs{"caaMockDNS"}, false, nil } -func (mock caaMockDNS) LookupHost(_ context.Context, hostname string) ([]netip.Addr, bdns.ResolverAddrs, error) { - return []netip.Addr{netip.MustParseAddr("127.0.0.1")}, bdns.ResolverAddrs{"caaMockDNS"}, nil +func (mock caaMockDNS) LookupHost(_ context.Context, hostname string) ([]netip.Addr, bdns.ResolverAddrs, bool, error) { + return []netip.Addr{netip.MustParseAddr("127.0.0.1")}, bdns.ResolverAddrs{"caaMockDNS"}, false, nil } -func (mock caaMockDNS) LookupCAA(_ context.Context, domain string) ([]*dns.CAA, string, bdns.ResolverAddrs, error) { +func (mock caaMockDNS) LookupCAA(_ context.Context, domain string) ([]*dns.CAA, string, bdns.ResolverAddrs, bool, 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") + return nil, "", bdns.ResolverAddrs{"caaMockDNS"}, false, fmt.Errorf("error") case "reserved.com": record.Tag = "issue" record.Value = "ca.com" @@ -63,11 +63,11 @@ func (mock caaMockDNS) LookupCAA(_ context.Context, domain string) ([]*dns.CAA, results = append(results, &record) case "com": // com has no CAA records. - return nil, "", bdns.ResolverAddrs{"caaMockDNS"}, nil + return nil, "", bdns.ResolverAddrs{"caaMockDNS"}, true, nil case "gonetld": - return nil, "", bdns.ResolverAddrs{"caaMockDNS"}, fmt.Errorf("NXDOMAIN") + return nil, "", bdns.ResolverAddrs{"caaMockDNS"}, false, fmt.Errorf("NXDOMAIN") case "servfail.com", "servfail.present.com": - return results, "", bdns.ResolverAddrs{"caaMockDNS"}, fmt.Errorf("SERVFAIL") + return results, "", bdns.ResolverAddrs{"caaMockDNS"}, false, fmt.Errorf("SERVFAIL") case "multi-crit-present.com": record.Flag = 1 record.Tag = "issue" @@ -189,7 +189,7 @@ func (mock caaMockDNS) LookupCAA(_ context.Context, domain string) ([]*dns.CAA, if len(results) > 0 { response = "foo" } - return results, response, bdns.ResolverAddrs{"caaMockDNS"}, nil + return results, response, bdns.ResolverAddrs{"caaMockDNS"}, true, nil } func TestCAATimeout(t *testing.T) { @@ -595,16 +595,16 @@ var errCAABrokenDNSClient = errors.New("dnsClient is broken") // 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) LookupTXT(_ context.Context, hostname string) ([]string, bdns.ResolverAddrs, bool, error) { + return nil, bdns.ResolverAddrs{"caaBrokenDNS"}, false, errCAABrokenDNSClient } -func (b caaBrokenDNS) LookupHost(_ context.Context, hostname string) ([]netip.Addr, bdns.ResolverAddrs, error) { - return nil, bdns.ResolverAddrs{"caaBrokenDNS"}, errCAABrokenDNSClient +func (b caaBrokenDNS) LookupHost(_ context.Context, hostname string) ([]netip.Addr, bdns.ResolverAddrs, bool, error) { + return nil, bdns.ResolverAddrs{"caaBrokenDNS"}, false, errCAABrokenDNSClient } -func (b caaBrokenDNS) LookupCAA(_ context.Context, domain string) ([]*dns.CAA, string, bdns.ResolverAddrs, error) { - return nil, "", bdns.ResolverAddrs{"caaBrokenDNS"}, errCAABrokenDNSClient +func (b caaBrokenDNS) LookupCAA(_ context.Context, domain string) ([]*dns.CAA, string, bdns.ResolverAddrs, bool, error) { + return nil, "", bdns.ResolverAddrs{"caaBrokenDNS"}, false, errCAABrokenDNSClient } // caaHijackedDNS implements the `dns.DNSClient` interface with a set of useful @@ -613,14 +613,14 @@ func (b caaBrokenDNS) LookupCAA(_ context.Context, domain string) ([]*dns.CAA, s // 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) LookupTXT(_ context.Context, hostname string) ([]string, bdns.ResolverAddrs, bool, error) { + return nil, bdns.ResolverAddrs{"caaHijackedDNS"}, false, nil } -func (h caaHijackedDNS) LookupHost(_ context.Context, hostname string) ([]netip.Addr, bdns.ResolverAddrs, error) { - return []netip.Addr{netip.MustParseAddr("127.0.0.1")}, bdns.ResolverAddrs{"caaHijackedDNS"}, nil +func (h caaHijackedDNS) LookupHost(_ context.Context, hostname string) ([]netip.Addr, bdns.ResolverAddrs, bool, error) { + return []netip.Addr{netip.MustParseAddr("127.0.0.1")}, bdns.ResolverAddrs{"caaHijackedDNS"}, false, nil } -func (h caaHijackedDNS) LookupCAA(_ context.Context, domain string) ([]*dns.CAA, string, bdns.ResolverAddrs, error) { +func (h caaHijackedDNS) LookupCAA(_ context.Context, domain string) ([]*dns.CAA, string, bdns.ResolverAddrs, bool, error) { // These records are altered from their caaMockDNS counterparts. Use this to // tickle remoteValidationFailures. var results []*dns.CAA @@ -631,7 +631,7 @@ func (h caaHijackedDNS) LookupCAA(_ context.Context, domain string) ([]*dns.CAA, record.Value = "other-ca.com" results = append(results, &record) case "present-dns-only.com": - return results, "", bdns.ResolverAddrs{"caaHijackedDNS"}, fmt.Errorf("SERVFAIL") + return results, "", bdns.ResolverAddrs{"caaHijackedDNS"}, false, fmt.Errorf("SERVFAIL") case "satisfiable-wildcard.com": record.Tag = "issuewild" record.Value = ";" @@ -645,7 +645,7 @@ func (h caaHijackedDNS) LookupCAA(_ context.Context, domain string) ([]*dns.CAA, if len(results) > 0 { response = "foo" } - return results, response, bdns.ResolverAddrs{"caaHijackedDNS"}, nil + return results, response, bdns.ResolverAddrs{"caaHijackedDNS"}, false, nil } // parseValidationLogEvent extracts ... from JSON={ ... } in a ValidateChallenge @@ -1234,9 +1234,9 @@ func TestSelectCAA(t *testing.T) { // 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}, + {"", false, nil, nil, false, "", nil, false, nil}, + {"", false, nil, nil, false, "", nil, false, nil}, + {"", false, nil, nil, false, "", nil, false, nil}, } s, err = selectCAA(r) test.Assert(t, s == nil, "set is not nil") @@ -1245,8 +1245,8 @@ func TestSelectCAA(t *testing.T) { // 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}, + {"foo.com", false, nil, nil, false, "", nil, false, errors.New("oops")}, + {"com", true, []*dns.CAA{&expected}, nil, false, "foo", nil, true, nil}, } s, err = selectCAA(r) test.Assert(t, s == nil, "set is not nil") @@ -1256,8 +1256,8 @@ func TestSelectCAA(t *testing.T) { // 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("")}, + {"foo.com", true, []*dns.CAA{&expected}, nil, false, "foo", nil, true, nil}, + {"com", false, nil, nil, false, "", nil, false, errors.New("")}, } s, err = selectCAA(r) test.AssertEquals(t, len(s.issue), 1) @@ -1268,9 +1268,9 @@ func TestSelectCAA(t *testing.T) { // 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}, + {"bar.foo.com", false, []*dns.CAA{}, []*dns.CAA{}, false, "", nil, true, nil}, + {"foo.com", true, []*dns.CAA{&expected}, nil, false, "foo", nil, true, nil}, + {"com", true, []*dns.CAA{&expected}, nil, false, "bar", nil, true, nil}, } s, err = selectCAA(r) test.AssertEquals(t, len(s.issue), 1) diff --git a/va/dns.go b/va/dns.go index ba854f89f44..d00cf33a63b 100644 --- a/va/dns.go +++ b/va/dns.go @@ -23,19 +23,19 @@ import ( // resolution library used by net/http. If there is an error resolving the // hostname, or if no usable IP addresses are available then a berrors.DNSError // instance is returned with a nil netip.Addr slice. -func (va ValidationAuthorityImpl) getAddrs(ctx context.Context, hostname string) ([]netip.Addr, bdns.ResolverAddrs, error) { - addrs, resolvers, err := va.dnsClient.LookupHost(ctx, hostname) +func (va ValidationAuthorityImpl) getAddrs(ctx context.Context, hostname string) ([]netip.Addr, bdns.ResolverAddrs, bool, error) { + addrs, resolvers, ad, err := va.dnsClient.LookupHost(ctx, hostname) if err != nil { - return nil, resolvers, berrors.DNSError("%v", err) + return nil, resolvers, false, berrors.DNSError("%v", err) } if len(addrs) == 0 { // This should be unreachable, as no valid IP addresses being found results // in an error being returned from LookupHost. - return nil, resolvers, berrors.DNSError("No valid IP addresses found for %s", hostname) + return nil, resolvers, false, berrors.DNSError("No valid IP addresses found for %s", hostname) } va.log.Debugf("Resolved addresses for %s: %s", hostname, addrs) - return addrs, resolvers, nil + return addrs, resolvers, ad, nil } // availableAddresses takes a ValidationRecord and splits the AddressesResolved @@ -109,7 +109,7 @@ func (va *ValidationAuthorityImpl) validateDNS(ctx context.Context, ident identi challengeSubdomain := fmt.Sprintf("%s.%s", challengePrefix, ident.Value) // Look for the required record in the DNS - txts, resolvers, err := va.dnsClient.LookupTXT(ctx, challengeSubdomain) + txts, resolvers, ad, err := va.dnsClient.LookupTXT(ctx, challengeSubdomain) if err != nil { return nil, berrors.DNSError("%s", err) } @@ -124,7 +124,7 @@ func (va *ValidationAuthorityImpl) validateDNS(ctx context.Context, ident identi for _, element := range txts { if subtle.ConstantTimeCompare([]byte(element), []byte(authorizedKeysDigest)) == 1 { // Successful challenge validation - return []core.ValidationRecord{{Hostname: ident.Value, ResolverAddrs: resolvers}}, nil + return []core.ValidationRecord{{Hostname: ident.Value, ResolverAddrs: resolvers, AD: ad}}, nil } } diff --git a/va/http.go b/va/http.go index e7b0ec3043e..5f701bd8695 100644 --- a/va/http.go +++ b/va/http.go @@ -178,8 +178,10 @@ type httpValidationTarget struct { next []netip.Addr // the current IP address being used for validation (if any) cur netip.Addr - // the DNS resolver(s) that will attempt to fulfill the validation request + // the DNS resolver(s) that were used to locate the IPs listed above resolvers bdns.ResolverAddrs + // whether the DNS query giving the IPs above was DNSSEC-validated + ad bool } // nextIP changes the cur IP by removing the first entry from the next slice and @@ -210,14 +212,15 @@ func (va *ValidationAuthorityImpl) newHTTPValidationTarget( query string) (*httpValidationTarget, error) { var addrs []netip.Addr var resolvers bdns.ResolverAddrs + var ad bool switch ident.Type { case identifier.TypeDNS: // Resolve IP addresses for the identifier - dnsAddrs, dnsResolvers, err := va.getAddrs(ctx, ident.Value) + dnsAddrs, dnsResolvers, dnsAD, err := va.getAddrs(ctx, ident.Value) if err != nil { return nil, err } - addrs, resolvers = dnsAddrs, dnsResolvers + addrs, resolvers, ad = dnsAddrs, dnsResolvers, dnsAD case identifier.TypeIP: netIP, err := netip.ParseAddr(ident.Value) if err != nil { @@ -235,6 +238,7 @@ func (va *ValidationAuthorityImpl) newHTTPValidationTarget( query: query, available: addrs, resolvers: resolvers, + ad: ad, } // Separate the addresses into the available v4 and v6 addresses @@ -384,6 +388,7 @@ func (va *ValidationAuthorityImpl) setupHTTPValidation( AddressesResolved: target.available, URL: reqURL, ResolverAddrs: target.resolvers, + AD: target.ad, } // Get the target IP to build a preresolved dialer with diff --git a/va/http_test.go b/va/http_test.go index c0aa497b74b..23030ec290d 100644 --- a/va/http_test.go +++ b/va/http_test.go @@ -61,8 +61,8 @@ type dnsMockReturnsUnroutable struct { *bdns.MockClient } -func (mock dnsMockReturnsUnroutable) LookupHost(_ context.Context, hostname string) ([]netip.Addr, bdns.ResolverAddrs, error) { - return []netip.Addr{netip.MustParseAddr("64.112.117.254")}, bdns.ResolverAddrs{"dnsMockReturnsUnroutable"}, nil +func (mock dnsMockReturnsUnroutable) LookupHost(_ context.Context, hostname string) ([]netip.Addr, bdns.ResolverAddrs, bool, error) { + return []netip.Addr{netip.MustParseAddr("64.112.117.254")}, bdns.ResolverAddrs{"dnsMockReturnsUnroutable"}, true, nil } // TestDialerTimeout tests that the preresolvedDialer's DialContext @@ -535,6 +535,7 @@ func TestSetupHTTPValidation(t *testing.T) { AddressesResolved: []netip.Addr{netip.MustParseAddr("::1"), netip.MustParseAddr("127.0.0.1")}, AddressUsed: netip.MustParseAddr("::1"), ResolverAddrs: []string{"MockClient"}, + AD: true, }, ExpectedDialer: &preresolvedDialer{ ip: netip.MustParseAddr("::1"), @@ -553,6 +554,7 @@ func TestSetupHTTPValidation(t *testing.T) { AddressesResolved: []netip.Addr{netip.MustParseAddr("::1"), netip.MustParseAddr("127.0.0.1")}, AddressUsed: netip.MustParseAddr("::1"), ResolverAddrs: []string{"MockClient"}, + AD: true, }, ExpectedDialer: &preresolvedDialer{ ip: netip.MustParseAddr("::1"), @@ -1045,6 +1047,7 @@ func TestFetchHTTP(t *testing.T) { AddressesResolved: []netip.Addr{netip.MustParseAddr("::1")}, AddressUsed: netip.MustParseAddr("::1"), ResolverAddrs: []string{"MockClient"}, + AD: true, }, { Hostname: "::1", @@ -1138,6 +1141,7 @@ func TestFetchHTTP(t *testing.T) { AddressesResolved: []netip.Addr{netip.MustParseAddr("::1")}, AddressUsed: netip.MustParseAddr("::1"), ResolverAddrs: []string{"MockClient"}, + AD: true, }, }, }, @@ -1155,6 +1159,7 @@ func TestFetchHTTP(t *testing.T) { // The first validation record should have used the IPv6 addr AddressUsed: netip.MustParseAddr("::1"), ResolverAddrs: []string{"MockClient"}, + AD: true, }, { Hostname: "ipv4.and.ipv6.localhost", @@ -1164,6 +1169,7 @@ func TestFetchHTTP(t *testing.T) { // The second validation record should have used the IPv4 addr as a fallback AddressUsed: netip.MustParseAddr("127.0.0.1"), ResolverAddrs: []string{"MockClient"}, + AD: true, }, }, }, diff --git a/va/tlsalpn.go b/va/tlsalpn.go index f297d16adf7..bee1735c07a 100644 --- a/va/tlsalpn.go +++ b/va/tlsalpn.go @@ -72,12 +72,13 @@ func (va *ValidationAuthorityImpl) tryGetChallengeCert( switch ident.Type { case identifier.TypeDNS: // Resolve IP addresses for the identifier - dnsAddrs, dnsResolvers, err := va.getAddrs(ctx, ident.Value) + dnsAddrs, dnsResolvers, ad, err := va.getAddrs(ctx, ident.Value) if err != nil { return nil, nil, validationRecord, err } addrs, validationRecord.ResolverAddrs = dnsAddrs, dnsResolvers validationRecord.AddressesResolved = addrs + validationRecord.AD = ad case identifier.TypeIP: netIP, err := netip.ParseAddr(ident.Value) if err != nil { @@ -280,14 +281,14 @@ func checkAcceptableExtensions(exts []pkix.Extension, requiredOIDs []asn1.Object for _, ext := range exts { if oidSeen[ext.Id.String()] { - return fmt.Errorf("Extension OID %s seen twice", ext.Id) + return fmt.Errorf("extension OID %s seen twice", ext.Id) } oidSeen[ext.Id.String()] = true } for _, required := range requiredOIDs { if !oidSeen[required.String()] { - return fmt.Errorf("Required extension OID %s is not present", required) + return fmt.Errorf("required extension OID %s is not present", required) } } diff --git a/va/tlsalpn_test.go b/va/tlsalpn_test.go index 31de4c2ad56..5ff27b4e744 100644 --- a/va/tlsalpn_test.go +++ b/va/tlsalpn_test.go @@ -436,7 +436,7 @@ func TestTLSALPN01ObsoleteFailure(t *testing.T) { _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization) test.AssertNotNil(t, err, "expected validation to fail") - test.AssertContains(t, err.Error(), "Required extension OID 1.3.6.1.5.5.7.1.31 is not present") + test.AssertContains(t, err.Error(), "required extension OID 1.3.6.1.5.5.7.1.31 is not present") } func TestValidateTLSALPN01BadChallenge(t *testing.T) { @@ -846,7 +846,7 @@ func TestAcceptableExtensions(t *testing.T) { onlyUnexpectedExt := []pkix.Extension{weirdExt} err = checkAcceptableExtensions(onlyUnexpectedExt, requireAcmeAndSAN) test.AssertError(t, err, "Missing required extensions") - test.AssertContains(t, err.Error(), "Required extension OID 1.3.6.1.5.5.7.1.31 is not present") + test.AssertContains(t, err.Error(), "required extension OID 1.3.6.1.5.5.7.1.31 is not present") okayExts := []pkix.Extension{acmeExtension, subjectAltName} err = checkAcceptableExtensions(okayExts, requireAcmeAndSAN) diff --git a/wfe2/wfe.go b/wfe2/wfe.go index efe6aa90051..c70030af872 100644 --- a/wfe2/wfe.go +++ b/wfe2/wfe.go @@ -1242,6 +1242,7 @@ func (wfe *WebFrontEndImpl) prepChallengeForDisplay( // This field is not useful for the client, only internal debugging, for idx := range challenge.ValidationRecord { challenge.ValidationRecord[idx].ResolverAddrs = nil + challenge.ValidationRecord[idx].AD = false } } From baea2d0aff359e99af340b9decc923d7f15ac70b Mon Sep 17 00:00:00 2001 From: Aaron Gable Date: Fri, 24 Oct 2025 16:23:05 -0700 Subject: [PATCH 2/2] And log it with CAA logs --- va/caa.go | 14 ++++++++------ va/caa_test.go | 20 ++++++++++---------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/va/caa.go b/va/caa.go index 679a24ea6c7..345b5e81709 100644 --- a/va/caa.go +++ b/va/caa.go @@ -154,13 +154,13 @@ func (va *ValidationAuthorityImpl) checkCAA( return errors.New("expected validationMethod or accountURIID not provided to checkCAA") } - foundAt, valid, response, err := va.checkCAARecords(ctx, ident, params) + foundAt, valid, response, ad, err := va.checkCAARecords(ctx, ident, params) if err != nil { return berrors.DNSError("%s", err) } - va.log.AuditInfof("Checked CAA records for %s, [Present: %t, Account ID: %d, Challenge: %s, Valid for issuance: %t, Found at: %q] Response=%q", - ident.Value, foundAt != "", params.accountURIID, params.validationMethod, valid, foundAt, response) + va.log.AuditInfof("Checked CAA records for %s, [Present: %t, Account ID: %d, Challenge: %s, Valid for issuance: %t, Found at: %q, AD: %t] Response=%q", + ident.Value, foundAt != "", params.accountURIID, params.validationMethod, valid, foundAt, ad, response) if !valid { return berrors.CAAError("CAA record for %s prevents issuance", foundAt) } @@ -306,7 +306,7 @@ func (va *ValidationAuthorityImpl) getCAA(ctx context.Context, hostname string) func (va *ValidationAuthorityImpl) checkCAARecords( ctx context.Context, ident identifier.ACMEIdentifier, - params *caaParams) (string, bool, string, error) { + params *caaParams) (string, bool, string, bool, error) { hostname := strings.ToLower(ident.Value) // If this is a wildcard name, remove the prefix var wildcard bool @@ -316,14 +316,16 @@ func (va *ValidationAuthorityImpl) checkCAARecords( } caaSet, err := va.getCAA(ctx, hostname) if err != nil { - return "", false, "", err + return "", false, "", false, err } raw := "" + ad := false if caaSet != nil { raw = caaSet.dig + ad = caaSet.ad } valid, foundAt := va.validateCAA(caaSet, wildcard, params) - return foundAt, valid, raw, nil + return foundAt, valid, raw, ad, nil } // validateCAA checks a provided *caaResult. When the wildcard argument is true diff --git a/va/caa_test.go b/va/caa_test.go index ba66bb2e0d9..5c7522e3d07 100644 --- a/va/caa_test.go +++ b/va/caa_test.go @@ -424,7 +424,7 @@ func TestCAAChecking(t *testing.T) { defer mockLog.Clear() t.Run(caaTest.Name, func(t *testing.T) { ident := identifier.NewDNS(caaTest.Domain) - foundAt, valid, _, err := va.checkCAARecords(ctx, ident, params) + foundAt, valid, _, _, err := va.checkCAARecords(ctx, ident, params) if err != nil { t.Errorf("checkCAARecords error for %s: %s", caaTest.Domain, err) } @@ -452,55 +452,55 @@ func TestCAALogging(t *testing.T) { 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\"", + 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\", AD: true] 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\"", + 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\", AD: true] 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\"", + 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\", AD: true] 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\"", + 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\", AD: true] 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\"", + 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\", AD: true] 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\"", + 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\", AD: true] 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\"", + 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\", AD: true] 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\"", + 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\", AD: true] 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\"", + 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\", AD: true] Response=\"foo\"", }, }