build: make build can produce an INSECURE debug bundle and verify-build certifies it green #309

Closed
opened 2026-08-20 11:59:27 +02:00 by clawbot · 2 comments
Collaborator

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_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.
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
Author
Collaborator

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).

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.
Author
Collaborator

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`.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/AutistMask#309