ra: check failed authorizations limit before attempting authz reuse (#5827)
Part of #5826
This commit is contained in:
parent
d3d5b12e59
commit
add5cfdb22
|
@ -28,11 +28,12 @@ func _() {
|
||||||
_ = x[ServeRenewalInfo-17]
|
_ = x[ServeRenewalInfo-17]
|
||||||
_ = x[GetAuthzReadOnly-18]
|
_ = x[GetAuthzReadOnly-18]
|
||||||
_ = x[GetAuthzUseIndex-19]
|
_ = x[GetAuthzUseIndex-19]
|
||||||
|
_ = x[CheckFailedAuthorizationsFirst-20]
|
||||||
}
|
}
|
||||||
|
|
||||||
const _FeatureFlag_name = "unusedPrecertificateRevocationStripDefaultSchemePortNonCFSSLSignerStoreIssuerInfoStreamlineOrderAndAuthzsCAAValidationMethodsCAAAccountURIEnforceMultiVAMultiVAFullResultsMandatoryPOSTAsGETAllowV1RegistrationV1DisableNewValidationsStoreRevokerInfoRestrictRSAKeySizesFasterNewOrdersRateLimitECDSAForAllServeRenewalInfoGetAuthzReadOnlyGetAuthzUseIndex"
|
const _FeatureFlag_name = "unusedPrecertificateRevocationStripDefaultSchemePortNonCFSSLSignerStoreIssuerInfoStreamlineOrderAndAuthzsCAAValidationMethodsCAAAccountURIEnforceMultiVAMultiVAFullResultsMandatoryPOSTAsGETAllowV1RegistrationV1DisableNewValidationsStoreRevokerInfoRestrictRSAKeySizesFasterNewOrdersRateLimitECDSAForAllServeRenewalInfoGetAuthzReadOnlyGetAuthzUseIndexCheckFailedAuthorizationsFirst"
|
||||||
|
|
||||||
var _FeatureFlag_index = [...]uint16{0, 6, 30, 52, 66, 81, 105, 125, 138, 152, 170, 188, 207, 230, 246, 265, 289, 300, 316, 332, 348}
|
var _FeatureFlag_index = [...]uint16{0, 6, 30, 52, 66, 81, 105, 125, 138, 152, 170, 188, 207, 230, 246, 265, 289, 300, 316, 332, 348, 378}
|
||||||
|
|
||||||
func (i FeatureFlag) String() string {
|
func (i FeatureFlag) String() string {
|
||||||
if i < 0 || i >= FeatureFlag(len(_FeatureFlag_index)-1) {
|
if i < 0 || i >= FeatureFlag(len(_FeatureFlag_index)-1) {
|
||||||
|
|
|
@ -59,6 +59,8 @@ const (
|
||||||
// GetAuthzUseIndex causes the SA to use to add a USE INDEX hint when it
|
// GetAuthzUseIndex causes the SA to use to add a USE INDEX hint when it
|
||||||
// queries the authz2 table.
|
// queries the authz2 table.
|
||||||
GetAuthzUseIndex
|
GetAuthzUseIndex
|
||||||
|
// Check the failed authorization limit before doing authz reuse.
|
||||||
|
CheckFailedAuthorizationsFirst
|
||||||
)
|
)
|
||||||
|
|
||||||
// List of features and their default value, protected by fMu
|
// List of features and their default value, protected by fMu
|
||||||
|
@ -83,6 +85,7 @@ var features = map[FeatureFlag]bool{
|
||||||
ServeRenewalInfo: false,
|
ServeRenewalInfo: false,
|
||||||
GetAuthzReadOnly: false,
|
GetAuthzReadOnly: false,
|
||||||
GetAuthzUseIndex: false,
|
GetAuthzUseIndex: false,
|
||||||
|
CheckFailedAuthorizationsFirst: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
var fMu = new(sync.RWMutex)
|
var fMu = new(sync.RWMutex)
|
||||||
|
|
|
@ -2,7 +2,9 @@ package ra
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
corepb "github.com/letsencrypt/boulder/core/proto"
|
||||||
"github.com/letsencrypt/boulder/mocks"
|
"github.com/letsencrypt/boulder/mocks"
|
||||||
sapb "github.com/letsencrypt/boulder/sa/proto"
|
sapb "github.com/letsencrypt/boulder/sa/proto"
|
||||||
grpc "google.golang.org/grpc"
|
grpc "google.golang.org/grpc"
|
||||||
|
@ -30,3 +32,25 @@ func (sa *mockInvalidAuthorizationsAuthority) CountInvalidAuthorizations2(ctx co
|
||||||
return &sapb.Count{}, nil
|
return &sapb.Count{}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// An authority that returns nonzero failures for CountInvalidAuthorizations2,
|
||||||
|
// and also returns existing authzs for the same domain from GetAuthorizations2
|
||||||
|
type mockInvalidPlusValidAuthzAuthority struct {
|
||||||
|
mockInvalidAuthorizationsAuthority
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sa *mockInvalidPlusValidAuthzAuthority) GetAuthorizations2(ctx context.Context, req *sapb.GetAuthorizationsRequest, _ ...grpc.CallOption) (*sapb.Authorizations, error) {
|
||||||
|
return &sapb.Authorizations{
|
||||||
|
Authz: []*sapb.Authorizations_MapElement{
|
||||||
|
{
|
||||||
|
Domain: sa.domainWithFailures, Authz: &corepb.Authorization{
|
||||||
|
Id: "1234",
|
||||||
|
Status: "valid",
|
||||||
|
Identifier: sa.domainWithFailures,
|
||||||
|
RegistrationID: 1234,
|
||||||
|
Expires: time.Date(2101, 12, 3, 0, 0, 0, 0, time.UTC).Unix(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
14
ra/ra.go
14
ra/ra.go
|
@ -2165,6 +2165,12 @@ func (ra *RegistrationAuthorityImpl) NewOrder(ctx context.Context, req *rapb.New
|
||||||
if err := ra.checkLimits(ctx, newOrder.Names, newOrder.RegistrationID); err != nil {
|
if err := ra.checkLimits(ctx, newOrder.Names, newOrder.RegistrationID); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if features.Enabled(features.CheckFailedAuthorizationsFirst) {
|
||||||
|
err := ra.checkInvalidAuthorizationLimits(ctx, newOrder.RegistrationID, newOrder.Names)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// An order's lifetime is effectively bound by the shortest remaining lifetime
|
// An order's lifetime is effectively bound by the shortest remaining lifetime
|
||||||
// of its associated authorizations. For that reason it would be Uncool if
|
// of its associated authorizations. For that reason it would be Uncool if
|
||||||
|
@ -2241,13 +2247,17 @@ func (ra *RegistrationAuthorityImpl) NewOrder(ctx context.Context, req *rapb.New
|
||||||
// If the order isn't fully authorized we need to check that the client has
|
// If the order isn't fully authorized we need to check that the client has
|
||||||
// rate limit room for more pending authorizations
|
// rate limit room for more pending authorizations
|
||||||
if len(missingAuthzNames) > 0 {
|
if len(missingAuthzNames) > 0 {
|
||||||
if err := ra.checkPendingAuthorizationLimit(ctx, newOrder.RegistrationID); err != nil {
|
err := ra.checkPendingAuthorizationLimit(ctx, newOrder.RegistrationID)
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := ra.checkInvalidAuthorizationLimits(ctx, newOrder.RegistrationID, missingAuthzNames); err != nil {
|
if !features.Enabled(features.CheckFailedAuthorizationsFirst) {
|
||||||
|
err := ra.checkInvalidAuthorizationLimits(ctx, newOrder.RegistrationID, missingAuthzNames)
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Loop through each of the names missing authzs and create a new pending
|
// Loop through each of the names missing authzs and create a new pending
|
||||||
// authorization for each.
|
// authorization for each.
|
||||||
|
|
|
@ -2470,6 +2470,46 @@ func TestNewOrderReuseInvalidAuthz(t *testing.T) {
|
||||||
test.AssertNotEquals(t, secondOrder.V2Authorizations[0], order.V2Authorizations[0])
|
test.AssertNotEquals(t, secondOrder.V2Authorizations[0], order.V2Authorizations[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test that the failed authorizations limit is checked before authz reuse.
|
||||||
|
func TestNewOrderCheckFailedAuthorizationsFirst(t *testing.T) {
|
||||||
|
_, _, ra, _, cleanUp := initAuthorities(t)
|
||||||
|
defer cleanUp()
|
||||||
|
|
||||||
|
_ = features.Set(map[string]bool{"CheckFailedAuthorizationsFirst": true})
|
||||||
|
defer features.Reset()
|
||||||
|
|
||||||
|
// Create an order (and thus a pending authz) for example.com
|
||||||
|
ctx := context.Background()
|
||||||
|
order, err := ra.NewOrder(ctx, &rapb.NewOrderRequest{
|
||||||
|
RegistrationID: Registration.Id,
|
||||||
|
Names: []string{"example.com"},
|
||||||
|
})
|
||||||
|
test.AssertNotError(t, err, "adding an initial order for regA")
|
||||||
|
test.AssertNotNil(t, order.Id, "initial order had a nil ID")
|
||||||
|
test.AssertEquals(t, numAuthorizations(order), 1)
|
||||||
|
|
||||||
|
// Now treat example.com as if it had a recent failure.
|
||||||
|
ra.SA = &mockInvalidPlusValidAuthzAuthority{mockInvalidAuthorizationsAuthority{domainWithFailures: "example.com"}}
|
||||||
|
// Set a very restrictive police for invalid authorizations - one failure
|
||||||
|
// and you're done for a day.
|
||||||
|
ra.rlPolicies = &dummyRateLimitConfig{
|
||||||
|
InvalidAuthorizationsPerAccountPolicy: ratelimit.RateLimitPolicy{
|
||||||
|
Threshold: 1,
|
||||||
|
Window: cmd.ConfigDuration{Duration: 24 * time.Hour},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creating an order for example.com should error with the "too many failed
|
||||||
|
// authorizations recently" error.
|
||||||
|
_, err = ra.NewOrder(ctx, &rapb.NewOrderRequest{
|
||||||
|
RegistrationID: Registration.Id,
|
||||||
|
Names: []string{"example.com"},
|
||||||
|
})
|
||||||
|
|
||||||
|
test.AssertError(t, err, "expected error for domain with too many failures")
|
||||||
|
test.AssertEquals(t, err.Error(), "too many failed authorizations recently: see https://letsencrypt.org/docs/rate-limits/")
|
||||||
|
}
|
||||||
|
|
||||||
// mockSAUnsafeAuthzReuse has a GetAuthorizations implementation that returns
|
// mockSAUnsafeAuthzReuse has a GetAuthorizations implementation that returns
|
||||||
// an HTTP-01 validated wildcard authz.
|
// an HTTP-01 validated wildcard authz.
|
||||||
type mockSAUnsafeAuthzReuse struct {
|
type mockSAUnsafeAuthzReuse struct {
|
||||||
|
|
Loading…
Reference in New Issue