Skip to content

Commit

Permalink
RA/VA: Add MPIC compliant DCV and CAA checks (#7870)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
beautifulentropy authored Dec 10, 2024
1 parent 071b8c5 commit dda8acc
Show file tree
Hide file tree
Showing 16 changed files with 1,512 additions and 364 deletions.
7 changes: 5 additions & 2 deletions cmd/boulder-ra/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -288,7 +289,6 @@ func main() {
authorizationLifetime,
pendingAuthorizationLifetime,
pubc,
caaClient,
c.RA.OrderLifetime.Duration,
c.RA.FinalizeTimeout.Duration,
ctp,
Expand All @@ -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
Expand Down
18 changes: 18 additions & 0 deletions features/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
96 changes: 64 additions & 32 deletions ra/ra.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -265,7 +255,6 @@ func NewRegistrationAuthorityImpl(
txnBuilder: txnBuilder,
maxNames: maxNames,
publisher: pubc,
caa: caaClient,
orderLifetime: orderLifetime,
finalizeTimeout: finalizeTimeout,
ctpolicy: ctp,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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")
Expand Down
Loading

0 comments on commit dda8acc

Please sign in to comment.