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:
parent
071b8c5b35
commit
dda8acc34a
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
96
ra/ra.go
96
ra/ra.go
|
|
@ -23,7 +23,6 @@ import (
|
|||
"github.com/jmhodges/clock"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"golang.org/x/crypto/ocsp"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
|
|
@ -52,6 +51,7 @@ import (
|
|||
"github.com/letsencrypt/boulder/ratelimits"
|
||||
"github.com/letsencrypt/boulder/revocation"
|
||||
sapb "github.com/letsencrypt/boulder/sa/proto"
|
||||
"github.com/letsencrypt/boulder/va"
|
||||
vapb "github.com/letsencrypt/boulder/va/proto"
|
||||
|
||||
"github.com/letsencrypt/boulder/web"
|
||||
|
|
@ -68,14 +68,6 @@ var (
|
|||
caaRecheckDuration = -7 * time.Hour
|
||||
)
|
||||
|
||||
type caaChecker interface {
|
||||
IsCAAValid(
|
||||
ctx context.Context,
|
||||
in *vapb.IsCAAValidRequest,
|
||||
opts ...grpc.CallOption,
|
||||
) (*vapb.IsCAAValidResponse, error)
|
||||
}
|
||||
|
||||
// RegistrationAuthorityImpl defines an RA.
|
||||
//
|
||||
// NOTE: All of the fields in RegistrationAuthorityImpl need to be
|
||||
|
|
@ -84,11 +76,10 @@ type RegistrationAuthorityImpl struct {
|
|||
rapb.UnsafeRegistrationAuthorityServer
|
||||
CA capb.CertificateAuthorityClient
|
||||
OCSP capb.OCSPGeneratorClient
|
||||
VA vapb.VAClient
|
||||
VA va.RemoteClients
|
||||
SA sapb.StorageAuthorityClient
|
||||
PA core.PolicyAuthority
|
||||
publisher pubpb.PublisherClient
|
||||
caa caaChecker
|
||||
|
||||
clk clock.Clock
|
||||
log blog.Logger
|
||||
|
|
@ -140,7 +131,6 @@ func NewRegistrationAuthorityImpl(
|
|||
authorizationLifetime time.Duration,
|
||||
pendingAuthorizationLifetime time.Duration,
|
||||
pubc pubpb.PublisherClient,
|
||||
caaClient caaChecker,
|
||||
orderLifetime time.Duration,
|
||||
finalizeTimeout time.Duration,
|
||||
ctp *ctpolicy.CTPolicy,
|
||||
|
|
@ -265,7 +255,6 @@ func NewRegistrationAuthorityImpl(
|
|||
txnBuilder: txnBuilder,
|
||||
maxNames: maxNames,
|
||||
publisher: pubc,
|
||||
caa: caaClient,
|
||||
orderLifetime: orderLifetime,
|
||||
finalizeTimeout: finalizeTimeout,
|
||||
ctpolicy: ctp,
|
||||
|
|
@ -849,12 +838,21 @@ func (ra *RegistrationAuthorityImpl) recheckCAA(ctx context.Context, authzs []*c
|
|||
}
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := ra.caa.IsCAAValid(ctx, &vapb.IsCAAValidRequest{
|
||||
Domain: name,
|
||||
ValidationMethod: method,
|
||||
AccountURIID: authz.RegistrationID,
|
||||
})
|
||||
var resp *vapb.IsCAAValidResponse
|
||||
var err error
|
||||
if !features.Get().EnforceMPIC {
|
||||
resp, err = ra.VA.IsCAAValid(ctx, &vapb.IsCAAValidRequest{
|
||||
Domain: name,
|
||||
ValidationMethod: method,
|
||||
AccountURIID: authz.RegistrationID,
|
||||
})
|
||||
} else {
|
||||
resp, err = ra.VA.DoCAA(ctx, &vapb.IsCAAValidRequest{
|
||||
Domain: name,
|
||||
ValidationMethod: method,
|
||||
AccountURIID: authz.RegistrationID,
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
ra.log.AuditErrf("Rechecking CAA: %s", err)
|
||||
err = berrors.InternalServerError(
|
||||
|
|
@ -1832,6 +1830,35 @@ func (ra *RegistrationAuthorityImpl) resetAccountPausingLimit(ctx context.Contex
|
|||
}
|
||||
}
|
||||
|
||||
// doDCVAndCAA performs DCV and CAA checks. When EnforceMPIC is enabled, the
|
||||
// checks are executed sequentially: DCV is performed first and CAA is only
|
||||
// checked if DCV is successful. Validation records from the DCV check are
|
||||
// returned even if the CAA check fails. When EnforceMPIC is disabled, DCV and
|
||||
// CAA checks are performed in the same request.
|
||||
func (ra *RegistrationAuthorityImpl) checkDCVAndCAA(ctx context.Context, dcvReq *vapb.PerformValidationRequest, caaReq *vapb.IsCAAValidRequest) (*corepb.ProblemDetails, []*corepb.ValidationRecord, error) {
|
||||
if !features.Get().EnforceMPIC {
|
||||
performValidationRes, err := ra.VA.PerformValidation(ctx, dcvReq)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return performValidationRes.Problem, performValidationRes.Records, nil
|
||||
} else {
|
||||
doDCVRes, err := ra.VA.DoDCV(ctx, dcvReq)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if doDCVRes.Problem != nil {
|
||||
return doDCVRes.Problem, doDCVRes.Records, nil
|
||||
}
|
||||
|
||||
doCAAResp, err := ra.VA.IsCAAValid(ctx, caaReq)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return doCAAResp.Problem, doDCVRes.Records, nil
|
||||
}
|
||||
}
|
||||
|
||||
// PerformValidation initiates validation for a specific challenge associated
|
||||
// with the given base authorization. The authorization and challenge are
|
||||
// updated based on the results.
|
||||
|
|
@ -1916,32 +1943,37 @@ func (ra *RegistrationAuthorityImpl) PerformValidation(
|
|||
copy(challenges, authz.Challenges)
|
||||
authz.Challenges = challenges
|
||||
chall, _ := bgrpc.ChallengeToPB(authz.Challenges[challIndex])
|
||||
req := vapb.PerformValidationRequest{
|
||||
DnsName: authz.Identifier.Value,
|
||||
Challenge: chall,
|
||||
Authz: &vapb.AuthzMeta{
|
||||
Id: authz.ID,
|
||||
RegID: authz.RegistrationID,
|
||||
checkProb, checkRecords, err := ra.checkDCVAndCAA(
|
||||
vaCtx,
|
||||
&vapb.PerformValidationRequest{
|
||||
DnsName: authz.Identifier.Value,
|
||||
Challenge: chall,
|
||||
Authz: &vapb.AuthzMeta{Id: authz.ID, RegID: authz.RegistrationID},
|
||||
ExpectedKeyAuthorization: expectedKeyAuthorization,
|
||||
},
|
||||
ExpectedKeyAuthorization: expectedKeyAuthorization,
|
||||
}
|
||||
res, err := ra.VA.PerformValidation(vaCtx, &req)
|
||||
&vapb.IsCAAValidRequest{
|
||||
Domain: authz.Identifier.Value,
|
||||
ValidationMethod: chall.Type,
|
||||
AccountURIID: authz.RegistrationID,
|
||||
AuthzID: authz.ID,
|
||||
},
|
||||
)
|
||||
challenge := &authz.Challenges[challIndex]
|
||||
var prob *probs.ProblemDetails
|
||||
if err != nil {
|
||||
prob = probs.ServerInternal("Could not communicate with VA")
|
||||
ra.log.AuditErrf("Could not communicate with VA: %s", err)
|
||||
} else {
|
||||
if res.Problem != nil {
|
||||
prob, err = bgrpc.PBToProblemDetails(res.Problem)
|
||||
if checkProb != nil {
|
||||
prob, err = bgrpc.PBToProblemDetails(checkProb)
|
||||
if err != nil {
|
||||
prob = probs.ServerInternal("Could not communicate with VA")
|
||||
ra.log.AuditErrf("Could not communicate with VA: %s", err)
|
||||
}
|
||||
}
|
||||
// Save the updated records
|
||||
records := make([]core.ValidationRecord, len(res.Records))
|
||||
for i, r := range res.Records {
|
||||
records := make([]core.ValidationRecord, len(checkRecords))
|
||||
for i, r := range checkRecords {
|
||||
records[i], err = bgrpc.PBToValidationRecord(r)
|
||||
if err != nil {
|
||||
prob = probs.ServerInternal("Records for validation corrupt")
|
||||
|
|
|
|||
132
ra/ra_test.go
132
ra/ra_test.go
|
|
@ -64,6 +64,7 @@ import (
|
|||
"github.com/letsencrypt/boulder/test"
|
||||
isa "github.com/letsencrypt/boulder/test/inmem/sa"
|
||||
"github.com/letsencrypt/boulder/test/vars"
|
||||
"github.com/letsencrypt/boulder/va"
|
||||
vapb "github.com/letsencrypt/boulder/va/proto"
|
||||
)
|
||||
|
||||
|
|
@ -156,14 +157,50 @@ func numAuthorizations(o *corepb.Order) int {
|
|||
}
|
||||
|
||||
type DummyValidationAuthority struct {
|
||||
performValidationRequest chan *vapb.PerformValidationRequest
|
||||
PerformValidationRequestResultError error
|
||||
PerformValidationRequestResultReturn *vapb.ValidationResult
|
||||
doDCVRequest chan *vapb.PerformValidationRequest
|
||||
doDCVError error
|
||||
doDCVResult *vapb.ValidationResult
|
||||
|
||||
doCAARequest chan *vapb.IsCAAValidRequest
|
||||
doCAAError error
|
||||
doCAAResponse *vapb.IsCAAValidResponse
|
||||
}
|
||||
|
||||
func (dva *DummyValidationAuthority) PerformValidation(ctx context.Context, req *vapb.PerformValidationRequest, _ ...grpc.CallOption) (*vapb.ValidationResult, error) {
|
||||
dva.performValidationRequest <- req
|
||||
return dva.PerformValidationRequestResultReturn, dva.PerformValidationRequestResultError
|
||||
dcvRes, err := dva.DoDCV(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if dcvRes.Problem != nil {
|
||||
return dcvRes, nil
|
||||
}
|
||||
caaResp, err := dva.DoCAA(ctx, &vapb.IsCAAValidRequest{
|
||||
Domain: req.DnsName,
|
||||
ValidationMethod: req.Challenge.Type,
|
||||
AccountURIID: req.Authz.RegID,
|
||||
AuthzID: req.Authz.Id,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &vapb.ValidationResult{
|
||||
Records: dcvRes.Records,
|
||||
Problem: caaResp.Problem,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (dva *DummyValidationAuthority) IsCAAValid(ctx context.Context, req *vapb.IsCAAValidRequest, _ ...grpc.CallOption) (*vapb.IsCAAValidResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "IsCAAValid not implemented")
|
||||
}
|
||||
|
||||
func (dva *DummyValidationAuthority) DoDCV(ctx context.Context, req *vapb.PerformValidationRequest, _ ...grpc.CallOption) (*vapb.ValidationResult, error) {
|
||||
dva.doDCVRequest <- req
|
||||
return dva.doDCVResult, dva.doDCVError
|
||||
}
|
||||
|
||||
func (dva *DummyValidationAuthority) DoCAA(ctx context.Context, req *vapb.IsCAAValidRequest, _ ...grpc.CallOption) (*vapb.IsCAAValidResponse, error) {
|
||||
dva.doCAARequest <- req
|
||||
return dva.doCAAResponse, dva.doCAAError
|
||||
}
|
||||
|
||||
var (
|
||||
|
|
@ -311,9 +348,11 @@ func initAuthorities(t *testing.T) (*DummyValidationAuthority, sapb.StorageAutho
|
|||
|
||||
saDBCleanUp := test.ResetBoulderTestDatabase(t)
|
||||
|
||||
va := &DummyValidationAuthority{
|
||||
performValidationRequest: make(chan *vapb.PerformValidationRequest, 1),
|
||||
dummyVA := &DummyValidationAuthority{
|
||||
doDCVRequest: make(chan *vapb.PerformValidationRequest, 1),
|
||||
doCAARequest: make(chan *vapb.IsCAAValidRequest, 1),
|
||||
}
|
||||
va := va.RemoteClients{VAClient: dummyVA, CAAClient: dummyVA}
|
||||
|
||||
pa, err := policy.New(map[core.AcmeChallenge]bool{
|
||||
core.ChallengeTypeHTTP01: true,
|
||||
|
|
@ -363,7 +402,7 @@ func initAuthorities(t *testing.T) (*DummyValidationAuthority, sapb.StorageAutho
|
|||
fc, log, stats,
|
||||
1, testKeyPolicy, limiter, txnBuilder, 100,
|
||||
300*24*time.Hour, 7*24*time.Hour,
|
||||
nil, noopCAA{},
|
||||
nil,
|
||||
7*24*time.Hour, 5*time.Minute,
|
||||
ctp, nil, nil)
|
||||
ra.SA = sa
|
||||
|
|
@ -371,7 +410,7 @@ func initAuthorities(t *testing.T) (*DummyValidationAuthority, sapb.StorageAutho
|
|||
ra.CA = ca
|
||||
ra.OCSP = &mocks.MockOCSPGenerator{}
|
||||
ra.PA = pa
|
||||
return va, sa, ra, rlSource, fc, cleanUp
|
||||
return dummyVA, sa, ra, rlSource, fc, cleanUp
|
||||
}
|
||||
|
||||
func TestValidateContacts(t *testing.T) {
|
||||
|
|
@ -689,7 +728,7 @@ func TestPerformValidationAlreadyValid(t *testing.T) {
|
|||
authzPB, err := bgrpc.AuthzToPB(authz)
|
||||
test.AssertNotError(t, err, "bgrpc.AuthzToPB failed")
|
||||
|
||||
va.PerformValidationRequestResultReturn = &vapb.ValidationResult{
|
||||
va.doDCVResult = &vapb.ValidationResult{
|
||||
Records: []*corepb.ValidationRecord{
|
||||
{
|
||||
AddressUsed: []byte("192.168.0.1"),
|
||||
|
|
@ -700,6 +739,7 @@ func TestPerformValidationAlreadyValid(t *testing.T) {
|
|||
},
|
||||
Problem: nil,
|
||||
}
|
||||
va.doCAAResponse = &vapb.IsCAAValidResponse{Problem: nil}
|
||||
|
||||
// A subsequent call to perform validation should return nil due
|
||||
// to being short-circuited because of valid authz reuse.
|
||||
|
|
@ -718,7 +758,7 @@ func TestPerformValidationSuccess(t *testing.T) {
|
|||
// We know this is OK because of TestNewAuthorization
|
||||
authzPB := createPendingAuthorization(t, sa, Identifier, fc.Now().Add(12*time.Hour))
|
||||
|
||||
va.PerformValidationRequestResultReturn = &vapb.ValidationResult{
|
||||
va.doDCVResult = &vapb.ValidationResult{
|
||||
Records: []*corepb.ValidationRecord{
|
||||
{
|
||||
AddressUsed: []byte("192.168.0.1"),
|
||||
|
|
@ -730,6 +770,7 @@ func TestPerformValidationSuccess(t *testing.T) {
|
|||
},
|
||||
Problem: nil,
|
||||
}
|
||||
va.doCAAResponse = &vapb.IsCAAValidResponse{Problem: nil}
|
||||
|
||||
now := fc.Now()
|
||||
challIdx := dnsChallIdx(t, authzPB.Challenges)
|
||||
|
|
@ -741,7 +782,7 @@ func TestPerformValidationSuccess(t *testing.T) {
|
|||
|
||||
var vaRequest *vapb.PerformValidationRequest
|
||||
select {
|
||||
case r := <-va.performValidationRequest:
|
||||
case r := <-va.doDCVRequest:
|
||||
vaRequest = r
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("Timed out waiting for DummyValidationAuthority.PerformValidation to complete")
|
||||
|
|
@ -822,7 +863,7 @@ func TestPerformValidation_FailedValidationsTriggerPauseIdentifiersRatelimit(t *
|
|||
|
||||
// Now a failed validation should result in the identifier being paused
|
||||
// due to the strict ratelimit.
|
||||
va.PerformValidationRequestResultReturn = &vapb.ValidationResult{
|
||||
va.doDCVResult = &vapb.ValidationResult{
|
||||
Records: []*corepb.ValidationRecord{
|
||||
{
|
||||
AddressUsed: []byte("192.168.0.1"),
|
||||
|
|
@ -832,6 +873,9 @@ func TestPerformValidation_FailedValidationsTriggerPauseIdentifiersRatelimit(t *
|
|||
ResolverAddrs: []string{"rebound"},
|
||||
},
|
||||
},
|
||||
Problem: nil,
|
||||
}
|
||||
va.doCAAResponse = &vapb.IsCAAValidResponse{
|
||||
Problem: &corepb.ProblemDetails{
|
||||
Detail: fmt.Sprintf("CAA invalid for %s", domain),
|
||||
},
|
||||
|
|
@ -893,8 +937,7 @@ func TestPerformValidation_FailedThenSuccessfulValidationResetsPauseIdentifiersR
|
|||
})
|
||||
test.AssertNotError(t, err, "updating rate limit bucket")
|
||||
|
||||
// Now a successful validation should reset the rate limit bucket.
|
||||
va.PerformValidationRequestResultReturn = &vapb.ValidationResult{
|
||||
va.doDCVResult = &vapb.ValidationResult{
|
||||
Records: []*corepb.ValidationRecord{
|
||||
{
|
||||
AddressUsed: []byte("192.168.0.1"),
|
||||
|
|
@ -906,6 +949,7 @@ func TestPerformValidation_FailedThenSuccessfulValidationResetsPauseIdentifiersR
|
|||
},
|
||||
Problem: nil,
|
||||
}
|
||||
va.doCAAResponse = &vapb.IsCAAValidResponse{Problem: nil}
|
||||
|
||||
_, err = ra.PerformValidation(ctx, &rapb.PerformValidationRequest{
|
||||
Authz: authzPB,
|
||||
|
|
@ -931,7 +975,7 @@ func TestPerformValidationVAError(t *testing.T) {
|
|||
|
||||
authzPB := createPendingAuthorization(t, sa, Identifier, fc.Now().Add(12*time.Hour))
|
||||
|
||||
va.PerformValidationRequestResultError = fmt.Errorf("Something went wrong")
|
||||
va.doDCVError = fmt.Errorf("Something went wrong")
|
||||
|
||||
challIdx := dnsChallIdx(t, authzPB.Challenges)
|
||||
authzPB, err := ra.PerformValidation(ctx, &rapb.PerformValidationRequest{
|
||||
|
|
@ -943,7 +987,7 @@ func TestPerformValidationVAError(t *testing.T) {
|
|||
|
||||
var vaRequest *vapb.PerformValidationRequest
|
||||
select {
|
||||
case r := <-va.performValidationRequest:
|
||||
case r := <-va.doDCVRequest:
|
||||
vaRequest = r
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("Timed out waiting for DummyValidationAuthority.PerformValidation to complete")
|
||||
|
|
@ -1668,7 +1712,7 @@ func TestDeactivateRegistration(t *testing.T) {
|
|||
test.AssertEquals(t, dbReg.Status, string(core.StatusDeactivated))
|
||||
}
|
||||
|
||||
// noopCAA implements caaChecker, always returning nil
|
||||
// noopCAA implements vapb.CAAClient, always returning nil
|
||||
type noopCAA struct{}
|
||||
|
||||
func (cr noopCAA) IsCAAValid(
|
||||
|
|
@ -1679,8 +1723,16 @@ func (cr noopCAA) IsCAAValid(
|
|||
return &vapb.IsCAAValidResponse{}, nil
|
||||
}
|
||||
|
||||
// caaRecorder implements caaChecker, always returning nil, but recording the
|
||||
// names it was called for.
|
||||
func (cr noopCAA) DoCAA(
|
||||
ctx context.Context,
|
||||
in *vapb.IsCAAValidRequest,
|
||||
opts ...grpc.CallOption,
|
||||
) (*vapb.IsCAAValidResponse, error) {
|
||||
return &vapb.IsCAAValidResponse{}, nil
|
||||
}
|
||||
|
||||
// caaRecorder implements vapb.CAAClient, always returning nil, but recording
|
||||
// the names it was called for.
|
||||
type caaRecorder struct {
|
||||
sync.Mutex
|
||||
names map[string]bool
|
||||
|
|
@ -1697,13 +1749,24 @@ func (cr *caaRecorder) IsCAAValid(
|
|||
return &vapb.IsCAAValidResponse{}, nil
|
||||
}
|
||||
|
||||
func (cr *caaRecorder) DoCAA(
|
||||
ctx context.Context,
|
||||
in *vapb.IsCAAValidRequest,
|
||||
opts ...grpc.CallOption,
|
||||
) (*vapb.IsCAAValidResponse, error) {
|
||||
cr.Lock()
|
||||
defer cr.Unlock()
|
||||
cr.names[in.Domain] = true
|
||||
return &vapb.IsCAAValidResponse{}, nil
|
||||
}
|
||||
|
||||
// Test that the right set of domain names have their CAA rechecked, based on
|
||||
// their `Validated` (attemptedAt in the database) timestamp.
|
||||
func TestRecheckCAADates(t *testing.T) {
|
||||
_, _, ra, _, fc, cleanUp := initAuthorities(t)
|
||||
defer cleanUp()
|
||||
recorder := &caaRecorder{names: make(map[string]bool)}
|
||||
ra.caa = recorder
|
||||
ra.VA = va.RemoteClients{CAAClient: recorder}
|
||||
ra.authorizationLifetime = 15 * time.Hour
|
||||
|
||||
recentValidated := fc.Now().Add(-1 * time.Hour)
|
||||
|
|
@ -1889,6 +1952,27 @@ func (cf *caaFailer) IsCAAValid(
|
|||
return cvrpb, nil
|
||||
}
|
||||
|
||||
func (cf *caaFailer) DoCAA(
|
||||
ctx context.Context,
|
||||
in *vapb.IsCAAValidRequest,
|
||||
opts ...grpc.CallOption,
|
||||
) (*vapb.IsCAAValidResponse, error) {
|
||||
cvrpb := &vapb.IsCAAValidResponse{}
|
||||
switch in.Domain {
|
||||
case "a.com":
|
||||
cvrpb.Problem = &corepb.ProblemDetails{
|
||||
Detail: "CAA invalid for a.com",
|
||||
}
|
||||
case "c.com":
|
||||
cvrpb.Problem = &corepb.ProblemDetails{
|
||||
Detail: "CAA invalid for c.com",
|
||||
}
|
||||
case "d.com":
|
||||
return nil, fmt.Errorf("Error checking CAA for d.com")
|
||||
}
|
||||
return cvrpb, nil
|
||||
}
|
||||
|
||||
func TestRecheckCAAEmpty(t *testing.T) {
|
||||
_, _, ra, _, _, cleanUp := initAuthorities(t)
|
||||
defer cleanUp()
|
||||
|
|
@ -1906,6 +1990,7 @@ func makeHTTP01Authorization(domain string) *core.Authorization {
|
|||
func TestRecheckCAASuccess(t *testing.T) {
|
||||
_, _, ra, _, _, cleanUp := initAuthorities(t)
|
||||
defer cleanUp()
|
||||
ra.VA = va.RemoteClients{CAAClient: &noopCAA{}}
|
||||
authzs := []*core.Authorization{
|
||||
makeHTTP01Authorization("a.com"),
|
||||
makeHTTP01Authorization("b.com"),
|
||||
|
|
@ -1918,7 +2003,7 @@ func TestRecheckCAASuccess(t *testing.T) {
|
|||
func TestRecheckCAAFail(t *testing.T) {
|
||||
_, _, ra, _, _, cleanUp := initAuthorities(t)
|
||||
defer cleanUp()
|
||||
ra.caa = &caaFailer{}
|
||||
ra.VA = va.RemoteClients{CAAClient: &caaFailer{}}
|
||||
authzs := []*core.Authorization{
|
||||
makeHTTP01Authorization("a.com"),
|
||||
makeHTTP01Authorization("b.com"),
|
||||
|
|
@ -1969,7 +2054,7 @@ func TestRecheckCAAFail(t *testing.T) {
|
|||
func TestRecheckCAAInternalServerError(t *testing.T) {
|
||||
_, _, ra, _, _, cleanUp := initAuthorities(t)
|
||||
defer cleanUp()
|
||||
ra.caa = &caaFailer{}
|
||||
ra.VA = va.RemoteClients{CAAClient: &caaFailer{}}
|
||||
authzs := []*core.Authorization{
|
||||
makeHTTP01Authorization("a.com"),
|
||||
makeHTTP01Authorization("b.com"),
|
||||
|
|
@ -3432,6 +3517,7 @@ func TestIssueCertificateAuditLog(t *testing.T) {
|
|||
func TestIssueCertificateCAACheckLog(t *testing.T) {
|
||||
_, sa, ra, _, fc, cleanUp := initAuthorities(t)
|
||||
defer cleanUp()
|
||||
ra.VA = va.RemoteClients{CAAClient: &noopCAA{}}
|
||||
|
||||
exp := fc.Now().Add(24 * time.Hour)
|
||||
recent := fc.Now().Add(-1 * time.Hour)
|
||||
|
|
|
|||
|
|
@ -129,7 +129,8 @@
|
|||
"AsyncFinalize": true,
|
||||
"UseKvLimitsForNewOrder": true,
|
||||
"AutomaticallyPauseZombieClients": true,
|
||||
"NoPendingAuthzReuse": true
|
||||
"NoPendingAuthzReuse": true,
|
||||
"EnforceMPIC": true
|
||||
},
|
||||
"ctLogs": {
|
||||
"stagger": "500ms",
|
||||
|
|
|
|||
|
|
@ -23,6 +23,11 @@
|
|||
"va.boulder"
|
||||
]
|
||||
},
|
||||
"va.CAA": {
|
||||
"clientNames": [
|
||||
"va.boulder"
|
||||
]
|
||||
},
|
||||
"grpc.health.v1.Health": {
|
||||
"clientNames": [
|
||||
"health-checker.boulder"
|
||||
|
|
|
|||
|
|
@ -23,6 +23,11 @@
|
|||
"va.boulder"
|
||||
]
|
||||
},
|
||||
"va.CAA": {
|
||||
"clientNames": [
|
||||
"va.boulder"
|
||||
]
|
||||
},
|
||||
"grpc.health.v1.Health": {
|
||||
"clientNames": [
|
||||
"health-checker.boulder"
|
||||
|
|
|
|||
|
|
@ -23,6 +23,11 @@
|
|||
"va.boulder"
|
||||
]
|
||||
},
|
||||
"va.CAA": {
|
||||
"clientNames": [
|
||||
"va.boulder"
|
||||
]
|
||||
},
|
||||
"grpc.health.v1.Health": {
|
||||
"clientNames": [
|
||||
"health-checker.boulder"
|
||||
|
|
|
|||
394
va/caa_test.go
394
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,
|
||||
},
|
||||
}
|
||||
|
||||
// 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))
|
||||
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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
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()
|
||||
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")
|
||||
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)
|
||||
}
|
||||
|
||||
var invalidRVACount int
|
||||
for _, x := range tc.remoteVAs {
|
||||
if x.ua == brokenUA || x.ua == hijackedUA {
|
||||
invalidRVACount++
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
gotRequestProbs := mockLog.GetAllMatching(" returned a problem: ")
|
||||
test.AssertEquals(t, len(gotRequestProbs), invalidRVACount)
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
if testFunc.name == "IsCAAValid" {
|
||||
var invalidRVACount int
|
||||
for _, x := range tc.remoteVAs {
|
||||
if x.ua == brokenUA || x.ua == hijackedUA {
|
||||
invalidRVACount++
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
gotRequestProbs := mockLog.GetAllMatching(" returned a problem: ")
|
||||
test.AssertEquals(t, len(gotRequestProbs), invalidRVACount)
|
||||
|
||||
if tc.expectedLabels != nil {
|
||||
test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, tc.expectedLabels, 1)
|
||||
}
|
||||
})
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
// 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)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
13
va/va.go
13
va/va.go
|
|
@ -36,8 +36,9 @@ const (
|
|||
PrimaryPerspective = "Primary"
|
||||
allPerspectives = "all"
|
||||
|
||||
opChallAndCAA = "challenge+caa"
|
||||
opCAA = "caa"
|
||||
opDCVAndCAA = "dcv+caa"
|
||||
opDCV = "dcv"
|
||||
opCAA = "caa"
|
||||
|
||||
pass = "pass"
|
||||
fail = "fail"
|
||||
|
|
@ -97,7 +98,7 @@ type RemoteVA struct {
|
|||
type vaMetrics struct {
|
||||
// validationLatency is a histogram of the latency to perform validations
|
||||
// from the primary and remote VA perspectives. It's labelled by:
|
||||
// - operation: VA.ValidateChallenge or VA.CheckCAA as [challenge|caa|challenge+caa]
|
||||
// - operation: VA.DoDCV or VA.DoCAA as [dcv|caa|dcv+caa]
|
||||
// - perspective: ValidationAuthorityImpl.perspective
|
||||
// - challenge_type: core.Challenge.Type
|
||||
// - problem_type: probs.ProblemType
|
||||
|
|
@ -438,7 +439,7 @@ func (va *ValidationAuthorityImpl) validateChallenge(
|
|||
// observeLatency records entries in the validationLatency histogram of the
|
||||
// latency to perform validations from the primary and remote VA perspectives.
|
||||
// The labels are:
|
||||
// - operation: VA.ValidateChallenge or VA.CheckCAA as [challenge|caa]
|
||||
// - operation: VA.DoDCV or VA.DoCAA as [dcv|caa]
|
||||
// - perspective: [ValidationAuthorityImpl.perspective|all]
|
||||
// - challenge_type: core.Challenge.Type
|
||||
// - problem_type: probs.ProblemType
|
||||
|
|
@ -714,10 +715,10 @@ func (va *ValidationAuthorityImpl) PerformValidation(ctx context.Context, req *v
|
|||
outcome = pass
|
||||
}
|
||||
// Observe local validation latency (primary|remote).
|
||||
va.observeLatency(opChallAndCAA, va.perspective, string(chall.Type), probType, outcome, localLatency)
|
||||
va.observeLatency(opDCVAndCAA, va.perspective, string(chall.Type), probType, outcome, localLatency)
|
||||
if va.isPrimaryVA() {
|
||||
// Observe total validation latency (primary+remote).
|
||||
va.observeLatency(opChallAndCAA, allPerspectives, string(chall.Type), probType, outcome, va.clk.Since(start))
|
||||
va.observeLatency(opDCVAndCAA, allPerspectives, string(chall.Type), probType, outcome, va.clk.Since(start))
|
||||
}
|
||||
|
||||
// Log the total validation latency.
|
||||
|
|
|
|||
550
va/va_test.go
550
va/va_test.go
|
|
@ -270,10 +270,18 @@ func (v cancelledVA) PerformValidation(_ context.Context, _ *vapb.PerformValidat
|
|||
return nil, context.Canceled
|
||||
}
|
||||
|
||||
func (v cancelledVA) DoDCV(_ context.Context, _ *vapb.PerformValidationRequest, _ ...grpc.CallOption) (*vapb.ValidationResult, error) {
|
||||
return nil, context.Canceled
|
||||
}
|
||||
|
||||
func (v cancelledVA) IsCAAValid(_ context.Context, _ *vapb.IsCAAValidRequest, _ ...grpc.CallOption) (*vapb.IsCAAValidResponse, error) {
|
||||
return nil, context.Canceled
|
||||
}
|
||||
|
||||
func (v cancelledVA) DoCAA(_ context.Context, _ *vapb.IsCAAValidRequest, _ ...grpc.CallOption) (*vapb.IsCAAValidResponse, error) {
|
||||
return nil, context.Canceled
|
||||
}
|
||||
|
||||
// brokenRemoteVA is a mock for the VAClient and CAAClient interfaces that always return
|
||||
// errors.
|
||||
type brokenRemoteVA struct{}
|
||||
|
|
@ -287,10 +295,19 @@ func (b brokenRemoteVA) PerformValidation(_ context.Context, _ *vapb.PerformVali
|
|||
return nil, errBrokenRemoteVA
|
||||
}
|
||||
|
||||
// DoDCV returns errBrokenRemoteVA unconditionally
|
||||
func (b brokenRemoteVA) DoDCV(_ context.Context, _ *vapb.PerformValidationRequest, _ ...grpc.CallOption) (*vapb.ValidationResult, error) {
|
||||
return nil, errBrokenRemoteVA
|
||||
}
|
||||
|
||||
func (b brokenRemoteVA) IsCAAValid(_ context.Context, _ *vapb.IsCAAValidRequest, _ ...grpc.CallOption) (*vapb.IsCAAValidResponse, error) {
|
||||
return nil, errBrokenRemoteVA
|
||||
}
|
||||
|
||||
func (b brokenRemoteVA) DoCAA(_ context.Context, _ *vapb.IsCAAValidRequest, _ ...grpc.CallOption) (*vapb.IsCAAValidResponse, error) {
|
||||
return nil, errBrokenRemoteVA
|
||||
}
|
||||
|
||||
// inMemVA is a wrapper which fulfills the VAClient and CAAClient
|
||||
// interfaces, but then forwards requests directly to its inner
|
||||
// ValidationAuthorityImpl rather than over the network. This lets a local
|
||||
|
|
@ -303,10 +320,18 @@ func (inmem *inMemVA) PerformValidation(ctx context.Context, req *vapb.PerformVa
|
|||
return inmem.rva.PerformValidation(ctx, req)
|
||||
}
|
||||
|
||||
func (inmem *inMemVA) DoDCV(ctx context.Context, req *vapb.PerformValidationRequest, _ ...grpc.CallOption) (*vapb.ValidationResult, error) {
|
||||
return inmem.rva.DoDCV(ctx, req)
|
||||
}
|
||||
|
||||
func (inmem *inMemVA) IsCAAValid(ctx context.Context, req *vapb.IsCAAValidRequest, _ ...grpc.CallOption) (*vapb.IsCAAValidResponse, error) {
|
||||
return inmem.rva.IsCAAValid(ctx, req)
|
||||
}
|
||||
|
||||
func (inmem *inMemVA) DoCAA(ctx context.Context, req *vapb.IsCAAValidRequest, _ ...grpc.CallOption) (*vapb.IsCAAValidResponse, error) {
|
||||
return inmem.rva.DoCAA(ctx, req)
|
||||
}
|
||||
|
||||
func TestNewValidationAuthorityImplWithDuplicateRemotes(t *testing.T) {
|
||||
var remoteVAs []RemoteVA
|
||||
for i := 0; i < 3; i++ {
|
||||
|
|
@ -333,7 +358,19 @@ func TestNewValidationAuthorityImplWithDuplicateRemotes(t *testing.T) {
|
|||
test.AssertContains(t, err.Error(), "duplicate remote VA perspective \"dadaist\"")
|
||||
}
|
||||
|
||||
type validationFuncRunner func(context.Context, *ValidationAuthorityImpl, *vapb.PerformValidationRequest) (*vapb.ValidationResult, error)
|
||||
|
||||
var runPerformValidation = func(ctx context.Context, va *ValidationAuthorityImpl, req *vapb.PerformValidationRequest) (*vapb.ValidationResult, error) {
|
||||
return va.PerformValidation(ctx, req)
|
||||
}
|
||||
|
||||
var runDoDCV = func(ctx context.Context, va *ValidationAuthorityImpl, req *vapb.PerformValidationRequest) (*vapb.ValidationResult, error) {
|
||||
return va.DoDCV(ctx, req)
|
||||
}
|
||||
|
||||
func TestPerformValidationWithMismatchedRemoteVAPerspectives(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mismatched1 := RemoteVA{
|
||||
RemoteClients: setupRemote(nil, "", nil, "dadaist", arin),
|
||||
Perspective: "baroque",
|
||||
|
|
@ -346,15 +383,36 @@ func TestPerformValidationWithMismatchedRemoteVAPerspectives(t *testing.T) {
|
|||
}
|
||||
remoteVAs := setupRemotes([]remoteConf{{rir: ripe}}, nil)
|
||||
remoteVAs = append(remoteVAs, mismatched1, mismatched2)
|
||||
va, mockLog := setup(nil, "", remoteVAs, nil)
|
||||
|
||||
req := createValidationRequest("good-dns01.com", core.ChallengeTypeDNS01)
|
||||
res, _ := va.PerformValidation(context.Background(), req)
|
||||
test.AssertNotNil(t, res.GetProblem(), "validation succeeded with mismatched remote VA perspectives")
|
||||
test.AssertEquals(t, len(mockLog.GetAllMatching("Expected perspective")), 2)
|
||||
testCases := []struct {
|
||||
validationFuncName string
|
||||
validationFunc validationFuncRunner
|
||||
}{
|
||||
{
|
||||
validationFuncName: "PerformValidation",
|
||||
validationFunc: runPerformValidation,
|
||||
},
|
||||
{
|
||||
validationFuncName: "DoDCV",
|
||||
validationFunc: runDoDCV,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.validationFuncName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
va, mockLog := setup(nil, "", remoteVAs, nil)
|
||||
req := createValidationRequest("good-dns01.com", core.ChallengeTypeDNS01)
|
||||
res, _ := tc.validationFunc(context.Background(), va, req)
|
||||
test.AssertNotNil(t, res.GetProblem(), "validation succeeded with mismatched remote VA perspectives")
|
||||
test.AssertEquals(t, len(mockLog.GetAllMatching("Expected perspective")), 2)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPerformValidationWithMismatchedRemoteVARIRs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mismatched1 := RemoteVA{
|
||||
RemoteClients: setupRemote(nil, "", nil, "dadaist", arin),
|
||||
Perspective: "dadaist",
|
||||
|
|
@ -367,12 +425,32 @@ func TestPerformValidationWithMismatchedRemoteVARIRs(t *testing.T) {
|
|||
}
|
||||
remoteVAs := setupRemotes([]remoteConf{{rir: ripe}}, nil)
|
||||
remoteVAs = append(remoteVAs, mismatched1, mismatched2)
|
||||
va, mockLog := setup(nil, "", remoteVAs, nil)
|
||||
|
||||
req := createValidationRequest("good-dns01.com", core.ChallengeTypeDNS01)
|
||||
res, _ := va.PerformValidation(context.Background(), req)
|
||||
test.AssertNotNil(t, res.GetProblem(), "validation succeeded with mismatched remote VA perspectives")
|
||||
test.AssertEquals(t, len(mockLog.GetAllMatching("Expected perspective")), 2)
|
||||
testCases := []struct {
|
||||
validationFuncName string
|
||||
validationFunc validationFuncRunner
|
||||
}{
|
||||
{
|
||||
validationFuncName: "PerformValidation",
|
||||
validationFunc: runPerformValidation,
|
||||
},
|
||||
{
|
||||
validationFuncName: "DoDCV",
|
||||
validationFunc: runDoDCV,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.validationFuncName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
va, mockLog := setup(nil, "", remoteVAs, nil)
|
||||
req := createValidationRequest("good-dns01.com", core.ChallengeTypeDNS01)
|
||||
res, _ := tc.validationFunc(context.Background(), va, req)
|
||||
test.AssertNotNil(t, res.GetProblem(), "validation succeeded with mismatched remote VA perspectives")
|
||||
test.AssertEquals(t, len(mockLog.GetAllMatching("Expected perspective")), 2)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateMalformedChallenge(t *testing.T) {
|
||||
|
|
@ -385,88 +463,203 @@ func TestValidateMalformedChallenge(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPerformValidationInvalid(t *testing.T) {
|
||||
t.Parallel()
|
||||
va, _ := setup(nil, "", nil, nil)
|
||||
|
||||
req := createValidationRequest("foo.com", core.ChallengeTypeDNS01)
|
||||
res, _ := va.PerformValidation(context.Background(), req)
|
||||
test.Assert(t, res.Problem != nil, "validation succeeded")
|
||||
test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, prometheus.Labels{
|
||||
"operation": opChallAndCAA,
|
||||
"perspective": va.perspective,
|
||||
"challenge_type": string(core.ChallengeTypeDNS01),
|
||||
"problem_type": string(probs.UnauthorizedProblem),
|
||||
"result": fail,
|
||||
}, 1)
|
||||
testCases := []struct {
|
||||
validationFuncName string
|
||||
validationFunc validationFuncRunner
|
||||
}{
|
||||
{
|
||||
validationFuncName: "PerformValidation",
|
||||
validationFunc: runPerformValidation,
|
||||
},
|
||||
{
|
||||
validationFuncName: "DoDCV",
|
||||
validationFunc: runDoDCV,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.validationFuncName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
req := createValidationRequest("foo.com", core.ChallengeTypeDNS01)
|
||||
res, _ := tc.validationFunc(context.Background(), va, req)
|
||||
test.Assert(t, res.Problem != nil, "validation succeeded")
|
||||
if tc.validationFuncName == "PerformValidation" {
|
||||
test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, prometheus.Labels{
|
||||
"operation": opDCVAndCAA,
|
||||
"perspective": va.perspective,
|
||||
"challenge_type": string(core.ChallengeTypeDNS01),
|
||||
"problem_type": string(probs.UnauthorizedProblem),
|
||||
"result": fail,
|
||||
}, 1)
|
||||
} else {
|
||||
test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, prometheus.Labels{
|
||||
"operation": opDCV,
|
||||
"perspective": va.perspective,
|
||||
"challenge_type": string(core.ChallengeTypeDNS01),
|
||||
"problem_type": string(probs.UnauthorizedProblem),
|
||||
"result": fail,
|
||||
}, 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInternalErrorLogged(t *testing.T) {
|
||||
va, mockLog := setup(nil, "", nil, nil)
|
||||
t.Parallel()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond)
|
||||
defer cancel()
|
||||
req := createValidationRequest("nonexistent.com", core.ChallengeTypeHTTP01)
|
||||
_, err := va.PerformValidation(ctx, req)
|
||||
test.AssertNotError(t, err, "failed validation should not be an error")
|
||||
matchingLogs := mockLog.GetAllMatching(
|
||||
`Validation result JSON=.*"InternalError":"127.0.0.1: Get.*nonexistent.com/\.well-known.*: context deadline exceeded`)
|
||||
test.AssertEquals(t, len(matchingLogs), 1)
|
||||
testCases := []struct {
|
||||
validationFuncName string
|
||||
validationFunc validationFuncRunner
|
||||
}{
|
||||
{
|
||||
validationFuncName: "PerformValidation",
|
||||
validationFunc: runPerformValidation,
|
||||
},
|
||||
{
|
||||
validationFuncName: "DoDCV",
|
||||
validationFunc: runDoDCV,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.validationFuncName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
va, mockLog := setup(nil, "", nil, nil)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond)
|
||||
defer cancel()
|
||||
req := createValidationRequest("nonexistent.com", core.ChallengeTypeHTTP01)
|
||||
_, err := tc.validationFunc(ctx, va, req)
|
||||
test.AssertNotError(t, err, "failed validation should not be an error")
|
||||
matchingLogs := mockLog.GetAllMatching(
|
||||
`Validation result JSON=.*"InternalError":"127.0.0.1: Get.*nonexistent.com/\.well-known.*: context deadline exceeded`)
|
||||
test.AssertEquals(t, len(matchingLogs), 1)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPerformValidationValid(t *testing.T) {
|
||||
va, mockLog := setup(nil, "", nil, nil)
|
||||
t.Parallel()
|
||||
|
||||
// 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))
|
||||
|
||||
test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, prometheus.Labels{
|
||||
"operation": opChallAndCAA,
|
||||
"perspective": va.perspective,
|
||||
"challenge_type": string(core.ChallengeTypeDNS01),
|
||||
"problem_type": "",
|
||||
"result": pass,
|
||||
}, 1)
|
||||
resultLog := mockLog.GetAllMatching(`Validation result`)
|
||||
if len(resultLog) != 1 {
|
||||
t.Fatalf("Wrong number of matching lines for 'Validation result'")
|
||||
testCases := []struct {
|
||||
validationFuncName string
|
||||
validationFunc validationFuncRunner
|
||||
}{
|
||||
{
|
||||
validationFuncName: "PerformValidation",
|
||||
validationFunc: runPerformValidation,
|
||||
},
|
||||
{
|
||||
validationFuncName: "DoDCV",
|
||||
validationFunc: runDoDCV,
|
||||
},
|
||||
}
|
||||
if !strings.Contains(resultLog[0], `"Identifier":"good-dns01.com"`) {
|
||||
t.Error("PerformValidation didn't log validation identifier.")
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.validationFuncName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
va, mockLog := setup(nil, "", nil, nil)
|
||||
|
||||
// create a challenge with well known token
|
||||
req := createValidationRequest("good-dns01.com", core.ChallengeTypeDNS01)
|
||||
res, _ := tc.validationFunc(context.Background(), va, req)
|
||||
test.Assert(t, res.Problem == nil, fmt.Sprintf("validation failed: %#v", res.Problem))
|
||||
if tc.validationFuncName == "PerformValidation" {
|
||||
test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, prometheus.Labels{
|
||||
"operation": opDCVAndCAA,
|
||||
"perspective": va.perspective,
|
||||
"challenge_type": string(core.ChallengeTypeDNS01),
|
||||
"problem_type": "",
|
||||
"result": pass,
|
||||
}, 1)
|
||||
} else {
|
||||
test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, prometheus.Labels{
|
||||
"operation": opDCV,
|
||||
"perspective": va.perspective,
|
||||
"challenge_type": string(core.ChallengeTypeDNS01),
|
||||
"problem_type": "",
|
||||
"result": pass,
|
||||
}, 1)
|
||||
}
|
||||
resultLog := mockLog.GetAllMatching(`Validation result`)
|
||||
if len(resultLog) != 1 {
|
||||
t.Fatalf("Wrong number of matching lines for 'Validation result'")
|
||||
}
|
||||
if !strings.Contains(resultLog[0], `"Identifier":"good-dns01.com"`) {
|
||||
t.Error("PerformValidation didn't log validation identifier.")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPerformValidationWildcard tests that the VA properly strips the `*.`
|
||||
// prefix from a wildcard name provided to the PerformValidation function.
|
||||
func TestPerformValidationWildcard(t *testing.T) {
|
||||
va, mockLog := setup(nil, "", nil, nil)
|
||||
t.Parallel()
|
||||
|
||||
// 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))
|
||||
|
||||
test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, prometheus.Labels{
|
||||
"operation": opChallAndCAA,
|
||||
"perspective": va.perspective,
|
||||
"challenge_type": string(core.ChallengeTypeDNS01),
|
||||
"problem_type": "",
|
||||
"result": pass,
|
||||
}, 1)
|
||||
resultLog := mockLog.GetAllMatching(`Validation result`)
|
||||
if len(resultLog) != 1 {
|
||||
t.Fatalf("Wrong number of matching lines for 'Validation result'")
|
||||
testCases := []struct {
|
||||
validationFuncName string
|
||||
validationFunc validationFuncRunner
|
||||
}{
|
||||
{
|
||||
validationFuncName: "PerformValidation",
|
||||
validationFunc: runPerformValidation,
|
||||
},
|
||||
{
|
||||
validationFuncName: "DoDCV",
|
||||
validationFunc: runDoDCV,
|
||||
},
|
||||
}
|
||||
|
||||
// We expect that the top level Identifier reflect the wildcard name
|
||||
if !strings.Contains(resultLog[0], `"Identifier":"*.good-dns01.com"`) {
|
||||
t.Errorf("PerformValidation didn't log correct validation identifier.")
|
||||
}
|
||||
// We expect that the ValidationRecord contain the correct non-wildcard
|
||||
// hostname that was validated
|
||||
if !strings.Contains(resultLog[0], `"hostname":"good-dns01.com"`) {
|
||||
t.Errorf("PerformValidation didn't log correct validation record hostname.")
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.validationFuncName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
va, mockLog := setup(nil, "", nil, nil)
|
||||
|
||||
// create a challenge with well known token
|
||||
req := createValidationRequest("*.good-dns01.com", core.ChallengeTypeDNS01)
|
||||
// perform a validation for a wildcard name
|
||||
res, _ := tc.validationFunc(context.Background(), va, req)
|
||||
test.Assert(t, res.Problem == nil, fmt.Sprintf("validation failed: %#v", res.Problem))
|
||||
if tc.validationFuncName == "PerformValidation" {
|
||||
test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, prometheus.Labels{
|
||||
"operation": opDCVAndCAA,
|
||||
"perspective": va.perspective,
|
||||
"challenge_type": string(core.ChallengeTypeDNS01),
|
||||
"problem_type": "",
|
||||
"result": pass,
|
||||
}, 1)
|
||||
} else {
|
||||
test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, prometheus.Labels{
|
||||
"operation": opDCV,
|
||||
"perspective": va.perspective,
|
||||
"challenge_type": string(core.ChallengeTypeDNS01),
|
||||
"problem_type": "",
|
||||
"result": pass,
|
||||
}, 1)
|
||||
}
|
||||
resultLog := mockLog.GetAllMatching(`Validation result`)
|
||||
if len(resultLog) != 1 {
|
||||
t.Fatalf("Wrong number of matching lines for 'Validation result'")
|
||||
}
|
||||
|
||||
// We expect that the top level Identifier reflect the wildcard name
|
||||
if !strings.Contains(resultLog[0], `"Identifier":"*.good-dns01.com"`) {
|
||||
t.Errorf("PerformValidation didn't log correct validation identifier.")
|
||||
}
|
||||
// We expect that the ValidationRecord contain the correct non-wildcard
|
||||
// hostname that was validated
|
||||
if !strings.Contains(resultLog[0], `"hostname":"good-dns01.com"`) {
|
||||
t.Errorf("PerformValidation didn't log correct validation record hostname.")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -571,6 +764,22 @@ func TestMultiVA(t *testing.T) {
|
|||
CAAClient: cancelledVA{},
|
||||
}
|
||||
|
||||
type testFunc struct {
|
||||
name string
|
||||
impl validationFuncRunner
|
||||
}
|
||||
|
||||
testFuncs := []testFunc{
|
||||
{
|
||||
name: "PerformValidation",
|
||||
impl: runPerformValidation,
|
||||
},
|
||||
{
|
||||
name: "DoDCV",
|
||||
impl: runDoDCV,
|
||||
},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
Name string
|
||||
Remotes []remoteConf
|
||||
|
|
@ -727,40 +936,58 @@ func TestMultiVA(t *testing.T) {
|
|||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
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 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)
|
||||
// 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)
|
||||
}
|
||||
|
||||
if tc.ExpectedLogContains != "" {
|
||||
lines := mockLog.GetAllMatching(tc.ExpectedLogContains)
|
||||
if len(lines) == 0 {
|
||||
t.Fatalf("Got log %v; expected %q", mockLog.GetAll(), tc.ExpectedLogContains)
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiVAEarlyReturn(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type testFunc struct {
|
||||
name string
|
||||
impl validationFuncRunner
|
||||
}
|
||||
|
||||
testFuncs := []testFunc{
|
||||
{
|
||||
name: "PerformValidation",
|
||||
impl: runPerformValidation,
|
||||
},
|
||||
{
|
||||
name: "DoDCV",
|
||||
impl: runDoDCV,
|
||||
},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
remoteConfs []remoteConf
|
||||
}{
|
||||
|
|
@ -793,80 +1020,123 @@ func TestMultiVAEarlyReturn(t *testing.T) {
|
|||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, testFunc := range testFuncs {
|
||||
t.Run(fmt.Sprintf("case %d"+"_"+testFunc.name, i), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Configure one test server per test case so that all tests can run in parallel.
|
||||
ms := httpMultiSrv(t, expectedToken, map[string]bool{pass: true, fail: false})
|
||||
defer ms.Close()
|
||||
// Configure one test server per test case so that all tests can run in parallel.
|
||||
ms := httpMultiSrv(t, expectedToken, map[string]bool{pass: true, fail: false})
|
||||
defer ms.Close()
|
||||
|
||||
localVA, _ := setupWithRemotes(ms.Server, pass, tc.remoteConfs, nil)
|
||||
localVA, _ := setupWithRemotes(ms.Server, pass, tc.remoteConfs, nil)
|
||||
|
||||
// Perform all validations
|
||||
start := time.Now()
|
||||
req := createValidationRequest("localhost", core.ChallengeTypeHTTP01)
|
||||
res, _ := localVA.PerformValidation(ctx, req)
|
||||
// Perform all validations
|
||||
start := time.Now()
|
||||
req := createValidationRequest("localhost", core.ChallengeTypeHTTP01)
|
||||
res, _ := testFunc.impl(ctx, localVA, req)
|
||||
|
||||
// It should always fail
|
||||
if res.Problem == nil {
|
||||
t.Error("expected prob from PerformValidation, got nil")
|
||||
}
|
||||
// It should always fail
|
||||
if res.Problem == nil {
|
||||
t.Error("expected prob from PerformValidation, got nil")
|
||||
}
|
||||
|
||||
elapsed := time.Since(start).Round(time.Millisecond).Milliseconds()
|
||||
elapsed := time.Since(start).Round(time.Millisecond).Milliseconds()
|
||||
|
||||
// The slow UA should sleep for `slowRemoteSleepMillis`. But the first remote
|
||||
// VA should fail quickly and the early-return code should cause the overall
|
||||
// overall validation to return a prob quickly (i.e. in less than half of
|
||||
// `slowRemoteSleepMillis`).
|
||||
if elapsed > slowRemoteSleepMillis/2 {
|
||||
t.Errorf(
|
||||
"Expected an early return from PerformValidation in < %d ms, took %d ms",
|
||||
slowRemoteSleepMillis/2, elapsed)
|
||||
}
|
||||
})
|
||||
// The slow UA should sleep for `slowRemoteSleepMillis`. But the first remote
|
||||
// VA should fail quickly and the early-return code should cause the overall
|
||||
// overall validation to return a prob quickly (i.e. in less than half of
|
||||
// `slowRemoteSleepMillis`).
|
||||
if elapsed > slowRemoteSleepMillis/2 {
|
||||
t.Errorf(
|
||||
"Expected an early return from PerformValidation in < %d ms, took %d ms",
|
||||
slowRemoteSleepMillis/2, elapsed)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiVAPolicy(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ms := httpMultiSrv(t, expectedToken, map[string]bool{pass: true, fail: false})
|
||||
defer ms.Close()
|
||||
|
||||
remoteConfs := []remoteConf{
|
||||
{ua: fail, rir: arin},
|
||||
{ua: fail, rir: ripe},
|
||||
{ua: fail, rir: apnic},
|
||||
}
|
||||
|
||||
// Create a local test VA with the remote VAs
|
||||
localVA, _ := setupWithRemotes(ms.Server, pass, remoteConfs, nil)
|
||||
testCases := []struct {
|
||||
validationFuncName string
|
||||
validationFunc validationFuncRunner
|
||||
}{
|
||||
{
|
||||
validationFuncName: "PerformValidation",
|
||||
validationFunc: runPerformValidation,
|
||||
},
|
||||
{
|
||||
validationFuncName: "DoDCV",
|
||||
validationFunc: runDoDCV,
|
||||
},
|
||||
}
|
||||
|
||||
// Perform validation for a domain not in the disabledDomains list
|
||||
req := createValidationRequest("letsencrypt.org", core.ChallengeTypeHTTP01)
|
||||
res, _ := localVA.PerformValidation(ctx, req)
|
||||
// It should fail
|
||||
if res.Problem == nil {
|
||||
t.Error("expected prob from PerformValidation, got nil")
|
||||
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, _ := tc.validationFunc(ctx, localVA, req)
|
||||
// It should fail
|
||||
if res.Problem == nil {
|
||||
t.Error("expected prob from PerformValidation, got nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiVALogging(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ms := httpMultiSrv(t, expectedToken, map[string]bool{pass: true, fail: false})
|
||||
defer ms.Close()
|
||||
|
||||
remoteConfs := []remoteConf{
|
||||
{ua: pass, rir: arin},
|
||||
{ua: pass, rir: ripe},
|
||||
{ua: pass, rir: apnic},
|
||||
}
|
||||
|
||||
va, _ := setupWithRemotes(ms.Server, pass, remoteConfs, nil)
|
||||
req := createValidationRequest("letsencrypt.org", core.ChallengeTypeHTTP01)
|
||||
res, err := va.PerformValidation(ctx, req)
|
||||
test.Assert(t, res.Problem == nil, fmt.Sprintf("validation failed with: %#v", res.Problem))
|
||||
test.AssertNotError(t, err, "performing validation")
|
||||
testCases := []struct {
|
||||
validationFuncName string
|
||||
validationFunc validationFuncRunner
|
||||
}{
|
||||
{
|
||||
validationFuncName: "PerformValidation",
|
||||
validationFunc: runPerformValidation,
|
||||
},
|
||||
{
|
||||
validationFuncName: "DoDCV",
|
||||
validationFunc: runDoDCV,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.validationFuncName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ms := httpMultiSrv(t, expectedToken, map[string]bool{pass: true, fail: false})
|
||||
defer ms.Close()
|
||||
|
||||
va, _ := setupWithRemotes(ms.Server, pass, remoteConfs, nil)
|
||||
req := createValidationRequest("letsencrypt.org", core.ChallengeTypeHTTP01)
|
||||
res, err := tc.validationFunc(ctx, va, req)
|
||||
test.Assert(t, res.Problem == nil, fmt.Sprintf("validation failed with: %#v", res.Problem))
|
||||
test.AssertNotError(t, err, "performing validation")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetailedError(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue