diff --git a/cmd/boulder-ra/main.go b/cmd/boulder-ra/main.go index 45d142e0bf4..40d4f2ffa64 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" ) @@ -301,7 +302,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 b7053fb2373..5c0e2dc518b 100644 --- a/features/features.go +++ b/features/features.go @@ -102,6 +102,10 @@ type Config struct { // and pause issuance for each (account, hostname) pair that repeatedly // fails validation. AutomaticallyPauseZombieClients bool + + // SeparateDCVAndCAAChecks causes the VA to perform DCV checks and CAA checks + // in separate steps, using separate VA methods DoDCV and DoCAA. + SeparateDCVAndCAAChecks bool } var fMu = new(sync.RWMutex) diff --git a/ra/ra.go b/ra/ra.go index 4c38937df4e..3553020fdbf 100644 --- a/ra/ra.go +++ b/ra/ra.go @@ -52,6 +52,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" @@ -74,6 +75,11 @@ type caaChecker interface { in *vapb.IsCAAValidRequest, opts ...grpc.CallOption, ) (*vapb.IsCAAValidResponse, error) + DoCAA( + ctx context.Context, + in *vapb.IsCAAValidRequest, + opts ...grpc.CallOption, + ) (*vapb.IsCAAValidResponse, error) } // RegistrationAuthorityImpl defines an RA. @@ -84,7 +90,7 @@ 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 @@ -849,12 +855,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().SeparateDCVAndCAAChecks { + resp, err = ra.caa.IsCAAValid(ctx, &vapb.IsCAAValidRequest{ + Domain: name, + ValidationMethod: method, + AccountURIID: authz.RegistrationID, + }) + } else { + resp, err = ra.caa.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 +1847,30 @@ func (ra *RegistrationAuthorityImpl) resetAccountPausingLimit(ctx context.Contex } } +func (ra *RegistrationAuthorityImpl) checkDCVAndCAA(ctx context.Context, dcvReq *vapb.PerformValidationRequest, caaReq *vapb.IsCAAValidRequest) (*corepb.ProblemDetails, []*corepb.ValidationRecord, error) { + if !features.Get().SeparateDCVAndCAAChecks { + 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 +1955,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 1da5ec912cc..c121c8f6fbf 100644 --- a/ra/ra_test.go +++ b/ra/ra_test.go @@ -63,6 +63,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" ) @@ -155,14 +156,51 @@ 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 + dva.doDCVRequest <- req + 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 ( @@ -310,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, 2), + doCAARequest: make(chan *vapb.IsCAAValidRequest, 2), } + va := va.RemoteClients{VAClient: dummyVA, CAAClient: dummyVA} pa, err := policy.New(map[core.AcmeChallenge]bool{ core.ChallengeTypeHTTP01: true, @@ -370,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) { @@ -688,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"), @@ -699,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. @@ -717,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"), @@ -729,6 +770,7 @@ func TestPerformValidationSuccess(t *testing.T) { }, Problem: nil, } + va.doCAAResponse = &vapb.IsCAAValidResponse{Problem: nil} now := fc.Now() challIdx := dnsChallIdx(t, authzPB.Challenges) @@ -740,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") @@ -821,16 +863,8 @@ func TestPerformValidation_FailedValidationsTriggerPauseIdentifiersRatelimit(t * // Now a failed validation should result in the identifier being paused // due to the strict ratelimit. - va.PerformValidationRequestResultReturn = &vapb.ValidationResult{ - Records: []*corepb.ValidationRecord{ - { - AddressUsed: []byte("192.168.0.1"), - Hostname: domain, - Port: "8080", - Url: fmt.Sprintf("http://%s/", domain), - ResolverAddrs: []string{"rebound"}, - }, - }, + va.doDCVResult = &vapb.ValidationResult{Problem: nil} + va.doCAAResponse = &vapb.IsCAAValidResponse{ Problem: &corepb.ProblemDetails{ Detail: fmt.Sprintf("CAA invalid for %s", domain), }, @@ -892,8 +926,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"), @@ -905,6 +938,7 @@ func TestPerformValidation_FailedThenSuccessfulValidationResetsPauseIdentifiersR }, Problem: nil, } + va.doCAAResponse = &vapb.IsCAAValidResponse{Problem: nil} _, err = ra.PerformValidation(ctx, &rapb.PerformValidationRequest{ Authz: authzPB, @@ -930,7 +964,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{ @@ -942,7 +976,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") @@ -1681,6 +1715,14 @@ func (cr noopCAA) IsCAAValid( return &vapb.IsCAAValidResponse{}, nil } +func (cr noopCAA) DoCAA( + ctx context.Context, + in *vapb.IsCAAValidRequest, + opts ...grpc.CallOption, +) (*vapb.IsCAAValidResponse, error) { + return &vapb.IsCAAValidResponse{}, nil +} + // caaRecorder implements caaChecker, always returning nil, but recording the // names it was called for. type caaRecorder struct { @@ -1699,6 +1741,17 @@ 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) { @@ -1891,6 +1944,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() diff --git a/test/config-next/ra.json b/test/config-next/ra.json index cacda398d5f..96dd7e8eaae 100644 --- a/test/config-next/ra.json +++ b/test/config-next/ra.json @@ -128,7 +128,8 @@ "features": { "AsyncFinalize": true, "UseKvLimitsForNewOrder": true, - "AutomaticallyPauseZombieClients": true + "AutomaticallyPauseZombieClients": true, + "SeparateDCVAndCAAChecks": 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/test/config-next/wfe2.json b/test/config-next/wfe2.json index c9fd2d3252e..a73cafbab0f 100644 --- a/test/config-next/wfe2.json +++ b/test/config-next/wfe2.json @@ -128,7 +128,8 @@ "PropagateCancels": true, "ServeRenewalInfo": true, "CheckIdentifiersPaused": true, - "UseKvLimitsForNewOrder": true + "UseKvLimitsForNewOrder": true, + "SeparateDCVAndCAAChecks": true }, "certProfiles": { "legacy": "The normal profile you know and love", 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/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 3a746e0660f..aed0a3aebd8 100644 --- a/va/va.go +++ b/va/va.go @@ -37,6 +37,7 @@ const ( allPerspectives = "all" opChallAndCAA = "challenge+caa" + opChall = "challenge" opCAA = "caa" pass = "pass" @@ -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 [challenge|caa|challenge+caa] // - perspective: ValidationAuthorityImpl.perspective // - challenge_type: core.Challenge.Type // - problem_type: probs.ProblemType @@ -437,7 +438,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 [challenge|caa] // - perspective: [ValidationAuthorityImpl.perspective|all] // - challenge_type: core.Challenge.Type // - problem_type: probs.ProblemType diff --git a/va/va_test.go b/va/va_test.go index b7925a3cc6c..6b484ee888a 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 } +// PerformValidation 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,48 +358,6 @@ func TestNewValidationAuthorityImplWithDuplicateRemotes(t *testing.T) { test.AssertContains(t, err.Error(), "duplicate remote VA perspective \"dadaist\"") } -func TestPerformValidationWithMismatchedRemoteVAPerspectives(t *testing.T) { - mismatched1 := RemoteVA{ - RemoteClients: setupRemote(nil, "", nil, "dadaist", arin), - Perspective: "baroque", - RIR: arin, - } - mismatched2 := RemoteVA{ - RemoteClients: setupRemote(nil, "", nil, "impressionist", ripe), - Perspective: "minimalist", - RIR: ripe, - } - 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) -} - -func TestPerformValidationWithMismatchedRemoteVARIRs(t *testing.T) { - mismatched1 := RemoteVA{ - RemoteClients: setupRemote(nil, "", nil, "dadaist", arin), - Perspective: "dadaist", - RIR: ripe, - } - mismatched2 := RemoteVA{ - RemoteClients: setupRemote(nil, "", nil, "impressionist", ripe), - Perspective: "impressionist", - RIR: arin, - } - 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) -} - func TestValidateMalformedChallenge(t *testing.T) { va, _ := setup(nil, "", nil, nil) @@ -384,89 +367,214 @@ func TestValidateMalformedChallenge(t *testing.T) { test.AssertEquals(t, prob.Type, probs.MalformedProblem) } +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 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": opChallAndCAA, + "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": opChall, + "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() + + 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() - 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) + 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": opChallAndCAA, + "perspective": va.perspective, + "challenge_type": string(core.ChallengeTypeDNS01), + "problem_type": "", + "result": pass, + }, 1) + } else { + test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, prometheus.Labels{ + "operation": opChall, + "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": opChallAndCAA, + "perspective": va.perspective, + "challenge_type": string(core.ChallengeTypeDNS01), + "problem_type": "", + "result": pass, + }, 1) + } else { + test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, prometheus.Labels{ + "operation": opChall, + "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.") + } + }) } } @@ -554,6 +662,7 @@ func TestPerformRemoteOperation(t *testing.T) { test.AssertContains(t, prob.Detail, tc.expectedDetail) }) } + } func TestMultiVA(t *testing.T) { @@ -571,6 +680,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,34 +852,36 @@ 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) + } } - } - }) + }) + } } } diff --git a/va/vampic.go b/va/vampic.go index 73e86792276..4442ee7ea73 100644 --- a/va/vampic.go +++ b/va/vampic.go @@ -4,34 +4,96 @@ import ( "context" "errors" "fmt" + "maps" "math/rand/v2" + "slices" "time" + "google.golang.org/protobuf/proto" + "github.com/letsencrypt/boulder/core" corepb "github.com/letsencrypt/boulder/core/proto" berrors "github.com/letsencrypt/boulder/errors" - "github.com/letsencrypt/boulder/features" 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" ) -// performRemoteOperation 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. +var ( + // 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 { + passedRIRs := []string{} + if passedRIRSet != nil { + for rir := range maps.Keys(passedRIRSet) { + passedRIRs = append(passedRIRs, rir) + } + slices.Sort(passedRIRs) + } + if passed == nil { + passed = []string{} + } + slices.Sort(passed) + if failed == nil { + failed = []string{} + } + slices.Sort(failed) + + 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) performRemoteOperation(ctx context.Context, op remoteOperation, req proto.Message) *probs.ProblemDetails { +func (va *ValidationAuthorityImpl) doRemoteOperation(ctx context.Context, op remoteOperation, req proto.Message) (*mpicSummary, *probs.ProblemDetails) { remoteVACount := len(va.remoteVAs) - if remoteVACount == 0 { - return nil + + // 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 + if remoteVACount < 3 { + return nil, probs.ServerInternal("Insufficient remote perspectives: need at least 3") } - isCAAValidReq, isCAACheck := req.(*vapb.IsCAAValidRequest) type response struct { addr string @@ -48,6 +110,17 @@ func (va *ValidationAuthorityImpl) performRemoteOperation(ctx context.Context, o 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]) } @@ -55,6 +128,7 @@ func (va *ValidationAuthorityImpl) performRemoteOperation(ctx context.Context, o required := remoteVACount - va.maxRemoteFailures var passed []string var failed []string + var passedRIRs = map[string]struct{}{} var firstProb *probs.ProblemDetails for resp := range responses { @@ -80,13 +154,10 @@ func (va *ValidationAuthorityImpl) performRemoteOperation(ctx context.Context, o va.log.Errf("Operation on Remote VA (%s) returned malformed problem: %s", resp.addr, err) currProb = probs.ServerInternal("Secondary validation RPC returned malformed result") } - if isCAACheck { - // We're checking CAA, log the problem. - va.log.Errf("Operation on Remote VA (%s) returned a problem: %s", resp.addr, currProb) - } } else { // The remote VA returned a successful result. passed = append(passed, resp.perspective) + passedRIRs[resp.rir] = struct{}{} } if firstProb == nil && currProb != nil { @@ -97,7 +168,7 @@ func (va *ValidationAuthorityImpl) performRemoteOperation(ctx context.Context, o // 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 { + if len(passed) >= required && len(passedRIRs) >= requiredRIRs { cancel() } if len(failed) > va.maxRemoteFailures { @@ -110,26 +181,28 @@ func (va *ValidationAuthorityImpl) performRemoteOperation(ctx context.Context, o } } - if isCAACheck { - // We're checking CAA, log the results. - va.logRemoteResults(isCAAValidReq, len(passed), len(failed)) - } - if len(passed) >= required { - return nil + if len(passedRIRs) >= requiredRIRs { + return summarizeMPIC(passed, failed, passedRIRs), nil + } + return summarizeMPIC(passed, failed, passedRIRs), probs.Unauthorized( + "During secondary validation: validation could not be corroborated by enough distinct RIRs", + ) + } else if len(failed) > va.maxRemoteFailures { firstProb.Detail = fmt.Sprintf("During secondary validation: %s", firstProb.Detail) - return firstProb + return summarizeMPIC(passed, failed, passedRIRs), firstProb + } else { // This condition should not occur - it indicates the passed/failed counts // neither met the required threshold nor the maxRemoteFailures threshold. - return probs.ServerInternal("Too few remote RPC results") + return summarizeMPIC(passed, failed, passedRIRs), probs.ServerInternal("Too few remote RPC results") } } -// verificationRequestEvent is logged once for each validation attempt. Its -// fields are exported for logging purposes. -type verificationRequestEvent struct { +// 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 @@ -137,19 +210,20 @@ type verificationRequestEvent struct { Error string `json:",omitempty"` InternalError string `json:",omitempty"` Latency float64 + Summary *mpicSummary `json:",omitempty"` } -// PerformValidation conducts a local Domain Control Validation (DCV) and CAA -// check for the specified challenge and dnsName. When invoked on the primary -// Validation Authority (VA) and the local validation succeeds, it also performs -// DCV and CAA checks using the configured remote VAs. Failed validations are -// indicated by a non-nil Problems in the returned ValidationResult. -// PerformValidation 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 does NOT implement Multi-Perspective -// Issuance Corroboration as defined in BRs Sections 3.2.2.9 and 5.4.1. -func (va *ValidationAuthorityImpl) PerformValidation(ctx context.Context, req *vapb.PerformValidationRequest) (*vapb.ValidationResult, error) { +// 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") } @@ -168,9 +242,10 @@ func (va *ValidationAuthorityImpl) PerformValidation(ctx context.Context, req *v // metrics and log validation errors. Below here, do not use := to redeclare // `prob`, or this will fail. var prob *probs.ProblemDetails + var summary *mpicSummary var localLatency time.Duration start := va.clk.Now() - logEvent := verificationRequestEvent{ + logEvent := validationLogEvent{ AuthzID: req.Authz.Id, Requester: req.Authz.RegID, Identifier: req.DnsName, @@ -189,10 +264,11 @@ 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(opChall, 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(opChall, allPerspectives, string(chall.Type), probType, outcome, va.clk.Since(start)) + logEvent.Summary = summary } // Log the total validation latency. @@ -204,13 +280,13 @@ func (va *ValidationAuthorityImpl) PerformValidation(ctx context.Context, req *v // *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.performLocalValidation( + records, err := va.validateChallenge( ctx, identifier.NewDNS(req.DnsName), - req.Authz.RegID, chall.Type, chall.Token, - req.ExpectedKeyAuthorization) + req.ExpectedKeyAuthorization, + ) // Stop the clock for local validation latency. localLatency = va.clk.Since(start) @@ -227,31 +303,39 @@ func (va *ValidationAuthorityImpl) PerformValidation(ctx context.Context, req *v return bgrpc.ValidationResultToPB(records, filterProblemDetails(prob), va.perspective, va.rir) } - // 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 audit-logged their - // own validation records, 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) + 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 audit-logged their + // own validation records, 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) } - return remoteva.PerformValidation(ctx, validationRequest) + summary, prob = va.doRemoteOperation(ctx, op, req) } - prob = va.performRemoteOperation(ctx, op, req) + return bgrpc.ValidationResultToPB(records, filterProblemDetails(prob), va.perspective, va.rir) } -// IsCAAValid checks requested CAA records from a VA, and recursively any RVAs -// configured in the VA. It returns a response or an error. -func (va *ValidationAuthorityImpl) IsCAAValid(ctx context.Context, req *vapb.IsCAAValidRequest) (*vapb.IsCAAValidResponse, error) { +// 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 := verificationRequestEvent{ - // TODO(#7061) Plumb req.Authz.Id as "AuthzID:" through from the RA to - // correlate which authz triggered this request. + logEvent := validationLogEvent{ + AuthzID: req.AuthzID, Requester: req.AccountURIID, Identifier: req.Domain, } @@ -268,6 +352,7 @@ func (va *ValidationAuthorityImpl) IsCAAValid(ctx context.Context, req *vapb.IsC } var prob *probs.ProblemDetails + var summary *mpicSummary var internalErr error var localLatency time.Duration start := va.clk.Now() @@ -288,6 +373,7 @@ func (va *ValidationAuthorityImpl) IsCAAValid(ctx context.Context, req *vapb.IsC 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() @@ -306,15 +392,16 @@ func (va *ValidationAuthorityImpl) IsCAAValid(ctx context.Context, req *vapb.IsC prob.Detail = fmt.Sprintf("While processing CAA for %s: %s", req.Domain, prob.Detail) } - if features.Get().EnforceMultiCAA { + 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.IsCAAValid(ctx, checkRequest) + return remoteva.DoCAA(ctx, checkRequest) } - remoteProb := va.performRemoteOperation(ctx, op, req) + 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 @@ -329,17 +416,16 @@ func (va *ValidationAuthorityImpl) IsCAAValid(ctx context.Context, req *vapb.IsC // sure it is UTF-8 clean now. prob = filterProblemDetails(prob) return &vapb.IsCAAValidResponse{ + Rir: va.rir, + Perspective: va.perspective, Problem: &corepb.ProblemDetails{ ProblemType: string(prob.Type), Detail: replaceInvalidUTF8([]byte(prob.Detail)), - }, - Perspective: va.perspective, - Rir: va.rir, - }, nil + }}, nil } else { return &vapb.IsCAAValidResponse{ - Perspective: va.perspective, Rir: va.rir, + Perspective: va.perspective, }, nil } }