nodejs.org icon indicating copy to clipboard operation
nodejs.org copied to clipboard

Let's document how to verify a Node.js downloads on the website

Open aduh95 opened this issue 7 months ago • 12 comments

As discussed in https://github.com/nodejs/node/issues/58904#issuecomment-3031456396, the way we document how to verify Node.js downloads is not ideal, and there seems to be consensus for switching our recommendation from the public OpenPGP.org server to our own nodejs/release-keys repository. On top of changes in the nodejs/node README, we should also host on the website what is the trusted way to verify a Node.js download.

What we need to provide on the website (presumably on the Downloads page) would be:

  • a git commit hash to a revision of nodejs/release-keys that contain keys to all.
  • a SHA-256 of the gpg-only-active-keys/pubring.kbx on that revision.

Opening this now in case it involves design changes, but it shouldn't land until after the nodejs/node README is edited (currently it still points to keys.openpgp.org as the recommended source).

aduh95 avatar Jul 03 '25 13:07 aduh95

Adding the commit hash would make the most sense to ensure that it's added to https://nodejs.org/dist/index.json.

My existing automation looks a bit like this for finding the right files for the version I need. Extending this to also fetch the commit hash of what keys to take would make sense, and then from there I can make SHA256's of that which I store in an array to act as TOFU pins for the keys.

        # Check for the latest NodeJS 22 version
        LATEST_NODE=$(curl --tlsv1.3 -s https://nodejs.org/dist/index.json | jq -re '.[] | select(.version | startswith("v22")) | .version' | head -n 1)

        # make and cd dir in tmp
        mkdir /tmp/node-install/
        cd /tmp/node-install/

        # Get SHA256s, signature and binaries for the desired version
        curl -O --tlsv1.3 -s --output /tmp/node-install/SHASUMS256.txt https://nodejs.org/dist/$LATEST_NODE/SHASUMS256.txt
        curl -O --tlsv1.3 -s --output /tmp/node-install/SHASUMS256.txt.sig https://nodejs.org/dist/$LATEST_NODE/SHASUMS256.txt.sig
        curl -O --tlsv1.3 -s --output /tmp/node-install/node-$LATEST_NODE-linux-x64.tar.xz https://nodejs.org/dist/$LATEST_NODE/node-$LATEST_NODE-linux-x64.tar.xz

        # Check SHA256 of the binaries tar, this is done BEFORE gpg checks because it is far less attack surface than GPG
        grep node-v*-linux-x64.tar.xz SHASUMS256.txt | sha256sum -c -
        if [ $? -ne 0 ]; then
          echo "File does not match pinned SHA256 hash. Potential supply chain attack detected on NodeJS CDN."
          exit 1
        fi

the-gabe avatar Jul 03 '25 14:07 the-gabe

Let me try to rephrase to make sure I understand the proposal. You'd like to see an object such as:

{
    "version": "v24.3.0",
    "date": "2025-06-24",
    "nodejs/release-keys": {
        "commit": "3ead0a2d07b13e469fd97ef39facf6d31993fb71",
        "sha256": "4f8664db3ba7311589efe3697881155f8ba4bb5d36fe84bdfa7e77359408b1bf"
    },
    …
}

Which you could use in your script such as:

set -ex
set -o pipefail

LATEST_NODE=$(curl -s https://nodejs.org/dist/index.json | jq -re 'first(select(.lts))')

PUBRING=$(mktemp)
curl -fsLo "$PUBRING" "https://github.com/nodejs/release-keys/raw/$(echo "$LATEST_NODE" | jq -re '.["nodejs/release-keys"].commit')/gpg-only-active-keys/pubring.kbx"
shasum -a 256 "$PUBRING" | grep -q "$(echo "$LATEST_NODE" | jq -re '.["nodejs/release-keys"].sha256')"

VERSION=$(echo "$LATEST_NODE" | jq -re '.version')
TAR="node-$VERSION-linux-x64.tar.xz"
curl -fso "$TAR" "https://nodejs.org/dist/$VERSION/$TAR"

curl -fs "https://nodejs.org/dist/${VERSION}/SHASUMS256.txt.asc" \
| gpgv --keyring="${PUBRING}" --output - \
| grep -E "$TAR" \
| shasum -c -

rm "$PUBRING"

I wonder if an issue with this approach is that it'd encourage folks to check keys dynamically, but their setup would be much more robust if they have hard-coded the hash 🤔

aduh95 avatar Jul 03 '25 15:07 aduh95

Yeah this seems pretty much what I am on about. The hash of the keyring would be subject to change though within the constraint of a major release as maintainers come/go and keys are rotated, so for that reason I would fetch it dynamically personally and rely on a hardcoded SHA256 of the keyring in my script and use that as a TOFU pin of the keyring. The downside is that it's a potential annoying thing to deal with, but it's worth the extra assurance IMHO.

<------------------------------ my own comments on the signing process as-is ------------------------------>

Ideally this signing process would be refactored to be much more robust, and a build comparison/attestation + shamir secret sharing approach would take place. This way you could have a dedicated signing key for each major release, and in order to sign anything, at least X out of Y maintainers have to approve a signature of a particular SHA256 derived from either the binaries produced or from the source tree as a whole for git tags.

This way maintainers would have the copy of the source tree locally to sign and then also build the binaries locally in order to reproduce what the build system produces, in order to both distrust the build system and other maintainers, eliminating a single maintainer going rogue potentially and doing evil signatures, or a compromised build machine anywhere. It also has the benefit of having a very clear decentralised system for making releases without leaving private keys on a build server somewhere and waiting for it to be stolen. You would have to pwn and/or place multiple maintainers under duress instead of just one to do some damage in this scenario.

the-gabe avatar Jul 03 '25 15:07 the-gabe

Sharing a secret does not decrease the risk of it leaking, quite the contrary – and in the hypothesis that there is a rogue releaser, having a shared secret would actually help them cover their traces. The plan is not really realistic anyway as we're building on platforms for which we have close to 0 contributors let alone releasers (e.g. Windows).

We're getting far off-track, this issue is meant to discuss about changes to the download page. Changes to the index.json file should be directed at https://github.com/nodejs/nodejs-dist-indexer, and changes to the release process to https://github.com/nodejs/Release. We can continue the discussion there if you want to champion a proposal, I'll be hiding our comments since they are off topic.

aduh95 avatar Jul 03 '25 16:07 aduh95

So something as critical as this to the entire platform security chain, you are just going to mark as off topic and not even open a new thread. Feel free to mark this off topic too, but just keep in mind if you don't follow these comments up you really should document the current shortcomings that put all of us nodejs users at risk. Ouch !!!

NoWayJA avatar Jul 03 '25 16:07 NoWayJA

@aduh95

there seems to be consensus for switching our recommendation from the public OpenPGP.org server to our own nodejs/release-keys repository

Could you please point to where you have gained consensus for this? I would expect to see an approved PR on https://github.com/nodejs/node/pulls with changes to https://github.com/nodejs/node/blob/main/README.md#verifying-binaries before claiming consensus. Perhaps I have overlooked something here?

https://github.com/nodejs/docker-node is currently and successfully using hkps://keys.openpgp.org with a fallback to keyserver.ubuntu.com to verify and install Node.js. See for instance https://github.com/nodejs/docker-node/blob/main/Dockerfile-debian.template

  # use pre-existing gpg directory, see https://github.com/nodejs/docker-node/pull/1895#issuecomment-1550389150
  && export GNUPGHOME="$(mktemp -d)" \
  # gpg keys listed at https://github.com/nodejs/node#release-keys
  && set -ex \
  && for key in \
    "${NODE_KEYS[@]}"
  ; do \
      { gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys "$key" && gpg --batch --fingerprint "$key"; } || \
      { gpg --batch --keyserver keyserver.ubuntu.com --recv-keys "$key" && gpg --batch --fingerprint "$key"; } ; \
  done \
  && curl -fsSLO --compressed "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-$ARCH.tar.xz" \
  && curl -fsSLO --compressed "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \
  && gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \
  && gpgconf --kill all \
  && rm -rf "$GNUPGHOME" \
  && grep " node-v$NODE_VERSION-linux-$ARCH.tar.xz\$" SHASUMS256.txt | sha256sum -c - \
  && tar -xJf "node-v$NODE_VERSION-linux-$ARCH.tar.xz" -C /usr/local --strip-components=1 --no-same-owner \
  && rm "node-v$NODE_VERSION-linux-$ARCH.tar.xz" SHASUMS256.txt.asc SHASUMS256.txt \
  && ln -s /usr/local/bin/node /usr/local/bin/nodejs \

If there is some reason why keyserver.ubuntu.com can no longer be used as a fallback, then perhaps you could explain?

I wonder also how Google manages to have one key for everything (see https://www.google.de/linuxrepositories/) whereas Node.js needs individual keys with a high maintenance overhead impacting downstream Docker building repos like https://github.com/nodejs/docker-node and https://github.com/cypress-io/cypress-docker-images with a dependence on individual Node.js releasers.

MikeMcC399 avatar Jul 09 '25 09:07 MikeMcC399

If there is some reason why keyserver.ubuntu.com can no longer be used as a fallback, then perhaps you could explain?

Recommending using nodejs/release-keys is not at all the same thing as recommending against using keyserver.ubuntu.com as a fallback.

I would expect to see an approved PR […] before claiming consensus

Well do you have any concerns with the suggested recommendation?

aduh95 avatar Jul 09 '25 09:07 aduh95

I've marked my previous post as off-topic, as it concerns details of how to verify Node.js downloads. Instead I note that the https://nodejs.org/en/download page already links to the https://github.com/nodejs/node#verifying-binaries section and to avoid repeating information, I suggest to leave it that way without making changes to the website

Image

MikeMcC399 avatar Jul 09 '25 10:07 MikeMcC399

the https://nodejs.org/en/download page already links to the https://github.com/nodejs/node#verifying-binaries section and to avoid repeating information, I suggest to leave it that way without making changes to the website

The idea behind having the information on the website is that if, for whatever reason, you cannot (or don't want to) access and/or trust github.com, having the information on the website provides an alternative. Repeating the information does indeed come with downsides (more maintenance burden to keep it up-to-date), but also with upsides (mainly the info is more broadly available). There's a tradeoff to be made, and maybe the current link is the "right" tradeoff, or maybe not, I don't claim consensus on that point to be clear.

aduh95 avatar Jul 09 '25 12:07 aduh95

Antoine can we just fetch (on SSR) GH raw and display it with our style ?

AugustinMauroy avatar Jul 13 '25 08:07 AugustinMauroy

@aduh95

There's a tradeoff to be made, and maybe the current link is the "right" tradeoff, or maybe not, I don't claim consensus on that point to be clear.

I would tend to keep it simple and retain just the current link https://github.com/nodejs/node#verifying-binaries where the content has just been updated. The keyrings are located on GitHub (https://github.com/nodejs/release-keys), so there is a reliance there in any case.

MikeMcC399 avatar Jul 28 '25 07:07 MikeMcC399

We are discussing it again in https://github.com/nodejs/node/pull/60490 and at least @aduh95 and I agree that this should not be in the Node.js README because it clutters the README with too many details that should be in a dedicated page. I suggest that we just move that information to the website and let the README link the website instead.

joyeecheung avatar Nov 02 '25 13:11 joyeecheung