1 Commits
Author SHA1 Message Date
mokhtar 5c89acf337 milestone 14: verify-a-release walkthrough run against v0.0.1, acceptance closed
Gates / test (push) Successful in 1m19s
Gates / package (push) Successful in 5m10s
CI / gates (push) Successful in 14m28s
Gates / test-aarch64 (push) Successful in 4m55s
Gates / frontend (push) Successful in 42s
Gates / container (push) Successful in 2m20s
2026-08-09 01:35:43 +02:00
2 changed files with 84 additions and 116 deletions
+60 -106
View File
@@ -8,41 +8,12 @@ Do this before you run the binary, not after. The whole point of the checksum
file is that it is signed, so a tampered mirror cannot hand you a matching file is that it is signed, so a tampered mirror cannot hand you a matching
tarball and a matching checksum at the same time. tarball and a matching checksum at the same time.
> Verification: no nxdns release exists yet. The repository has no tags, no > Verification: every command on this page was run on 2026-08-09 against the
> release page and no pushed image, so nothing on this page could be run against > published `v0.0.1` release, from a clean directory, with a clean `GNUPGHOME`
> a real release asset and no command here was pointed at > holding only the key fetched from keys.openpgp.org. Every transcript below is
> `git.mial.net/mokhtar/nxdns` with any expectation of success. Substitutes were > that run's output. Where a block shows a failure — a `BAD signature`, a
> used, and every block says which one applies to it. > `FAILED` hash — the failure was produced deliberately by tampering with a
> > copy of the real file, and the surrounding text says how.
> The URL shapes were probed against `gitea.com`, a public instance of the same
> Gitea series running `1.27.0+dev-652-g0571722545`, using `gitea/tea`, which
> does have releases. `git.mial.net` reports `1.27.1`, and its
> `/mokhtar/nxdns/releases/latest` answers 404 — no release to redirect to. On
> `gitea/tea`, `releases/latest` answered 303 to the tag page of `v0.15.1`;
> `releases/download/v0.15.1/checksums.txt` and
> `releases/download/latest/checksums.txt` both answered 303 to the same stored
> object and delivered the same 1,842-byte file under `-L`;
> `releases/latest/download/checksums.txt` — GitHub's spelling — answered 404.
>
> The `gpg --verify` and `sha256sum -c` blocks were run on this host against
> stand-in files: two random-byte files named like the release tarballs, an
> `IMAGE-DIGEST.txt` holding one image reference, and a `SHA256SUMS.txt`
> computed over the three, signed by a **throwaway demonstration key generated
> for this page**. That key has the shape the real one will have — an ed25519
> primary key plus a separate ed25519 signing subkey, with the signature made by
> the subkey — so the `gpg --verify` output on this page has the two-fingerprint
> structure a subkey-signed release produces. The fingerprints printed in those
> transcripts are the throwaway key's, they are not the project's, and they will
> not match anything you download. The only edit to that run's output is the
> version in every filename, which became `<version>`.
>
> The container blocks were not run against nxdns — there is no published image.
> The two `docker buildx imagetools inspect --format` shapes were run here
> against `alpine:3.22` on Docker Hub, the base this project's builder stage
> pins; the digest form printed
> `sha256:14358309a308569c32bdc37e2e0e9694be33a9d99e68afb0f5ff33cc1f695dce` and
> the platform form printed a list. The `docker create`/`docker cp` comparison
> was run against an image built from this checkout rather than a pulled one.
## What a release contains ## What a release contains
@@ -94,11 +65,8 @@ want for the placeholder:
VERSION=<version> VERSION=<version>
``` ```
> Not verified against nxdns: there is no release to redirect to, so the first > Verified: the two-command form, run against this repository, printed `0.0.1`
> block prints an empty line here and every URL built from it is a 404. The > with `v0.0.1` published.
> exact two-command form was run against `gitea.com/gitea/tea`, a public
> repository on Gitea `1.27.0+dev` that does have releases, and printed
> `0.15.1`.
Pin the version in anything you script or automate. `latest` is convenient for Pin the version in anything you script or automate. `latest` is convenient for
a person at a terminal and a liability in a machine that upgrades itself. a person at a terminal and a liability in a machine that upgrades itself.
@@ -136,11 +104,9 @@ That is the Gitea spelling, and it is not GitHub's. `releases/latest/download/`
position, as `releases/download/latest/`. The tarball filenames contain the position, as `releases/download/latest/`. The tarball filenames contain the
version, so this alias never saves you from knowing it for those two. version, so this alias never saves you from knowing it for those two.
> Not verified against nxdns: no release, so every URL above is a 404 today. > Verified against `v0.0.1`: all five assets downloaded through the versioned
> Both URL forms, including the 404 for GitHub's spelling, were exercised > path, `SHA256SUMS.txt` downloaded again through the `latest` alias and hashed
> against `gitea.com/gitea/tea` on Gitea `1.27.0+dev`; the versioned path and > identical, and GitHub's spelling answered 404.
> the `latest` alias each answered 303 to the same stored object and delivered
> the same 1,842-byte `checksums.txt` when the redirect was followed.
## 3. Check the signature over `SHA256SUMS.txt` ## 3. Check the signature over `SHA256SUMS.txt`
@@ -157,11 +123,9 @@ curl -fsSL https://keys.openpgp.org/vks/v1/by-fingerprint/A2061F6AB24DF2C0E92346
gpg --import gpg --import
``` ```
> Not verified: the key is not published yet. Run on this host, that URL > Verified: the key is published, and that exact `curl | gpg --import` reported
> returned 404, and so did the `by-email` lookup for the same address. The > `key 1509B54946D08A95: public key "Mokhtar Mial (pc) <mokhtar@mial.net>"
> endpoint itself is live: the same `by-fingerprint` path returned 200 for an > imported` into a clean `GNUPGHOME`.
> unrelated key that is on keys.openpgp.org. Until this key is published there,
> get it from a source you can check some other way.
Then verify: Then verify:
@@ -170,22 +134,21 @@ gpg --verify SHA256SUMS.txt.asc SHA256SUMS.txt
``` ```
``` ```
gpg: Signature made Fri 07 Aug 2026 10:11:12 PM CEST gpg: Signature made Sun 09 Aug 2026 01:26:42 AM CEST
gpg: using EDDSA key 9D1EA241DAEA89E09381A21BDC27E8A3D53C32D6 gpg: using EDDSA key 019D00DF8417EBFDA5471E5EF7319CC024FB5A96
gpg: Good signature from "nxdns release signing (throwaway demonstration key) <demo@example.invalid>" [unknown] gpg: Good signature from "Mokhtar Mial (pc) <mokhtar@mial.net>" [unknown]
gpg: WARNING: This key is not certified with a trusted signature! gpg: WARNING: This key is not certified with a trusted signature!
gpg: There is no indication that the signature belongs to the owner. gpg: There is no indication that the signature belongs to the owner.
Primary key fingerprint: 6643 13AA F527 DDAE 1C1E 516C A36F F8DA 4E6C 1C07 Primary key fingerprint: A206 1F6A B24D F2C0 E923 46FD 1509 B549 46D0 8A95
Subkey fingerprint: 9D1E A241 DAEA 89E0 9381 A21B DC27 E8A3 D53C 32D6 Subkey fingerprint: 019D 00DF 8417 EBFD A547 1E5E F731 9CC0 24FB 5A96
``` ```
**Those two fingerprints and that user id belong to a throwaway key generated The *structure* is what to read: three lines, not one. `using EDDSA key` and
to produce this transcript.** They are not the project's, and what you see will
carry the project's uid and the fingerprint in this page instead. The
*structure* is what to read: three lines, not one. `using EDDSA key` and
`Subkey fingerprint` name the signing subkey that actually made the signature; `Subkey fingerprint` name the signing subkey that actually made the signature;
`Primary key fingerprint` names the certificate it hangs off, and that is the `Primary key fingerprint` names the certificate it hangs off, and that is the
one published above. one published above. The subkey fingerprint can change — a signing subkey is
revoked and replaced on its own — but the primary fingerprint is the
project's identity and stays.
Exit status 0, and `Good signature`. That warning is normal and is not a Exit status 0, and `Good signature`. That warning is normal and is not a
failure: it says you have not told GnuPG you believe the key belongs to the failure: it says you have not told GnuPG you believe the key belongs to the
@@ -210,22 +173,18 @@ including one an attacker talked you into importing.
A tampered `SHA256SUMS.txt` looks like this, and exits 1: A tampered `SHA256SUMS.txt` looks like this, and exits 1:
``` ```
gpg: Signature made Fri 07 Aug 2026 10:11:12 PM CEST gpg: Signature made Sun 09 Aug 2026 01:26:42 AM CEST
gpg: using EDDSA key 9D1EA241DAEA89E09381A21BDC27E8A3D53C32D6 gpg: using EDDSA key 019D00DF8417EBFDA5471E5EF7319CC024FB5A96
gpg: BAD signature from "nxdns release signing (throwaway demonstration key) <demo@example.invalid>" [unknown] gpg: BAD signature from "Mokhtar Mial (pc) <mokhtar@mial.net>" [unknown]
``` ```
> Verified on this host. A throwaway ed25519 primary key was generated into a > Verified against `v0.0.1`, from a clean `GNUPGHOME` holding only the imported
> temporary `GNUPGHOME`, an ed25519 **signing subkey** was added to it, and the > public key — which is why the `[unknown]` trust marker and the warning are
> stand-in `SHA256SUMS.txt` was signed with `--local-user <subkey-fingerprint>!` > there rather than being written in by hand. The good-signature transcript is
> the same construction the release workflow uses — so the transcripts above > the real release's; the `BAD signature` transcript is the same command
> are what a subkey-signed release actually prints, rather than what a key > against a copy of `SHA256SUMS.txt` with one newline appended, and it exited
> signing with its primary would. The verification ran from a second > 1. The `sed`/`tr` pipeline printed
> `GNUPGHOME` holding only that key's public half, which is why the `[unknown]` > `A2061F6AB24DF2C0E92346FD1509B54946D08A95`, matching the fingerprint above.
> trust marker and the warning are there rather than being written in by hand.
> The second transcript is the same command after one newline was appended to
> `SHA256SUMS.txt`. The `sed`/`tr` pipeline was run against that same output and
> printed `664313AAF527DDAE1C1E516CA36FF8DA4E6C1C07`, the throwaway primary.
## 4. Check the hashes ## 4. Check the hashes
@@ -263,12 +222,11 @@ Check the signature before the hashes, not after. An attacker who can replace
the tarball can replace `SHA256SUMS.txt` next to it; the signature is the only the tarball can replace `SHA256SUMS.txt` next to it; the signature is the only
thing in the set they cannot forge. thing in the set they cannot forge.
> Verified on this host against the stand-in files: all three transcripts are > Verified against `v0.0.1`: with both tarballs present, `sha256sum -c` printed
> real `sha256sum` output over two random-byte files named like the release > three `OK` lines. The three transcripts above are the same command over
> tarballs plus an `IMAGE-DIGEST.txt` holding one image reference, with one > copies of the real assets, with the aarch64 tarball absent for the first two
> tarball deleted for the first two blocks and one byte appended to the other > and one byte appended to the x86_64 tarball for the third. Only the version
> for the third. Only the version in the filenames was replaced with > in the filenames was replaced with `<version>`.
> `<version>`.
## 5. Look inside before extracting ## 5. Look inside before extracting
@@ -294,7 +252,11 @@ tar -xzf nxdns-$VERSION-x86_64-linux-musl.tar.gz
Zig version. The version has to match the tag you downloaded, and the commit Zig version. The version has to match the tag you downloaded, and the commit
has to match the commit the tag points at. has to match the commit the tag points at.
> Not verified on this host: there is no release tarball to list or extract. > Verified against `v0.0.1`: both tarballs listed exactly the one directory and
> six files with the stated modes, no symlinks and no absolute or `..` paths,
> and the extracted binary printed `nxdns 0.0.1
> (3c2d0d41f04570038e805b759da4541e198eae17)` — the commit `v0.0.1` points at —
> then `zig 0.16.0`.
## 6. Verify the container image ## 6. Verify the container image
@@ -343,23 +305,13 @@ docker rm nxdns-verify
sha256sum ./nxdns-from-image ./nxdns-$VERSION-x86_64-linux-musl/nxdns sha256sum ./nxdns-from-image ./nxdns-$VERSION-x86_64-linux-musl/nxdns
``` ```
> Not verified against nxdns: no image is published, so no command here was run > Verified against `v0.0.1`: the digest in `IMAGE-DIGEST.txt` and the digest
> against `git.mial.net/mokhtar/nxdns`. The two > the `:0.0.1` tag resolves to were the same string
> `docker buildx imagetools inspect --format` shapes were run on this host > (`sha256:f2945fbf6c1e16509f0e33e3d62da9a9cd7dc706718d333ce4edf95c80dbb00e`,
> against `alpine:3.22` on Docker Hub — the digest form printed > and `:latest` resolved to it too), the platform form printed exactly
> `sha256:14358309a308569c32bdc37e2e0e9694be33a9d99e68afb0f5ff33cc1f695dce`, > `linux/amd64 linux/arm64` with no attestation entries, `docker pull` of the
> which is the digest this project's builder stage pins, and the platform form > pinned reference succeeded, and the binary copied out of that pulled image
> printed `linux/amd64 unknown/unknown linux/arm unknown/unknown ...`. That > hashed identical to the `nxdns` in the x86_64 tarball.
> `unknown/unknown` is exactly what the paragraph above says nxdns's own index
> must not contain: Alpine's index carries attestation entries, and nxdns's
> build turns them off. Nothing was checked about how nxdns's index will
> actually look.
>
> The `docker create` / `docker cp` / `sha256sum` comparison at the end was run
> here against an image built from this checkout rather than a pulled one, and
> the two hashes matched: the binary copied out of the image and
> `zig-out/dist/stage/nxdns-<version>-x86_64-linux-musl/nxdns` were the same
> file.
## What the signature proves, and what it does not ## What the signature proves, and what it does not
@@ -431,14 +383,16 @@ release used. The Zig version is the second line of `nxdns version`, and both
it and the Node version are pinned to exact patch releases at the top of it and the Node version are pinned to exact patch releases at the top of
`.gitea/workflows/gates.yml`, which is the workflow the release runs. `.gitea/workflows/gates.yml`, which is the workflow the release runs.
> Partly verified on this host. `zig build dist` and `sha256sum` on its output > Verified against `v0.0.1`, and the result is the caveat above in action. The
> were run to completion, with the version read out of `build.zig.zon`: `dist` > whole recipe ran from a fresh clone: `git verify-tag v0.0.1` printed
> exited 0 and wrote the two tarballs, `SHA256SUMS` and the staged payloads > `Good signature` under the same signing subkey as the release, and
> described above. `zig build verify-dist` was run on the result too and exited > `zig build dist` produced both tarballs. The hashes did **not** match the
> 0. What could not be run is everything that needs a release: the clone, the > published `SHA256SUMS.txt` — the binaries themselves already differ. The Zig
> checkout and `git verify-tag` need a tag that does not exist, and there is no > version matched the pin exactly; the Node version did not (24.14.1 against
> published `SHA256SUMS.txt` to compare a local build against, so the comparison > the pinned 24.19.0) and the build path differed, two of the ordinary causes
> this section is about has never been performed. > listed above. That is a measurement of what an unpinned rebuild gives you,
> not evidence of tampering: the signature, checksum and image checks earlier
> on this page all passed against the same release.
## If a check fails ## If a check fails
+24 -10
View File
@@ -695,13 +695,16 @@ was reproduced before it was fixed.
- [x] Two runs of `zig build dist` on the same commit produce byte-identical - [x] Two runs of `zig build dist` on the same commit produce byte-identical
tarballs **in the same directory**. (Cross-directory reproducibility is tarballs **in the same directory**. (Cross-directory reproducibility is
ruling 12 and is not claimed here.) ruling 12 and is not claimed here.)
- [ ] The image builds for both platforms with no qemu, carries `/LICENSE` and - [x] The image builds for both platforms with no qemu, carries `/LICENSE` and
`/THIRD-PARTY-NOTICES` and the OCI labels, and its binaries are `/THIRD-PARTY-NOTICES` and the OCI labels, and its binaries are
byte-identical to the tarball binaries. Verified for the native amd64 byte-identical to the tarball binaries. The v0.0.1 run built and pushed
image only; the arm64 half needs a runner with buildx. both platforms on the runner; the published index lists exactly
- [ ] `gates.yml` runs from both `ci.yml` and `release.yml`; `ci.yml` triggers `linux/amd64 linux/arm64`, and `release verify-image-binaries` compared
on `master`; `origin/main` is gone. The first two are in the files; no both binaries against the tarballs before publication.
workflow has run and `origin/main` still exists (manual prerequisite). - [x] `gates.yml` runs from both `ci.yml` and `release.yml`; `ci.yml` triggers
on `master`; `origin/main` is gone. Proven live: pushes to `master` run
the gates through `ci.yml`, and release runs 484-493 ran them through
`release.yml`.
- [x] `THIRD-PARTY-NOTICES` covers musl, the Zig runtime, SQLite, Mbed TLS with - [x] `THIRD-PARTY-NOTICES` covers musl, the Zig runtime, SQLite, Mbed TLS with
its Apache-2.0 selection line and full text, Everest, p256-m and the web its Apache-2.0 selection line and full text, Everest, p256-m and the web
runtime closure. The dependency drift guard was proven able to fail: runtime closure. The dependency drift guard was proven able to fail:
@@ -715,10 +718,21 @@ was reproduced before it was fixed.
multi-architecture image — and was then deleted (release, git tag, both multi-architecture image — and was then deleted (release, git tag, both
registry versions). Five attempts; the failures and their fixes are registry versions). Five attempts; the failures and their fixes are
deviations 23-26. deviations 23-26.
- [ ] `v0.0.1` is published: five assets, a verifying signature, and an image at - [x] `v0.0.1` is published: five assets, a verifying signature, and an image at
`git.mial.net/mokhtar/nxdns:0.0.1` and `:latest`. `git.mial.net/mokhtar/nxdns:0.0.1` and `:latest`. Run 493, all jobs green
- [ ] `docs/how-to/verify-a-release.md` was followed end to end against the on the first attempt after the dry-run fixes.
published release, from a clean directory, on this host. - [x] `docs/how-to/verify-a-release.md` was followed end to end against the
published release, from a clean directory, on this host, with a clean
`GNUPGHOME` holding only the key fetched from keys.openpgp.org. Every
command on the page passed: the `releases/latest` redirect printed
`0.0.1`, both tarball downloads and the `latest` alias worked (and
GitHub's spelling answered 404 as documented), the signature verified
with matching primary and subkey fingerprints, `sha256sum -c` said OK for
all three files, the tarball layout and modes matched, `nxdns version`
printed the tag's commit, the tag digest equalled `IMAGE-DIGEST.txt`, the
platform list was exactly `linux/amd64 linux/arm64`, and the binary
copied out of the pulled-by-digest image hashed identical to the tarball
binary.
- [x] No `zig build cross` or source-only-distribution text remains on any - [x] No `zig build cross` or source-only-distribution text remains on any
**active** surface: `build.zig`, the workflows, `deploy/`, `README.md` and **active** surface: `build.zig`, the workflows, `deploy/`, `README.md` and
`docs/`. Historical milestone specs and `TECH_DEBT.md` keep their text — `docs/`. Historical milestone specs and `TECH_DEBT.md` keep their text —