New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve renewal rate limiting #2832
Changes from 2 commits
a79a73d
b0d8345
ed3ff70
369dfbc
76e30d1
4ec800d
712b4a6
8314b9e
c32623c
10c32a6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -434,19 +434,18 @@ func (ssa *SQLStorageAuthority) countCertificatesByExactName(domain string, earl | |
} | ||
|
||
// countCertificates returns, for a single domain, the count of | ||
// certificates issued in the given time range for that domain using the | ||
// non-renewal certificates issued in the given time range for that domain using the | ||
// provided query, assumed to be either `countCertificatesExactSelect` or | ||
// `countCertificatesSelect`. | ||
// `countCertificatesSelect`. Renewals of certificates issued within the same | ||
// window are considered "free" and not returned as part of this count. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "and are not counted" instead of "not returned as part of this count." |
||
// | ||
// The highest count this function can return is 10,000. If there are more | ||
// certificates than that matching one of the provided domain names, it will return | ||
// TooManyCertificatesError. | ||
func (ssa *SQLStorageAuthority) countCertificates(domain string, earliest, latest time.Time, query string) (int, error) { | ||
var count int64 | ||
const max = 10000 | ||
var serials []struct { | ||
Serial string | ||
} | ||
var serials []string | ||
_, err := ssa.dbMap.Select( | ||
&serials, | ||
query, | ||
|
@@ -463,12 +462,28 @@ func (ssa *SQLStorageAuthority) countCertificates(domain string, earliest, lates | |
} else if count > max { | ||
return max, TooManyCertificatesError(fmt.Sprintf("More than %d issuedName entries for %s.", max, domain)) | ||
} | ||
serialMap := make(map[string]struct{}, len(serials)) | ||
for _, s := range serials { | ||
serialMap[s.Serial] = struct{}{} | ||
|
||
// If there are no serials found, short circuit since there isn't subsequent | ||
// work to do | ||
if len(serials) == 0 { | ||
return 0, nil | ||
} | ||
|
||
// Find all FQDN Set Hashes with the serials from the issuedNames table that | ||
// were visible within our search window | ||
fqdnSets, err := ssa.getFQDNSetsBySerials(serials) | ||
if err != nil { | ||
return -1, err | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we actually use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I was following the convention that the function was using before I touched it: https://github.com/letsencrypt/boulder/blob/master/sa/sa.go#L469:L471 |
||
} | ||
|
||
return len(serialMap), nil | ||
// Using those FQDN Set Hashes, we can then find all of the non-renewal | ||
// issuances with a second query against the fqdnSets table using the set | ||
// hashes we know about | ||
nonRenewalIssuances, err := ssa.getNewIssuancesByFQDNSet(fqdnSets, earliest) | ||
if err != nil { | ||
return -1, err | ||
} | ||
return nonRenewalIssuances, nil | ||
} | ||
|
||
// GetCertificate takes a serial number and returns the corresponding | ||
|
@@ -1031,6 +1046,101 @@ func (ssa *SQLStorageAuthority) CountFQDNSets(ctx context.Context, window time.D | |
return count, err | ||
} | ||
|
||
// getFQDNSetsBySerials returns a slice of []byte `setHash` entries from the | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's do Also, this comment mostly documents the return type and parameters (which can be deduced from the code). Instead, it's better to talk more about the "why." E.g. "finds the setHashes corresponding to a set of certificate serials so we can check whether any previous certificate was issued with the same set of names." |
||
// fqdnSets table for the certificate serials provided. | ||
func (ssa *SQLStorageAuthority) getFQDNSetsBySerials(serials []string) ([][]byte, error) { | ||
var fqdnSets [][]byte | ||
|
||
// It is unexpected that this function would be called with no serials | ||
if len(serials) == 0 { | ||
err := fmt.Errorf("getFQDNSetsBySerials called with no serials") | ||
ssa.log.AuditErr(err.Error()) | ||
return nil, err | ||
} | ||
|
||
qmarks := make([]string, len(serials)) | ||
params := make([]interface{}, len(serials)) | ||
for i, serial := range serials { | ||
params[i] = serial | ||
qmarks[i] = "?" | ||
} | ||
query := "SELECT setHash FROM fqdnSets " + | ||
"WHERE serial IN (" + strings.Join(qmarks, ",") + ") " | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Trailing space in string. |
||
_, err := ssa.dbMap.Select( | ||
&fqdnSets, | ||
query, | ||
params...) | ||
|
||
if err != nil && err != sql.ErrNoRows { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ErrNoRows should be considered an error here because it's an internal consistency violation. If the serial existed in issuedNames, it should exist here. |
||
return nil, err | ||
} | ||
return fqdnSets, nil | ||
} | ||
|
||
// getNewIssuancesByFQDNSet returns a count of new issuances (renewals are not | ||
// included) for a given slice of fqdnSets that were issued after the earliest | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. s/that were issued after/that occurred after/ |
||
// parameter. | ||
func (ssa *SQLStorageAuthority) getNewIssuancesByFQDNSet(fqdnSets [][]byte, earliest time.Time) (int, error) { | ||
var results []struct { | ||
Serial string | ||
SetHash []byte | ||
Issued time.Time | ||
} | ||
|
||
qmarks := make([]string, len(fqdnSets)) | ||
params := make([]interface{}, len(fqdnSets)) | ||
for i, setHash := range fqdnSets { | ||
params[i] = setHash | ||
qmarks[i] = "?" | ||
} | ||
|
||
query := "SELECT serial, setHash, issued FROM fqdnSets " + | ||
"WHERE setHash IN (" + strings.Join(qmarks, ",") + ") " + | ||
"ORDER BY setHash, issued" | ||
|
||
// First, find the serial, sethash and issued date from the fqdnSets table for | ||
// the given fqdn set hashes | ||
_, err := ssa.dbMap.Select( | ||
&results, | ||
query, | ||
params...) | ||
if err != nil && err != sql.ErrNoRows { | ||
return -1, err | ||
} | ||
|
||
// If there are no results we have encountered a major error and | ||
// should loudly complain | ||
if err == sql.ErrNoRows || len(results) == 0 { | ||
ssa.log.AuditErr(fmt.Sprintf("Found no results from fqdnSets for setHashes known to exist: %#v", fqdnSets)) | ||
return 0, err | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Per above comment you're also not consistently returning There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch - in that case I'll just update the whole thing to use |
||
} | ||
|
||
processedSetHashes := make(map[string]struct{}) | ||
issuanceCount := 0 | ||
// Loop through each set hash result, counting issuance per unique set hash | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. *issuances |
||
// that are within the window specified by the earliest parameter | ||
for _, result := range results { | ||
key := string(result.SetHash) | ||
// Skip set hashes that we have already processed - we only care about the | ||
// first issuance | ||
if _, exists := processedSetHashes[key]; exists { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note: if we use a
In general, I encourage maps of bools for sets rather than maps of empty struct unless the set is very large, since the readability is typically more important than size in memory. I acknowledge that my advice on this may have changed during the course of the project. :-) |
||
continue | ||
} | ||
|
||
// If the issued date is before our earliest cutoff then skip it | ||
if result.Issued.Before(earliest) { | ||
continue | ||
} | ||
|
||
// Otherwise note the issuance and mark the set hash as processed | ||
issuanceCount++ | ||
processedSetHashes[key] = struct{}{} | ||
} | ||
|
||
// Return the count of how many non-renewal issuances there were | ||
return issuanceCount, nil | ||
} | ||
|
||
// FQDNSetExists returns a bool indicating if one or more FQDN sets |names| | ||
// exists in the database | ||
func (ssa *SQLStorageAuthority) FQDNSetExists(ctx context.Context, names []string) (bool, error) { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's change this to "non-renewal certificate issuances," which I think parses a bit better.