Skip to content

Commit f686a79

Browse files
committed
add a rate limiter per issue
1 parent 6dbb2e2 commit f686a79

File tree

10 files changed

+124
-0
lines changed

10 files changed

+124
-0
lines changed

cmd/tesseract/aws/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ var (
7171
acceptSHA1 = flag.Bool("accept_sha1_signing_algorithms", true, "If true, accept chains that use SHA-1 based signing algorithms. This flag will eventually be removed, and such algorithms will be rejected.")
7272
enablePublicationAwaiter = flag.Bool("enable_publication_awaiter", true, "If true then the certificate is integrated into log before returning the response.")
7373
notBeforeRL = flag.String("rate_limit_old_not_before", "", "Optionally rate limits submissions with old notBefore dates. Expects a value of with the format: \"<go duration>:<rate limit>\", e.g. \"30d:50\" would impose a limit of 50 certs/s on submissions whose notBefore date is >= 30days old.")
74+
issuerRL = flag.Float64("rate_limit_per_issuer", -1, "Optionally rate limits submissions per issuer per second. Disabled when null or negative.")
7475

7576
// Performance flags
7677
httpDeadline = flag.Duration("http_deadline", time.Second*10, "Deadline for HTTP requests.")
@@ -141,6 +142,7 @@ eventually go away. See /internal/lax509/README.md for more information.`)
141142

142143
hOpts := tesseract.LogHandlerOpts{
143144
NotBeforeRL: notBeforeRLFromFlags(),
145+
IssuerRL: *issuerRL,
144146
DedupRL: dedupRL,
145147
}
146148
logHandler, err := tesseract.NewLogHandler(ctx, *origin, signer, chainValidationConfig, newAWSStorage, *httpDeadline, *maskInternalErrors, *pathPrefix, hOpts)

cmd/tesseract/gcp/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ var (
7272
enablePublicationAwaiter = flag.Bool("enable_publication_awaiter", true, "If true then the certificate is integrated into log before returning the response.")
7373
witnessPolicyFile = flag.String("witness_policy_file", "", "(Optional) Path to the file containing the witness policy in the format described at https://git.glasklar.is/sigsum/core/sigsum-go/-/blob/main/doc/policy.md")
7474
notBeforeRL = flag.String("rate_limit_old_not_before", "", "Optionally rate limits submissions with old notBefore dates. Expects a value of with the format: \"<go duration>:<rate limit>\", e.g. \"30d:50\" would impose a limit of 50 certs/s on submissions whose notBefore date is >= 30days old.")
75+
issuerRL = flag.Float64("rate_limit_per_issuer", -1, "Optionally rate limits submissions per issuer per second. Disabled when null or negative.")
7576

7677
// Performance flags
7778
httpDeadline = flag.Duration("http_deadline", time.Second*10, "Deadline for HTTP requests.")
@@ -128,6 +129,7 @@ eventually go away. See /internal/lax509/README.md for more information.`)
128129

129130
hOpts := tesseract.LogHandlerOpts{
130131
NotBeforeRL: notBeforeRLFromFlags(),
132+
IssuerRL: *issuerRL,
131133
DedupRL: dedupRL,
132134
}
133135
logHandler, err := tesseract.NewLogHandler(ctx, *origin, signer, chainValidationConfig, newGCPStorage, *httpDeadline, *maskInternalErrors, *pathPrefix, hOpts)

cmd/tesseract/posix/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ var (
7575
enablePublicationAwaiter = flag.Bool("enable_publication_awaiter", true, "If true then the certificate is integrated into log before returning the response.")
7676
witnessPolicyFile = flag.String("witness_policy_file", "", "(Optional) Path to the file containing the witness policy in the format describe at https://git.glasklar.is/sigsum/core/sigsum-go/-/blob/main/doc/policy.md")
7777
notBeforeRL = flag.String("rate_limit_old_not_before", "", "Optionally rate limits submissions with old notBefore dates. Expects a value of with the format: \"<go duration>:<rate limit>\", e.g. \"30d:50\" would impose a limit of 50 certs/s on submissions whose notBefore date is >= 30days old.")
78+
issuerRL = flag.Float64("rate_limit_per_issuer", -1, "Optionally rate limits submissions per issuer per second. Disabled when null or negative.")
7879

7980
// Performance flags
8081
httpDeadline = flag.Duration("http_deadline", time.Second*10, "Deadline for HTTP requests.")
@@ -121,6 +122,7 @@ eventually go away. See /internal/lax509/README.md for more information.`)
121122

122123
hOpts := tesseract.LogHandlerOpts{
123124
NotBeforeRL: notBeforeRLFromFlags(),
125+
IssuerRL: *issuerRL,
124126
DedupRL: dedupRL,
125127
}
126128
logHandler, err := tesseract.NewLogHandler(ctx, *origin, signer, chainValidationConfig, newStorage, *httpDeadline, *maskInternalErrors, *pathPrefix, hOpts)

ctlog.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ type NotBeforeRL struct {
131131

132132
type LogHandlerOpts struct {
133133
NotBeforeRL *NotBeforeRL
134+
IssuerRL float64
134135
DedupRL float64
135136
}
136137

@@ -163,6 +164,9 @@ func NewLogHandler(ctx context.Context, origin string, signer crypto.Signer, cfg
163164
if opts.NotBeforeRL != nil {
164165
ctOpts.RateLimits.NotBefore(opts.NotBeforeRL.AgeThreshold, opts.NotBeforeRL.RateLimit)
165166
}
167+
if opts.IssuerRL > 0 {
168+
ctOpts.RateLimits.Issuer(opts.IssuerRL)
169+
}
166170
if opts.DedupRL >= 0 {
167171
ctOpts.RateLimits.Dedup(opts.DedupRL)
168172
}

deployment/modules/gcp/gce/tesseract/main.tf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ module "gce_container_tesseract" {
4141
"--enable_publication_awaiter=${var.enable_publication_awaiter}",
4242
"--accept_sha1_signing_algorithms=true",
4343
"--rate_limit_old_not_before=${var.rate_limit_old_not_before}",
44+
"--rate_limit_per_issuer=${var.rate_limit_per_issuer}",
4445
"--rate_limit_dedup=${var.rate_limit_dedup}"
4546
]
4647
tty : true # maybe remove this

deployment/modules/gcp/gce/tesseract/variables.tf

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,12 @@ variable "rate_limit_old_not_before" {
106106
default = ""
107107
}
108108

109+
variable "rate_limit_per_issuer" {
110+
description = "Set to rate limit submissions per issuer per second."
111+
type = number
112+
default = -1
113+
}
114+
109115
variable "rate_limit_dedup" {
110116
description = "Set to rate limit duplicate submissions per second."
111117
type = number

deployment/modules/gcp/tesseract/gce/main.tf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ module "gce" {
4242
batch_max_size = var.batch_max_size
4343
enable_publication_awaiter = var.enable_publication_awaiter
4444
rate_limit_old_not_before = var.rate_limit_old_not_before
45+
rate_limit_per_issuer = var.rate_limit_per_issuer
4546
rate_limit_dedup = var.rate_limit_dedup
4647

4748
depends_on = [

deployment/modules/gcp/tesseract/gce/variables.tf

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,12 @@ variable "rate_limit_old_not_before" {
105105
default = ""
106106
}
107107

108+
variable "rate_limit_per_issuer" {
109+
description = "Set to rate limit submissions per issuer per second."
110+
type = number
111+
default = -1
112+
}
113+
108114
variable "rate_limit_dedup" {
109115
description = "Set to rate limit duplicate submissions per second."
110116
type = number

internal/ct/handlers.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package ct
1616

1717
import (
1818
"context"
19+
"crypto/sha256"
1920
"crypto/x509"
2021
"encoding/base64"
2122
"encoding/json"
@@ -195,6 +196,8 @@ type RateLimits struct {
195196
notBeforeLimit time.Duration
196197
notBefore *rate.Limiter
197198
dedup *rate.Limiter
199+
issuerLimit float64
200+
issuer *sync.Map
198201
}
199202

200203
// NotBefore configures a rate limit on old certs.
@@ -206,6 +209,15 @@ func (r *RateLimits) NotBefore(age time.Duration, limit float64) {
206209
klog.Infof("Configured OldSubmission limiter with %0.2f qps for certs aged >= %s", limit, age)
207210
}
208211

212+
// Issuer configures a rate limit on the first issuer of each chain.
213+
//
214+
// Submissions will be subject to the specified number of entries per second.
215+
func (r *RateLimits) Issuer(limit float64) {
216+
r.issuerLimit = limit
217+
r.issuer = &sync.Map{}
218+
klog.Infof("Configured Issuer limiter with %0.2f qps per issuer", limit)
219+
}
220+
209221
// Dedup configures a rate limit on entries being deduplicated.
210222
//
211223
// Submissions will be subject to the specified number of entries per second.
@@ -231,6 +243,24 @@ func (r *RateLimits) AcceptNotBefore(ctx context.Context, chain []*x509.Certific
231243
return true
232244
}
233245

246+
// AcceptIssuer returns true if a chain should be accepted based on its first issuer, and false othwerwise.
247+
func (r *RateLimits) AcceptIssuer(ctx context.Context, chain []*x509.Certificate) bool {
248+
if len(chain) == 0 {
249+
return false
250+
}
251+
if r.issuer != nil && r.issuerLimit >= 0 {
252+
issuer := sha256.Sum256(chain[0].RawIssuer)
253+
v, _ := r.issuer.LoadOrStore(issuer, rate.NewLimiter(rate.Limit(r.issuerLimit), int(math.Ceil(r.issuerLimit))))
254+
rl := v.(*rate.Limiter)
255+
if rl.Allow() {
256+
return true
257+
}
258+
rateLimitedRequests.Add(ctx, 1, metric.WithAttributes(rateLimitReasonKey.String("issuer")))
259+
return false
260+
}
261+
return true
262+
}
263+
234264
// AcceptDedup returns true if a duplicate entry is permitted to be resolved.
235265
func (r *RateLimits) AcceptDedup(ctx context.Context) bool {
236266
if r.dedup != nil {
@@ -354,6 +384,11 @@ func addChainInternal(ctx context.Context, opts *HandlerOptions, log *log, w htt
354384
opts.RequestLog.addCertToChain(ctx, cert)
355385
}
356386

387+
if ok := opts.RateLimits.AcceptIssuer(ctx, chain); !ok {
388+
w.Header().Add("Retry-After", strconv.Itoa(rand.IntN(5)+1)) // random retry within [1,6) seconds
389+
return http.StatusTooManyRequests, []attribute.KeyValue{tooManyRequestsReasonKey.String("rate_limit_issuer")}, errors.New(http.StatusText(http.StatusTooManyRequests))
390+
}
391+
357392
// Get the current time in the form used throughout RFC6962, namely milliseconds since Unix
358393
// epoch, and use this throughout.
359394
nanosPerMilli := int64(time.Millisecond / time.Nanosecond)

internal/ct/handlers_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -823,6 +823,71 @@ func TestMaxDedupInFlight(t *testing.T) {
823823
}
824824
}
825825

826+
func TestLimitIssuer(t *testing.T) {
827+
var tests = []struct {
828+
descr string
829+
chains [][]string
830+
wants []int
831+
maxRate float64
832+
}{
833+
{
834+
descr: "no-limit",
835+
chains: [][]string{
836+
{testdata.CertFromIntermediate, testdata.IntermediateFromRoot, testdata.CACertPEM},
837+
{testdata.CertFromIntermediate, testdata.IntermediateFromRoot, testdata.CACertPEM},
838+
},
839+
wants: []int{http.StatusOK, http.StatusOK},
840+
maxRate: -1,
841+
},
842+
{
843+
descr: "success",
844+
chains: [][]string{
845+
{testdata.CertFromIntermediate, testdata.IntermediateFromRoot, testdata.CACertPEM},
846+
{testdata.CertFromIntermediate, testdata.IntermediateFromRoot, testdata.CACertPEM},
847+
},
848+
maxRate: 100,
849+
wants: []int{http.StatusOK, http.StatusOK},
850+
},
851+
{
852+
descr: "issuer-limited",
853+
chains: [][]string{
854+
{testdata.CertFromIntermediate, testdata.IntermediateFromRoot, testdata.CACertPEM},
855+
{testdata.CertFromIntermediate, testdata.IntermediateFromRoot, testdata.CACertPEM},
856+
},
857+
maxRate: 1,
858+
wants: []int{http.StatusOK, http.StatusTooManyRequests},
859+
},
860+
}
861+
862+
for _, test := range tests {
863+
log, _ := setupTestLog(t)
864+
hhOpts := hOpts()
865+
if test.maxRate > 0 {
866+
hhOpts.RateLimits.Issuer(test.maxRate)
867+
}
868+
server := setupTestServer(t, log, path.Join(prefix, rfc6962.AddChainPath), hhOpts)
869+
defer server.Close()
870+
defer timeSource.Reset()
871+
872+
// Increment time to make it unique for each test case.
873+
t.Run(test.descr, func(t *testing.T) {
874+
for i, chain := range test.chains {
875+
pool := loadCertsIntoPoolOrDie(t, chain)
876+
chain := createJSONChain(t, *pool)
877+
878+
resp, err := http.Post(server.URL+rfc6962.AddChainPath, "application/json", chain)
879+
880+
if err != nil {
881+
t.Fatalf("http.Post(%s)=(_,%q); want (_,nil)", rfc6962.AddChainPath, err)
882+
}
883+
if got, want := resp.StatusCode, test.wants[i]; got != want {
884+
t.Errorf("http.Post(%s)=(%d,nil); want (%d,nil)", rfc6962.AddChainPath, got, want)
885+
}
886+
}
887+
})
888+
}
889+
}
890+
826891
func TestRateLimiter(t *testing.T) {
827892
certAge := time.Minute
828893
cert := &x509.Certificate{

0 commit comments

Comments
 (0)