Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c89acf337 |
+60
-106
@@ -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
|
||||
tarball and a matching checksum at the same time.
|
||||
|
||||
> Verification: no nxdns release exists yet. The repository has no tags, no
|
||||
> release page and no pushed image, so nothing on this page could be run against
|
||||
> a real release asset and no command here was pointed at
|
||||
> `git.mial.net/mokhtar/nxdns` with any expectation of success. Substitutes were
|
||||
> used, and every block says which one applies to it.
|
||||
>
|
||||
> 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.
|
||||
> Verification: every command on this page was run on 2026-08-09 against the
|
||||
> published `v0.0.1` release, from a clean directory, with a clean `GNUPGHOME`
|
||||
> holding only the key fetched from keys.openpgp.org. Every transcript below is
|
||||
> that run's output. Where a block shows a failure — a `BAD signature`, a
|
||||
> `FAILED` hash — the failure was produced deliberately by tampering with a
|
||||
> copy of the real file, and the surrounding text says how.
|
||||
|
||||
## What a release contains
|
||||
|
||||
@@ -94,11 +65,8 @@ want for the placeholder:
|
||||
VERSION=<version>
|
||||
```
|
||||
|
||||
> Not verified against nxdns: there is no release to redirect to, so the first
|
||||
> 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`.
|
||||
> Verified: the two-command form, run against this repository, printed `0.0.1`
|
||||
> with `v0.0.1` published.
|
||||
|
||||
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.
|
||||
@@ -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
|
||||
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.
|
||||
> Both URL forms, including the 404 for GitHub's spelling, were exercised
|
||||
> 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.
|
||||
> Verified against `v0.0.1`: all five assets downloaded through the versioned
|
||||
> path, `SHA256SUMS.txt` downloaded again through the `latest` alias and hashed
|
||||
> identical, and GitHub's spelling answered 404.
|
||||
|
||||
## 3. Check the signature over `SHA256SUMS.txt`
|
||||
|
||||
@@ -157,11 +123,9 @@ curl -fsSL https://keys.openpgp.org/vks/v1/by-fingerprint/A2061F6AB24DF2C0E92346
|
||||
gpg --import
|
||||
```
|
||||
|
||||
> Not verified: the key is not published yet. Run on this host, that URL
|
||||
> returned 404, and so did the `by-email` lookup for the same address. The
|
||||
> 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.
|
||||
> Verified: the key is published, and that exact `curl | gpg --import` reported
|
||||
> `key 1509B54946D08A95: public key "Mokhtar Mial (pc) <mokhtar@mial.net>"
|
||||
> imported` into a clean `GNUPGHOME`.
|
||||
|
||||
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: using EDDSA key 9D1EA241DAEA89E09381A21BDC27E8A3D53C32D6
|
||||
gpg: Good signature from "nxdns release signing (throwaway demonstration key) <demo@example.invalid>" [unknown]
|
||||
gpg: Signature made Sun 09 Aug 2026 01:26:42 AM CEST
|
||||
gpg: using EDDSA key 019D00DF8417EBFDA5471E5EF7319CC024FB5A96
|
||||
gpg: Good signature from "Mokhtar Mial (pc) <mokhtar@mial.net>" [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: 6643 13AA F527 DDAE 1C1E 516C A36F F8DA 4E6C 1C07
|
||||
Subkey fingerprint: 9D1E A241 DAEA 89E0 9381 A21B DC27 E8A3 D53C 32D6
|
||||
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
|
||||
```
|
||||
|
||||
**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
|
||||
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;
|
||||
`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
|
||||
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:
|
||||
|
||||
```
|
||||
gpg: Signature made Fri 07 Aug 2026 10:11:12 PM CEST
|
||||
gpg: using EDDSA key 9D1EA241DAEA89E09381A21BDC27E8A3D53C32D6
|
||||
gpg: BAD signature from "nxdns release signing (throwaway demonstration key) <demo@example.invalid>" [unknown]
|
||||
gpg: Signature made Sun 09 Aug 2026 01:26:42 AM CEST
|
||||
gpg: using EDDSA key 019D00DF8417EBFDA5471E5EF7319CC024FB5A96
|
||||
gpg: BAD signature from "Mokhtar Mial (pc) <mokhtar@mial.net>" [unknown]
|
||||
```
|
||||
|
||||
> Verified on this host. A throwaway ed25519 primary key was generated into a
|
||||
> temporary `GNUPGHOME`, an ed25519 **signing subkey** was added to it, and the
|
||||
> stand-in `SHA256SUMS.txt` was signed with `--local-user <subkey-fingerprint>!`
|
||||
> — the same construction the release workflow uses — so the transcripts above
|
||||
> are what a subkey-signed release actually prints, rather than what a key
|
||||
> signing with its primary would. The verification ran from a second
|
||||
> `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.
|
||||
> Verified against `v0.0.1`, from a clean `GNUPGHOME` holding only the imported
|
||||
> public key — which is why the `[unknown]` trust marker and the warning are
|
||||
> there rather than being written in by hand. The good-signature transcript is
|
||||
> the real release's; the `BAD signature` transcript is the same command
|
||||
> against a copy of `SHA256SUMS.txt` with one newline appended, and it exited
|
||||
> 1. The `sed`/`tr` pipeline printed
|
||||
> `A2061F6AB24DF2C0E92346FD1509B54946D08A95`, matching the fingerprint above.
|
||||
|
||||
## 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
|
||||
thing in the set they cannot forge.
|
||||
|
||||
> Verified on this host against the stand-in files: all three transcripts are
|
||||
> real `sha256sum` output over two random-byte files named like the release
|
||||
> tarballs plus an `IMAGE-DIGEST.txt` holding one image reference, with one
|
||||
> tarball deleted for the first two blocks and one byte appended to the other
|
||||
> for the third. Only the version in the filenames was replaced with
|
||||
> `<version>`.
|
||||
> Verified against `v0.0.1`: with both tarballs present, `sha256sum -c` printed
|
||||
> three `OK` lines. The three transcripts above are the same command over
|
||||
> copies of the real assets, with the aarch64 tarball absent for the first two
|
||||
> and one byte appended to the x86_64 tarball for the third. Only the version
|
||||
> in the filenames was replaced with `<version>`.
|
||||
|
||||
## 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
|
||||
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
|
||||
|
||||
@@ -343,23 +305,13 @@ docker rm nxdns-verify
|
||||
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
|
||||
> against `git.mial.net/mokhtar/nxdns`. The two
|
||||
> `docker buildx imagetools inspect --format` shapes were run on this host
|
||||
> against `alpine:3.22` on Docker Hub — the digest form printed
|
||||
> `sha256:14358309a308569c32bdc37e2e0e9694be33a9d99e68afb0f5ff33cc1f695dce`,
|
||||
> which is the digest this project's builder stage pins, and the platform form
|
||||
> 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.
|
||||
> Verified against `v0.0.1`: the digest in `IMAGE-DIGEST.txt` and the digest
|
||||
> the `:0.0.1` tag resolves to were the same string
|
||||
> (`sha256:f2945fbf6c1e16509f0e33e3d62da9a9cd7dc706718d333ce4edf95c80dbb00e`,
|
||||
> and `:latest` resolved to it too), the platform form printed exactly
|
||||
> `linux/amd64 linux/arm64` with no attestation entries, `docker pull` of the
|
||||
> pinned reference succeeded, and the binary copied out of that pulled image
|
||||
> hashed identical to the `nxdns` in the x86_64 tarball.
|
||||
|
||||
## 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
|
||||
`.gitea/workflows/gates.yml`, which is the workflow the release runs.
|
||||
|
||||
> Partly verified on this host. `zig build dist` and `sha256sum` on its output
|
||||
> were run to completion, with the version read out of `build.zig.zon`: `dist`
|
||||
> exited 0 and wrote the two tarballs, `SHA256SUMS` and the staged payloads
|
||||
> described above. `zig build verify-dist` was run on the result too and exited
|
||||
> 0. What could not be run is everything that needs a release: the clone, the
|
||||
> checkout and `git verify-tag` need a tag that does not exist, and there is no
|
||||
> published `SHA256SUMS.txt` to compare a local build against, so the comparison
|
||||
> this section is about has never been performed.
|
||||
> Verified against `v0.0.1`, and the result is the caveat above in action. The
|
||||
> whole recipe ran from a fresh clone: `git verify-tag v0.0.1` printed
|
||||
> `Good signature` under the same signing subkey as the release, and
|
||||
> `zig build dist` produced both tarballs. The hashes did **not** match the
|
||||
> published `SHA256SUMS.txt` — the binaries themselves already differ. The Zig
|
||||
> version matched the pin exactly; the Node version did not (24.14.1 against
|
||||
> the pinned 24.19.0) and the build path differed, two of the ordinary causes
|
||||
> 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
|
||||
|
||||
|
||||
+24
-10
@@ -695,13 +695,16 @@ was reproduced before it was fixed.
|
||||
- [x] Two runs of `zig build dist` on the same commit produce byte-identical
|
||||
tarballs **in the same directory**. (Cross-directory reproducibility is
|
||||
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
|
||||
byte-identical to the tarball binaries. Verified for the native amd64
|
||||
image only; the arm64 half needs a runner with buildx.
|
||||
- [ ] `gates.yml` runs from both `ci.yml` and `release.yml`; `ci.yml` triggers
|
||||
on `master`; `origin/main` is gone. The first two are in the files; no
|
||||
workflow has run and `origin/main` still exists (manual prerequisite).
|
||||
byte-identical to the tarball binaries. The v0.0.1 run built and pushed
|
||||
both platforms on the runner; the published index lists exactly
|
||||
`linux/amd64 linux/arm64`, and `release verify-image-binaries` compared
|
||||
both binaries against the tarballs before publication.
|
||||
- [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
|
||||
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:
|
||||
@@ -715,10 +718,21 @@ was reproduced before it was fixed.
|
||||
multi-architecture image — and was then deleted (release, git tag, both
|
||||
registry versions). Five attempts; the failures and their fixes are
|
||||
deviations 23-26.
|
||||
- [ ] `v0.0.1` is published: five assets, a verifying signature, and an image at
|
||||
`git.mial.net/mokhtar/nxdns:0.0.1` and `:latest`.
|
||||
- [ ] `docs/how-to/verify-a-release.md` was followed end to end against the
|
||||
published release, from a clean directory, on this host.
|
||||
- [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`. Run 493, all jobs green
|
||||
on the first attempt after the dry-run fixes.
|
||||
- [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
|
||||
**active** surface: `build.zig`, the workflows, `deploy/`, `README.md` and
|
||||
`docs/`. Historical milestone specs and `TECH_DEBT.md` keep their text —
|
||||
|
||||
Reference in New Issue
Block a user