Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
beautifulentropy committed Nov 9, 2023
1 parent 1bb8ef6 commit c96c473
Show file tree
Hide file tree
Showing 11 changed files with 569 additions and 66 deletions.
23 changes: 1 addition & 22 deletions ra/ra.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import (

"github.com/jmhodges/clock"
"github.com/prometheus/client_golang/prometheus"
"github.com/weppos/publicsuffix-go/publicsuffix"
"golang.org/x/crypto/ocsp"
"google.golang.org/grpc"
"google.golang.org/protobuf/proto"
Expand Down Expand Up @@ -1377,26 +1376,6 @@ func (ra *RegistrationAuthorityImpl) getSCTs(ctx context.Context, cert []byte, e
return scts, nil
}

// domainsForRateLimiting transforms a list of FQDNs into a list of eTLD+1's
// for the purpose of rate limiting. It also de-duplicates the output
// domains. Exact public suffix matches are included.
func domainsForRateLimiting(names []string) []string {
var domains []string
for _, name := range names {
domain, err := publicsuffix.Domain(name)
if err != nil {
// The only possible errors are:
// (1) publicsuffix.Domain is giving garbage values
// (2) the public suffix is the domain itself
// We assume 2 and include the original name in the result.
domains = append(domains, name)
} else {
domains = append(domains, domain)
}
}
return core.UniqueLowerNames(domains)
}

// enforceNameCounts uses the provided count RPC to find a count of certificates
// for each of the names. If the count for any of the names exceeds the limit
// for the given registration then the names out of policy are returned to be
Expand Down Expand Up @@ -1451,7 +1430,7 @@ func (ra *RegistrationAuthorityImpl) checkCertificatesPerNameLimit(ctx context.C
return nil
}

tldNames := domainsForRateLimiting(names)
tldNames := ratelimits.DomainsForRateLimiting(names)
namesOutOfLimit, earliest, err := ra.enforceNameCounts(ctx, tldNames, limit, regID)
if err != nil {
return fmt.Errorf("checking certificates per name limit for %q: %s",
Expand Down
20 changes: 0 additions & 20 deletions ra/ra_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1113,26 +1113,6 @@ func TestAuthzFailedRateLimitingNewOrder(t *testing.T) {
testcase()
}

func TestDomainsForRateLimiting(t *testing.T) {
domains := domainsForRateLimiting([]string{})
test.AssertEquals(t, len(domains), 0)

domains = domainsForRateLimiting([]string{"www.example.com", "example.com"})
test.AssertDeepEquals(t, domains, []string{"example.com"})

domains = domainsForRateLimiting([]string{"www.example.com", "example.com", "www.example.co.uk"})
test.AssertDeepEquals(t, domains, []string{"example.co.uk", "example.com"})

domains = domainsForRateLimiting([]string{"www.example.com", "example.com", "www.example.co.uk", "co.uk"})
test.AssertDeepEquals(t, domains, []string{"co.uk", "example.co.uk", "example.com"})

domains = domainsForRateLimiting([]string{"foo.bar.baz.www.example.com", "baz.example.com"})
test.AssertDeepEquals(t, domains, []string{"example.com"})

domains = domainsForRateLimiting([]string{"github.io", "foo.github.io", "bar.github.io"})
test.AssertDeepEquals(t, domains, []string{"bar.github.io", "foo.github.io", "github.io"})
}

type mockSAWithNameCounts struct {
mocks.StorageAuthority
nameCounts *sapb.CountByNames
Expand Down
129 changes: 124 additions & 5 deletions ratelimits/bucket.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
package ratelimits

import (
"errors"
"fmt"
"net"
"strconv"

"github.com/letsencrypt/boulder/core"
)

// BucketId should only be created using the New*BucketId functions. It is used
// by the Limiter to look up the bucket and limit overrides for a specific
// subscriber and limit.
type BucketId struct {
// limit is the name of the associated rate limit. It is used for looking up
// default limits.
limit Name
// limitName is the name of the associated rate limit. It is used for
// looking up default limits.
limitName Name

// bucketKey is the limit Name enum (e.g. "1") concatenated with the
// subscriber identifier specific to the associate limit Name type.
Expand All @@ -27,7 +31,7 @@ func NewRegistrationsPerIPAddressBucketId(ip net.IP) (BucketId, error) {
return BucketId{}, err
}
return BucketId{
limit: NewRegistrationsPerIPAddress,
limitName: NewRegistrationsPerIPAddress,
bucketKey: joinWithColon(NewRegistrationsPerIPAddress.EnumString(), id),
}, nil
}
Expand All @@ -46,7 +50,122 @@ func NewRegistrationsPerIPv6RangeBucketId(ip net.IP) (BucketId, error) {
return BucketId{}, err
}
return BucketId{
limit: NewRegistrationsPerIPv6Range,
limitName: NewRegistrationsPerIPv6Range,
bucketKey: joinWithColon(NewRegistrationsPerIPv6Range.EnumString(), id),
}, nil
}

// NewOrdersPerAccountBucketId returns a BucketId for the provided ACME
// registration Id.
func NewOrdersPerAccountBucketId(regId int64) (BucketId, error) {
id := strconv.FormatInt(regId, 10)
err := validateIdForName(NewOrdersPerAccount, id)
if err != nil {
return BucketId{}, err
}
return BucketId{
limitName: NewOrdersPerAccount,
bucketKey: joinWithColon(NewOrdersPerAccount.EnumString(), id),
}, nil
}

// NewFailedAuthorizationsPerAccountBucketId returns a BucketId for the provided
// ACME registration Id.
func NewFailedAuthorizationsPerAccountBucketId(regId int64) (BucketId, error) {
id := strconv.FormatInt(regId, 10)
err := validateIdForName(FailedAuthorizationsPerAccount, id)
if err != nil {
return BucketId{}, err
}
return BucketId{
limitName: FailedAuthorizationsPerAccount,
bucketKey: joinWithColon(FailedAuthorizationsPerAccount.EnumString(), id),
}, nil
}

// NewCertificatesPerDomainBucketId returns a BucketId for the provided order
// domain name.
func NewCertificatesPerDomainBucketId(orderName string) (BucketId, error) {
err := validateIdForName(CertificatesPerDomain, orderName)
if err != nil {
return BucketId{}, err
}
return BucketId{
limitName: CertificatesPerDomain,
bucketKey: joinWithColon(CertificatesPerDomain.EnumString(), orderName),
}, nil
}

// newCertificatesPerDomainPerAccountBucketId is only referenced internally.
// Buckets for CertificatesPerDomainPerAccount are created by calling
// NewCertificatesPerDomainBucketsWithCost().
func newCertificatesPerDomainPerAccountBucketId(regId int64) (BucketId, error) {
id := strconv.FormatInt(regId, 10)
err := validateIdForName(CertificatesPerDomainPerAccount, id)
if err != nil {
return BucketId{}, err
}
return BucketId{
limitName: CertificatesPerDomainPerAccount,
bucketKey: joinWithColon(CertificatesPerDomainPerAccount.EnumString(), id),
}, nil
}

// NewCertificatesPerDomainTransactions returns a slice of Transactions for the
// provided order domain names. The cost specified will be applied per eTLD+1
// name present in the orderDomains.
//
// Note: when overrides to the CertificatesPerDomainPerAccount are configured
// for the subscriber, the cost:
// - MUST be consumed from the CertificatesPerDomainPerAccount bucket and
// - SHOULD be consumed from each CertificatesPerDomain bucket, if possible.
//
// When a CertificatesPerDomainPerAccount override is configured, all of the
// CertificatesPerDomain transactions returned by this function will be marked
// as optimistic and the combined cost of all of these transactions will be
// specified in a CertificatesPerDomainPerAccount transaction as well.
func NewCertificatesPerDomainTransactions(limiter *Limiter, regId int64, orderDomains []string, cost int64) ([]Transaction, error) {
id, err := newCertificatesPerDomainPerAccountBucketId(regId)
if err != nil {
return nil, err
}
certsPerDomainPerAccountLimit, err := limiter.getLimit(CertificatesPerDomainPerAccount, id.bucketKey)
if err != nil {
if !errors.Is(err, errLimitDisabled) {
return nil, err
}
}

var txns []Transaction
var certsPerDomainPerAccountCost int64
for _, name := range DomainsForRateLimiting(orderDomains) {
bucketId, err := NewCertificatesPerDomainBucketId(name)
if err != nil {
return nil, err
}
certsPerDomainPerAccountCost += cost
if certsPerDomainPerAccountLimit.isOverride {
txns = append(txns, newOptimisticTransaction(bucketId, cost))
} else {
txns = append(txns, NewTransaction(bucketId, cost))
}
}
if certsPerDomainPerAccountLimit.isOverride {
txns = append(txns, NewTransaction(id, certsPerDomainPerAccountCost))
}
return txns, nil
}

// NewCertificatesPerFQDNSetBucket returns a BucketId for the provided order
// domain names.
func NewCertificatesPerFQDNSetBucket(orderNames []string) (BucketId, error) {
id := string(core.HashNames(orderNames))
err := validateIdForName(CertificatesPerFQDNSet, id)
if err != nil {
return BucketId{}, err
}
return BucketId{
limitName: CertificatesPerFQDNSet,
bucketKey: joinWithColon(CertificatesPerFQDNSet.EnumString(), id),
}, nil
}
Loading

0 comments on commit c96c473

Please sign in to comment.