diff --git a/cmd/boulder-ra/main.go b/cmd/boulder-ra/main.go index 45d142e0bf4..9710e4b5176 100644 --- a/cmd/boulder-ra/main.go +++ b/cmd/boulder-ra/main.go @@ -25,6 +25,7 @@ import ( "github.com/letsencrypt/boulder/ratelimits" bredis "github.com/letsencrypt/boulder/redis" sapb "github.com/letsencrypt/boulder/sa/proto" + "github.com/letsencrypt/boulder/va" vapb "github.com/letsencrypt/boulder/va/proto" ) @@ -288,7 +289,6 @@ func main() { authorizationLifetime, pendingAuthorizationLifetime, pubc, - caaClient, c.RA.OrderLifetime.Duration, c.RA.FinalizeTimeout.Duration, ctp, @@ -301,7 +301,10 @@ func main() { cmd.FailOnError(policyErr, "Couldn't load rate limit policies file") rai.PA = pa - rai.VA = vac + rai.VA = va.RemoteClients{ + VAClient: vac, + CAAClient: caaClient, + } rai.CA = cac rai.OCSP = ocspc rai.SA = sac diff --git a/features/features.go b/features/features.go index 20986c8d22b..3a7f1369096 100644 --- a/features/features.go +++ b/features/features.go @@ -108,6 +108,24 @@ type Config struct { // functionality (valid authz reuse) while letting us simplify our code by // removing pending authz reuse. NoPendingAuthzReuse bool + + // EnforceMPIC enforces SC-067 V3: Require Multi-Perspective Issuance + // Corroboration by: + // - Requiring at least three distinct perspectives, as outlined in the + // "Phased Implementation Timeline" in BRs section 3.2.2.9 ("Effective + // March 15, 2025"). + // - Ensuring that corroborating (passing) perspectives reside in at least + // 2 distinct Regional Internet Registries (RIRs) per the "Phased + // Implementation Timeline" in BRs section 3.2.2.9 ("Effective March 15, + // 2026"). + // - Including an MPIC summary consisting of: passing perspectives, failing + // perspectives, passing RIRs, and a quorum met for issuance (e.g., 2/3 + // or 3/3) in each validation audit log event, per BRs Section 5.4.1, + // Requirement 2.8. + // + // This feature flag also causes CAA checks to happen after all remote VAs + // have passed DCV. + EnforceMPIC bool } var fMu = new(sync.RWMutex) diff --git a/ra/ra.go b/ra/ra.go index ed559be508c..64d494c7473 100644 --- a/ra/ra.go +++ b/ra/ra.go @@ -23,7 +23,6 @@ import ( "github.com/jmhodges/clock" "github.com/prometheus/client_golang/prometheus" "golang.org/x/crypto/ocsp" - "google.golang.org/grpc" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/emptypb" @@ -52,6 +51,7 @@ import ( "github.com/letsencrypt/boulder/ratelimits" "github.com/letsencrypt/boulder/revocation" sapb "github.com/letsencrypt/boulder/sa/proto" + "github.com/letsencrypt/boulder/va" vapb "github.com/letsencrypt/boulder/va/proto" "github.com/letsencrypt/boulder/web" @@ -68,14 +68,6 @@ var ( caaRecheckDuration = -7 * time.Hour ) -type caaChecker interface { - IsCAAValid( - ctx context.Context, - in *vapb.IsCAAValidRequest, - opts ...grpc.CallOption, - ) (*vapb.IsCAAValidResponse, error) -} - // RegistrationAuthorityImpl defines an RA. // // NOTE: All of the fields in RegistrationAuthorityImpl need to be @@ -84,11 +76,10 @@ type RegistrationAuthorityImpl struct { rapb.UnsafeRegistrationAuthorityServer CA capb.CertificateAuthorityClient OCSP capb.OCSPGeneratorClient - VA vapb.VAClient + VA va.RemoteClients SA sapb.StorageAuthorityClient PA core.PolicyAuthority publisher pubpb.PublisherClient - caa caaChecker clk clock.Clock log blog.Logger @@ -140,7 +131,6 @@ func NewRegistrationAuthorityImpl( authorizationLifetime time.Duration, pendingAuthorizationLifetime time.Duration, pubc pubpb.PublisherClient, - caaClient caaChecker, orderLifetime time.Duration, finalizeTimeout time.Duration, ctp *ctpolicy.CTPolicy, @@ -265,7 +255,6 @@ func NewRegistrationAuthorityImpl( txnBuilder: txnBuilder, maxNames: maxNames, publisher: pubc, - caa: caaClient, orderLifetime: orderLifetime, finalizeTimeout: finalizeTimeout, ctpolicy: ctp, @@ -849,12 +838,21 @@ func (ra *RegistrationAuthorityImpl) recheckCAA(ctx context.Context, authzs []*c } return } - - resp, err := ra.caa.IsCAAValid(ctx, &vapb.IsCAAValidRequest{ - Domain: name, - ValidationMethod: method, - AccountURIID: authz.RegistrationID, - }) + var resp *vapb.IsCAAValidResponse + var err error + if !features.Get().EnforceMPIC { + resp, err = ra.VA.IsCAAValid(ctx, &vapb.IsCAAValidRequest{ + Domain: name, + ValidationMethod: method, + AccountURIID: authz.RegistrationID, + }) + } else { + resp, err = ra.VA.DoCAA(ctx, &vapb.IsCAAValidRequest{ + Domain: name, + ValidationMethod: method, + AccountURIID: authz.RegistrationID, + }) + } if err != nil { ra.log.AuditErrf("Rechecking CAA: %s", err) err = berrors.InternalServerError( @@ -1832,6 +1830,35 @@ func (ra *RegistrationAuthorityImpl) resetAccountPausingLimit(ctx context.Contex } } +// doDCVAndCAA performs DCV and CAA checks. When EnforceMPIC is enabled, the +// checks are executed sequentially: DCV is performed first and CAA is only +// checked if DCV is successful. Validation records from the DCV check are +// returned even if the CAA check fails. When EnforceMPIC is disabled, DCV and +// CAA checks are performed in the same request. +func (ra *RegistrationAuthorityImpl) checkDCVAndCAA(ctx context.Context, dcvReq *vapb.PerformValidationRequest, caaReq *vapb.IsCAAValidRequest) (*corepb.ProblemDetails, []*corepb.ValidationRecord, error) { + if !features.Get().EnforceMPIC { + performValidationRes, err := ra.VA.PerformValidation(ctx, dcvReq) + if err != nil { + return nil, nil, err + } + return performValidationRes.Problem, performValidationRes.Records, nil + } else { + doDCVRes, err := ra.VA.DoDCV(ctx, dcvReq) + if err != nil { + return nil, nil, err + } + if doDCVRes.Problem != nil { + return doDCVRes.Problem, doDCVRes.Records, nil + } + + doCAAResp, err := ra.VA.IsCAAValid(ctx, caaReq) + if err != nil { + return nil, nil, err + } + return doCAAResp.Problem, doDCVRes.Records, nil + } +} + // PerformValidation initiates validation for a specific challenge associated // with the given base authorization. The authorization and challenge are // updated based on the results. @@ -1916,32 +1943,37 @@ func (ra *RegistrationAuthorityImpl) PerformValidation( copy(challenges, authz.Challenges) authz.Challenges = challenges chall, _ := bgrpc.ChallengeToPB(authz.Challenges[challIndex]) - req := vapb.PerformValidationRequest{ - DnsName: authz.Identifier.Value, - Challenge: chall, - Authz: &vapb.AuthzMeta{ - Id: authz.ID, - RegID: authz.RegistrationID, + checkProb, checkRecords, err := ra.checkDCVAndCAA( + vaCtx, + &vapb.PerformValidationRequest{ + DnsName: authz.Identifier.Value, + Challenge: chall, + Authz: &vapb.AuthzMeta{Id: authz.ID, RegID: authz.RegistrationID}, + ExpectedKeyAuthorization: expectedKeyAuthorization, }, - ExpectedKeyAuthorization: expectedKeyAuthorization, - } - res, err := ra.VA.PerformValidation(vaCtx, &req) + &vapb.IsCAAValidRequest{ + Domain: authz.Identifier.Value, + ValidationMethod: chall.Type, + AccountURIID: authz.RegistrationID, + AuthzID: authz.ID, + }, + ) challenge := &authz.Challenges[challIndex] var prob *probs.ProblemDetails if err != nil { prob = probs.ServerInternal("Could not communicate with VA") ra.log.AuditErrf("Could not communicate with VA: %s", err) } else { - if res.Problem != nil { - prob, err = bgrpc.PBToProblemDetails(res.Problem) + if checkProb != nil { + prob, err = bgrpc.PBToProblemDetails(checkProb) if err != nil { prob = probs.ServerInternal("Could not communicate with VA") ra.log.AuditErrf("Could not communicate with VA: %s", err) } } // Save the updated records - records := make([]core.ValidationRecord, len(res.Records)) - for i, r := range res.Records { + records := make([]core.ValidationRecord, len(checkRecords)) + for i, r := range checkRecords { records[i], err = bgrpc.PBToValidationRecord(r) if err != nil { prob = probs.ServerInternal("Records for validation corrupt") diff --git a/ra/ra_test.go b/ra/ra_test.go index 25c6b960157..336313a32e2 100644 --- a/ra/ra_test.go +++ b/ra/ra_test.go @@ -64,6 +64,7 @@ import ( "github.com/letsencrypt/boulder/test" isa "github.com/letsencrypt/boulder/test/inmem/sa" "github.com/letsencrypt/boulder/test/vars" + "github.com/letsencrypt/boulder/va" vapb "github.com/letsencrypt/boulder/va/proto" ) @@ -156,14 +157,50 @@ func numAuthorizations(o *corepb.Order) int { } type DummyValidationAuthority struct { - performValidationRequest chan *vapb.PerformValidationRequest - PerformValidationRequestResultError error - PerformValidationRequestResultReturn *vapb.ValidationResult + doDCVRequest chan *vapb.PerformValidationRequest + doDCVError error + doDCVResult *vapb.ValidationResult + + doCAARequest chan *vapb.IsCAAValidRequest + doCAAError error + doCAAResponse *vapb.IsCAAValidResponse } func (dva *DummyValidationAuthority) PerformValidation(ctx context.Context, req *vapb.PerformValidationRequest, _ ...grpc.CallOption) (*vapb.ValidationResult, error) { - dva.performValidationRequest <- req - return dva.PerformValidationRequestResultReturn, dva.PerformValidationRequestResultError + dcvRes, err := dva.DoDCV(ctx, req) + if err != nil { + return nil, err + } + if dcvRes.Problem != nil { + return dcvRes, nil + } + caaResp, err := dva.DoCAA(ctx, &vapb.IsCAAValidRequest{ + Domain: req.DnsName, + ValidationMethod: req.Challenge.Type, + AccountURIID: req.Authz.RegID, + AuthzID: req.Authz.Id, + }) + if err != nil { + return nil, err + } + return &vapb.ValidationResult{ + Records: dcvRes.Records, + Problem: caaResp.Problem, + }, nil +} + +func (dva *DummyValidationAuthority) IsCAAValid(ctx context.Context, req *vapb.IsCAAValidRequest, _ ...grpc.CallOption) (*vapb.IsCAAValidResponse, error) { + return nil, status.Error(codes.Unimplemented, "IsCAAValid not implemented") +} + +func (dva *DummyValidationAuthority) DoDCV(ctx context.Context, req *vapb.PerformValidationRequest, _ ...grpc.CallOption) (*vapb.ValidationResult, error) { + dva.doDCVRequest <- req + return dva.doDCVResult, dva.doDCVError +} + +func (dva *DummyValidationAuthority) DoCAA(ctx context.Context, req *vapb.IsCAAValidRequest, _ ...grpc.CallOption) (*vapb.IsCAAValidResponse, error) { + dva.doCAARequest <- req + return dva.doCAAResponse, dva.doCAAError } var ( @@ -311,9 +348,11 @@ func initAuthorities(t *testing.T) (*DummyValidationAuthority, sapb.StorageAutho saDBCleanUp := test.ResetBoulderTestDatabase(t) - va := &DummyValidationAuthority{ - performValidationRequest: make(chan *vapb.PerformValidationRequest, 1), + dummyVA := &DummyValidationAuthority{ + doDCVRequest: make(chan *vapb.PerformValidationRequest, 1), + doCAARequest: make(chan *vapb.IsCAAValidRequest, 1), } + va := va.RemoteClients{VAClient: dummyVA, CAAClient: dummyVA} pa, err := policy.New(map[core.AcmeChallenge]bool{ core.ChallengeTypeHTTP01: true, @@ -363,7 +402,7 @@ func initAuthorities(t *testing.T) (*DummyValidationAuthority, sapb.StorageAutho fc, log, stats, 1, testKeyPolicy, limiter, txnBuilder, 100, 300*24*time.Hour, 7*24*time.Hour, - nil, noopCAA{}, + nil, 7*24*time.Hour, 5*time.Minute, ctp, nil, nil) ra.SA = sa @@ -371,7 +410,7 @@ func initAuthorities(t *testing.T) (*DummyValidationAuthority, sapb.StorageAutho ra.CA = ca ra.OCSP = &mocks.MockOCSPGenerator{} ra.PA = pa - return va, sa, ra, rlSource, fc, cleanUp + return dummyVA, sa, ra, rlSource, fc, cleanUp } func TestValidateContacts(t *testing.T) { @@ -689,7 +728,7 @@ func TestPerformValidationAlreadyValid(t *testing.T) { authzPB, err := bgrpc.AuthzToPB(authz) test.AssertNotError(t, err, "bgrpc.AuthzToPB failed") - va.PerformValidationRequestResultReturn = &vapb.ValidationResult{ + va.doDCVResult = &vapb.ValidationResult{ Records: []*corepb.ValidationRecord{ { AddressUsed: []byte("192.168.0.1"), @@ -700,6 +739,7 @@ func TestPerformValidationAlreadyValid(t *testing.T) { }, Problem: nil, } + va.doCAAResponse = &vapb.IsCAAValidResponse{Problem: nil} // A subsequent call to perform validation should return nil due // to being short-circuited because of valid authz reuse. @@ -718,7 +758,7 @@ func TestPerformValidationSuccess(t *testing.T) { // We know this is OK because of TestNewAuthorization authzPB := createPendingAuthorization(t, sa, Identifier, fc.Now().Add(12*time.Hour)) - va.PerformValidationRequestResultReturn = &vapb.ValidationResult{ + va.doDCVResult = &vapb.ValidationResult{ Records: []*corepb.ValidationRecord{ { AddressUsed: []byte("192.168.0.1"), @@ -730,6 +770,7 @@ func TestPerformValidationSuccess(t *testing.T) { }, Problem: nil, } + va.doCAAResponse = &vapb.IsCAAValidResponse{Problem: nil} now := fc.Now() challIdx := dnsChallIdx(t, authzPB.Challenges) @@ -741,7 +782,7 @@ func TestPerformValidationSuccess(t *testing.T) { var vaRequest *vapb.PerformValidationRequest select { - case r := <-va.performValidationRequest: + case r := <-va.doDCVRequest: vaRequest = r case <-time.After(time.Second): t.Fatal("Timed out waiting for DummyValidationAuthority.PerformValidation to complete") @@ -822,7 +863,7 @@ func TestPerformValidation_FailedValidationsTriggerPauseIdentifiersRatelimit(t * // Now a failed validation should result in the identifier being paused // due to the strict ratelimit. - va.PerformValidationRequestResultReturn = &vapb.ValidationResult{ + va.doDCVResult = &vapb.ValidationResult{ Records: []*corepb.ValidationRecord{ { AddressUsed: []byte("192.168.0.1"), @@ -832,6 +873,9 @@ func TestPerformValidation_FailedValidationsTriggerPauseIdentifiersRatelimit(t * ResolverAddrs: []string{"rebound"}, }, }, + Problem: nil, + } + va.doCAAResponse = &vapb.IsCAAValidResponse{ Problem: &corepb.ProblemDetails{ Detail: fmt.Sprintf("CAA invalid for %s", domain), }, @@ -893,8 +937,7 @@ func TestPerformValidation_FailedThenSuccessfulValidationResetsPauseIdentifiersR }) test.AssertNotError(t, err, "updating rate limit bucket") - // Now a successful validation should reset the rate limit bucket. - va.PerformValidationRequestResultReturn = &vapb.ValidationResult{ + va.doDCVResult = &vapb.ValidationResult{ Records: []*corepb.ValidationRecord{ { AddressUsed: []byte("192.168.0.1"), @@ -906,6 +949,7 @@ func TestPerformValidation_FailedThenSuccessfulValidationResetsPauseIdentifiersR }, Problem: nil, } + va.doCAAResponse = &vapb.IsCAAValidResponse{Problem: nil} _, err = ra.PerformValidation(ctx, &rapb.PerformValidationRequest{ Authz: authzPB, @@ -931,7 +975,7 @@ func TestPerformValidationVAError(t *testing.T) { authzPB := createPendingAuthorization(t, sa, Identifier, fc.Now().Add(12*time.Hour)) - va.PerformValidationRequestResultError = fmt.Errorf("Something went wrong") + va.doDCVError = fmt.Errorf("Something went wrong") challIdx := dnsChallIdx(t, authzPB.Challenges) authzPB, err := ra.PerformValidation(ctx, &rapb.PerformValidationRequest{ @@ -943,7 +987,7 @@ func TestPerformValidationVAError(t *testing.T) { var vaRequest *vapb.PerformValidationRequest select { - case r := <-va.performValidationRequest: + case r := <-va.doDCVRequest: vaRequest = r case <-time.After(time.Second): t.Fatal("Timed out waiting for DummyValidationAuthority.PerformValidation to complete") @@ -1668,7 +1712,7 @@ func TestDeactivateRegistration(t *testing.T) { test.AssertEquals(t, dbReg.Status, string(core.StatusDeactivated)) } -// noopCAA implements caaChecker, always returning nil +// noopCAA implements vapb.CAAClient, always returning nil type noopCAA struct{} func (cr noopCAA) IsCAAValid( @@ -1679,8 +1723,16 @@ func (cr noopCAA) IsCAAValid( return &vapb.IsCAAValidResponse{}, nil } -// caaRecorder implements caaChecker, always returning nil, but recording the -// names it was called for. +func (cr noopCAA) DoCAA( + ctx context.Context, + in *vapb.IsCAAValidRequest, + opts ...grpc.CallOption, +) (*vapb.IsCAAValidResponse, error) { + return &vapb.IsCAAValidResponse{}, nil +} + +// caaRecorder implements vapb.CAAClient, always returning nil, but recording +// the names it was called for. type caaRecorder struct { sync.Mutex names map[string]bool @@ -1697,13 +1749,24 @@ func (cr *caaRecorder) IsCAAValid( return &vapb.IsCAAValidResponse{}, nil } +func (cr *caaRecorder) DoCAA( + ctx context.Context, + in *vapb.IsCAAValidRequest, + opts ...grpc.CallOption, +) (*vapb.IsCAAValidResponse, error) { + cr.Lock() + defer cr.Unlock() + cr.names[in.Domain] = true + return &vapb.IsCAAValidResponse{}, nil +} + // Test that the right set of domain names have their CAA rechecked, based on // their `Validated` (attemptedAt in the database) timestamp. func TestRecheckCAADates(t *testing.T) { _, _, ra, _, fc, cleanUp := initAuthorities(t) defer cleanUp() recorder := &caaRecorder{names: make(map[string]bool)} - ra.caa = recorder + ra.VA = va.RemoteClients{CAAClient: recorder} ra.authorizationLifetime = 15 * time.Hour recentValidated := fc.Now().Add(-1 * time.Hour) @@ -1889,6 +1952,27 @@ func (cf *caaFailer) IsCAAValid( return cvrpb, nil } +func (cf *caaFailer) DoCAA( + ctx context.Context, + in *vapb.IsCAAValidRequest, + opts ...grpc.CallOption, +) (*vapb.IsCAAValidResponse, error) { + cvrpb := &vapb.IsCAAValidResponse{} + switch in.Domain { + case "a.com": + cvrpb.Problem = &corepb.ProblemDetails{ + Detail: "CAA invalid for a.com", + } + case "c.com": + cvrpb.Problem = &corepb.ProblemDetails{ + Detail: "CAA invalid for c.com", + } + case "d.com": + return nil, fmt.Errorf("Error checking CAA for d.com") + } + return cvrpb, nil +} + func TestRecheckCAAEmpty(t *testing.T) { _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() @@ -1906,6 +1990,7 @@ func makeHTTP01Authorization(domain string) *core.Authorization { func TestRecheckCAASuccess(t *testing.T) { _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() + ra.VA = va.RemoteClients{CAAClient: &noopCAA{}} authzs := []*core.Authorization{ makeHTTP01Authorization("a.com"), makeHTTP01Authorization("b.com"), @@ -1918,7 +2003,7 @@ func TestRecheckCAASuccess(t *testing.T) { func TestRecheckCAAFail(t *testing.T) { _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() - ra.caa = &caaFailer{} + ra.VA = va.RemoteClients{CAAClient: &caaFailer{}} authzs := []*core.Authorization{ makeHTTP01Authorization("a.com"), makeHTTP01Authorization("b.com"), @@ -1969,7 +2054,7 @@ func TestRecheckCAAFail(t *testing.T) { func TestRecheckCAAInternalServerError(t *testing.T) { _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() - ra.caa = &caaFailer{} + ra.VA = va.RemoteClients{CAAClient: &caaFailer{}} authzs := []*core.Authorization{ makeHTTP01Authorization("a.com"), makeHTTP01Authorization("b.com"), @@ -3432,6 +3517,7 @@ func TestIssueCertificateAuditLog(t *testing.T) { func TestIssueCertificateCAACheckLog(t *testing.T) { _, sa, ra, _, fc, cleanUp := initAuthorities(t) defer cleanUp() + ra.VA = va.RemoteClients{CAAClient: &noopCAA{}} exp := fc.Now().Add(24 * time.Hour) recent := fc.Now().Add(-1 * time.Hour) diff --git a/test/config-next/ra.json b/test/config-next/ra.json index 8ed02cfb8c1..a6692c7bd89 100644 --- a/test/config-next/ra.json +++ b/test/config-next/ra.json @@ -129,7 +129,8 @@ "AsyncFinalize": true, "UseKvLimitsForNewOrder": true, "AutomaticallyPauseZombieClients": true, - "NoPendingAuthzReuse": true + "NoPendingAuthzReuse": true, + "EnforceMPIC": true }, "ctLogs": { "stagger": "500ms", diff --git a/test/config-next/remoteva-a.json b/test/config-next/remoteva-a.json index d50c08a5d3b..1967643ceff 100644 --- a/test/config-next/remoteva-a.json +++ b/test/config-next/remoteva-a.json @@ -23,6 +23,11 @@ "va.boulder" ] }, + "va.CAA": { + "clientNames": [ + "va.boulder" + ] + }, "grpc.health.v1.Health": { "clientNames": [ "health-checker.boulder" diff --git a/test/config-next/remoteva-b.json b/test/config-next/remoteva-b.json index 6cc5df2087c..f2167497cd7 100644 --- a/test/config-next/remoteva-b.json +++ b/test/config-next/remoteva-b.json @@ -23,6 +23,11 @@ "va.boulder" ] }, + "va.CAA": { + "clientNames": [ + "va.boulder" + ] + }, "grpc.health.v1.Health": { "clientNames": [ "health-checker.boulder" diff --git a/test/config-next/remoteva-c.json b/test/config-next/remoteva-c.json index 6e485a456ac..6616e4c5c4d 100644 --- a/test/config-next/remoteva-c.json +++ b/test/config-next/remoteva-c.json @@ -23,6 +23,11 @@ "va.boulder" ] }, + "va.CAA": { + "clientNames": [ + "va.boulder" + ] + }, "grpc.health.v1.Health": { "clientNames": [ "health-checker.boulder" diff --git a/va/caa_test.go b/va/caa_test.go index 9d7d0189159..c65270bcc1f 100644 --- a/va/caa_test.go +++ b/va/caa_test.go @@ -2,9 +2,12 @@ package va import ( "context" + "encoding/json" "errors" "fmt" "net" + "regexp" + "slices" "strings" "testing" @@ -518,57 +521,107 @@ func TestCAALogging(t *testing.T) { } } +type caaCheckFuncRunner func(context.Context, *ValidationAuthorityImpl, *vapb.IsCAAValidRequest) (*vapb.IsCAAValidResponse, error) + +var runIsCAAValid = func(ctx context.Context, va *ValidationAuthorityImpl, req *vapb.IsCAAValidRequest) (*vapb.IsCAAValidResponse, error) { + return va.IsCAAValid(ctx, req) +} + +var runDoCAA = func(ctx context.Context, va *ValidationAuthorityImpl, req *vapb.IsCAAValidRequest) (*vapb.IsCAAValidResponse, error) { + return va.DoCAA(ctx, req) +} + // TestIsCAAValidErrMessage tests that an error result from `va.IsCAAValid` // includes the domain name that was being checked in the failure detail. func TestIsCAAValidErrMessage(t *testing.T) { + t.Parallel() va, _ := setup(nil, "", nil, caaMockDNS{}) - // Call IsCAAValid with a domain we know fails with a generic error from the - // caaMockDNS. - domain := "caa-timeout.com" - resp, err := va.IsCAAValid(ctx, &vapb.IsCAAValidRequest{ - Domain: domain, - ValidationMethod: string(core.ChallengeTypeHTTP01), - AccountURIID: 12345, - }) + testCases := []struct { + name string + caaCheckFunc caaCheckFuncRunner + }{ + { + name: "IsCAAValid", + caaCheckFunc: runIsCAAValid, + }, + { + name: "DoCAA", + caaCheckFunc: runDoCAA, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + // Call the operation with a domain we know fails with a generic error from the + // caaMockDNS. + domain := "caa-timeout.com" + resp, err := tc.caaCheckFunc(ctx, va, &vapb.IsCAAValidRequest{ + Domain: domain, + 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)) + // 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)) + }) + } } // TestIsCAAValidParams tests that the IsCAAValid method rejects any requests // which do not have the necessary parameters to do CAA Account and Method // Binding checks. func TestIsCAAValidParams(t *testing.T) { + t.Parallel() va, _ := setup(nil, "", nil, caaMockDNS{}) + testCases := []struct { + name string + caaCheckFunc caaCheckFuncRunner + }{ + { + name: "IsCAAValid", + caaCheckFunc: runIsCAAValid, + }, + { + name: "DoCAA", + caaCheckFunc: runDoCAA, + }, + } - // Calling IsCAAValid without a ValidationMethod should fail. - _, err := va.IsCAAValid(ctx, &vapb.IsCAAValidRequest{ - Domain: "present.com", - AccountURIID: 12345, - }) - test.AssertError(t, err, "calling IsCAAValid without a ValidationMethod") + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() - // Calling IsCAAValid with an invalid ValidationMethod should fail. - _, err = va.IsCAAValid(ctx, &vapb.IsCAAValidRequest{ - Domain: "present.com", - ValidationMethod: "tls-sni-01", - AccountURIID: 12345, - }) - test.AssertError(t, err, "calling IsCAAValid with a bad ValidationMethod") + // Calling IsCAAValid without a ValidationMethod should fail. + _, err := tc.caaCheckFunc(ctx, va, &vapb.IsCAAValidRequest{ + Domain: "present.com", + AccountURIID: 12345, + }) + test.AssertError(t, err, "calling IsCAAValid without a ValidationMethod") - // Calling IsCAAValid without an AccountURIID should fail. - _, err = va.IsCAAValid(ctx, &vapb.IsCAAValidRequest{ - Domain: "present.com", - ValidationMethod: string(core.ChallengeTypeHTTP01), - }) - test.AssertError(t, err, "calling IsCAAValid without an AccountURIID") + // Calling IsCAAValid with an invalid ValidationMethod should fail. + _, err = tc.caaCheckFunc(ctx, va, &vapb.IsCAAValidRequest{ + Domain: "present.com", + ValidationMethod: "tls-sni-01", + AccountURIID: 12345, + }) + test.AssertError(t, err, "calling IsCAAValid with a bad ValidationMethod") + + // Calling IsCAAValid without an AccountURIID should fail. + _, err = tc.caaCheckFunc(ctx, va, &vapb.IsCAAValidRequest{ + Domain: "present.com", + ValidationMethod: string(core.ChallengeTypeHTTP01), + }) + test.AssertError(t, err, "calling IsCAAValid without an AccountURIID") + }) + } } var errCAABrokenDNSClient = errors.New("dnsClient is broken") @@ -653,6 +706,25 @@ func (h caaHijackedDNS) LookupCAA(_ context.Context, domain string) ([]*dns.CAA, 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. @@ -663,13 +735,32 @@ func TestMultiCAARechecking(t *testing.T) { hijackedUA = "hijacked" ) + type testFunc struct { + name string + impl caaCheckFuncRunner + } + + testFuncs := []testFunc{ + { + name: "IsCAAValid", + impl: runIsCAAValid, + }, + { + name: "DoCAA", + impl: runDoCAA, + }, + } + testCases := []struct { - name string + name string + // method is only set inside of the test loop. + methodName string domains string remoteVAs []remoteConf expectedProbSubstring string expectedProbType probs.ProblemType expectedDiffLogSubstring string + expectedSummary *mpicSummary expectedLabels prometheus.Labels localDNSClient bdns.Client }{ @@ -714,6 +805,12 @@ func TestMultiCAARechecking(t *testing.T) { domains: "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}, @@ -733,7 +830,13 @@ func TestMultiCAARechecking(t *testing.T) { expectedProbSubstring: "During secondary validation: While processing CAA", expectedProbType: probs.DNSProblem, expectedDiffLogSubstring: `"RemoteSuccesses":1,"RemoteFailures":2`, - localDNSClient: caaMockDNS{}, + 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{}}, @@ -753,7 +856,13 @@ func TestMultiCAARechecking(t *testing.T) { expectedProbSubstring: "During secondary validation: While processing CAA", expectedProbType: probs.DNSProblem, expectedDiffLogSubstring: `"RemoteSuccesses":0,"RemoteFailures":3`, - localDNSClient: caaMockDNS{}, + 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{}}, @@ -788,7 +897,13 @@ func TestMultiCAARechecking(t *testing.T) { name: "functional localVA, 1 broken RVA, CAA issue type present", domains: "present.com", expectedDiffLogSubstring: `"RemoteSuccesses":2,"RemoteFailures":1`, - localDNSClient: caaMockDNS{}, + 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}, @@ -808,7 +923,13 @@ func TestMultiCAARechecking(t *testing.T) { expectedProbSubstring: "During secondary validation: While processing CAA", expectedProbType: probs.DNSProblem, expectedDiffLogSubstring: `"RemoteSuccesses":1,"RemoteFailures":2`, - localDNSClient: caaMockDNS{}, + 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{}}, @@ -828,7 +949,13 @@ func TestMultiCAARechecking(t *testing.T) { expectedProbSubstring: "During secondary validation: While processing CAA", expectedProbType: probs.DNSProblem, expectedDiffLogSubstring: `"RemoteSuccesses":0,"RemoteFailures":3`, - localDNSClient: caaMockDNS{}, + 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{}}, @@ -860,7 +987,13 @@ func TestMultiCAARechecking(t *testing.T) { name: "1 hijacked RVA, CAA issue type present", domains: "present.com", expectedDiffLogSubstring: `"RemoteSuccesses":2,"RemoteFailures":1`, - localDNSClient: caaMockDNS{}, + 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}, @@ -873,7 +1006,13 @@ func TestMultiCAARechecking(t *testing.T) { expectedProbSubstring: "During secondary validation: While processing CAA", expectedProbType: probs.CAAProblem, expectedDiffLogSubstring: `"RemoteSuccesses":1,"RemoteFailures":2`, - localDNSClient: caaMockDNS{}, + 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{}}, @@ -886,7 +1025,13 @@ func TestMultiCAARechecking(t *testing.T) { expectedProbSubstring: "During secondary validation: While processing CAA", expectedProbType: probs.CAAProblem, expectedDiffLogSubstring: `"RemoteSuccesses":0,"RemoteFailures":3`, - localDNSClient: caaMockDNS{}, + 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{}}, @@ -897,7 +1042,13 @@ func TestMultiCAARechecking(t *testing.T) { name: "1 hijacked RVA, CAA issuewild type present", domains: "satisfiable-wildcard.com", expectedDiffLogSubstring: `"RemoteSuccesses":2,"RemoteFailures":1`, - localDNSClient: caaMockDNS{}, + 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}, @@ -910,7 +1061,13 @@ func TestMultiCAARechecking(t *testing.T) { expectedProbSubstring: "During secondary validation: While processing CAA", expectedProbType: probs.CAAProblem, expectedDiffLogSubstring: `"RemoteSuccesses":1,"RemoteFailures":2`, - localDNSClient: caaMockDNS{}, + 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{}}, @@ -923,7 +1080,13 @@ func TestMultiCAARechecking(t *testing.T) { expectedProbSubstring: "During secondary validation: While processing CAA", expectedProbType: probs.CAAProblem, expectedDiffLogSubstring: `"RemoteSuccesses":0,"RemoteFailures":3`, - localDNSClient: caaMockDNS{}, + 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{}}, @@ -934,7 +1097,13 @@ func TestMultiCAARechecking(t *testing.T) { name: "1 hijacked RVA, CAA issuewild type present, 1 failure allowed", domains: "satisfiable-wildcard.com", expectedDiffLogSubstring: `"RemoteSuccesses":2,"RemoteFailures":1`, - localDNSClient: caaMockDNS{}, + 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}, @@ -947,7 +1116,13 @@ func TestMultiCAARechecking(t *testing.T) { expectedProbSubstring: "During secondary validation: While processing CAA", expectedProbType: probs.CAAProblem, expectedDiffLogSubstring: `"RemoteSuccesses":1,"RemoteFailures":2`, - localDNSClient: caaMockDNS{}, + 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{}}, @@ -960,7 +1135,13 @@ func TestMultiCAARechecking(t *testing.T) { expectedProbSubstring: "During secondary validation: While processing CAA", expectedProbType: probs.CAAProblem, expectedDiffLogSubstring: `"RemoteSuccesses":0,"RemoteFailures":3`, - localDNSClient: caaMockDNS{}, + 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{}}, @@ -970,64 +1151,77 @@ func TestMultiCAARechecking(t *testing.T) { } 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.IsCAAValid(context.TODO(), &vapb.IsCAAValidRequest{ - Domain: tc.domains, - 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") - } + for _, testFunc := range testFuncs { + t.Run(tc.name+"_"+testFunc.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 := testFunc.impl(context.TODO(), va, &vapb.IsCAAValidRequest{ + Domain: tc.domains, + 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.expectedProbType != "" { + test.AssertNotNil(t, isValidRes.Problem, "IsCAAValidRequest returned nil problem, but should not have") + test.AssertEquals(t, string(tc.expectedProbType), isValidRes.Problem.ProblemType) + } - var invalidRVACount int - for _, x := range tc.remoteVAs { - if x.ua == brokenUA || x.ua == hijackedUA { - invalidRVACount++ + if testFunc.name == "IsCAAValid" { + var invalidRVACount int + for _, x := range tc.remoteVAs { + if x.ua == brokenUA || x.ua == hijackedUA { + invalidRVACount++ + } + } + + gotRequestProbs := mockLog.GetAllMatching(" returned a problem: ") + test.AssertEquals(t, len(gotRequestProbs), invalidRVACount) + + gotDifferential := mockLog.GetAllMatching("remoteVADifferentials JSON=.*") + if tc.expectedDiffLogSubstring != "" { + test.AssertEquals(t, len(gotDifferential), 1) + test.AssertContains(t, gotDifferential[0], tc.expectedDiffLogSubstring) + } else { + test.AssertEquals(t, len(gotDifferential), 0) + } } - } - gotRequestProbs := mockLog.GetAllMatching(" returned a problem: ") - test.AssertEquals(t, len(gotRequestProbs), invalidRVACount) + if testFunc.name == "DoCAA" && 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) + } - gotDifferential := mockLog.GetAllMatching("remoteVADifferentials JSON=.*") - if tc.expectedDiffLogSubstring != "" { - test.AssertEquals(t, len(gotDifferential), 1) - test.AssertContains(t, gotDifferential[0], tc.expectedDiffLogSubstring) - } else { - test.AssertEquals(t, len(gotDifferential), 0) - } + 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) + } - 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) + } - if tc.expectedLabels != nil { - test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, tc.expectedLabels, 1) - } - }) + }) + } } } diff --git a/va/dns_test.go b/va/dns_test.go index c75fa156149..58f157647e8 100644 --- a/va/dns_test.go +++ b/va/dns_test.go @@ -29,7 +29,7 @@ func TestDNSValidationEmpty(t *testing.T) { test.AssertEquals(t, res.Problem.Detail, "No TXT record found at _acme-challenge.empty-txts.com") test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, prometheus.Labels{ - "operation": opChallAndCAA, + "operation": opDCVAndCAA, "perspective": va.perspective, "challenge_type": string(core.ChallengeTypeDNS01), "problem_type": string(probs.UnauthorizedProblem), diff --git a/va/proto/va.pb.go b/va/proto/va.pb.go index 1e33a925407..abc195705a3 100644 --- a/va/proto/va.pb.go +++ b/va/proto/va.pb.go @@ -30,6 +30,7 @@ type IsCAAValidRequest struct { Domain string `protobuf:"bytes,1,opt,name=domain,proto3" json:"domain,omitempty"` ValidationMethod string `protobuf:"bytes,2,opt,name=validationMethod,proto3" json:"validationMethod,omitempty"` AccountURIID int64 `protobuf:"varint,3,opt,name=accountURIID,proto3" json:"accountURIID,omitempty"` + AuthzID string `protobuf:"bytes,4,opt,name=authzID,proto3" json:"authzID,omitempty"` } func (x *IsCAAValidRequest) Reset() { @@ -85,6 +86,13 @@ func (x *IsCAAValidRequest) GetAccountURIID() int64 { return 0 } +func (x *IsCAAValidRequest) GetAuthzID() string { + if x != nil { + return x.AuthzID + } + return "" +} + // If CAA is valid for the requested domain, the problem will be empty type IsCAAValidResponse struct { state protoimpl.MessageState @@ -351,61 +359,70 @@ var File_va_proto protoreflect.FileDescriptor var file_va_proto_rawDesc = []byte{ 0x0a, 0x08, 0x76, 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x02, 0x76, 0x61, 0x1a, 0x15, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x63, 0x6f, 0x72, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x7b, 0x0a, 0x11, 0x49, 0x73, 0x43, 0x41, 0x41, 0x56, 0x61, - 0x6c, 0x69, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, - 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, - 0x69, 0x6e, 0x12, 0x2a, 0x0a, 0x10, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x76, 0x61, - 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x22, - 0x0a, 0x0c, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x55, 0x52, 0x49, 0x49, 0x44, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x55, 0x52, 0x49, - 0x49, 0x44, 0x22, 0x78, 0x0a, 0x12, 0x49, 0x73, 0x43, 0x41, 0x41, 0x56, 0x61, 0x6c, 0x69, 0x64, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x62, - 0x6c, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x63, 0x6f, 0x72, 0x65, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x95, 0x01, 0x0a, 0x11, 0x49, 0x73, 0x43, 0x41, 0x41, 0x56, + 0x61, 0x6c, 0x69, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x64, + 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, + 0x61, 0x69, 0x6e, 0x12, 0x2a, 0x0a, 0x10, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x76, + 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, + 0x22, 0x0a, 0x0c, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x55, 0x52, 0x49, 0x49, 0x44, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x55, 0x52, + 0x49, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x49, 0x44, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x49, 0x44, 0x22, 0x78, 0x0a, + 0x12, 0x49, 0x73, 0x43, 0x41, 0x41, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x50, 0x72, 0x6f, 0x62, + 0x6c, 0x65, 0x6d, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x62, + 0x6c, 0x65, 0x6d, 0x12, 0x20, 0x0a, 0x0b, 0x70, 0x65, 0x72, 0x73, 0x70, 0x65, 0x63, 0x74, 0x69, + 0x76, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x65, 0x72, 0x73, 0x70, 0x65, + 0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x72, 0x69, 0x72, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x03, 0x72, 0x69, 0x72, 0x22, 0xc4, 0x01, 0x0a, 0x18, 0x50, 0x65, 0x72, 0x66, + 0x6f, 0x72, 0x6d, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6e, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6e, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2d, + 0x0a, 0x09, 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x0f, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x43, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, + 0x67, 0x65, 0x52, 0x09, 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x12, 0x23, 0x0a, + 0x05, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x76, + 0x61, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x05, 0x61, 0x75, 0x74, + 0x68, 0x7a, 0x12, 0x3a, 0x0a, 0x18, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x4b, 0x65, + 0x79, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x18, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x4b, 0x65, + 0x79, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x31, + 0x0a, 0x09, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x0e, 0x0a, 0x02, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x72, + 0x65, 0x67, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x72, 0x65, 0x67, 0x49, + 0x44, 0x22, 0xa8, 0x01, 0x0a, 0x10, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x30, 0x0a, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x56, + 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, + 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x12, 0x2e, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x62, + 0x6c, 0x65, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x50, 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x12, 0x20, 0x0a, 0x0b, 0x70, 0x65, 0x72, 0x73, 0x70, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x65, 0x72, 0x73, 0x70, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x72, 0x69, - 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x72, 0x69, 0x72, 0x22, 0xc4, 0x01, 0x0a, - 0x18, 0x50, 0x65, 0x72, 0x66, 0x6f, 0x72, 0x6d, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6e, 0x73, - 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6e, 0x73, 0x4e, - 0x61, 0x6d, 0x65, 0x12, 0x2d, 0x0a, 0x09, 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x43, 0x68, - 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x52, 0x09, 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, - 0x67, 0x65, 0x12, 0x23, 0x0a, 0x05, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x0d, 0x2e, 0x76, 0x61, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x4d, 0x65, 0x74, 0x61, - 0x52, 0x05, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x12, 0x3a, 0x0a, 0x18, 0x65, 0x78, 0x70, 0x65, 0x63, - 0x74, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x18, 0x65, 0x78, 0x70, 0x65, 0x63, - 0x74, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x22, 0x31, 0x0a, 0x09, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x4d, 0x65, 0x74, 0x61, - 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, - 0x12, 0x14, 0x0a, 0x05, 0x72, 0x65, 0x67, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x05, 0x72, 0x65, 0x67, 0x49, 0x44, 0x22, 0xa8, 0x01, 0x0a, 0x10, 0x56, 0x61, 0x6c, 0x69, 0x64, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x30, 0x0a, 0x07, 0x72, - 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x63, - 0x6f, 0x72, 0x65, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, - 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x12, 0x2e, 0x0a, - 0x07, 0x70, 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, - 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x50, 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x44, 0x65, 0x74, - 0x61, 0x69, 0x6c, 0x73, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x12, 0x20, 0x0a, - 0x0b, 0x70, 0x65, 0x72, 0x73, 0x70, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0b, 0x70, 0x65, 0x72, 0x73, 0x70, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x12, - 0x10, 0x0a, 0x03, 0x72, 0x69, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x72, 0x69, - 0x72, 0x32, 0x4f, 0x0a, 0x02, 0x56, 0x41, 0x12, 0x49, 0x0a, 0x11, 0x50, 0x65, 0x72, 0x66, 0x6f, - 0x72, 0x6d, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x2e, 0x76, - 0x61, 0x2e, 0x50, 0x65, 0x72, 0x66, 0x6f, 0x72, 0x6d, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x76, 0x61, 0x2e, - 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, - 0x22, 0x00, 0x32, 0x44, 0x0a, 0x03, 0x43, 0x41, 0x41, 0x12, 0x3d, 0x0a, 0x0a, 0x49, 0x73, 0x43, - 0x41, 0x41, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x12, 0x15, 0x2e, 0x76, 0x61, 0x2e, 0x49, 0x73, 0x43, - 0x41, 0x41, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, - 0x2e, 0x76, 0x61, 0x2e, 0x49, 0x73, 0x43, 0x41, 0x41, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, - 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x65, 0x74, 0x73, 0x65, 0x6e, 0x63, 0x72, 0x79, - 0x70, 0x74, 0x2f, 0x62, 0x6f, 0x75, 0x6c, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x61, 0x2f, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x72, 0x69, 0x72, 0x32, 0x8e, 0x01, 0x0a, + 0x02, 0x56, 0x41, 0x12, 0x49, 0x0a, 0x11, 0x50, 0x65, 0x72, 0x66, 0x6f, 0x72, 0x6d, 0x56, 0x61, + 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x2e, 0x76, 0x61, 0x2e, 0x50, 0x65, + 0x72, 0x66, 0x6f, 0x72, 0x6d, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x76, 0x61, 0x2e, 0x56, 0x61, 0x6c, 0x69, + 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x00, 0x12, 0x3d, + 0x0a, 0x05, 0x44, 0x6f, 0x44, 0x43, 0x56, 0x12, 0x1c, 0x2e, 0x76, 0x61, 0x2e, 0x50, 0x65, 0x72, + 0x66, 0x6f, 0x72, 0x6d, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x76, 0x61, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x00, 0x32, 0x7e, 0x0a, + 0x03, 0x43, 0x41, 0x41, 0x12, 0x3d, 0x0a, 0x0a, 0x49, 0x73, 0x43, 0x41, 0x41, 0x56, 0x61, 0x6c, + 0x69, 0x64, 0x12, 0x15, 0x2e, 0x76, 0x61, 0x2e, 0x49, 0x73, 0x43, 0x41, 0x41, 0x56, 0x61, 0x6c, + 0x69, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x76, 0x61, 0x2e, 0x49, + 0x73, 0x43, 0x41, 0x41, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x00, 0x12, 0x38, 0x0a, 0x05, 0x44, 0x6f, 0x43, 0x41, 0x41, 0x12, 0x15, 0x2e, 0x76, + 0x61, 0x2e, 0x49, 0x73, 0x43, 0x41, 0x41, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x76, 0x61, 0x2e, 0x49, 0x73, 0x43, 0x41, 0x41, 0x56, 0x61, + 0x6c, 0x69, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x29, 0x5a, + 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x65, 0x74, 0x73, + 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x2f, 0x62, 0x6f, 0x75, 0x6c, 0x64, 0x65, 0x72, 0x2f, + 0x76, 0x61, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -438,11 +455,15 @@ var file_va_proto_depIdxs = []int32{ 7, // 3: va.ValidationResult.records:type_name -> core.ValidationRecord 5, // 4: va.ValidationResult.problem:type_name -> core.ProblemDetails 2, // 5: va.VA.PerformValidation:input_type -> va.PerformValidationRequest - 0, // 6: va.CAA.IsCAAValid:input_type -> va.IsCAAValidRequest - 4, // 7: va.VA.PerformValidation:output_type -> va.ValidationResult - 1, // 8: va.CAA.IsCAAValid:output_type -> va.IsCAAValidResponse - 7, // [7:9] is the sub-list for method output_type - 5, // [5:7] is the sub-list for method input_type + 2, // 6: va.VA.DoDCV:input_type -> va.PerformValidationRequest + 0, // 7: va.CAA.IsCAAValid:input_type -> va.IsCAAValidRequest + 0, // 8: va.CAA.DoCAA:input_type -> va.IsCAAValidRequest + 4, // 9: va.VA.PerformValidation:output_type -> va.ValidationResult + 4, // 10: va.VA.DoDCV:output_type -> va.ValidationResult + 1, // 11: va.CAA.IsCAAValid:output_type -> va.IsCAAValidResponse + 1, // 12: va.CAA.DoCAA:output_type -> va.IsCAAValidResponse + 9, // [9:13] is the sub-list for method output_type + 5, // [5:9] is the sub-list for method input_type 5, // [5:5] is the sub-list for extension type_name 5, // [5:5] is the sub-list for extension extendee 0, // [0:5] is the sub-list for field type_name diff --git a/va/proto/va.proto b/va/proto/va.proto index c42313990a1..44fa5c6e6e1 100644 --- a/va/proto/va.proto +++ b/va/proto/va.proto @@ -7,10 +7,12 @@ import "core/proto/core.proto"; service VA { rpc PerformValidation(PerformValidationRequest) returns (ValidationResult) {} + rpc DoDCV(PerformValidationRequest) returns (ValidationResult) {} } service CAA { rpc IsCAAValid(IsCAAValidRequest) returns (IsCAAValidResponse) {} + rpc DoCAA(IsCAAValidRequest) returns (IsCAAValidResponse) {} } message IsCAAValidRequest { @@ -18,6 +20,7 @@ message IsCAAValidRequest { string domain = 1; string validationMethod = 2; int64 accountURIID = 3; + string authzID = 4; } // If CAA is valid for the requested domain, the problem will be empty diff --git a/va/proto/va_grpc.pb.go b/va/proto/va_grpc.pb.go index b7c3df4f33b..55eced18465 100644 --- a/va/proto/va_grpc.pb.go +++ b/va/proto/va_grpc.pb.go @@ -20,6 +20,7 @@ const _ = grpc.SupportPackageIsVersion9 const ( VA_PerformValidation_FullMethodName = "/va.VA/PerformValidation" + VA_DoDCV_FullMethodName = "/va.VA/DoDCV" ) // VAClient is the client API for VA service. @@ -27,6 +28,7 @@ const ( // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type VAClient interface { PerformValidation(ctx context.Context, in *PerformValidationRequest, opts ...grpc.CallOption) (*ValidationResult, error) + DoDCV(ctx context.Context, in *PerformValidationRequest, opts ...grpc.CallOption) (*ValidationResult, error) } type vAClient struct { @@ -47,11 +49,22 @@ func (c *vAClient) PerformValidation(ctx context.Context, in *PerformValidationR return out, nil } +func (c *vAClient) DoDCV(ctx context.Context, in *PerformValidationRequest, opts ...grpc.CallOption) (*ValidationResult, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ValidationResult) + err := c.cc.Invoke(ctx, VA_DoDCV_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // VAServer is the server API for VA service. // All implementations must embed UnimplementedVAServer // for forward compatibility type VAServer interface { PerformValidation(context.Context, *PerformValidationRequest) (*ValidationResult, error) + DoDCV(context.Context, *PerformValidationRequest) (*ValidationResult, error) mustEmbedUnimplementedVAServer() } @@ -62,6 +75,9 @@ type UnimplementedVAServer struct { func (UnimplementedVAServer) PerformValidation(context.Context, *PerformValidationRequest) (*ValidationResult, error) { return nil, status.Errorf(codes.Unimplemented, "method PerformValidation not implemented") } +func (UnimplementedVAServer) DoDCV(context.Context, *PerformValidationRequest) (*ValidationResult, error) { + return nil, status.Errorf(codes.Unimplemented, "method DoDCV not implemented") +} func (UnimplementedVAServer) mustEmbedUnimplementedVAServer() {} // UnsafeVAServer may be embedded to opt out of forward compatibility for this service. @@ -93,6 +109,24 @@ func _VA_PerformValidation_Handler(srv interface{}, ctx context.Context, dec fun return interceptor(ctx, in, info, handler) } +func _VA_DoDCV_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PerformValidationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(VAServer).DoDCV(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: VA_DoDCV_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(VAServer).DoDCV(ctx, req.(*PerformValidationRequest)) + } + return interceptor(ctx, in, info, handler) +} + // VA_ServiceDesc is the grpc.ServiceDesc for VA service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -104,6 +138,10 @@ var VA_ServiceDesc = grpc.ServiceDesc{ MethodName: "PerformValidation", Handler: _VA_PerformValidation_Handler, }, + { + MethodName: "DoDCV", + Handler: _VA_DoDCV_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "va.proto", @@ -111,6 +149,7 @@ var VA_ServiceDesc = grpc.ServiceDesc{ const ( CAA_IsCAAValid_FullMethodName = "/va.CAA/IsCAAValid" + CAA_DoCAA_FullMethodName = "/va.CAA/DoCAA" ) // CAAClient is the client API for CAA service. @@ -118,6 +157,7 @@ const ( // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type CAAClient interface { IsCAAValid(ctx context.Context, in *IsCAAValidRequest, opts ...grpc.CallOption) (*IsCAAValidResponse, error) + DoCAA(ctx context.Context, in *IsCAAValidRequest, opts ...grpc.CallOption) (*IsCAAValidResponse, error) } type cAAClient struct { @@ -138,11 +178,22 @@ func (c *cAAClient) IsCAAValid(ctx context.Context, in *IsCAAValidRequest, opts return out, nil } +func (c *cAAClient) DoCAA(ctx context.Context, in *IsCAAValidRequest, opts ...grpc.CallOption) (*IsCAAValidResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(IsCAAValidResponse) + err := c.cc.Invoke(ctx, CAA_DoCAA_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // CAAServer is the server API for CAA service. // All implementations must embed UnimplementedCAAServer // for forward compatibility type CAAServer interface { IsCAAValid(context.Context, *IsCAAValidRequest) (*IsCAAValidResponse, error) + DoCAA(context.Context, *IsCAAValidRequest) (*IsCAAValidResponse, error) mustEmbedUnimplementedCAAServer() } @@ -153,6 +204,9 @@ type UnimplementedCAAServer struct { func (UnimplementedCAAServer) IsCAAValid(context.Context, *IsCAAValidRequest) (*IsCAAValidResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method IsCAAValid not implemented") } +func (UnimplementedCAAServer) DoCAA(context.Context, *IsCAAValidRequest) (*IsCAAValidResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DoCAA not implemented") +} func (UnimplementedCAAServer) mustEmbedUnimplementedCAAServer() {} // UnsafeCAAServer may be embedded to opt out of forward compatibility for this service. @@ -184,6 +238,24 @@ func _CAA_IsCAAValid_Handler(srv interface{}, ctx context.Context, dec func(inte return interceptor(ctx, in, info, handler) } +func _CAA_DoCAA_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(IsCAAValidRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CAAServer).DoCAA(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: CAA_DoCAA_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CAAServer).DoCAA(ctx, req.(*IsCAAValidRequest)) + } + return interceptor(ctx, in, info, handler) +} + // CAA_ServiceDesc is the grpc.ServiceDesc for CAA service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -195,6 +267,10 @@ var CAA_ServiceDesc = grpc.ServiceDesc{ MethodName: "IsCAAValid", Handler: _CAA_IsCAAValid_Handler, }, + { + MethodName: "DoCAA", + Handler: _CAA_DoCAA_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "va.proto", diff --git a/va/va.go b/va/va.go index d54f9214077..a1e2cd4492e 100644 --- a/va/va.go +++ b/va/va.go @@ -36,8 +36,9 @@ const ( PrimaryPerspective = "Primary" allPerspectives = "all" - opChallAndCAA = "challenge+caa" - opCAA = "caa" + opDCVAndCAA = "dcv+caa" + opDCV = "dcv" + opCAA = "caa" pass = "pass" fail = "fail" @@ -97,7 +98,7 @@ type RemoteVA struct { type vaMetrics struct { // validationLatency is a histogram of the latency to perform validations // from the primary and remote VA perspectives. It's labelled by: - // - operation: VA.ValidateChallenge or VA.CheckCAA as [challenge|caa|challenge+caa] + // - operation: VA.DoDCV or VA.DoCAA as [dcv|caa|dcv+caa] // - perspective: ValidationAuthorityImpl.perspective // - challenge_type: core.Challenge.Type // - problem_type: probs.ProblemType @@ -438,7 +439,7 @@ func (va *ValidationAuthorityImpl) validateChallenge( // observeLatency records entries in the validationLatency histogram of the // latency to perform validations from the primary and remote VA perspectives. // The labels are: -// - operation: VA.ValidateChallenge or VA.CheckCAA as [challenge|caa] +// - operation: VA.DoDCV or VA.DoCAA as [dcv|caa] // - perspective: [ValidationAuthorityImpl.perspective|all] // - challenge_type: core.Challenge.Type // - problem_type: probs.ProblemType @@ -714,10 +715,10 @@ func (va *ValidationAuthorityImpl) PerformValidation(ctx context.Context, req *v outcome = pass } // Observe local validation latency (primary|remote). - va.observeLatency(opChallAndCAA, va.perspective, string(chall.Type), probType, outcome, localLatency) + va.observeLatency(opDCVAndCAA, va.perspective, string(chall.Type), probType, outcome, localLatency) if va.isPrimaryVA() { // Observe total validation latency (primary+remote). - va.observeLatency(opChallAndCAA, allPerspectives, string(chall.Type), probType, outcome, va.clk.Since(start)) + va.observeLatency(opDCVAndCAA, allPerspectives, string(chall.Type), probType, outcome, va.clk.Since(start)) } // Log the total validation latency. diff --git a/va/va_test.go b/va/va_test.go index b7925a3cc6c..c54fa680b99 100644 --- a/va/va_test.go +++ b/va/va_test.go @@ -270,10 +270,18 @@ func (v cancelledVA) PerformValidation(_ context.Context, _ *vapb.PerformValidat return nil, context.Canceled } +func (v cancelledVA) DoDCV(_ context.Context, _ *vapb.PerformValidationRequest, _ ...grpc.CallOption) (*vapb.ValidationResult, error) { + return nil, context.Canceled +} + func (v cancelledVA) IsCAAValid(_ context.Context, _ *vapb.IsCAAValidRequest, _ ...grpc.CallOption) (*vapb.IsCAAValidResponse, error) { return nil, context.Canceled } +func (v cancelledVA) DoCAA(_ context.Context, _ *vapb.IsCAAValidRequest, _ ...grpc.CallOption) (*vapb.IsCAAValidResponse, error) { + return nil, context.Canceled +} + // brokenRemoteVA is a mock for the VAClient and CAAClient interfaces that always return // errors. type brokenRemoteVA struct{} @@ -287,10 +295,19 @@ func (b brokenRemoteVA) PerformValidation(_ context.Context, _ *vapb.PerformVali return nil, errBrokenRemoteVA } +// DoDCV returns errBrokenRemoteVA unconditionally +func (b brokenRemoteVA) DoDCV(_ context.Context, _ *vapb.PerformValidationRequest, _ ...grpc.CallOption) (*vapb.ValidationResult, error) { + return nil, errBrokenRemoteVA +} + func (b brokenRemoteVA) IsCAAValid(_ context.Context, _ *vapb.IsCAAValidRequest, _ ...grpc.CallOption) (*vapb.IsCAAValidResponse, error) { return nil, errBrokenRemoteVA } +func (b brokenRemoteVA) DoCAA(_ context.Context, _ *vapb.IsCAAValidRequest, _ ...grpc.CallOption) (*vapb.IsCAAValidResponse, error) { + return nil, errBrokenRemoteVA +} + // inMemVA is a wrapper which fulfills the VAClient and CAAClient // interfaces, but then forwards requests directly to its inner // ValidationAuthorityImpl rather than over the network. This lets a local @@ -303,10 +320,18 @@ func (inmem *inMemVA) PerformValidation(ctx context.Context, req *vapb.PerformVa return inmem.rva.PerformValidation(ctx, req) } +func (inmem *inMemVA) DoDCV(ctx context.Context, req *vapb.PerformValidationRequest, _ ...grpc.CallOption) (*vapb.ValidationResult, error) { + return inmem.rva.DoDCV(ctx, req) +} + func (inmem *inMemVA) IsCAAValid(ctx context.Context, req *vapb.IsCAAValidRequest, _ ...grpc.CallOption) (*vapb.IsCAAValidResponse, error) { return inmem.rva.IsCAAValid(ctx, req) } +func (inmem *inMemVA) DoCAA(ctx context.Context, req *vapb.IsCAAValidRequest, _ ...grpc.CallOption) (*vapb.IsCAAValidResponse, error) { + return inmem.rva.DoCAA(ctx, req) +} + func TestNewValidationAuthorityImplWithDuplicateRemotes(t *testing.T) { var remoteVAs []RemoteVA for i := 0; i < 3; i++ { @@ -333,7 +358,19 @@ func TestNewValidationAuthorityImplWithDuplicateRemotes(t *testing.T) { test.AssertContains(t, err.Error(), "duplicate remote VA perspective \"dadaist\"") } +type validationFuncRunner func(context.Context, *ValidationAuthorityImpl, *vapb.PerformValidationRequest) (*vapb.ValidationResult, error) + +var runPerformValidation = func(ctx context.Context, va *ValidationAuthorityImpl, req *vapb.PerformValidationRequest) (*vapb.ValidationResult, error) { + return va.PerformValidation(ctx, req) +} + +var runDoDCV = func(ctx context.Context, va *ValidationAuthorityImpl, req *vapb.PerformValidationRequest) (*vapb.ValidationResult, error) { + return va.DoDCV(ctx, req) +} + func TestPerformValidationWithMismatchedRemoteVAPerspectives(t *testing.T) { + t.Parallel() + mismatched1 := RemoteVA{ RemoteClients: setupRemote(nil, "", nil, "dadaist", arin), Perspective: "baroque", @@ -346,15 +383,36 @@ func TestPerformValidationWithMismatchedRemoteVAPerspectives(t *testing.T) { } remoteVAs := setupRemotes([]remoteConf{{rir: ripe}}, nil) remoteVAs = append(remoteVAs, mismatched1, mismatched2) - va, mockLog := setup(nil, "", remoteVAs, nil) - req := createValidationRequest("good-dns01.com", core.ChallengeTypeDNS01) - res, _ := va.PerformValidation(context.Background(), req) - test.AssertNotNil(t, res.GetProblem(), "validation succeeded with mismatched remote VA perspectives") - test.AssertEquals(t, len(mockLog.GetAllMatching("Expected perspective")), 2) + testCases := []struct { + validationFuncName string + validationFunc validationFuncRunner + }{ + { + validationFuncName: "PerformValidation", + validationFunc: runPerformValidation, + }, + { + validationFuncName: "DoDCV", + validationFunc: runDoDCV, + }, + } + for _, tc := range testCases { + t.Run(tc.validationFuncName, func(t *testing.T) { + t.Parallel() + + va, mockLog := setup(nil, "", remoteVAs, nil) + req := createValidationRequest("good-dns01.com", core.ChallengeTypeDNS01) + res, _ := tc.validationFunc(context.Background(), va, req) + test.AssertNotNil(t, res.GetProblem(), "validation succeeded with mismatched remote VA perspectives") + test.AssertEquals(t, len(mockLog.GetAllMatching("Expected perspective")), 2) + }) + } } func TestPerformValidationWithMismatchedRemoteVARIRs(t *testing.T) { + t.Parallel() + mismatched1 := RemoteVA{ RemoteClients: setupRemote(nil, "", nil, "dadaist", arin), Perspective: "dadaist", @@ -367,12 +425,32 @@ func TestPerformValidationWithMismatchedRemoteVARIRs(t *testing.T) { } remoteVAs := setupRemotes([]remoteConf{{rir: ripe}}, nil) remoteVAs = append(remoteVAs, mismatched1, mismatched2) - va, mockLog := setup(nil, "", remoteVAs, nil) - req := createValidationRequest("good-dns01.com", core.ChallengeTypeDNS01) - res, _ := va.PerformValidation(context.Background(), req) - test.AssertNotNil(t, res.GetProblem(), "validation succeeded with mismatched remote VA perspectives") - test.AssertEquals(t, len(mockLog.GetAllMatching("Expected perspective")), 2) + testCases := []struct { + validationFuncName string + validationFunc validationFuncRunner + }{ + { + validationFuncName: "PerformValidation", + validationFunc: runPerformValidation, + }, + { + validationFuncName: "DoDCV", + validationFunc: runDoDCV, + }, + } + + for _, tc := range testCases { + t.Run(tc.validationFuncName, func(t *testing.T) { + t.Parallel() + + va, mockLog := setup(nil, "", remoteVAs, nil) + req := createValidationRequest("good-dns01.com", core.ChallengeTypeDNS01) + res, _ := tc.validationFunc(context.Background(), va, req) + test.AssertNotNil(t, res.GetProblem(), "validation succeeded with mismatched remote VA perspectives") + test.AssertEquals(t, len(mockLog.GetAllMatching("Expected perspective")), 2) + }) + } } func TestValidateMalformedChallenge(t *testing.T) { @@ -385,88 +463,203 @@ func TestValidateMalformedChallenge(t *testing.T) { } func TestPerformValidationInvalid(t *testing.T) { + t.Parallel() va, _ := setup(nil, "", nil, nil) - req := createValidationRequest("foo.com", core.ChallengeTypeDNS01) - res, _ := va.PerformValidation(context.Background(), req) - test.Assert(t, res.Problem != nil, "validation succeeded") - test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, prometheus.Labels{ - "operation": opChallAndCAA, - "perspective": va.perspective, - "challenge_type": string(core.ChallengeTypeDNS01), - "problem_type": string(probs.UnauthorizedProblem), - "result": fail, - }, 1) + testCases := []struct { + validationFuncName string + validationFunc validationFuncRunner + }{ + { + validationFuncName: "PerformValidation", + validationFunc: runPerformValidation, + }, + { + validationFuncName: "DoDCV", + validationFunc: runDoDCV, + }, + } + + for _, tc := range testCases { + t.Run(tc.validationFuncName, func(t *testing.T) { + t.Parallel() + + req := createValidationRequest("foo.com", core.ChallengeTypeDNS01) + res, _ := tc.validationFunc(context.Background(), va, req) + test.Assert(t, res.Problem != nil, "validation succeeded") + if tc.validationFuncName == "PerformValidation" { + test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, prometheus.Labels{ + "operation": opDCVAndCAA, + "perspective": va.perspective, + "challenge_type": string(core.ChallengeTypeDNS01), + "problem_type": string(probs.UnauthorizedProblem), + "result": fail, + }, 1) + } else { + test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, prometheus.Labels{ + "operation": opDCV, + "perspective": va.perspective, + "challenge_type": string(core.ChallengeTypeDNS01), + "problem_type": string(probs.UnauthorizedProblem), + "result": fail, + }, 1) + } + }) + } } func TestInternalErrorLogged(t *testing.T) { - va, mockLog := setup(nil, "", nil, nil) + t.Parallel() - ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond) - defer cancel() - req := createValidationRequest("nonexistent.com", core.ChallengeTypeHTTP01) - _, err := va.PerformValidation(ctx, req) - test.AssertNotError(t, err, "failed validation should not be an error") - matchingLogs := mockLog.GetAllMatching( - `Validation result JSON=.*"InternalError":"127.0.0.1: Get.*nonexistent.com/\.well-known.*: context deadline exceeded`) - test.AssertEquals(t, len(matchingLogs), 1) + testCases := []struct { + validationFuncName string + validationFunc validationFuncRunner + }{ + { + validationFuncName: "PerformValidation", + validationFunc: runPerformValidation, + }, + { + validationFuncName: "DoDCV", + validationFunc: runDoDCV, + }, + } + + for _, tc := range testCases { + t.Run(tc.validationFuncName, func(t *testing.T) { + t.Parallel() + + va, mockLog := setup(nil, "", nil, nil) + + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond) + defer cancel() + req := createValidationRequest("nonexistent.com", core.ChallengeTypeHTTP01) + _, err := tc.validationFunc(ctx, va, req) + test.AssertNotError(t, err, "failed validation should not be an error") + matchingLogs := mockLog.GetAllMatching( + `Validation result JSON=.*"InternalError":"127.0.0.1: Get.*nonexistent.com/\.well-known.*: context deadline exceeded`) + test.AssertEquals(t, len(matchingLogs), 1) + }) + } } func TestPerformValidationValid(t *testing.T) { - va, mockLog := setup(nil, "", nil, nil) - - // create a challenge with well known token - req := createValidationRequest("good-dns01.com", core.ChallengeTypeDNS01) - res, _ := va.PerformValidation(context.Background(), req) - test.Assert(t, res.Problem == nil, fmt.Sprintf("validation failed: %#v", res.Problem)) + t.Parallel() - test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, prometheus.Labels{ - "operation": opChallAndCAA, - "perspective": va.perspective, - "challenge_type": string(core.ChallengeTypeDNS01), - "problem_type": "", - "result": pass, - }, 1) - resultLog := mockLog.GetAllMatching(`Validation result`) - if len(resultLog) != 1 { - t.Fatalf("Wrong number of matching lines for 'Validation result'") + testCases := []struct { + validationFuncName string + validationFunc validationFuncRunner + }{ + { + validationFuncName: "PerformValidation", + validationFunc: runPerformValidation, + }, + { + validationFuncName: "DoDCV", + validationFunc: runDoDCV, + }, } - if !strings.Contains(resultLog[0], `"Identifier":"good-dns01.com"`) { - t.Error("PerformValidation didn't log validation identifier.") + + for _, tc := range testCases { + t.Run(tc.validationFuncName, func(t *testing.T) { + t.Parallel() + + va, mockLog := setup(nil, "", nil, nil) + + // create a challenge with well known token + req := createValidationRequest("good-dns01.com", core.ChallengeTypeDNS01) + res, _ := tc.validationFunc(context.Background(), va, req) + test.Assert(t, res.Problem == nil, fmt.Sprintf("validation failed: %#v", res.Problem)) + if tc.validationFuncName == "PerformValidation" { + test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, prometheus.Labels{ + "operation": opDCVAndCAA, + "perspective": va.perspective, + "challenge_type": string(core.ChallengeTypeDNS01), + "problem_type": "", + "result": pass, + }, 1) + } else { + test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, prometheus.Labels{ + "operation": opDCV, + "perspective": va.perspective, + "challenge_type": string(core.ChallengeTypeDNS01), + "problem_type": "", + "result": pass, + }, 1) + } + resultLog := mockLog.GetAllMatching(`Validation result`) + if len(resultLog) != 1 { + t.Fatalf("Wrong number of matching lines for 'Validation result'") + } + if !strings.Contains(resultLog[0], `"Identifier":"good-dns01.com"`) { + t.Error("PerformValidation didn't log validation identifier.") + } + }) } } // TestPerformValidationWildcard tests that the VA properly strips the `*.` // prefix from a wildcard name provided to the PerformValidation function. func TestPerformValidationWildcard(t *testing.T) { - va, mockLog := setup(nil, "", nil, nil) - - // create a challenge with well known token - req := createValidationRequest("*.good-dns01.com", core.ChallengeTypeDNS01) - // perform a validation for a wildcard name - res, _ := va.PerformValidation(context.Background(), req) - test.Assert(t, res.Problem == nil, fmt.Sprintf("validation failed: %#v", res.Problem)) + t.Parallel() - test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, prometheus.Labels{ - "operation": opChallAndCAA, - "perspective": va.perspective, - "challenge_type": string(core.ChallengeTypeDNS01), - "problem_type": "", - "result": pass, - }, 1) - resultLog := mockLog.GetAllMatching(`Validation result`) - if len(resultLog) != 1 { - t.Fatalf("Wrong number of matching lines for 'Validation result'") + testCases := []struct { + validationFuncName string + validationFunc validationFuncRunner + }{ + { + validationFuncName: "PerformValidation", + validationFunc: runPerformValidation, + }, + { + validationFuncName: "DoDCV", + validationFunc: runDoDCV, + }, } - // We expect that the top level Identifier reflect the wildcard name - if !strings.Contains(resultLog[0], `"Identifier":"*.good-dns01.com"`) { - t.Errorf("PerformValidation didn't log correct validation identifier.") - } - // We expect that the ValidationRecord contain the correct non-wildcard - // hostname that was validated - if !strings.Contains(resultLog[0], `"hostname":"good-dns01.com"`) { - t.Errorf("PerformValidation didn't log correct validation record hostname.") + for _, tc := range testCases { + t.Run(tc.validationFuncName, func(t *testing.T) { + t.Parallel() + + va, mockLog := setup(nil, "", nil, nil) + + // create a challenge with well known token + req := createValidationRequest("*.good-dns01.com", core.ChallengeTypeDNS01) + // perform a validation for a wildcard name + res, _ := tc.validationFunc(context.Background(), va, req) + test.Assert(t, res.Problem == nil, fmt.Sprintf("validation failed: %#v", res.Problem)) + if tc.validationFuncName == "PerformValidation" { + test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, prometheus.Labels{ + "operation": opDCVAndCAA, + "perspective": va.perspective, + "challenge_type": string(core.ChallengeTypeDNS01), + "problem_type": "", + "result": pass, + }, 1) + } else { + test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, prometheus.Labels{ + "operation": opDCV, + "perspective": va.perspective, + "challenge_type": string(core.ChallengeTypeDNS01), + "problem_type": "", + "result": pass, + }, 1) + } + resultLog := mockLog.GetAllMatching(`Validation result`) + if len(resultLog) != 1 { + t.Fatalf("Wrong number of matching lines for 'Validation result'") + } + + // We expect that the top level Identifier reflect the wildcard name + if !strings.Contains(resultLog[0], `"Identifier":"*.good-dns01.com"`) { + t.Errorf("PerformValidation didn't log correct validation identifier.") + } + // We expect that the ValidationRecord contain the correct non-wildcard + // hostname that was validated + if !strings.Contains(resultLog[0], `"hostname":"good-dns01.com"`) { + t.Errorf("PerformValidation didn't log correct validation record hostname.") + } + }) } } @@ -571,6 +764,22 @@ func TestMultiVA(t *testing.T) { CAAClient: cancelledVA{}, } + type testFunc struct { + name string + impl validationFuncRunner + } + + testFuncs := []testFunc{ + { + name: "PerformValidation", + impl: runPerformValidation, + }, + { + name: "DoDCV", + impl: runDoDCV, + }, + } + testCases := []struct { Name string Remotes []remoteConf @@ -727,40 +936,58 @@ func TestMultiVA(t *testing.T) { } for _, tc := range testCases { - t.Run(tc.Name, func(t *testing.T) { - t.Parallel() - - // Configure one test server per test case so that all tests can run in parallel. - ms := httpMultiSrv(t, expectedToken, map[string]bool{pass: true, fail: false}) - defer ms.Close() - - // Configure a primary VA with testcase remote VAs. - localVA, mockLog := setupWithRemotes(ms.Server, tc.PrimaryUA, tc.Remotes, nil) - - // Perform all validations - res, _ := localVA.PerformValidation(ctx, req) - if res.Problem == nil && tc.ExpectedProbType != "" { - t.Errorf("expected prob %v, got nil", tc.ExpectedProbType) - } else if res.Problem != nil && tc.ExpectedProbType == "" { - t.Errorf("expected no prob, got %v", res.Problem) - } else if res.Problem != nil && tc.ExpectedProbType != "" { - // That result should match expected. - test.AssertEquals(t, res.Problem.ProblemType, tc.ExpectedProbType) - } + for _, testFunc := range testFuncs { + t.Run(tc.Name+"_"+testFunc.name, func(t *testing.T) { + t.Parallel() + + // Configure one test server per test case so that all tests can run in parallel. + ms := httpMultiSrv(t, expectedToken, map[string]bool{pass: true, fail: false}) + defer ms.Close() + + // Configure a primary VA with testcase remote VAs. + localVA, mockLog := setupWithRemotes(ms.Server, tc.PrimaryUA, tc.Remotes, nil) + + // Perform all validations + res, _ := testFunc.impl(ctx, localVA, req) + if res.Problem == nil && tc.ExpectedProbType != "" { + t.Errorf("expected prob %v, got nil", tc.ExpectedProbType) + } else if res.Problem != nil && tc.ExpectedProbType == "" { + t.Errorf("expected no prob, got %v", res.Problem) + } else if res.Problem != nil && tc.ExpectedProbType != "" { + // That result should match expected. + test.AssertEquals(t, res.Problem.ProblemType, tc.ExpectedProbType) + } - if tc.ExpectedLogContains != "" { - lines := mockLog.GetAllMatching(tc.ExpectedLogContains) - if len(lines) == 0 { - t.Fatalf("Got log %v; expected %q", mockLog.GetAll(), tc.ExpectedLogContains) + if tc.ExpectedLogContains != "" { + lines := mockLog.GetAllMatching(tc.ExpectedLogContains) + if len(lines) == 0 { + t.Fatalf("Got log %v; expected %q", mockLog.GetAll(), tc.ExpectedLogContains) + } } - } - }) + }) + } } } func TestMultiVAEarlyReturn(t *testing.T) { t.Parallel() + type testFunc struct { + name string + impl validationFuncRunner + } + + testFuncs := []testFunc{ + { + name: "PerformValidation", + impl: runPerformValidation, + }, + { + name: "DoDCV", + impl: runDoDCV, + }, + } + testCases := []struct { remoteConfs []remoteConf }{ @@ -793,80 +1020,123 @@ func TestMultiVAEarlyReturn(t *testing.T) { } for i, tc := range testCases { - t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { - t.Parallel() + for _, testFunc := range testFuncs { + t.Run(fmt.Sprintf("case %d"+"_"+testFunc.name, i), func(t *testing.T) { + t.Parallel() - // Configure one test server per test case so that all tests can run in parallel. - ms := httpMultiSrv(t, expectedToken, map[string]bool{pass: true, fail: false}) - defer ms.Close() + // Configure one test server per test case so that all tests can run in parallel. + ms := httpMultiSrv(t, expectedToken, map[string]bool{pass: true, fail: false}) + defer ms.Close() - localVA, _ := setupWithRemotes(ms.Server, pass, tc.remoteConfs, nil) + localVA, _ := setupWithRemotes(ms.Server, pass, tc.remoteConfs, nil) - // Perform all validations - start := time.Now() - req := createValidationRequest("localhost", core.ChallengeTypeHTTP01) - res, _ := localVA.PerformValidation(ctx, req) + // Perform all validations + start := time.Now() + req := createValidationRequest("localhost", core.ChallengeTypeHTTP01) + res, _ := testFunc.impl(ctx, localVA, req) - // It should always fail - if res.Problem == nil { - t.Error("expected prob from PerformValidation, got nil") - } + // It should always fail + if res.Problem == nil { + t.Error("expected prob from PerformValidation, got nil") + } - elapsed := time.Since(start).Round(time.Millisecond).Milliseconds() + elapsed := time.Since(start).Round(time.Millisecond).Milliseconds() - // The slow UA should sleep for `slowRemoteSleepMillis`. But the first remote - // VA should fail quickly and the early-return code should cause the overall - // overall validation to return a prob quickly (i.e. in less than half of - // `slowRemoteSleepMillis`). - if elapsed > slowRemoteSleepMillis/2 { - t.Errorf( - "Expected an early return from PerformValidation in < %d ms, took %d ms", - slowRemoteSleepMillis/2, elapsed) - } - }) + // The slow UA should sleep for `slowRemoteSleepMillis`. But the first remote + // VA should fail quickly and the early-return code should cause the overall + // overall validation to return a prob quickly (i.e. in less than half of + // `slowRemoteSleepMillis`). + if elapsed > slowRemoteSleepMillis/2 { + t.Errorf( + "Expected an early return from PerformValidation in < %d ms, took %d ms", + slowRemoteSleepMillis/2, elapsed) + } + }) + } } } func TestMultiVAPolicy(t *testing.T) { t.Parallel() - ms := httpMultiSrv(t, expectedToken, map[string]bool{pass: true, fail: false}) - defer ms.Close() - remoteConfs := []remoteConf{ {ua: fail, rir: arin}, {ua: fail, rir: ripe}, {ua: fail, rir: apnic}, } - // Create a local test VA with the remote VAs - localVA, _ := setupWithRemotes(ms.Server, pass, remoteConfs, nil) + testCases := []struct { + validationFuncName string + validationFunc validationFuncRunner + }{ + { + validationFuncName: "PerformValidation", + validationFunc: runPerformValidation, + }, + { + validationFuncName: "DoDCV", + validationFunc: runDoDCV, + }, + } + + for _, tc := range testCases { + t.Run(tc.validationFuncName, func(t *testing.T) { + t.Parallel() + + ms := httpMultiSrv(t, expectedToken, map[string]bool{pass: true, fail: false}) + defer ms.Close() + + // Create a local test VA with the remote VAs + localVA, _ := setupWithRemotes(ms.Server, pass, remoteConfs, nil) - // Perform validation for a domain not in the disabledDomains list - req := createValidationRequest("letsencrypt.org", core.ChallengeTypeHTTP01) - res, _ := localVA.PerformValidation(ctx, req) - // It should fail - if res.Problem == nil { - t.Error("expected prob from PerformValidation, got nil") + // Perform validation for a domain not in the disabledDomains list + req := createValidationRequest("letsencrypt.org", core.ChallengeTypeHTTP01) + res, _ := tc.validationFunc(ctx, localVA, req) + // It should fail + if res.Problem == nil { + t.Error("expected prob from PerformValidation, got nil") + } + }) } } + func TestMultiVALogging(t *testing.T) { t.Parallel() - ms := httpMultiSrv(t, expectedToken, map[string]bool{pass: true, fail: false}) - defer ms.Close() - remoteConfs := []remoteConf{ {ua: pass, rir: arin}, {ua: pass, rir: ripe}, {ua: pass, rir: apnic}, } - va, _ := setupWithRemotes(ms.Server, pass, remoteConfs, nil) - req := createValidationRequest("letsencrypt.org", core.ChallengeTypeHTTP01) - res, err := va.PerformValidation(ctx, req) - test.Assert(t, res.Problem == nil, fmt.Sprintf("validation failed with: %#v", res.Problem)) - test.AssertNotError(t, err, "performing validation") + testCases := []struct { + validationFuncName string + validationFunc validationFuncRunner + }{ + { + validationFuncName: "PerformValidation", + validationFunc: runPerformValidation, + }, + { + validationFuncName: "DoDCV", + validationFunc: runDoDCV, + }, + } + + for _, tc := range testCases { + t.Run(tc.validationFuncName, func(t *testing.T) { + t.Parallel() + + ms := httpMultiSrv(t, expectedToken, map[string]bool{pass: true, fail: false}) + defer ms.Close() + + va, _ := setupWithRemotes(ms.Server, pass, remoteConfs, nil) + req := createValidationRequest("letsencrypt.org", core.ChallengeTypeHTTP01) + res, err := tc.validationFunc(ctx, va, req) + test.Assert(t, res.Problem == nil, fmt.Sprintf("validation failed with: %#v", res.Problem)) + test.AssertNotError(t, err, "performing validation") + }) + } } func TestDetailedError(t *testing.T) { diff --git a/va/vampic.go b/va/vampic.go new file mode 100644 index 00000000000..28b6036f7d8 --- /dev/null +++ b/va/vampic.go @@ -0,0 +1,428 @@ +package va + +import ( + "context" + "errors" + "fmt" + "maps" + "math/rand/v2" + "slices" + "time" + + "github.com/letsencrypt/boulder/core" + corepb "github.com/letsencrypt/boulder/core/proto" + berrors "github.com/letsencrypt/boulder/errors" + bgrpc "github.com/letsencrypt/boulder/grpc" + "github.com/letsencrypt/boulder/identifier" + "github.com/letsencrypt/boulder/probs" + vapb "github.com/letsencrypt/boulder/va/proto" + "google.golang.org/protobuf/proto" +) + +const ( + // requiredRIRs is the minimum number of distinct Regional Internet + // Registries required for MPIC-compliant validation. Per BRs Section + // 3.2.2.9, starting March 15, 2026, the required number is 2. + requiredRIRs = 2 +) + +// mpicSummary is returned by doRemoteOperation and contains a summary of the +// validation results for logging purposes. To ensure that the JSON output does +// not contain nil slices, and to ensure deterministic output use the +// summarizeMPIC function to prepare an mpicSummary. +type mpicSummary struct { + // Passed are the perspectives that passed validation. + Passed []string `json:"passedPerspectives"` + + // Failed are the perspectives that failed validation. + Failed []string `json:"failedPerspectives"` + + // PassedRIRs are the Regional Internet Registries that the passing + // perspectives reside in. + PassedRIRs []string `json:"passedRIRs"` + + // QuorumResult is the Multi-Perspective Issuance Corroboration quorum + // result, per BRs Section 5.4.1, Requirement 2.7 (i.e., "3/4" which should + // be interpreted as "Three (3) out of four (4) attempted Network + // Perspectives corroborated the determinations made by the Primary Network + // Perspective". + QuorumResult string `json:"quorumResult"` +} + +// summarizeMPIC prepares an *mpicSummary for logging, ensuring there are no nil +// slices and output is deterministic. +func summarizeMPIC(passed, failed []string, passedRIRSet map[string]struct{}) *mpicSummary { + if passed == nil { + passed = []string{} + } + slices.Sort(passed) + if failed == nil { + failed = []string{} + } + slices.Sort(failed) + + passedRIRs := []string{} + if passedRIRSet != nil { + for rir := range maps.Keys(passedRIRSet) { + passedRIRs = append(passedRIRs, rir) + } + } + slices.Sort(passedRIRs) + + return &mpicSummary{ + Passed: passed, + Failed: failed, + PassedRIRs: passedRIRs, + QuorumResult: fmt.Sprintf("%d/%d", len(passed), len(passed)+len(failed)), + } +} + +// doRemoteOperation concurrently calls the provided operation with `req` and a +// RemoteVA once for each configured RemoteVA. It cancels remaining operations +// and returns early if either the required number of successful results is +// obtained or the number of failures exceeds va.maxRemoteFailures. +// +// Internal logic errors are logged. If the number of operation failures exceeds +// va.maxRemoteFailures, the first encountered problem is returned as a +// *probs.ProblemDetails. +func (va *ValidationAuthorityImpl) doRemoteOperation(ctx context.Context, op remoteOperation, req proto.Message) (*mpicSummary, *probs.ProblemDetails) { + remoteVACount := len(va.remoteVAs) + // - Mar 15, 2026: MUST implement using at least 3 perspectives + // - Jun 15, 2026: MUST implement using at least 4 perspectives + // - Dec 15, 2026: MUST implement using at least 5 perspectives + // See "Phased Implementation Timeline" in + // https://github.com/cabforum/servercert/blob/main/docs/BR.md#3229-multi-perspective-issuance-corroboration + if remoteVACount < 3 { + return nil, probs.ServerInternal("Insufficient remote perspectives: need at least 3") + } + + type response struct { + addr string + perspective string + rir string + result remoteResult + err error + } + + subCtx, cancel := context.WithCancel(ctx) + defer cancel() + + responses := make(chan *response, remoteVACount) + for _, i := range rand.Perm(remoteVACount) { + go func(rva RemoteVA) { + res, err := op(subCtx, rva, req) + if err != nil { + responses <- &response{rva.Address, rva.Perspective, rva.RIR, res, err} + return + } + if res.GetPerspective() != rva.Perspective || res.GetRir() != rva.RIR { + err = fmt.Errorf( + "Expected perspective %q (%q) but got reply from %q (%q) - misconfiguration likely", rva.Perspective, rva.RIR, res.GetPerspective(), res.GetRir(), + ) + responses <- &response{rva.Address, rva.Perspective, rva.RIR, res, err} + return + } + responses <- &response{rva.Address, rva.Perspective, rva.RIR, res, err} + }(va.remoteVAs[i]) + } + + required := remoteVACount - va.maxRemoteFailures + var passed []string + var failed []string + var passedRIRs = map[string]struct{}{} + var firstProb *probs.ProblemDetails + + for resp := range responses { + var currProb *probs.ProblemDetails + + if resp.err != nil { + // Failed to communicate with the remote VA. + failed = append(failed, resp.perspective) + + if core.IsCanceled(resp.err) { + currProb = probs.ServerInternal("Secondary validation RPC canceled") + } else { + va.log.Errf("Operation on remote VA (%s) failed: %s", resp.addr, resp.err) + currProb = probs.ServerInternal("Secondary validation RPC failed") + } + } else if resp.result.GetProblem() != nil { + // The remote VA returned a problem. + failed = append(failed, resp.perspective) + + var err error + currProb, err = bgrpc.PBToProblemDetails(resp.result.GetProblem()) + if err != nil { + va.log.Errf("Operation on Remote VA (%s) returned malformed problem: %s", resp.addr, err) + currProb = probs.ServerInternal("Secondary validation RPC returned malformed result") + } + } else { + // The remote VA returned a successful result. + passed = append(passed, resp.perspective) + passedRIRs[resp.rir] = struct{}{} + } + + if firstProb == nil && currProb != nil { + // A problem was encountered for the first time. + firstProb = currProb + } + + // To respond faster, if we get enough successes or too many failures, we cancel remaining RPCs. + // Finish the loop to collect remaining responses into `failed` so we can rely on having a response + // for every request we made. + if len(passed) >= required && len(passedRIRs) >= requiredRIRs { + cancel() + } + if len(failed) > va.maxRemoteFailures { + cancel() + } + + // Once all the VAs have returned a result, break the loop. + if len(passed)+len(failed) >= remoteVACount { + break + } + } + if len(passed) >= required && len(passedRIRs) >= requiredRIRs { + return summarizeMPIC(passed, failed, passedRIRs), nil + } + if firstProb == nil { + // This should never happen. If we didn't meet the thresholds above we + // should have seen at least one error. + return summarizeMPIC(passed, failed, passedRIRs), probs.ServerInternal( + "During secondary validation: validation failed but the problem is unavailable") + } + firstProb.Detail = fmt.Sprintf("During secondary validation: %s", firstProb.Detail) + return summarizeMPIC(passed, failed, passedRIRs), firstProb +} + +// validationLogEvent is a struct that contains the information needed to log +// the results of DoCAA and DoDCV. +type validationLogEvent struct { + AuthzID string + Requester int64 + Identifier string + Challenge core.Challenge + Error string `json:",omitempty"` + InternalError string `json:",omitempty"` + Latency float64 + Summary *mpicSummary `json:",omitempty"` +} + +// DoDCV conducts a local Domain Control Validation (DCV) for the specified +// challenge. When invoked on the primary Validation Authority (VA) and the +// local validation succeeds, it also performs DCV validations using the +// configured remote VAs. Failed validations are indicated by a non-nil Problems +// in the returned ValidationResult. DoDCV returns error only for internal logic +// errors (and the client may receive errors from gRPC in the event of a +// communication problem). ValidationResult always includes a list of +// ValidationRecords, even when it also contains Problems. This method +// implements the DCV portion of Multi-Perspective Issuance Corroboration as +// defined in BRs Sections 3.2.2.9 and 5.4.1. +func (va *ValidationAuthorityImpl) DoDCV(ctx context.Context, req *vapb.PerformValidationRequest) (*vapb.ValidationResult, error) { + if core.IsAnyNilOrZero(req, req.DnsName, req.Challenge, req.Authz, req.ExpectedKeyAuthorization) { + return nil, berrors.InternalServerError("Incomplete validation request") + } + + chall, err := bgrpc.PBToChallenge(req.Challenge) + if err != nil { + return nil, errors.New("challenge failed to deserialize") + } + + err = chall.CheckPending() + if err != nil { + return nil, berrors.MalformedError("challenge failed consistency check: %s", err) + } + + // Initialize variables and a deferred function to handle validation latency + // metrics, log validation errors, and log an MPIC summary. Avoid using := + // to redeclare `prob`, `localLatency`, or `summary` below this point. + var prob *probs.ProblemDetails + var summary *mpicSummary + var localLatency time.Duration + start := va.clk.Now() + logEvent := validationLogEvent{ + AuthzID: req.Authz.Id, + Requester: req.Authz.RegID, + Identifier: req.DnsName, + Challenge: chall, + } + defer func() { + probType := "" + outcome := fail + if prob != nil { + probType = string(prob.Type) + logEvent.Error = prob.Error() + logEvent.Challenge.Error = prob + logEvent.Challenge.Status = core.StatusInvalid + } else { + logEvent.Challenge.Status = core.StatusValid + outcome = pass + } + // Observe local validation latency (primary|remote). + va.observeLatency(opDCV, va.perspective, string(chall.Type), probType, outcome, localLatency) + if va.isPrimaryVA() { + // Observe total validation latency (primary+remote). + va.observeLatency(opDCV, allPerspectives, string(chall.Type), probType, outcome, va.clk.Since(start)) + logEvent.Summary = summary + } + + // Log the total validation latency. + logEvent.Latency = va.clk.Since(start).Round(time.Millisecond).Seconds() + va.log.AuditObject("Validation result", logEvent) + }() + + // Do local validation. Note that we process the result in a couple ways + // *before* checking whether it returned an error. These few checks are + // carefully written to ensure that they work whether the local validation + // was successful or not, and cannot themselves fail. + records, err := va.validateChallenge( + ctx, + identifier.NewDNS(req.DnsName), + chall.Type, + chall.Token, + req.ExpectedKeyAuthorization, + ) + + // Stop the clock for local validation latency. + localLatency = va.clk.Since(start) + + // Check for malformed ValidationRecords + logEvent.Challenge.ValidationRecord = records + if err == nil && !logEvent.Challenge.RecordsSane() { + err = errors.New("records from local validation failed sanity check") + } + + if err != nil { + logEvent.InternalError = err.Error() + prob = detailedError(err) + return bgrpc.ValidationResultToPB(records, filterProblemDetails(prob), va.perspective, va.rir) + } + + if va.isPrimaryVA() { + // Do remote validation. We do this after local validation is complete + // to avoid wasting work when validation will fail anyway. This only + // returns a singular problem, because the remote VAs have already + // logged their own validationLogEvent, and it's not helpful to present + // multiple large errors to the end user. + op := func(ctx context.Context, remoteva RemoteVA, req proto.Message) (remoteResult, error) { + validationRequest, ok := req.(*vapb.PerformValidationRequest) + if !ok { + return nil, fmt.Errorf("got type %T, want *vapb.PerformValidationRequest", req) + } + return remoteva.DoDCV(ctx, validationRequest) + } + summary, prob = va.doRemoteOperation(ctx, op, req) + } + return bgrpc.ValidationResultToPB(records, filterProblemDetails(prob), va.perspective, va.rir) +} + +// DoCAA conducts a CAA check for the specified dnsName. When invoked on the +// primary Validation Authority (VA) and the local check succeeds, it also +// performs CAA checks using the configured remote VAs. Failed checks are +// indicated by a non-nil Problems in the returned ValidationResult. DoCAA +// returns error only for internal logic errors (and the client may receive +// errors from gRPC in the event of a communication problem). This method +// implements the CAA portion of Multi-Perspective Issuance Corroboration as +// defined in BRs Sections 3.2.2.9 and 5.4.1. +func (va *ValidationAuthorityImpl) DoCAA(ctx context.Context, req *vapb.IsCAAValidRequest) (*vapb.IsCAAValidResponse, error) { + if core.IsAnyNilOrZero(req.Domain, req.ValidationMethod, req.AccountURIID) { + return nil, berrors.InternalServerError("incomplete IsCAAValid request") + } + logEvent := validationLogEvent{ + AuthzID: req.AuthzID, + Requester: req.AccountURIID, + Identifier: req.Domain, + } + + challType := core.AcmeChallenge(req.ValidationMethod) + if !challType.IsValid() { + return nil, berrors.InternalServerError("unrecognized validation method %q", req.ValidationMethod) + } + + acmeID := identifier.NewDNS(req.Domain) + params := &caaParams{ + accountURIID: req.AccountURIID, + validationMethod: challType, + } + + // Initialize variables and a deferred function to handle check latency + // metrics, log check errors, and log an MPIC summary. Avoid using := to + // redeclare `prob`, `localLatency`, or `summary` below this point. + var prob *probs.ProblemDetails + var summary *mpicSummary + var internalErr error + var localLatency time.Duration + start := va.clk.Now() + + defer func() { + probType := "" + outcome := fail + if prob != nil { + // CAA check failed. + probType = string(prob.Type) + logEvent.Error = prob.Error() + } else { + // CAA check passed. + outcome = pass + } + // Observe local check latency (primary|remote). + va.observeLatency(opCAA, va.perspective, string(challType), probType, outcome, localLatency) + if va.isPrimaryVA() { + // Observe total check latency (primary+remote). + va.observeLatency(opCAA, allPerspectives, string(challType), probType, outcome, va.clk.Since(start)) + logEvent.Summary = summary + } + // Log the total check latency. + logEvent.Latency = va.clk.Since(start).Round(time.Millisecond).Seconds() + + va.log.AuditObject("CAA check result", logEvent) + }() + + internalErr = va.checkCAA(ctx, acmeID, params) + + // Stop the clock for local check latency. + localLatency = va.clk.Since(start) + + if internalErr != nil { + logEvent.InternalError = internalErr.Error() + prob = detailedError(internalErr) + prob.Detail = fmt.Sprintf("While processing CAA for %s: %s", req.Domain, prob.Detail) + } + + if va.isPrimaryVA() { + op := func(ctx context.Context, remoteva RemoteVA, req proto.Message) (remoteResult, error) { + checkRequest, ok := req.(*vapb.IsCAAValidRequest) + if !ok { + return nil, fmt.Errorf("got type %T, want *vapb.IsCAAValidRequest", req) + } + return remoteva.DoCAA(ctx, checkRequest) + } + var remoteProb *probs.ProblemDetails + summary, remoteProb = va.doRemoteOperation(ctx, op, req) + // If the remote result was a non-nil problem then fail the CAA check + if remoteProb != nil { + prob = remoteProb + va.log.Infof("CAA check failed due to remote failures: identifier=%v err=%s", + req.Domain, remoteProb) + } + } + + if prob != nil { + // The ProblemDetails will be serialized through gRPC, which requires UTF-8. + // It will also later be serialized in JSON, which defaults to UTF-8. Make + // sure it is UTF-8 clean now. + prob = filterProblemDetails(prob) + return &vapb.IsCAAValidResponse{ + Problem: &corepb.ProblemDetails{ + ProblemType: string(prob.Type), + Detail: replaceInvalidUTF8([]byte(prob.Detail)), + }, + Perspective: va.perspective, + Rir: va.rir, + }, nil + } else { + return &vapb.IsCAAValidResponse{ + Perspective: va.perspective, + Rir: va.rir, + }, nil + } +}