coreos / rpm-ostree

⚛📦 Hybrid image/package system with atomic upgrades and package layering
https://coreos.github.io/rpm-ostree
Other
857 stars 193 forks source link

Use rpm-oxide for rpm parsing and processing #2836

Open DemiMarie opened 3 years ago

DemiMarie commented 3 years ago

To reduce attack surface, rpm-ostree should integrate with the rpm-oxide toolkit from Qubes OS. This toolkit provides Rust parsers and serializers for RPM headers, and supports validating and canonicalizing RPM packages and OpenPGP signatures. It was built to work around the security vulnerabilities I discovered in RPM, and to that end is written in Rust. The rpm-oxide toolkit uses librpm for all cryptographic operations. As a result, it is fully compliant with system-wide cryptographic policies, and trusts the same keys RPM does. All OpenPGP signatures are validated before being passed to librpm, so vulnerabilities in librpm’s OpenPGP parser do not affect it.

Currently, the main user of rpm-oxide is the (included) rpmcanon command-line canonicalizer. rpmcanon is used in production as part of Qubes OS’s dom0 updater in R4.1, and is currently being backported to R4.0. rpmcanon successfully processes the entire Fedora 32 package repository, and rejects only one (malformed) package from the Fedora 25 repository.

jlebon commented 3 years ago

Neat! Do you have the API docs hosted somewhere? Are all the metadata fields available in the RPM header also available via rpm-oxide?

DemiMarie commented 3 years ago

Neat! Do you have the API docs hosted somewhere?

Not yet. rpm-oxide has not been published to crates.io, either. That said, all of rpm-oxide’s releases are indicated by Git tags signed by @marmarek.

Are all the metadata fields available in the RPM header also available via rpm-oxide?

There is a callback-based API that is used internally and exposes everything, but I don’t remember if it is public. That said, it is not very idiomatic Rust. What kind of API would you prefer?

cgwalters commented 3 years ago

I'm in favor of this, broadly speaking.

We may need to add some sort of feature flags that would allow a user to easily fall back if something goes wrong. Something like echo rpm-oxide: false >> /etc/rpm-ostreed.conf or so?

The keyring bits look a bit tricky, see https://github.com/rpm-software-management/libdnf/issues/43 - I assume Qubes is using the usual GPG keys stored in the rpmdb, but we rely on /etc/pki/rpm-gpg. I think we could either make that an option, or add an API to feed keys directly?

Can you link to the code in dom0 that consumes this? (I briefly looked through obvious candidates in https://github.com/QubesOS but couldn't find the updater) In particular what I'm wondering here is - after verification, the resulting payload is normally a cpio, but that's not well defined, right? IIRC RPM can use something else for large packages.

I assume in your case you're likely just passing the whole file/fd from the start back to librpm (after payload verification?). In our case we want to do some streaming transformations (e.g. rewriting /var/ to systemd-tmpfiles) so we might end up passing the fd back to rpm2archive or so (xref https://github.com/coreos/rpm-ostree/issues/2458 ). That said, a lot of things would get better with an API where we extract the header separately using this code (bit for bit) rather than having librpm do it.

DemiMarie commented 3 years ago

I'm in favor of this, broadly speaking.

We may need to add some sort of feature flags that would allow a user to easily fall back if something goes wrong. Something like echo rpm-oxide: false >> /etc/rpm-ostreed.conf or so?

The keyring bits look a bit tricky, see rpm-software-management/libdnf#43 - I assume Qubes is using the usual GPG keys stored in the rpmdb, but we rely on /etc/pki/rpm-gpg. I think we could either make that an option, or add an API to feed keys directly?

That would not be too difficult. It would definitely require an OpenPGP signature packet parser. It would also require either generating a custom librpm keyring or invoking the underlying cryptographic libraries directly.

Can you link to the code in dom0 that consumes this? (I briefly looked through obvious candidates in https://github.com/QubesOS but couldn't find the updater) In particular what I'm wondering here is - after verification, the resulting payload is normally a cpio, but that's not well defined, right? IIRC RPM can use something else for large packages.

Qubes OS uses the rpmcanon command line tool via the qubes-receive-updates script.

I assume in your case you're likely just passing the whole file/fd from the start back to librpm (after payload verification?). In our case we want to do some streaming transformations (e.g. rewriting /var/ to systemd-tmpfiles) so we might end up passing the fd back to rpm2archive or so (xref #2458 ). That said, a lot of things would get better with an API where we extract the header separately using this code (bit for bit) rather than having librpm do it.

In Qubes OS, the (validated) output is just written to a file, from which it is incorporated into an RPM repository. Qubes OS already does transformations on the signature header. Transformations on the main header would be possible but riskier, as they break the signature.

cgwalters commented 3 years ago

Qubes OS uses the rpmcanon command line tool via the qubes-receive-updates script.

There's a lot of code there that's not in the library. A few questions:

DemiMarie commented 3 years ago

Qubes OS uses the rpmcanon command line tool via the qubes-receive-updates script.

There's a lot of code there that's not in the library. A few questions:

Most of this code is related to filesystem I/O. Please note that this code is pre-1.0 and was rushed out to mitigate vulnerabilities in RPM. It has been audited and is used in production, but needs more testing and cleanup before it is ready for broader use.

  • What's up with the cfg(not(linux)) code? What OS is that and why do you care?

This code is a workaround for the lack of O_TMPFILE support on non-Linux systems. It can and should be replaced by a proper atomic file writing interface.

  • Why not move the "verify and canonicalize rpm" code into the library and have it take just an impl std::io::Write letting the caller do whatever they want with the data, keeping the nontrivial "rename file" bits for the CLI?

I have not done it yet :smile:. Will do.

  • Related to that on Linux I try to use O_TMPFILE which avoids the need to do "unlink on failure" etc.; this is bound up nicely in an API on our openat-ext crate) - EDIT: But I guess you're trying to avoid many external crate deps?

RPM-Oxide does not use any external crates, at all. That is why it has the unstable rustc_private dependency in the CLI. As a matter of policy, Qubes OS does not rely on external code that it cannot verify by means of secure hash or code signature.

  • More broadly - if the rpm-oxide code has GPG validated the header (and all RPMs are ultimately trusted, so no concern about malformed headers), do you have a sense of whether known exploits in librpm are still reachable?

All of the known ones are not reachable. I believe unknown ones are also not reachable. That said, rpm-oxide is intended to be used in cases where the RPM is not ultimately trusted, too.

cgwalters commented 3 years ago

Initial draft in https://github.com/coreos/rpm-ostree/pull/2861

cgwalters commented 3 years ago

One thing I realized is that actually because libdnf today does the "load all the /etc/pki/rpm-gpg keys" into an rpmKeyring instance, we should be able to just pass that along to the verifier. Looks like that just may need some small patches.

DemiMarie commented 3 years ago

One thing I realized is that actually because libdnf today does the "load all the /etc/pki/rpm-gpg keys" into an rpmKeyring instance, we should be able to just pass that along to the verifier. Looks like that just may need some small patches.

Indeed! PRs (and issues) are welcome. Also, if you know of a good, decently-license source of malformed signatures (for tests), that would be greatly appreciated.