build: make verify-build take an explicit expectation and a build receipt (closes #309)
All checks were successful
check / check (push) Successful in 30s
e2e / e2e-chrome (push) Successful in 1m10s
e2e / e2e-firefox (push) Successful in 22s

verify-build read its expectation from AUTISTMASK_DEBUG in its own environment
and the Makefile invoked it bare, so an operator with that variable exported
who ran the release target got an INSECURE debug build — every wallet it
creates uses the publicly committed test phrase — verified green, exit 0. It
also had no provenance: a 26-byte file containing the right marker string
passed, the content script and manifest.json were never inspected, and an
entire hand-written dist/ passed.

--expect release|debug and --receipt PATH are now both required, with no
defaults and nothing read from the environment. build.js records every file it
emits with its sha256 and writes the receipt; the Makefile mktemps it outside
the repo per invocation with a trap, and build.js refuses a receipt path inside
dist/. Verification runs three passes in a load-bearing order — receipt shape,
full dist/ walk, then per-file bytes — so an unwalkable subtree cannot make
files look absent. dist/constants-bundles.txt, which was an unsigned trust root
living inside the tree it vouched for, is gone.

What this proves is bounded and stated as such: dist/ is byte-for-byte the
output of the build.js run that just finished, within one make build
invocation. It proves nothing about the honesty of the source tree or build.js,
and nothing to anyone handed a dist/ from elsewhere — that is signing, #310.
The standalone make verify-build target is removed because its only input would
be dist/ itself, i.e. the artifact vouching for itself.

Verified: make check green, test-verify-build 39 cases (was 18), test-e2e 55/55
and test-e2e-firefox 8/8 with make build running uncached inside both images.
All four original bypasses now exit 1. Mutations: digests disabled fails
exactly 4 cases, dropping the dist/ walk fails exactly 8, restoring the ambient
fallback fails exactly 1.
This commit was merged in pull request #330.
This commit is contained in:
2026-08-20 14:24:55 +02:00
parent c8c2af0c6b
commit aea999db85
6 changed files with 1118 additions and 281 deletions

32
TODO.md
View File

@@ -25,8 +25,8 @@ pre-1.0, working towards the 1.0.0 milestone. Tagged v0.1.0 on 2026-02-27. The
milestone is in flight on `next`; its `next` -> `main` PR is
[#190](https://git.eeqj.de/sneak/AutistMask/pulls/190). `make check` verified
green on `next` at `e9fa8be` on 2026-08-10, and `make build` produces
`dist/chrome/` and `dist/firefox/` with every bundle verified to have `DEBUG`
compiled off.
`dist/chrome/` and `dist/firefox/`, verified against the build's own receipt to
be exactly what that build emitted with `DEBUG` compiled off.
The backlog lives on the
[Gitea tracker](https://git.eeqj.de/sneak/AutistMask/issues), which is
@@ -44,6 +44,34 @@ but the review is broader than any of them.
# Completed Steps
- 2026-08-20: `make build` can no longer hand back a debug build, and
`script/verify-build` can no longer be satisfied by bytes the build did not
produce ([#309](https://git.eeqj.de/sneak/AutistMask/issues/309)). The
verifier used to compute its expectation from `AUTISTMASK_DEBUG` in its own
environment, so an operator with that exported who ran the release target got
a debug bundle — every wallet it creates carrying the publicly committed test
phrase — certified green at exit 0. The expected mode is now the required
argument `--expect release|debug`, with no default and nothing read from the
environment, and the `Makefile` scrubs the flag from the verifier while
deliberately leaving it reaching the compiler, so that shell fails the build
loudly instead of quietly getting something other than what it asked for.
Provenance was the other half: the check was a marker grep over a file list
read back out of `dist/`, so a 26-byte file containing only
`autistmask-build-debug=off` verified `ok`, `manifest.json` and the content
script that runs on every page were never read at all, and an entire
hand-written `dist/` passed. `build.js` now records every file it emits, with
its sha256 and whether it is one of the bundles containing `constants.js`,
into a receipt whose path the `Makefile` makes fresh per invocation outside
the repo and deletes afterwards; `dist/constants-bundles.txt` is gone, and
`dist/` is cleared before a build so it holds only what that build wrote. The
standalone `make verify-build` target went with it: re-verifying a `dist/`
from the `dist/` itself is the thing that was broken. What this establishes is
narrow and stated as such in README.md — `dist/` is byte for byte the output
of the `build.js` run that just finished — and it is not signing, which is
[#310](https://git.eeqj.de/sneak/AutistMask/issues/310).
`script/test-verify-build` grew from 18 cases to 39, including one per
demonstrated bypass and the `make -n` read-back that proves the recipes pass
the mode as an argument.
- 2026-08-20: A hostile ERC-20 symbol no longer renders as live HTML in the
popup ([#307](https://git.eeqj.de/sneak/AutistMask/issues/307)). A token
symbol is whatever the contract's `symbol()` returns, the block explorer