Andre Bogus Andre "llogiq" Bogus is a Rust contributor and Clippy maintainer. A musician-turned-programmer, he has worked in many fields, from voice acting and teaching, to programming and managing software projects. He enjoys learning new things and telling others about them.

Comparing Rust supply chain safety tools

8 min read 2397

Rust Logo

Rust makes it easy to add dependencies to your project: edit your Cargo.toml (or use cargo add to have it changed for you from the command line), and you get to use the crate. But do you know what you’ve added to your project? Are you sure you can trust those lines of code?

Of course, some developers won’t bother to find out. After all, if a crate is in wide use, what are the chances it’s doing something fishy? As it turns out, there’s ample precedent for that.

In the JavaScript world, popular npm libraries can be hijacked from the original maintainer and subverted for presumably criminal purposes. Nowadays, researchers count the number of compromised or otherwise malicious packages in the thousands.

Even when there’s no ill will, libraries can contain weaknesses — the recent Log4Shell debacle gave many Java system maintainers sleepless nights.

But what can we do as developers? Sticking our collective heads in the sand is a recipe for disaster. Checking your entire dependency with your own eyes is too costly for all but the most security-sensitive projects.

In this post, we’ll look at tools that will give us a modicum of security for minimal effort, focus our attention where it is likely needed, or even amplify our review efforts for the improvement of our community’s supply chain security.

Using cargo-audit for security in Rust

Cargo-audit is built by the Rust Secure Code working group and is the canonical end-user interface to their RustSec Advisory Database. You can install it with cargo install cargo-audit.

Once installed, you can run it by starting it from the base of your cargo projects by calling cargo audit. It will look into your Cargo.toml and scan the RustSec database for all your dependencies, printing all advisories it finds.

If you care about both your and your user’s security (and you should!), this is the absolute minimum amount of effort you can expend to ensure at least no known problems lurk in your dependency tree.

For example, here I run cargo audit on my mutagen crate:

We made a custom demo for .
No really. Click here to check it out.

$ cargo audit
    Fetching advisory database from `https://github.com/RustSec/advisory-db.git`
      Loaded 394 security advisories (from /home/andre/.cargo/advisory-db)
    Updating crates.io index
    Scanning Cargo.lock for vulnerabilities (60 crate dependencies)
Crate:         failure
Version:       0.1.8
Warning:       unmaintained
Title:         failure is officially deprecated/unmaintained
Date:          2020-05-02
ID:            RUSTSEC-2020-0036
URL:           https://rustsec.org/advisories/RUSTSEC-2020-0036
Dependency tree:
failure 0.1.8
├── mutagen-core 0.2.0
│   ├── mutagen-transform 0.2.0
│   │   └── mutagen 0.2.0
│   │       ├── mutagen-selftest 0.2.0
│   │       ├── feature-gated 0.2.0
│   │       ├── example-with-integration-tests-v2 0.2.0
│   │       ├── example-with-integration-tests-v1 0.2.0
│   │       └── example-simple 0.2.0
│   ├── mutagen-selftest 0.2.0
│   ├── mutagen 0.2.0
│   └── cargo-mutagen 0.2.1
└── cargo-mutagen 0.2.1

I should really switch to anyhow or something similar. Can someone please push a PR? ☺️

Checking sources and dependency licenses with cargo-deny

Built by Embark, cargo-deny goes a few steps further than cargo-audit and checks the sources and licenses of dependencies. Use it if you intend to publish your crate with the intention of people using it — there’s nothing like an incompatible license to sour the day of a Rustacean!

On the other hand, cargo-deny needs some information about your crate, so, to use it, you need to configure it. Luckily, you don’t have to manually write anything — a simple cargo deny init will do just that for you.

Once you’ve run that, you’ll find a fresh new file called deny.toml that you can edit to configure and customize your checks. You can read more about this in the GitHub repo.

(Interestingly, cargo-deny showed me I lost all license information on all Cargo.toml files at some point.)

Once correctly configured, you’ll get a reassuring one-line result (depending on your configuration, as some checks may be missing in your output):

advisories ok, bans ok, licenses ok, sources ok 

However, this assumes that your dependencies are up to date, otherwise, you may get a bunch of wordy warnings. In my compact_arena crate, for example, I used an older version of compiletest, which had a few dependencies in search of a maintainer, so I received some warnings like:

warning[A003]: term is looking for a new maintainer
   ┌─ /home/andre/projects/compact_arena/Cargo.lock:33:1
   │
33 │ term 0.6.1 registry+https://github.com/rust-lang/crates.io-index
   │ ---------------------------------------------------------------- unmaintained advisory detected
   │
   = ID: RUSTSEC-2018-0015
   = Advisory: https://rustsec.org/advisories/RUSTSEC-2018-0015
   = The author of the `term` crate does not have time to maintain it and is looking
     for a new maintainer.

     Some maintained alternatives you can switch to instead, depending
     on your needs:

     - \[`crossterm`\](https://github.com/crossterm-rs/crossterm)
     - \[`termcolor`\](https://crates.io/crates/termcolor)
     - \[`yansi`\](https://crates.io/crates/yansi)
   = Announcement: https://github.com/Stebalien/term/issues/93
   = Solution: No safe upgrade is available!
   = term v0.6.1
     └── tester v0.7.0
         └── compiletest_rs v0.5.0
             └── (dev) compact_arena v0.4.1

Getting several pages of these warnings may be disheartening, but there are some tools to deal with this situation.

Using cargo outdated and cargo duplicates

The canonical cargo command to update dependencies is cargo update. Still, those automatically applied updates won’t change major versions, as they may be incompatible.

So, you can call cargo outdated (after installing it with cargo install cargo-outdated) to find out what dependencies can be updated even further.

On the latest version of flame, for example, I get:

$ cargo outdated
Name                       Project  Compat   Latest   Kind    Platform
----                       -------  ------   ------   ----    --------
proc-macro2->unicode-xid   0.2.1    0.2.2    0.2.2    Normal  ---
quote->proc-macro2         1.0.24   1.0.36   1.0.36   Normal  ---
serde                      1.0.118  1.0.136  1.0.136  Normal  ---
serde_derive               1.0.118  1.0.136  1.0.136  Normal  ---
serde_derive->proc-macro2  1.0.24   1.0.36   1.0.36   Normal  ---
serde_derive->quote        1.0.7    1.0.15   1.0.15   Normal  ---
serde_derive->syn          1.0.54   1.0.86   1.0.86   Normal  ---
serde_json                 1.0.60   1.0.79   1.0.79   Normal  ---
serde_json->itoa           0.4.6    1.0.1    1.0.1    Normal  ---
serde_json->ryu            1.0.5    1.0.9    1.0.9    Normal  ---
serde_json->serde          1.0.118  1.0.136  1.0.136  Normal  ---
syn->proc-macro2           1.0.24   1.0.36   1.0.36   Normal  ---
syn->quote                 1.0.7    1.0.15   1.0.15   Normal  ---
syn->unicode-xid           0.2.1    0.2.2    0.2.2    Normal  ---
thread-id->libc            0.2.81   0.2.119  0.2.119  Normal  cfg(unix)

The latest version may be incompatible, so we may need to fix problems that arise from an update. There’s no single correct way to decide when to update, so you should always consider the costs of updating compared to the cost of keeping the current version.

This won’t solve our duplicate dependency problem, though. The reason for this problem is that cargo will often try to use the newest compatible version if possible.

However, as the dependency tree grows, so does the probability that a certain dependency appears multiple times — albeit with incompatible versions. Cargo’s solution is to simply build both versions and make them different name spaces.

If you use the type Foo from foo-0.3.2 and have a dependency that uses a Foo from foo-0.1.5, those Foos won’t be compatible. Worse, compiling your project will pull in both versions and compile them separately.

Installing cargo duplicates via cargo install cargo-duplicates will allow you to call cargo duplicates to at least find the parts of the dependency tree to either up- or downgrade. For example, on the current version of bytecount, I found two versions of cfg-if:

Package  Versions
-------  --------
cfg-if   1.0.0  0.1.10

cfg-if 1.0.0:
- Because of bytecount 0.6.2 => criterion 0.3.3 => rayon 1.5.0 => rayon-core 1.9.0 => crossbeam-channel 0.5.0 => cfg-if 1.0.0
- Because of bytecount 0.6.2 => criterion 0.3.3 => rayon 1.5.0 => crossbeam-deque 0.8.0 => cfg-if 1.0.0
- Because of bytecount 0.6.2 => criterion 0.3.3 => rayon 1.5.0 => rayon-core 1.9.0 => crossbeam-deque 0.8.0 => cfg-if 1.0.0
- Because of bytecount 0.6.2 => criterion 0.3.3 => rayon 1.5.0 => crossbeam-deque 0.8.0 => crossbeam-epoch 0.9.0 => cfg-if 1.0.0
- Because of bytecount 0.6.2 => criterion 0.3.3 => rayon 1.5.0 => rayon-core 1.9.0 => crossbeam-deque 0.8.0 => crossbeam-epoch 0.9.0 => cfg-if 1.0.0
- Because of bytecount 0.6.2 => criterion 0.3.3 => rayon 1.5.0 => rayon-core 1.9.0 => crossbeam-channel 0.5.0 => crossbeam-utils 0.8.0 => cfg-if 1.0.0
- Because of bytecount 0.6.2 => criterion 0.3.3 => rayon 1.5.0 => crossbeam-deque 0.8.0 => crossbeam-utils 0.8.0 => cfg-if 1.0.0
- Because of bytecount 0.6.2 => criterion 0.3.3 => rayon 1.5.0 => rayon-core 1.9.0 => crossbeam-deque 0.8.0 => crossbeam-utils 0.8.0 => cfg-if 1.0.0
- Because of bytecount 0.6.2 => criterion 0.3.3 => rayon 1.5.0 => crossbeam-deque 0.8.0 => crossbeam-epoch 0.9.0 => crossbeam-utils 0.8.0 => cfg-if 1.0.0
- Because of bytecount 0.6.2 => criterion 0.3.3 => rayon 1.5.0 => rayon-core 1.9.0 => crossbeam-deque 0.8.0 => crossbeam-epoch 0.9.0 => crossbeam-utils 0.8.0 => cfg-if 1.0.0
- Because of bytecount 0.6.2 => criterion 0.3.3 => rayon 1.5.0 => rayon-core 1.9.0 => crossbeam-utils 0.8.0 => cfg-if 1.0.0
- Because of bytecount 0.6.2 => rand 0.8.4 => rand_core 0.6.3 => getrandom 0.2.3 => cfg-if 1.0.0
- Because of bytecount 0.6.2 => quickcheck 1.0.3 => rand 0.8.4 => rand_core 0.6.3 => getrandom 0.2.3 => cfg-if 1.0.0
- Because of bytecount 0.6.2 => rand 0.8.4 => rand_chacha 0.3.1 => rand_core 0.6.3 => getrandom 0.2.3 => cfg-if 1.0.0
- Because of bytecount 0.6.2 => quickcheck 1.0.3 => rand 0.8.4 => rand_chacha 0.3.1 => rand_core 0.6.3 => getrandom 0.2.3 => cfg-if 1.0.0
- Because of bytecount 0.6.2 => rand 0.8.4 => rand_hc 0.3.1 => rand_core 0.6.3 => getrandom 0.2.3 => cfg-if 1.0.0
- Because of bytecount 0.6.2 => quickcheck 1.0.3 => rand 0.8.4 => rand_hc 0.3.1 => rand_core 0.6.3 => getrandom 0.2.3 => cfg-if 1.0.0
- Because of bytecount 0.6.2 => criterion 0.3.3 => plotters 0.2.15 => js-sys 0.3.46 => wasm-bindgen 0.2.69 => cfg-if 1.0.0
- Because of bytecount 0.6.2 => criterion 0.3.3 => plotters 0.2.15 => web-sys 0.3.46 => js-sys 0.3.46 => wasm-bindgen 0.2.69 => cfg-if 1.0.0
- Because of bytecount 0.6.2 => criterion 0.3.3 => plotters 0.2.15 => wasm-bindgen 0.2.69 => cfg-if 1.0.0
- Because of bytecount 0.6.2 => criterion 0.3.3 => plotters 0.2.15 => web-sys 0.3.46 => wasm-bindgen 0.2.69 => cfg-if 1.0.0
cfg-if 0.1.10:
- Because of bytecount 0.6.2 => quickcheck 1.0.3 => env_logger 0.8.4 => log 0.4.11 => cfg-if 0.1.10
- Because of bytecount 0.6.2 => quickcheck 1.0.3 => log 0.4.11 => cfg-if 0.1.10
- Because of bytecount 0.6.2 => criterion 0.3.3 => plotters 0.2.15 => js-sys 0.3.46 => wasm-bindgen 0.2.69 => wasm-bindgen-macro 0.2.69 => wasm-bindgen-macro-support 0.2.69 => wasm-bindgen-backend 0.2.69 => log 0.4.11 => cfg-if 0.1.10
- Because of bytecount 0.6.2 => criterion 0.3.3 => plotters 0.2.15 => web-sys 0.3.46 => js-sys 0.3.46 => wasm-bindgen 0.2.69 => wasm-bindgen-macro 0.2.69 => wasm-bindgen-macro-support 0.2.69 => wasm-bindgen-backend 0.2.69 => log 0.4.11 => cfg-if 0.1.10
- Because of bytecount 0.6.2 => criterion 0.3.3 => plotters 0.2.15 => wasm-bindgen 0.2.69 => wasm-bindgen-macro 0.2.69 => wasm-bindgen-macro-support 0.2.69 => wasm-bindgen-backend 0.2.69 => log 0.4.11 => cfg-if 0.1.10
- Because of bytecount 0.6.2 => criterion 0.3.3 => plotters 0.2.15 => web-sys 0.3.46 => wasm-bindgen 0.2.69 => wasm-bindgen-macro 0.2.69 => wasm-bindgen-macro-support 0.2.69 => wasm-bindgen-backend 0.2.69 => log 0.4.11 => cfg-if 0.1.10
- Because of bytecount 0.6.2 => packed_simd_2 0.3.4 => cfg-if 0.1.10

Someone should really fix this and make our build a tiny bit faster 🙂 .

Cargo-geiger and cargo-osha in Rust

Rust prides itself on safety. Its guarantees around memory safety are unmatched. However, it can only do that by also rejecting valid programs.

To be able to function as a systems language, it had to include a trap door to avoid those restrictions in cases where the programmer can ensure the safety of the code but cannot convince the compiler of that. That’s where unsafe comes in (I’ve previously written about this topic).

When a Rust program fails to live up to the memory safety we require, the lines of code that must be audited are within modules that contain unsafe code. Yet not all of this code needs to be your own. You may have pulled in unsafe code throughout the dependency tree without even knowing it.

Cargo-geiger — installed with cargo install cargo-geiger — will give you an overview of all the packages you use that include unsafe code. For example, in flame, I get the following:

Metric output format: x/y
    x = unsafe code used by the build
    y = total unsafe code found in the crate

Symbols:
    :) = No `unsafe` usage found, declares #![forbid(unsafe_code)]
    ?  = No `unsafe` usage found, missing #![forbid(unsafe_code)]
    !  = `unsafe` usage found

Functions  Expressions  Impls  Traits  Methods  Dependency

0/0        0/0          0/0    0/0     0/0      ?  flame 0.2.1-pre
0/0        7/7          1/1    0/0     0/0      !  ├── lazy_static 1.4.0
0/0        0/0          0/0    0/0     0/0      :) ├── serde 1.0.118
0/0        0/0          0/0    0/0     0/0      ?  │   └── serde_derive 1.0.118
0/0        0/0          0/0    0/0     0/0      ?  │       ├── proc-macro2 1.0.24
0/0        0/0          0/0    0/0     0/0      :) │       │   └── unicode-xid 0.2.1
0/0        0/0          0/0    0/0     0/0      :) │       ├── quote 1.0.7
0/0        0/0          0/0    0/0     0/0      ?  │       │   └── proc-macro2 1.0.24
0/0        45/45        3/3    0/0     2/2      !  │       └── syn 1.0.54
0/0        0/0          0/0    0/0     0/0      ?  │           ├── proc-macro2 1.0.24
0/0        0/0          0/0    0/0     0/0      :) │           ├── quote 1.0.7
0/0        0/0          0/0    0/0     0/0      :) │           └── unicode-xid 0.2.1
0/0        0/0          0/0    0/0     0/0      ?  ├── serde_derive 1.0.118
0/0        3/5          0/0    0/0     0/0      !  ├── serde_json 1.0.60
0/0        1/1          0/0    0/0     0/0      !  │   ├── itoa 0.4.6
8/12       674/921      0/0    0/0     2/2      !  │   ├── ryu 1.0.5
0/0        0/0          0/0    0/0     0/0      :) │   └── serde 1.0.118
0/0        4/4          0/0    0/0     0/0      !  └── thread-id 4.0.0
0/5        12/259       0/0    0/0     2/22     !      └── libc 0.2.81

8/17       746/1242     4/4    0/0     6/26

Using cargo-crev in Rust

This is all nice and well, but at some point, someone is going to have to pull up their sleeves and perform a proper code review. Wouldn’t it be nice to share the load of reviewing code with others we can trust and that’s secured by cryptographic signatures so that we can trust that the reviewers are who they claim to be?

Look no further than cargo-crev, which allows us to create a digital fingerprint and directly start entering reviews. Here’s how to set it up:

$ cargo install cargo-crev
$ # Note that hereby we trust the author of crev:
$ cargo crev trust --level high https://github.com/dpc/crev-proofs
$ cargo crev repo fetch all
$ cargo crev id set-url <your proof repo>
$ # as this exhorts you to set your password
$ cargo crev id passwd

Regarding <your proof repo>: you need to fork https://github.com/crev-dev/crev-proofs to set this up. If you have a GitHub account, you can go ahead and fork, otherwise, you can clone the repo and push it into whatever git service you use.

Now we are ready to review:

$ cargo crev open <crate>
$ cargo crev review <crate>

I have no idea why that isn’t one command, but the first command will prepare things and the second will open your $EDITOR with a review template that will contain a few fields. I’ve just submitted the review for my compact_arena crate.

kind: package review
version: -1
date: "2022-04-03T15:53:31.338910703+02:00"
from:
  id-type: crev
  id: FJuAGAaomkJL8JJojZdS_jVpqHIW__7QSXcWSnjAB6U
package:
  source: "https://crates.io"f
  name: compact_arena
  version: 0.4.1
  revision: b66f8a2f000cd91ce18f67f4c9f9974fe6e19ded
  digest: 6_pE4YUU_SbeJmBUty7B8buurO68FNBNvR3j6iAn9Kk
review:
  thoroughness: medium
  understanding: high
  rating: positive
alternatives:
  - source: "https://crates.io"
    name: generational-arena
comment: |-
  The idea behind this crate is that one can use small index types and use the
  lifetime tokens to ensure the validity of the indices, so that the actual
  indexing can be unchecked.

  Earlier versions had problems getting the compiler to check the
  lifetimes for all types, including those that did not implement Drop. For
  those, we implemented a Guard type that ensures the arena lifetime is
  actually observed.

  Basically the lifetime tokens bind the indices returned by *Arena::add to the
  arena. Neither through threads nor recursion can one mix up arenas, and as
  Arenas don't allow deleting items, the index lifetime can only live as long
  as the arena it refers to.

  I have done my due diligence and filed for a CVE as well as yanking all
  previous unsound versions.

Hopefully, that’ll set the record straight – this crate is safe to use! 😇

If you want to opt to trust my reviews, you can do so by typing cargo crev trust --level high https://github.com/llogiq/crev-proofs into your terminal. Perhaps I’ll get around to reviewing more crates in the future.

That’s all, folks!

Whether you want to learn about your dependency tree, look for unsafe code, reduce your build times (and implied attack surface), or help improve the state of code review for us all, you now have the tools at your disposal to do so. Go forth and stay safe! 👋

LogRocket: Full visibility into production Rust apps

Debugging Rust applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Rust app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.

Modernize how you debug your Rust apps — .

Andre Bogus Andre "llogiq" Bogus is a Rust contributor and Clippy maintainer. A musician-turned-programmer, he has worked in many fields, from voice acting and teaching, to programming and managing software projects. He enjoys learning new things and telling others about them.

One Reply to “Comparing Rust supply chain safety tools”

  1. The link “a popular npm library” appears to be invalid. It links to an article about testing, not about Javascript supply of chain attack.

Leave a Reply