Found by the pre-1.0 deployability audit (#303). Blocker: the build-integrity control can be satisfied by the artifact it exists to reject.
script/verify-build:225-231 computes its expectation by reading AUTISTMASK_DEBUGout of the verifier's own environment. Makefile:50-54 invokes it bare, so it inherits the shell that ran the build. README.md:51 states "make build always produces a release build". It does not.
Reproduction
An operator with AUTISTMASK_DEBUG=1 exported in their shell, running the release target, in a throwaway copy of the tree:
### make build
Build mode: DEBUG (INSECURE - hardcoded test mnemonic, do not ship)
ok: dist/chrome/src/popup/index.js (autistmask-build-debug=on)
... (4 bundles)
verify-build: 4 bundle(s) verified autistmask-build-debug=on
### make build exit: 0
popup bundle contains DEBUG:!0 : true
Loaded in a real Chrome, that build:
[DBG] generated recovery phrase: "cube evolve unfold result inch risk jealous skill hotel bulb night wreck"
[DBG] resulting address: 0x66133E8ea0f5D1d612D2502a968757D1048c214a
[DBG] banner: "DEBUG / INSECURE (main)"
That is the publicly committed phrase from src/shared/constants.js:25-26. Every wallet such a build creates is drainable by anyone who reads this repo.
Separately, verify-build has zero provenance — it is a marker grep, not a build-integrity control. Demonstrated:
dist/chrome/src/popup/index.js replaced by a 26-byte file containing only the string autistmask-build-debug=off verifies ok.
dist/chrome/src/content/index.js (which runs on <all_urls>) and dist/chrome/manifest.json are not inspected at all, so arbitrary attacker content in them passes.
An entire hand-written dist/ unrelated to this tree passes with verify-build: 1 bundle(s) verified.
The trust root is dist/constants-bundles.txt, unsigned, inside the tree it describes.
The red DEBUG / INSECURE banner does appear, which is what makes this a build-process blocker rather than a certain-loss one.
Definition of done
make build scrubs the flag and the expectation is an explicit argument, never ambient env: env -u AUTISTMASK_DEBUG script/verify-build --expect release for build, --expect debug for build-debug. No argument = fail.
A shell with AUTISTMASK_DEBUG=1 exported running make buildfails the build, with a script/test-verify-build case asserting exactly that.
verify-build covers every emitted file in dist/<browser>/, including manifest.json and the content script, not only bundles listed in a file that lives inside the tree it vouches for.
A bundle that does not come from this build cannot satisfy it — the file list is derived from the build that just ran, not read back from dist/.
make check green.
Found by the pre-1.0 deployability audit (https://git.eeqj.de/sneak/AutistMask/issues/303). **Blocker: the build-integrity control can be satisfied by the artifact it exists to reject.**
`script/verify-build:225-231` computes its expectation by reading `AUTISTMASK_DEBUG` **out of the verifier's own environment**. `Makefile:50-54` invokes it bare, so it inherits the shell that ran the build. `README.md:51` states "`make build` always produces a release build". It does not.
## Reproduction
An operator with `AUTISTMASK_DEBUG=1` exported in their shell, running the **release** target, in a throwaway copy of the tree:
```
### make build
Build mode: DEBUG (INSECURE - hardcoded test mnemonic, do not ship)
ok: dist/chrome/src/popup/index.js (autistmask-build-debug=on)
... (4 bundles)
verify-build: 4 bundle(s) verified autistmask-build-debug=on
### make build exit: 0
popup bundle contains DEBUG:!0 : true
```
Loaded in a real Chrome, that build:
```
[DBG] generated recovery phrase: "cube evolve unfold result inch risk jealous skill hotel bulb night wreck"
[DBG] resulting address: 0x66133E8ea0f5D1d612D2502a968757D1048c214a
[DBG] banner: "DEBUG / INSECURE (main)"
```
That is the publicly committed phrase from `src/shared/constants.js:25-26`. Every wallet such a build creates is drainable by anyone who reads this repo.
Separately, `verify-build` has **zero provenance** — it is a marker grep, not a build-integrity control. Demonstrated:
- `dist/chrome/src/popup/index.js` replaced by a 26-byte file containing only the string `autistmask-build-debug=off` verifies `ok`.
- `dist/chrome/src/content/index.js` (which runs on `<all_urls>`) and `dist/chrome/manifest.json` are not inspected at all, so arbitrary attacker content in them passes.
- An entire hand-written `dist/` unrelated to this tree passes with `verify-build: 1 bundle(s) verified`.
The trust root is `dist/constants-bundles.txt`, unsigned, inside the tree it describes.
The red `DEBUG / INSECURE` banner does appear, which is what makes this a build-process blocker rather than a certain-loss one.
## Definition of done
- [ ] `make build` scrubs the flag and the expectation is an explicit argument, never ambient env: `env -u AUTISTMASK_DEBUG script/verify-build --expect release` for `build`, `--expect debug` for `build-debug`. No argument = fail.
- [ ] A shell with `AUTISTMASK_DEBUG=1` exported running `make build` **fails the build**, with a `script/test-verify-build` case asserting exactly that.
- [ ] `verify-build` covers every emitted file in `dist/<browser>/`, including `manifest.json` and the content script, not only bundles listed in a file that lives inside the tree it vouches for.
- [ ] A bundle that does not come from this build cannot satisfy it — the file list is derived from the build that just ran, not read back from `dist/`.
- [ ] `make check` green.
clawbot
added this to the 1.0.0 milestone 2026-08-20 11:59:27 +02:00
Part 1.script/verify-build stops reading AUTISTMASK_DEBUG entirely. It takes --expect release|debug (required; missing, repeated or unknown arguments fail). Makefile calls env -u AUTISTMASK_DEBUG script/verify-build --expect release ... for build and --expect debug for build-debug. AUTISTMASK_DEBUG=1 make build therefore still compiles a debug bundle and then fails at verification, which is the loud outcome the issue asks for rather than a silent release.
Part 2. The file list moves out of dist/. build.js records every path it emits (bundles, popup HTML, both styles.css copies, both manifest.json) and writes a receipt — one line per emitted file: sha256, whether the bundle contains src/shared/constants.js, and the path — to the path in AUTISTMASK_BUILD_RECEIPT. The Makefile creates that path with mktemp per invocation, outside the repo, and removes it in a trap; build.js refuses a receipt path inside dist/. dist/constants-bundles.txt is deleted. build.js also clears dist/ first, so the tree holds only what this build wrote.
verify-build --receipt PATH then asserts: every receipt entry exists as a regular file with matching bytes; every audited bundle carries the expected marker; no non-audited file carries one; and every file and symlink found under dist/ appears in the receipt. So the 26-byte marker file, a tampered manifest.json or content script, an extra file, and a wholesale hand-written dist/ all fail.
What that does and does not prove goes in the PR body — it is a within-one-make build-invocation guarantee, not cryptographic provenance for a third party holding a dist/ (that is #310).
make verify-build (the standalone re-check target) goes away: without a receipt from a build that just ran, it cannot honestly assert anything, and keeping it would preserve exactly the trust root this issue is about.
script/test-verify-build is extended in place: the receipt replaces the manifest in the fixture, the existing failure modes (grep exit-2, find status, newline/trailing-space paths, symlinked dist/, the permission cases and their root probe) are kept, and cases are added for each of the four demonstrated bypasses, for a missing --expect, and for AUTISTMASK_DEBUG=1 being ignored by the verifier.
Plan.
**Part 1.** `script/verify-build` stops reading `AUTISTMASK_DEBUG` entirely. It takes `--expect release|debug` (required; missing, repeated or unknown arguments fail). `Makefile` calls `env -u AUTISTMASK_DEBUG script/verify-build --expect release ...` for `build` and `--expect debug` for `build-debug`. `AUTISTMASK_DEBUG=1 make build` therefore still compiles a debug bundle and then fails at verification, which is the loud outcome the issue asks for rather than a silent release.
**Part 2.** The file list moves out of `dist/`. `build.js` records every path it emits (bundles, popup HTML, both `styles.css` copies, both `manifest.json`) and writes a receipt — one line per emitted file: sha256, whether the bundle contains `src/shared/constants.js`, and the path — to the path in `AUTISTMASK_BUILD_RECEIPT`. The `Makefile` creates that path with `mktemp` per invocation, outside the repo, and removes it in a trap; `build.js` refuses a receipt path inside `dist/`. `dist/constants-bundles.txt` is deleted. `build.js` also clears `dist/` first, so the tree holds only what this build wrote.
`verify-build --receipt PATH` then asserts: every receipt entry exists as a regular file with matching bytes; every audited bundle carries the expected marker; no non-audited file carries one; and every file and symlink found under `dist/` appears in the receipt. So the 26-byte marker file, a tampered `manifest.json` or content script, an extra file, and a wholesale hand-written `dist/` all fail.
What that does and does not prove goes in the PR body — it is a within-one-`make build`-invocation guarantee, not cryptographic provenance for a third party holding a `dist/` (that is [#310](https://git.eeqj.de/sneak/AutistMask/issues/310)).
`make verify-build` (the standalone re-check target) goes away: without a receipt from a build that just ran, it cannot honestly assert anything, and keeping it would preserve exactly the trust root this issue is about.
`script/test-verify-build` is extended in place: the receipt replaces the manifest in the fixture, the existing failure modes (grep exit-2, `find` status, newline/trailing-space paths, symlinked `dist/`, the permission cases and their root probe) are kept, and cases are added for each of the four demonstrated bypasses, for a missing `--expect`, and for `AUTISTMASK_DEBUG=1` being ignored by the verifier.
Built and pushed as #330 (branch issue-309-build-integrity, base next). Full rationale is in the PR body; the short version and how it was verified:
Part 1.script/verify-build reads nothing from its environment. The mode is the required argument --expect release|debug, with no default; make build passes --expect release under env -u AUTISTMASK_DEBUG, make build-debug passes --expect debug. AUTISTMASK_DEBUG=1 is deliberately still allowed to reach the compiler, so that shell compiles a debug bundle and then fails the target instead of quietly getting one. Run by hand on the branch: AUTISTMASK_DEBUG=1 make build now exits 2 on is autistmask-build-debug=on but this build was told to expect autistmask-build-debug=off, where before it printed verify-build: 4 bundle(s) verified autistmask-build-debug=on and exited 0.
Part 2.build.js records every file it emits and writes a receipt of them (path, sha256, whether the file bundles constants.js) to a path the Makefile mktemps per invocation outside the repo and deletes afterwards; a receipt path inside dist/ is refused, dist/ is cleared before a build, and dist/constants-bundles.txt is gone. verify-build checks the receipt's shape, then that dist/ holds nothing the build did not emit and no symlinks, then every recorded file's bytes and every audited bundle's marker. All four demonstrated bypasses now fail: the 26-byte marker-only file, the tampered content script, the tampered manifest.json, and the hand-written dist/.
The guarantee is stated precisely rather than broadly, in README.md and in the script header: dist/ is byte for byte the output of the build.js run that just finished. It says nothing about the honesty of the source tree or of build.js, and nothing to a third party holding a dist/ — that is signing, #310. One consequence: the standalone make verify-build target is removed, because re-verifying a dist/ from a list inside it is the defect itself.
Verified.make check green (39 suites / 811 tests, test-verify-build: 39 case(s) passed, up from 18 cases), and green inside the pinned image via docker build --no-cache-filter=check . so the stage demonstrably ran, where the harness runs as root and reports the setpriv runner rather than skipping the permission cases. Coverage proved non-vacuous by mutation: disabling the digest comparison fails exactly the four bypass cases, removing the dist/ walk fails the eight extra-file and symlink cases, restoring the ambient-environment fallback fails the missing---expect case, breaking the Makefile recipe fails the make -n wiring cases, and dropping manifest.json from the recorded emissions fails a real make build.
Built and pushed as [#330](https://git.eeqj.de/sneak/AutistMask/pulls/330) (branch `issue-309-build-integrity`, base `next`). Full rationale is in the PR body; the short version and how it was verified:
**Part 1.** `script/verify-build` reads nothing from its environment. The mode is the required argument `--expect release|debug`, with no default; `make build` passes `--expect release` under `env -u AUTISTMASK_DEBUG`, `make build-debug` passes `--expect debug`. `AUTISTMASK_DEBUG=1` is deliberately still allowed to reach the compiler, so that shell compiles a debug bundle and then fails the target instead of quietly getting one. Run by hand on the branch: `AUTISTMASK_DEBUG=1 make build` now exits 2 on `is autistmask-build-debug=on but this build was told to expect autistmask-build-debug=off`, where before it printed `verify-build: 4 bundle(s) verified autistmask-build-debug=on` and exited 0.
**Part 2.** `build.js` records every file it emits and writes a receipt of them (path, sha256, whether the file bundles `constants.js`) to a path the `Makefile` mktemps per invocation outside the repo and deletes afterwards; a receipt path inside `dist/` is refused, `dist/` is cleared before a build, and `dist/constants-bundles.txt` is gone. `verify-build` checks the receipt's shape, then that `dist/` holds nothing the build did not emit and no symlinks, then every recorded file's bytes and every audited bundle's marker. All four demonstrated bypasses now fail: the 26-byte marker-only file, the tampered content script, the tampered `manifest.json`, and the hand-written `dist/`.
The guarantee is stated precisely rather than broadly, in README.md and in the script header: `dist/` is byte for byte the output of the `build.js` run that just finished. It says nothing about the honesty of the source tree or of `build.js`, and nothing to a third party holding a `dist/` — that is signing, [#310](https://git.eeqj.de/sneak/AutistMask/issues/310). One consequence: the standalone `make verify-build` target is removed, because re-verifying a `dist/` from a list inside it is the defect itself.
**Verified.** `make check` green (39 suites / 811 tests, `test-verify-build: 39 case(s) passed`, up from 18 cases), and green inside the pinned image via `docker build --no-cache-filter=check .` so the stage demonstrably ran, where the harness runs as root and reports the `setpriv` runner rather than skipping the permission cases. Coverage proved non-vacuous by mutation: disabling the digest comparison fails exactly the four bypass cases, removing the `dist/` walk fails the eight extra-file and symlink cases, restoring the ambient-environment fallback fails the missing-`--expect` case, breaking the `Makefile` recipe fails the `make -n` wiring cases, and dropping `manifest.json` from the recorded emissions fails a real `make build`.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Found by the pre-1.0 deployability audit (#303). Blocker: the build-integrity control can be satisfied by the artifact it exists to reject.
script/verify-build:225-231computes its expectation by readingAUTISTMASK_DEBUGout of the verifier's own environment.Makefile:50-54invokes it bare, so it inherits the shell that ran the build.README.md:51states "make buildalways produces a release build". It does not.Reproduction
An operator with
AUTISTMASK_DEBUG=1exported in their shell, running the release target, in a throwaway copy of the tree:Loaded in a real Chrome, that build:
That is the publicly committed phrase from
src/shared/constants.js:25-26. Every wallet such a build creates is drainable by anyone who reads this repo.Separately,
verify-buildhas zero provenance — it is a marker grep, not a build-integrity control. Demonstrated:dist/chrome/src/popup/index.jsreplaced by a 26-byte file containing only the stringautistmask-build-debug=offverifiesok.dist/chrome/src/content/index.js(which runs on<all_urls>) anddist/chrome/manifest.jsonare not inspected at all, so arbitrary attacker content in them passes.dist/unrelated to this tree passes withverify-build: 1 bundle(s) verified.The trust root is
dist/constants-bundles.txt, unsigned, inside the tree it describes.The red
DEBUG / INSECUREbanner does appear, which is what makes this a build-process blocker rather than a certain-loss one.Definition of done
make buildscrubs the flag and the expectation is an explicit argument, never ambient env:env -u AUTISTMASK_DEBUG script/verify-build --expect releaseforbuild,--expect debugforbuild-debug. No argument = fail.AUTISTMASK_DEBUG=1exported runningmake buildfails the build, with ascript/test-verify-buildcase asserting exactly that.verify-buildcovers every emitted file indist/<browser>/, includingmanifest.jsonand the content script, not only bundles listed in a file that lives inside the tree it vouches for.dist/.make checkgreen.Plan.
Part 1.
script/verify-buildstops readingAUTISTMASK_DEBUGentirely. It takes--expect release|debug(required; missing, repeated or unknown arguments fail).Makefilecallsenv -u AUTISTMASK_DEBUG script/verify-build --expect release ...forbuildand--expect debugforbuild-debug.AUTISTMASK_DEBUG=1 make buildtherefore still compiles a debug bundle and then fails at verification, which is the loud outcome the issue asks for rather than a silent release.Part 2. The file list moves out of
dist/.build.jsrecords every path it emits (bundles, popup HTML, bothstyles.csscopies, bothmanifest.json) and writes a receipt — one line per emitted file: sha256, whether the bundle containssrc/shared/constants.js, and the path — to the path inAUTISTMASK_BUILD_RECEIPT. TheMakefilecreates that path withmktempper invocation, outside the repo, and removes it in a trap;build.jsrefuses a receipt path insidedist/.dist/constants-bundles.txtis deleted.build.jsalso clearsdist/first, so the tree holds only what this build wrote.verify-build --receipt PATHthen asserts: every receipt entry exists as a regular file with matching bytes; every audited bundle carries the expected marker; no non-audited file carries one; and every file and symlink found underdist/appears in the receipt. So the 26-byte marker file, a tamperedmanifest.jsonor content script, an extra file, and a wholesale hand-writtendist/all fail.What that does and does not prove goes in the PR body — it is a within-one-
make build-invocation guarantee, not cryptographic provenance for a third party holding adist/(that is #310).make verify-build(the standalone re-check target) goes away: without a receipt from a build that just ran, it cannot honestly assert anything, and keeping it would preserve exactly the trust root this issue is about.script/test-verify-buildis extended in place: the receipt replaces the manifest in the fixture, the existing failure modes (grep exit-2,findstatus, newline/trailing-space paths, symlinkeddist/, the permission cases and their root probe) are kept, and cases are added for each of the four demonstrated bypasses, for a missing--expect, and forAUTISTMASK_DEBUG=1being ignored by the verifier.Built and pushed as #330 (branch
issue-309-build-integrity, basenext). Full rationale is in the PR body; the short version and how it was verified:Part 1.
script/verify-buildreads nothing from its environment. The mode is the required argument--expect release|debug, with no default;make buildpasses--expect releaseunderenv -u AUTISTMASK_DEBUG,make build-debugpasses--expect debug.AUTISTMASK_DEBUG=1is deliberately still allowed to reach the compiler, so that shell compiles a debug bundle and then fails the target instead of quietly getting one. Run by hand on the branch:AUTISTMASK_DEBUG=1 make buildnow exits 2 onis autistmask-build-debug=on but this build was told to expect autistmask-build-debug=off, where before it printedverify-build: 4 bundle(s) verified autistmask-build-debug=onand exited 0.Part 2.
build.jsrecords every file it emits and writes a receipt of them (path, sha256, whether the file bundlesconstants.js) to a path theMakefilemktemps per invocation outside the repo and deletes afterwards; a receipt path insidedist/is refused,dist/is cleared before a build, anddist/constants-bundles.txtis gone.verify-buildchecks the receipt's shape, then thatdist/holds nothing the build did not emit and no symlinks, then every recorded file's bytes and every audited bundle's marker. All four demonstrated bypasses now fail: the 26-byte marker-only file, the tampered content script, the tamperedmanifest.json, and the hand-writtendist/.The guarantee is stated precisely rather than broadly, in README.md and in the script header:
dist/is byte for byte the output of thebuild.jsrun that just finished. It says nothing about the honesty of the source tree or ofbuild.js, and nothing to a third party holding adist/— that is signing, #310. One consequence: the standalonemake verify-buildtarget is removed, because re-verifying adist/from a list inside it is the defect itself.Verified.
make checkgreen (39 suites / 811 tests,test-verify-build: 39 case(s) passed, up from 18 cases), and green inside the pinned image viadocker build --no-cache-filter=check .so the stage demonstrably ran, where the harness runs as root and reports thesetprivrunner rather than skipping the permission cases. Coverage proved non-vacuous by mutation: disabling the digest comparison fails exactly the four bypass cases, removing thedist/walk fails the eight extra-file and symlink cases, restoring the ambient-environment fallback fails the missing---expectcase, breaking theMakefilerecipe fails themake -nwiring cases, and droppingmanifest.jsonfrom the recorded emissions fails a realmake build.