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

Validate self-signatures and require subkey bindings on PGP public keys #1788

Closed
wants to merge 3 commits into from

Conversation

pmatilai
Copy link
Member

@pmatilai pmatilai commented Sep 30, 2021

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.

int xx = -1;
DIGEST_CTX hash = rpmDigestInit(selfsig->hash_algo, 0);
hashKey(hash, &all[0]);

Copy link
Contributor

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.

Copy link
Member Author

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...

Copy link
Contributor

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.

Copy link
Member Author

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...)

Copy link
Member Author

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);
Copy link
Contributor

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?

Copy link
Contributor

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.

Copy link
Member Author

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?

Copy link
Contributor

@mlschroe mlschroe Oct 11, 2021

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.

Copy link
Member Author

@pmatilai pmatilai Oct 11, 2021

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.

Copy link
Contributor

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)

Copy link
Contributor

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.

Copy link
Member Author

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.

rpmio/rpmpgp.c Outdated Show resolved Hide resolved
break;

if (pkt->tag == PGPTAG_PUBLIC_SUBKEY)
expect = PGPTAG_SIGNATURE;
Copy link
Contributor

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?

Copy link
Member Author

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.

Copy link
Member Author

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.

Copy link
Contributor

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.
Copy link
Contributor

@DemiMarie DemiMarie left a 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.

Comment on lines +559 to +560
if (sigalg->setmpi(sigalg, i, p))
break;
Copy link
Contributor

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.

Copy link
Member Author

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.

Copy link
Contributor

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;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
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.

Copy link
Member Author

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.

Copy link
Member Author

@pmatilai pmatilai Oct 13, 2021

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);
Copy link
Contributor

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;
Copy link
Contributor

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)
{
Copy link
Contributor

@DemiMarie DemiMarie Oct 11, 2021

Choose a reason for hiding this comment

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

Suggested change
{
{
assert(i > 0 && "pgpVerifySelf passed invalid i");

Copy link
Member Author

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

Edited

Copy link
Member Author

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.

Copy link
Contributor

@DemiMarie DemiMarie left a 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)
{
Copy link
Contributor

Choose a reason for hiding this comment

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

Edited

Comment on lines +559 to +560
if (sigalg->setmpi(sigalg, i, p))
break;
Copy link
Contributor

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.

Comment on lines +1127 to +1128
/* ignore unknown types */
rc = 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/* ignore unknown types */
rc = 0;
/* reject unknown types */

Copy link
Member Author

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.

@pmatilai
Copy link
Member Author

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.

@pmatilai pmatilai closed this Oct 13, 2021
@soig
Copy link
Contributor

soig commented Feb 5, 2022

Hi
Can you backport those fixes to rpm-4.16.x?
We need them for fixing the CVE in Mageia 8 (and probably other distributions).
Thanks
b5e8bc7 & 9f03f42 apply cleanly.
But not bd36c5d since there's been refactoring between 4.16 & 4.17

@DemiMarie
Copy link
Contributor

DemiMarie commented Feb 5, 2022

Hi
Can you backport those fixes to rpm-4.16.x?
We need them for fixing the CVE in Mageia 8 (and probably other distributions).
Thanks
b5e8bc7 & 9f03f42 apply cleanly.
But not bd36c5d since there's been refactoring between 4.16 & 4.17

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.

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.

4 participants