ra/ratelimits: Update tests, use new TransactionBuilder constructor, fix ARI rate limit exception (#7869)
Add a new `ratelimits.NewTransactionBuilderWithLimits` constructor which takes pre-populated rate limit data, instead of filenames for reading it off disk. Use this new constructor to change rate limits during RA tests, instead of using extra `testdata` files. Fix ARI renewals' exception from rate limits: consider `isARIRenewal` as part of the `isRenewal` arg to `checkNewOrderLimits`. Remove obsolete RA tests for rate limits that are now only checked in the WFE. Update remaining new order rate limit tests from deprecated `ratelimit`s to new Redis `ratelimits`.
This commit is contained in:
		
							parent
							
								
									0e5e1e98d1
								
							
						
					
					
						commit
						62299362bd
					
				| 
						 | 
				
			
			@ -273,7 +273,7 @@ func main() {
 | 
			
		|||
		source := ratelimits.NewRedisSource(limiterRedis.Ring, clk, scope)
 | 
			
		||||
		limiter, err = ratelimits.NewLimiter(clk, source, scope)
 | 
			
		||||
		cmd.FailOnError(err, "Failed to create rate limiter")
 | 
			
		||||
		txnBuilder, err = ratelimits.NewTransactionBuilder(c.RA.Limiter.Defaults, c.RA.Limiter.Overrides)
 | 
			
		||||
		txnBuilder, err = ratelimits.NewTransactionBuilderFromFiles(c.RA.Limiter.Defaults, c.RA.Limiter.Overrides)
 | 
			
		||||
		cmd.FailOnError(err, "Failed to create rate limits transaction builder")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -365,7 +365,7 @@ func main() {
 | 
			
		|||
		source := ratelimits.NewRedisSource(limiterRedis.Ring, clk, stats)
 | 
			
		||||
		limiter, err = ratelimits.NewLimiter(clk, source, stats)
 | 
			
		||||
		cmd.FailOnError(err, "Failed to create rate limiter")
 | 
			
		||||
		txnBuilder, err = ratelimits.NewTransactionBuilder(c.WFE.Limiter.Defaults, c.WFE.Limiter.Overrides)
 | 
			
		||||
		txnBuilder, err = ratelimits.NewTransactionBuilderFromFiles(c.WFE.Limiter.Defaults, c.WFE.Limiter.Overrides)
 | 
			
		||||
		cmd.FailOnError(err, "Failed to create rate limits transaction builder")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										369
									
								
								ra/ra_test.go
								
								
								
								
							
							
						
						
									
										369
									
								
								ra/ra_test.go
								
								
								
								
							| 
						 | 
				
			
			@ -261,55 +261,6 @@ var (
 | 
			
		|||
 | 
			
		||||
var ctx = context.Background()
 | 
			
		||||
 | 
			
		||||
// dummyRateLimitConfig satisfies the rl.RateLimitConfig interface while
 | 
			
		||||
// allowing easy mocking of the individual RateLimitPolicy's
 | 
			
		||||
type dummyRateLimitConfig struct {
 | 
			
		||||
	CertificatesPerNamePolicy             ratelimit.RateLimitPolicy
 | 
			
		||||
	RegistrationsPerIPPolicy              ratelimit.RateLimitPolicy
 | 
			
		||||
	RegistrationsPerIPRangePolicy         ratelimit.RateLimitPolicy
 | 
			
		||||
	PendingAuthorizationsPerAccountPolicy ratelimit.RateLimitPolicy
 | 
			
		||||
	NewOrdersPerAccountPolicy             ratelimit.RateLimitPolicy
 | 
			
		||||
	InvalidAuthorizationsPerAccountPolicy ratelimit.RateLimitPolicy
 | 
			
		||||
	CertificatesPerFQDNSetPolicy          ratelimit.RateLimitPolicy
 | 
			
		||||
	CertificatesPerFQDNSetFastPolicy      ratelimit.RateLimitPolicy
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *dummyRateLimitConfig) CertificatesPerName() ratelimit.RateLimitPolicy {
 | 
			
		||||
	return r.CertificatesPerNamePolicy
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *dummyRateLimitConfig) RegistrationsPerIP() ratelimit.RateLimitPolicy {
 | 
			
		||||
	return r.RegistrationsPerIPPolicy
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *dummyRateLimitConfig) RegistrationsPerIPRange() ratelimit.RateLimitPolicy {
 | 
			
		||||
	return r.RegistrationsPerIPRangePolicy
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *dummyRateLimitConfig) PendingAuthorizationsPerAccount() ratelimit.RateLimitPolicy {
 | 
			
		||||
	return r.PendingAuthorizationsPerAccountPolicy
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *dummyRateLimitConfig) NewOrdersPerAccount() ratelimit.RateLimitPolicy {
 | 
			
		||||
	return r.NewOrdersPerAccountPolicy
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *dummyRateLimitConfig) InvalidAuthorizationsPerAccount() ratelimit.RateLimitPolicy {
 | 
			
		||||
	return r.InvalidAuthorizationsPerAccountPolicy
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *dummyRateLimitConfig) CertificatesPerFQDNSet() ratelimit.RateLimitPolicy {
 | 
			
		||||
	return r.CertificatesPerFQDNSetPolicy
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *dummyRateLimitConfig) CertificatesPerFQDNSetFast() ratelimit.RateLimitPolicy {
 | 
			
		||||
	return r.CertificatesPerFQDNSetFastPolicy
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *dummyRateLimitConfig) LoadPolicies(contents []byte) error {
 | 
			
		||||
	return nil // NOP - unrequired behaviour for this mock
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func newAcctKey(t *testing.T) []byte {
 | 
			
		||||
	key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
 | 
			
		||||
	jwk := &jose.JSONWebKey{Key: key.Public()}
 | 
			
		||||
| 
						 | 
				
			
			@ -392,7 +343,7 @@ func initAuthorities(t *testing.T) (*DummyValidationAuthority, sapb.StorageAutho
 | 
			
		|||
	rlSource := ratelimits.NewInmemSource()
 | 
			
		||||
	limiter, err := ratelimits.NewLimiter(fc, rlSource, stats)
 | 
			
		||||
	test.AssertNotError(t, err, "making limiter")
 | 
			
		||||
	txnBuilder, err := ratelimits.NewTransactionBuilder("../test/config-next/wfe2-ratelimit-defaults.yml", "")
 | 
			
		||||
	txnBuilder, err := ratelimits.NewTransactionBuilderFromFiles("../test/config-next/wfe2-ratelimit-defaults.yml", "")
 | 
			
		||||
	test.AssertNotError(t, err, "making transaction composer")
 | 
			
		||||
 | 
			
		||||
	testKeyPolicy, err := goodkey.NewPolicy(nil, nil)
 | 
			
		||||
| 
						 | 
				
			
			@ -844,8 +795,14 @@ func TestPerformValidation_FailedValidationsTriggerPauseIdentifiersRatelimit(t *
 | 
			
		|||
		out:                    pauseChan,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Override the default ratelimits to only allow one failed validation per 24 hours.
 | 
			
		||||
	txnBuilder, err := ratelimits.NewTransactionBuilder("testdata/one-failed-validation-before-pausing.yml", "")
 | 
			
		||||
	// Set the default ratelimits to only allow one failed validation per 24
 | 
			
		||||
	// hours before pausing.
 | 
			
		||||
	txnBuilder, err := ratelimits.NewTransactionBuilder(ratelimits.LimitConfigs{
 | 
			
		||||
		ratelimits.FailedAuthorizationsForPausingPerDomainPerAccount.String(): &ratelimits.LimitConfig{
 | 
			
		||||
			Burst:  1,
 | 
			
		||||
			Count:  1,
 | 
			
		||||
			Period: config.Duration{Duration: time.Hour * 24}},
 | 
			
		||||
	})
 | 
			
		||||
	test.AssertNotError(t, err, "making transaction composer")
 | 
			
		||||
	ra.txnBuilder = txnBuilder
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1055,101 +1012,6 @@ func TestCertificateKeyNotEqualAccountKey(t *testing.T) {
 | 
			
		|||
	test.AssertEquals(t, err.Error(), "certificate public key must be different than account key")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestNewOrderRateLimiting(t *testing.T) {
 | 
			
		||||
	_, _, ra, _, fc, cleanUp := initAuthorities(t)
 | 
			
		||||
	defer cleanUp()
 | 
			
		||||
 | 
			
		||||
	// Create a dummy rate limit config that sets a NewOrdersPerAccount rate
 | 
			
		||||
	// limit with a very low threshold/short window
 | 
			
		||||
	rateLimitDuration := 5 * time.Minute
 | 
			
		||||
	ra.rlPolicies = &dummyRateLimitConfig{
 | 
			
		||||
		NewOrdersPerAccountPolicy: ratelimit.RateLimitPolicy{
 | 
			
		||||
			Threshold: 1,
 | 
			
		||||
			Window:    config.Duration{Duration: rateLimitDuration},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	orderOne := &rapb.NewOrderRequest{
 | 
			
		||||
		RegistrationID: Registration.Id,
 | 
			
		||||
		DnsNames:       []string{"first.example.com"},
 | 
			
		||||
	}
 | 
			
		||||
	orderTwo := &rapb.NewOrderRequest{
 | 
			
		||||
		RegistrationID: Registration.Id,
 | 
			
		||||
		DnsNames:       []string{"second.example.com"},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// To start, it should be possible to create a new order
 | 
			
		||||
	_, err := ra.NewOrder(ctx, orderOne)
 | 
			
		||||
	test.AssertNotError(t, err, "NewOrder for orderOne failed")
 | 
			
		||||
 | 
			
		||||
	// Advance the clock 1s to separate the orders in time
 | 
			
		||||
	fc.Add(time.Second)
 | 
			
		||||
 | 
			
		||||
	// Creating an order immediately after the first with different names
 | 
			
		||||
	// should fail
 | 
			
		||||
	_, err = ra.NewOrder(ctx, orderTwo)
 | 
			
		||||
	test.AssertError(t, err, "NewOrder for orderTwo succeeded, should have been ratelimited")
 | 
			
		||||
 | 
			
		||||
	// Creating the first order again should succeed because of order reuse, no
 | 
			
		||||
	// new pending order is produced.
 | 
			
		||||
	_, err = ra.NewOrder(ctx, orderOne)
 | 
			
		||||
	test.AssertNotError(t, err, "Reuse of orderOne failed")
 | 
			
		||||
 | 
			
		||||
	// Advancing the clock by 2 * the rate limit duration should allow orderTwo to
 | 
			
		||||
	// succeed
 | 
			
		||||
	fc.Add(2 * rateLimitDuration)
 | 
			
		||||
	_, err = ra.NewOrder(ctx, orderTwo)
 | 
			
		||||
	test.AssertNotError(t, err, "NewOrder for orderTwo failed after advancing clock")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TestEarlyOrderRateLimiting tests that NewOrder applies the certificates per
 | 
			
		||||
// name/per FQDN rate limits against the order names.
 | 
			
		||||
func TestEarlyOrderRateLimiting(t *testing.T) {
 | 
			
		||||
	_, _, ra, _, _, cleanUp := initAuthorities(t)
 | 
			
		||||
	defer cleanUp()
 | 
			
		||||
 | 
			
		||||
	rateLimitDuration := 5 * time.Minute
 | 
			
		||||
 | 
			
		||||
	domain := "early-ratelimit-example.com"
 | 
			
		||||
 | 
			
		||||
	// Set a mock RL policy with a CertificatesPerName threshold for the domain
 | 
			
		||||
	// name so low if it were enforced it would prevent a new order for any names.
 | 
			
		||||
	ra.rlPolicies = &dummyRateLimitConfig{
 | 
			
		||||
		CertificatesPerNamePolicy: ratelimit.RateLimitPolicy{
 | 
			
		||||
			Threshold: 10,
 | 
			
		||||
			Window:    config.Duration{Duration: rateLimitDuration},
 | 
			
		||||
			// Setting the Threshold to 0 skips applying the rate limit. Setting an
 | 
			
		||||
			// override to 0 does the trick.
 | 
			
		||||
			Overrides: map[string]int64{
 | 
			
		||||
				domain: 0,
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		NewOrdersPerAccountPolicy: ratelimit.RateLimitPolicy{
 | 
			
		||||
			Threshold: 10,
 | 
			
		||||
			Window:    config.Duration{Duration: rateLimitDuration},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Request an order for the test domain
 | 
			
		||||
	newOrder := &rapb.NewOrderRequest{
 | 
			
		||||
		RegistrationID: Registration.Id,
 | 
			
		||||
		DnsNames:       []string{domain},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// With the feature flag enabled the NewOrder request should fail because of
 | 
			
		||||
	// the CertificatesPerNamePolicy.
 | 
			
		||||
	_, err := ra.NewOrder(ctx, newOrder)
 | 
			
		||||
	test.AssertError(t, err, "NewOrder did not apply cert rate limits with feature flag enabled")
 | 
			
		||||
 | 
			
		||||
	var bErr *berrors.BoulderError
 | 
			
		||||
	test.Assert(t, errors.As(err, &bErr), "NewOrder did not return a boulder error")
 | 
			
		||||
	test.AssertEquals(t, bErr.RetryAfter, rateLimitDuration)
 | 
			
		||||
 | 
			
		||||
	// The err should be the expected rate limit error
 | 
			
		||||
	expected := "too many certificates already issued for \"early-ratelimit-example.com\". Retry after 2020-03-04T05:05:00Z: see https://letsencrypt.org/docs/rate-limits/#new-orders-per-account"
 | 
			
		||||
	test.AssertEquals(t, bErr.Error(), expected)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// mockInvalidAuthorizationsAuthority is a mock which claims that the given
 | 
			
		||||
// domain has one invalid authorization.
 | 
			
		||||
type mockInvalidAuthorizationsAuthority struct {
 | 
			
		||||
| 
						 | 
				
			
			@ -1169,16 +1031,18 @@ func TestAuthzFailedRateLimitingNewOrder(t *testing.T) {
 | 
			
		|||
	_, _, ra, _, _, cleanUp := initAuthorities(t)
 | 
			
		||||
	defer cleanUp()
 | 
			
		||||
 | 
			
		||||
	ra.rlPolicies = &dummyRateLimitConfig{
 | 
			
		||||
		InvalidAuthorizationsPerAccountPolicy: ratelimit.RateLimitPolicy{
 | 
			
		||||
			Threshold: 1,
 | 
			
		||||
			Window:    config.Duration{Duration: 1 * time.Hour},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	txnBuilder, err := ratelimits.NewTransactionBuilder(ratelimits.LimitConfigs{
 | 
			
		||||
		ratelimits.FailedAuthorizationsForPausingPerDomainPerAccount.String(): &ratelimits.LimitConfig{
 | 
			
		||||
			Burst:  1,
 | 
			
		||||
			Count:  1,
 | 
			
		||||
			Period: config.Duration{Duration: time.Hour * 1}},
 | 
			
		||||
	})
 | 
			
		||||
	test.AssertNotError(t, err, "making transaction composer")
 | 
			
		||||
	ra.txnBuilder = txnBuilder
 | 
			
		||||
 | 
			
		||||
	limit := ra.rlPolicies.InvalidAuthorizationsPerAccount()
 | 
			
		||||
	ra.SA = &mockInvalidAuthorizationsAuthority{domainWithFailures: "all.i.do.is.lose.com"}
 | 
			
		||||
	err := ra.checkInvalidAuthorizationLimits(ctx, Registration.Id,
 | 
			
		||||
	err = ra.checkInvalidAuthorizationLimits(ctx, Registration.Id,
 | 
			
		||||
		[]string{"charlie.brown.com", "all.i.do.is.lose.com"}, limit)
 | 
			
		||||
	test.AssertError(t, err, "checkInvalidAuthorizationLimits did not encounter expected rate limit error")
 | 
			
		||||
	test.AssertEquals(t, err.Error(), "too many failed authorizations recently: see https://letsencrypt.org/docs/rate-limits/#authorization-failures-per-hostname-per-account")
 | 
			
		||||
| 
						 | 
				
			
			@ -1541,8 +1405,7 @@ func (m mockSAWithFQDNSet) FQDNSetTimestampsForWindow(_ context.Context, req *sa
 | 
			
		|||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TestExactPublicSuffixCertLimit tests the behaviour of issue #2681 with and
 | 
			
		||||
// without the feature flag for the fix enabled.
 | 
			
		||||
// TestExactPublicSuffixCertLimit tests the behaviour of issue #2681.
 | 
			
		||||
// See https://github.com/letsencrypt/boulder/issues/2681
 | 
			
		||||
func TestExactPublicSuffixCertLimit(t *testing.T) {
 | 
			
		||||
	_, _, ra, _, fc, cleanUp := initAuthorities(t)
 | 
			
		||||
| 
						 | 
				
			
			@ -1637,8 +1500,14 @@ func TestDeactivateAuthorization_Pausing(t *testing.T) {
 | 
			
		|||
	features.Set(features.Config{AutomaticallyPauseZombieClients: true})
 | 
			
		||||
	defer features.Reset()
 | 
			
		||||
 | 
			
		||||
	// Override the default ratelimits to only allow one failed validation.
 | 
			
		||||
	txnBuilder, err := ratelimits.NewTransactionBuilder("testdata/one-failed-validation-before-pausing.yml", "")
 | 
			
		||||
	// Set the default ratelimits to only allow one failed validation per 24
 | 
			
		||||
	// hours before pausing.
 | 
			
		||||
	txnBuilder, err := ratelimits.NewTransactionBuilder(ratelimits.LimitConfigs{
 | 
			
		||||
		ratelimits.FailedAuthorizationsForPausingPerDomainPerAccount.String(): &ratelimits.LimitConfig{
 | 
			
		||||
			Burst:  1,
 | 
			
		||||
			Count:  1,
 | 
			
		||||
			Period: config.Duration{Duration: time.Hour * 24}},
 | 
			
		||||
	})
 | 
			
		||||
	test.AssertNotError(t, err, "making transaction composer")
 | 
			
		||||
	ra.txnBuilder = txnBuilder
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -2392,113 +2261,6 @@ func TestNewOrder_AuthzReuse_NoPending(t *testing.T) {
 | 
			
		|||
	test.AssertNotEquals(t, new.V2Authorizations[0], extant.V2Authorizations[0])
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// mockSACountPendingFails has a CountPendingAuthorizations2 implementation
 | 
			
		||||
// that always returns error
 | 
			
		||||
type mockSACountPendingFails struct {
 | 
			
		||||
	sapb.StorageAuthorityClient
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (mock *mockSACountPendingFails) CountPendingAuthorizations2(ctx context.Context, req *sapb.RegistrationID, _ ...grpc.CallOption) (*sapb.Count, error) {
 | 
			
		||||
	return nil, errors.New("counting is slow and boring")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Ensure that we don't bother to call the SA to count pending authorizations
 | 
			
		||||
// when an "unlimited" limit is set.
 | 
			
		||||
func TestPendingAuthorizationsUnlimited(t *testing.T) {
 | 
			
		||||
	_, _, ra, _, _, cleanUp := initAuthorities(t)
 | 
			
		||||
	defer cleanUp()
 | 
			
		||||
 | 
			
		||||
	ra.rlPolicies = &dummyRateLimitConfig{
 | 
			
		||||
		PendingAuthorizationsPerAccountPolicy: ratelimit.RateLimitPolicy{
 | 
			
		||||
			Threshold: 1,
 | 
			
		||||
			Window:    config.Duration{Duration: 24 * time.Hour},
 | 
			
		||||
			RegistrationOverrides: map[int64]int64{
 | 
			
		||||
				13: -1,
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ra.SA = &mockSACountPendingFails{}
 | 
			
		||||
 | 
			
		||||
	limit := ra.rlPolicies.PendingAuthorizationsPerAccount()
 | 
			
		||||
	err := ra.checkPendingAuthorizationLimit(context.Background(), 13, limit)
 | 
			
		||||
	test.AssertNotError(t, err, "checking pending authorization limit")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// An authority that returns nonzero failures for CountInvalidAuthorizations2,
 | 
			
		||||
// and also returns existing authzs for the same domain from GetAuthorizations2
 | 
			
		||||
type mockInvalidPlusValidAuthzAuthority struct {
 | 
			
		||||
	mockSAWithAuthzs
 | 
			
		||||
	domainWithFailures string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (sa *mockInvalidPlusValidAuthzAuthority) CountInvalidAuthorizations2(ctx context.Context, req *sapb.CountInvalidAuthorizationsRequest, _ ...grpc.CallOption) (*sapb.Count, error) {
 | 
			
		||||
	if req.DnsName == sa.domainWithFailures {
 | 
			
		||||
		return &sapb.Count{Count: 1}, nil
 | 
			
		||||
	} else {
 | 
			
		||||
		return &sapb.Count{}, nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Test that the failed authorizations limit is checked before authz reuse.
 | 
			
		||||
func TestNewOrderCheckFailedAuthorizationsFirst(t *testing.T) {
 | 
			
		||||
	_, _, ra, _, clk, cleanUp := initAuthorities(t)
 | 
			
		||||
	defer cleanUp()
 | 
			
		||||
 | 
			
		||||
	// Create an order (and thus a pending authz) for example.com
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	order, err := ra.NewOrder(ctx, &rapb.NewOrderRequest{
 | 
			
		||||
		RegistrationID: Registration.Id,
 | 
			
		||||
		DnsNames:       []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, but also a valid authz.
 | 
			
		||||
	expires := clk.Now().Add(24 * time.Hour)
 | 
			
		||||
	ra.SA = &mockInvalidPlusValidAuthzAuthority{
 | 
			
		||||
		mockSAWithAuthzs: mockSAWithAuthzs{
 | 
			
		||||
			authzs: []*core.Authorization{
 | 
			
		||||
				{
 | 
			
		||||
					ID:             "1",
 | 
			
		||||
					Identifier:     identifier.NewDNS("example.com"),
 | 
			
		||||
					RegistrationID: Registration.Id,
 | 
			
		||||
					Expires:        &expires,
 | 
			
		||||
					Status:         "valid",
 | 
			
		||||
					Challenges: []core.Challenge{
 | 
			
		||||
						{
 | 
			
		||||
							Type:   core.ChallengeTypeHTTP01,
 | 
			
		||||
							Status: core.StatusValid,
 | 
			
		||||
							Token:  core.NewToken(),
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		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:    config.Duration{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,
 | 
			
		||||
		DnsNames:       []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/#authorization-failures-per-hostname-per-account")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// mockSAWithAuthzs has a GetAuthorizations2 method that returns the protobuf
 | 
			
		||||
// version of its authzs struct member. It also has a fake GetOrderForNames
 | 
			
		||||
// which always fails, and a fake NewOrderAndAuthzs which always succeeds, to
 | 
			
		||||
| 
						 | 
				
			
			@ -4622,83 +4384,6 @@ func TestAdministrativelyRevokeCertificate(t *testing.T) {
 | 
			
		|||
	test.AssertError(t, err, "AdministrativelyRevokeCertificate should have failed with just serial for keyCompromise")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestNewOrderRateLimitingExempt(t *testing.T) {
 | 
			
		||||
	_, _, ra, _, _, cleanUp := initAuthorities(t)
 | 
			
		||||
	defer cleanUp()
 | 
			
		||||
 | 
			
		||||
	// Set up a rate limit policy that allows 1 order every 5 minutes.
 | 
			
		||||
	rateLimitDuration := 5 * time.Minute
 | 
			
		||||
	ra.rlPolicies = &dummyRateLimitConfig{
 | 
			
		||||
		NewOrdersPerAccountPolicy: ratelimit.RateLimitPolicy{
 | 
			
		||||
			Threshold: 1,
 | 
			
		||||
			Window:    config.Duration{Duration: rateLimitDuration},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	exampleOrderOne := &rapb.NewOrderRequest{
 | 
			
		||||
		RegistrationID: Registration.Id,
 | 
			
		||||
		DnsNames:       []string{"first.example.com", "second.example.com"},
 | 
			
		||||
	}
 | 
			
		||||
	exampleOrderTwo := &rapb.NewOrderRequest{
 | 
			
		||||
		RegistrationID: Registration.Id,
 | 
			
		||||
		DnsNames:       []string{"first.example.com", "third.example.com"},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Create an order immediately.
 | 
			
		||||
	_, err := ra.NewOrder(ctx, exampleOrderOne)
 | 
			
		||||
	test.AssertNotError(t, err, "orderOne should have succeeded")
 | 
			
		||||
 | 
			
		||||
	// Create another order immediately. This should fail.
 | 
			
		||||
	_, err = ra.NewOrder(ctx, exampleOrderTwo)
 | 
			
		||||
	test.AssertError(t, err, "orderTwo should have failed")
 | 
			
		||||
 | 
			
		||||
	// Exempt orderTwo from rate limiting.
 | 
			
		||||
	exampleOrderTwo.IsARIRenewal = true
 | 
			
		||||
	_, err = ra.NewOrder(ctx, exampleOrderTwo)
 | 
			
		||||
	test.AssertNotError(t, err, "orderTwo should have succeeded")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestNewOrderFailedAuthzRateLimitingExempt(t *testing.T) {
 | 
			
		||||
	_, _, ra, _, _, cleanUp := initAuthorities(t)
 | 
			
		||||
	defer cleanUp()
 | 
			
		||||
 | 
			
		||||
	exampleOrder := &rapb.NewOrderRequest{
 | 
			
		||||
		RegistrationID: Registration.Id,
 | 
			
		||||
		DnsNames:       []string{"example.com"},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Create an order, and thus a pending authz, for "example.com".
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	order, err := ra.NewOrder(ctx, exampleOrder)
 | 
			
		||||
	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)
 | 
			
		||||
 | 
			
		||||
	// Mock SA that has a failed authorization for "example.com".
 | 
			
		||||
	ra.SA = &mockInvalidPlusValidAuthzAuthority{
 | 
			
		||||
		mockSAWithAuthzs{authzs: []*core.Authorization{}},
 | 
			
		||||
		"example.com",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Set up a rate limit policy that allows 1 order every 24 hours.
 | 
			
		||||
	ra.rlPolicies = &dummyRateLimitConfig{
 | 
			
		||||
		InvalidAuthorizationsPerAccountPolicy: ratelimit.RateLimitPolicy{
 | 
			
		||||
			Threshold: 1,
 | 
			
		||||
			Window:    config.Duration{Duration: 24 * time.Hour},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Requesting a new order for "example.com" should fail due to too many
 | 
			
		||||
	// failed authorizations.
 | 
			
		||||
	_, err = ra.NewOrder(ctx, exampleOrder)
 | 
			
		||||
	test.AssertError(t, err, "expected error for domain with too many failures")
 | 
			
		||||
 | 
			
		||||
	// Exempt the order from rate limiting.
 | 
			
		||||
	exampleOrder.IsARIRenewal = true
 | 
			
		||||
	_, err = ra.NewOrder(ctx, exampleOrder)
 | 
			
		||||
	test.AssertNotError(t, err, "limit exempt order should have succeeded")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// An authority that returns an error from NewOrderAndAuthzs if the
 | 
			
		||||
// "ReplacesSerial" field of the request is empty.
 | 
			
		||||
type mockNewOrderMustBeReplacementAuthority struct {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +0,0 @@
 | 
			
		|||
FailedAuthorizationsForPausingPerDomainPerAccount:
 | 
			
		||||
  count: 1
 | 
			
		||||
  burst: 1
 | 
			
		||||
  period: 24h
 | 
			
		||||
| 
						 | 
				
			
			@ -10,7 +10,7 @@ import (
 | 
			
		|||
// returns a Decision struct with the result of the decision and the updated
 | 
			
		||||
// TAT. The cost must be 0 or greater and <= the burst capacity of the limit.
 | 
			
		||||
func maybeSpend(clk clock.Clock, txn Transaction, tat time.Time) *Decision {
 | 
			
		||||
	if txn.cost < 0 || txn.cost > txn.limit.Burst {
 | 
			
		||||
	if txn.cost < 0 || txn.cost > txn.limit.burst {
 | 
			
		||||
		// The condition above is the union of the conditions checked in Check
 | 
			
		||||
		// and Spend methods of Limiter. If this panic is reached, it means that
 | 
			
		||||
		// the caller has introduced a bug.
 | 
			
		||||
| 
						 | 
				
			
			@ -67,7 +67,7 @@ func maybeSpend(clk clock.Clock, txn Transaction, tat time.Time) *Decision {
 | 
			
		|||
// or greater. A cost will only be refunded up to the burst capacity of the
 | 
			
		||||
// limit. A partial refund is still considered successful.
 | 
			
		||||
func maybeRefund(clk clock.Clock, txn Transaction, tat time.Time) *Decision {
 | 
			
		||||
	if txn.cost < 0 || txn.cost > txn.limit.Burst {
 | 
			
		||||
	if txn.cost < 0 || txn.cost > txn.limit.burst {
 | 
			
		||||
		// The condition above is checked in the Refund method of Limiter. If
 | 
			
		||||
		// this panic is reached, it means that the caller has introduced a bug.
 | 
			
		||||
		panic("invalid cost for maybeRefund")
 | 
			
		||||
| 
						 | 
				
			
			@ -80,7 +80,7 @@ func maybeRefund(clk clock.Clock, txn Transaction, tat time.Time) *Decision {
 | 
			
		|||
		// The TAT is in the past, therefore the bucket is full.
 | 
			
		||||
		return &Decision{
 | 
			
		||||
			allowed:     false,
 | 
			
		||||
			remaining:   txn.limit.Burst,
 | 
			
		||||
			remaining:   txn.limit.burst,
 | 
			
		||||
			retryIn:     time.Duration(0),
 | 
			
		||||
			resetIn:     time.Duration(0),
 | 
			
		||||
			newTAT:      tat,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,13 +5,14 @@ import (
 | 
			
		|||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/jmhodges/clock"
 | 
			
		||||
 | 
			
		||||
	"github.com/letsencrypt/boulder/config"
 | 
			
		||||
	"github.com/letsencrypt/boulder/test"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestDecide(t *testing.T) {
 | 
			
		||||
	clk := clock.NewFake()
 | 
			
		||||
	limit := &limit{Burst: 10, Count: 1, Period: config.Duration{Duration: time.Second}}
 | 
			
		||||
	limit := &limit{burst: 10, count: 1, period: config.Duration{Duration: time.Second}}
 | 
			
		||||
	limit.precompute()
 | 
			
		||||
 | 
			
		||||
	// Begin by using 1 of our 10 requests.
 | 
			
		||||
| 
						 | 
				
			
			@ -138,7 +139,7 @@ func TestDecide(t *testing.T) {
 | 
			
		|||
 | 
			
		||||
func TestMaybeRefund(t *testing.T) {
 | 
			
		||||
	clk := clock.NewFake()
 | 
			
		||||
	limit := &limit{Burst: 10, Count: 1, Period: config.Duration{Duration: time.Second}}
 | 
			
		||||
	limit := &limit{burst: 10, count: 1, period: config.Duration{Duration: time.Second}}
 | 
			
		||||
	limit.precompute()
 | 
			
		||||
 | 
			
		||||
	// Begin by using 1 of our 10 requests.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,11 +15,12 @@ import (
 | 
			
		|||
// currently configured.
 | 
			
		||||
var errLimitDisabled = errors.New("limit disabled")
 | 
			
		||||
 | 
			
		||||
// limit defines the configuration for a rate limit or a rate limit override.
 | 
			
		||||
// LimitConfig defines the exportable configuration for a rate limit or a rate
 | 
			
		||||
// limit override, without a `limit`'s internal fields.
 | 
			
		||||
//
 | 
			
		||||
// The zero value of this struct is invalid, because some of the fields must
 | 
			
		||||
// be greater than zero.
 | 
			
		||||
type limit struct {
 | 
			
		||||
// The zero value of this struct is invalid, because some of the fields must be
 | 
			
		||||
// greater than zero.
 | 
			
		||||
type LimitConfig struct {
 | 
			
		||||
	// Burst specifies maximum concurrent allowed requests at any given time. It
 | 
			
		||||
	// must be greater than zero.
 | 
			
		||||
	Burst int64
 | 
			
		||||
| 
						 | 
				
			
			@ -31,6 +32,26 @@ type limit struct {
 | 
			
		|||
	// Period is the duration of time in which the count (of requests) is
 | 
			
		||||
	// allowed. It must be greater than zero.
 | 
			
		||||
	Period config.Duration
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type LimitConfigs map[string]*LimitConfig
 | 
			
		||||
 | 
			
		||||
// limit defines the configuration for a rate limit or a rate limit override.
 | 
			
		||||
//
 | 
			
		||||
// The zero value of this struct is invalid, because some of the fields must
 | 
			
		||||
// be greater than zero.
 | 
			
		||||
type limit struct {
 | 
			
		||||
	// burst specifies maximum concurrent allowed requests at any given time. It
 | 
			
		||||
	// must be greater than zero.
 | 
			
		||||
	burst int64
 | 
			
		||||
 | 
			
		||||
	// count is the number of requests allowed per period. It must be greater
 | 
			
		||||
	// than zero.
 | 
			
		||||
	count int64
 | 
			
		||||
 | 
			
		||||
	// period is the duration of time in which the count (of requests) is
 | 
			
		||||
	// allowed. It must be greater than zero.
 | 
			
		||||
	period config.Duration
 | 
			
		||||
 | 
			
		||||
	// name is the name of the limit. It must be one of the Name enums defined
 | 
			
		||||
	// in this package.
 | 
			
		||||
| 
						 | 
				
			
			@ -59,19 +80,19 @@ func (l *limit) isOverride() bool {
 | 
			
		|||
 | 
			
		||||
// precompute calculates the emissionInterval and burstOffset for the limit.
 | 
			
		||||
func (l *limit) precompute() {
 | 
			
		||||
	l.emissionInterval = l.Period.Nanoseconds() / l.Count
 | 
			
		||||
	l.burstOffset = l.emissionInterval * l.Burst
 | 
			
		||||
	l.emissionInterval = l.period.Nanoseconds() / l.count
 | 
			
		||||
	l.burstOffset = l.emissionInterval * l.burst
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func validateLimit(l *limit) error {
 | 
			
		||||
	if l.Burst <= 0 {
 | 
			
		||||
		return fmt.Errorf("invalid burst '%d', must be > 0", l.Burst)
 | 
			
		||||
	if l.burst <= 0 {
 | 
			
		||||
		return fmt.Errorf("invalid burst '%d', must be > 0", l.burst)
 | 
			
		||||
	}
 | 
			
		||||
	if l.Count <= 0 {
 | 
			
		||||
		return fmt.Errorf("invalid count '%d', must be > 0", l.Count)
 | 
			
		||||
	if l.count <= 0 {
 | 
			
		||||
		return fmt.Errorf("invalid count '%d', must be > 0", l.count)
 | 
			
		||||
	}
 | 
			
		||||
	if l.Period.Duration <= 0 {
 | 
			
		||||
		return fmt.Errorf("invalid period '%s', must be > 0", l.Period)
 | 
			
		||||
	if l.period.Duration <= 0 {
 | 
			
		||||
		return fmt.Errorf("invalid period '%s', must be > 0", l.period)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -79,8 +100,8 @@ func validateLimit(l *limit) error {
 | 
			
		|||
type limits map[string]*limit
 | 
			
		||||
 | 
			
		||||
// loadDefaults marshals the defaults YAML file at path into a map of limits.
 | 
			
		||||
func loadDefaults(path string) (limits, error) {
 | 
			
		||||
	lm := make(limits)
 | 
			
		||||
func loadDefaults(path string) (LimitConfigs, error) {
 | 
			
		||||
	lm := make(LimitConfigs)
 | 
			
		||||
	data, err := os.ReadFile(path)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
| 
						 | 
				
			
			@ -93,7 +114,7 @@ func loadDefaults(path string) (limits, error) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
type overrideYAML struct {
 | 
			
		||||
	limit `yaml:",inline"`
 | 
			
		||||
	LimitConfig `yaml:",inline"`
 | 
			
		||||
	// Ids is a list of ids that this override applies to.
 | 
			
		||||
	Ids []struct {
 | 
			
		||||
		Id string `yaml:"id"`
 | 
			
		||||
| 
						 | 
				
			
			@ -142,30 +163,31 @@ func parseOverrideNameId(key string) (Name, string, error) {
 | 
			
		|||
	return name, id, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// loadAndParseOverrideLimits loads override limits from YAML. The YAML file
 | 
			
		||||
// must be formatted as a list of maps, where each map has a single key
 | 
			
		||||
// representing the limit name and a value that is a map containing the limit
 | 
			
		||||
// fields and an additional 'ids' field that is a list of ids that this override
 | 
			
		||||
// applies to.
 | 
			
		||||
func loadAndParseOverrideLimits(path string) (limits, error) {
 | 
			
		||||
	fromFile, err := loadOverrides(path)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
// parseOverrideLimits validates a YAML list of override limits. It must be
 | 
			
		||||
// formatted as a list of maps, where each map has a single key representing the
 | 
			
		||||
// limit name and a value that is a map containing the limit fields and an
 | 
			
		||||
// additional 'ids' field that is a list of ids that this override applies to.
 | 
			
		||||
func parseOverrideLimits(newOverridesYAML overridesYAML) (limits, error) {
 | 
			
		||||
	parsed := make(limits)
 | 
			
		||||
 | 
			
		||||
	for _, ov := range fromFile {
 | 
			
		||||
	for _, ov := range newOverridesYAML {
 | 
			
		||||
		for k, v := range ov {
 | 
			
		||||
			limit := &v.limit
 | 
			
		||||
			err = validateLimit(limit)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, fmt.Errorf("validating override limit %q: %w", k, err)
 | 
			
		||||
			}
 | 
			
		||||
			name, ok := stringToName[k]
 | 
			
		||||
			if !ok {
 | 
			
		||||
				return nil, fmt.Errorf("unrecognized name %q in override limit, must be one of %v", k, limitNames)
 | 
			
		||||
			}
 | 
			
		||||
			v.limit.name = name
 | 
			
		||||
 | 
			
		||||
			lim := &limit{
 | 
			
		||||
				burst:  v.Burst,
 | 
			
		||||
				count:  v.Count,
 | 
			
		||||
				period: v.Period,
 | 
			
		||||
				name:   name,
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			err := validateLimit(lim)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, fmt.Errorf("validating override limit %q: %w", k, err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			for _, entry := range v.Ids {
 | 
			
		||||
				id := entry.Id
 | 
			
		||||
| 
						 | 
				
			
			@ -174,42 +196,45 @@ func loadAndParseOverrideLimits(path string) (limits, error) {
 | 
			
		|||
					return nil, fmt.Errorf(
 | 
			
		||||
						"validating name %s and id %q for override limit %q: %w", name, id, k, err)
 | 
			
		||||
				}
 | 
			
		||||
				limit.overrideKey = joinWithColon(name.EnumString(), id)
 | 
			
		||||
				lim.overrideKey = joinWithColon(name.EnumString(), id)
 | 
			
		||||
				if name == CertificatesPerFQDNSet {
 | 
			
		||||
					// FQDNSet hashes are not a nice thing to ask for in a
 | 
			
		||||
					// config file, so we allow the user to specify a
 | 
			
		||||
					// comma-separated list of FQDNs and compute the hash here.
 | 
			
		||||
					id = fmt.Sprintf("%x", core.HashNames(strings.Split(id, ",")))
 | 
			
		||||
				}
 | 
			
		||||
				limit.precompute()
 | 
			
		||||
				parsed[joinWithColon(name.EnumString(), id)] = limit
 | 
			
		||||
				lim.precompute()
 | 
			
		||||
				parsed[joinWithColon(name.EnumString(), id)] = lim
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return parsed, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// loadAndParseDefaultLimits loads default limits from YAML, validates them, and
 | 
			
		||||
// parses them into a map of limits keyed by 'Name'.
 | 
			
		||||
func loadAndParseDefaultLimits(path string) (limits, error) {
 | 
			
		||||
	fromFile, err := loadDefaults(path)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	parsed := make(limits, len(fromFile))
 | 
			
		||||
// parseDefaultLimits validates a map of default limits and rekeys it by 'Name'.
 | 
			
		||||
func parseDefaultLimits(newDefaultLimits LimitConfigs) (limits, error) {
 | 
			
		||||
	parsed := make(limits)
 | 
			
		||||
 | 
			
		||||
	for k, v := range fromFile {
 | 
			
		||||
		err := validateLimit(v)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, fmt.Errorf("parsing default limit %q: %w", k, err)
 | 
			
		||||
		}
 | 
			
		||||
	for k, v := range newDefaultLimits {
 | 
			
		||||
		name, ok := stringToName[k]
 | 
			
		||||
		if !ok {
 | 
			
		||||
			return nil, fmt.Errorf("unrecognized name %q in default limit, must be one of %v", k, limitNames)
 | 
			
		||||
		}
 | 
			
		||||
		v.name = name
 | 
			
		||||
		v.precompute()
 | 
			
		||||
		parsed[name.EnumString()] = v
 | 
			
		||||
 | 
			
		||||
		lim := &limit{
 | 
			
		||||
			burst:  v.Burst,
 | 
			
		||||
			count:  v.Count,
 | 
			
		||||
			period: v.Period,
 | 
			
		||||
			name:   name,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err := validateLimit(lim)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, fmt.Errorf("parsing default limit %q: %w", k, err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		lim.precompute()
 | 
			
		||||
		parsed[name.EnumString()] = lim
 | 
			
		||||
	}
 | 
			
		||||
	return parsed, nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -222,26 +247,39 @@ type limitRegistry struct {
 | 
			
		|||
	overrides limits
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func newLimitRegistry(defaults, overrides string) (*limitRegistry, error) {
 | 
			
		||||
	var err error
 | 
			
		||||
	registry := &limitRegistry{}
 | 
			
		||||
	registry.defaults, err = loadAndParseDefaultLimits(defaults)
 | 
			
		||||
func newLimitRegistryFromFiles(defaults, overrides string) (*limitRegistry, error) {
 | 
			
		||||
	defaultsData, err := loadDefaults(defaults)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if overrides == "" {
 | 
			
		||||
		// No overrides specified, initialize an empty map.
 | 
			
		||||
		registry.overrides = make(limits)
 | 
			
		||||
		return registry, nil
 | 
			
		||||
		return newLimitRegistry(defaultsData, nil)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	registry.overrides, err = loadAndParseOverrideLimits(overrides)
 | 
			
		||||
	overridesData, err := loadOverrides(overrides)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return registry, nil
 | 
			
		||||
	return newLimitRegistry(defaultsData, overridesData)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func newLimitRegistry(defaults LimitConfigs, overrides overridesYAML) (*limitRegistry, error) {
 | 
			
		||||
	regDefaults, err := parseDefaultLimits(defaults)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	regOverrides, err := parseOverrideLimits(overrides)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &limitRegistry{
 | 
			
		||||
		defaults:  regDefaults,
 | 
			
		||||
		overrides: regOverrides,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// getLimit returns the limit for the specified by name and bucketKey, name is
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,6 +9,32 @@ import (
 | 
			
		|||
	"github.com/letsencrypt/boulder/test"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// loadAndParseDefaultLimits is a helper that calls both loadDefaults and
 | 
			
		||||
// parseDefaultLimits to handle a YAML file.
 | 
			
		||||
//
 | 
			
		||||
// TODO(#7901): Update the tests to test these functions individually.
 | 
			
		||||
func loadAndParseDefaultLimits(path string) (limits, error) {
 | 
			
		||||
	fromFile, err := loadDefaults(path)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return parseDefaultLimits(fromFile)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// loadAndParseOverrideLimits is a helper that calls both loadOverrides and
 | 
			
		||||
// parseOverrideLimits to handle a YAML file.
 | 
			
		||||
//
 | 
			
		||||
// TODO(#7901): Update the tests to test these functions individually.
 | 
			
		||||
func loadAndParseOverrideLimits(path string) (limits, error) {
 | 
			
		||||
	fromFile, err := loadOverrides(path)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return parseOverrideLimits(fromFile)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestParseOverrideNameId(t *testing.T) {
 | 
			
		||||
	// 'enum:ipv4'
 | 
			
		||||
	// Valid IPv4 address.
 | 
			
		||||
| 
						 | 
				
			
			@ -42,14 +68,14 @@ func TestParseOverrideNameId(t *testing.T) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
func TestValidateLimit(t *testing.T) {
 | 
			
		||||
	err := validateLimit(&limit{Burst: 1, Count: 1, Period: config.Duration{Duration: time.Second}})
 | 
			
		||||
	err := validateLimit(&limit{burst: 1, count: 1, period: config.Duration{Duration: time.Second}})
 | 
			
		||||
	test.AssertNotError(t, err, "valid limit")
 | 
			
		||||
 | 
			
		||||
	// All of the following are invalid.
 | 
			
		||||
	for _, l := range []*limit{
 | 
			
		||||
		{Burst: 0, Count: 1, Period: config.Duration{Duration: time.Second}},
 | 
			
		||||
		{Burst: 1, Count: 0, Period: config.Duration{Duration: time.Second}},
 | 
			
		||||
		{Burst: 1, Count: 1, Period: config.Duration{Duration: 0}},
 | 
			
		||||
		{burst: 0, count: 1, period: config.Duration{Duration: time.Second}},
 | 
			
		||||
		{burst: 1, count: 0, period: config.Duration{Duration: time.Second}},
 | 
			
		||||
		{burst: 1, count: 1, period: config.Duration{Duration: 0}},
 | 
			
		||||
	} {
 | 
			
		||||
		err = validateLimit(l)
 | 
			
		||||
		test.AssertError(t, err, "limit should be invalid")
 | 
			
		||||
| 
						 | 
				
			
			@ -61,29 +87,29 @@ func TestLoadAndParseOverrideLimits(t *testing.T) {
 | 
			
		|||
	l, err := loadAndParseOverrideLimits("testdata/working_override.yml")
 | 
			
		||||
	test.AssertNotError(t, err, "valid single override limit")
 | 
			
		||||
	expectKey := joinWithColon(NewRegistrationsPerIPAddress.EnumString(), "10.0.0.2")
 | 
			
		||||
	test.AssertEquals(t, l[expectKey].Burst, int64(40))
 | 
			
		||||
	test.AssertEquals(t, l[expectKey].Count, int64(40))
 | 
			
		||||
	test.AssertEquals(t, l[expectKey].Period.Duration, time.Second)
 | 
			
		||||
	test.AssertEquals(t, l[expectKey].burst, int64(40))
 | 
			
		||||
	test.AssertEquals(t, l[expectKey].count, int64(40))
 | 
			
		||||
	test.AssertEquals(t, l[expectKey].period.Duration, time.Second)
 | 
			
		||||
 | 
			
		||||
	// Load single valid override limit with a 'domain' Id.
 | 
			
		||||
	l, err = loadAndParseOverrideLimits("testdata/working_override_regid_domain.yml")
 | 
			
		||||
	test.AssertNotError(t, err, "valid single override limit with Id of regId:domain")
 | 
			
		||||
	expectKey = joinWithColon(CertificatesPerDomain.EnumString(), "example.com")
 | 
			
		||||
	test.AssertEquals(t, l[expectKey].Burst, int64(40))
 | 
			
		||||
	test.AssertEquals(t, l[expectKey].Count, int64(40))
 | 
			
		||||
	test.AssertEquals(t, l[expectKey].Period.Duration, time.Second)
 | 
			
		||||
	test.AssertEquals(t, l[expectKey].burst, int64(40))
 | 
			
		||||
	test.AssertEquals(t, l[expectKey].count, int64(40))
 | 
			
		||||
	test.AssertEquals(t, l[expectKey].period.Duration, time.Second)
 | 
			
		||||
 | 
			
		||||
	// Load multiple valid override limits with 'regId' Ids.
 | 
			
		||||
	l, err = loadAndParseOverrideLimits("testdata/working_overrides.yml")
 | 
			
		||||
	test.AssertNotError(t, err, "multiple valid override limits")
 | 
			
		||||
	expectKey1 := joinWithColon(NewRegistrationsPerIPAddress.EnumString(), "10.0.0.2")
 | 
			
		||||
	test.AssertEquals(t, l[expectKey1].Burst, int64(40))
 | 
			
		||||
	test.AssertEquals(t, l[expectKey1].Count, int64(40))
 | 
			
		||||
	test.AssertEquals(t, l[expectKey1].Period.Duration, time.Second)
 | 
			
		||||
	test.AssertEquals(t, l[expectKey1].burst, int64(40))
 | 
			
		||||
	test.AssertEquals(t, l[expectKey1].count, int64(40))
 | 
			
		||||
	test.AssertEquals(t, l[expectKey1].period.Duration, time.Second)
 | 
			
		||||
	expectKey2 := joinWithColon(NewRegistrationsPerIPv6Range.EnumString(), "2001:0db8:0000::/48")
 | 
			
		||||
	test.AssertEquals(t, l[expectKey2].Burst, int64(50))
 | 
			
		||||
	test.AssertEquals(t, l[expectKey2].Count, int64(50))
 | 
			
		||||
	test.AssertEquals(t, l[expectKey2].Period.Duration, time.Second*2)
 | 
			
		||||
	test.AssertEquals(t, l[expectKey2].burst, int64(50))
 | 
			
		||||
	test.AssertEquals(t, l[expectKey2].count, int64(50))
 | 
			
		||||
	test.AssertEquals(t, l[expectKey2].period.Duration, time.Second*2)
 | 
			
		||||
 | 
			
		||||
	// Load multiple valid override limits with 'fqdnSet' Ids, as follows:
 | 
			
		||||
	//   - CertificatesPerFQDNSet:example.com
 | 
			
		||||
| 
						 | 
				
			
			@ -97,15 +123,15 @@ func TestLoadAndParseOverrideLimits(t *testing.T) {
 | 
			
		|||
	test.AssertNotError(t, err, "valid fqdnSet with three domains should not fail")
 | 
			
		||||
	l, err = loadAndParseOverrideLimits("testdata/working_overrides_regid_fqdnset.yml")
 | 
			
		||||
	test.AssertNotError(t, err, "multiple valid override limits with 'fqdnSet' Ids")
 | 
			
		||||
	test.AssertEquals(t, l[firstEntryKey].Burst, int64(40))
 | 
			
		||||
	test.AssertEquals(t, l[firstEntryKey].Count, int64(40))
 | 
			
		||||
	test.AssertEquals(t, l[firstEntryKey].Period.Duration, time.Second)
 | 
			
		||||
	test.AssertEquals(t, l[secondEntryKey].Burst, int64(50))
 | 
			
		||||
	test.AssertEquals(t, l[secondEntryKey].Count, int64(50))
 | 
			
		||||
	test.AssertEquals(t, l[secondEntryKey].Period.Duration, time.Second*2)
 | 
			
		||||
	test.AssertEquals(t, l[thirdEntryKey].Burst, int64(60))
 | 
			
		||||
	test.AssertEquals(t, l[thirdEntryKey].Count, int64(60))
 | 
			
		||||
	test.AssertEquals(t, l[thirdEntryKey].Period.Duration, time.Second*3)
 | 
			
		||||
	test.AssertEquals(t, l[firstEntryKey].burst, int64(40))
 | 
			
		||||
	test.AssertEquals(t, l[firstEntryKey].count, int64(40))
 | 
			
		||||
	test.AssertEquals(t, l[firstEntryKey].period.Duration, time.Second)
 | 
			
		||||
	test.AssertEquals(t, l[secondEntryKey].burst, int64(50))
 | 
			
		||||
	test.AssertEquals(t, l[secondEntryKey].count, int64(50))
 | 
			
		||||
	test.AssertEquals(t, l[secondEntryKey].period.Duration, time.Second*2)
 | 
			
		||||
	test.AssertEquals(t, l[thirdEntryKey].burst, int64(60))
 | 
			
		||||
	test.AssertEquals(t, l[thirdEntryKey].count, int64(60))
 | 
			
		||||
	test.AssertEquals(t, l[thirdEntryKey].period.Duration, time.Second*3)
 | 
			
		||||
 | 
			
		||||
	// Path is empty string.
 | 
			
		||||
	_, err = loadAndParseOverrideLimits("")
 | 
			
		||||
| 
						 | 
				
			
			@ -152,19 +178,19 @@ func TestLoadAndParseDefaultLimits(t *testing.T) {
 | 
			
		|||
	// Load a single valid default limit.
 | 
			
		||||
	l, err := loadAndParseDefaultLimits("testdata/working_default.yml")
 | 
			
		||||
	test.AssertNotError(t, err, "valid single default limit")
 | 
			
		||||
	test.AssertEquals(t, l[NewRegistrationsPerIPAddress.EnumString()].Burst, int64(20))
 | 
			
		||||
	test.AssertEquals(t, l[NewRegistrationsPerIPAddress.EnumString()].Count, int64(20))
 | 
			
		||||
	test.AssertEquals(t, l[NewRegistrationsPerIPAddress.EnumString()].Period.Duration, time.Second)
 | 
			
		||||
	test.AssertEquals(t, l[NewRegistrationsPerIPAddress.EnumString()].burst, int64(20))
 | 
			
		||||
	test.AssertEquals(t, l[NewRegistrationsPerIPAddress.EnumString()].count, int64(20))
 | 
			
		||||
	test.AssertEquals(t, l[NewRegistrationsPerIPAddress.EnumString()].period.Duration, time.Second)
 | 
			
		||||
 | 
			
		||||
	// Load multiple valid default limits.
 | 
			
		||||
	l, err = loadAndParseDefaultLimits("testdata/working_defaults.yml")
 | 
			
		||||
	test.AssertNotError(t, err, "multiple valid default limits")
 | 
			
		||||
	test.AssertEquals(t, l[NewRegistrationsPerIPAddress.EnumString()].Burst, int64(20))
 | 
			
		||||
	test.AssertEquals(t, l[NewRegistrationsPerIPAddress.EnumString()].Count, int64(20))
 | 
			
		||||
	test.AssertEquals(t, l[NewRegistrationsPerIPAddress.EnumString()].Period.Duration, time.Second)
 | 
			
		||||
	test.AssertEquals(t, l[NewRegistrationsPerIPv6Range.EnumString()].Burst, int64(30))
 | 
			
		||||
	test.AssertEquals(t, l[NewRegistrationsPerIPv6Range.EnumString()].Count, int64(30))
 | 
			
		||||
	test.AssertEquals(t, l[NewRegistrationsPerIPv6Range.EnumString()].Period.Duration, time.Second*2)
 | 
			
		||||
	test.AssertEquals(t, l[NewRegistrationsPerIPAddress.EnumString()].burst, int64(20))
 | 
			
		||||
	test.AssertEquals(t, l[NewRegistrationsPerIPAddress.EnumString()].count, int64(20))
 | 
			
		||||
	test.AssertEquals(t, l[NewRegistrationsPerIPAddress.EnumString()].period.Duration, time.Second)
 | 
			
		||||
	test.AssertEquals(t, l[NewRegistrationsPerIPv6Range.EnumString()].burst, int64(30))
 | 
			
		||||
	test.AssertEquals(t, l[NewRegistrationsPerIPv6Range.EnumString()].count, int64(30))
 | 
			
		||||
	test.AssertEquals(t, l[NewRegistrationsPerIPv6Range.EnumString()].period.Duration, time.Second*2)
 | 
			
		||||
 | 
			
		||||
	// Path is empty string.
 | 
			
		||||
	_, err = loadAndParseDefaultLimits("")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -117,8 +117,8 @@ func (d *Decision) Result(now time.Time) error {
 | 
			
		|||
		return berrors.RegistrationsPerIPAddressError(
 | 
			
		||||
			retryAfter,
 | 
			
		||||
			"too many new registrations (%d) from this IP address in the last %s, retry after %s",
 | 
			
		||||
			d.transaction.limit.Burst,
 | 
			
		||||
			d.transaction.limit.Period.Duration,
 | 
			
		||||
			d.transaction.limit.burst,
 | 
			
		||||
			d.transaction.limit.period.Duration,
 | 
			
		||||
			retryAfterTs,
 | 
			
		||||
		)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -126,16 +126,16 @@ func (d *Decision) Result(now time.Time) error {
 | 
			
		|||
		return berrors.RegistrationsPerIPv6RangeError(
 | 
			
		||||
			retryAfter,
 | 
			
		||||
			"too many new registrations (%d) from this /48 subnet of IPv6 addresses in the last %s, retry after %s",
 | 
			
		||||
			d.transaction.limit.Burst,
 | 
			
		||||
			d.transaction.limit.Period.Duration,
 | 
			
		||||
			d.transaction.limit.burst,
 | 
			
		||||
			d.transaction.limit.period.Duration,
 | 
			
		||||
			retryAfterTs,
 | 
			
		||||
		)
 | 
			
		||||
	case NewOrdersPerAccount:
 | 
			
		||||
		return berrors.NewOrdersPerAccountError(
 | 
			
		||||
			retryAfter,
 | 
			
		||||
			"too many new orders (%d) from this account in the last %s, retry after %s",
 | 
			
		||||
			d.transaction.limit.Burst,
 | 
			
		||||
			d.transaction.limit.Period.Duration,
 | 
			
		||||
			d.transaction.limit.burst,
 | 
			
		||||
			d.transaction.limit.period.Duration,
 | 
			
		||||
			retryAfterTs,
 | 
			
		||||
		)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -149,9 +149,9 @@ func (d *Decision) Result(now time.Time) error {
 | 
			
		|||
		return berrors.FailedAuthorizationsPerDomainPerAccountError(
 | 
			
		||||
			retryAfter,
 | 
			
		||||
			"too many failed authorizations (%d) for %q in the last %s, retry after %s",
 | 
			
		||||
			d.transaction.limit.Burst,
 | 
			
		||||
			d.transaction.limit.burst,
 | 
			
		||||
			domain,
 | 
			
		||||
			d.transaction.limit.Period.Duration,
 | 
			
		||||
			d.transaction.limit.period.Duration,
 | 
			
		||||
			retryAfterTs,
 | 
			
		||||
		)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -165,9 +165,9 @@ func (d *Decision) Result(now time.Time) error {
 | 
			
		|||
		return berrors.CertificatesPerDomainError(
 | 
			
		||||
			retryAfter,
 | 
			
		||||
			"too many certificates (%d) already issued for %q in the last %s, retry after %s",
 | 
			
		||||
			d.transaction.limit.Burst,
 | 
			
		||||
			d.transaction.limit.burst,
 | 
			
		||||
			domain,
 | 
			
		||||
			d.transaction.limit.Period.Duration,
 | 
			
		||||
			d.transaction.limit.period.Duration,
 | 
			
		||||
			retryAfterTs,
 | 
			
		||||
		)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -175,8 +175,8 @@ func (d *Decision) Result(now time.Time) error {
 | 
			
		|||
		return berrors.CertificatesPerFQDNSetError(
 | 
			
		||||
			retryAfter,
 | 
			
		||||
			"too many certificates (%d) already issued for this exact set of domains in the last %s, retry after %s",
 | 
			
		||||
			d.transaction.limit.Burst,
 | 
			
		||||
			d.transaction.limit.Period.Duration,
 | 
			
		||||
			d.transaction.limit.burst,
 | 
			
		||||
			d.transaction.limit.period.Duration,
 | 
			
		||||
			retryAfterTs,
 | 
			
		||||
		)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -285,7 +285,7 @@ func (l *Limiter) BatchSpend(ctx context.Context, txns []Transaction) (*Decision
 | 
			
		|||
		d := maybeSpend(l.clk, txn, storedTAT)
 | 
			
		||||
 | 
			
		||||
		if txn.limit.isOverride() {
 | 
			
		||||
			utilization := float64(txn.limit.Burst-d.remaining) / float64(txn.limit.Burst)
 | 
			
		||||
			utilization := float64(txn.limit.burst-d.remaining) / float64(txn.limit.burst)
 | 
			
		||||
			l.overrideUsageGauge.WithLabelValues(txn.limit.name.String(), txn.limit.overrideKey).Set(utilization)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -32,7 +32,7 @@ func newTestLimiter(t *testing.T, s Source, clk clock.FakeClock) *Limiter {
 | 
			
		|||
//   - 'NewRegistrationsPerIPAddress' burst: 20 count: 20 period: 1s
 | 
			
		||||
//   - 'NewRegistrationsPerIPAddress:10.0.0.2' burst: 40 count: 40 period: 1s
 | 
			
		||||
func newTestTransactionBuilder(t *testing.T) *TransactionBuilder {
 | 
			
		||||
	c, err := NewTransactionBuilder("testdata/working_default.yml", "testdata/working_override.yml")
 | 
			
		||||
	c, err := NewTransactionBuilderFromFiles("testdata/working_default.yml", "testdata/working_override.yml")
 | 
			
		||||
	test.AssertNotError(t, err, "should not error")
 | 
			
		||||
	return c
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -484,8 +484,8 @@ func TestRateLimitError(t *testing.T) {
 | 
			
		|||
				transaction: Transaction{
 | 
			
		||||
					limit: &limit{
 | 
			
		||||
						name:   NewRegistrationsPerIPAddress,
 | 
			
		||||
						Burst:  10,
 | 
			
		||||
						Period: config.Duration{Duration: time.Hour},
 | 
			
		||||
						burst:  10,
 | 
			
		||||
						period: config.Duration{Duration: time.Hour},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
| 
						 | 
				
			
			@ -500,8 +500,8 @@ func TestRateLimitError(t *testing.T) {
 | 
			
		|||
				transaction: Transaction{
 | 
			
		||||
					limit: &limit{
 | 
			
		||||
						name:   NewRegistrationsPerIPv6Range,
 | 
			
		||||
						Burst:  5,
 | 
			
		||||
						Period: config.Duration{Duration: time.Hour},
 | 
			
		||||
						burst:  5,
 | 
			
		||||
						period: config.Duration{Duration: time.Hour},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
| 
						 | 
				
			
			@ -516,8 +516,8 @@ func TestRateLimitError(t *testing.T) {
 | 
			
		|||
				transaction: Transaction{
 | 
			
		||||
					limit: &limit{
 | 
			
		||||
						name:   NewOrdersPerAccount,
 | 
			
		||||
						Burst:  2,
 | 
			
		||||
						Period: config.Duration{Duration: time.Hour},
 | 
			
		||||
						burst:  2,
 | 
			
		||||
						period: config.Duration{Duration: time.Hour},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
| 
						 | 
				
			
			@ -532,8 +532,8 @@ func TestRateLimitError(t *testing.T) {
 | 
			
		|||
				transaction: Transaction{
 | 
			
		||||
					limit: &limit{
 | 
			
		||||
						name:   FailedAuthorizationsPerDomainPerAccount,
 | 
			
		||||
						Burst:  7,
 | 
			
		||||
						Period: config.Duration{Duration: time.Hour},
 | 
			
		||||
						burst:  7,
 | 
			
		||||
						period: config.Duration{Duration: time.Hour},
 | 
			
		||||
					},
 | 
			
		||||
					bucketKey: "4:12345:example.com",
 | 
			
		||||
				},
 | 
			
		||||
| 
						 | 
				
			
			@ -549,8 +549,8 @@ func TestRateLimitError(t *testing.T) {
 | 
			
		|||
				transaction: Transaction{
 | 
			
		||||
					limit: &limit{
 | 
			
		||||
						name:   CertificatesPerDomain,
 | 
			
		||||
						Burst:  3,
 | 
			
		||||
						Period: config.Duration{Duration: time.Hour},
 | 
			
		||||
						burst:  3,
 | 
			
		||||
						period: config.Duration{Duration: time.Hour},
 | 
			
		||||
					},
 | 
			
		||||
					bucketKey: "5:example.org",
 | 
			
		||||
				},
 | 
			
		||||
| 
						 | 
				
			
			@ -566,8 +566,8 @@ func TestRateLimitError(t *testing.T) {
 | 
			
		|||
				transaction: Transaction{
 | 
			
		||||
					limit: &limit{
 | 
			
		||||
						name:   CertificatesPerDomainPerAccount,
 | 
			
		||||
						Burst:  3,
 | 
			
		||||
						Period: config.Duration{Duration: time.Hour},
 | 
			
		||||
						burst:  3,
 | 
			
		||||
						period: config.Duration{Duration: time.Hour},
 | 
			
		||||
					},
 | 
			
		||||
					bucketKey: "6:12345678:example.net",
 | 
			
		||||
				},
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -129,7 +129,7 @@ func validateTransaction(txn Transaction) (Transaction, error) {
 | 
			
		|||
	if txn.cost < 0 {
 | 
			
		||||
		return Transaction{}, ErrInvalidCost
 | 
			
		||||
	}
 | 
			
		||||
	if txn.limit.Burst == 0 {
 | 
			
		||||
	if txn.limit.burst == 0 {
 | 
			
		||||
		// This should never happen. If the limit was loaded from a file,
 | 
			
		||||
		// Burst was validated then. If this is a zero-valued Transaction
 | 
			
		||||
		// (that is, an allow-only transaction), then validateTransaction
 | 
			
		||||
| 
						 | 
				
			
			@ -137,7 +137,7 @@ func validateTransaction(txn Transaction) (Transaction, error) {
 | 
			
		|||
		// valid.
 | 
			
		||||
		return Transaction{}, fmt.Errorf("invalid limit, burst must be > 0")
 | 
			
		||||
	}
 | 
			
		||||
	if txn.cost > txn.limit.Burst {
 | 
			
		||||
	if txn.cost > txn.limit.burst {
 | 
			
		||||
		return Transaction{}, ErrInvalidCostOverLimit
 | 
			
		||||
	}
 | 
			
		||||
	return txn, nil
 | 
			
		||||
| 
						 | 
				
			
			@ -183,12 +183,23 @@ type TransactionBuilder struct {
 | 
			
		|||
	*limitRegistry
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewTransactionBuilderFromFiles returns a new *TransactionBuilder. The
 | 
			
		||||
// provided defaults and overrides paths are expected to be paths to YAML files
 | 
			
		||||
// that contain the default and override limits, respectively. Overrides is
 | 
			
		||||
// optional, defaults is required.
 | 
			
		||||
func NewTransactionBuilderFromFiles(defaults, overrides string) (*TransactionBuilder, error) {
 | 
			
		||||
	registry, err := newLimitRegistryFromFiles(defaults, overrides)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return &TransactionBuilder{registry}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewTransactionBuilder returns a new *TransactionBuilder. The provided
 | 
			
		||||
// defaults and overrides paths are expected to be paths to YAML files that
 | 
			
		||||
// contain the default and override limits, respectively. Overrides is optional,
 | 
			
		||||
// defaults is required.
 | 
			
		||||
func NewTransactionBuilder(defaults, overrides string) (*TransactionBuilder, error) {
 | 
			
		||||
	registry, err := newLimitRegistry(defaults, overrides)
 | 
			
		||||
// defaults map is expected to contain default limit data. Overrides are not
 | 
			
		||||
// supported. Defaults is required.
 | 
			
		||||
func NewTransactionBuilder(defaults LimitConfigs) (*TransactionBuilder, error) {
 | 
			
		||||
	registry, err := newLimitRegistry(defaults, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,17 +5,19 @@ import (
 | 
			
		|||
	"net"
 | 
			
		||||
	"sort"
 | 
			
		||||
	"testing"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/letsencrypt/boulder/config"
 | 
			
		||||
	"github.com/letsencrypt/boulder/core"
 | 
			
		||||
	"github.com/letsencrypt/boulder/test"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestNewTransactionBuilder_WithBadLimitsPath(t *testing.T) {
 | 
			
		||||
func TestNewTransactionBuilderFromFiles_WithBadLimitsPath(t *testing.T) {
 | 
			
		||||
	t.Parallel()
 | 
			
		||||
	_, err := NewTransactionBuilder("testdata/does-not-exist.yml", "")
 | 
			
		||||
	_, err := NewTransactionBuilderFromFiles("testdata/does-not-exist.yml", "")
 | 
			
		||||
	test.AssertError(t, err, "should error")
 | 
			
		||||
 | 
			
		||||
	_, err = NewTransactionBuilder("testdata/defaults.yml", "testdata/does-not-exist.yml")
 | 
			
		||||
	_, err = NewTransactionBuilderFromFiles("testdata/defaults.yml", "testdata/does-not-exist.yml")
 | 
			
		||||
	test.AssertError(t, err, "should error")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -29,7 +31,7 @@ func sortTransactions(txns []Transaction) []Transaction {
 | 
			
		|||
func TestNewRegistrationsPerIPAddressTransactions(t *testing.T) {
 | 
			
		||||
	t.Parallel()
 | 
			
		||||
 | 
			
		||||
	tb, err := NewTransactionBuilder("../test/config-next/wfe2-ratelimit-defaults.yml", "")
 | 
			
		||||
	tb, err := NewTransactionBuilderFromFiles("../test/config-next/wfe2-ratelimit-defaults.yml", "")
 | 
			
		||||
	test.AssertNotError(t, err, "creating TransactionBuilder")
 | 
			
		||||
 | 
			
		||||
	// A check-and-spend transaction for the global limit.
 | 
			
		||||
| 
						 | 
				
			
			@ -42,7 +44,7 @@ func TestNewRegistrationsPerIPAddressTransactions(t *testing.T) {
 | 
			
		|||
func TestNewRegistrationsPerIPv6AddressTransactions(t *testing.T) {
 | 
			
		||||
	t.Parallel()
 | 
			
		||||
 | 
			
		||||
	tb, err := NewTransactionBuilder("../test/config-next/wfe2-ratelimit-defaults.yml", "")
 | 
			
		||||
	tb, err := NewTransactionBuilderFromFiles("../test/config-next/wfe2-ratelimit-defaults.yml", "")
 | 
			
		||||
	test.AssertNotError(t, err, "creating TransactionBuilder")
 | 
			
		||||
 | 
			
		||||
	// A check-and-spend transaction for the global limit.
 | 
			
		||||
| 
						 | 
				
			
			@ -55,7 +57,7 @@ func TestNewRegistrationsPerIPv6AddressTransactions(t *testing.T) {
 | 
			
		|||
func TestNewOrdersPerAccountTransactions(t *testing.T) {
 | 
			
		||||
	t.Parallel()
 | 
			
		||||
 | 
			
		||||
	tb, err := NewTransactionBuilder("../test/config-next/wfe2-ratelimit-defaults.yml", "")
 | 
			
		||||
	tb, err := NewTransactionBuilderFromFiles("../test/config-next/wfe2-ratelimit-defaults.yml", "")
 | 
			
		||||
	test.AssertNotError(t, err, "creating TransactionBuilder")
 | 
			
		||||
 | 
			
		||||
	// A check-and-spend transaction for the global limit.
 | 
			
		||||
| 
						 | 
				
			
			@ -68,7 +70,7 @@ func TestNewOrdersPerAccountTransactions(t *testing.T) {
 | 
			
		|||
func TestFailedAuthorizationsPerDomainPerAccountTransactions(t *testing.T) {
 | 
			
		||||
	t.Parallel()
 | 
			
		||||
 | 
			
		||||
	tb, err := NewTransactionBuilder("../test/config-next/wfe2-ratelimit-defaults.yml", "testdata/working_override_13371338.yml")
 | 
			
		||||
	tb, err := NewTransactionBuilderFromFiles("../test/config-next/wfe2-ratelimit-defaults.yml", "testdata/working_override_13371338.yml")
 | 
			
		||||
	test.AssertNotError(t, err, "creating TransactionBuilder")
 | 
			
		||||
 | 
			
		||||
	// A check-only transaction for the default per-account limit.
 | 
			
		||||
| 
						 | 
				
			
			@ -105,7 +107,7 @@ func TestFailedAuthorizationsPerDomainPerAccountTransactions(t *testing.T) {
 | 
			
		|||
func TestFailedAuthorizationsForPausingPerDomainPerAccountTransactions(t *testing.T) {
 | 
			
		||||
	t.Parallel()
 | 
			
		||||
 | 
			
		||||
	tb, err := NewTransactionBuilder("../test/config-next/wfe2-ratelimit-defaults.yml", "testdata/working_override_13371338.yml")
 | 
			
		||||
	tb, err := NewTransactionBuilderFromFiles("../test/config-next/wfe2-ratelimit-defaults.yml", "testdata/working_override_13371338.yml")
 | 
			
		||||
	test.AssertNotError(t, err, "creating TransactionBuilder")
 | 
			
		||||
 | 
			
		||||
	// A transaction for the per-account limit override.
 | 
			
		||||
| 
						 | 
				
			
			@ -119,7 +121,7 @@ func TestFailedAuthorizationsForPausingPerDomainPerAccountTransactions(t *testin
 | 
			
		|||
func TestCertificatesPerDomainTransactions(t *testing.T) {
 | 
			
		||||
	t.Parallel()
 | 
			
		||||
 | 
			
		||||
	tb, err := NewTransactionBuilder("../test/config-next/wfe2-ratelimit-defaults.yml", "")
 | 
			
		||||
	tb, err := NewTransactionBuilderFromFiles("../test/config-next/wfe2-ratelimit-defaults.yml", "")
 | 
			
		||||
	test.AssertNotError(t, err, "creating TransactionBuilder")
 | 
			
		||||
 | 
			
		||||
	// One check-only transaction for the global limit.
 | 
			
		||||
| 
						 | 
				
			
			@ -140,7 +142,7 @@ func TestCertificatesPerDomainTransactions(t *testing.T) {
 | 
			
		|||
func TestCertificatesPerDomainPerAccountTransactions(t *testing.T) {
 | 
			
		||||
	t.Parallel()
 | 
			
		||||
 | 
			
		||||
	tb, err := NewTransactionBuilder("../test/config-next/wfe2-ratelimit-defaults.yml", "testdata/working_override_13371338.yml")
 | 
			
		||||
	tb, err := NewTransactionBuilderFromFiles("../test/config-next/wfe2-ratelimit-defaults.yml", "testdata/working_override_13371338.yml")
 | 
			
		||||
	test.AssertNotError(t, err, "creating TransactionBuilder")
 | 
			
		||||
 | 
			
		||||
	// We only expect a single check-only transaction for the per-account limit
 | 
			
		||||
| 
						 | 
				
			
			@ -191,7 +193,7 @@ func TestCertificatesPerDomainPerAccountTransactions(t *testing.T) {
 | 
			
		|||
func TestCertificatesPerFQDNSetTransactions(t *testing.T) {
 | 
			
		||||
	t.Parallel()
 | 
			
		||||
 | 
			
		||||
	tb, err := NewTransactionBuilder("../test/config-next/wfe2-ratelimit-defaults.yml", "")
 | 
			
		||||
	tb, err := NewTransactionBuilderFromFiles("../test/config-next/wfe2-ratelimit-defaults.yml", "")
 | 
			
		||||
	test.AssertNotError(t, err, "creating TransactionBuilder")
 | 
			
		||||
 | 
			
		||||
	// A single check-only transaction for the global limit.
 | 
			
		||||
| 
						 | 
				
			
			@ -202,3 +204,25 @@ func TestCertificatesPerFQDNSetTransactions(t *testing.T) {
 | 
			
		|||
	test.Assert(t, txn.checkOnly(), "should be check-only")
 | 
			
		||||
	test.Assert(t, !txn.limit.isOverride(), "should not be an override")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestNewTransactionBuilder(t *testing.T) {
 | 
			
		||||
	t.Parallel()
 | 
			
		||||
 | 
			
		||||
	expectedBurst := int64(10000)
 | 
			
		||||
	expectedCount := int64(10000)
 | 
			
		||||
	expectedPeriod := config.Duration{Duration: time.Hour * 168}
 | 
			
		||||
 | 
			
		||||
	tb, err := NewTransactionBuilder(LimitConfigs{
 | 
			
		||||
		NewRegistrationsPerIPAddress.String(): &LimitConfig{
 | 
			
		||||
			Burst:  expectedBurst,
 | 
			
		||||
			Count:  expectedCount,
 | 
			
		||||
			Period: expectedPeriod},
 | 
			
		||||
	})
 | 
			
		||||
	test.AssertNotError(t, err, "creating TransactionBuilder")
 | 
			
		||||
 | 
			
		||||
	newRegDefault, ok := tb.limitRegistry.defaults[NewRegistrationsPerIPAddress.EnumString()]
 | 
			
		||||
	test.Assert(t, ok, "NewRegistrationsPerIPAddress was not populated in registry")
 | 
			
		||||
	test.AssertEquals(t, newRegDefault.burst, expectedBurst)
 | 
			
		||||
	test.AssertEquals(t, newRegDefault.count, expectedCount)
 | 
			
		||||
	test.AssertEquals(t, newRegDefault.period, expectedPeriod)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2406,7 +2406,9 @@ func (wfe *WebFrontEndImpl) NewOrder(
 | 
			
		|||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	refundLimits, err := wfe.checkNewOrderLimits(ctx, acct.ID, names, isRenewal)
 | 
			
		||||
	refundLimits := func() {}
 | 
			
		||||
	if !isARIRenewal {
 | 
			
		||||
		refundLimits, err = wfe.checkNewOrderLimits(ctx, acct.ID, names, isRenewal || isARIRenewal)
 | 
			
		||||
		if err != nil && features.Get().UseKvLimitsForNewOrder {
 | 
			
		||||
			if errors.Is(err, berrors.RateLimit) {
 | 
			
		||||
				wfe.sendError(response, logEvent, probs.RateLimited(err.Error()), err)
 | 
			
		||||
| 
						 | 
				
			
			@ -2416,6 +2418,7 @@ func (wfe *WebFrontEndImpl) NewOrder(
 | 
			
		|||
				return
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var newOrderSuccessful bool
 | 
			
		||||
	var errIsRateLimit bool
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										110
									
								
								wfe2/wfe_test.go
								
								
								
								
							
							
						
						
									
										110
									
								
								wfe2/wfe_test.go
								
								
								
								
							| 
						 | 
				
			
			@ -36,6 +36,7 @@ import (
 | 
			
		|||
	"google.golang.org/protobuf/types/known/timestamppb"
 | 
			
		||||
 | 
			
		||||
	"github.com/letsencrypt/boulder/cmd"
 | 
			
		||||
	"github.com/letsencrypt/boulder/config"
 | 
			
		||||
	"github.com/letsencrypt/boulder/core"
 | 
			
		||||
	corepb "github.com/letsencrypt/boulder/core/proto"
 | 
			
		||||
	berrors "github.com/letsencrypt/boulder/errors"
 | 
			
		||||
| 
						 | 
				
			
			@ -410,7 +411,7 @@ func setupWFE(t *testing.T) (WebFrontEndImpl, clock.FakeClock, requestSigner) {
 | 
			
		|||
	// Setup rate limiting.
 | 
			
		||||
	limiter, err := ratelimits.NewLimiter(fc, ratelimits.NewInmemSource(), stats)
 | 
			
		||||
	test.AssertNotError(t, err, "making limiter")
 | 
			
		||||
	txnBuilder, err := ratelimits.NewTransactionBuilder("../test/config-next/wfe2-ratelimit-defaults.yml", "")
 | 
			
		||||
	txnBuilder, err := ratelimits.NewTransactionBuilderFromFiles("../test/config-next/wfe2-ratelimit-defaults.yml", "")
 | 
			
		||||
	test.AssertNotError(t, err, "making transaction composer")
 | 
			
		||||
 | 
			
		||||
	unpauseSigner, err := unpause.NewJWTSigner(cmd.HMACKeyConfig{KeyFile: "../test/secrets/sfe_unpause_key"})
 | 
			
		||||
| 
						 | 
				
			
			@ -4224,20 +4225,26 @@ func Test_sendErrorInternalServerError(t *testing.T) {
 | 
			
		|||
	test.AssertEquals(t, testResponse.Header().Get("Retry-After"), "60")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type mockSA struct {
 | 
			
		||||
// mockSAForARI provides a mock SA with the methods required for an issuance and
 | 
			
		||||
// a renewal with the ARI `Replaces` field.
 | 
			
		||||
type mockSAForARI struct {
 | 
			
		||||
	sapb.StorageAuthorityReadOnlyClient
 | 
			
		||||
	cert *corepb.Certificate
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (sa *mockSAForARI) FQDNSetExists(ctx context.Context, in *sapb.FQDNSetExistsRequest, opts ...grpc.CallOption) (*sapb.Exists, error) {
 | 
			
		||||
	return &sapb.Exists{Exists: false}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetCertificate returns the inner certificate if it matches the given serial.
 | 
			
		||||
func (sa *mockSA) GetCertificate(ctx context.Context, req *sapb.Serial, _ ...grpc.CallOption) (*corepb.Certificate, error) {
 | 
			
		||||
func (sa *mockSAForARI) GetCertificate(ctx context.Context, req *sapb.Serial, _ ...grpc.CallOption) (*corepb.Certificate, error) {
 | 
			
		||||
	if req.Serial == sa.cert.Serial {
 | 
			
		||||
		return sa.cert, nil
 | 
			
		||||
	}
 | 
			
		||||
	return nil, berrors.NotFoundError("certificate with serial %q not found", req.Serial)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (sa *mockSA) ReplacementOrderExists(ctx context.Context, in *sapb.Serial, opts ...grpc.CallOption) (*sapb.Exists, error) {
 | 
			
		||||
func (sa *mockSAForARI) ReplacementOrderExists(ctx context.Context, in *sapb.Serial, opts ...grpc.CallOption) (*sapb.Exists, error) {
 | 
			
		||||
	if in.Serial == sa.cert.Serial {
 | 
			
		||||
		return &sapb.Exists{Exists: false}, nil
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -4245,11 +4252,11 @@ func (sa *mockSA) ReplacementOrderExists(ctx context.Context, in *sapb.Serial, o
 | 
			
		|||
	return &sapb.Exists{Exists: true}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (sa *mockSA) IncidentsForSerial(ctx context.Context, in *sapb.Serial, opts ...grpc.CallOption) (*sapb.Incidents, error) {
 | 
			
		||||
func (sa *mockSAForARI) IncidentsForSerial(ctx context.Context, in *sapb.Serial, opts ...grpc.CallOption) (*sapb.Incidents, error) {
 | 
			
		||||
	return &sapb.Incidents{}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (sa *mockSA) GetCertificateStatus(ctx context.Context, in *sapb.Serial, opts ...grpc.CallOption) (*corepb.CertificateStatus, error) {
 | 
			
		||||
func (sa *mockSAForARI) GetCertificateStatus(ctx context.Context, in *sapb.Serial, opts ...grpc.CallOption) (*corepb.CertificateStatus, error) {
 | 
			
		||||
	return &corepb.CertificateStatus{Serial: in.Serial, Status: string(core.OCSPStatusGood)}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -4267,7 +4274,7 @@ func TestOrderMatchesReplacement(t *testing.T) {
 | 
			
		|||
	mockDer, err := x509.CreateCertificate(rand.Reader, &rawCert, &rawCert, &testKey.PublicKey, testKey)
 | 
			
		||||
	test.AssertNotError(t, err, "failed to create test certificate")
 | 
			
		||||
 | 
			
		||||
	wfe.sa = &mockSA{
 | 
			
		||||
	wfe.sa = &mockSAForARI{
 | 
			
		||||
		cert: &corepb.Certificate{
 | 
			
		||||
			RegistrationID: 1,
 | 
			
		||||
			Serial:         expectSerial.String(),
 | 
			
		||||
| 
						 | 
				
			
			@ -4414,9 +4421,10 @@ func TestCountNewOrderWithReplaces(t *testing.T) {
 | 
			
		|||
	wfe, _, signer := setupWFE(t)
 | 
			
		||||
 | 
			
		||||
	expectExpiry := time.Now().AddDate(0, 0, 1)
 | 
			
		||||
	var expectAKID []byte
 | 
			
		||||
	// Pick a random issuer to "issue" expectCert.
 | 
			
		||||
	var issuer *issuance.Certificate
 | 
			
		||||
	for _, v := range wfe.issuerCertificates {
 | 
			
		||||
		expectAKID = v.SubjectKeyId
 | 
			
		||||
		issuer = v
 | 
			
		||||
		break
 | 
			
		||||
	}
 | 
			
		||||
	testKey, _ := rsa.GenerateKey(rand.Reader, 1024)
 | 
			
		||||
| 
						 | 
				
			
			@ -4425,7 +4433,7 @@ func TestCountNewOrderWithReplaces(t *testing.T) {
 | 
			
		|||
		NotAfter:       expectExpiry,
 | 
			
		||||
		DNSNames:       []string{"example.com"},
 | 
			
		||||
		SerialNumber:   expectSerial,
 | 
			
		||||
		AuthorityKeyId: expectAKID,
 | 
			
		||||
		AuthorityKeyId: issuer.SubjectKeyId,
 | 
			
		||||
	}
 | 
			
		||||
	expectCertId, err := makeARICertID(expectCert)
 | 
			
		||||
	test.AssertNotError(t, err, "failed to create test cert id")
 | 
			
		||||
| 
						 | 
				
			
			@ -4433,7 +4441,7 @@ func TestCountNewOrderWithReplaces(t *testing.T) {
 | 
			
		|||
	test.AssertNotError(t, err, "failed to create test certificate")
 | 
			
		||||
 | 
			
		||||
	// MockSA that returns the certificate with the expected serial.
 | 
			
		||||
	wfe.sa = &mockSA{
 | 
			
		||||
	wfe.sa = &mockSAForARI{
 | 
			
		||||
		cert: &corepb.Certificate{
 | 
			
		||||
			RegistrationID: 1,
 | 
			
		||||
			Serial:         core.SerialToString(expectSerial),
 | 
			
		||||
| 
						 | 
				
			
			@ -4456,3 +4464,83 @@ func TestCountNewOrderWithReplaces(t *testing.T) {
 | 
			
		|||
	test.AssertEquals(t, responseWriter.Code, http.StatusCreated)
 | 
			
		||||
	test.AssertMetricWithLabelsEquals(t, wfe.stats.ariReplacementOrders, prometheus.Labels{"isReplacement": "true", "limitsExempt": "true"}, 1)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestNewOrderRateLimits(t *testing.T) {
 | 
			
		||||
	wfe, fc, signer := setupWFE(t)
 | 
			
		||||
 | 
			
		||||
	features.Set(features.Config{UseKvLimitsForNewOrder: true})
 | 
			
		||||
	defer features.Reset()
 | 
			
		||||
 | 
			
		||||
	// Set the default ratelimits to only allow one new order per account per 24
 | 
			
		||||
	// hours.
 | 
			
		||||
	txnBuilder, err := ratelimits.NewTransactionBuilder(ratelimits.LimitConfigs{
 | 
			
		||||
		ratelimits.NewOrdersPerAccount.String(): &ratelimits.LimitConfig{
 | 
			
		||||
			Burst:  1,
 | 
			
		||||
			Count:  1,
 | 
			
		||||
			Period: config.Duration{Duration: time.Hour * 24}},
 | 
			
		||||
	})
 | 
			
		||||
	test.AssertNotError(t, err, "making transaction composer")
 | 
			
		||||
	wfe.txnBuilder = txnBuilder
 | 
			
		||||
 | 
			
		||||
	// Pick a random issuer to "issue" extantCert.
 | 
			
		||||
	var issuer *issuance.Certificate
 | 
			
		||||
	for _, v := range wfe.issuerCertificates {
 | 
			
		||||
		issuer = v
 | 
			
		||||
		break
 | 
			
		||||
	}
 | 
			
		||||
	testKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
 | 
			
		||||
	test.AssertNotError(t, err, "failed to create test key")
 | 
			
		||||
	extantCert := &x509.Certificate{
 | 
			
		||||
		NotBefore:      fc.Now(),
 | 
			
		||||
		NotAfter:       fc.Now().AddDate(0, 0, 90),
 | 
			
		||||
		DNSNames:       []string{"example.com"},
 | 
			
		||||
		SerialNumber:   big.NewInt(1337),
 | 
			
		||||
		AuthorityKeyId: issuer.SubjectKeyId,
 | 
			
		||||
	}
 | 
			
		||||
	extantCertId, err := makeARICertID(extantCert)
 | 
			
		||||
	test.AssertNotError(t, err, "failed to create test cert id")
 | 
			
		||||
	extantDer, err := x509.CreateCertificate(rand.Reader, extantCert, extantCert, &testKey.PublicKey, testKey)
 | 
			
		||||
	test.AssertNotError(t, err, "failed to create test certificate")
 | 
			
		||||
 | 
			
		||||
	// Mock SA that returns the certificate with the expected serial.
 | 
			
		||||
	wfe.sa = &mockSAForARI{
 | 
			
		||||
		cert: &corepb.Certificate{
 | 
			
		||||
			RegistrationID: 1,
 | 
			
		||||
			Serial:         core.SerialToString(extantCert.SerialNumber),
 | 
			
		||||
			Der:            extantDer,
 | 
			
		||||
			Issued:         timestamppb.New(extantCert.NotBefore),
 | 
			
		||||
			Expires:        timestamppb.New(extantCert.NotAfter),
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Set the fake clock forward to 1s past the suggested renewal window start
 | 
			
		||||
	// time.
 | 
			
		||||
	renewalWindowStart := core.RenewalInfoSimple(extantCert.NotBefore, extantCert.NotAfter).SuggestedWindow.Start
 | 
			
		||||
	fc.Set(renewalWindowStart.Add(time.Second))
 | 
			
		||||
 | 
			
		||||
	mux := wfe.Handler(metrics.NoopRegisterer)
 | 
			
		||||
 | 
			
		||||
	// Request the certificate for the first time. Because we mocked together
 | 
			
		||||
	// the certificate, it will have been issued 60 days ago.
 | 
			
		||||
	r := signAndPost(signer, newOrderPath, "http://localhost"+newOrderPath,
 | 
			
		||||
		`{"Identifiers": [{"type": "dns", "value": "example.com"}]}`)
 | 
			
		||||
	responseWriter := httptest.NewRecorder()
 | 
			
		||||
	mux.ServeHTTP(responseWriter, r)
 | 
			
		||||
	test.AssertEquals(t, responseWriter.Code, http.StatusCreated)
 | 
			
		||||
 | 
			
		||||
	// Request another, identical certificate. This should fail for violating
 | 
			
		||||
	// the NewOrdersPerAccount rate limit.
 | 
			
		||||
	r = signAndPost(signer, newOrderPath, "http://localhost"+newOrderPath,
 | 
			
		||||
		`{"Identifiers": [{"type": "dns", "value": "example.com"}]}`)
 | 
			
		||||
	responseWriter = httptest.NewRecorder()
 | 
			
		||||
	mux.ServeHTTP(responseWriter, r)
 | 
			
		||||
	test.AssertEquals(t, responseWriter.Code, http.StatusTooManyRequests)
 | 
			
		||||
 | 
			
		||||
	// Make a request with the "Replaces" field, which should satisfy ARI checks
 | 
			
		||||
	// and therefore bypass the rate limit.
 | 
			
		||||
	r = signAndPost(signer, newOrderPath, "http://localhost"+newOrderPath,
 | 
			
		||||
		fmt.Sprintf(`{"Identifiers": [{"type": "dns", "value": "example.com"}],	"Replaces": %q}`, extantCertId))
 | 
			
		||||
	responseWriter = httptest.NewRecorder()
 | 
			
		||||
	mux.ServeHTTP(responseWriter, r)
 | 
			
		||||
	test.AssertEquals(t, responseWriter.Code, http.StatusCreated)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue