RA: Use Validation Profiles to determine order/authz lifetimes (#7989)
Add three new fields to the ra.ValidationProfile structure, representing the profile's pending authorization lifetime (used to assign an expiration when a new authz is created), valid authorization lifetime (used to assign an expiration when an authz is successfully validated), and order lifetime (used to assign an expiration when a new order is created). Remove the prior top-level fields which controlled these values across all orders. Add a "defaultProfileName" field to the RA as well, to facilitate looking up a default set of lifetimes when the order doesn't specify a profile. If this default name is explicitly configured, always provide it to the CA when requesting issuance, so we don't have to duplicate the default between the two services. Modify the RA's config struct in a corresponding way: add three new fields to the ValidationProfiles structure, and deprecate the three old top-level fields. Also upgrade the ra.NewValidationProfile constructor to handle these new fields, including doing validation on their values. Fixes https://github.com/letsencrypt/boulder/issues/7605
This commit is contained in:
parent
6695895f8b
commit
2f8c6bc522
|
@ -3,7 +3,6 @@ package notmain
|
|||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
|
@ -86,24 +85,33 @@ type Config struct {
|
|||
// considered valid for. Given a value of 300 days when used with a 90-day
|
||||
// cert lifetime, this allows creation of certs that will cover a whole
|
||||
// year, plus a grace period of a month.
|
||||
AuthorizationLifetimeDays int `validate:"required,min=1,max=397"`
|
||||
//
|
||||
// Deprecated: use ValidationProfiles.[profile].ValidAuthzLifetime instead.
|
||||
// TODO(#7986): Remove this.
|
||||
AuthorizationLifetimeDays int `validate:"omitempty,required_without=ValidationProfiles,min=1,max=397"`
|
||||
|
||||
// PendingAuthorizationLifetimeDays defines how long authorizations may be in
|
||||
// the pending state. If you can't respond to a challenge this quickly, then
|
||||
// you need to request a new challenge.
|
||||
PendingAuthorizationLifetimeDays int `validate:"required,min=1,max=29"`
|
||||
//
|
||||
// Deprecated: use ValidationProfiles.[profile].PendingAuthzLifetime instead.
|
||||
// TODO(#7986): Remove this.
|
||||
PendingAuthorizationLifetimeDays int `validate:"omitempty,required_without=ValidationProfiles,min=1,max=29"`
|
||||
|
||||
// ValidationProfiles is a map of validation profiles to their
|
||||
// respective issuance allow lists. If a profile is not included in this
|
||||
// mapping, it cannot be used by any account. If this field is left
|
||||
// empty, all profiles are open to all accounts.
|
||||
ValidationProfiles map[string]struct {
|
||||
// AllowList specifies the path to a YAML file containing a list of
|
||||
// account IDs permitted to use this profile. If no path is
|
||||
// specified, the profile is open to all accounts. If the file
|
||||
// exists but is empty, the profile is closed to all accounts.
|
||||
AllowList string `validate:"omitempty"`
|
||||
}
|
||||
// TODO(#7986): Make this field required.
|
||||
ValidationProfiles map[string]ra.ValidationProfileConfig `validate:"omitempty"`
|
||||
|
||||
// DefaultProfileName sets the profile to use if one wasn't provided by the
|
||||
// client in the new-order request. Must match a configured validation
|
||||
// profile or the RA will fail to start. Must match a certificate profile
|
||||
// configured in the CA or finalization will fail for orders using this
|
||||
// default.
|
||||
// TODO(#7986): Make this field unconditionally required.
|
||||
DefaultProfileName string `validate:"required_with=ValidationProfiles"`
|
||||
|
||||
// MustStapleAllowList specifies the path to a YAML file containing a
|
||||
// list of account IDs permitted to request certificates with the OCSP
|
||||
|
@ -117,7 +125,10 @@ type Config struct {
|
|||
|
||||
// OrderLifetime is how far in the future an Order's expiration date should
|
||||
// be set when it is first created.
|
||||
OrderLifetime config.Duration
|
||||
//
|
||||
// Deprecated: Use ValidationProfiles.[profile].OrderLifetime instead.
|
||||
// TODO(#7986): Remove this.
|
||||
OrderLifetime config.Duration `validate:"omitempty,required_without=ValidationProfiles"`
|
||||
|
||||
// FinalizeTimeout is how long the RA is willing to wait for the Order
|
||||
// finalization process to take. This config parameter only has an effect
|
||||
|
@ -255,39 +266,30 @@ func main() {
|
|||
|
||||
ctp = ctpolicy.New(pubc, sctLogs, infoLogs, finalLogs, c.RA.CTLogs.Stagger.Duration, logger, scope)
|
||||
|
||||
// Baseline Requirements v1.8.1 section 4.2.1: "any reused data, document,
|
||||
// or completed validation MUST be obtained no more than 398 days prior
|
||||
// to issuing the Certificate". If unconfigured or the configured value is
|
||||
// greater than 397 days, bail out.
|
||||
if c.RA.AuthorizationLifetimeDays <= 0 || c.RA.AuthorizationLifetimeDays > 397 {
|
||||
cmd.Fail("authorizationLifetimeDays value must be greater than 0 and less than 398")
|
||||
// TODO(#7986): Remove this fallback, error out if no default is configured.
|
||||
if c.RA.DefaultProfileName == "" {
|
||||
c.RA.DefaultProfileName = ra.UnconfiguredDefaultProfileName
|
||||
}
|
||||
authorizationLifetime := time.Duration(c.RA.AuthorizationLifetimeDays) * 24 * time.Hour
|
||||
logger.Infof("Configured default profile name set to: %s", c.RA.DefaultProfileName)
|
||||
|
||||
// The Baseline Requirements v1.8.1 state that validation tokens "MUST
|
||||
// NOT be used for more than 30 days from its creation". If unconfigured
|
||||
// or the configured value pendingAuthorizationLifetimeDays is greater
|
||||
// than 29 days, bail out.
|
||||
if c.RA.PendingAuthorizationLifetimeDays <= 0 || c.RA.PendingAuthorizationLifetimeDays > 29 {
|
||||
cmd.Fail("pendingAuthorizationLifetimeDays value must be greater than 0 and less than 30")
|
||||
}
|
||||
pendingAuthorizationLifetime := time.Duration(c.RA.PendingAuthorizationLifetimeDays) * 24 * time.Hour
|
||||
|
||||
var validationProfiles map[string]*ra.ValidationProfile
|
||||
if c.RA.ValidationProfiles != nil {
|
||||
validationProfiles = make(map[string]*ra.ValidationProfile)
|
||||
for profileName, v := range c.RA.ValidationProfiles {
|
||||
var allowList *allowlist.List[int64]
|
||||
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)
|
||||
// TODO(#7986): Remove this fallback, error out if no profiles are configured.
|
||||
if len(c.RA.ValidationProfiles) == 0 {
|
||||
c.RA.ValidationProfiles = map[string]ra.ValidationProfileConfig{
|
||||
c.RA.DefaultProfileName: {
|
||||
PendingAuthzLifetime: config.Duration{
|
||||
Duration: time.Duration(c.RA.PendingAuthorizationLifetimeDays) * 24 * time.Hour},
|
||||
ValidAuthzLifetime: config.Duration{
|
||||
Duration: time.Duration(c.RA.AuthorizationLifetimeDays) * 24 * time.Hour},
|
||||
OrderLifetime: c.RA.OrderLifetime,
|
||||
// Leave the allowlist empty, so all accounts have access to this
|
||||
// default profile.
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
validationProfiles, err := ra.NewValidationProfiles(c.RA.DefaultProfileName, c.RA.ValidationProfiles)
|
||||
cmd.FailOnError(err, "Failed to load validation profiles")
|
||||
|
||||
var mustStapleAllowList *allowlist.List[int64]
|
||||
if c.RA.MustStapleAllowList != "" {
|
||||
data, err := os.ReadFile(c.RA.MustStapleAllowList)
|
||||
|
@ -331,12 +333,9 @@ func main() {
|
|||
limiter,
|
||||
txnBuilder,
|
||||
c.RA.MaxNames,
|
||||
authorizationLifetime,
|
||||
pendingAuthorizationLifetime,
|
||||
validationProfiles,
|
||||
mustStapleAllowList,
|
||||
pubc,
|
||||
c.RA.OrderLifetime.Duration,
|
||||
c.RA.FinalizeTimeout.Duration,
|
||||
ctp,
|
||||
apc,
|
||||
|
|
346
ra/ra.go
346
ra/ra.go
|
@ -10,6 +10,7 @@ import (
|
|||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
@ -30,6 +31,7 @@ import (
|
|||
akamaipb "github.com/letsencrypt/boulder/akamai/proto"
|
||||
"github.com/letsencrypt/boulder/allowlist"
|
||||
capb "github.com/letsencrypt/boulder/ca/proto"
|
||||
"github.com/letsencrypt/boulder/config"
|
||||
"github.com/letsencrypt/boulder/core"
|
||||
corepb "github.com/letsencrypt/boulder/core/proto"
|
||||
csrlib "github.com/letsencrypt/boulder/csr"
|
||||
|
@ -66,19 +68,6 @@ var (
|
|||
caaRecheckDuration = -7 * time.Hour
|
||||
)
|
||||
|
||||
// ValidationProfile holds the allowlist for a given validation profile.
|
||||
type ValidationProfile struct {
|
||||
// allowList holds the set of account IDs allowed to use this profile. If
|
||||
// nil, the profile is open to all accounts (everyone is allowed).
|
||||
allowList *allowlist.List[int64]
|
||||
}
|
||||
|
||||
// NewValidationProfile creates a new ValidationProfile with the provided
|
||||
// allowList. A nil allowList is interpreted as open access for all accounts.
|
||||
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
|
||||
|
@ -92,21 +81,17 @@ type RegistrationAuthorityImpl struct {
|
|||
PA core.PolicyAuthority
|
||||
publisher pubpb.PublisherClient
|
||||
|
||||
clk clock.Clock
|
||||
log blog.Logger
|
||||
keyPolicy goodkey.KeyPolicy
|
||||
// How long before a newly created authorization expires.
|
||||
authorizationLifetime time.Duration
|
||||
pendingAuthorizationLifetime time.Duration
|
||||
validationProfiles map[string]*ValidationProfile
|
||||
mustStapleAllowList *allowlist.List[int64]
|
||||
maxContactsPerReg int
|
||||
limiter *ratelimits.Limiter
|
||||
txnBuilder *ratelimits.TransactionBuilder
|
||||
maxNames int
|
||||
orderLifetime time.Duration
|
||||
finalizeTimeout time.Duration
|
||||
drainWG sync.WaitGroup
|
||||
clk clock.Clock
|
||||
log blog.Logger
|
||||
keyPolicy goodkey.KeyPolicy
|
||||
profiles *validationProfiles
|
||||
mustStapleAllowList *allowlist.List[int64]
|
||||
maxContactsPerReg int
|
||||
limiter *ratelimits.Limiter
|
||||
txnBuilder *ratelimits.TransactionBuilder
|
||||
maxNames int
|
||||
finalizeTimeout time.Duration
|
||||
drainWG sync.WaitGroup
|
||||
|
||||
issuersByNameID map[issuance.NameID]*issuance.Certificate
|
||||
purger akamaipb.AkamaiPurgerClient
|
||||
|
@ -142,12 +127,9 @@ func NewRegistrationAuthorityImpl(
|
|||
limiter *ratelimits.Limiter,
|
||||
txnBuilder *ratelimits.TransactionBuilder,
|
||||
maxNames int,
|
||||
authorizationLifetime time.Duration,
|
||||
pendingAuthorizationLifetime time.Duration,
|
||||
validationProfiles map[string]*ValidationProfile,
|
||||
profiles *validationProfiles,
|
||||
mustStapleAllowList *allowlist.List[int64],
|
||||
pubc pubpb.PublisherClient,
|
||||
orderLifetime time.Duration,
|
||||
finalizeTimeout time.Duration,
|
||||
ctp *ctpolicy.CTPolicy,
|
||||
purger akamaipb.AkamaiPurgerClient,
|
||||
|
@ -262,40 +244,163 @@ func NewRegistrationAuthorityImpl(
|
|||
}
|
||||
|
||||
ra := &RegistrationAuthorityImpl{
|
||||
clk: clk,
|
||||
log: logger,
|
||||
authorizationLifetime: authorizationLifetime,
|
||||
pendingAuthorizationLifetime: pendingAuthorizationLifetime,
|
||||
validationProfiles: validationProfiles,
|
||||
mustStapleAllowList: mustStapleAllowList,
|
||||
maxContactsPerReg: maxContactsPerReg,
|
||||
keyPolicy: keyPolicy,
|
||||
limiter: limiter,
|
||||
txnBuilder: txnBuilder,
|
||||
maxNames: maxNames,
|
||||
publisher: pubc,
|
||||
orderLifetime: orderLifetime,
|
||||
finalizeTimeout: finalizeTimeout,
|
||||
ctpolicy: ctp,
|
||||
ctpolicyResults: ctpolicyResults,
|
||||
purger: purger,
|
||||
issuersByNameID: issuersByNameID,
|
||||
namesPerCert: namesPerCert,
|
||||
newRegCounter: newRegCounter,
|
||||
recheckCAACounter: recheckCAACounter,
|
||||
newCertCounter: newCertCounter,
|
||||
revocationReasonCounter: revocationReasonCounter,
|
||||
authzAges: authzAges,
|
||||
orderAges: orderAges,
|
||||
inflightFinalizes: inflightFinalizes,
|
||||
certCSRMismatch: certCSRMismatch,
|
||||
pauseCounter: pauseCounter,
|
||||
mustStapleRequestsCounter: mustStapleRequestsCounter,
|
||||
newOrUpdatedContactCounter: newOrUpdatedContactCounter,
|
||||
clk: clk,
|
||||
log: logger,
|
||||
profiles: profiles,
|
||||
mustStapleAllowList: mustStapleAllowList,
|
||||
maxContactsPerReg: maxContactsPerReg,
|
||||
keyPolicy: keyPolicy,
|
||||
limiter: limiter,
|
||||
txnBuilder: txnBuilder,
|
||||
maxNames: maxNames,
|
||||
publisher: pubc,
|
||||
finalizeTimeout: finalizeTimeout,
|
||||
ctpolicy: ctp,
|
||||
ctpolicyResults: ctpolicyResults,
|
||||
purger: purger,
|
||||
issuersByNameID: issuersByNameID,
|
||||
namesPerCert: namesPerCert,
|
||||
newRegCounter: newRegCounter,
|
||||
recheckCAACounter: recheckCAACounter,
|
||||
newCertCounter: newCertCounter,
|
||||
revocationReasonCounter: revocationReasonCounter,
|
||||
authzAges: authzAges,
|
||||
orderAges: orderAges,
|
||||
inflightFinalizes: inflightFinalizes,
|
||||
certCSRMismatch: certCSRMismatch,
|
||||
pauseCounter: pauseCounter,
|
||||
mustStapleRequestsCounter: mustStapleRequestsCounter,
|
||||
newOrUpdatedContactCounter: newOrUpdatedContactCounter,
|
||||
}
|
||||
return ra
|
||||
}
|
||||
|
||||
// UnconfiguredDefaultProfileName is a unique string which the RA can use to
|
||||
// identify a profile, but also detect that no profiles were explicitly
|
||||
// configured, and therefore should not be assumed to exist outside the RA.
|
||||
// TODO(#7986): Remove this when the defaultProfileName config is required.
|
||||
const UnconfiguredDefaultProfileName = "unconfiguredDefaultProfileName"
|
||||
|
||||
// ValidationProfileConfig is a config struct which can be used to create a
|
||||
// ValidationProfile.
|
||||
type ValidationProfileConfig struct {
|
||||
// PendingAuthzLifetime defines how far in the future an authorization's
|
||||
// "expires" timestamp is set when it is first created, i.e. how much
|
||||
// time the applicant has to attempt the challenge.
|
||||
PendingAuthzLifetime config.Duration `validate:"required"`
|
||||
// ValidAuthzLifetime defines how far in the future an authorization's
|
||||
// "expires" timestamp is set when one of its challenges is fulfilled,
|
||||
// i.e. how long a validated authorization may be reused.
|
||||
ValidAuthzLifetime config.Duration `validate:"required"`
|
||||
// OrderLifetime defines how far in the future an order's "expires"
|
||||
// timestamp is set when it is first created, i.e. how much time the
|
||||
// applicant has to fulfill all challenges and finalize the order. This is
|
||||
// a maximum time: if the order reuses an authorization and that authz
|
||||
// expires earlier than this OrderLifetime would otherwise set, then the
|
||||
// order's expiration is brought in to match that authorization.
|
||||
OrderLifetime config.Duration `validate:"required"`
|
||||
// AllowList specifies the path to a YAML file containing a list of
|
||||
// account IDs permitted to use this profile. If no path is
|
||||
// specified, the profile is open to all accounts. If the file
|
||||
// exists but is empty, the profile is closed to all accounts.
|
||||
AllowList string `validate:"omitempty"`
|
||||
}
|
||||
|
||||
// validationProfile holds the order and authz lifetimes and allowlist for a
|
||||
// given validation profile.
|
||||
type validationProfile struct {
|
||||
// PendingAuthzLifetime defines how far in the future an authorization's
|
||||
// "expires" timestamp is set when it is first created, i.e. how much
|
||||
// time the applicant has to attempt the challenge.
|
||||
pendingAuthzLifetime time.Duration
|
||||
// ValidAuthzLifetime defines how far in the future an authorization's
|
||||
// "expires" timestamp is set when one of its challenges is fulfilled,
|
||||
// i.e. how long a validated authorization may be reused.
|
||||
validAuthzLifetime time.Duration
|
||||
// OrderLifetime defines how far in the future an order's "expires"
|
||||
// timestamp is set when it is first created, i.e. how much time the
|
||||
// applicant has to fulfill all challenges and finalize the order. This is
|
||||
// a maximum time: if the order reuses an authorization and that authz
|
||||
// expires earlier than this OrderLifetime would otherwise set, then the
|
||||
// order's expiration is brought in to match that authorization.
|
||||
orderLifetime time.Duration
|
||||
// allowList holds the set of account IDs allowed to use this profile. If
|
||||
// nil, the profile is open to all accounts (everyone is allowed).
|
||||
allowList *allowlist.List[int64]
|
||||
}
|
||||
|
||||
// validationProfiles provides access to the set of configured profiles,
|
||||
// including the default profile for orders/authzs which do not specify one.
|
||||
type validationProfiles struct {
|
||||
defaultName string
|
||||
byName map[string]*validationProfile
|
||||
}
|
||||
|
||||
// NewValidationProfiles builds a new validationProfiles struct from the given
|
||||
// configs and default name. It enforces that the given authorization lifetimes
|
||||
// are within the bounds mandated by the Baseline Requirements.
|
||||
func NewValidationProfiles(defaultName string, configs map[string]ValidationProfileConfig) (*validationProfiles, error) {
|
||||
profiles := make(map[string]*validationProfile, len(configs))
|
||||
|
||||
for name, config := range configs {
|
||||
// The Baseline Requirements v1.8.1 state that validation tokens "MUST
|
||||
// NOT be used for more than 30 days from its creation". If unconfigured
|
||||
// or the configured value pendingAuthorizationLifetimeDays is greater
|
||||
// than 29 days, bail out.
|
||||
if config.PendingAuthzLifetime.Duration <= 0 || config.PendingAuthzLifetime.Duration > 29*(24*time.Hour) {
|
||||
return nil, fmt.Errorf("PendingAuthzLifetime value must be greater than 0 and less than 30d, but got %q", config.PendingAuthzLifetime.Duration)
|
||||
}
|
||||
|
||||
// Baseline Requirements v1.8.1 section 4.2.1: "any reused data, document,
|
||||
// or completed validation MUST be obtained no more than 398 days prior
|
||||
// to issuing the Certificate". If unconfigured or the configured value is
|
||||
// greater than 397 days, bail out.
|
||||
if config.ValidAuthzLifetime.Duration <= 0 || config.ValidAuthzLifetime.Duration > 397*(24*time.Hour) {
|
||||
return nil, fmt.Errorf("ValidAuthzLifetime value must be greater than 0 and less than 398d, but got %q", config.ValidAuthzLifetime.Duration)
|
||||
}
|
||||
|
||||
var allowList *allowlist.List[int64]
|
||||
if config.AllowList != "" {
|
||||
data, err := os.ReadFile(config.AllowList)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading allowlist: %w", err)
|
||||
}
|
||||
allowList, err = allowlist.NewFromYAML[int64](data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing allowlist: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
profiles[name] = &validationProfile{
|
||||
pendingAuthzLifetime: config.PendingAuthzLifetime.Duration,
|
||||
validAuthzLifetime: config.ValidAuthzLifetime.Duration,
|
||||
orderLifetime: config.OrderLifetime.Duration,
|
||||
allowList: allowList,
|
||||
}
|
||||
}
|
||||
|
||||
_, ok := profiles[defaultName]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no profile configured matching default profile name %q", defaultName)
|
||||
}
|
||||
|
||||
return &validationProfiles{
|
||||
defaultName: defaultName,
|
||||
byName: profiles,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (vp *validationProfiles) get(name string) (*validationProfile, error) {
|
||||
if name == "" {
|
||||
name = vp.defaultName
|
||||
}
|
||||
profile, ok := vp.byName[name]
|
||||
if !ok {
|
||||
return nil, berrors.InvalidProfileError("unrecognized profile name %q", name)
|
||||
}
|
||||
return profile, nil
|
||||
}
|
||||
|
||||
// certificateRequestAuthz is a struct for holding information about a valid
|
||||
// authz referenced during a certificateRequestEvent. It holds both the
|
||||
// authorization ID and the challenge type that made the authorization valid. We
|
||||
|
@ -961,6 +1066,11 @@ func (ra *RegistrationAuthorityImpl) validateFinalizeRequest(
|
|||
req.Order.Status)
|
||||
}
|
||||
|
||||
profile, err := ra.profiles.get(req.Order.CertificateProfileName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// There should never be an order with 0 names at the stage, but we check to
|
||||
// be on the safe side, throwing an internal server error if this assumption
|
||||
// is ever violated.
|
||||
|
@ -1040,7 +1150,7 @@ func (ra *RegistrationAuthorityImpl) validateFinalizeRequest(
|
|||
ID: authz.ID,
|
||||
ChallengeType: solvedByChallengeType,
|
||||
}
|
||||
authzAge := (ra.authorizationLifetime - authz.Expires.Sub(ra.clk.Now())).Seconds()
|
||||
authzAge := (profile.validAuthzLifetime - authz.Expires.Sub(ra.clk.Now())).Seconds()
|
||||
ra.authzAges.WithLabelValues("FinalizeOrder", string(authz.Status)).Observe(authzAge)
|
||||
}
|
||||
logEvent.Authorizations = logEventAuthzs
|
||||
|
@ -1078,9 +1188,18 @@ func (ra *RegistrationAuthorityImpl) issueCertificateOuter(
|
|||
logEvent.PreviousCertificateIssued = timestamps.Timestamps[0].AsTime()
|
||||
}
|
||||
|
||||
// If the order didn't request a specific profile and we have a default
|
||||
// configured, provide it to the CA so we can stop relying on the CA's
|
||||
// configured default.
|
||||
// TODO(#7309): Make this unconditional.
|
||||
profileName := order.CertificateProfileName
|
||||
if profileName == "" && ra.profiles.defaultName != UnconfiguredDefaultProfileName {
|
||||
profileName = ra.profiles.defaultName
|
||||
}
|
||||
|
||||
// Step 3: Issue the Certificate
|
||||
cert, cpId, err := ra.issueCertificateInner(
|
||||
ctx, csr, isRenewal, order.CertificateProfileName, accountID(order.RegistrationID), orderID(order.Id))
|
||||
ctx, csr, isRenewal, profileName, accountID(order.RegistrationID), orderID(order.Id))
|
||||
|
||||
// Step 4: Fail the order if necessary, and update metrics and log fields
|
||||
var result string
|
||||
|
@ -1328,17 +1447,11 @@ func (ra *RegistrationAuthorityImpl) UpdateRegistrationKey(ctx context.Context,
|
|||
|
||||
// recordValidation records an authorization validation event,
|
||||
// it should only be used on v2 style authorizations.
|
||||
func (ra *RegistrationAuthorityImpl) recordValidation(ctx context.Context, authID string, authExpires *time.Time, challenge *core.Challenge) error {
|
||||
func (ra *RegistrationAuthorityImpl) recordValidation(ctx context.Context, authID string, authExpires time.Time, challenge *core.Challenge) error {
|
||||
authzID, err := strconv.ParseInt(authID, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var expires time.Time
|
||||
if challenge.Status == core.StatusInvalid {
|
||||
expires = *authExpires
|
||||
} else {
|
||||
expires = ra.clk.Now().Add(ra.authorizationLifetime)
|
||||
}
|
||||
vr, err := bgrpc.ValidationResultToPB(challenge.ValidationRecord, challenge.Error, "", "")
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -1350,7 +1463,7 @@ func (ra *RegistrationAuthorityImpl) recordValidation(ctx context.Context, authI
|
|||
_, err = ra.SA.FinalizeAuthorization2(ctx, &sapb.FinalizeAuthorizationRequest{
|
||||
Id: authzID,
|
||||
Status: string(challenge.Status),
|
||||
Expires: timestamppb.New(expires),
|
||||
Expires: timestamppb.New(authExpires),
|
||||
Attempted: string(challenge.Type),
|
||||
AttemptedAt: validated,
|
||||
ValidationRecords: vr.Records,
|
||||
|
@ -1472,6 +1585,11 @@ func (ra *RegistrationAuthorityImpl) PerformValidation(
|
|||
return nil, berrors.MalformedError("expired authorization")
|
||||
}
|
||||
|
||||
profile, err := ra.profiles.get(authz.CertificateProfileName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
challIndex := int(req.ChallengeIndex)
|
||||
if challIndex >= len(authz.Challenges) {
|
||||
return nil,
|
||||
|
@ -1573,6 +1691,7 @@ func (ra *RegistrationAuthorityImpl) PerformValidation(
|
|||
prob = probs.ServerInternal("Records for validation failed sanity check")
|
||||
}
|
||||
|
||||
expires := *authz.Expires
|
||||
if prob != nil {
|
||||
challenge.Status = core.StatusInvalid
|
||||
challenge.Error = prob
|
||||
|
@ -1582,6 +1701,7 @@ func (ra *RegistrationAuthorityImpl) PerformValidation(
|
|||
}
|
||||
} else {
|
||||
challenge.Status = core.StatusValid
|
||||
expires = ra.clk.Now().Add(profile.validAuthzLifetime)
|
||||
if features.Get().AutomaticallyPauseZombieClients {
|
||||
ra.resetAccountPausingLimit(vaCtx, authz.RegistrationID, authz.Identifier)
|
||||
}
|
||||
|
@ -1589,7 +1709,7 @@ func (ra *RegistrationAuthorityImpl) PerformValidation(
|
|||
challenge.Validated = &vStart
|
||||
authz.Challenges[challIndex] = *challenge
|
||||
|
||||
err = ra.recordValidation(vaCtx, authz.ID, authz.Expires, challenge)
|
||||
err = ra.recordValidation(vaCtx, authz.ID, expires, challenge)
|
||||
if err != nil {
|
||||
if errors.Is(err, berrors.NotFound) {
|
||||
// We log NotFound at a lower level because this is largely due to a
|
||||
|
@ -2186,23 +2306,20 @@ func (ra *RegistrationAuthorityImpl) NewOrder(ctx context.Context, req *rapb.New
|
|||
"Order cannot contain more than %d DNS names", ra.maxNames)
|
||||
}
|
||||
|
||||
if req.CertificateProfileName != "" && ra.validationProfiles != nil {
|
||||
vp, ok := ra.validationProfiles[req.CertificateProfileName]
|
||||
if !ok {
|
||||
return nil, berrors.MalformedError("requested certificate profile %q not found",
|
||||
req.CertificateProfileName,
|
||||
)
|
||||
}
|
||||
if vp.allowList != nil && !vp.allowList.Contains(req.RegistrationID) {
|
||||
return nil, berrors.UnauthorizedError("account ID %d is not permitted to use certificate profile %q",
|
||||
req.RegistrationID,
|
||||
req.CertificateProfileName,
|
||||
)
|
||||
}
|
||||
profile, err := ra.profiles.get(req.CertificateProfileName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if profile.allowList != nil && !profile.allowList.Contains(req.RegistrationID) {
|
||||
return nil, berrors.UnauthorizedError("account ID %d is not permitted to use certificate 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)
|
||||
err = ra.PA.WillingToIssue(newOrder.DnsNames)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -2290,12 +2407,19 @@ func (ra *RegistrationAuthorityImpl) NewOrder(ctx context.Context, req *rapb.New
|
|||
missingAuthzIdents = append(missingAuthzIdents, ident)
|
||||
continue
|
||||
}
|
||||
|
||||
// If the authz is associated with the wrong profile, don't reuse it.
|
||||
if authz.CertificateProfileName != req.CertificateProfileName {
|
||||
missingAuthzIdents = append(missingAuthzIdents, ident)
|
||||
continue
|
||||
}
|
||||
authzAge := (ra.authorizationLifetime - authz.Expires.Sub(ra.clk.Now())).Seconds()
|
||||
|
||||
// This is only used for our metrics.
|
||||
authzAge := (profile.validAuthzLifetime - authz.Expires.Sub(ra.clk.Now())).Seconds()
|
||||
if authz.Status == core.StatusPending {
|
||||
authzAge = (profile.pendingAuthzLifetime - authz.Expires.Sub(ra.clk.Now())).Seconds()
|
||||
}
|
||||
|
||||
// If the identifier is a wildcard and the existing authz only has one
|
||||
// DNS-01 type challenge we can reuse it. In theory we will
|
||||
// never get back an authorization for a domain with a wildcard prefix
|
||||
|
@ -2332,17 +2456,30 @@ func (ra *RegistrationAuthorityImpl) NewOrder(ctx context.Context, req *rapb.New
|
|||
// authorization for each.
|
||||
var newAuthzs []*sapb.NewAuthzRequest
|
||||
for _, ident := range missingAuthzIdents {
|
||||
pb, err := ra.createPendingAuthz(newOrder.RegistrationID, ident)
|
||||
challTypes, err := ra.PA.ChallengeTypesFor(ident)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newAuthzs = append(newAuthzs, pb)
|
||||
|
||||
var challStrs []string
|
||||
for _, t := range challTypes {
|
||||
challStrs = append(challStrs, string(t))
|
||||
}
|
||||
|
||||
newAuthzs = append(newAuthzs, &sapb.NewAuthzRequest{
|
||||
Identifier: ident.AsProto(),
|
||||
RegistrationID: newOrder.RegistrationID,
|
||||
Expires: timestamppb.New(ra.clk.Now().Add(profile.pendingAuthzLifetime).Truncate(time.Second)),
|
||||
ChallengeTypes: challStrs,
|
||||
Token: core.NewToken(),
|
||||
})
|
||||
|
||||
ra.authzAges.WithLabelValues("NewOrder", string(core.StatusPending)).Observe(0)
|
||||
}
|
||||
|
||||
// Start with the order's own expiry as the minExpiry. We only care
|
||||
// about authz expiries that are sooner than the order's expiry
|
||||
minExpiry := ra.clk.Now().Add(ra.orderLifetime)
|
||||
minExpiry := ra.clk.Now().Add(profile.orderLifetime)
|
||||
|
||||
// Check the reused authorizations to see if any have an expiry before the
|
||||
// minExpiry (the order's lifetime)
|
||||
|
@ -2362,7 +2499,7 @@ func (ra *RegistrationAuthorityImpl) NewOrder(ctx context.Context, req *rapb.New
|
|||
// If the newly created pending authz's have an expiry closer than the
|
||||
// minExpiry the minExpiry is the pending authz expiry.
|
||||
if len(newAuthzs) > 0 {
|
||||
newPendingAuthzExpires := ra.clk.Now().Add(ra.pendingAuthorizationLifetime)
|
||||
newPendingAuthzExpires := ra.clk.Now().Add(profile.pendingAuthzLifetime)
|
||||
if newPendingAuthzExpires.Before(minExpiry) {
|
||||
minExpiry = newPendingAuthzExpires
|
||||
}
|
||||
|
@ -2391,31 +2528,6 @@ func (ra *RegistrationAuthorityImpl) NewOrder(ctx context.Context, req *rapb.New
|
|||
return storedOrder, nil
|
||||
}
|
||||
|
||||
// createPendingAuthz checks that a name is allowed for issuance and creates the
|
||||
// necessary challenges for it and puts this and all of the relevant information
|
||||
// into a corepb.Authorization for transmission to the SA to be stored
|
||||
func (ra *RegistrationAuthorityImpl) createPendingAuthz(reg int64, ident identifier.ACMEIdentifier) (*sapb.NewAuthzRequest, error) {
|
||||
challTypes, err := ra.PA.ChallengeTypesFor(ident)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
challStrs := make([]string, len(challTypes))
|
||||
for i, t := range challTypes {
|
||||
challStrs[i] = string(t)
|
||||
}
|
||||
|
||||
authz := &sapb.NewAuthzRequest{
|
||||
Identifier: ident.AsProto(),
|
||||
RegistrationID: reg,
|
||||
Expires: timestamppb.New(ra.clk.Now().Add(ra.pendingAuthorizationLifetime).Truncate(time.Second)),
|
||||
ChallengeTypes: challStrs,
|
||||
Token: core.NewToken(),
|
||||
}
|
||||
|
||||
return authz, nil
|
||||
}
|
||||
|
||||
// wildcardOverlap takes a slice of domain names and returns an error if any of
|
||||
// them is a non-wildcard FQDN that overlaps with a wildcard domain in the map.
|
||||
func wildcardOverlap(dnsNames []string) error {
|
||||
|
|
270
ra/ra_test.go
270
ra/ra_test.go
|
@ -10,6 +10,7 @@ import (
|
|||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
|
@ -155,6 +156,13 @@ func numAuthorizations(o *corepb.Order) int {
|
|||
return len(o.V2Authorizations)
|
||||
}
|
||||
|
||||
// def is a test-only helper that returns the default validation profile
|
||||
// and is guaranteed to succeed because the validationProfile constructor
|
||||
// ensures that the default name has a corresponding profile.
|
||||
func (vp *validationProfiles) def() *validationProfile {
|
||||
return vp.byName[vp.defaultName]
|
||||
}
|
||||
|
||||
type DummyValidationAuthority struct {
|
||||
doDCVRequest chan *vapb.PerformValidationRequest
|
||||
doDCVError error
|
||||
|
@ -340,15 +348,19 @@ func initAuthorities(t *testing.T) (*DummyValidationAuthority, sapb.StorageAutho
|
|||
testKeyPolicy, err := goodkey.NewPolicy(nil, nil)
|
||||
test.AssertNotError(t, err, "making keypolicy")
|
||||
|
||||
profiles := &validationProfiles{
|
||||
defaultName: "test",
|
||||
byName: map[string]*validationProfile{"test": {
|
||||
pendingAuthzLifetime: 7 * 24 * time.Hour,
|
||||
validAuthzLifetime: 300 * 24 * time.Hour,
|
||||
orderLifetime: 7 * 24 * time.Hour,
|
||||
}},
|
||||
}
|
||||
|
||||
ra := NewRegistrationAuthorityImpl(
|
||||
fc, log, stats,
|
||||
1, testKeyPolicy, limiter, txnBuilder, 100,
|
||||
300*24*time.Hour, 7*24*time.Hour,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
7*24*time.Hour, 5*time.Minute,
|
||||
ctp, nil, nil)
|
||||
profiles, nil, nil, 5*time.Minute, ctp, nil, nil)
|
||||
ra.SA = sa
|
||||
ra.VA = va
|
||||
ra.CA = ca
|
||||
|
@ -633,7 +645,7 @@ func TestPerformValidationSuccess(t *testing.T) {
|
|||
|
||||
// The DB authz's expiry should be equal to the current time plus the
|
||||
// configured authorization lifetime
|
||||
test.AssertEquals(t, dbAuthzPB.Expires.AsTime(), now.Add(ra.authorizationLifetime))
|
||||
test.AssertEquals(t, dbAuthzPB.Expires.AsTime(), now.Add(ra.profiles.def().validAuthzLifetime))
|
||||
|
||||
// Check that validated timestamp was recorded, stored, and retrieved
|
||||
expectedValidated := fc.Now()
|
||||
|
@ -1053,7 +1065,7 @@ func TestRecheckCAADates(t *testing.T) {
|
|||
defer cleanUp()
|
||||
recorder := &caaRecorder{names: make(map[string]bool)}
|
||||
ra.VA = va.RemoteClients{CAAClient: recorder}
|
||||
ra.authorizationLifetime = 15 * time.Hour
|
||||
ra.profiles.def().validAuthzLifetime = 15 * time.Hour
|
||||
|
||||
recentValidated := fc.Now().Add(-1 * time.Hour)
|
||||
recentExpires := fc.Now().Add(15 * time.Hour)
|
||||
|
@ -1363,7 +1375,7 @@ func TestNewOrder(t *testing.T) {
|
|||
})
|
||||
test.AssertNotError(t, err, "ra.NewOrder failed")
|
||||
test.AssertEquals(t, orderA.RegistrationID, int64(1))
|
||||
test.AssertEquals(t, orderA.Expires.AsTime(), now.Add(ra.orderLifetime))
|
||||
test.AssertEquals(t, orderA.Expires.AsTime(), now.Add(ra.profiles.def().orderLifetime))
|
||||
test.AssertEquals(t, len(orderA.DnsNames), 3)
|
||||
test.AssertEquals(t, orderA.CertificateProfileName, "test")
|
||||
// We expect the order names to have been sorted, deduped, and lowercased
|
||||
|
@ -1403,6 +1415,9 @@ func TestNewOrder_OrderReuse(t *testing.T) {
|
|||
secondReg, err := ra.NewRegistration(context.Background(), input)
|
||||
test.AssertNotError(t, err, "Error creating a second test registration")
|
||||
|
||||
// Insert a second (albeit identical) profile to reference
|
||||
ra.profiles.byName["different"] = ra.profiles.def()
|
||||
|
||||
testCases := []struct {
|
||||
Name string
|
||||
RegistrationID int64
|
||||
|
@ -1492,7 +1507,7 @@ func TestNewOrder_OrderReuse_Expired(t *testing.T) {
|
|||
defer cleanUp()
|
||||
|
||||
// Set the order lifetime to something short and known.
|
||||
ra.orderLifetime = time.Hour
|
||||
ra.profiles.def().orderLifetime = time.Hour
|
||||
|
||||
// Create an initial order.
|
||||
extant, err := ra.NewOrder(context.Background(), &rapb.NewOrderRequest{
|
||||
|
@ -1676,48 +1691,115 @@ func TestNewOrder_AuthzReuse_NoPending(t *testing.T) {
|
|||
test.AssertNotEquals(t, new.V2Authorizations[0], extant.V2Authorizations[0])
|
||||
}
|
||||
|
||||
func TestNewOrder_ValidationProfiles(t *testing.T) {
|
||||
_, _, ra, _, _, cleanUp := initAuthorities(t)
|
||||
defer cleanUp()
|
||||
|
||||
ra.profiles = &validationProfiles{
|
||||
defaultName: "one",
|
||||
byName: map[string]*validationProfile{
|
||||
"one": {
|
||||
pendingAuthzLifetime: 1 * 24 * time.Hour,
|
||||
validAuthzLifetime: 1 * 24 * time.Hour,
|
||||
orderLifetime: 1 * 24 * time.Hour,
|
||||
},
|
||||
"two": {
|
||||
pendingAuthzLifetime: 2 * 24 * time.Hour,
|
||||
validAuthzLifetime: 2 * 24 * time.Hour,
|
||||
orderLifetime: 2 * 24 * time.Hour,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
profile string
|
||||
wantExpires time.Time
|
||||
}{
|
||||
{
|
||||
// A request with no profile should get an order and authzs with one-day lifetimes.
|
||||
name: "no profile specified",
|
||||
profile: "",
|
||||
wantExpires: ra.clk.Now().Add(1 * 24 * time.Hour),
|
||||
},
|
||||
{
|
||||
// A request for profile one should get an order and authzs with one-day lifetimes.
|
||||
name: "profile one",
|
||||
profile: "one",
|
||||
wantExpires: ra.clk.Now().Add(1 * 24 * time.Hour),
|
||||
},
|
||||
{
|
||||
// A request for profile two should get an order and authzs with one-day lifetimes.
|
||||
name: "profile two",
|
||||
profile: "two",
|
||||
wantExpires: ra.clk.Now().Add(2 * 24 * time.Hour),
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
order, err := ra.NewOrder(context.Background(), &rapb.NewOrderRequest{
|
||||
RegistrationID: Registration.Id,
|
||||
DnsNames: []string{randomDomain()},
|
||||
CertificateProfileName: tc.profile,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("creating order: %s", err)
|
||||
}
|
||||
gotExpires := order.Expires.AsTime()
|
||||
if gotExpires != tc.wantExpires {
|
||||
t.Errorf("NewOrder(profile: %q).Expires = %s, expected %s", tc.profile, gotExpires, tc.wantExpires)
|
||||
}
|
||||
|
||||
authz, err := ra.GetAuthorization(context.Background(), &rapb.GetAuthorizationRequest{
|
||||
Id: order.V2Authorizations[0],
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("fetching test authz: %s", err)
|
||||
}
|
||||
gotExpires = authz.Expires.AsTime()
|
||||
if gotExpires != tc.wantExpires {
|
||||
t.Errorf("GetAuthorization(profile: %q).Expires = %s, expected %s", tc.profile, gotExpires, tc.wantExpires)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewOrder_ProfileSelectionAllowList(t *testing.T) {
|
||||
_, _, ra, _, _, cleanUp := initAuthorities(t)
|
||||
defer cleanUp()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
validationProfiles map[string]*ValidationProfile
|
||||
validationProfiles map[string]*validationProfile
|
||||
expectErr bool
|
||||
expectErrContains string
|
||||
}{
|
||||
{
|
||||
name: "Allow all account IDs regardless of profile",
|
||||
validationProfiles: nil,
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "Allow all account IDs for this specific profile",
|
||||
validationProfiles: map[string]*ValidationProfile{
|
||||
"test": NewValidationProfile(nil),
|
||||
name: "Allow all account IDs",
|
||||
validationProfiles: map[string]*validationProfile{
|
||||
"test": {allowList: nil},
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "Deny all but account Id 1337",
|
||||
validationProfiles: map[string]*ValidationProfile{
|
||||
"test": NewValidationProfile(allowlist.NewList([]int64{1337})),
|
||||
validationProfiles: map[string]*validationProfile{
|
||||
"test": {allowList: allowlist.NewList([]int64{1337})},
|
||||
},
|
||||
expectErr: true,
|
||||
expectErrContains: "not permitted to use certificate profile",
|
||||
},
|
||||
{
|
||||
name: "Deny all",
|
||||
validationProfiles: map[string]*ValidationProfile{
|
||||
"test": NewValidationProfile(allowlist.NewList([]int64{})),
|
||||
validationProfiles: map[string]*validationProfile{
|
||||
"test": {allowList: allowlist.NewList([]int64{})},
|
||||
},
|
||||
expectErr: true,
|
||||
expectErrContains: "not permitted to use certificate profile",
|
||||
},
|
||||
{
|
||||
name: "Allow Registration.Id",
|
||||
validationProfiles: map[string]*ValidationProfile{
|
||||
"test": NewValidationProfile(allowlist.NewList([]int64{Registration.Id})),
|
||||
validationProfiles: map[string]*validationProfile{
|
||||
"test": {allowList: allowlist.NewList([]int64{Registration.Id})},
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
|
@ -1725,7 +1807,7 @@ func TestNewOrder_ProfileSelectionAllowList(t *testing.T) {
|
|||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ra.validationProfiles = tc.validationProfiles
|
||||
ra.profiles.byName = tc.validationProfiles
|
||||
|
||||
orderReq := &rapb.NewOrderRequest{
|
||||
RegistrationID: Registration.Id,
|
||||
|
@ -2069,7 +2151,7 @@ func TestNewOrderExpiry(t *testing.T) {
|
|||
names := []string{"zombo.com"}
|
||||
|
||||
// Set the order lifetime to 48 hours.
|
||||
ra.orderLifetime = 48 * time.Hour
|
||||
ra.profiles.def().orderLifetime = 48 * time.Hour
|
||||
|
||||
// Use an expiry that is sooner than the configured order expiry but greater
|
||||
// than 24 hours away.
|
||||
|
@ -2115,8 +2197,8 @@ func TestNewOrderExpiry(t *testing.T) {
|
|||
test.AssertEquals(t, order.Expires.AsTime(), fakeAuthzExpires)
|
||||
|
||||
// Set the order lifetime to be lower than the fakeAuthzLifetime
|
||||
ra.orderLifetime = 12 * time.Hour
|
||||
expectedOrderExpiry := clk.Now().Add(ra.orderLifetime)
|
||||
ra.profiles.def().orderLifetime = 12 * time.Hour
|
||||
expectedOrderExpiry := clk.Now().Add(12 * time.Hour)
|
||||
// Create the order again
|
||||
order, err = ra.NewOrder(ctx, orderReq)
|
||||
// It shouldn't fail
|
||||
|
@ -2751,7 +2833,7 @@ func TestIssueCertificateAuditLog(t *testing.T) {
|
|||
|
||||
// Make some valid authorizations for some names using different challenge types
|
||||
names := []string{"not-example.com", "www.not-example.com", "still.not-example.com", "definitely.not-example.com"}
|
||||
exp := ra.clk.Now().Add(ra.orderLifetime)
|
||||
exp := ra.clk.Now().Add(ra.profiles.def().orderLifetime)
|
||||
challs := []core.AcmeChallenge{core.ChallengeTypeHTTP01, core.ChallengeTypeDNS01, core.ChallengeTypeHTTP01, core.ChallengeTypeDNS01}
|
||||
var authzIDs []int64
|
||||
for i, name := range names {
|
||||
|
@ -2984,11 +3066,11 @@ func TestUpdateMissingAuthorization(t *testing.T) {
|
|||
|
||||
// Twiddle the authz to pretend its been validated by the VA
|
||||
authz.Challenges[0].Status = "valid"
|
||||
err = ra.recordValidation(ctx, authz.ID, authz.Expires, &authz.Challenges[0])
|
||||
err = ra.recordValidation(ctx, authz.ID, fc.Now().Add(24*time.Hour), &authz.Challenges[0])
|
||||
test.AssertNotError(t, err, "ra.recordValidation failed")
|
||||
|
||||
// Try to record the same validation a second time.
|
||||
err = ra.recordValidation(ctx, authz.ID, authz.Expires, &authz.Challenges[0])
|
||||
err = ra.recordValidation(ctx, authz.ID, fc.Now().Add(25*time.Hour), &authz.Challenges[0])
|
||||
test.AssertError(t, err, "ra.recordValidation didn't fail")
|
||||
test.AssertErrorIs(t, err, berrors.NotFound)
|
||||
}
|
||||
|
@ -3177,7 +3259,7 @@ func TestIssueCertificateInnerErrs(t *testing.T) {
|
|||
|
||||
// Make some valid authorizations for some names
|
||||
names := []string{"not-example.com", "www.not-example.com", "still.not-example.com", "definitely.not-example.com"}
|
||||
exp := ra.clk.Now().Add(ra.orderLifetime)
|
||||
exp := ra.clk.Now().Add(ra.profiles.def().orderLifetime)
|
||||
var authzIDs []int64
|
||||
for _, name := range names {
|
||||
authzIDs = append(authzIDs, createFinalizedAuthorization(t, sa, name, exp, core.ChallengeTypeHTTP01, ra.clk.Now()))
|
||||
|
@ -3306,11 +3388,12 @@ func (sa *mockSAWithFinalize) FQDNSetTimestampsForWindow(ctx context.Context, in
|
|||
}, nil
|
||||
}
|
||||
|
||||
func TestIssueCertificateInnerWithProfile(t *testing.T) {
|
||||
func TestIssueCertificateOuter(t *testing.T) {
|
||||
_, _, ra, _, fc, cleanup := initAuthorities(t)
|
||||
defer cleanup()
|
||||
ra.SA = &mockSAWithFinalize{}
|
||||
|
||||
// Generate a reasonable-looking CSR and cert to pass the matchesCSR check.
|
||||
// Create a CSR to submit and a certificate for the fake CA to return.
|
||||
testKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
test.AssertNotError(t, err, "generating test key")
|
||||
csrDER, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{DNSNames: []string{"example.com"}}, testKey)
|
||||
|
@ -3327,71 +3410,68 @@ func TestIssueCertificateInnerWithProfile(t *testing.T) {
|
|||
test.AssertNotError(t, err, "creating test cert")
|
||||
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
|
||||
|
||||
// Use a mock CA that will record the profile name and profile hash included
|
||||
// in the RA's request messages. Populate it with the cert generated above.
|
||||
mockCA := MockCARecordingProfile{inner: &mocks.MockCA{PEM: certPEM}}
|
||||
ra.CA = &mockCA
|
||||
|
||||
ra.SA = &mockSAWithFinalize{}
|
||||
|
||||
// Call issueCertificateInner with the CSR generated above and the profile
|
||||
// name "default", which will cause the mockCA to return a specific hash.
|
||||
_, cpId, err := ra.issueCertificateInner(context.Background(), csr, false, "default", 1, 1)
|
||||
test.AssertNotError(t, err, "issuing cert with profile name")
|
||||
test.AssertEquals(t, mockCA.profileName, cpId.name)
|
||||
test.AssertByteEquals(t, mockCA.profileHash, cpId.hash)
|
||||
}
|
||||
|
||||
func TestIssueCertificateOuter(t *testing.T) {
|
||||
_, sa, ra, _, fc, cleanup := initAuthorities(t)
|
||||
defer cleanup()
|
||||
|
||||
// Make some valid authorizations for some names
|
||||
names := []string{"not-example.com", "www.not-example.com", "still.not-example.com", "definitely.not-example.com"}
|
||||
exp := ra.clk.Now().Add(ra.orderLifetime)
|
||||
var authzIDs []int64
|
||||
for _, name := range names {
|
||||
authzIDs = append(authzIDs, createFinalizedAuthorization(t, sa, name, exp, core.ChallengeTypeHTTP01, ra.clk.Now()))
|
||||
}
|
||||
|
||||
// Create a pending order for all of the names
|
||||
order, err := sa.NewOrderAndAuthzs(context.Background(), &sapb.NewOrderAndAuthzsRequest{
|
||||
NewOrder: &sapb.NewOrderRequest{
|
||||
RegistrationID: Registration.Id,
|
||||
Expires: timestamppb.New(exp),
|
||||
DnsNames: names,
|
||||
V2Authorizations: authzIDs,
|
||||
CertificateProfileName: "philsProfile",
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
profile string
|
||||
wantProfile string
|
||||
wantHash string
|
||||
}{
|
||||
{
|
||||
name: "select default profile when none specified",
|
||||
wantProfile: "test", // matches ra.defaultProfileName
|
||||
wantHash: "9f86d081884c7d65",
|
||||
},
|
||||
})
|
||||
test.AssertNotError(t, err, "Could not add test order with finalized authz IDs")
|
||||
{
|
||||
name: "default profile specified",
|
||||
profile: "test",
|
||||
wantProfile: "test",
|
||||
wantHash: "9f86d081884c7d65",
|
||||
},
|
||||
{
|
||||
name: "other profile specified",
|
||||
profile: "other",
|
||||
wantProfile: "other",
|
||||
wantHash: "d9298a10d1b07358",
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Use a mock CA that will record the profile name and profile hash included
|
||||
// in the RA's request messages. Populate it with the cert generated above.
|
||||
mockCA := MockCARecordingProfile{inner: &mocks.MockCA{PEM: certPEM}}
|
||||
ra.CA = &mockCA
|
||||
|
||||
testKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
test.AssertNotError(t, err, "generating test key")
|
||||
csrDER, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{DNSNames: []string{"example.com"}}, testKey)
|
||||
test.AssertNotError(t, err, "creating test csr")
|
||||
csr, err := x509.ParseCertificateRequest(csrDER)
|
||||
test.AssertNotError(t, err, "parsing test csr")
|
||||
certDER, err := x509.CreateCertificate(rand.Reader, &x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
DNSNames: []string{"example.com"},
|
||||
NotBefore: fc.Now(),
|
||||
BasicConstraintsValid: true,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
|
||||
}, &x509.Certificate{}, testKey.Public(), testKey)
|
||||
test.AssertNotError(t, err, "creating test cert")
|
||||
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
|
||||
order := &corepb.Order{
|
||||
RegistrationID: Registration.Id,
|
||||
Expires: timestamppb.New(fc.Now().Add(24 * time.Hour)),
|
||||
DnsNames: []string{"example.com"},
|
||||
CertificateProfileName: tc.profile,
|
||||
}
|
||||
|
||||
// Use a mock CA that will record the profile name and profile hash included
|
||||
// in the RA's request messages. Populate it with the cert generated above.
|
||||
mockCA := MockCARecordingProfile{inner: &mocks.MockCA{PEM: certPEM}}
|
||||
ra.CA = &mockCA
|
||||
order, err = ra.issueCertificateOuter(context.Background(), order, csr, certificateRequestEvent{})
|
||||
|
||||
ra.SA = &mockSAWithFinalize{}
|
||||
// The resulting order should have new fields populated
|
||||
if order.Status != string(core.StatusValid) {
|
||||
t.Errorf("order.Status = %+v, want %+v", order.Status, core.StatusValid)
|
||||
}
|
||||
if order.CertificateSerial != core.SerialToString(big.NewInt(1)) {
|
||||
t.Errorf("CertificateSerial = %+v, want %+v", order.CertificateSerial, 1)
|
||||
}
|
||||
|
||||
_, err = ra.issueCertificateOuter(context.Background(), order, csr, certificateRequestEvent{})
|
||||
test.AssertNotError(t, err, "Could not issue certificate")
|
||||
test.AssertMetricWithLabelsEquals(t, ra.newCertCounter, prometheus.Labels{"profileName": mockCA.profileName, "profileHash": fmt.Sprintf("%x", mockCA.profileHash)}, 1)
|
||||
// The recorded profile and profile hash should match what we expect.
|
||||
if mockCA.profileName != tc.wantProfile {
|
||||
t.Errorf("recorded profileName = %+v, want %+v", mockCA.profileName, tc.wantProfile)
|
||||
}
|
||||
wantHash, err := hex.DecodeString(tc.wantHash)
|
||||
if err != nil {
|
||||
t.Fatalf("decoding test hash: %s", err)
|
||||
}
|
||||
if !bytes.Equal(mockCA.profileHash, wantHash) {
|
||||
t.Errorf("recorded profileName = %x, want %x", mockCA.profileHash, wantHash)
|
||||
}
|
||||
test.AssertMetricWithLabelsEquals(t, ra.newCertCounter, prometheus.Labels{"profileName": tc.wantProfile, "profileHash": tc.wantHash}, 1)
|
||||
ra.newCertCounter.Reset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewOrderMaxNames(t *testing.T) {
|
||||
|
|
|
@ -27,10 +27,7 @@
|
|||
"maxContactsPerRegistration": 3,
|
||||
"hostnamePolicyFile": "test/hostname-policy.yaml",
|
||||
"maxNames": 100,
|
||||
"authorizationLifetimeDays": 30,
|
||||
"pendingAuthorizationLifetimeDays": 7,
|
||||
"goodkey": {},
|
||||
"orderLifetime": "168h",
|
||||
"finalizeTimeout": "30s",
|
||||
"issuerCerts": [
|
||||
"test/certs/webpki/int-rsa-a.cert.pem",
|
||||
|
@ -40,6 +37,19 @@
|
|||
"test/certs/webpki/int-ecdsa-b.cert.pem",
|
||||
"test/certs/webpki/int-ecdsa-c.cert.pem"
|
||||
],
|
||||
"validationProfiles": {
|
||||
"legacy": {
|
||||
"pendingAuthzLifetime": "168h",
|
||||
"validAuthzLifetime": "720h",
|
||||
"orderLifetime": "168h"
|
||||
},
|
||||
"modern": {
|
||||
"pendingAuthzLifetime": "7h",
|
||||
"validAuthzLifetime": "7h",
|
||||
"orderLifetime": "7h"
|
||||
}
|
||||
},
|
||||
"defaultProfileName": "legacy",
|
||||
"tls": {
|
||||
"caCertFile": "test/certs/ipki/minica.pem",
|
||||
"certFile": "test/certs/ipki/ra.boulder/cert.pem",
|
||||
|
|
Loading…
Reference in New Issue