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

rpmKeyring: Support keys with the same key ID #3398

Merged
merged 1 commit into from
Oct 24, 2024

Conversation

ffesti
Copy link
Contributor

@ffesti ffesti commented Oct 22, 2024

Loop over the candidates during signature verification and use the one verifying it - iff any. Otherwise use last key with matching key ID (basically a random one).

Resolves: #3334

@ffesti ffesti requested a review from a team as a code owner October 22, 2024 09:44
@ffesti ffesti requested review from pmatilai and removed request for a team October 22, 2024 09:44
@ffesti
Copy link
Contributor Author

ffesti commented Oct 22, 2024

For now the test case only has two keys with matching key IDs. Will add tests with 3 or 4 keys with the same id at some later point.

@ffesti ffesti force-pushed the 3334 branch 3 times, most recently from d64ab89 to f48524f Compare October 22, 2024 12:11

auto range = keyring->keys.equal_range(key->keyid);
auto item = range.first;
for (; item != range.second; item++) {
Copy link
Member

Choose a reason for hiding this comment

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

When at all possible, keep the loop variable inside the for. It should also be a reference, this is now making copies of the keys as it loops, and favor pre-increment. And then the whole thing becomes something like

for (auto const & item = range.first; item != range.second; ++item) {
    if (item->second->fp == key->fp)
        return rpmPubkeyLink(item->second);
}
return NULL;

Alternatively, leave the rkey declaration before the loop, just initialized to NULL and break out as needed, that's kinda matter of style.

/* use first verifying key or last one with matching keyid */
auto range = keyring->keys.equal_range(keyid);
auto item = range.first;
for (; item != range.second; item++) {
Copy link
Member

Choose a reason for hiding this comment

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

Same as above, make item a reference inside the for-loop, pre-increment.

rpmio/rpmkeyring.cc Outdated Show resolved Hide resolved

if (key)
pgpkey = key->pgpkey;
if (pgpVerifySignature(pgpkey, sig, ctx) == RPMRC_OK)
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't seem right, we're now verifying the same signature twice. It's expensive enough that we don't want to be doing that.

if (range.first != range.second && !key) {
for (item = range.first; item != range.second; item++) {
key = item->second;
rc = pubkeyVerifySig(key, sig, ctx);
Copy link
Member

Choose a reason for hiding this comment

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

Don't reverify, just collect any errors on the first run and then log them all if the result is a failure.

@ffesti
Copy link
Contributor Author

ffesti commented Oct 23, 2024

rpmKeyringModify still has auto item = range.first; outside the loop. But this is going to change for #3350 anyway.

@ffesti ffesti requested a review from pmatilai October 23, 2024 12:25
@pmatilai
Copy link
Member

Being outside loop is not a crime, it's just something to generally avoid if possible.

On that note, ignore my earlier mumble about references and copies (as you did) - the thing you get from equal_range() is an iterator, not the item itself. "item" is not the best of names for it in all these cases. "it" is the c++ iterator counterpart for "int i" idiom.

rpmKeyringVerifySig2() is unnecessarily convoluted though. Just push all the lints into the vector and on non-ok final result log it all in a loop, and use a c++ string so you don't need to manually clear it at the end. The no-key sanity check should only ever occur if we didn't find a key at all, not on any failure, and should use the same exact return code handling as the keyed version - you'll want a small helper function to handle the lints collection and all.

if (lints) {
if (rc != RPMRC_OK) {
/* Only failing keys */
for (auto item : results) {
Copy link
Member

Choose a reason for hiding this comment

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

Oh and here we get to the wonders of C++ diversity 😆 - this syntax makes copies, so except for trivialities like integers you always want to use a reference in this construct, called "range based for loop". And, whenever possible, use a const reference at that.
So: for (auto const & item) (and here "item" is appropriate because it is a reference to the item, not an iterator)

@ffesti ffesti force-pushed the 3334 branch 2 times, most recently from 9bef89e to 02842c1 Compare October 23, 2024 14:20
@ffesti
Copy link
Contributor Author

ffesti commented Oct 23, 2024

OK, simplified the code. Just print out the right messages. I'd guess there shouldn't be a message for RPMRC_OK but the code for now does not make that assumption.

@pmatilai
Copy link
Member

pmatilai commented Oct 24, 2024

Okay, looks better, but there are a couple of further improvements to be made. This being security sensitive code, you'll want the logic to be as lean and mean and obvious as you possibly can.

There are two entirely separate cases here:

  • no key, which can only return FAIL/NOKEY
  • one or more matches, which can only return FAIL/OK
    You'll want those in a top-level if-else so there's no doubt whatsoever: only one of them can execute, and keyptr can only be set by the latter. Which means "key" can and should move to a more local scope in the second. And for the no key case, it'd be a good idea to add an actual assert to make it 200% clear: it can never ever return OK.

Add that helper function around the pgpVerifySignature2() and lints collection as I suggested in the previous comment, it's not that it's a large amount of code but to prove a point: even the simple three lines differ here, the first one using a gcc-extension syntax. Plus it'll make the business logic stand out more by isolating the C strings management out of sight entirely.

The log output logic can probably simplified a lot by realizing that if we get an OK, we can just wipe out any previous results away: with the above logic impelemented, they can only be from non-matching keys and uninteresting if we found a match. So just do results.clear() if you got OK and then you can simply log any non-empty output.


for (auto const & item : results) {
if (((rc == RPMRC_OK) == (item.first == RPMRC_OK))
&& item.second != "")
Copy link
Member

Choose a reason for hiding this comment

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

item.second.empty() would be the more appropriate test for an empty C++ string. Not that it affects the results here.


if (keyring && sig) {
if (sig && ctx) {
char *lints = NULL;
Copy link
Member

@pmatilai pmatilai Oct 24, 2024

Choose a reason for hiding this comment

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

This will be moot once you add the wrapper around pgpVerifySignature2() which makes lints local, but this is another example of a variable in the wrong scope and in this case, dangerous: lints can be free'd and then accessed again later, but the pointer is not NULL'ed which is akin to leaving a booby trap in the code. Does pgpVerifySignature2() always assign to the lints pointer when passed? Maybe it does, but if not then we can end up accessing freed memory. You don't want to rely on such a thing.

rpmPubkey key = NULL;
std::vector<std::pair<int, std::string>> results = {};
Copy link
Member

@pmatilai pmatilai Oct 24, 2024

Choose a reason for hiding this comment

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

And a scope-issue here too - the results vector is only needed inside the main if, so that's where it belongs. And key inside the only code that can actually end with it being set.

Copy link
Member

Choose a reason for hiding this comment

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

Variables being in the most local scope is something that really belongs to self-review part, FWIW.

*keyptr = pubkeyPrimarykey(key);

for (auto const & item : results) {
if (((rc == RPMRC_OK) == (item.first == RPMRC_OK))
Copy link
Member

Choose a reason for hiding this comment

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

Just a side-remark as I think clearing the results on OK is a better solution here, but such code can usually be made far more readable by giving the complex condition a human understandable name. Eg this might be something like

bool relevant_result = ((rc == RPMRC_OK) == (item.first == RPMRC_OK));
if (relevant_result && !item.second.empty)
    rpmlog()...

@ffesti
Copy link
Contributor Author

ffesti commented Oct 24, 2024

There are two entirely separate cases here:

* no key, which can only return FAIL/NOKEY

* one or more matches, which can only return FAIL/OK
  You'll want those in a top-level if-else so there's no doubt whatsoever: only one of them can execute, and keyptr can only be set by the latter. Which means "key" can and should move to a more local scope in the second. And for the no key case, it'd be a good idea to add an actual assert to make it 200% clear: it can never ever return OK.

This is wrong IMHO. There may be keys that get dropped in the loop because we can figure out they do not match. If we can get the fingerprint of some signatures at some point we might be able to filter out non-matching keys even more. In these cases we still need to run the NOKEY part if all keys got dropped.

Also even in the NOKEY part we want to write NULL to the keyptr so callers can rely on their key variable getting updated.

@pmatilai
Copy link
Member

Oh, right. I missed the implications of the newly added filtering in the loop and that of course changes the landscape a lot, so taking that part back.

As for the keyptr setting - yup, for NOKEY you'll want it and the key set to an explicit NULL, and similarly explicit for the case where we actually use a key value in there. It's not at all obvious what the value of "key" is when it falls out of the loops and all above it.

@pmatilai
Copy link
Member

Okay, seeing the latest version: I'd add an explicit "key = NULL" to the "No key" case, and then we can call this done.

@pmatilai
Copy link
Member

...and then you don't need to NULL it in the loop.

Loop over the candidates during signature verification and use the one
verifying it - iff any. Otherwise print error messages from all
candidates.

Resolves: rpm-software-management#3334
@ffesti
Copy link
Contributor Author

ffesti commented Oct 24, 2024

Guess it is a matter of taste. The original code just made sure the value of key was valid all the time - NULL or not. Now that we rely on the results vector that is not needed as urgently

@pmatilai
Copy link
Member

Matter of taste to a degree, sure, but we completely rely on the results vector for the nokey case anyhow so to me it's clearer that all the nokey logic is then in one spot.

Copy link
Member

@pmatilai pmatilai left a comment

Choose a reason for hiding this comment

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

Looks fine to me now.

@pmatilai pmatilai merged commit 09510e4 into rpm-software-management:master Oct 24, 2024
1 check passed
@ffesti ffesti deleted the 3334 branch October 25, 2024 08:59
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.

keyring only allows unique key IDs
2 participants