Skip to content
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

Check for duplicate certs before adding to db #5497

Merged
merged 3 commits into from
Jul 8, 2021
Merged

Conversation

andygabby
Copy link
Member

Error at SA if the certificate or precertificate already exist in the
database

Fixes: #5468

Error at SA if the certificate or precertificate already exist in the
database

Fixes: #5468
Comment on lines 65 to 66
var rows []interface{}
results, err := txWithCtx.Select(&rows, "SELECT id FROM precertificates WHERE serial=?", serialHex)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's simpler (and maybe faster?) to do:

Suggested change
var rows []interface{}
results, err := txWithCtx.Select(&rows, "SELECT id FROM precertificates WHERE serial=?", serialHex)
c, err := txWithCtx.SelectInt("SELECT count(id) FROM precertificates WHERE serial=?", serialHex)

and then compare c > 0 instead of len(results) > 0.

Copy link
Member

@beautifulentropy beautifulentropy Jun 17, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the gorp docs:

SelectInt executes the given query, which should be a SELECT statement for a single integer column, and returns the value of the first row returned. If no rows are found, zero is returned.

So using 0 as a sentinel would be c == 0.

Alternatively, you can do a SelectOne() and your sentinel would be: errors.Is(err, sql.ErrNoRows) but you'll also have to check for fmt.Errorf("gorp: multiple rows returned for: %s - %v", query, args) so the SelectInt() is probably the more elegant solution.

If you use a SelectOne(), make a composite type with an exported field of ID int64 and pass a slice of that composite type as the holder.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that my suggestion is to select count(id), not just id, so we're not looking for a sentinel here -- we're literally just looking for "how many rows would you have returned?", and if the answer is anything greater than zero, then we have a problem.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that my suggestion is to select count(id), not just id, so we're not looking for a sentinel here -- we're literally just looking for "how many rows would you have returned?", and if the answer is anything greater than zero, then we have a problem.

Ah, I missed that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @aarongable's proposal in concept, but there's a catch: we use the db.Executor interface here, which is a subset of the methods generally available on gorp.DbMap. It includes Select and SelectOne, but it doesn't include SelectInt. I think it's probably not worth plumbing through SelectInt (and making sure it's implemented in all relevant DB mocks) for this small optimization. What do you think, @aarongable ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gah, you're probably right that it isn't worthwhile "just" for this, but I do see a bunch of other places where we could use it. In particular, there are a bunch of places where we use SelectOne(&count, ...) in order to get a single integer out of the database. Which is also what we should do here, if we don't plumb SelectInt all the way through the wrapper.

Comment on lines 65 to 66
var rows []interface{}
results, err := txWithCtx.Select(&rows, "SELECT id FROM precertificates WHERE serial=?", serialHex)
Copy link
Member

@beautifulentropy beautifulentropy Jun 17, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the gorp docs:

SelectInt executes the given query, which should be a SELECT statement for a single integer column, and returns the value of the first row returned. If no rows are found, zero is returned.

So using 0 as a sentinel would be c == 0.

Alternatively, you can do a SelectOne() and your sentinel would be: errors.Is(err, sql.ErrNoRows) but you'll also have to check for fmt.Errorf("gorp: multiple rows returned for: %s - %v", query, args) so the SelectInt() is probably the more elegant solution.

If you use a SelectOne(), make a composite type with an exported field of ID int64 and pass a slice of that composite type as the holder.

- Switch to SelectOne
- Add test cases for duplicate cert and precertificates
@andygabby andygabby marked this pull request as ready for review June 22, 2021 23:34
@andygabby andygabby requested a review from a team as a code owner June 22, 2021 23:34
Copy link
Contributor

@jsha jsha left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good as-is. As a followup, it would be nice to add an extra layer of enforcement for the property I mentioned in #5467 (comment): that anytime we are treating rows as duplicates in this table it is because they are exact byte-for-byte DER duplicates.

What I imagine that looking like here is changing the SelectOne to also get the DER. We would then compare that to the DER we're trying to insert. If it's the same, we can happily return DuplicateError. Otherwise, we should go ahead with the insert (to ensure we have a copy for investigation), and audit log the problem. Alternately, we could return some error other than DuplicateError, which would cause the CA to keep the certificate in its orphan queue and keep retrying, which would eventually show up in the metrics.

@andygabby andygabby merged commit ad34089 into main Jul 8, 2021
@andygabby andygabby deleted the andygabby/issue5468 branch July 8, 2021 18:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Avoid inserting duplicate certificates or precertificates
4 participants