RA/VA: Add MPIC compliant DCV and CAA checks (#7870)

Today, we have VA.PerformValidation, a method called by the RA at
challenge time to perform DCV and check CAA. We also have VA.IsCAAValid,
a method invoked by the RA at finalize time when a CAA re-check is
necessary. Both of these methods can be executed on remote VA
perspectives by calling the generic VA.performRemoteValidation.

This change splits VA.PerformValidation into VA.DoDCV and VA.DoCAA,
which are both called on remote VA perspectives by calling the generic
VA.doRemoteOperation. VA.DoDCV, VA.DoCAA, and VA.doRemoteOperation
fulfill the requirements of SC-067 V3: Require Multi-Perspective
Issuance Corroboration by:

- Requiring at least three distinct perspectives, as outlined in the
"Phased Implementation Timeline" in BRs section 3.2.2.9 ("Effective
March 15, 2025").
- Ensuring that the number of non-corroborating (failing) perspectives
remains below the threshold defined by the "Table: Quorum Requirements"
in BRs section 3.2.2.9.
- Ensuring that corroborating (passing) perspectives reside in at least
2 distinct Regional Internet Registries (RIRs) per the "Phased
Implementation Timeline" in BRs section 3.2.2.9 ("Effective March 15,
2026").
- Including an MPIC summary consisting of: passing perspectives, failing
perspectives, passing RIRs, and a quorum met for issuance (e.g., 2/3 or
3/3) in each validation audit log event, per BRs Section 5.4.1,
Requirement 2.8.

When the new SeparateDCVAndCAAChecks feature flag is enabled on the RA,
calls to VA.IsCAAValid (during finalization) and VA.PerformValidation
(during challenge) are replaced with calls to VA.DoCAA and a sequence of
VA.DoDCV followed by VA.DoCAA, respectively.

Fixes #7612
Fixes #7614
Fixes #7615
Fixes #7616
This commit is contained in:
Samantha Frank 2024-12-10 11:26:08 -05:00 committed by GitHub
parent 071b8c5b35
commit dda8acc34a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 1509 additions and 361 deletions

View File

@ -25,6 +25,7 @@ import (
"github.com/letsencrypt/boulder/ratelimits"
bredis "github.com/letsencrypt/boulder/redis"
sapb "github.com/letsencrypt/boulder/sa/proto"
"github.com/letsencrypt/boulder/va"
vapb "github.com/letsencrypt/boulder/va/proto"
)
@ -288,7 +289,6 @@ func main() {
authorizationLifetime,
pendingAuthorizationLifetime,
pubc,
caaClient,
c.RA.OrderLifetime.Duration,
c.RA.FinalizeTimeout.Duration,
ctp,
@ -301,7 +301,10 @@ func main() {
cmd.FailOnError(policyErr, "Couldn't load rate limit policies file")
rai.PA = pa
rai.VA = vac
rai.VA = va.RemoteClients{
VAClient: vac,
CAAClient: caaClient,
}
rai.CA = cac
rai.OCSP = ocspc
rai.SA = sac

View File

@ -108,6 +108,24 @@ type Config struct {
// functionality (valid authz reuse) while letting us simplify our code by
// removing pending authz reuse.
NoPendingAuthzReuse bool
// EnforceMPIC enforces SC-067 V3: Require Multi-Perspective Issuance
// Corroboration by:
// - Requiring at least three distinct perspectives, as outlined in the
// "Phased Implementation Timeline" in BRs section 3.2.2.9 ("Effective
// March 15, 2025").
// - Ensuring that corroborating (passing) perspectives reside in at least
// 2 distinct Regional Internet Registries (RIRs) per the "Phased
// Implementation Timeline" in BRs section 3.2.2.9 ("Effective March 15,
// 2026").
// - Including an MPIC summary consisting of: passing perspectives, failing
// perspectives, passing RIRs, and a quorum met for issuance (e.g., 2/3
// or 3/3) in each validation audit log event, per BRs Section 5.4.1,
// Requirement 2.8.
//
// This feature flag also causes CAA checks to happen after all remote VAs
// have passed DCV.
EnforceMPIC bool
}
var fMu = new(sync.RWMutex)

View File

@ -23,7 +23,6 @@ import (
"github.com/jmhodges/clock"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/crypto/ocsp"
"google.golang.org/grpc"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/emptypb"
@ -52,6 +51,7 @@ import (
"github.com/letsencrypt/boulder/ratelimits"
"github.com/letsencrypt/boulder/revocation"
sapb "github.com/letsencrypt/boulder/sa/proto"
"github.com/letsencrypt/boulder/va"
vapb "github.com/letsencrypt/boulder/va/proto"
"github.com/letsencrypt/boulder/web"
@ -68,14 +68,6 @@ var (
caaRecheckDuration = -7 * time.Hour
)
type caaChecker interface {
IsCAAValid(
ctx context.Context,
in *vapb.IsCAAValidRequest,
opts ...grpc.CallOption,
) (*vapb.IsCAAValidResponse, error)
}
// RegistrationAuthorityImpl defines an RA.
//
// NOTE: All of the fields in RegistrationAuthorityImpl need to be
@ -84,11 +76,10 @@ type RegistrationAuthorityImpl struct {
rapb.UnsafeRegistrationAuthorityServer
CA capb.CertificateAuthorityClient
OCSP capb.OCSPGeneratorClient
VA vapb.VAClient
VA va.RemoteClients
SA sapb.StorageAuthorityClient
PA core.PolicyAuthority
publisher pubpb.PublisherClient
caa caaChecker
clk clock.Clock
log blog.Logger
@ -140,7 +131,6 @@ func NewRegistrationAuthorityImpl(
authorizationLifetime time.Duration,
pendingAuthorizationLifetime time.Duration,
pubc pubpb.PublisherClient,
caaClient caaChecker,
orderLifetime time.Duration,
finalizeTimeout time.Duration,
ctp *ctpolicy.CTPolicy,
@ -265,7 +255,6 @@ func NewRegistrationAuthorityImpl(
txnBuilder: txnBuilder,
maxNames: maxNames,
publisher: pubc,
caa: caaClient,
orderLifetime: orderLifetime,
finalizeTimeout: finalizeTimeout,
ctpolicy: ctp,
@ -849,12 +838,21 @@ func (ra *RegistrationAuthorityImpl) recheckCAA(ctx context.Context, authzs []*c
}
return
}
resp, err := ra.caa.IsCAAValid(ctx, &vapb.IsCAAValidRequest{
var resp *vapb.IsCAAValidResponse
var err error
if !features.Get().EnforceMPIC {
resp, err = ra.VA.IsCAAValid(ctx, &vapb.IsCAAValidRequest{
Domain: name,
ValidationMethod: method,
AccountURIID: authz.RegistrationID,
})
} else {
resp, err = ra.VA.DoCAA(ctx, &vapb.IsCAAValidRequest{
Domain: name,
ValidationMethod: method,
AccountURIID: authz.RegistrationID,
})
}
if err != nil {
ra.log.AuditErrf("Rechecking CAA: %s", err)
err = berrors.InternalServerError(
@ -1832,6 +1830,35 @@ func (ra *RegistrationAuthorityImpl) resetAccountPausingLimit(ctx context.Contex
}
}
// doDCVAndCAA performs DCV and CAA checks. When EnforceMPIC is enabled, the
// checks are executed sequentially: DCV is performed first and CAA is only
// checked if DCV is successful. Validation records from the DCV check are
// returned even if the CAA check fails. When EnforceMPIC is disabled, DCV and
// CAA checks are performed in the same request.
func (ra *RegistrationAuthorityImpl) checkDCVAndCAA(ctx context.Context, dcvReq *vapb.PerformValidationRequest, caaReq *vapb.IsCAAValidRequest) (*corepb.ProblemDetails, []*corepb.ValidationRecord, error) {
if !features.Get().EnforceMPIC {
performValidationRes, err := ra.VA.PerformValidation(ctx, dcvReq)
if err != nil {
return nil, nil, err
}
return performValidationRes.Problem, performValidationRes.Records, nil
} else {
doDCVRes, err := ra.VA.DoDCV(ctx, dcvReq)
if err != nil {
return nil, nil, err
}
if doDCVRes.Problem != nil {
return doDCVRes.Problem, doDCVRes.Records, nil
}
doCAAResp, err := ra.VA.IsCAAValid(ctx, caaReq)
if err != nil {
return nil, nil, err
}
return doCAAResp.Problem, doDCVRes.Records, nil
}
}
// PerformValidation initiates validation for a specific challenge associated
// with the given base authorization. The authorization and challenge are
// updated based on the results.
@ -1916,32 +1943,37 @@ func (ra *RegistrationAuthorityImpl) PerformValidation(
copy(challenges, authz.Challenges)
authz.Challenges = challenges
chall, _ := bgrpc.ChallengeToPB(authz.Challenges[challIndex])
req := vapb.PerformValidationRequest{
checkProb, checkRecords, err := ra.checkDCVAndCAA(
vaCtx,
&vapb.PerformValidationRequest{
DnsName: authz.Identifier.Value,
Challenge: chall,
Authz: &vapb.AuthzMeta{
Id: authz.ID,
RegID: authz.RegistrationID,
},
Authz: &vapb.AuthzMeta{Id: authz.ID, RegID: authz.RegistrationID},
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")

View File

@ -64,6 +64,7 @@ import (
"github.com/letsencrypt/boulder/test"
isa "github.com/letsencrypt/boulder/test/inmem/sa"
"github.com/letsencrypt/boulder/test/vars"
"github.com/letsencrypt/boulder/va"
vapb "github.com/letsencrypt/boulder/va/proto"
)
@ -156,14 +157,50 @@ func numAuthorizations(o *corepb.Order) int {
}
type DummyValidationAuthority struct {
performValidationRequest chan *vapb.PerformValidationRequest
PerformValidationRequestResultError error
PerformValidationRequestResultReturn *vapb.ValidationResult
doDCVRequest chan *vapb.PerformValidationRequest
doDCVError error
doDCVResult *vapb.ValidationResult
doCAARequest chan *vapb.IsCAAValidRequest
doCAAError error
doCAAResponse *vapb.IsCAAValidResponse
}
func (dva *DummyValidationAuthority) PerformValidation(ctx context.Context, req *vapb.PerformValidationRequest, _ ...grpc.CallOption) (*vapb.ValidationResult, error) {
dva.performValidationRequest <- req
return dva.PerformValidationRequestResultReturn, dva.PerformValidationRequestResultError
dcvRes, err := dva.DoDCV(ctx, req)
if err != nil {
return nil, err
}
if dcvRes.Problem != nil {
return dcvRes, nil
}
caaResp, err := dva.DoCAA(ctx, &vapb.IsCAAValidRequest{
Domain: req.DnsName,
ValidationMethod: req.Challenge.Type,
AccountURIID: req.Authz.RegID,
AuthzID: req.Authz.Id,
})
if err != nil {
return nil, err
}
return &vapb.ValidationResult{
Records: dcvRes.Records,
Problem: caaResp.Problem,
}, nil
}
func (dva *DummyValidationAuthority) IsCAAValid(ctx context.Context, req *vapb.IsCAAValidRequest, _ ...grpc.CallOption) (*vapb.IsCAAValidResponse, error) {
return nil, status.Error(codes.Unimplemented, "IsCAAValid not implemented")
}
func (dva *DummyValidationAuthority) DoDCV(ctx context.Context, req *vapb.PerformValidationRequest, _ ...grpc.CallOption) (*vapb.ValidationResult, error) {
dva.doDCVRequest <- req
return dva.doDCVResult, dva.doDCVError
}
func (dva *DummyValidationAuthority) DoCAA(ctx context.Context, req *vapb.IsCAAValidRequest, _ ...grpc.CallOption) (*vapb.IsCAAValidResponse, error) {
dva.doCAARequest <- req
return dva.doCAAResponse, dva.doCAAError
}
var (
@ -311,9 +348,11 @@ func initAuthorities(t *testing.T) (*DummyValidationAuthority, sapb.StorageAutho
saDBCleanUp := test.ResetBoulderTestDatabase(t)
va := &DummyValidationAuthority{
performValidationRequest: make(chan *vapb.PerformValidationRequest, 1),
dummyVA := &DummyValidationAuthority{
doDCVRequest: make(chan *vapb.PerformValidationRequest, 1),
doCAARequest: make(chan *vapb.IsCAAValidRequest, 1),
}
va := va.RemoteClients{VAClient: dummyVA, CAAClient: dummyVA}
pa, err := policy.New(map[core.AcmeChallenge]bool{
core.ChallengeTypeHTTP01: true,
@ -363,7 +402,7 @@ func initAuthorities(t *testing.T) (*DummyValidationAuthority, sapb.StorageAutho
fc, log, stats,
1, testKeyPolicy, limiter, txnBuilder, 100,
300*24*time.Hour, 7*24*time.Hour,
nil, noopCAA{},
nil,
7*24*time.Hour, 5*time.Minute,
ctp, nil, nil)
ra.SA = sa
@ -371,7 +410,7 @@ func initAuthorities(t *testing.T) (*DummyValidationAuthority, sapb.StorageAutho
ra.CA = ca
ra.OCSP = &mocks.MockOCSPGenerator{}
ra.PA = pa
return va, sa, ra, rlSource, fc, cleanUp
return dummyVA, sa, ra, rlSource, fc, cleanUp
}
func TestValidateContacts(t *testing.T) {
@ -689,7 +728,7 @@ func TestPerformValidationAlreadyValid(t *testing.T) {
authzPB, err := bgrpc.AuthzToPB(authz)
test.AssertNotError(t, err, "bgrpc.AuthzToPB failed")
va.PerformValidationRequestResultReturn = &vapb.ValidationResult{
va.doDCVResult = &vapb.ValidationResult{
Records: []*corepb.ValidationRecord{
{
AddressUsed: []byte("192.168.0.1"),
@ -700,6 +739,7 @@ func TestPerformValidationAlreadyValid(t *testing.T) {
},
Problem: nil,
}
va.doCAAResponse = &vapb.IsCAAValidResponse{Problem: nil}
// A subsequent call to perform validation should return nil due
// to being short-circuited because of valid authz reuse.
@ -718,7 +758,7 @@ func TestPerformValidationSuccess(t *testing.T) {
// We know this is OK because of TestNewAuthorization
authzPB := createPendingAuthorization(t, sa, Identifier, fc.Now().Add(12*time.Hour))
va.PerformValidationRequestResultReturn = &vapb.ValidationResult{
va.doDCVResult = &vapb.ValidationResult{
Records: []*corepb.ValidationRecord{
{
AddressUsed: []byte("192.168.0.1"),
@ -730,6 +770,7 @@ func TestPerformValidationSuccess(t *testing.T) {
},
Problem: nil,
}
va.doCAAResponse = &vapb.IsCAAValidResponse{Problem: nil}
now := fc.Now()
challIdx := dnsChallIdx(t, authzPB.Challenges)
@ -741,7 +782,7 @@ func TestPerformValidationSuccess(t *testing.T) {
var vaRequest *vapb.PerformValidationRequest
select {
case r := <-va.performValidationRequest:
case r := <-va.doDCVRequest:
vaRequest = r
case <-time.After(time.Second):
t.Fatal("Timed out waiting for DummyValidationAuthority.PerformValidation to complete")
@ -822,7 +863,7 @@ func TestPerformValidation_FailedValidationsTriggerPauseIdentifiersRatelimit(t *
// Now a failed validation should result in the identifier being paused
// due to the strict ratelimit.
va.PerformValidationRequestResultReturn = &vapb.ValidationResult{
va.doDCVResult = &vapb.ValidationResult{
Records: []*corepb.ValidationRecord{
{
AddressUsed: []byte("192.168.0.1"),
@ -832,6 +873,9 @@ func TestPerformValidation_FailedValidationsTriggerPauseIdentifiersRatelimit(t *
ResolverAddrs: []string{"rebound"},
},
},
Problem: nil,
}
va.doCAAResponse = &vapb.IsCAAValidResponse{
Problem: &corepb.ProblemDetails{
Detail: fmt.Sprintf("CAA invalid for %s", domain),
},
@ -893,8 +937,7 @@ func TestPerformValidation_FailedThenSuccessfulValidationResetsPauseIdentifiersR
})
test.AssertNotError(t, err, "updating rate limit bucket")
// Now a successful validation should reset the rate limit bucket.
va.PerformValidationRequestResultReturn = &vapb.ValidationResult{
va.doDCVResult = &vapb.ValidationResult{
Records: []*corepb.ValidationRecord{
{
AddressUsed: []byte("192.168.0.1"),
@ -906,6 +949,7 @@ func TestPerformValidation_FailedThenSuccessfulValidationResetsPauseIdentifiersR
},
Problem: nil,
}
va.doCAAResponse = &vapb.IsCAAValidResponse{Problem: nil}
_, err = ra.PerformValidation(ctx, &rapb.PerformValidationRequest{
Authz: authzPB,
@ -931,7 +975,7 @@ func TestPerformValidationVAError(t *testing.T) {
authzPB := createPendingAuthorization(t, sa, Identifier, fc.Now().Add(12*time.Hour))
va.PerformValidationRequestResultError = fmt.Errorf("Something went wrong")
va.doDCVError = fmt.Errorf("Something went wrong")
challIdx := dnsChallIdx(t, authzPB.Challenges)
authzPB, err := ra.PerformValidation(ctx, &rapb.PerformValidationRequest{
@ -943,7 +987,7 @@ func TestPerformValidationVAError(t *testing.T) {
var vaRequest *vapb.PerformValidationRequest
select {
case r := <-va.performValidationRequest:
case r := <-va.doDCVRequest:
vaRequest = r
case <-time.After(time.Second):
t.Fatal("Timed out waiting for DummyValidationAuthority.PerformValidation to complete")
@ -1668,7 +1712,7 @@ func TestDeactivateRegistration(t *testing.T) {
test.AssertEquals(t, dbReg.Status, string(core.StatusDeactivated))
}
// noopCAA implements caaChecker, always returning nil
// noopCAA implements vapb.CAAClient, always returning nil
type noopCAA struct{}
func (cr noopCAA) IsCAAValid(
@ -1679,8 +1723,16 @@ func (cr noopCAA) IsCAAValid(
return &vapb.IsCAAValidResponse{}, nil
}
// caaRecorder implements caaChecker, always returning nil, but recording the
// names it was called for.
func (cr noopCAA) DoCAA(
ctx context.Context,
in *vapb.IsCAAValidRequest,
opts ...grpc.CallOption,
) (*vapb.IsCAAValidResponse, error) {
return &vapb.IsCAAValidResponse{}, nil
}
// caaRecorder implements vapb.CAAClient, always returning nil, but recording
// the names it was called for.
type caaRecorder struct {
sync.Mutex
names map[string]bool
@ -1697,13 +1749,24 @@ func (cr *caaRecorder) IsCAAValid(
return &vapb.IsCAAValidResponse{}, nil
}
func (cr *caaRecorder) DoCAA(
ctx context.Context,
in *vapb.IsCAAValidRequest,
opts ...grpc.CallOption,
) (*vapb.IsCAAValidResponse, error) {
cr.Lock()
defer cr.Unlock()
cr.names[in.Domain] = true
return &vapb.IsCAAValidResponse{}, nil
}
// Test that the right set of domain names have their CAA rechecked, based on
// their `Validated` (attemptedAt in the database) timestamp.
func TestRecheckCAADates(t *testing.T) {
_, _, ra, _, fc, cleanUp := initAuthorities(t)
defer cleanUp()
recorder := &caaRecorder{names: make(map[string]bool)}
ra.caa = recorder
ra.VA = va.RemoteClients{CAAClient: recorder}
ra.authorizationLifetime = 15 * time.Hour
recentValidated := fc.Now().Add(-1 * time.Hour)
@ -1889,6 +1952,27 @@ func (cf *caaFailer) IsCAAValid(
return cvrpb, nil
}
func (cf *caaFailer) DoCAA(
ctx context.Context,
in *vapb.IsCAAValidRequest,
opts ...grpc.CallOption,
) (*vapb.IsCAAValidResponse, error) {
cvrpb := &vapb.IsCAAValidResponse{}
switch in.Domain {
case "a.com":
cvrpb.Problem = &corepb.ProblemDetails{
Detail: "CAA invalid for a.com",
}
case "c.com":
cvrpb.Problem = &corepb.ProblemDetails{
Detail: "CAA invalid for c.com",
}
case "d.com":
return nil, fmt.Errorf("Error checking CAA for d.com")
}
return cvrpb, nil
}
func TestRecheckCAAEmpty(t *testing.T) {
_, _, ra, _, _, cleanUp := initAuthorities(t)
defer cleanUp()
@ -1906,6 +1990,7 @@ func makeHTTP01Authorization(domain string) *core.Authorization {
func TestRecheckCAASuccess(t *testing.T) {
_, _, ra, _, _, cleanUp := initAuthorities(t)
defer cleanUp()
ra.VA = va.RemoteClients{CAAClient: &noopCAA{}}
authzs := []*core.Authorization{
makeHTTP01Authorization("a.com"),
makeHTTP01Authorization("b.com"),
@ -1918,7 +2003,7 @@ func TestRecheckCAASuccess(t *testing.T) {
func TestRecheckCAAFail(t *testing.T) {
_, _, ra, _, _, cleanUp := initAuthorities(t)
defer cleanUp()
ra.caa = &caaFailer{}
ra.VA = va.RemoteClients{CAAClient: &caaFailer{}}
authzs := []*core.Authorization{
makeHTTP01Authorization("a.com"),
makeHTTP01Authorization("b.com"),
@ -1969,7 +2054,7 @@ func TestRecheckCAAFail(t *testing.T) {
func TestRecheckCAAInternalServerError(t *testing.T) {
_, _, ra, _, _, cleanUp := initAuthorities(t)
defer cleanUp()
ra.caa = &caaFailer{}
ra.VA = va.RemoteClients{CAAClient: &caaFailer{}}
authzs := []*core.Authorization{
makeHTTP01Authorization("a.com"),
makeHTTP01Authorization("b.com"),
@ -3432,6 +3517,7 @@ func TestIssueCertificateAuditLog(t *testing.T) {
func TestIssueCertificateCAACheckLog(t *testing.T) {
_, sa, ra, _, fc, cleanUp := initAuthorities(t)
defer cleanUp()
ra.VA = va.RemoteClients{CAAClient: &noopCAA{}}
exp := fc.Now().Add(24 * time.Hour)
recent := fc.Now().Add(-1 * time.Hour)

View File

@ -129,7 +129,8 @@
"AsyncFinalize": true,
"UseKvLimitsForNewOrder": true,
"AutomaticallyPauseZombieClients": true,
"NoPendingAuthzReuse": true
"NoPendingAuthzReuse": true,
"EnforceMPIC": true
},
"ctLogs": {
"stagger": "500ms",

View File

@ -23,6 +23,11 @@
"va.boulder"
]
},
"va.CAA": {
"clientNames": [
"va.boulder"
]
},
"grpc.health.v1.Health": {
"clientNames": [
"health-checker.boulder"

View File

@ -23,6 +23,11 @@
"va.boulder"
]
},
"va.CAA": {
"clientNames": [
"va.boulder"
]
},
"grpc.health.v1.Health": {
"clientNames": [
"health-checker.boulder"

View File

@ -23,6 +23,11 @@
"va.boulder"
]
},
"va.CAA": {
"clientNames": [
"va.boulder"
]
},
"grpc.health.v1.Health": {
"clientNames": [
"health-checker.boulder"

View File

@ -2,9 +2,12 @@ package va
import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"regexp"
"slices"
"strings"
"testing"
@ -518,15 +521,43 @@ 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
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 := va.IsCAAValid(ctx, &vapb.IsCAAValidRequest{
resp, err := tc.caaCheckFunc(ctx, va, &vapb.IsCAAValidRequest{
Domain: domain,
ValidationMethod: string(core.ChallengeTypeHTTP01),
AccountURIID: 12345,
@ -540,23 +571,43 @@ func TestIsCAAValidErrMessage(t *testing.T) {
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,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// Calling IsCAAValid without a ValidationMethod should fail.
_, err := va.IsCAAValid(ctx, &vapb.IsCAAValidRequest{
_, err := tc.caaCheckFunc(ctx, va, &vapb.IsCAAValidRequest{
Domain: "present.com",
AccountURIID: 12345,
})
test.AssertError(t, err, "calling IsCAAValid without a ValidationMethod")
// Calling IsCAAValid with an invalid ValidationMethod should fail.
_, err = va.IsCAAValid(ctx, &vapb.IsCAAValidRequest{
_, err = tc.caaCheckFunc(ctx, va, &vapb.IsCAAValidRequest{
Domain: "present.com",
ValidationMethod: "tls-sni-01",
AccountURIID: 12345,
@ -564,11 +615,13 @@ func TestIsCAAValidParams(t *testing.T) {
test.AssertError(t, err, "calling IsCAAValid with a bad ValidationMethod")
// Calling IsCAAValid without an AccountURIID should fail.
_, err = va.IsCAAValid(ctx, &vapb.IsCAAValidRequest{
_, 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
// 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,6 +830,12 @@ func TestMultiCAARechecking(t *testing.T) {
expectedProbSubstring: "During secondary validation: While processing CAA",
expectedProbType: probs.DNSProblem,
expectedDiffLogSubstring: `"RemoteSuccesses":1,"RemoteFailures":2`,
expectedSummary: &mpicSummary{
Passed: []string{"dc-2-APNIC"},
Failed: []string{"dc-0-ARIN", "dc-1-RIPE"},
PassedRIRs: []string{apnic},
QuorumResult: "1/3",
},
localDNSClient: caaMockDNS{},
remoteVAs: []remoteConf{
{ua: brokenUA, rir: arin, dns: caaBrokenDNS{}},
@ -753,6 +856,12 @@ func TestMultiCAARechecking(t *testing.T) {
expectedProbSubstring: "During secondary validation: While processing CAA",
expectedProbType: probs.DNSProblem,
expectedDiffLogSubstring: `"RemoteSuccesses":0,"RemoteFailures":3`,
expectedSummary: &mpicSummary{
Passed: []string{},
Failed: []string{"dc-0-ARIN", "dc-1-RIPE", "dc-2-APNIC"},
PassedRIRs: []string{},
QuorumResult: "0/3",
},
localDNSClient: caaMockDNS{},
remoteVAs: []remoteConf{
{ua: brokenUA, rir: arin, dns: caaBrokenDNS{}},
@ -788,6 +897,12 @@ func TestMultiCAARechecking(t *testing.T) {
name: "functional localVA, 1 broken RVA, CAA issue type present",
domains: "present.com",
expectedDiffLogSubstring: `"RemoteSuccesses":2,"RemoteFailures":1`,
expectedSummary: &mpicSummary{
Passed: []string{"dc-1-RIPE", "dc-2-APNIC"},
Failed: []string{"dc-0-ARIN"},
PassedRIRs: []string{ripe, apnic},
QuorumResult: "2/3",
},
localDNSClient: caaMockDNS{},
remoteVAs: []remoteConf{
{ua: brokenUA, rir: arin, dns: caaBrokenDNS{}},
@ -808,6 +923,12 @@ func TestMultiCAARechecking(t *testing.T) {
expectedProbSubstring: "During secondary validation: While processing CAA",
expectedProbType: probs.DNSProblem,
expectedDiffLogSubstring: `"RemoteSuccesses":1,"RemoteFailures":2`,
expectedSummary: &mpicSummary{
Passed: []string{"dc-2-APNIC"},
Failed: []string{"dc-0-ARIN", "dc-1-RIPE"},
PassedRIRs: []string{apnic},
QuorumResult: "1/3",
},
localDNSClient: caaMockDNS{},
remoteVAs: []remoteConf{
{ua: brokenUA, rir: arin, dns: caaBrokenDNS{}},
@ -828,6 +949,12 @@ func TestMultiCAARechecking(t *testing.T) {
expectedProbSubstring: "During secondary validation: While processing CAA",
expectedProbType: probs.DNSProblem,
expectedDiffLogSubstring: `"RemoteSuccesses":0,"RemoteFailures":3`,
expectedSummary: &mpicSummary{
Passed: []string{},
Failed: []string{"dc-0-ARIN", "dc-1-RIPE", "dc-2-APNIC"},
PassedRIRs: []string{},
QuorumResult: "0/3",
},
localDNSClient: caaMockDNS{},
remoteVAs: []remoteConf{
{ua: brokenUA, rir: arin, dns: caaBrokenDNS{}},
@ -860,6 +987,12 @@ func TestMultiCAARechecking(t *testing.T) {
name: "1 hijacked RVA, CAA issue type present",
domains: "present.com",
expectedDiffLogSubstring: `"RemoteSuccesses":2,"RemoteFailures":1`,
expectedSummary: &mpicSummary{
Passed: []string{"dc-1-RIPE", "dc-2-APNIC"},
Failed: []string{"dc-0-ARIN"},
PassedRIRs: []string{ripe, apnic},
QuorumResult: "2/3",
},
localDNSClient: caaMockDNS{},
remoteVAs: []remoteConf{
{ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}},
@ -873,6 +1006,12 @@ func TestMultiCAARechecking(t *testing.T) {
expectedProbSubstring: "During secondary validation: While processing CAA",
expectedProbType: probs.CAAProblem,
expectedDiffLogSubstring: `"RemoteSuccesses":1,"RemoteFailures":2`,
expectedSummary: &mpicSummary{
Passed: []string{"dc-2-APNIC"},
Failed: []string{"dc-0-ARIN", "dc-1-RIPE"},
PassedRIRs: []string{apnic},
QuorumResult: "1/3",
},
localDNSClient: caaMockDNS{},
remoteVAs: []remoteConf{
{ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}},
@ -886,6 +1025,12 @@ func TestMultiCAARechecking(t *testing.T) {
expectedProbSubstring: "During secondary validation: While processing CAA",
expectedProbType: probs.CAAProblem,
expectedDiffLogSubstring: `"RemoteSuccesses":0,"RemoteFailures":3`,
expectedSummary: &mpicSummary{
Passed: []string{},
Failed: []string{"dc-0-ARIN", "dc-1-RIPE", "dc-2-APNIC"},
PassedRIRs: []string{},
QuorumResult: "0/3",
},
localDNSClient: caaMockDNS{},
remoteVAs: []remoteConf{
{ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}},
@ -897,6 +1042,12 @@ func TestMultiCAARechecking(t *testing.T) {
name: "1 hijacked RVA, CAA issuewild type present",
domains: "satisfiable-wildcard.com",
expectedDiffLogSubstring: `"RemoteSuccesses":2,"RemoteFailures":1`,
expectedSummary: &mpicSummary{
Passed: []string{"dc-1-RIPE", "dc-2-APNIC"},
Failed: []string{"dc-0-ARIN"},
PassedRIRs: []string{ripe, apnic},
QuorumResult: "2/3",
},
localDNSClient: caaMockDNS{},
remoteVAs: []remoteConf{
{ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}},
@ -910,6 +1061,12 @@ func TestMultiCAARechecking(t *testing.T) {
expectedProbSubstring: "During secondary validation: While processing CAA",
expectedProbType: probs.CAAProblem,
expectedDiffLogSubstring: `"RemoteSuccesses":1,"RemoteFailures":2`,
expectedSummary: &mpicSummary{
Passed: []string{"dc-2-APNIC"},
Failed: []string{"dc-0-ARIN", "dc-1-RIPE"},
PassedRIRs: []string{apnic},
QuorumResult: "1/3",
},
localDNSClient: caaMockDNS{},
remoteVAs: []remoteConf{
{ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}},
@ -923,6 +1080,12 @@ func TestMultiCAARechecking(t *testing.T) {
expectedProbSubstring: "During secondary validation: While processing CAA",
expectedProbType: probs.CAAProblem,
expectedDiffLogSubstring: `"RemoteSuccesses":0,"RemoteFailures":3`,
expectedSummary: &mpicSummary{
Passed: []string{},
Failed: []string{"dc-0-ARIN", "dc-1-RIPE", "dc-2-APNIC"},
PassedRIRs: []string{},
QuorumResult: "0/3",
},
localDNSClient: caaMockDNS{},
remoteVAs: []remoteConf{
{ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}},
@ -934,6 +1097,12 @@ 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`,
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{}},
@ -947,6 +1116,12 @@ func TestMultiCAARechecking(t *testing.T) {
expectedProbSubstring: "During secondary validation: While processing CAA",
expectedProbType: probs.CAAProblem,
expectedDiffLogSubstring: `"RemoteSuccesses":1,"RemoteFailures":2`,
expectedSummary: &mpicSummary{
Passed: []string{"dc-2-APNIC"},
Failed: []string{"dc-0-ARIN", "dc-1-RIPE"},
PassedRIRs: []string{apnic},
QuorumResult: "1/3",
},
localDNSClient: caaMockDNS{},
remoteVAs: []remoteConf{
{ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}},
@ -960,6 +1135,12 @@ func TestMultiCAARechecking(t *testing.T) {
expectedProbSubstring: "During secondary validation: While processing CAA",
expectedProbType: probs.CAAProblem,
expectedDiffLogSubstring: `"RemoteSuccesses":0,"RemoteFailures":3`,
expectedSummary: &mpicSummary{
Passed: []string{},
Failed: []string{"dc-0-ARIN", "dc-1-RIPE", "dc-2-APNIC"},
PassedRIRs: []string{},
QuorumResult: "0/3",
},
localDNSClient: caaMockDNS{},
remoteVAs: []remoteConf{
{ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}},
@ -970,7 +1151,8 @@ func TestMultiCAARechecking(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
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()
@ -979,7 +1161,7 @@ func TestMultiCAARechecking(t *testing.T) {
})
defer features.Reset()
isValidRes, err := va.IsCAAValid(context.TODO(), &vapb.IsCAAValidRequest{
isValidRes, err := testFunc.impl(context.TODO(), va, &vapb.IsCAAValidRequest{
Domain: tc.domains,
ValidationMethod: string(core.ChallengeTypeDNS01),
AccountURIID: 1,
@ -998,6 +1180,7 @@ func TestMultiCAARechecking(t *testing.T) {
test.AssertEquals(t, string(tc.expectedProbType), isValidRes.Problem.ProblemType)
}
if testFunc.name == "IsCAAValid" {
var invalidRVACount int
for _, x := range tc.remoteVAs {
if x.ua == brokenUA || x.ua == hijackedUA {
@ -1015,6 +1198,15 @@ func TestMultiCAARechecking(t *testing.T) {
} else {
test.AssertEquals(t, len(gotDifferential), 0)
}
}
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)
}
gotAnyRemoteFailures := mockLog.GetAllMatching("CAA check failed due to remote failures:")
if len(gotAnyRemoteFailures) >= 1 {
@ -1027,9 +1219,11 @@ func TestMultiCAARechecking(t *testing.T) {
if tc.expectedLabels != nil {
test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, tc.expectedLabels, 1)
}
})
}
}
}
func TestCAAFailure(t *testing.T) {
hs := httpSrv(t, expectedToken)

View File

@ -29,7 +29,7 @@ func TestDNSValidationEmpty(t *testing.T) {
test.AssertEquals(t, res.Problem.Detail, "No TXT record found at _acme-challenge.empty-txts.com")
test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, prometheus.Labels{
"operation": opChallAndCAA,
"operation": opDCVAndCAA,
"perspective": va.perspective,
"challenge_type": string(core.ChallengeTypeDNS01),
"problem_type": string(probs.UnauthorizedProblem),

View File

@ -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

View File

@ -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

View File

@ -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",

View File

@ -36,7 +36,8 @@ const (
PrimaryPerspective = "Primary"
allPerspectives = "all"
opChallAndCAA = "challenge+caa"
opDCVAndCAA = "dcv+caa"
opDCV = "dcv"
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 [dcv|caa|dcv+caa]
// - perspective: ValidationAuthorityImpl.perspective
// - challenge_type: core.Challenge.Type
// - problem_type: probs.ProblemType
@ -438,7 +439,7 @@ func (va *ValidationAuthorityImpl) validateChallenge(
// observeLatency records entries in the validationLatency histogram of the
// latency to perform validations from the primary and remote VA perspectives.
// The labels are:
// - operation: VA.ValidateChallenge or VA.CheckCAA as [challenge|caa]
// - operation: VA.DoDCV or VA.DoCAA as [dcv|caa]
// - perspective: [ValidationAuthorityImpl.perspective|all]
// - challenge_type: core.Challenge.Type
// - problem_type: probs.ProblemType
@ -714,10 +715,10 @@ func (va *ValidationAuthorityImpl) PerformValidation(ctx context.Context, req *v
outcome = pass
}
// Observe local validation latency (primary|remote).
va.observeLatency(opChallAndCAA, va.perspective, string(chall.Type), probType, outcome, localLatency)
va.observeLatency(opDCVAndCAA, va.perspective, string(chall.Type), probType, outcome, localLatency)
if va.isPrimaryVA() {
// Observe total validation latency (primary+remote).
va.observeLatency(opChallAndCAA, allPerspectives, string(chall.Type), probType, outcome, va.clk.Since(start))
va.observeLatency(opDCVAndCAA, allPerspectives, string(chall.Type), probType, outcome, va.clk.Since(start))
}
// Log the total validation latency.

View File

@ -270,10 +270,18 @@ func (v cancelledVA) PerformValidation(_ context.Context, _ *vapb.PerformValidat
return nil, context.Canceled
}
func (v cancelledVA) DoDCV(_ context.Context, _ *vapb.PerformValidationRequest, _ ...grpc.CallOption) (*vapb.ValidationResult, error) {
return nil, context.Canceled
}
func (v cancelledVA) IsCAAValid(_ context.Context, _ *vapb.IsCAAValidRequest, _ ...grpc.CallOption) (*vapb.IsCAAValidResponse, error) {
return nil, context.Canceled
}
func (v cancelledVA) DoCAA(_ context.Context, _ *vapb.IsCAAValidRequest, _ ...grpc.CallOption) (*vapb.IsCAAValidResponse, error) {
return nil, context.Canceled
}
// brokenRemoteVA is a mock for the VAClient and CAAClient interfaces that always return
// errors.
type brokenRemoteVA struct{}
@ -287,10 +295,19 @@ func (b brokenRemoteVA) PerformValidation(_ context.Context, _ *vapb.PerformVali
return nil, errBrokenRemoteVA
}
// DoDCV returns errBrokenRemoteVA unconditionally
func (b brokenRemoteVA) DoDCV(_ context.Context, _ *vapb.PerformValidationRequest, _ ...grpc.CallOption) (*vapb.ValidationResult, error) {
return nil, errBrokenRemoteVA
}
func (b brokenRemoteVA) IsCAAValid(_ context.Context, _ *vapb.IsCAAValidRequest, _ ...grpc.CallOption) (*vapb.IsCAAValidResponse, error) {
return nil, errBrokenRemoteVA
}
func (b brokenRemoteVA) DoCAA(_ context.Context, _ *vapb.IsCAAValidRequest, _ ...grpc.CallOption) (*vapb.IsCAAValidResponse, error) {
return nil, errBrokenRemoteVA
}
// inMemVA is a wrapper which fulfills the VAClient and CAAClient
// interfaces, but then forwards requests directly to its inner
// ValidationAuthorityImpl rather than over the network. This lets a local
@ -303,10 +320,18 @@ func (inmem *inMemVA) PerformValidation(ctx context.Context, req *vapb.PerformVa
return inmem.rva.PerformValidation(ctx, req)
}
func (inmem *inMemVA) DoDCV(ctx context.Context, req *vapb.PerformValidationRequest, _ ...grpc.CallOption) (*vapb.ValidationResult, error) {
return inmem.rva.DoDCV(ctx, req)
}
func (inmem *inMemVA) IsCAAValid(ctx context.Context, req *vapb.IsCAAValidRequest, _ ...grpc.CallOption) (*vapb.IsCAAValidResponse, error) {
return inmem.rva.IsCAAValid(ctx, req)
}
func (inmem *inMemVA) DoCAA(ctx context.Context, req *vapb.IsCAAValidRequest, _ ...grpc.CallOption) (*vapb.IsCAAValidResponse, error) {
return inmem.rva.DoCAA(ctx, req)
}
func TestNewValidationAuthorityImplWithDuplicateRemotes(t *testing.T) {
var remoteVAs []RemoteVA
for i := 0; i < 3; i++ {
@ -333,7 +358,19 @@ func TestNewValidationAuthorityImplWithDuplicateRemotes(t *testing.T) {
test.AssertContains(t, err.Error(), "duplicate remote VA perspective \"dadaist\"")
}
type validationFuncRunner func(context.Context, *ValidationAuthorityImpl, *vapb.PerformValidationRequest) (*vapb.ValidationResult, error)
var runPerformValidation = func(ctx context.Context, va *ValidationAuthorityImpl, req *vapb.PerformValidationRequest) (*vapb.ValidationResult, error) {
return va.PerformValidation(ctx, req)
}
var runDoDCV = func(ctx context.Context, va *ValidationAuthorityImpl, req *vapb.PerformValidationRequest) (*vapb.ValidationResult, error) {
return va.DoDCV(ctx, req)
}
func TestPerformValidationWithMismatchedRemoteVAPerspectives(t *testing.T) {
t.Parallel()
mismatched1 := RemoteVA{
RemoteClients: setupRemote(nil, "", nil, "dadaist", arin),
Perspective: "baroque",
@ -346,15 +383,36 @@ func TestPerformValidationWithMismatchedRemoteVAPerspectives(t *testing.T) {
}
remoteVAs := setupRemotes([]remoteConf{{rir: ripe}}, nil)
remoteVAs = append(remoteVAs, mismatched1, mismatched2)
va, mockLog := setup(nil, "", remoteVAs, nil)
testCases := []struct {
validationFuncName string
validationFunc validationFuncRunner
}{
{
validationFuncName: "PerformValidation",
validationFunc: runPerformValidation,
},
{
validationFuncName: "DoDCV",
validationFunc: runDoDCV,
},
}
for _, tc := range testCases {
t.Run(tc.validationFuncName, func(t *testing.T) {
t.Parallel()
va, mockLog := setup(nil, "", remoteVAs, nil)
req := createValidationRequest("good-dns01.com", core.ChallengeTypeDNS01)
res, _ := va.PerformValidation(context.Background(), req)
res, _ := tc.validationFunc(context.Background(), va, req)
test.AssertNotNil(t, res.GetProblem(), "validation succeeded with mismatched remote VA perspectives")
test.AssertEquals(t, len(mockLog.GetAllMatching("Expected perspective")), 2)
})
}
}
func TestPerformValidationWithMismatchedRemoteVARIRs(t *testing.T) {
t.Parallel()
mismatched1 := RemoteVA{
RemoteClients: setupRemote(nil, "", nil, "dadaist", arin),
Perspective: "dadaist",
@ -367,12 +425,32 @@ func TestPerformValidationWithMismatchedRemoteVARIRs(t *testing.T) {
}
remoteVAs := setupRemotes([]remoteConf{{rir: ripe}}, nil)
remoteVAs = append(remoteVAs, mismatched1, mismatched2)
va, mockLog := setup(nil, "", remoteVAs, nil)
testCases := []struct {
validationFuncName string
validationFunc validationFuncRunner
}{
{
validationFuncName: "PerformValidation",
validationFunc: runPerformValidation,
},
{
validationFuncName: "DoDCV",
validationFunc: runDoDCV,
},
}
for _, tc := range testCases {
t.Run(tc.validationFuncName, func(t *testing.T) {
t.Parallel()
va, mockLog := setup(nil, "", remoteVAs, nil)
req := createValidationRequest("good-dns01.com", core.ChallengeTypeDNS01)
res, _ := va.PerformValidation(context.Background(), req)
res, _ := tc.validationFunc(context.Background(), va, req)
test.AssertNotNil(t, res.GetProblem(), "validation succeeded with mismatched remote VA perspectives")
test.AssertEquals(t, len(mockLog.GetAllMatching("Expected perspective")), 2)
})
}
}
func TestValidateMalformedChallenge(t *testing.T) {
@ -385,48 +463,130 @@ func TestValidateMalformedChallenge(t *testing.T) {
}
func TestPerformValidationInvalid(t *testing.T) {
t.Parallel()
va, _ := setup(nil, "", nil, nil)
testCases := []struct {
validationFuncName string
validationFunc validationFuncRunner
}{
{
validationFuncName: "PerformValidation",
validationFunc: runPerformValidation,
},
{
validationFuncName: "DoDCV",
validationFunc: runDoDCV,
},
}
for _, tc := range testCases {
t.Run(tc.validationFuncName, func(t *testing.T) {
t.Parallel()
req := createValidationRequest("foo.com", core.ChallengeTypeDNS01)
res, _ := va.PerformValidation(context.Background(), req)
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,
"operation": opDCVAndCAA,
"perspective": va.perspective,
"challenge_type": string(core.ChallengeTypeDNS01),
"problem_type": string(probs.UnauthorizedProblem),
"result": fail,
}, 1)
} else {
test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, prometheus.Labels{
"operation": opDCV,
"perspective": va.perspective,
"challenge_type": string(core.ChallengeTypeDNS01),
"problem_type": string(probs.UnauthorizedProblem),
"result": fail,
}, 1)
}
})
}
}
func TestInternalErrorLogged(t *testing.T) {
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()
va, mockLog := setup(nil, "", nil, nil)
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond)
defer cancel()
req := createValidationRequest("nonexistent.com", core.ChallengeTypeHTTP01)
_, err := va.PerformValidation(ctx, req)
_, 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) {
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()
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)
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,
"operation": opDCVAndCAA,
"perspective": va.perspective,
"challenge_type": string(core.ChallengeTypeDNS01),
"problem_type": "",
"result": pass,
}, 1)
} else {
test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, prometheus.Labels{
"operation": opDCV,
"perspective": va.perspective,
"challenge_type": string(core.ChallengeTypeDNS01),
"problem_type": "",
"result": pass,
}, 1)
}
resultLog := mockLog.GetAllMatching(`Validation result`)
if len(resultLog) != 1 {
t.Fatalf("Wrong number of matching lines for 'Validation result'")
@ -434,26 +594,57 @@ func TestPerformValidationValid(t *testing.T) {
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) {
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()
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)
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,
"operation": opDCVAndCAA,
"perspective": va.perspective,
"challenge_type": string(core.ChallengeTypeDNS01),
"problem_type": "",
"result": pass,
}, 1)
} else {
test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, prometheus.Labels{
"operation": opDCV,
"perspective": va.perspective,
"challenge_type": string(core.ChallengeTypeDNS01),
"problem_type": "",
"result": pass,
}, 1)
}
resultLog := mockLog.GetAllMatching(`Validation result`)
if len(resultLog) != 1 {
t.Fatalf("Wrong number of matching lines for 'Validation result'")
@ -468,6 +659,8 @@ func TestPerformValidationWildcard(t *testing.T) {
if !strings.Contains(resultLog[0], `"hostname":"good-dns01.com"`) {
t.Errorf("PerformValidation didn't log correct validation record hostname.")
}
})
}
}
func TestDCVAndCAASequencing(t *testing.T) {
@ -571,6 +764,22 @@ func TestMultiVA(t *testing.T) {
CAAClient: cancelledVA{},
}
type testFunc struct {
name string
impl validationFuncRunner
}
testFuncs := []testFunc{
{
name: "PerformValidation",
impl: runPerformValidation,
},
{
name: "DoDCV",
impl: runDoDCV,
},
}
testCases := []struct {
Name string
Remotes []remoteConf
@ -727,7 +936,8 @@ func TestMultiVA(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
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.
@ -738,7 +948,7 @@ func TestMultiVA(t *testing.T) {
localVA, mockLog := setupWithRemotes(ms.Server, tc.PrimaryUA, tc.Remotes, nil)
// Perform all validations
res, _ := localVA.PerformValidation(ctx, req)
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 == "" {
@ -757,10 +967,27 @@ func TestMultiVA(t *testing.T) {
})
}
}
}
func TestMultiVAEarlyReturn(t *testing.T) {
t.Parallel()
type testFunc struct {
name string
impl validationFuncRunner
}
testFuncs := []testFunc{
{
name: "PerformValidation",
impl: runPerformValidation,
},
{
name: "DoDCV",
impl: runDoDCV,
},
}
testCases := []struct {
remoteConfs []remoteConf
}{
@ -793,7 +1020,8 @@ func TestMultiVAEarlyReturn(t *testing.T) {
}
for i, tc := range testCases {
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
for _, testFunc := range testFuncs {
t.Run(fmt.Sprintf("case %d"+"_"+testFunc.name, i), func(t *testing.T) {
t.Parallel()
// Configure one test server per test case so that all tests can run in parallel.
@ -805,7 +1033,7 @@ func TestMultiVAEarlyReturn(t *testing.T) {
// Perform all validations
start := time.Now()
req := createValidationRequest("localhost", core.ChallengeTypeHTTP01)
res, _ := localVA.PerformValidation(ctx, req)
res, _ := testFunc.impl(ctx, localVA, req)
// It should always fail
if res.Problem == nil {
@ -826,47 +1054,89 @@ func TestMultiVAEarlyReturn(t *testing.T) {
})
}
}
}
func TestMultiVAPolicy(t *testing.T) {
t.Parallel()
ms := httpMultiSrv(t, expectedToken, map[string]bool{pass: true, fail: false})
defer ms.Close()
remoteConfs := []remoteConf{
{ua: fail, rir: arin},
{ua: fail, rir: ripe},
{ua: fail, rir: apnic},
}
testCases := []struct {
validationFuncName string
validationFunc validationFuncRunner
}{
{
validationFuncName: "PerformValidation",
validationFunc: runPerformValidation,
},
{
validationFuncName: "DoDCV",
validationFunc: runDoDCV,
},
}
for _, tc := range testCases {
t.Run(tc.validationFuncName, func(t *testing.T) {
t.Parallel()
ms := httpMultiSrv(t, expectedToken, map[string]bool{pass: true, fail: false})
defer ms.Close()
// Create a local test VA with the remote VAs
localVA, _ := setupWithRemotes(ms.Server, pass, remoteConfs, nil)
// Perform validation for a domain not in the disabledDomains list
req := createValidationRequest("letsencrypt.org", core.ChallengeTypeHTTP01)
res, _ := localVA.PerformValidation(ctx, req)
res, _ := tc.validationFunc(ctx, localVA, req)
// It should fail
if res.Problem == nil {
t.Error("expected prob from PerformValidation, got nil")
}
})
}
}
func TestMultiVALogging(t *testing.T) {
t.Parallel()
ms := httpMultiSrv(t, expectedToken, map[string]bool{pass: true, fail: false})
defer ms.Close()
remoteConfs := []remoteConf{
{ua: pass, rir: arin},
{ua: pass, rir: ripe},
{ua: pass, rir: apnic},
}
testCases := []struct {
validationFuncName string
validationFunc validationFuncRunner
}{
{
validationFuncName: "PerformValidation",
validationFunc: runPerformValidation,
},
{
validationFuncName: "DoDCV",
validationFunc: runDoDCV,
},
}
for _, tc := range testCases {
t.Run(tc.validationFuncName, func(t *testing.T) {
t.Parallel()
ms := httpMultiSrv(t, expectedToken, map[string]bool{pass: true, fail: false})
defer ms.Close()
va, _ := setupWithRemotes(ms.Server, pass, remoteConfs, nil)
req := createValidationRequest("letsencrypt.org", core.ChallengeTypeHTTP01)
res, err := va.PerformValidation(ctx, req)
res, err := tc.validationFunc(ctx, va, req)
test.Assert(t, res.Problem == nil, fmt.Sprintf("validation failed with: %#v", res.Problem))
test.AssertNotError(t, err, "performing validation")
})
}
}
func TestDetailedError(t *testing.T) {

428
va/vampic.go Normal file
View File

@ -0,0 +1,428 @@
package va
import (
"context"
"errors"
"fmt"
"maps"
"math/rand/v2"
"slices"
"time"
"github.com/letsencrypt/boulder/core"
corepb "github.com/letsencrypt/boulder/core/proto"
berrors "github.com/letsencrypt/boulder/errors"
bgrpc "github.com/letsencrypt/boulder/grpc"
"github.com/letsencrypt/boulder/identifier"
"github.com/letsencrypt/boulder/probs"
vapb "github.com/letsencrypt/boulder/va/proto"
"google.golang.org/protobuf/proto"
)
const (
// requiredRIRs is the minimum number of distinct Regional Internet
// Registries required for MPIC-compliant validation. Per BRs Section
// 3.2.2.9, starting March 15, 2026, the required number is 2.
requiredRIRs = 2
)
// mpicSummary is returned by doRemoteOperation and contains a summary of the
// validation results for logging purposes. To ensure that the JSON output does
// not contain nil slices, and to ensure deterministic output use the
// summarizeMPIC function to prepare an mpicSummary.
type mpicSummary struct {
// Passed are the perspectives that passed validation.
Passed []string `json:"passedPerspectives"`
// Failed are the perspectives that failed validation.
Failed []string `json:"failedPerspectives"`
// PassedRIRs are the Regional Internet Registries that the passing
// perspectives reside in.
PassedRIRs []string `json:"passedRIRs"`
// QuorumResult is the Multi-Perspective Issuance Corroboration quorum
// result, per BRs Section 5.4.1, Requirement 2.7 (i.e., "3/4" which should
// be interpreted as "Three (3) out of four (4) attempted Network
// Perspectives corroborated the determinations made by the Primary Network
// Perspective".
QuorumResult string `json:"quorumResult"`
}
// summarizeMPIC prepares an *mpicSummary for logging, ensuring there are no nil
// slices and output is deterministic.
func summarizeMPIC(passed, failed []string, passedRIRSet map[string]struct{}) *mpicSummary {
if passed == nil {
passed = []string{}
}
slices.Sort(passed)
if failed == nil {
failed = []string{}
}
slices.Sort(failed)
passedRIRs := []string{}
if passedRIRSet != nil {
for rir := range maps.Keys(passedRIRSet) {
passedRIRs = append(passedRIRs, rir)
}
}
slices.Sort(passedRIRs)
return &mpicSummary{
Passed: passed,
Failed: failed,
PassedRIRs: passedRIRs,
QuorumResult: fmt.Sprintf("%d/%d", len(passed), len(passed)+len(failed)),
}
}
// doRemoteOperation concurrently calls the provided operation with `req` and a
// RemoteVA once for each configured RemoteVA. It cancels remaining operations
// and returns early if either the required number of successful results is
// obtained or the number of failures exceeds va.maxRemoteFailures.
//
// Internal logic errors are logged. If the number of operation failures exceeds
// va.maxRemoteFailures, the first encountered problem is returned as a
// *probs.ProblemDetails.
func (va *ValidationAuthorityImpl) doRemoteOperation(ctx context.Context, op remoteOperation, req proto.Message) (*mpicSummary, *probs.ProblemDetails) {
remoteVACount := len(va.remoteVAs)
// - Mar 15, 2026: MUST implement using at least 3 perspectives
// - Jun 15, 2026: MUST implement using at least 4 perspectives
// - Dec 15, 2026: MUST implement using at least 5 perspectives
// See "Phased Implementation Timeline" in
// https://github.com/cabforum/servercert/blob/main/docs/BR.md#3229-multi-perspective-issuance-corroboration
if remoteVACount < 3 {
return nil, probs.ServerInternal("Insufficient remote perspectives: need at least 3")
}
type response struct {
addr string
perspective string
rir string
result remoteResult
err error
}
subCtx, cancel := context.WithCancel(ctx)
defer cancel()
responses := make(chan *response, remoteVACount)
for _, i := range rand.Perm(remoteVACount) {
go func(rva RemoteVA) {
res, err := op(subCtx, rva, req)
if err != nil {
responses <- &response{rva.Address, rva.Perspective, rva.RIR, res, err}
return
}
if res.GetPerspective() != rva.Perspective || res.GetRir() != rva.RIR {
err = fmt.Errorf(
"Expected perspective %q (%q) but got reply from %q (%q) - misconfiguration likely", rva.Perspective, rva.RIR, res.GetPerspective(), res.GetRir(),
)
responses <- &response{rva.Address, rva.Perspective, rva.RIR, res, err}
return
}
responses <- &response{rva.Address, rva.Perspective, rva.RIR, res, err}
}(va.remoteVAs[i])
}
required := remoteVACount - va.maxRemoteFailures
var passed []string
var failed []string
var passedRIRs = map[string]struct{}{}
var firstProb *probs.ProblemDetails
for resp := range responses {
var currProb *probs.ProblemDetails
if resp.err != nil {
// Failed to communicate with the remote VA.
failed = append(failed, resp.perspective)
if core.IsCanceled(resp.err) {
currProb = probs.ServerInternal("Secondary validation RPC canceled")
} else {
va.log.Errf("Operation on remote VA (%s) failed: %s", resp.addr, resp.err)
currProb = probs.ServerInternal("Secondary validation RPC failed")
}
} else if resp.result.GetProblem() != nil {
// The remote VA returned a problem.
failed = append(failed, resp.perspective)
var err error
currProb, err = bgrpc.PBToProblemDetails(resp.result.GetProblem())
if err != nil {
va.log.Errf("Operation on Remote VA (%s) returned malformed problem: %s", resp.addr, err)
currProb = probs.ServerInternal("Secondary validation RPC returned malformed result")
}
} else {
// The remote VA returned a successful result.
passed = append(passed, resp.perspective)
passedRIRs[resp.rir] = struct{}{}
}
if firstProb == nil && currProb != nil {
// A problem was encountered for the first time.
firstProb = currProb
}
// To respond faster, if we get enough successes or too many failures, we cancel remaining RPCs.
// Finish the loop to collect remaining responses into `failed` so we can rely on having a response
// for every request we made.
if len(passed) >= required && len(passedRIRs) >= requiredRIRs {
cancel()
}
if len(failed) > va.maxRemoteFailures {
cancel()
}
// Once all the VAs have returned a result, break the loop.
if len(passed)+len(failed) >= remoteVACount {
break
}
}
if len(passed) >= required && len(passedRIRs) >= requiredRIRs {
return summarizeMPIC(passed, failed, passedRIRs), nil
}
if firstProb == nil {
// This should never happen. If we didn't meet the thresholds above we
// should have seen at least one error.
return summarizeMPIC(passed, failed, passedRIRs), probs.ServerInternal(
"During secondary validation: validation failed but the problem is unavailable")
}
firstProb.Detail = fmt.Sprintf("During secondary validation: %s", firstProb.Detail)
return summarizeMPIC(passed, failed, passedRIRs), firstProb
}
// validationLogEvent is a struct that contains the information needed to log
// the results of DoCAA and DoDCV.
type validationLogEvent struct {
AuthzID string
Requester int64
Identifier string
Challenge core.Challenge
Error string `json:",omitempty"`
InternalError string `json:",omitempty"`
Latency float64
Summary *mpicSummary `json:",omitempty"`
}
// DoDCV conducts a local Domain Control Validation (DCV) for the specified
// challenge. When invoked on the primary Validation Authority (VA) and the
// local validation succeeds, it also performs DCV validations using the
// configured remote VAs. Failed validations are indicated by a non-nil Problems
// in the returned ValidationResult. DoDCV returns error only for internal logic
// errors (and the client may receive errors from gRPC in the event of a
// communication problem). ValidationResult always includes a list of
// ValidationRecords, even when it also contains Problems. This method
// implements the DCV portion of Multi-Perspective Issuance Corroboration as
// defined in BRs Sections 3.2.2.9 and 5.4.1.
func (va *ValidationAuthorityImpl) DoDCV(ctx context.Context, req *vapb.PerformValidationRequest) (*vapb.ValidationResult, error) {
if core.IsAnyNilOrZero(req, req.DnsName, req.Challenge, req.Authz, req.ExpectedKeyAuthorization) {
return nil, berrors.InternalServerError("Incomplete validation request")
}
chall, err := bgrpc.PBToChallenge(req.Challenge)
if err != nil {
return nil, errors.New("challenge failed to deserialize")
}
err = chall.CheckPending()
if err != nil {
return nil, berrors.MalformedError("challenge failed consistency check: %s", err)
}
// Initialize variables and a deferred function to handle validation latency
// metrics, log validation errors, and log an MPIC summary. Avoid using :=
// to redeclare `prob`, `localLatency`, or `summary` below this point.
var prob *probs.ProblemDetails
var summary *mpicSummary
var localLatency time.Duration
start := va.clk.Now()
logEvent := validationLogEvent{
AuthzID: req.Authz.Id,
Requester: req.Authz.RegID,
Identifier: req.DnsName,
Challenge: chall,
}
defer func() {
probType := ""
outcome := fail
if prob != nil {
probType = string(prob.Type)
logEvent.Error = prob.Error()
logEvent.Challenge.Error = prob
logEvent.Challenge.Status = core.StatusInvalid
} else {
logEvent.Challenge.Status = core.StatusValid
outcome = pass
}
// Observe local validation latency (primary|remote).
va.observeLatency(opDCV, va.perspective, string(chall.Type), probType, outcome, localLatency)
if va.isPrimaryVA() {
// Observe total validation latency (primary+remote).
va.observeLatency(opDCV, allPerspectives, string(chall.Type), probType, outcome, va.clk.Since(start))
logEvent.Summary = summary
}
// Log the total validation latency.
logEvent.Latency = va.clk.Since(start).Round(time.Millisecond).Seconds()
va.log.AuditObject("Validation result", logEvent)
}()
// Do local validation. Note that we process the result in a couple ways
// *before* checking whether it returned an error. These few checks are
// carefully written to ensure that they work whether the local validation
// was successful or not, and cannot themselves fail.
records, err := va.validateChallenge(
ctx,
identifier.NewDNS(req.DnsName),
chall.Type,
chall.Token,
req.ExpectedKeyAuthorization,
)
// Stop the clock for local validation latency.
localLatency = va.clk.Since(start)
// Check for malformed ValidationRecords
logEvent.Challenge.ValidationRecord = records
if err == nil && !logEvent.Challenge.RecordsSane() {
err = errors.New("records from local validation failed sanity check")
}
if err != nil {
logEvent.InternalError = err.Error()
prob = detailedError(err)
return bgrpc.ValidationResultToPB(records, filterProblemDetails(prob), va.perspective, va.rir)
}
if va.isPrimaryVA() {
// Do remote validation. We do this after local validation is complete
// to avoid wasting work when validation will fail anyway. This only
// returns a singular problem, because the remote VAs have already
// logged their own validationLogEvent, and it's not helpful to present
// multiple large errors to the end user.
op := func(ctx context.Context, remoteva RemoteVA, req proto.Message) (remoteResult, error) {
validationRequest, ok := req.(*vapb.PerformValidationRequest)
if !ok {
return nil, fmt.Errorf("got type %T, want *vapb.PerformValidationRequest", req)
}
return remoteva.DoDCV(ctx, validationRequest)
}
summary, prob = va.doRemoteOperation(ctx, op, req)
}
return bgrpc.ValidationResultToPB(records, filterProblemDetails(prob), va.perspective, va.rir)
}
// DoCAA conducts a CAA check for the specified dnsName. When invoked on the
// primary Validation Authority (VA) and the local check succeeds, it also
// performs CAA checks using the configured remote VAs. Failed checks are
// indicated by a non-nil Problems in the returned ValidationResult. DoCAA
// returns error only for internal logic errors (and the client may receive
// errors from gRPC in the event of a communication problem). This method
// implements the CAA portion of Multi-Perspective Issuance Corroboration as
// defined in BRs Sections 3.2.2.9 and 5.4.1.
func (va *ValidationAuthorityImpl) DoCAA(ctx context.Context, req *vapb.IsCAAValidRequest) (*vapb.IsCAAValidResponse, error) {
if core.IsAnyNilOrZero(req.Domain, req.ValidationMethod, req.AccountURIID) {
return nil, berrors.InternalServerError("incomplete IsCAAValid request")
}
logEvent := validationLogEvent{
AuthzID: req.AuthzID,
Requester: req.AccountURIID,
Identifier: req.Domain,
}
challType := core.AcmeChallenge(req.ValidationMethod)
if !challType.IsValid() {
return nil, berrors.InternalServerError("unrecognized validation method %q", req.ValidationMethod)
}
acmeID := identifier.NewDNS(req.Domain)
params := &caaParams{
accountURIID: req.AccountURIID,
validationMethod: challType,
}
// Initialize variables and a deferred function to handle check latency
// metrics, log check errors, and log an MPIC summary. Avoid using := to
// redeclare `prob`, `localLatency`, or `summary` below this point.
var prob *probs.ProblemDetails
var summary *mpicSummary
var internalErr error
var localLatency time.Duration
start := va.clk.Now()
defer func() {
probType := ""
outcome := fail
if prob != nil {
// CAA check failed.
probType = string(prob.Type)
logEvent.Error = prob.Error()
} else {
// CAA check passed.
outcome = pass
}
// Observe local check latency (primary|remote).
va.observeLatency(opCAA, va.perspective, string(challType), probType, outcome, localLatency)
if va.isPrimaryVA() {
// Observe total check latency (primary+remote).
va.observeLatency(opCAA, allPerspectives, string(challType), probType, outcome, va.clk.Since(start))
logEvent.Summary = summary
}
// Log the total check latency.
logEvent.Latency = va.clk.Since(start).Round(time.Millisecond).Seconds()
va.log.AuditObject("CAA check result", logEvent)
}()
internalErr = va.checkCAA(ctx, acmeID, params)
// Stop the clock for local check latency.
localLatency = va.clk.Since(start)
if internalErr != nil {
logEvent.InternalError = internalErr.Error()
prob = detailedError(internalErr)
prob.Detail = fmt.Sprintf("While processing CAA for %s: %s", req.Domain, prob.Detail)
}
if va.isPrimaryVA() {
op := func(ctx context.Context, remoteva RemoteVA, req proto.Message) (remoteResult, error) {
checkRequest, ok := req.(*vapb.IsCAAValidRequest)
if !ok {
return nil, fmt.Errorf("got type %T, want *vapb.IsCAAValidRequest", req)
}
return remoteva.DoCAA(ctx, checkRequest)
}
var remoteProb *probs.ProblemDetails
summary, remoteProb = va.doRemoteOperation(ctx, op, req)
// If the remote result was a non-nil problem then fail the CAA check
if remoteProb != nil {
prob = remoteProb
va.log.Infof("CAA check failed due to remote failures: identifier=%v err=%s",
req.Domain, remoteProb)
}
}
if prob != nil {
// The ProblemDetails will be serialized through gRPC, which requires UTF-8.
// It will also later be serialized in JSON, which defaults to UTF-8. Make
// sure it is UTF-8 clean now.
prob = filterProblemDetails(prob)
return &vapb.IsCAAValidResponse{
Problem: &corepb.ProblemDetails{
ProblemType: string(prob.Type),
Detail: replaceInvalidUTF8([]byte(prob.Detail)),
},
Perspective: va.perspective,
Rir: va.rir,
}, nil
} else {
return &vapb.IsCAAValidResponse{
Perspective: va.perspective,
Rir: va.rir,
}, nil
}
}