fix: verify the build against its own receipt, with the expected mode as an argument (closes #309)
script/verify-build computed its expectation from AUTISTMASK_DEBUG in its own environment, and the Makefile invoked it bare, so an operator with that flag exported who ran the release target got a debug bundle -- every wallet it creates carrying the publicly committed test recovery phrase -- verified green at exit 0. The mode is now the required argument --expect release|debug, with no default and nothing read from the environment; make build passes --expect release on an env -u AUTISTMASK_DEBUG environment and make build-debug passes --expect debug. The flag is deliberately still allowed to reach the compiler, so a shell that has it exported fails make build loudly rather than quietly receiving something other than the release build it asked for. The other half was provenance. 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 as "1 bundle(s) verified". build.js now records every file it emits and writes a receipt of them -- path, sha256, and whether the file is one of the bundles containing constants.js -- to a path the Makefile creates with mktemp per invocation, outside the repo, and deletes afterwards; a receipt path inside dist/ is refused. dist/ is cleared before a build, so it holds only what that build wrote. dist/constants-bundles.txt is gone, and with it the standalone make verify-build target: re-verifying a dist/ out of the dist/ itself is the thing that was broken. verify-build now checks the receipt's shape, then that dist/ contains nothing the build did not emit and no symlinks, then each recorded file's bytes against its digest and each audited bundle's marker against --expect. The guarantee is narrow and README.md states it as such: dist/ is byte for byte the output of the build.js run that just finished. It proves nothing about the honesty of the source tree or of build.js, and offers nothing to a third party holding a dist/. That is signing: #310 script/test-verify-build goes from 18 cases to 39, extended in place: one per demonstrated bypass, the missing/invalid argument cases, an AUTISTMASK_DEBUG=1 environment that the verifier must ignore, debug bundles that must fail --expect release, and four checks that read the make build and make build-debug recipes back out of make -n. The existing failure modes (grep exit-2, find's status, newline and trailing-space paths, symlinked dist/, and the root probe that refuses to count permission cases vacuously) are kept. Verified: make check green (39 suites / 811 tests, 39 verify-build cases, permission cases enabled), and green again inside the pinned image via script/cibuild with --no-cache-filter=check, where the harness runs as root and reports the setpriv runner rather than skipping. Non-vacuity proved 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 AUTISTMASK_DEBUG fallback fails the no---expect case, breaking the Makefile recipe fails the wiring cases, and dropping manifest.json from the recorded emissions fails a real make build.
This commit is contained in:
82
README.md
82
README.md
@@ -48,28 +48,57 @@ Load the extension:
|
||||
|
||||
### Debug Builds
|
||||
|
||||
`make build` always produces a release build: the build-time `DEBUG` constant is
|
||||
`false`, so wallet creation uses real entropy and the red banner is off. To
|
||||
produce a debug build instead, set `AUTISTMASK_DEBUG=1` in the environment:
|
||||
`make build` never hands back a debug build. `make build-debug` is the only
|
||||
target that produces one:
|
||||
|
||||
```bash
|
||||
make build-debug # or: AUTISTMASK_DEBUG=1 make build
|
||||
make build-debug
|
||||
```
|
||||
|
||||
Only the exact value `1` enables it; any other value (including unset, empty, or
|
||||
`true`) yields a release build, so a typo cannot accidentally ship the debug
|
||||
behavior. The build prints which mode it used. See the
|
||||
`AUTISTMASK_DEBUG=1` still selects the debug compile, and only the exact value
|
||||
`1` does; any other value (including unset, empty, or `true`) yields a release
|
||||
build, so a typo cannot accidentally ship the debug behavior. But it is the
|
||||
compiler's input, not the verifier's: if it happens to be exported in the shell
|
||||
that runs `make build`, that target compiles a debug bundle and then **fails**,
|
||||
because it tells `script/verify-build` in so many words that it was supposed to
|
||||
produce a release build. It used to be that the verifier read the same variable
|
||||
out of its own environment, agreed with itself, and reported a debug artifact as
|
||||
verified. The build prints which mode it used. See the
|
||||
[DEBUG Mode Policy](#debug-mode-policy) for what the flag changes. **Never
|
||||
distribute a debug build** — every wallet it creates gets the same publicly
|
||||
known test recovery phrase.
|
||||
|
||||
Both builds end by running `script/verify-build`, which reads the compiled
|
||||
Both targets end by running `script/verify-build`, which reads the compiled
|
||||
`DEBUG` state back out of the emitted bundles and fails the build if it is not
|
||||
the one that was asked for. The test suite cannot check this: it loads
|
||||
`src/shared/constants.js` outside a bundle, so it only ever sees the fallback
|
||||
value. The assertion is on the artifacts because that is where the property
|
||||
lives.
|
||||
|
||||
### Build Receipts
|
||||
|
||||
`build.js` records every file it emits — path, sha256, and whether the file is
|
||||
one of the bundles containing `src/shared/constants.js` — into a build receipt,
|
||||
and `script/verify-build` checks `dist/` against that receipt: every recorded
|
||||
file present with exactly the recorded bytes, every audited bundle carrying the
|
||||
requested `DEBUG` marker, and nothing under `dist/` that the build did not
|
||||
write. The `Makefile` creates the receipt path with `mktemp` per invocation,
|
||||
outside the repo, and deletes it afterwards.
|
||||
|
||||
That is what ties the check to a build rather than to a directory. What it
|
||||
establishes is narrow and worth stating exactly: `dist/` is byte for byte the
|
||||
output of the `build.js` run that just finished, with nothing added, removed or
|
||||
altered in between. It establishes nothing about whether the source tree or
|
||||
`build.js` were honest, and it offers nothing to someone handed a `dist/` from
|
||||
elsewhere — without the receipt from its own build there is no input to the
|
||||
check. Verifiable provenance for a third party is signing, which this is not.
|
||||
|
||||
There is deliberately no target that re-verifies an existing `dist/` on its own.
|
||||
The list of files to check has to come from the build that produced them; read
|
||||
back out of `dist/`, it is the artifact vouching for itself, which is how a
|
||||
26-byte file containing only the marker string, a hostile content script, and an
|
||||
entire hand-written `dist/` all used to verify green.
|
||||
|
||||
## Entrypoints
|
||||
|
||||
This repository adheres to the
|
||||
@@ -114,20 +143,26 @@ provide:
|
||||
serves. Run deliberately, never as part of a build: the output is committed
|
||||
and there is no runtime fetch, so the shipped list is as fresh as the last
|
||||
vendoring run that was released
|
||||
- `script/verify-build` — assert the compiled `DEBUG` state of the bundles in
|
||||
`dist/`: every bundle containing `src/shared/constants.js` must have `DEBUG`
|
||||
off, or on when `AUTISTMASK_DEBUG=1`. Run automatically at the end of
|
||||
`make build` and `make build-debug`; fails loudly rather than passing if it
|
||||
cannot determine a bundle's state. Not part of `make check`, which does not
|
||||
depend on build artifacts existing.
|
||||
- `script/verify-build --expect release|debug --receipt PATH` — assert that
|
||||
`dist/` is exactly what the build that just ran emitted, and that the compiled
|
||||
`DEBUG` state of the bundles in it is the one that was asked for. Both
|
||||
arguments are required and neither has a default: the expected mode is stated
|
||||
by the caller rather than read from `AUTISTMASK_DEBUG`, and the file list
|
||||
comes from the build's receipt rather than from `dist/` (see
|
||||
[Build Receipts](#build-receipts)). Run automatically at the end of
|
||||
`make build` and `make build-debug`; fails loudly rather than passing whenever
|
||||
it cannot determine something. Not part of `make check`, which does not depend
|
||||
on build artifacts existing.
|
||||
- `script/test-verify-build` — exercise every failure mode of
|
||||
`script/verify-build` against a fixture tree in a temp dir, asserting the exit
|
||||
status and the message of each. Part of `make check`; it reads no build
|
||||
artifacts and writes nothing under `dist/`. The cases that depend on file
|
||||
permissions cannot mean anything for a process that is not subject to them, so
|
||||
the harness proves its runner against a mode-000 file before counting them,
|
||||
dropping to an unprivileged user when run as root; if it cannot, it skips
|
||||
those cases and says so in a banner rather than passing them.
|
||||
status and the message of each, and read the `make build` and
|
||||
`make build-debug` recipes back out of `make -n` to check that they pass the
|
||||
mode as an argument on a scrubbed environment. Part of `make check`; it reads
|
||||
no build artifacts and writes nothing under `dist/`. The cases that depend on
|
||||
file permissions cannot mean anything for a process that is not subject to
|
||||
them, so the harness proves its runner against a mode-000 file before counting
|
||||
them, dropping to an unprivileged user when run as root; if it cannot, it
|
||||
skips those cases and says so in a banner rather than passing them.
|
||||
- `script/docker` — build the Docker image tagged via `script/projectname`
|
||||
- `script/cibuild` — CI entrypoint: plain `docker build .`
|
||||
- `script/precommit` — run by the git pre-commit hook; runs `script/check`
|
||||
@@ -140,9 +175,10 @@ The Makefile shims to those. It also carries a few targets that have no
|
||||
of `script/bootstrap`. Frozen so a stale `yarn.lock` fails instead of being
|
||||
silently rewritten. Use `make setup` for a fresh clone.
|
||||
- `make hooks` — shims to `script/install-precommit`
|
||||
- `make build` — build the extension into `dist/chrome/` and `dist/firefox/`
|
||||
- `make build-debug` — the same build with `AUTISTMASK_DEBUG=1` (see
|
||||
[Debug Builds](#debug-builds))
|
||||
- `make build` — build the extension into `dist/chrome/` and `dist/firefox/`,
|
||||
then verify the result against the build's receipt as a release build
|
||||
- `make build-debug` — the same build with `AUTISTMASK_DEBUG=1`, verified as a
|
||||
debug build (see [Debug Builds](#debug-builds))
|
||||
- `make clean` — remove `dist/`
|
||||
- `make dev` — build in watch mode
|
||||
|
||||
|
||||
Reference in New Issue
Block a user