RFC: Improve signature verification
Add a new CLI command audit signatures that verifies the npm signatures in a packages packument. It works on the current install.
Signatures are only useful if people verify them. Signature verification is currently a manual and complex process, involving the Keybase CLI to fetch the npm public key.
Our long-term goal for supply chain security is that all software is signed and verified in a transparent and user-friendly way.
Signing and verifying published packages protects users against a malicious mirror or proxy intercepting and tamptering with the package response (MITM attack). Mirrors and proxyies are common in the npm ecosystem, significantly increasing the possible attack surface.
References
Related to https://github.com/npm/rfcs/pull/76
What signatures in a packument? If i can use npm to verify them, how can i use npm to generate them?
Separately, I’d love some motivating examples of actual npm incidents where package signing would have prevented or mitigated the incident, to ensure that this doesn’t end up being security theater.
What signatures in a packument? If i can use npm to verify them, how can i use npm to generate them?
@ljharb 👋 The current dist.npm-signature field that's already in the packument is generated on npm's servers when the package is published using npm's private PGP key. This proposal suggests updating to use a new/more modern key, and bake in validation in the cli. Third-party registries could go also generate signatures on publish and populate the packument in the same way.
Supporting user-generated build time signatures is also something we're actively investigating but not as part of this work.
I’m a bit confused. You’re saying that all public npm registry packages have this signature automatically?
I’m a bit confused. You’re saying that all public npm registry packages have this signature automatically?
Yep. So when you publish a package to npm, a signature is created over the package_name@version:integrity string using a private PGP key, the public key is on keybase today, this gets added to the packument. Here are the docs on verifying using keybase.
This flow is pretty clunky and slow as you need to run this one-by-one over each package you want to verify.
There are two things proposed in this RFC:
-
- Update the signing key, move away from PGP and create light weight ECDSA signatures that can be validated native using node's
cryptolibrary
- Update the signing key, move away from PGP and create light weight ECDSA signatures that can be validated native using node's
-
- Provide a streamlined way to verify these signatures using a new cli command
Separately, I’d love some motivating examples of actual npm incidents where package signing would have prevented or mitigated the incident, to ensure that this doesn’t end up being security theater.
This is a great question and will see if I can dig out some examples.
Why then would this require an opt-in, instead of just running as part of the install?
Separately, I’d love some motivating examples of actual npm incidents where package signing would have prevented or mitigated the incident, to ensure that this doesn’t end up being security theater.
Three are no directly related attacks on npm that I'm aware of but I've found some similar incidents where users downloaded malicious content from a download server/mirror that would have been mitigated by signing and verifying:
Arguably, this proposal is only fixing what's already there (registry signatures), by making signature validation easy, it's not brining something fundamentally new to the table.
There is a whole other host of attack vectors that are not covered with this change that I'd love us to tackle in future. Particularly the ones outlined in TUF Attacks and Weaknesses.
Why then would this require an opt-in, instead of just running as part of the install?
Yes good point. I think doing validation on install is something we should aim for but adding it as a separate command to begin with will allow us to experiment with the user experience a bit more freely, compared to npm install. Once we're happy with the overall ergonomics we could fold it into install.
There are a few prickly edge cases with validation that we'd need to carefully consider before adding it to install:
- While we can enforce validation on all packages fetched from npmjs.org as they will have signatures set, we can't do the same for packages fetched from third-party registries until they add support. Users either need to manually trust keys or be warned some packages are unsigned (these warnings might be unwanted if a user didn't explicitly ask to verify).
- AFAIK there's no way to reliably differentiate between a npmjs.org mirror and a third-party registry where users host their own packages, so we might need to resort to showing a warning when verifying installed packages that have "missing" signatures (the mirror could try circumventing validation by just omitting the
signaturesarray from the served packument. - ~~While we can use node's
cryptolibrary to do validation, it still uses OpenSSL installed on the machine, so there's a chance this versions is way out of date and doesn't support the elliptic curves we need (prime256v1in OpenSSL). We can collect feedback from users running this validation command, and error with a useful message if OpenSSL needs updating.~~ Update: node bundles openssl so this isn't a concern.
Re: OpenSSL
nodejs vendors all dependencies including OpenSSL. So while it is possible to use a system version, that is an edge case and not the norm for systems running node.
An extra command for this seems harmless on the surface, but it really seems like it adds minimal value - since it’s confined to public registry packages, pending support in third-party registries), and unless it can actually mitigate a MitM attack, which seems only plausible if someone has hijacked your DNS and there’s no cert pinning/HSTS involved.
I’m assuming here that it would literally never fail for any package on the public registry that made it down to the user unmodified?
@ljharb I think it makes sense to reframe this as "cleaning up existing debt" rather than thinking of anything as net new.
- We have signatures already, this improved them
- We have a documented way to verify them, this drops the requirement for 3rd party tools
as @feelepxyz pointed out above this is not by any means the "end" to the improvements or work we want to do around signatures... but the alternative imho is removing signatures altogether, not avoiding this work. Is that what you are advocating for?
@MylesBorins i'm not advocating for anything just yet; i'm trying to understand the use case and implications.
It seems like verifying the signatures automatically on install - as opposed to providing an explicit command for it - and ignoring any packages that lack signatures, would be a no-harm improvement; and then perhaps those who want to fail on packages that lack signatures could use a config option to do so?
By shipping a separate command, it is implied that using it increases security, ie, that it protects against an actual attack. I haven't yet heard any specific argument that it does so, and I think security theater is worse than no security at all (ie, i think removing the signatures would be a more secure outcome than providing this as a separate command if verifying the signatures didn't actually prevent anything).
By shipping a separate command, it is implied that using it increases security, ie, that it protects against an actual attack. I haven't yet heard any specific argument that it does so
Hi @ljharb!
The cli will ship with a copy of the public key, so by default an attacker could no longer MITM packages. @feelepxyz talks about this in the Motivation section 🙂.
--
You've mentioned concerns around security theatre, and I think that's appropriate! I think it's correct that this work will not, by itself, add a lot of trust to the registry.
This work will basically only protect you against from someone running a fake mirror that injects its own code into the packages you download.
But it does allow us to start adding new and better trust guarantees over time!
@phillmv thanks for the context. so you're saying that this helps the case where someone MitMs the public registry (which, ftr, would be a drastically reduced attack surface if the npm cli used HSTS/cert pinning), and also tries to modify the contents of public packages?
If so, then I agree it's valuable, and provides a real (if minor) security benefit. However, why require a separate command? Why not just do this by default as part of npm install/npm ci? Who wouldn't want this? (assuming it doesn't fail on packages with missing keys, which could be an additional config to enable)
Why not just do this by default as part of npm install/npm ci
@ljharb I think this is a step towards that, but the amount of scrutiny that npm install gets for performance + stability I think it would be pre-mature to introduce this functionality into the critical path so quickly.
We could introduce a flag for install and setting to support this functionality 🤔 , but with how npm works that would sadly just fail silently on older version that don't support the flag 🙃
Gotcha. ok so, the proposal is for a separate command, that could presumably be configured to fail, or ignore, packages that lack a signature (failing on it would probably break everyone that has an internal registry), with an eventual eye for including it in npm install?
If so, thanks for bearing with me, and that sounds great. I'd suggest npm audit signatures or something, to pair with npm audit licenses :-)
where someone MitMs the public registry (which, ftr, would be a drastically reduced attack surface if the npm cli used HSTS/cert pinning), and also tries to modify the contents of public packages?
Disappointingly, this won't protect you against the public registry being MITM'd (yet) as we'd loose guarantees that we're getting the right public keys. It protects you against the case where you are pulling packages from a npm proxy/mirror of the public registry that have been tampered with. Some companies run a mirror that they install packages from. Verifying signatures would verify that the package you got was the same one that was uploaded to npm.
This is arguably an incredibly niche attack vector, but we're planning a bunch of follow up work that will be transparent to users. One particular improvement is actually protecting against the public registry being MITD'd or worse, compromised entirely.
Yarn is an another example where they run their own proxy in front of the npm registry. So if Yarn CLI started verifying npm registry signatures they would give users some more guarantees that the package installed is the same one as the one published to the npm registry.
Gotcha. ok so, the proposal is for a separate command, that could presumably be configured to fail, or ignore, packages that lack a signature (failing on it would probably break everyone that has an internal registry), with an eventual eye for including it in
npm install?
Yes exactly 💯 I believe starting with a separate command will give us some wiggle room to figure out what configuration is needed and what a good default will be for most users, that could then be included in install by default.
I'd suggest
npm audit signaturesor something, to pair withnpm audit licenses:-)
Thanks for pointing this out! I had no idea about npm audit licenses, will investigate this.
see #182 for that one
@darcyclarke is this scheduled for the 6th of April? I won't be able to make it that day, could we move it to the week after, 13th of April?
In general, I do like this. The one caveat I do have is that I've often seen users want author signing, as opposed to registry signing. My instinct here is that most people will assume this is, in fact, verifying author signing. I will admit that upon reading the comments before digging into the contents, this was absolutely what I thought this proposal was going for (and I was very confused why @ljharb wasn't sure of an example where author signing would have prevented an attack).
It would be extremely nice if whatever name we land on disambiguates that this is, in fact, signing happening by the npm registry and not authors.
Alternatively, adding author signing and then having npm audit signatures do both registry and author signing would be awesome. I'd love to see the https://github.com/bnb.keys system mirrored into npm and have https://www.npmjs.com/~bnb.keys work :)
@darcyclarke is this scheduled for the 6th of April? I won't be able to make it that day, could we move it to the week after, 13th of April?
The meetings are weekly, so we can discuss it in the April 13th meeting :)
Alternatively, adding author signing and then having npm audit signatures do both registry and author signing would be awesome. I'd love to see the https://github.com/bnb.keys system mirrored into npm and have https://www.npmjs.com/~bnb.keys work :)
I'd be curious to know how the community would react to a situation in which GitHub keys are used for signing and validation. @MylesBorins is this something that has been discussed previously? This would seem to beg questions of how tightly we want to bind identities between npm and GitHub. But yes, it's nice that PKI is effectively solved w/ SSH-based signatures on GitHub. I'd personally be (very) in favor of this scenario, but I don't know the community implications.
It would be extremely nice if whatever name we land on disambiguates that this is, in fact, signing happening by the npm registry and not authors.
Yes I was initially thinking we should call this command verify-registry-signature. This is more explicit but also limits the scope of what this command can do in future.
Alternatively, adding author signing and then having
npm audit signaturesdo both registry and author signing would be awesome.
Agreed! This is in line with what we've been thinking about, hence the more generic command name. Once we release some form of author/build signing this command would also verify these signatures 👌
Author or build signing would follow much later as it's a big piece of work that requires a lot of design that's still up in the air (we're keen to do better than author PGP signatures and looking at sigstore/cosign).
The current plan is to go with a generic command like npm audit signatures or npm verify-signatures that only supported registry signatures to begin with, with the plan to also support author/build signatures in future.
@bnb do you have any objections to the above plan if we also make it more obvious what type of signatures are being verified in the command output?
@bnb do you have any objections to the above plan if we also make it more obvious what type of signatures are being verified in the command output?
If this is the intent, I'm chill with it (with the obvious caveat that it's not a commitment). I think somehow communicating in CLI output that the current implementation is verifying registry-generated signatures once you've run the command, that'd be sufficent.
so, like:
npm audit signatures
> verifying registry-generated package signatures 1/1839
and later:
npm audit signatures
> verifying registry-generated package signatures: 1/1839
> verifying author-generated package signatures: 32/992
(Obviously not final UI/UX, but hopefully the point is clear!)
this is absolutely outside of the scope of this RFC, but wanted to share because I thought it might be nifty:
I've also been a fan of third-party hooks into the CLI and making it a platform for some time now (nobody else has ever been, though), but it'd be cool if I could install a dependency (npmDependencies lol) from GitHub or Snyk or WhiteSource that also let them hook into the above and do their own signature checking:
npm audit signatures
> verifying registry-generated package signatures: 1/1839
> verifying author-generated package signatures: 32/992
> verifying GitHub Security package signatures: 23/1728
> verifying WhiteSource package signatures: 77/1431
I think somehow communicating in CLI output that the current implementation is verifying registry-generated signatures once you've run the command, that'd be sufficent.
Yeah exactly 👍 I think we can make it clear in the output once you run it. Will try and incorporate this in the examples.
I've also been a fan of third-party hooks into the CLI and making it a platform for some time now (nobody else has ever been, though), but it'd be cool if I could install a dependency (
npmDependencieslol) from GitHub or Snyk or WhiteSource that also let them hook into the above and do their own signature checking:
Interesting! I had thought about supporting third-party npm registries, e.g. GitHub Packages, Artifactory, Verdaccio etc but this seems slightly different. Do you mean the dependency can define a hook/script that is run by the CLI? Do you have an example of how something like this looks?
Interesting! I had thought about supporting third-party npm registries, e.g. GitHub Packages, Artifactory, Verdaccio etc but this seems slightly different. Do you mean the dependency can define a hook/script that is run by the CLI? Do you have an example of how something like this looks?
Yeah, you basically nailed it. Given all this is about packages, leveraging that same package ecosystem to extend functionality helps make the CLI more of a platform. I think the closest example would be along the lines of VS Marketplace and VS Code Extensions, except instead of an Electron app it's just a CLI. There are obvious bounds that are needed (an example in VS Code is that certain rendering bits can't be messed with) but providing that sandbox allows for an ecosystem to grow and hook in the workflows you're already using with their products directly into the CLI.
@feelepxyz we'll keep the item on the agenda for next week for sure. Appreciate you stewarding this.
Feedback from the Open RFC Meeting:
- We should aim to enable signature verification by default on
installbut seems sensible to launch it as a separate command to test it in the wild first, and do a fast follow to enable by default oninstall - We should probably pick defaults on
installthat ignore missing signatures and just fail on signatures that explicitly fail verification against a public key - We could give users options to turn on more strict checks for missing sigs etc when running
install - Investigate using
host/.well-known/npm-keysURL prefix for the public key
In order to enable by default on install, perhaps we should first add a config flag that controls whether it does run on install? that way, the semver-major change could be flipping the default of that config.