From 62b82061c9cac65381021b1f66a848e66ce8c2fd Mon Sep 17 00:00:00 2001 From: Samantha Date: Mon, 20 Jan 2025 18:32:21 -0500 Subject: [PATCH] RA: Allow profile selection to be gated on account-based allow lists --- cmd/boulder-ra/main.go | 23 +++++++++++++++++++++++ ra/ra.go | 28 ++++++++++++++++++++++++++++ ra/ra_test.go | 1 + 3 files changed, 52 insertions(+) diff --git a/cmd/boulder-ra/main.go b/cmd/boulder-ra/main.go index e0b2e3e915b..d094ee89dc9 100644 --- a/cmd/boulder-ra/main.go +++ b/cmd/boulder-ra/main.go @@ -3,10 +3,12 @@ package notmain import ( "context" "flag" + "fmt" "os" "time" akamaipb "github.com/letsencrypt/boulder/akamai/proto" + "github.com/letsencrypt/boulder/allowlist" capb "github.com/letsencrypt/boulder/ca/proto" "github.com/letsencrypt/boulder/cmd" "github.com/letsencrypt/boulder/config" @@ -91,6 +93,15 @@ type Config struct { // you need to request a new challenge. PendingAuthorizationLifetimeDays int `validate:"required,min=1,max=29"` + // ValidationProfiles is a map of validation profiles to their + // respective issuance allow lists. + ValidationProfiles map[string]struct { + // AllowList specifies the file path to a YAML list of account IDs + // permitted to use this profile. If left empty, no accounts are + // allowed to use this profile. + AllowList string `validate:"omitempty"` + } + // GoodKey is an embedded config stanza for the goodkey library. GoodKey goodkey.Config @@ -252,6 +263,17 @@ func main() { } pendingAuthorizationLifetime := time.Duration(c.RA.PendingAuthorizationLifetimeDays) * 24 * time.Hour + validationProfiles := make(map[string]*ra.ValidationProfile) + for profileName, v := range c.RA.ValidationProfiles { + if v.AllowList != "" { + data, err := os.ReadFile(v.AllowList) + cmd.FailOnError(err, fmt.Sprintf("Failed to read allow list for profile %q", profileName)) + allowList, err := allowlist.NewFromYAML[int64](data) + cmd.FailOnError(err, fmt.Sprintf("Failed to parse allow list for profile %q", profileName)) + validationProfiles[profileName] = ra.NewValidationProfile(allowList) + } + } + if features.Get().AsyncFinalize && c.RA.FinalizeTimeout.Duration == 0 { cmd.Fail("finalizeTimeout must be supplied when AsyncFinalize feature is enabled") } @@ -289,6 +311,7 @@ func main() { c.RA.MaxNames, authorizationLifetime, pendingAuthorizationLifetime, + validationProfiles, pubc, c.RA.OrderLifetime.Duration, c.RA.FinalizeTimeout.Duration, diff --git a/ra/ra.go b/ra/ra.go index 65cabc73794..b1fb514f9cb 100644 --- a/ra/ra.go +++ b/ra/ra.go @@ -29,6 +29,7 @@ import ( "github.com/letsencrypt/boulder/akamai" akamaipb "github.com/letsencrypt/boulder/akamai/proto" + "github.com/letsencrypt/boulder/allowlist" capb "github.com/letsencrypt/boulder/ca/proto" "github.com/letsencrypt/boulder/core" corepb "github.com/letsencrypt/boulder/core/proto" @@ -66,6 +67,18 @@ var ( caaRecheckDuration = -7 * time.Hour ) +// ValidationProfile holds the allowlist for a given validation profile. +type ValidationProfile struct { + // allowList the set of account IDs allowed to use this profile. If left + // empty, no accounts are allowed to use this profile. + allowList *allowlist.List[int64] +} + +// NewValidationProfile creates a new ValidationProfile with the provided allowList. +func NewValidationProfile(allowList *allowlist.List[int64]) *ValidationProfile { + return &ValidationProfile{allowList: allowList} +} + // RegistrationAuthorityImpl defines an RA. // // NOTE: All of the fields in RegistrationAuthorityImpl need to be @@ -85,6 +98,7 @@ type RegistrationAuthorityImpl struct { // How long before a newly created authorization expires. authorizationLifetime time.Duration pendingAuthorizationLifetime time.Duration + validationProfiles map[string]*ValidationProfile maxContactsPerReg int limiter *ratelimits.Limiter txnBuilder *ratelimits.TransactionBuilder @@ -127,6 +141,7 @@ func NewRegistrationAuthorityImpl( maxNames int, authorizationLifetime time.Duration, pendingAuthorizationLifetime time.Duration, + validationProfiles map[string]*ValidationProfile, pubc pubpb.PublisherClient, orderLifetime time.Duration, finalizeTimeout time.Duration, @@ -245,6 +260,7 @@ func NewRegistrationAuthorityImpl( log: logger, authorizationLifetime: authorizationLifetime, pendingAuthorizationLifetime: pendingAuthorizationLifetime, + validationProfiles: validationProfiles, maxContactsPerReg: maxContactsPerReg, keyPolicy: keyPolicy, limiter: limiter, @@ -2084,6 +2100,18 @@ func (ra *RegistrationAuthorityImpl) NewOrder(ctx context.Context, req *rapb.New "Order cannot contain more than %d DNS names", ra.maxNames) } + if req.CertificateProfileName != "" { + profileValidation, ok := ra.validationProfiles[req.CertificateProfileName] + if ok { + if !profileValidation.allowList.Contains(req.RegistrationID) { + return nil, berrors.UnauthorizedError("account ID %d is not permitted to use profile %q", + req.RegistrationID, + req.CertificateProfileName, + ) + } + } + } + // Validate that our policy allows issuing for each of the names in the order err := ra.PA.WillingToIssue(newOrder.DnsNames) if err != nil { diff --git a/ra/ra_test.go b/ra/ra_test.go index b4008a05c97..44968b09967 100644 --- a/ra/ra_test.go +++ b/ra/ra_test.go @@ -342,6 +342,7 @@ func initAuthorities(t *testing.T) (*DummyValidationAuthority, sapb.StorageAutho 1, testKeyPolicy, limiter, txnBuilder, 100, 300*24*time.Hour, 7*24*time.Hour, nil, + nil, 7*24*time.Hour, 5*time.Minute, ctp, nil, nil) ra.SA = sa