-
Notifications
You must be signed in to change notification settings - Fork 375
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
Validate self-signatures and require subkey bindings on PGP public keys #1788
Conversation
cf99101
to
5641a79
Compare
int xx = -1; | ||
DIGEST_CTX hash = rpmDigestInit(selfsig->hash_algo, 0); | ||
hashKey(hash, &all[0]); | ||
|
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.
You might want to verify the package types of the package you're hashing, i.e. all[i-1]
should be of the required type.
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.
Does it actually matter though? I mean, if the type is wrong then the hash will also be wrong and simply fail to pass? Okay, as a belt and suspenders kind of thing I suppose...
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.
I also think that it does not matter, but it's the right thing to do for security relevant code ;-)
Anyway, for the _CERT verification you can't use all[i-1] as there may be multiple signatures.
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.
Oh, good point on the CERT verification (annoying as it may be...)
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.
Changed it to check for the types and more errors overall, userid lookup in the CERT case still missing.
} | ||
} | ||
|
||
if (pgpPrtPkt(&pkt, digp)) | ||
if (digp->tag == PGPTAG_PUBLIC_KEY && pkt->tag == PGPTAG_SIGNATURE) | ||
selfsig = pgpDigParamsNew(pkt->tag); |
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.
The code assumes that the self-sig always comes first if there are multiple signatures. Does the standard say something about this?
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.
Maybe you should also check that the issuer id matches and ignore non-selfsigned sigs.
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.
https://datatracker.ietf.org/doc/html/rfc4880#section-11.1 says:
Each Subkey packet MUST be followed by one Signature packet, which should be a subkey binding signature issued by the top-level key.
Surely a signature with a different issuer will simply not pass?
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.
Sure, but that's a problem as the signature checking code below is done for all signatures, not just the subkey binding one.
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.
I'm probably Monday dense here, but I fail to see the problem.
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.
Maybe it's me ;)
My point is that selfsig will also be set for signatures that use a different key (i.e. not self-signed) and the code below will call pgpVerifySelf on them and then break the loop as the verification will fail.
(Of course I'm talking about _CERT sigs, not the subkey binding one)
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.
I am fine with assuming and requiring that the self-signature comes first, provided that doing so is interoperable. Keys with non-self-signatures obviously need to be accepted, but I am fine with RPM ignoring the extra signatures and omitting them from the parsed result.
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.
My point is that selfsig will also be set for signatures that use a different key (i.e. not self-signed) and the code below will call pgpVerifySelf on them and then break the loop as the verification will fail.
Right, got it. I think.
This is all a surprisingly nasty tangle of logic. Perhaps I should split the _CERT part out of this to simplify, that's not part of the CVE anyway and that's what I want to get out of the way.
break; | ||
|
||
if (pkt->tag == PGPTAG_PUBLIC_SUBKEY) | ||
expect = PGPTAG_SIGNATURE; |
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.
Should we also enforce a self-sig on a User ID Packet?
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.
I considered that, but it'd be against the spec:
Immediately following each User ID packet, there are zero or more Signature packets.
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.
It'd be good to differentiate between verified and non-verified somehow though.
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.
I believe GPG ignores User ID packets that have no self-signatures. RPM should either reject such packets or ignore them; processing them as if they were verified is a bad idea.
No immediate effect but needed by the following commits.
No functional changes, just to reduce code duplication and needed by the following commits.
All subkeys must be followed by a binding signature by the primary key as per the OpenPGP RFC, enforce the presence and validity in the parser. In addition, validate user ID certification signatures when present. The implementation is as kludgey as they come to work around our simple-minded parser structure without touching API, to maximise backportability. Store all the raw packets internally as we decode them to be able to access previous elements at will, needed to validate ordering and access the actual data. Add testcases for manipulated keys whose import previously would succeed. Depends on the two previous commits: db7ab0a and e3dd156 Fixes CVE-2021-3521 and more.
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.
At a minimum, there needs to be a check for signature type in the code that verifies package signatures, now that such signatures will no longer be automatically rejected.
if (sigalg->setmpi(sigalg, i, p)) | ||
break; |
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.
With this change, RPM needs to check that package signatures are in fact signatures of binary documents. I am not aware of an actual exploit, but it is the Right Thing To Do, since a security patch should not regress security elsewhere.
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.
Well, instead of removing the check maybe we should check for the kinds that we actually support in the PGP parser. We dont do TEXT anyway.
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.
A certification or binding signature isn’t a valid package signature and should not be accepted where a package signature is required. The parser doesn’t currently know what kind of signature is expected and so can’t make that decision. That said, another option would be to add a new API function that only parses signatures, and which expects the type of the signature as an argument.
if (pkttype == PGPTAG_SIGNATURE) | ||
break; | ||
|
||
if (alloced <= i) { | ||
alloced *= 2; |
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.
alloced *= 2; | |
if (alloced > INT_MAX / 2 || alloced > SIZE_MAX / (4 * sizeof(*all))) | |
abort(); | |
alloced *= 2; |
This is a last-ditch check against memory corruption due to integer overflow. Such long keys should be rejected earlier in any case.
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.
No. Just no. I've told you before, we wont be littering the code-base with stuff like this. If we did, nobody could find the actual code from underneath.
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.
To elaborate on that a bit, the suggested change is simply absurd when you could simply place an upper bound and error out if exceeded.
} | ||
} | ||
|
||
if (pgpPrtPkt(&pkt, digp)) | ||
if (digp->tag == PGPTAG_PUBLIC_KEY && pkt->tag == PGPTAG_SIGNATURE) | ||
selfsig = pgpDigParamsNew(pkt->tag); |
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.
I am fine with assuming and requiring that the self-signature comes first, provided that doing so is interoperable. Keys with non-self-signatures obviously need to be accepted, but I am fine with RPM ignoring the extra signatures and omitting them from the parsed result.
break; | ||
|
||
if (pkt->tag == PGPTAG_PUBLIC_SUBKEY) | ||
expect = PGPTAG_SIGNATURE; |
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.
I believe GPG ignores User ID packets that have no self-signatures. RPM should either reject such packets or ignore them; processing them as if they were verified is a bad idea.
|
||
static int pgpVerifySelf(pgpDigParams key, pgpDigParams selfsig, | ||
const struct pgpPkt *all, int i) | ||
{ |
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.
{ | |
{ | |
assert(i > 0 && "pgpVerifySelf passed invalid i"); |
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.
You're suggesting to add two lines of a comment to explain what an assert() is? Come on.
Review of the PGP matters is totally welcome, but material like this is only good for raising my blood pressure.
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.
Edited
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.
The point is that we don't sprinkle material like this around. This is just redundant clutter in the codebase which makes it unreadable. Whenever you feel the need to add a comment or an assert, it's more likely because the code in question is dumb and could be written in a better way. Such as here.
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.
Package signatures need to be checked to be of PGPSIGTYPE_BINARY
, and keys with third-party certifications must not be rejected. I believe nonsensical signature types should be rejected.
|
||
static int pgpVerifySelf(pgpDigParams key, pgpDigParams selfsig, | ||
const struct pgpPkt *all, int i) | ||
{ |
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.
Edited
if (sigalg->setmpi(sigalg, i, p)) | ||
break; |
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.
A certification or binding signature isn’t a valid package signature and should not be accepted where a package signature is required. The parser doesn’t currently know what kind of signature is expected and so can’t make that decision. That said, another option would be to add a new API function that only parses signatures, and which expects the type of the signature as an argument.
/* ignore unknown types */ | ||
rc = 0; |
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.
/* ignore unknown types */ | |
rc = 0; | |
/* reject unknown types */ |
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.
No, rejecting types we cannot handle would only cause us to fail on perfectly legitimate keys. IIRC the PGP spec quite specifically tells you to ignore what you don't know, which generally is the key to future expandable standards.
The subkey binding part simplified a bit and split to #1795, the user certification is more involved and has all manner of strange open questions, I don't have time to deal with that now. Thanks for the feedback so far. |
The OpenPGP packet parser has had a lot of hardening between 4.16 and 4.17. daeddb0 adds a check that b5e8bc7 removed, and there are other commits that fix various undefined behaviors. I suggest backporting all of the commits in https://github.com/rpm-software-management/rpm/commits/714e606558b4517bd2d02d03a0ddad7da79a58c6/rpmio/rpmpgp.c. |
All subkeys must be followed by a binding signature by the primary key as per the OpenPGP RFC, enforce the presence and validity in the parser. In addition, validate user ID certification signatures when present.
The implementation is as kludgey as they come to work around our simple-minded parser structure without touching API, to maximise backportability. Store all the raw packets internally as we decode them to be able to access previous elements at will, needed to validate ordering and access the actual data. Add testcases for manipulated keys whose import previously would succeed.
Depends on the two previous commits:
db7ab0a and
e3dd156
Fixes CVE-2021-3521 and more.