Files
nxdns/docs/how-to/verify-a-release.md
T

7.9 KiB

Verify a release

Check that an nxdns release you downloaded is the one the project published, and that it arrived intact. Do it before you run the binary.

What a release contains

The release page has five files for each version:

File What it is
nxdns-<version>-x86_64-linux-musl.tar.gz nxdns for x86_64 Linux
nxdns-<version>-aarch64-linux-musl.tar.gz nxdns for 64-bit ARM Linux, such as a Raspberry Pi
SHA256SUMS.txt The SHA-256 of both tarballs and of IMAGE-DIGEST.txt
SHA256SUMS.txt.asc An OpenPGP signature over SHA256SUMS.txt
IMAGE-DIGEST.txt The container image of this version, pinned by digest

The signature covers SHA256SUMS.txt, and SHA256SUMS.txt covers everything else. Someone who can replace a tarball on its way to you can replace the checksum file next to it, but cannot sign it.

1. Download

mkdir -p ~/nxdns-release && cd ~/nxdns-release
BASE=https://git.mial.net/mokhtar/nxdns/releases/download/v<version>
curl -fLO "$BASE/nxdns-<version>-x86_64-linux-musl.tar.gz"
curl -fLO "$BASE/SHA256SUMS.txt"
curl -fLO "$BASE/SHA256SUMS.txt.asc"
curl -fLO "$BASE/IMAGE-DIGEST.txt"

Take the aarch64 tarball instead for a Raspberry Pi.

2. Import the signing key

Releases are signed by a subkey of this key:

A2061F6AB24DF2C0E92346FD1509B54946D08A95
curl -fsSL https://keys.openpgp.org/vks/v1/by-fingerprint/A2061F6AB24DF2C0E92346FD1509B54946D08A95 |
  gpg --import

The same key signs every commit and tag in the repository, so git verify-tag v<version> in a clone confirms it independently.

3. Check the signature

gpg --verify SHA256SUMS.txt.asc SHA256SUMS.txt
gpg: Signature made <date and time of the release>
gpg:                using EDDSA key 019D00DF8417EBFDA5471E5EF7319CC024FB5A96
gpg: Good signature from "Mokhtar Mial (pc) <…>" [unknown]
gpg: WARNING: This key is not certified with a trusted signature!
gpg:          There is no indication that the signature belongs to the owner.
Primary key fingerprint: A206 1F6A B24D F2C0 E923  46FD 1509 B549 46D0 8A95
     Subkey fingerprint: 019D 00DF 8417 EBFD A547  1E5E F731 9CC0 24FB 5A96

Two things must hold: the output says Good signature, and the Primary key fingerprint is the fingerprint in step 2. GnuPG prints it in groups of four; this prints it unspaced for comparison:

gpg --verify SHA256SUMS.txt.asc SHA256SUMS.txt 2>&1 |
  sed -n 's/^Primary key fingerprint: //p' | tr -d ' '

Do not skip the fingerprint. gpg --verify accepts a good signature from any key in your keyring. The WARNING lines are normal: they only mean you have not told GnuPG that you trust the key.

4. Check the hashes

sha256sum -c --ignore-missing SHA256SUMS.txt
nxdns-<version>-x86_64-linux-musl.tar.gz: OK
IMAGE-DIGEST.txt: OK

--ignore-missing skips the tarball you did not download. A file that does not match prints FAILED and sha256sum exits 1.

5. Look inside the tarball

tar -tvzf nxdns-<version>-x86_64-linux-musl.tar.gz
drwxr-xr-x 0/0               0 1970-01-01 01:00 nxdns-<version>-x86_64-linux-musl
-rw-r--r-- 0/0            4229 1970-01-01 01:00 nxdns-<version>-x86_64-linux-musl/INSTALL.md
-rw-r--r-- 0/0           14210 1970-01-01 01:00 nxdns-<version>-x86_64-linux-musl/LICENSE
-rw-r--r-- 0/0           72967 1970-01-01 01:00 nxdns-<version>-x86_64-linux-musl/THIRD-PARTY-NOTICES
-rwxr-xr-x 0/0         7257952 1970-01-01 01:00 nxdns-<version>-x86_64-linux-musl/nxdns
-rw-r--r-- 0/0              31 1970-01-01 01:00 nxdns-<version>-x86_64-linux-musl/nxdns.conf
-rw-r--r-- 0/0            1547 1970-01-01 01:00 nxdns-<version>-x86_64-linux-musl/nxdns.service

Expect one directory and these six files, with no links and no path that starts with / or contains ... The sizes differ from release to release; the dates are always the epoch, shown in your time zone. Then:

tar -xzf nxdns-<version>-x86_64-linux-musl.tar.gz
./nxdns-<version>-x86_64-linux-musl/nxdns version

The first line it prints must be nxdns <version>, the version you downloaded.

6. Verify the container image

IMAGE-DIGEST.txt names the image this version published, pinned by digest:

git.mial.net/mokhtar/nxdns:<version>@sha256:<64 hex digits>

The signature you checked covers this line. A tag in a registry can be moved; a digest cannot. Pull the pinned reference:

docker pull "$(cat IMAGE-DIGEST.txt)"

Or check that the tag still points at it, comparing the two lines it prints:

cut -d@ -f2 IMAGE-DIGEST.txt
docker buildx imagetools inspect git.mial.net/mokhtar/nxdns:<version> --format '{{.Manifest.Digest}}'

The image holds exactly linux/amd64 and linux/arm64, and its binary is the same file as the one in the x86_64 tarball:

docker create --name nxdns-verify "$(cat IMAGE-DIGEST.txt)"
docker cp nxdns-verify:/nxdns ./nxdns-from-image
docker rm nxdns-verify
sha256sum ./nxdns-from-image ./nxdns-<version>-x86_64-linux-musl/nxdns

If a check fails

Do not run the binary.

  • sha256sum says FAILED after a good signature: the download is damaged or was replaced. Delete it and download it again, over another network if you can.
  • gpg says BAD signature: SHA256SUMS.txt and its signature do not belong together. Download both again; a signature left over from another version is the usual cause.
  • gpg says No public key: import the key from step 2.
  • The fingerprint does not match step 2: take this one seriously. Do not extract the tarball, and do not import other keys to make it pass.

What the signature does not prove

The signature proves the files came from the project's release pipeline and that you have the bytes it uploaded. It does not prove that the binary was built from the published source: the machine that built it also signed it.

The release builds are reproducible, so you can check that yourself by building the same version from source and comparing hashes. It takes the exact toolchain the release used, named at the top of .gitea/workflows/gates.yml: ZIG_VERSION with its ZIG_TARBALL_SHA256, NODE_VERSION and NPM_VERSION. Use the official Zig tarball from ziglang.org, not a distribution's package of the same version: a distribution build of Zig produces different machine code while printing the same version. The rebuild reproduces the release only on x86_64 Linux with glibc.

git clone https://git.mial.net/mokhtar/nxdns
cd nxdns
git checkout v<version>
git verify-tag v<version>

ZIG_VERSION=$(sed -n 's/^  ZIG_VERSION: "\(.*\)"$/\1/p' .gitea/workflows/gates.yml)
ZIG_TARBALL_SHA256=$(sed -n 's/^  ZIG_TARBALL_SHA256: "\(.*\)"$/\1/p' .gitea/workflows/gates.yml)
curl -fsSL -o ../zig.tar.xz "https://ziglang.org/download/$ZIG_VERSION/zig-x86_64-linux-$ZIG_VERSION.tar.xz"
(cd .. && echo "$ZIG_TARBALL_SHA256  zig.tar.xz" | sha256sum -c - && tar -xJf zig.tar.xz)
ZIG="$(cd .. && pwd)/zig-x86_64-linux-$ZIG_VERSION/zig"

Then build in a clean environment. A locale, a time zone or a umask picked up from your shell changes the output, and the two npm_config_* paths must not exist:

env -i PATH="$PATH" HOME="$HOME" LC_ALL=C LANG=C TZ=UTC SOURCE_DATE_EPOCH=0 CI=true npm_config_userconfig=/nonexistent/npmrc-user npm_config_globalconfig=/nonexistent/npmrc-global \
  sh -c 'cd admin && umask 022 && npm ci && npm run build'

env -i PATH="$PATH" HOME="$HOME" LC_ALL=C LANG=C TZ=UTC SOURCE_DATE_EPOCH=0 CI=true npm_config_userconfig=/nonexistent/npmrc-user npm_config_globalconfig=/nonexistent/npmrc-global \
  sh -c 'umask 022 && exec "'"$ZIG"'" build dist \
    -Dversion-string=<version> \
    -Dadmin-dist=admin/dist -Doptimize=ReleaseSafe \
    --cache-dir "$(mktemp -d)"'

sha256sum zig-out/dist/nxdns-<version>-*.tar.gz

Both hashes must match the tarball lines of the published SHA256SUMS.txt. If they do not, check the toolchain and the environment first: a distribution's Zig, a different Node or npm, or a build outside env -i are the usual causes.