7 Commits
Author SHA1 Message Date
mokhtar 31d8baad15 changelog: 0.0.1 releases today
Release / guard (push) Successful in 1m30s
Release / gates (push) Successful in 14m44s
Gates / test (push) Successful in 1m19s
Gates / test-aarch64 (push) Successful in 4m59s
Gates / frontend (push) Successful in 48s
Gates / package (push) Successful in 5m13s
Gates / container (push) Successful in 2m23s
Release / publish (push) Successful in 53s
2026-08-09 01:01:55 +02:00
mokhtar 3c38e18027 changelog and spec: dry run complete, restore 0.0.1, record deviations 25 and 26 2026-08-08 21:43:36 +02:00
mokhtar 1a82b0491a release: name the image by the public registry host, not the internal server url 2026-08-08 21:14:37 +02:00
mokhtar d7f63b9f6e release: move publication orchestration into tools/release.zig, pin rotated subkey 2026-08-08 20:22:04 +02:00
mokhtar fbd70e6a70 release: refetch the annotated tag object that checkout replaces 2026-08-08 16:03:04 +02:00
mokhtar 95939fedf0 changelog: temporary 0.0.0 section for the release dry run 2026-08-08 15:35:40 +02:00
mokhtar 75038b61a6 release: pin the artifact-signing subkey fingerprint 2026-08-08 14:44:07 +02:00
2 changed files with 116 additions and 84 deletions
+106 -60
View File
@@ -8,12 +8,41 @@ 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: every command on this page was run on 2026-08-09 against the > Verification: no nxdns release exists yet. The repository has no tags, no
> published `v0.0.1` release, from a clean directory, with a clean `GNUPGHOME` > release page and no pushed image, so nothing on this page could be run against
> holding only the key fetched from keys.openpgp.org. Every transcript below is > a real release asset and no command here was pointed at
> that run's output. Where a block shows a failure — a `BAD signature`, a > `git.mial.net/mokhtar/nxdns` with any expectation of success. Substitutes were
> `FAILED` hash — the failure was produced deliberately by tampering with a > used, and every block says which one applies to it.
> 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
@@ -65,8 +94,11 @@ want for the placeholder:
VERSION=<version> VERSION=<version>
``` ```
> Verified: the two-command form, run against this repository, printed `0.0.1` > Not verified against nxdns: there is no release to redirect to, so the first
> with `v0.0.1` published. > block prints an empty line here and every URL built from it is a 404. The
> 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.
@@ -104,9 +136,11 @@ 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.
> Verified against `v0.0.1`: all five assets downloaded through the versioned > Not verified against nxdns: no release, so every URL above is a 404 today.
> path, `SHA256SUMS.txt` downloaded again through the `latest` alias and hashed > Both URL forms, including the 404 for GitHub's spelling, were exercised
> identical, and GitHub's spelling answered 404. > against `gitea.com/gitea/tea` on Gitea `1.27.0+dev`; the versioned path and
> 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`
@@ -123,9 +157,11 @@ curl -fsSL https://keys.openpgp.org/vks/v1/by-fingerprint/A2061F6AB24DF2C0E92346
gpg --import gpg --import
``` ```
> Verified: the key is published, and that exact `curl | gpg --import` reported > Not verified: the key is not published yet. Run on this host, that URL
> `key 1509B54946D08A95: public key "Mokhtar Mial (pc) <mokhtar@mial.net>" > returned 404, and so did the `by-email` lookup for the same address. The
> imported` into a clean `GNUPGHOME`. > endpoint itself is live: the same `by-fingerprint` path returned 200 for an
> 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:
@@ -134,21 +170,22 @@ gpg --verify SHA256SUMS.txt.asc SHA256SUMS.txt
``` ```
``` ```
gpg: Signature made Sun 09 Aug 2026 01:26:42 AM CEST gpg: Signature made Fri 07 Aug 2026 10:11:12 PM CEST
gpg: using EDDSA key 019D00DF8417EBFDA5471E5EF7319CC024FB5A96 gpg: using EDDSA key 9D1EA241DAEA89E09381A21BDC27E8A3D53C32D6
gpg: Good signature from "Mokhtar Mial (pc) <mokhtar@mial.net>" [unknown] gpg: Good signature from "nxdns release signing (throwaway demonstration key) <demo@example.invalid>" [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: A206 1F6A B24D F2C0 E923 46FD 1509 B549 46D0 8A95 Primary key fingerprint: 6643 13AA F527 DDAE 1C1E 516C A36F F8DA 4E6C 1C07
Subkey fingerprint: 019D 00DF 8417 EBFD A547 1E5E F731 9CC0 24FB 5A96 Subkey fingerprint: 9D1E A241 DAEA 89E0 9381 A21B DC27 E8A3 D53C 32D6
``` ```
The *structure* is what to read: three lines, not one. `using EDDSA key` and **Those two fingerprints and that user id belong to a throwaway key generated
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. The subkey fingerprint can change — a signing subkey is one published above.
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
@@ -173,18 +210,22 @@ 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 Sun 09 Aug 2026 01:26:42 AM CEST gpg: Signature made Fri 07 Aug 2026 10:11:12 PM CEST
gpg: using EDDSA key 019D00DF8417EBFDA5471E5EF7319CC024FB5A96 gpg: using EDDSA key 9D1EA241DAEA89E09381A21BDC27E8A3D53C32D6
gpg: BAD signature from "Mokhtar Mial (pc) <mokhtar@mial.net>" [unknown] gpg: BAD signature from "nxdns release signing (throwaway demonstration key) <demo@example.invalid>" [unknown]
``` ```
> Verified against `v0.0.1`, from a clean `GNUPGHOME` holding only the imported > Verified on this host. A throwaway ed25519 primary key was generated into a
> public key — which is why the `[unknown]` trust marker and the warning are > temporary `GNUPGHOME`, an ed25519 **signing subkey** was added to it, and the
> there rather than being written in by hand. The good-signature transcript is > stand-in `SHA256SUMS.txt` was signed with `--local-user <subkey-fingerprint>!`
> the real release's; the `BAD signature` transcript is the same command > the same construction the release workflow uses — so the transcripts above
> against a copy of `SHA256SUMS.txt` with one newline appended, and it exited > are what a subkey-signed release actually prints, rather than what a key
> 1. The `sed`/`tr` pipeline printed > signing with its primary would. The verification ran from a second
> `A2061F6AB24DF2C0E92346FD1509B54946D08A95`, matching the fingerprint above. > `GNUPGHOME` holding only that key's public half, which is why the `[unknown]`
> 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
@@ -222,11 +263,12 @@ 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 against `v0.0.1`: with both tarballs present, `sha256sum -c` printed > Verified on this host against the stand-in files: all three transcripts are
> three `OK` lines. The three transcripts above are the same command over > real `sha256sum` output over two random-byte files named like the release
> copies of the real assets, with the aarch64 tarball absent for the first two > tarballs plus an `IMAGE-DIGEST.txt` holding one image reference, with one
> and one byte appended to the x86_64 tarball for the third. Only the version > tarball deleted for the first two blocks and one byte appended to the other
> in the filenames was replaced with `<version>`. > for the third. Only the version in the filenames was replaced with
> `<version>`.
## 5. Look inside before extracting ## 5. Look inside before extracting
@@ -252,11 +294,7 @@ 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.
> Verified against `v0.0.1`: both tarballs listed exactly the one directory and > Not verified on this host: there is no release tarball to list or extract.
> 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
@@ -305,13 +343,23 @@ 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
``` ```
> Verified against `v0.0.1`: the digest in `IMAGE-DIGEST.txt` and the digest > Not verified against nxdns: no image is published, so no command here was run
> the `:0.0.1` tag resolves to were the same string > against `git.mial.net/mokhtar/nxdns`. The two
> (`sha256:f2945fbf6c1e16509f0e33e3d62da9a9cd7dc706718d333ce4edf95c80dbb00e`, > `docker buildx imagetools inspect --format` shapes were run on this host
> and `:latest` resolved to it too), the platform form printed exactly > against `alpine:3.22` on Docker Hub — the digest form printed
> `linux/amd64 linux/arm64` with no attestation entries, `docker pull` of the > `sha256:14358309a308569c32bdc37e2e0e9694be33a9d99e68afb0f5ff33cc1f695dce`,
> pinned reference succeeded, and the binary copied out of that pulled image > which is the digest this project's builder stage pins, and the platform form
> hashed identical to the `nxdns` in the x86_64 tarball. > printed `linux/amd64 unknown/unknown linux/arm unknown/unknown ...`. That
> `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
@@ -383,16 +431,14 @@ 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.
> Verified against `v0.0.1`, and the result is the caveat above in action. The > Partly verified on this host. `zig build dist` and `sha256sum` on its output
> whole recipe ran from a fresh clone: `git verify-tag v0.0.1` printed > were run to completion, with the version read out of `build.zig.zon`: `dist`
> `Good signature` under the same signing subkey as the release, and > exited 0 and wrote the two tarballs, `SHA256SUMS` and the staged payloads
> `zig build dist` produced both tarballs. The hashes did **not** match the > described above. `zig build verify-dist` was run on the result too and exited
> published `SHA256SUMS.txt` — the binaries themselves already differ. The Zig > 0. What could not be run is everything that needs a release: the clone, the
> version matched the pin exactly; the Node version did not (24.14.1 against > checkout and `git verify-tag` need a tag that does not exist, and there is no
> the pinned 24.19.0) and the build path differed, two of the ordinary causes > published `SHA256SUMS.txt` to compare a local build against, so the comparison
> listed above. That is a measurement of what an unpinned rebuild gives you, > this section is about has never been performed.
> 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
+10 -24
View File
@@ -695,16 +695,13 @@ 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.)
- [x] The image builds for both platforms with no qemu, carries `/LICENSE` and - [ ] 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. The v0.0.1 run built and pushed byte-identical to the tarball binaries. Verified for the native amd64
both platforms on the runner; the published index lists exactly image only; the arm64 half needs a runner with buildx.
`linux/amd64 linux/arm64`, and `release verify-image-binaries` compared - [ ] `gates.yml` runs from both `ci.yml` and `release.yml`; `ci.yml` triggers
both binaries against the tarballs before publication. on `master`; `origin/main` is gone. The first two are in the files; no
- [x] `gates.yml` runs from both `ci.yml` and `release.yml`; `ci.yml` triggers workflow has run and `origin/main` still exists (manual prerequisite).
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:
@@ -718,21 +715,10 @@ 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.
- [x] `v0.0.1` is published: five assets, a verifying signature, and an image at - [ ] `v0.0.1` is published: five assets, a verifying signature, and an image at
`git.mial.net/mokhtar/nxdns:0.0.1` and `:latest`. Run 493, all jobs green `git.mial.net/mokhtar/nxdns:0.0.1` and `:latest`.
on the first attempt after the dry-run fixes. - [ ] `docs/how-to/verify-a-release.md` was followed end to end against the
- [x] `docs/how-to/verify-a-release.md` was followed end to end against the published release, from a clean directory, on this host.
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 —