Skip to content

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:

FileWhat it is
nxdns-0.0.25-x86_64-linux-musl.tar.gznxdns for x86_64 Linux
nxdns-0.0.25-aarch64-linux-musl.tar.gznxdns for 64-bit ARM Linux, such as a Raspberry Pi
SHA256SUMS.txtThe SHA-256 of both tarballs and of IMAGE-DIGEST.txt
SHA256SUMS.txt.ascAn OpenPGP signature over SHA256SUMS.txt
IMAGE-DIGEST.txtThe 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

Terminal window
mkdir -p ~/nxdns-release && cd ~/nxdns-release
BASE=https://git.mial.net/mokhtar/nxdns/releases/download/v0.0.25
curl -fLO "$BASE/nxdns-0.0.25-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
Terminal window
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 v0.0.25 in a clone confirms it independently.

3. Check the signature

Terminal window
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:

Terminal window
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

Terminal window
sha256sum -c --ignore-missing SHA256SUMS.txt
nxdns-0.0.25-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

Terminal window
tar -tvzf nxdns-0.0.25-x86_64-linux-musl.tar.gz
drwxr-xr-x 0/0 0 1970-01-01 01:00 nxdns-0.0.25-x86_64-linux-musl
-rw-r--r-- 0/0 4229 1970-01-01 01:00 nxdns-0.0.25-x86_64-linux-musl/INSTALL.md
-rw-r--r-- 0/0 14210 1970-01-01 01:00 nxdns-0.0.25-x86_64-linux-musl/LICENSE
-rw-r--r-- 0/0 72967 1970-01-01 01:00 nxdns-0.0.25-x86_64-linux-musl/THIRD-PARTY-NOTICES
-rwxr-xr-x 0/0 7257952 1970-01-01 01:00 nxdns-0.0.25-x86_64-linux-musl/nxdns
-rw-r--r-- 0/0 31 1970-01-01 01:00 nxdns-0.0.25-x86_64-linux-musl/nxdns.conf
-rw-r--r-- 0/0 1547 1970-01-01 01:00 nxdns-0.0.25-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:

Terminal window
tar -xzf nxdns-0.0.25-x86_64-linux-musl.tar.gz
./nxdns-0.0.25-x86_64-linux-musl/nxdns version

The first line it prints must be nxdns 0.0.25, 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:0.0.25@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:

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

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

Terminal window
cut -d@ -f2 IMAGE-DIGEST.txt
docker buildx imagetools inspect git.mial.net/mokhtar/nxdns:0.0.25 --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:

Terminal window
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-0.0.25-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.

Terminal window
git clone https://git.mial.net/mokhtar/nxdns
cd nxdns
git checkout v0.0.25
git verify-tag v0.0.25
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:

Terminal window
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=0.0.25 \
-Dadmin-dist=admin/dist -Doptimize=ReleaseSafe \
--cache-dir "$(mktemp -d)"'
sha256sum zig-out/dist/nxdns-0.0.25-*.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.