Pub: Per-package sha256 checksum field in pubspec.lock

Created on 26 Apr 2020  路  21Comments  路  Source: dart-lang/pub

I would like to be able to verify the integrity of individually downloaded packages by comparing sha256 checksums stored in pubspec.lock.

My motivation for doing so is to improve the experience of using pub2nix, my solution for building pub packages with nix.

I鈥檓 currently working around this by parsing the lock file and generating nix expressions containing the hash ahead of time. This is necessary because nix doesn鈥檛 allow network access at runtime unless a hash of the expected result is provided.

If the entry for each package in pubspec.lock also contained a sha256 field then I could eliminate the manual code generation step altogether and have a seamless experience.

In the meantime I plan to update pub2nix to behave as if this field was present, and update the codegen step to modify the lock file and add the hashes.

enhancement low priority

Most helpful comment

@fkirc, pub doesn't run any postinstall hooks, so you can review code before you use it.

Well, this is already a huge improvement over npm 馃憤

In either, case: pub.dev does not allow mutation of a package version after it has been published, hence, version locking is sufficient, adding a hash to pubspec.yaml would only serve to allow the pub client to verify this behavior independently.

I fully trust the pub.dev-team that they are doing their job properly, however nobody is perfect.
pub.dev could be hacked, whereas sha256-collisions are practically impossible.
In other words, if we have sha256's, then we have an independent way to verify any given lockfile with absolute certainty.

I agree, it's attractive to verify the version lock.

I fully understand that this is not the highest priority right now, but it would be nice to have a general commitment to increase security within the Flutter-ecosystem.
If such a commitment is there, then it is only a matter of time until client-side sha256s are implemented.

All 21 comments

This seems like a good idea.
I cannot give a timeline currently.

Is sha256 a strict requirement for nix? Or does it just have to be some hash?

One feature also supports sha1 and sha512 but sha256 is used extensively throughout nix tooling.

I鈥檓 not aware of any other hashing algorithms that are supported out of the box.

FWIW I鈥檓 close to being done on refactoring pub2nix in the way I mentioned and patching the lock file is working well in the meantime.

@paulyoung, I'm curious, how do you install the dependencies? pub get can't use tarballs. Of course you can extract the tarballs a head of running pub get and do a similar for git dependencies. But afaik pub directory naming conventions inside PUB_CACHE are not obvious (especially for git and http-only sources).

If you only want to support pub.dev you could make a proxy on localhost that downloads, hashes and cache the response requested from pub.dev. Just set the PUB_HOSTED_URL=http://localhost:3000 (or something like that), and make the proxy handle patterns from: https://github.com/dart-lang/pub/blob/master/doc/repository-spec-v2.md

What I did in https://github.com/paulyoung/pub2nix/blob/6726c11766a0883ac7c103594930db5831bce6a2/install.nix has been working for me so far.

Like I said I鈥檝e been doing some refactoring and haven鈥檛 quite gotten to reviewing what happens there again. It鈥檚 been a while since I first wrote it.

Based on #2359 I鈥檝e switched to producing .dart_tool/package_config.json instead of .packages so I anticipate there may be some changes relating to that.

Doing .dart_tool/package_config.json is definitely better than .packages, going forward .dart_tool/package_config.json is going to contain language version and other stuff.
You probably don't have to write it your self (there is a lot corner cases), you can do pub get --offline, as long as the PUB_CACHE has been pre-populated with extracted tarballs this should work.

But converting URL to directory has a few corner cases:
https://github.com/dart-lang/pub/blob/6deb457048deb435009b36a4cd2d86003d107cf4/lib/src/source/hosted.dart#L441-L468

@jonasfj I don't think those approaches will work for me but thanks for the info on producing a directory, that's really helpful!

There is something I've encountered that perhaps you might be able to me understand though;

Once I've created a package_config.json file where the rootUri fields are paths to the nix store, why does pub still look for things in .pub-cache?

For example, I'll get an error like Could not find a file named "pubspec.yaml" in "/private/var/folders/f1/rffgvyz94lqc3kl6wtx18w6w0000gn/T/nix-build-pub2nix_simple_example.drv-0/.pub-cache/hosted/pub.dartlang.org/_fe_analyzer_shared-2.2.0" even though my package_config.json file contains an entry for that package.

My current workaround (similar to the linked install.nix above) is to create a .pub-cache directory in the nix sandbox and populate it but ideally I'd be able to tell pub to not attempt to use the cache and use the paths in package_config.json instead.

What command are you running when you get that error?

The package configuration is a layered approach:

  • pubspec.yaml is the input to pub as constraints to a resolution.
  • pubspec.lock is the file pub uses to store the resolution it made. It can be checked in to version control if you want to be sure everybody uses the same resolution.
    It says what packages it found, where they come from, and in what version.
    It does not have local paths.
  • package_config.json is the file pub writes to tell dart where it put the packages.

Pub has a hard-coded convention for where it stores the packages given the entries of the lockfile. (For a hosted package the rule is here: https://github.com/dart-lang/pub/blob/master/lib/src/source/hosted.dart#L276)

you can do pub get --offline, as long as the PUB_CACHE has been pre-populated with extracted tarballs this should work

@jonasfj Yes, actually this is what I'm doing despite what I implied earlier. However, I'm wondering why PUB_CACHE needs to populated at all when package_config.json contains paths to the packages.

What command are you running when you get that error?

@sigurdm that was when running pub run build_runner build in a sandboxed nix without creating a .pub-cache directory first. I was trying this because I assumed the paths in package_config.json would be used instead of checking the cache.

For those interested; I pushed a first pass of the refactor I mentioned to https://github.com/paulyoung/pub2nix. It includes a working example.

Most of the logic is in https://github.com/paulyoung/pub2nix/blob/156b8878c0d62f1b6fd11790112c26199861797a/build/common.nix

However, I'm wondering why PUB_CACHE needs to populated at all when package_config.json contains paths to the packages.

It shouldn't. PUB_CACHE env variable is only used by the pub command. Check if you have a .packages because some programs may still be using it. We are in the middle of migration to .dart_tool/package_config.json.

Thanks to the information shared in this thread, I have a better understanding of what's happening now and believe I just need to focus on faithfully producing a .pub-cache directory.

If I produce my own .packages file and/or .package_config.json containing paths to packages in the Nix store, when I do pub get --offline I get errors like Because pub2nix_simple_example depends on test any which doesn't exist (could not find package test in cache), version solving failed.

If I add the --verbose flag, I see the following:

FINE: Pub 2.7.2
MSG : Resolving dependencies...
SLVR: fact: pub2nix_simple_example is 0.0.0
SLVR: derived: pub2nix_simple_example
SLVR: fact: pub2nix_simple_example depends on build_runner ^1.3.3
SLVR: fact: pub2nix_simple_example depends on build_test ^0.10.6
SLVR: fact: pub2nix_simple_example depends on test ^1.6.1
SLVR:   selecting pub2nix_simple_example
SLVR:   derived: test ^1.6.1
SLVR:   derived: build_test ^0.10.6
SLVR:   derived: build_runner ^1.3.3
SLVR:   fact: no versions of test match 1.14.2
SLVR:   derived: not test 1.14.2
IO  : Finding versions of test in /homeless-shelter/.pub-cache/hosted/pub.dartlang.org
SLVR:   fact: test doesn't exist (could not find package test in cache)
SLVR:   conflict: test doesn't exist (could not find package test in cache)
SLVR:   ! test any is satisfied by test ^1.6.1
SLVR:   ! which is caused by "pub2nix_simple_example depends on test ^1.6.1"
SLVR:   ! thus: version solving failed
SLVR: Version solving took 0:00:00.136118 seconds.

/homeless-shelter/ is $HOME in a Nix sandbox, which demonstrates that pub get --offline is looking in ~/.pub-cache for packages regardless, and in spite of paths being provided in .packages file and/or .package_config.json. If I create my own .pub-cache directory and export PUB_CACHE=.pub-cache everything works.

Additionally, as @jonasfj mentioned earlier, .package_config.json is overwritten as a result of pub get --offline.

This all aligns with what @sigurdm explained in https://github.com/dart-lang/pub/issues/2462#issuecomment-620426605. I just had the relationship between .pub-cache and package_config.json backwards in my head.

Just a note that we discussed today, so we don't have to rediscover when/if we get around to do this:

  • git dependencies already have the revision-number in the lock-file to ensure integrity.
  • path dependencies are inherently unstable, and it probably doesn't make sense to validate them against a hash from the lock-file

For the Nix use case specifically, and regarding git dependencies, the author of a similar tool for building Rust crates with Nix opened this issue: https://github.com/rust-lang/cargo/issues/7059

Hi there! We're developing a Crypto app with Flutter and came across this thread, we'd love for this feature or some sort of checksum validation for dependencies to increase security.
So thumbs up for this :smile:

@ivaneidel just to clarify, you can get that added security today if you use Nix with my pub2Nix project.

There's an additional step to produce the checksums, and removing that step was the motivation for this issue.

@paulyoung oh! Great, thanks for the clarification, I haven't used Nix, but I'll take a look at it and will check your project!

I just stumbled upon this after trying Flutter for the first time.
In fact, I believe that per-package sha256's are an absolutely critical security feature.
Let me cite @paulyoung :

A sha256 is necessary because nix doesn鈥檛 allow network access at runtime unless a hash of the expected result is provided.

If we look at this closer, then we can see very good security-reasons for this.
Let me give you an example that I often experienced with npm:

I see a random npm-package on GitHub that has not been updated for _12 months_.
The package seems good, but I might need to fork it or add an additional feature.
I do not really trust the author, but a quick code-inspection does not look suspicious.
Therefore, I do the following:

  • Clone the repo
  • Run npm install, which executes arbitrary post-install scripts.
  • Run npm run build/test, which executes arbitrary scripts as well.

Now let's recall that this package has not been changed since 12 months.
Therefore, if we have package-hashes that are _12 months old_, then we get the following security-protections:

12-month-old package-hashes provide almost 100% protection against _all_ security-exploits that have been created by humanity within the last 12 months. This is an extremely powerful protection because it protects against all (unpatched) zero-day-exploits that have been developed within the last 12 months. Moreover, in many cases, "old" package-hashes can make exploits against "newer" system-software impossible, because exploit-developers do not have a magical time-machine during exploit-development.
In other words, even if one of those packages uses a _12-month-old exploit_ against me, then there is still a good chance that this 12-month old exploit will fail just because it has not been updated to work against my newer system-software. This protection can remain even if my newer system-software is not specifically patched against this exploit.

Just to be clear, I fully recognize that security-measures are not for free and need a cost-benefit-analysis.
However, for the case of package-hashes, I believe that the cost-benefit-ratio is extremely good in favor of package-hashes.

Additional reasons for package-hashes are traceability, reproducibility and accountability, but those are entirely different topics on its own.

@fkirc, pub doesn't run any postinstall hooks, so you can review code before you use it.

In either, case: pub.dev does not allow mutation of a package version after it has been published, hence, version locking is sufficient, adding a hash to pubspec.yaml would only serve to allow the pub client to verify this behavior independently.

I agree, it's attractive to verify the version lock.

@fkirc, pub doesn't run any postinstall hooks, so you can review code before you use it.

Well, this is already a huge improvement over npm 馃憤

In either, case: pub.dev does not allow mutation of a package version after it has been published, hence, version locking is sufficient, adding a hash to pubspec.yaml would only serve to allow the pub client to verify this behavior independently.

I fully trust the pub.dev-team that they are doing their job properly, however nobody is perfect.
pub.dev could be hacked, whereas sha256-collisions are practically impossible.
In other words, if we have sha256's, then we have an independent way to verify any given lockfile with absolute certainty.

I agree, it's attractive to verify the version lock.

I fully understand that this is not the highest priority right now, but it would be nice to have a general commitment to increase security within the Flutter-ecosystem.
If such a commitment is there, then it is only a matter of time until client-side sha256s are implemented.

I fully understand that this is not the highest priority right now, but it would be nice to have a general commitment to increase security within the Flutter-ecosystem.

Last year we launched _verified publishers_ on pub.dev to make it easier to trust publishers (at-least those with a well-known domain and trusted brand like dart.dev, flutter.dev, microsoft.com, etc). We started to send emails to package owners when a new version is published, increasing the chances that unauthorized package publication doesn't go unnoticed. We added dart pub outdated to help upgrading packages. The list goes on :D

But there is still many more things we could do to improve security, and I'm sure we'll continue to make improvements over time.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

sanjidtt picture sanjidtt  路  36Comments

DartBot picture DartBot  路  22Comments

wh120 picture wh120  路  24Comments

crajygemer picture crajygemer  路  24Comments

kevmoo picture kevmoo  路  53Comments