From 265689e1399b14d18f6f7e1ddea749ca13053c1b Mon Sep 17 00:00:00 2001 From: che cheng Date: Mon, 24 Aug 2026 04:48:39 +0800 Subject: [PATCH 1/6] fix: verify resident macdoc before session probe (#161) --- .claude-plugin/marketplace.json | 2 +- plugins/macdoc/.claude-plugin/plugin.json | 2 +- plugins/macdoc/CHANGELOG.md | 10 +++ plugins/macdoc/hooks/session-start.sh | 24 +++++- .../macdoc/tests/session-start-reverify.sh | 75 +++++++++++++++++++ 5 files changed, 107 insertions(+), 6 deletions(-) create mode 100644 plugins/macdoc/tests/session-start-reverify.sh diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 00b7dbe0..711aa435 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -20,7 +20,7 @@ }, { "name": "macdoc", - "version": "1.4.0", + "version": "1.4.1", "description": "macOS 原生文件處理 CLI — 格式轉換、VLM OCR(含 host profile 設定)、SRT 處理。v1.2.0: session-start hook 自動安裝 signed CLI binary(arm64)。", "author": { "name": "Che Cheng" diff --git a/plugins/macdoc/.claude-plugin/plugin.json b/plugins/macdoc/.claude-plugin/plugin.json index 03e6e063..2ca235c9 100644 --- a/plugins/macdoc/.claude-plugin/plugin.json +++ b/plugins/macdoc/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "macdoc", "description": "macOS 原生文件處理 CLI — 格式轉換、VLM OCR(含 host profile 設定)、SRT 處理。v1.2.0: session-start hook 自動安裝 signed CLI binary(arm64)。", - "version": "1.4.0", + "version": "1.4.1", "binary_version": "0.7.0", "author": { "name": "Che Cheng" diff --git a/plugins/macdoc/CHANGELOG.md b/plugins/macdoc/CHANGELOG.md index 554a7e52..c5c69424 100644 --- a/plugins/macdoc/CHANGELOG.md +++ b/plugins/macdoc/CHANGELOG.md @@ -9,6 +9,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 > `plugin.json` description field. Section categorization is best-effort — > review and refine `Added` / `Changed` / `Fixed` etc. as needed. +## [1.4.1] - 2026-08-24 + +### Fixed + +- SessionStart 在執行常駐 `~/bin/macdoc --version` 或採用 sidecar fast path 前,先以 Developer ID requirement 重驗 binary(PsychQuant/macdoc#161)。簽章不符時不執行該檔,改強制嘗試一次下載;下載失敗仍維持 session fail-soft。 + +### Tests + +- 新增 resident binary 對抗測試:偽造正確版本但 codesign 失敗的 binary 不得被執行;合法且版本相符的 binary 必須先驗簽、再執行版本探測,且維持零網路 fast path。 + ## [1.4.0] - 2026-08-19 ### Changed diff --git a/plugins/macdoc/hooks/session-start.sh b/plugins/macdoc/hooks/session-start.sh index 3fa8d32e..954d956f 100755 --- a/plugins/macdoc/hooks/session-start.sh +++ b/plugins/macdoc/hooks/session-start.sh @@ -24,6 +24,10 @@ REQUIREMENT='=anchor apple generic and certificate 1[field.1.2.840.113635.100.6. note() { echo "macdoc plugin: $1" >&2; } soft_exit() { note "$1"; exit 0; } # fail-soft: never break session start +verify_binary() { + codesign --verify --strict -R "$REQUIREMENT" "$1" 2>/dev/null +} + [ "$(uname -m)" = "arm64" ] || exit 0 # arm64-only release; Intel builds from source (silent — not an error) PLUGIN_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" @@ -34,6 +38,18 @@ WANT=$(grep -oE '"binary_version"[[:space:]]*:[[:space:]]*"[^"]+"' "$PLUGIN_JSON | head -1 | sed -E 's/.*"([^"]+)"$/\1/' || true) [ -n "$WANT" ] || exit 0 # no pinned CLI version — nothing to manage +# Exec-time re-verification happens before even asking the resident binary for +# its version. A binary that merely prints WANT must never reach a fast path +# unless its Developer ID chain + Team requirement is valid (#161). +RESIDENT_VERIFIED=false +if [ -x "$BINARY" ]; then + if verify_binary "$BINARY"; then + RESIDENT_VERIFIED=true + else + note "existing binary failed signature verification — forcing one re-download attempt" + fi +fi + # --version with a 5s alarm (a hung/planted binary must not stall every # session start — fail-soft covers errors, not hangs; codex V114 HIGH-1). # Probe writes to a FILE, not a pipe: a killed probe may leave grandchildren @@ -43,17 +59,17 @@ WANT=$(grep -oE '"binary_version"[[:space:]]*:[[:space:]]*"[^"]+"' "$PLUGIN_JSON # re-download loop (codex V114 M-2). HAVE="" PROBE=$(mktemp "${TMPDIR:-/tmp}/.macdoc.probe.XXXXXX" 2>/dev/null) || PROBE="" -if [ -n "$PROBE" ]; then +if $RESIDENT_VERIFIED && [ -n "$PROBE" ]; then { perl -e 'alarm 5; exec @ARGV' -- "$BINARY" --version "$PROBE" 2>/dev/null; } 2>/dev/null || true HAVE=$(head -1 "$PROBE" 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true) rm -f "$PROBE" fi -[ "$HAVE" = "$WANT" ] && exit 0 # fast path: version matches, zero network +$RESIDENT_VERIFIED && [ "$HAVE" = "$WANT" ] && exit 0 # verified fast path: version matches, zero network # Loop-guard sidecar: if a previous session already installed WANT but the # binary self-reports an unparsable/odd version, do not re-download forever. GUARD="$INSTALL_DIR/.${BINARY_NAME}.installed_version" -[ -x "$BINARY" ] && [ "$(cat "$GUARD" 2>/dev/null)" = "$WANT" ] && exit 0 +$RESIDENT_VERIFIED && [ "$(cat "$GUARD" 2>/dev/null)" = "$WANT" ] && exit 0 mkdir -p "$INSTALL_DIR" 2>/dev/null || soft_exit "cannot create $INSTALL_DIR — skipping auto-install" TMP=$(mktemp "$INSTALL_DIR/.${BINARY_NAME}.download.XXXXXX" 2>/dev/null) || soft_exit "mktemp failed — skipping auto-install" @@ -68,7 +84,7 @@ EXPECTED=$(curl -fsSL --proto '=https' --tlsv1.2 --max-time 30 "$URL.sha256" 2>/ || soft_exit "missing/malformed .sha256 asset — refusing to install unverified binary" [[ "$(shasum -a 256 "$TMP" | awk '{print $1}')" == "$EXPECTED" ]] \ || soft_exit "sha256 mismatch — refusing to install" -codesign --verify --strict -R "$REQUIREMENT" "$TMP" 2>/dev/null \ +verify_binary "$TMP" \ || soft_exit "code-signature verification failed (not Developer ID Team 6W377FS7BS) — refusing to install" chmod +x "$TMP" || soft_exit "chmod failed" diff --git a/plugins/macdoc/tests/session-start-reverify.sh b/plugins/macdoc/tests/session-start-reverify.sh new file mode 100644 index 00000000..e1aa34d5 --- /dev/null +++ b/plugins/macdoc/tests/session-start-reverify.sh @@ -0,0 +1,75 @@ +#!/bin/bash + +set -euo pipefail + +ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd) +HOOK="$ROOT/plugins/macdoc/hooks/session-start.sh" +TEST_ROOT=$(mktemp -d "${TMPDIR:-/tmp}/macdoc-session-start-test.XXXXXX") +trap 'rm -rf "$TEST_ROOT"' EXIT + +FAKE_PATH="$TEST_ROOT/fake-path" +INSTALL_DIR="$TEST_ROOT/install" +EVENT_LOG="$TEST_ROOT/events.log" +mkdir -p "$FAKE_PATH" "$INSTALL_DIR" + +cat > "$FAKE_PATH/uname" <<'EOF' +#!/bin/bash +echo arm64 +EOF + +cat > "$FAKE_PATH/codesign" <<'EOF' +#!/bin/bash +echo codesign >> "$EVENT_LOG" +exit "${FAKE_CODESIGN_EXIT:-0}" +EOF + +cat > "$FAKE_PATH/curl" <<'EOF' +#!/bin/bash +echo curl >> "$EVENT_LOG" +exit 22 +EOF + +cat > "$INSTALL_DIR/macdoc" <<'EOF' +#!/bin/bash +echo binary >> "$EVENT_LOG" +echo 'macdoc 0.7.0' +EOF + +chmod +x "$FAKE_PATH/uname" "$FAKE_PATH/codesign" "$FAKE_PATH/curl" "$INSTALL_DIR/macdoc" + +run_hook() { + : > "$EVENT_LOG" + EVENT_LOG="$EVENT_LOG" \ + FAKE_CODESIGN_EXIT="$1" \ + MACDOC_INSTALL_DIR="$INSTALL_DIR" \ + PATH="$FAKE_PATH:$PATH" \ + bash "$HOOK" >/dev/null 2>&1 +} + +run_hook 1 +if grep -qx binary "$EVENT_LOG"; then + echo "FAIL: signature-rejected resident binary was executed" >&2 + exit 1 +fi +[[ "$(head -1 "$EVENT_LOG")" == "codesign" ]] || { + echo "FAIL: resident signature verification was not the first action" >&2 + exit 1 +} +grep -qx curl "$EVENT_LOG" || { + echo "FAIL: rejected resident binary did not force a download attempt" >&2 + exit 1 +} + +run_hook 0 +first_event=$(sed -n '1p' "$EVENT_LOG") +second_event=$(sed -n '2p' "$EVENT_LOG") +[[ "$first_event" == "codesign" && "$second_event" == "binary" ]] || { + echo "FAIL: verified fast path must verify before executing --version; got: $(tr '\n' ' ' < "$EVENT_LOG")" >&2 + exit 1 +} +if grep -qx curl "$EVENT_LOG"; then + echo "FAIL: verified matching binary unexpectedly hit the network" >&2 + exit 1 +fi + +echo "PASS: SessionStart verifies resident binary before execution" From bf1a2332bd880ad45836060ca7f268ee437ee682 Mon Sep 17 00:00:00 2001 From: che cheng Date: Mon, 24 Aug 2026 04:53:04 +0800 Subject: [PATCH 2/6] fix: eliminate resident probe swap window (#161) --- plugins/macdoc/CHANGELOG.md | 4 +- plugins/macdoc/hooks/session-start.sh | 32 ++---- .../macdoc/tests/session-start-reverify.sh | 100 +++++++++++++----- 3 files changed, 82 insertions(+), 54 deletions(-) diff --git a/plugins/macdoc/CHANGELOG.md b/plugins/macdoc/CHANGELOG.md index c5c69424..c2497402 100644 --- a/plugins/macdoc/CHANGELOG.md +++ b/plugins/macdoc/CHANGELOG.md @@ -13,11 +13,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- SessionStart 在執行常駐 `~/bin/macdoc --version` 或採用 sidecar fast path 前,先以 Developer ID requirement 重驗 binary(PsychQuant/macdoc#161)。簽章不符時不執行該檔,改強制嘗試一次下載;下載失敗仍維持 session fail-soft。 +- SessionStart 不再執行常駐 `~/bin/macdoc --version`(PsychQuant/macdoc#161)。它先以固定 `/usr/bin/codesign`(測試可明示 override)重驗 resident bytes,再只讀 installer sidecar 判斷版本;簽章不符、sidecar 缺失或版本不同時改嘗試一次 verified download。這消除驗簽後從可替換路徑執行的 swap window,下載失敗仍維持 session fail-soft。 ### Tests -- 新增 resident binary 對抗測試:偽造正確版本但 codesign 失敗的 binary 不得被執行;合法且版本相符的 binary 必須先驗簽、再執行版本探測,且維持零網路 fast path。 +- 新增 resident binary 對抗測試:簽章不符的 binary 不得被執行且只嘗試一次下載;合法且 sidecar 版本相符的 binary 只驗簽、不執行、不連網;下載候選必須同時通過 release digest 與 codesign 才能安裝。 ## [1.4.0] - 2026-08-19 diff --git a/plugins/macdoc/hooks/session-start.sh b/plugins/macdoc/hooks/session-start.sh index 954d956f..f0d9b4b5 100755 --- a/plugins/macdoc/hooks/session-start.sh +++ b/plugins/macdoc/hooks/session-start.sh @@ -20,12 +20,13 @@ BINARY_NAME="macdoc" INSTALL_DIR="${MACDOC_INSTALL_DIR:-$HOME/bin}" # override for tests BINARY="$INSTALL_DIR/$BINARY_NAME" REQUIREMENT='=anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] exists and certificate leaf[field.1.2.840.113635.100.6.1.13] exists and certificate leaf[subject.OU] = "6W377FS7BS"' +CODESIGN_BIN="${MACDOC_CODESIGN_BIN:-/usr/bin/codesign}" note() { echo "macdoc plugin: $1" >&2; } soft_exit() { note "$1"; exit 0; } # fail-soft: never break session start verify_binary() { - codesign --verify --strict -R "$REQUIREMENT" "$1" 2>/dev/null + "$CODESIGN_BIN" --verify --strict -R "$REQUIREMENT" "$1" 2>/dev/null } [ "$(uname -m)" = "arm64" ] || exit 0 # arm64-only release; Intel builds from source (silent — not an error) @@ -45,31 +46,16 @@ RESIDENT_VERIFIED=false if [ -x "$BINARY" ]; then if verify_binary "$BINARY"; then RESIDENT_VERIFIED=true - else - note "existing binary failed signature verification — forcing one re-download attempt" fi fi -# --version with a 5s alarm (a hung/planted binary must not stall every -# session start — fail-soft covers errors, not hangs; codex V114 HIGH-1). -# Probe writes to a FILE, not a pipe: a killed probe may leave grandchildren -# holding an inherited pipe fd, and command substitution would then wait on -# the pipe far past the alarm (empirically reproduced with a sleep-300 fake). -# Normalize to the semver token so banner-style output doesn't force a -# re-download loop (codex V114 M-2). -HAVE="" -PROBE=$(mktemp "${TMPDIR:-/tmp}/.macdoc.probe.XXXXXX" 2>/dev/null) || PROBE="" -if $RESIDENT_VERIFIED && [ -n "$PROBE" ]; then - { perl -e 'alarm 5; exec @ARGV' -- "$BINARY" --version "$PROBE" 2>/dev/null; } 2>/dev/null || true - HAVE=$(head -1 "$PROBE" 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true) - rm -f "$PROBE" -fi -$RESIDENT_VERIFIED && [ "$HAVE" = "$WANT" ] && exit 0 # verified fast path: version matches, zero network - -# Loop-guard sidecar: if a previous session already installed WANT but the -# binary self-reports an unparsable/odd version, do not re-download forever. +# SessionStart never executes the resident binary. Its version comes only from +# the installer-written sidecar, after the resident bytes pass codesign. This +# removes the verify-then-exec path-swap window; missing/stale sidecars cause a +# verified replacement download rather than probing untrusted executable code. GUARD="$INSTALL_DIR/.${BINARY_NAME}.installed_version" -$RESIDENT_VERIFIED && [ "$(cat "$GUARD" 2>/dev/null)" = "$WANT" ] && exit 0 +HAVE=$(tr -d '[:space:]' < "$GUARD" 2>/dev/null || true) +$RESIDENT_VERIFIED && [ "$HAVE" = "$WANT" ] && exit 0 mkdir -p "$INSTALL_DIR" 2>/dev/null || soft_exit "cannot create $INSTALL_DIR — skipping auto-install" TMP=$(mktemp "$INSTALL_DIR/.${BINARY_NAME}.download.XXXXXX" 2>/dev/null) || soft_exit "mktemp failed — skipping auto-install" @@ -77,7 +63,7 @@ trap 'rm -f "$TMP"' EXIT URL="https://github.com/$REPO/releases/download/v$WANT/$BINARY_NAME" curl -fsSL --proto '=https' --tlsv1.2 --max-time 300 "$URL" -o "$TMP" 2>/dev/null \ - || soft_exit "download failed for v$WANT (keeping existing ${HAVE:-none}); manual: https://github.com/$REPO/releases" + || soft_exit "download failed for v$WANT; resident binary was not executed. Manual: https://github.com/$REPO/releases" EXPECTED=$(curl -fsSL --proto '=https' --tlsv1.2 --max-time 30 "$URL.sha256" 2>/dev/null | head -1 | awk '{print $1}') [[ "$EXPECTED" =~ ^[0-9a-fA-F]{64}$ ]] \ diff --git a/plugins/macdoc/tests/session-start-reverify.sh b/plugins/macdoc/tests/session-start-reverify.sh index e1aa34d5..44d3730f 100644 --- a/plugins/macdoc/tests/session-start-reverify.sh +++ b/plugins/macdoc/tests/session-start-reverify.sh @@ -10,6 +10,9 @@ trap 'rm -rf "$TEST_ROOT"' EXIT FAKE_PATH="$TEST_ROOT/fake-path" INSTALL_DIR="$TEST_ROOT/install" EVENT_LOG="$TEST_ROOT/events.log" +RESIDENT="$INSTALL_DIR/macdoc" +GUARD="$INSTALL_DIR/.macdoc.installed_version" +CANDIDATE="$TEST_ROOT/candidate-macdoc" mkdir -p "$FAKE_PATH" "$INSTALL_DIR" cat > "$FAKE_PATH/uname" <<'EOF' @@ -19,57 +22,96 @@ EOF cat > "$FAKE_PATH/codesign" <<'EOF' #!/bin/bash -echo codesign >> "$EVENT_LOG" -exit "${FAKE_CODESIGN_EXIT:-0}" +target="" +for target in "$@"; do :; done +echo "codesign:$target" >> "$EVENT_LOG" +if [ "$target" = "$RESIDENT_PATH" ]; then + exit "${RESIDENT_CODESIGN_EXIT:-0}" +fi +exit "${DOWNLOAD_CODESIGN_EXIT:-0}" EOF cat > "$FAKE_PATH/curl" <<'EOF' #!/bin/bash -echo curl >> "$EVENT_LOG" -exit 22 +if [ "${FAKE_CURL_MODE:-fail}" != "success" ]; then + echo curl-download >> "$EVENT_LOG" + exit 22 +fi + +output="" +previous="" +for argument in "$@"; do + if [ "$previous" = "-o" ]; then output="$argument"; fi + previous="$argument" +done + +if [ -n "$output" ]; then + echo curl-download >> "$EVENT_LOG" + cp "$DOWNLOAD_SOURCE" "$output" +else + echo curl-sha >> "$EVENT_LOG" + shasum -a 256 "$DOWNLOAD_SOURCE" | awk '{print $1}' +fi +EOF + +cat > "$RESIDENT" <<'EOF' +#!/bin/bash +echo resident-executed >> "$EVENT_LOG" +echo 'macdoc 0.7.0' EOF -cat > "$INSTALL_DIR/macdoc" <<'EOF' +cat > "$CANDIDATE" <<'EOF' #!/bin/bash -echo binary >> "$EVENT_LOG" +echo candidate-executed >> "$EVENT_LOG" echo 'macdoc 0.7.0' EOF -chmod +x "$FAKE_PATH/uname" "$FAKE_PATH/codesign" "$FAKE_PATH/curl" "$INSTALL_DIR/macdoc" +chmod +x "$FAKE_PATH/uname" "$FAKE_PATH/codesign" "$FAKE_PATH/curl" "$RESIDENT" "$CANDIDATE" run_hook() { : > "$EVENT_LOG" EVENT_LOG="$EVENT_LOG" \ - FAKE_CODESIGN_EXIT="$1" \ + RESIDENT_PATH="$RESIDENT" \ + RESIDENT_CODESIGN_EXIT="$1" \ + DOWNLOAD_CODESIGN_EXIT="$2" \ + FAKE_CURL_MODE="$3" \ + DOWNLOAD_SOURCE="$CANDIDATE" \ + MACDOC_CODESIGN_BIN="$FAKE_PATH/codesign" \ MACDOC_INSTALL_DIR="$INSTALL_DIR" \ PATH="$FAKE_PATH:$PATH" \ bash "$HOOK" >/dev/null 2>&1 } -run_hook 1 -if grep -qx binary "$EVENT_LOG"; then - echo "FAIL: signature-rejected resident binary was executed" >&2 - exit 1 -fi -[[ "$(head -1 "$EVENT_LOG")" == "codesign" ]] || { - echo "FAIL: resident signature verification was not the first action" >&2 - exit 1 -} -grep -qx curl "$EVENT_LOG" || { - echo "FAIL: rejected resident binary did not force a download attempt" >&2 +# A rejected resident must not execute, even if it prints the pinned version. +rm -f "$GUARD" +run_hook 1 0 fail +grep -qx "codesign:$RESIDENT" "$EVENT_LOG" +! grep -q 'executed' "$EVENT_LOG" +[[ "$(grep -c '^curl-download$' "$EVENT_LOG")" -eq 1 ]] || { + echo "FAIL: rejected resident must force exactly one download attempt" >&2 exit 1 } -run_hook 0 -first_event=$(sed -n '1p' "$EVENT_LOG") -second_event=$(sed -n '2p' "$EVENT_LOG") -[[ "$first_event" == "codesign" && "$second_event" == "binary" ]] || { - echo "FAIL: verified fast path must verify before executing --version; got: $(tr '\n' ' ' < "$EVENT_LOG")" >&2 +# A verified resident with a matching installer sidecar takes a zero-network +# fast path without executing the binary during SessionStart. +echo 0.7.0 > "$GUARD" +run_hook 0 0 fail +[[ "$(wc -l < "$EVENT_LOG" | tr -d ' ')" -eq 1 ]] || { + echo "FAIL: verified matching resident should only be signature-checked; got: $(tr '\n' ' ' < "$EVENT_LOG")" >&2 exit 1 } -if grep -qx curl "$EVENT_LOG"; then - echo "FAIL: verified matching binary unexpectedly hit the network" >&2 - exit 1 -fi +grep -qx "codesign:$RESIDENT" "$EVENT_LOG" +! grep -q 'executed\|curl-' "$EVENT_LOG" + +# A rejected resident can be replaced only by bytes whose release digest and +# signature both pass. The candidate is installed but never executed by hook. +run_hook 1 0 success +grep -qx "codesign:$RESIDENT" "$EVENT_LOG" +grep -qx curl-download "$EVENT_LOG" +grep -qx curl-sha "$EVENT_LOG" +[[ "$(grep -c '^codesign:' "$EVENT_LOG")" -eq 2 ]] +! grep -q 'executed' "$EVENT_LOG" +cmp -s "$RESIDENT" "$CANDIDATE" +[[ "$(cat "$GUARD")" = "0.7.0" ]] -echo "PASS: SessionStart verifies resident binary before execution" +echo "PASS: SessionStart never executes resident binary and installs only a verified candidate" From e278d12414a646e7c0474ca8d4308bb58cfd4d06 Mon Sep 17 00:00:00 2001 From: che cheng Date: Mon, 24 Aug 2026 04:56:03 +0800 Subject: [PATCH 3/6] fix: harden SessionStart verifier boundary (#161) --- plugins/macdoc/CHANGELOG.md | 2 +- plugins/macdoc/hooks/session-start.sh | 2 +- .../macdoc/tests/session-start-reverify.sh | 92 ++++++++++++------- 3 files changed, 62 insertions(+), 34 deletions(-) diff --git a/plugins/macdoc/CHANGELOG.md b/plugins/macdoc/CHANGELOG.md index c2497402..b29cd6ed 100644 --- a/plugins/macdoc/CHANGELOG.md +++ b/plugins/macdoc/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- SessionStart 不再執行常駐 `~/bin/macdoc --version`(PsychQuant/macdoc#161)。它先以固定 `/usr/bin/codesign`(測試可明示 override)重驗 resident bytes,再只讀 installer sidecar 判斷版本;簽章不符、sidecar 缺失或版本不同時改嘗試一次 verified download。這消除驗簽後從可替換路徑執行的 swap window,下載失敗仍維持 session fail-soft。 +- SessionStart 不再執行常駐 `~/bin/macdoc --version`(PsychQuant/macdoc#161)。它先以固定 `/usr/bin/codesign` 重驗 resident bytes,再只讀 installer sidecar 判斷版本;簽章不符、sidecar 缺失或版本不同時改嘗試一次 verified download。這消除驗簽後從可替換路徑執行的 swap window,下載失敗仍維持 session fail-soft。 ### Tests diff --git a/plugins/macdoc/hooks/session-start.sh b/plugins/macdoc/hooks/session-start.sh index f0d9b4b5..48fb708a 100755 --- a/plugins/macdoc/hooks/session-start.sh +++ b/plugins/macdoc/hooks/session-start.sh @@ -20,7 +20,7 @@ BINARY_NAME="macdoc" INSTALL_DIR="${MACDOC_INSTALL_DIR:-$HOME/bin}" # override for tests BINARY="$INSTALL_DIR/$BINARY_NAME" REQUIREMENT='=anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] exists and certificate leaf[field.1.2.840.113635.100.6.1.13] exists and certificate leaf[subject.OU] = "6W377FS7BS"' -CODESIGN_BIN="${MACDOC_CODESIGN_BIN:-/usr/bin/codesign}" +CODESIGN_BIN="/usr/bin/codesign" note() { echo "macdoc plugin: $1" >&2; } soft_exit() { note "$1"; exit 0; } # fail-soft: never break session start diff --git a/plugins/macdoc/tests/session-start-reverify.sh b/plugins/macdoc/tests/session-start-reverify.sh index 44d3730f..3103c1dd 100644 --- a/plugins/macdoc/tests/session-start-reverify.sh +++ b/plugins/macdoc/tests/session-start-reverify.sh @@ -12,7 +12,9 @@ INSTALL_DIR="$TEST_ROOT/install" EVENT_LOG="$TEST_ROOT/events.log" RESIDENT="$INSTALL_DIR/macdoc" GUARD="$INSTALL_DIR/.macdoc.installed_version" -CANDIDATE="$TEST_ROOT/candidate-macdoc" +UNSIGNED_CANDIDATE="$TEST_ROOT/unsigned-candidate" +SIGNED_FIXTURE="${MACDOC_SIGNED_FIXTURE:-$HOME/bin/macdoc}" +REQUIREMENT='=anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] exists and certificate leaf[field.1.2.840.113635.100.6.1.13] exists and certificate leaf[subject.OU] = "6W377FS7BS"' mkdir -p "$FAKE_PATH" "$INSTALL_DIR" cat > "$FAKE_PATH/uname" <<'EOF' @@ -20,15 +22,12 @@ cat > "$FAKE_PATH/uname" <<'EOF' echo arm64 EOF +# This deliberate bypass attempt must be ignored by production. Tests set the +# old override variable, but SessionStart must still use /usr/bin/codesign. cat > "$FAKE_PATH/codesign" <<'EOF' #!/bin/bash -target="" -for target in "$@"; do :; done -echo "codesign:$target" >> "$EVENT_LOG" -if [ "$target" = "$RESIDENT_PATH" ]; then - exit "${RESIDENT_CODESIGN_EXIT:-0}" -fi -exit "${DOWNLOAD_CODESIGN_EXIT:-0}" +echo fake-codesign >> "$EVENT_LOG" +exit 0 EOF cat > "$FAKE_PATH/curl" <<'EOF' @@ -60,58 +59,87 @@ echo resident-executed >> "$EVENT_LOG" echo 'macdoc 0.7.0' EOF -cat > "$CANDIDATE" <<'EOF' +cat > "$UNSIGNED_CANDIDATE" <<'EOF' #!/bin/bash echo candidate-executed >> "$EVENT_LOG" echo 'macdoc 0.7.0' EOF -chmod +x "$FAKE_PATH/uname" "$FAKE_PATH/codesign" "$FAKE_PATH/curl" "$RESIDENT" "$CANDIDATE" +chmod +x "$FAKE_PATH/uname" "$FAKE_PATH/codesign" "$FAKE_PATH/curl" "$RESIDENT" "$UNSIGNED_CANDIDATE" run_hook() { : > "$EVENT_LOG" EVENT_LOG="$EVENT_LOG" \ - RESIDENT_PATH="$RESIDENT" \ - RESIDENT_CODESIGN_EXIT="$1" \ - DOWNLOAD_CODESIGN_EXIT="$2" \ - FAKE_CURL_MODE="$3" \ - DOWNLOAD_SOURCE="$CANDIDATE" \ + FAKE_CURL_MODE="$1" \ + DOWNLOAD_SOURCE="$2" \ MACDOC_CODESIGN_BIN="$FAKE_PATH/codesign" \ MACDOC_INSTALL_DIR="$INSTALL_DIR" \ PATH="$FAKE_PATH:$PATH" \ bash "$HOOK" >/dev/null 2>&1 } -# A rejected resident must not execute, even if it prints the pinned version. -rm -f "$GUARD" -run_hook 1 0 fail -grep -qx "codesign:$RESIDENT" "$EVENT_LOG" -! grep -q 'executed' "$EVENT_LOG" +assert_no_execution() { + if grep -q 'executed' "$EVENT_LOG"; then + echo "FAIL: SessionStart executed resident or candidate: $(tr '\n' ' ' < "$EVENT_LOG")" >&2 + exit 1 + fi +} + +# A rejected resident must ignore a hostile verifier override, never execute, +# and force exactly one download attempt even if its sidecar claims WANT. +echo 0.7.0 > "$GUARD" +run_hook fail "$UNSIGNED_CANDIDATE" +assert_no_execution +if grep -qx fake-codesign "$EVENT_LOG"; then + echo "FAIL: production honored MACDOC_CODESIGN_BIN instead of /usr/bin/codesign" >&2 + exit 1 +fi [[ "$(grep -c '^curl-download$' "$EVENT_LOG")" -eq 1 ]] || { echo "FAIL: rejected resident must force exactly one download attempt" >&2 exit 1 } +# An unsigned downloaded candidate may match its release digest, but codesign +# must still reject it and leave the resident/sidecar unchanged. +rm -f "$GUARD" +run_hook success "$UNSIGNED_CANDIDATE" +assert_no_execution +[[ ! -f "$GUARD" ]] +grep -qx curl-download "$EVENT_LOG" +grep -qx curl-sha "$EVENT_LOG" +grep -q 'resident-executed' "$RESIDENT" + +# Full valid-path coverage needs a real Team-signed fixture. Keep the hostile +# cases above mandatory; gate only the positive cases for CI machines without +# the released binary. +if ! /usr/bin/codesign --verify --strict -R "$REQUIREMENT" "$SIGNED_FIXTURE" 2>/dev/null; then + echo "SKIP: positive signed-fixture cases (set MACDOC_SIGNED_FIXTURE)" + echo "PASS: rejected resident/candidate are never executed" + exit 0 +fi + # A verified resident with a matching installer sidecar takes a zero-network # fast path without executing the binary during SessionStart. +cp "$SIGNED_FIXTURE" "$RESIDENT" +chmod +x "$RESIDENT" echo 0.7.0 > "$GUARD" -run_hook 0 0 fail -[[ "$(wc -l < "$EVENT_LOG" | tr -d ' ')" -eq 1 ]] || { - echo "FAIL: verified matching resident should only be signature-checked; got: $(tr '\n' ' ' < "$EVENT_LOG")" >&2 +run_hook fail "$SIGNED_FIXTURE" +assert_no_execution +[[ ! -s "$EVENT_LOG" ]] || { + echo "FAIL: verified matching resident should not hit test doubles: $(tr '\n' ' ' < "$EVENT_LOG")" >&2 exit 1 } -grep -qx "codesign:$RESIDENT" "$EVENT_LOG" -! grep -q 'executed\|curl-' "$EVENT_LOG" -# A rejected resident can be replaced only by bytes whose release digest and -# signature both pass. The candidate is installed but never executed by hook. -run_hook 1 0 success -grep -qx "codesign:$RESIDENT" "$EVENT_LOG" +# A rejected resident can be replaced only by signed bytes whose release +# digest matches. The candidate is installed but never executed by the hook. +cp "$UNSIGNED_CANDIDATE" "$RESIDENT" +chmod +x "$RESIDENT" +rm -f "$GUARD" +run_hook success "$SIGNED_FIXTURE" +assert_no_execution grep -qx curl-download "$EVENT_LOG" grep -qx curl-sha "$EVENT_LOG" -[[ "$(grep -c '^codesign:' "$EVENT_LOG")" -eq 2 ]] -! grep -q 'executed' "$EVENT_LOG" -cmp -s "$RESIDENT" "$CANDIDATE" +cmp -s "$RESIDENT" "$SIGNED_FIXTURE" [[ "$(cat "$GUARD")" = "0.7.0" ]] echo "PASS: SessionStart never executes resident binary and installs only a verified candidate" From a58884d892215d2b2f33e0862d448a086734b308 Mon Sep 17 00:00:00 2001 From: che cheng Date: Mon, 24 Aug 2026 05:01:11 +0800 Subject: [PATCH 4/6] fix: pin exact macdoc release bytes in hook (#161) --- .claude-plugin/marketplace.json | 3 ++- plugins/macdoc/.claude-plugin/plugin.json | 1 + plugins/macdoc/CHANGELOG.md | 4 +-- plugins/macdoc/hooks/session-start.sh | 11 ++++++-- .../macdoc/tests/session-start-reverify.sh | 25 +++++++++++++------ 5 files changed, 31 insertions(+), 13 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 711aa435..29c57193 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -27,7 +27,8 @@ }, "source": "./plugins/macdoc", "category": "productivity", - "binary_version": "0.7.0" + "binary_version": "0.7.0", + "binary_sha256": "9fe09f26b6c8f97f13520ec2f618b8918b475de9ebc8a9980fd64897b39ec298" }, { "name": "che-pdf-mcp", diff --git a/plugins/macdoc/.claude-plugin/plugin.json b/plugins/macdoc/.claude-plugin/plugin.json index 2ca235c9..dbd3dec9 100644 --- a/plugins/macdoc/.claude-plugin/plugin.json +++ b/plugins/macdoc/.claude-plugin/plugin.json @@ -3,6 +3,7 @@ "description": "macOS 原生文件處理 CLI — 格式轉換、VLM OCR(含 host profile 設定)、SRT 處理。v1.2.0: session-start hook 自動安裝 signed CLI binary(arm64)。", "version": "1.4.1", "binary_version": "0.7.0", + "binary_sha256": "9fe09f26b6c8f97f13520ec2f618b8918b475de9ebc8a9980fd64897b39ec298", "author": { "name": "Che Cheng" } diff --git a/plugins/macdoc/CHANGELOG.md b/plugins/macdoc/CHANGELOG.md index b29cd6ed..20f9e50a 100644 --- a/plugins/macdoc/CHANGELOG.md +++ b/plugins/macdoc/CHANGELOG.md @@ -13,11 +13,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- SessionStart 不再執行常駐 `~/bin/macdoc --version`(PsychQuant/macdoc#161)。它先以固定 `/usr/bin/codesign` 重驗 resident bytes,再只讀 installer sidecar 判斷版本;簽章不符、sidecar 缺失或版本不同時改嘗試一次 verified download。這消除驗簽後從可替換路徑執行的 swap window,下載失敗仍維持 session fail-soft。 +- SessionStart 不再執行常駐 `~/bin/macdoc --version`(PsychQuant/macdoc#161)。它先以固定 `/usr/bin/codesign` 與 plugin-pinned `binary_sha256` 重驗 exact release bytes,再只讀 installer sidecar 判斷版本;簽章/digest 不符、sidecar 缺失或版本不同時改嘗試一次 verified download。下載的 `.sha256` asset 也必須等於 plugin pin。這消除驗簽後從可替換路徑執行的 swap window,下載失敗仍維持 session fail-soft。 ### Tests -- 新增 resident binary 對抗測試:簽章不符的 binary 不得被執行且只嘗試一次下載;合法且 sidecar 版本相符的 binary 只驗簽、不執行、不連網;下載候選必須同時通過 release digest 與 codesign 才能安裝。 +- 新增 resident binary 對抗測試:簽章不符的 binary 不得被執行且只嘗試一次下載;合法且 digest/sidecar 相符的 binary 只驗簽、不執行、不連網;錯誤 release digest 必須在安裝前拒絕;候選必須同時通過 plugin pin、release digest 與 codesign 才能安裝。 ## [1.4.0] - 2026-08-19 diff --git a/plugins/macdoc/hooks/session-start.sh b/plugins/macdoc/hooks/session-start.sh index 48fb708a..46c0bbf2 100755 --- a/plugins/macdoc/hooks/session-start.sh +++ b/plugins/macdoc/hooks/session-start.sh @@ -38,6 +38,10 @@ PLUGIN_JSON="$PLUGIN_ROOT/.claude-plugin/plugin.json" WANT=$(grep -oE '"binary_version"[[:space:]]*:[[:space:]]*"[^"]+"' "$PLUGIN_JSON" 2>/dev/null \ | head -1 | sed -E 's/.*"([^"]+)"$/\1/' || true) [ -n "$WANT" ] || exit 0 # no pinned CLI version — nothing to manage +WANT_SHA=$(grep -oE '"binary_sha256"[[:space:]]*:[[:space:]]*"[^"]+"' "$PLUGIN_JSON" 2>/dev/null \ + | head -1 | sed -E 's/.*"([^"]+)"$/\1/' || true) +[[ "$WANT_SHA" =~ ^[0-9a-fA-F]{64}$ ]] \ + || soft_exit "missing/malformed binary_sha256 in plugin.json — refusing to trust or install a resident binary" # Exec-time re-verification happens before even asking the resident binary for # its version. A binary that merely prints WANT must never reach a fast path @@ -45,7 +49,8 @@ WANT=$(grep -oE '"binary_version"[[:space:]]*:[[:space:]]*"[^"]+"' "$PLUGIN_JSON RESIDENT_VERIFIED=false if [ -x "$BINARY" ]; then if verify_binary "$BINARY"; then - RESIDENT_VERIFIED=true + RESIDENT_SHA=$(/usr/bin/shasum -a 256 "$BINARY" 2>/dev/null | awk '{print $1}') + [ "$RESIDENT_SHA" = "$WANT_SHA" ] && RESIDENT_VERIFIED=true fi fi @@ -68,7 +73,9 @@ curl -fsSL --proto '=https' --tlsv1.2 --max-time 300 "$URL" -o "$TMP" 2>/dev/nul EXPECTED=$(curl -fsSL --proto '=https' --tlsv1.2 --max-time 30 "$URL.sha256" 2>/dev/null | head -1 | awk '{print $1}') [[ "$EXPECTED" =~ ^[0-9a-fA-F]{64}$ ]] \ || soft_exit "missing/malformed .sha256 asset — refusing to install unverified binary" -[[ "$(shasum -a 256 "$TMP" | awk '{print $1}')" == "$EXPECTED" ]] \ +[[ "$EXPECTED" == "$WANT_SHA" ]] \ + || soft_exit "release sha256 asset does not match pinned binary_sha256 — refusing to install" +[[ "$(/usr/bin/shasum -a 256 "$TMP" | awk '{print $1}')" == "$WANT_SHA" ]] \ || soft_exit "sha256 mismatch — refusing to install" verify_binary "$TMP" \ || soft_exit "code-signature verification failed (not Developer ID Team 6W377FS7BS) — refusing to install" diff --git a/plugins/macdoc/tests/session-start-reverify.sh b/plugins/macdoc/tests/session-start-reverify.sh index 3103c1dd..bf6f44a7 100644 --- a/plugins/macdoc/tests/session-start-reverify.sh +++ b/plugins/macdoc/tests/session-start-reverify.sh @@ -10,6 +10,7 @@ trap 'rm -rf "$TEST_ROOT"' EXIT FAKE_PATH="$TEST_ROOT/fake-path" INSTALL_DIR="$TEST_ROOT/install" EVENT_LOG="$TEST_ROOT/events.log" +HOOK_STDERR="$TEST_ROOT/hook.stderr" RESIDENT="$INSTALL_DIR/macdoc" GUARD="$INSTALL_DIR/.macdoc.installed_version" UNSIGNED_CANDIDATE="$TEST_ROOT/unsigned-candidate" @@ -49,7 +50,11 @@ if [ -n "$output" ]; then cp "$DOWNLOAD_SOURCE" "$output" else echo curl-sha >> "$EVENT_LOG" - shasum -a 256 "$DOWNLOAD_SOURCE" | awk '{print $1}' + if [ "${FAKE_SHA_MODE:-actual}" = "wrong" ]; then + echo 0000000000000000000000000000000000000000000000000000000000000000 + else + shasum -a 256 "$DOWNLOAD_SOURCE" | awk '{print $1}' + fi fi EOF @@ -72,10 +77,11 @@ run_hook() { EVENT_LOG="$EVENT_LOG" \ FAKE_CURL_MODE="$1" \ DOWNLOAD_SOURCE="$2" \ + FAKE_SHA_MODE="$3" \ MACDOC_CODESIGN_BIN="$FAKE_PATH/codesign" \ MACDOC_INSTALL_DIR="$INSTALL_DIR" \ PATH="$FAKE_PATH:$PATH" \ - bash "$HOOK" >/dev/null 2>&1 + bash "$HOOK" >/dev/null 2>"$HOOK_STDERR" } assert_no_execution() { @@ -88,7 +94,7 @@ assert_no_execution() { # A rejected resident must ignore a hostile verifier override, never execute, # and force exactly one download attempt even if its sidecar claims WANT. echo 0.7.0 > "$GUARD" -run_hook fail "$UNSIGNED_CANDIDATE" +run_hook fail "$UNSIGNED_CANDIDATE" actual assert_no_execution if grep -qx fake-codesign "$EVENT_LOG"; then echo "FAIL: production honored MACDOC_CODESIGN_BIN instead of /usr/bin/codesign" >&2 @@ -99,15 +105,18 @@ fi exit 1 } -# An unsigned downloaded candidate may match its release digest, but codesign -# must still reject it and leave the resident/sidecar unchanged. +# A mismatched release digest must be rejected before candidate installation. rm -f "$GUARD" -run_hook success "$UNSIGNED_CANDIDATE" +run_hook success "$UNSIGNED_CANDIDATE" wrong assert_no_execution [[ ! -f "$GUARD" ]] grep -qx curl-download "$EVENT_LOG" grep -qx curl-sha "$EVENT_LOG" grep -q 'resident-executed' "$RESIDENT" +grep -q 'release sha256 asset does not match pinned binary_sha256' "$HOOK_STDERR" || { + echo "FAIL: mismatched release digest did not trip the pinned-SHA gate" >&2 + exit 1 +} # Full valid-path coverage needs a real Team-signed fixture. Keep the hostile # cases above mandatory; gate only the positive cases for CI machines without @@ -123,7 +132,7 @@ fi cp "$SIGNED_FIXTURE" "$RESIDENT" chmod +x "$RESIDENT" echo 0.7.0 > "$GUARD" -run_hook fail "$SIGNED_FIXTURE" +run_hook fail "$SIGNED_FIXTURE" actual assert_no_execution [[ ! -s "$EVENT_LOG" ]] || { echo "FAIL: verified matching resident should not hit test doubles: $(tr '\n' ' ' < "$EVENT_LOG")" >&2 @@ -135,7 +144,7 @@ assert_no_execution cp "$UNSIGNED_CANDIDATE" "$RESIDENT" chmod +x "$RESIDENT" rm -f "$GUARD" -run_hook success "$SIGNED_FIXTURE" +run_hook success "$SIGNED_FIXTURE" actual assert_no_execution grep -qx curl-download "$EVENT_LOG" grep -qx curl-sha "$EVENT_LOG" From 98acc5bcfba323a24e603bc91356ee4cd700b37d Mon Sep 17 00:00:00 2001 From: che cheng Date: Mon, 24 Aug 2026 05:04:10 +0800 Subject: [PATCH 5/6] fix: isolate SessionStart trust-chain tools (#161) --- plugins/macdoc/CHANGELOG.md | 2 +- plugins/macdoc/hooks/session-start.sh | 16 ++++++++---- .../macdoc/tests/session-start-reverify.sh | 25 +++++++++++++++---- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/plugins/macdoc/CHANGELOG.md b/plugins/macdoc/CHANGELOG.md index 20f9e50a..5e20cbc5 100644 --- a/plugins/macdoc/CHANGELOG.md +++ b/plugins/macdoc/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- SessionStart 不再執行常駐 `~/bin/macdoc --version`(PsychQuant/macdoc#161)。它先以固定 `/usr/bin/codesign` 與 plugin-pinned `binary_sha256` 重驗 exact release bytes,再只讀 installer sidecar 判斷版本;簽章/digest 不符、sidecar 缺失或版本不同時改嘗試一次 verified download。下載的 `.sha256` asset 也必須等於 plugin pin。這消除驗簽後從可替換路徑執行的 swap window,下載失敗仍維持 session fail-soft。 +- SessionStart 不再執行常駐 `~/bin/macdoc --version`(PsychQuant/macdoc#161)。它先固定 system PATH,再以 `/usr/bin/codesign` 與 plugin-pinned `binary_sha256` 重驗 exact release bytes,只讀 installer sidecar 判斷版本;簽章/digest 不符、sidecar 缺失或版本不同時改嘗試一次 verified download。下載的 `.sha256` asset 也必須等於 plugin pin。這消除驗簽後從可替換路徑執行的 swap window,也避免 user-writable `~/bin` 劫持 trust-chain 工具;下載失敗仍維持 session fail-soft。 ### Tests diff --git a/plugins/macdoc/hooks/session-start.sh b/plugins/macdoc/hooks/session-start.sh index 46c0bbf2..9bed612e 100755 --- a/plugins/macdoc/hooks/session-start.sh +++ b/plugins/macdoc/hooks/session-start.sh @@ -14,6 +14,11 @@ set -u +# The install target commonly lives in ~/bin, which may also lead PATH. Never +# resolve trust-chain utilities through a user-writable directory (#161). +PATH="/usr/bin:/bin:/usr/sbin:/sbin" +export PATH + REPO="PsychQuant/macdoc" BINARY_NAME="macdoc" [ -n "${HOME:-}" ] || exit 0 # no HOME (exotic env) — nothing sane to do, never break session @@ -21,6 +26,7 @@ INSTALL_DIR="${MACDOC_INSTALL_DIR:-$HOME/bin}" # override for tests BINARY="$INSTALL_DIR/$BINARY_NAME" REQUIREMENT='=anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] exists and certificate leaf[field.1.2.840.113635.100.6.1.13] exists and certificate leaf[subject.OU] = "6W377FS7BS"' CODESIGN_BIN="/usr/bin/codesign" +CURL_BIN="${MACDOC_CURL_BIN:-/usr/bin/curl}" note() { echo "macdoc plugin: $1" >&2; } soft_exit() { note "$1"; exit 0; } # fail-soft: never break session start @@ -29,7 +35,7 @@ verify_binary() { "$CODESIGN_BIN" --verify --strict -R "$REQUIREMENT" "$1" 2>/dev/null } -[ "$(uname -m)" = "arm64" ] || exit 0 # arm64-only release; Intel builds from source (silent — not an error) +[ "$(/usr/bin/uname -m)" = "arm64" ] || exit 0 # arm64-only release; Intel builds from source (silent — not an error) PLUGIN_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" PLUGIN_JSON="$PLUGIN_ROOT/.claude-plugin/plugin.json" @@ -49,7 +55,7 @@ WANT_SHA=$(grep -oE '"binary_sha256"[[:space:]]*:[[:space:]]*"[^"]+"' "$PLUGIN_J RESIDENT_VERIFIED=false if [ -x "$BINARY" ]; then if verify_binary "$BINARY"; then - RESIDENT_SHA=$(/usr/bin/shasum -a 256 "$BINARY" 2>/dev/null | awk '{print $1}') + RESIDENT_SHA=$(/usr/bin/shasum -a 256 "$BINARY" 2>/dev/null | /usr/bin/awk '{print $1}') [ "$RESIDENT_SHA" = "$WANT_SHA" ] && RESIDENT_VERIFIED=true fi fi @@ -67,15 +73,15 @@ TMP=$(mktemp "$INSTALL_DIR/.${BINARY_NAME}.download.XXXXXX" 2>/dev/null) || soft trap 'rm -f "$TMP"' EXIT URL="https://github.com/$REPO/releases/download/v$WANT/$BINARY_NAME" -curl -fsSL --proto '=https' --tlsv1.2 --max-time 300 "$URL" -o "$TMP" 2>/dev/null \ +"$CURL_BIN" -fsSL --proto '=https' --tlsv1.2 --max-time 300 "$URL" -o "$TMP" 2>/dev/null \ || soft_exit "download failed for v$WANT; resident binary was not executed. Manual: https://github.com/$REPO/releases" -EXPECTED=$(curl -fsSL --proto '=https' --tlsv1.2 --max-time 30 "$URL.sha256" 2>/dev/null | head -1 | awk '{print $1}') +EXPECTED=$("$CURL_BIN" -fsSL --proto '=https' --tlsv1.2 --max-time 30 "$URL.sha256" 2>/dev/null | /usr/bin/head -1 | /usr/bin/awk '{print $1}') [[ "$EXPECTED" =~ ^[0-9a-fA-F]{64}$ ]] \ || soft_exit "missing/malformed .sha256 asset — refusing to install unverified binary" [[ "$EXPECTED" == "$WANT_SHA" ]] \ || soft_exit "release sha256 asset does not match pinned binary_sha256 — refusing to install" -[[ "$(/usr/bin/shasum -a 256 "$TMP" | awk '{print $1}')" == "$WANT_SHA" ]] \ +[[ "$(/usr/bin/shasum -a 256 "$TMP" | /usr/bin/awk '{print $1}')" == "$WANT_SHA" ]] \ || soft_exit "sha256 mismatch — refusing to install" verify_binary "$TMP" \ || soft_exit "code-signature verification failed (not Developer ID Team 6W377FS7BS) — refusing to install" diff --git a/plugins/macdoc/tests/session-start-reverify.sh b/plugins/macdoc/tests/session-start-reverify.sh index bf6f44a7..9c55244e 100644 --- a/plugins/macdoc/tests/session-start-reverify.sh +++ b/plugins/macdoc/tests/session-start-reverify.sh @@ -20,6 +20,7 @@ mkdir -p "$FAKE_PATH" "$INSTALL_DIR" cat > "$FAKE_PATH/uname" <<'EOF' #!/bin/bash +echo fake-uname >> "$EVENT_LOG" echo arm64 EOF @@ -31,6 +32,12 @@ echo fake-codesign >> "$EVENT_LOG" exit 0 EOF +cat > "$FAKE_PATH/awk" <<'EOF' +#!/bin/bash +echo fake-awk >> "$EVENT_LOG" +exec /usr/bin/awk "$@" +EOF + cat > "$FAKE_PATH/curl" <<'EOF' #!/bin/bash if [ "${FAKE_CURL_MODE:-fail}" != "success" ]; then @@ -70,7 +77,7 @@ echo candidate-executed >> "$EVENT_LOG" echo 'macdoc 0.7.0' EOF -chmod +x "$FAKE_PATH/uname" "$FAKE_PATH/codesign" "$FAKE_PATH/curl" "$RESIDENT" "$UNSIGNED_CANDIDATE" +chmod +x "$FAKE_PATH/uname" "$FAKE_PATH/codesign" "$FAKE_PATH/awk" "$FAKE_PATH/curl" "$RESIDENT" "$UNSIGNED_CANDIDATE" run_hook() { : > "$EVENT_LOG" @@ -78,6 +85,7 @@ run_hook() { FAKE_CURL_MODE="$1" \ DOWNLOAD_SOURCE="$2" \ FAKE_SHA_MODE="$3" \ + MACDOC_CURL_BIN="$FAKE_PATH/curl" \ MACDOC_CODESIGN_BIN="$FAKE_PATH/codesign" \ MACDOC_INSTALL_DIR="$INSTALL_DIR" \ PATH="$FAKE_PATH:$PATH" \ @@ -91,15 +99,19 @@ assert_no_execution() { fi } +assert_trust_tools_not_hijacked() { + if grep -q '^fake-\(uname\|awk\|codesign\)$' "$EVENT_LOG"; then + echo "FAIL: SessionStart resolved a trust-chain tool through hostile PATH: $(tr '\n' ' ' < "$EVENT_LOG")" >&2 + exit 1 + fi +} + # A rejected resident must ignore a hostile verifier override, never execute, # and force exactly one download attempt even if its sidecar claims WANT. echo 0.7.0 > "$GUARD" run_hook fail "$UNSIGNED_CANDIDATE" actual assert_no_execution -if grep -qx fake-codesign "$EVENT_LOG"; then - echo "FAIL: production honored MACDOC_CODESIGN_BIN instead of /usr/bin/codesign" >&2 - exit 1 -fi +assert_trust_tools_not_hijacked [[ "$(grep -c '^curl-download$' "$EVENT_LOG")" -eq 1 ]] || { echo "FAIL: rejected resident must force exactly one download attempt" >&2 exit 1 @@ -109,6 +121,7 @@ fi rm -f "$GUARD" run_hook success "$UNSIGNED_CANDIDATE" wrong assert_no_execution +assert_trust_tools_not_hijacked [[ ! -f "$GUARD" ]] grep -qx curl-download "$EVENT_LOG" grep -qx curl-sha "$EVENT_LOG" @@ -134,6 +147,7 @@ chmod +x "$RESIDENT" echo 0.7.0 > "$GUARD" run_hook fail "$SIGNED_FIXTURE" actual assert_no_execution +assert_trust_tools_not_hijacked [[ ! -s "$EVENT_LOG" ]] || { echo "FAIL: verified matching resident should not hit test doubles: $(tr '\n' ' ' < "$EVENT_LOG")" >&2 exit 1 @@ -146,6 +160,7 @@ chmod +x "$RESIDENT" rm -f "$GUARD" run_hook success "$SIGNED_FIXTURE" actual assert_no_execution +assert_trust_tools_not_hijacked grep -qx curl-download "$EVENT_LOG" grep -qx curl-sha "$EVENT_LOG" cmp -s "$RESIDENT" "$SIGNED_FIXTURE" From 8da091b61a61babd8343a6a868c741a9321336db Mon Sep 17 00:00:00 2001 From: che cheng Date: Mon, 24 Aug 2026 05:07:18 +0800 Subject: [PATCH 6/6] test: exercise fixed SessionStart trust chain (#161) --- plugins/macdoc/CHANGELOG.md | 2 +- plugins/macdoc/hooks/session-start-verify.sh | 27 +++ plugins/macdoc/hooks/session-start.sh | 34 ++-- .../macdoc/tests/session-start-reverify.sh | 164 ++++++------------ 4 files changed, 99 insertions(+), 128 deletions(-) create mode 100644 plugins/macdoc/hooks/session-start-verify.sh diff --git a/plugins/macdoc/CHANGELOG.md b/plugins/macdoc/CHANGELOG.md index 5e20cbc5..62713a9a 100644 --- a/plugins/macdoc/CHANGELOG.md +++ b/plugins/macdoc/CHANGELOG.md @@ -17,7 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Tests -- 新增 resident binary 對抗測試:簽章不符的 binary 不得被執行且只嘗試一次下載;合法且 digest/sidecar 相符的 binary 只驗簽、不執行、不連網;錯誤 release digest 必須在安裝前拒絕;候選必須同時通過 plugin pin、release digest 與 codesign 才能安裝。 +- 新增共用驗證 library 與 resident binary 對抗測試:簽章不符的 binary 不得被執行;合法且 digest/sidecar 相符的 binary 只驗簽、不執行、不連網;release-pin、下載 bytes 與 codesign 三個 candidate gate 各有獨立負向測試,並以實際 signed v0.7.0 fixture 驗證正向路徑。 ## [1.4.0] - 2026-08-19 diff --git a/plugins/macdoc/hooks/session-start-verify.sh b/plugins/macdoc/hooks/session-start-verify.sh new file mode 100644 index 00000000..9efaec55 --- /dev/null +++ b/plugins/macdoc/hooks/session-start-verify.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +# Shared verification primitives for the macdoc SessionStart hook. Production +# and tests call these exact functions; trust-chain executables are fixed. + +macdoc_verify_binary() { + /usr/bin/codesign --verify --strict -R "$2" "$1" 2>/dev/null +} + +macdoc_sha256_file() { + /usr/bin/shasum -a 256 "$1" 2>/dev/null | /usr/bin/awk '{print $1}' +} + +# Return codes identify the failed gate without parsing prose: +# 10 = release asset digest differs from plugin pin +# 11 = downloaded bytes differ from plugin pin +# 12 = candidate does not satisfy the Developer ID requirement +macdoc_verify_candidate() { + local candidate=$1 + local release_sha=$2 + local pinned_sha=$3 + local requirement=$4 + + [ "$release_sha" = "$pinned_sha" ] || return 10 + [ "$(macdoc_sha256_file "$candidate")" = "$pinned_sha" ] || return 11 + macdoc_verify_binary "$candidate" "$requirement" || return 12 +} diff --git a/plugins/macdoc/hooks/session-start.sh b/plugins/macdoc/hooks/session-start.sh index 9bed612e..ec8df516 100755 --- a/plugins/macdoc/hooks/session-start.sh +++ b/plugins/macdoc/hooks/session-start.sh @@ -25,21 +25,20 @@ BINARY_NAME="macdoc" INSTALL_DIR="${MACDOC_INSTALL_DIR:-$HOME/bin}" # override for tests BINARY="$INSTALL_DIR/$BINARY_NAME" REQUIREMENT='=anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] exists and certificate leaf[field.1.2.840.113635.100.6.1.13] exists and certificate leaf[subject.OU] = "6W377FS7BS"' -CODESIGN_BIN="/usr/bin/codesign" -CURL_BIN="${MACDOC_CURL_BIN:-/usr/bin/curl}" note() { echo "macdoc plugin: $1" >&2; } soft_exit() { note "$1"; exit 0; } # fail-soft: never break session start -verify_binary() { - "$CODESIGN_BIN" --verify --strict -R "$REQUIREMENT" "$1" 2>/dev/null -} - [ "$(/usr/bin/uname -m)" = "arm64" ] || exit 0 # arm64-only release; Intel builds from source (silent — not an error) PLUGIN_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" PLUGIN_JSON="$PLUGIN_ROOT/.claude-plugin/plugin.json" [ -f "$PLUGIN_JSON" ] || exit 0 +VERIFY_LIB="$PLUGIN_ROOT/hooks/session-start-verify.sh" +[ -f "$VERIFY_LIB" ] || soft_exit "verification library missing — refusing to trust or install a resident binary" +# Runtime path is derived from this plugin root. +# shellcheck disable=SC1090,SC1091 +. "$VERIFY_LIB" WANT=$(grep -oE '"binary_version"[[:space:]]*:[[:space:]]*"[^"]+"' "$PLUGIN_JSON" 2>/dev/null \ | head -1 | sed -E 's/.*"([^"]+)"$/\1/' || true) @@ -54,8 +53,8 @@ WANT_SHA=$(grep -oE '"binary_sha256"[[:space:]]*:[[:space:]]*"[^"]+"' "$PLUGIN_J # unless its Developer ID chain + Team requirement is valid (#161). RESIDENT_VERIFIED=false if [ -x "$BINARY" ]; then - if verify_binary "$BINARY"; then - RESIDENT_SHA=$(/usr/bin/shasum -a 256 "$BINARY" 2>/dev/null | /usr/bin/awk '{print $1}') + if macdoc_verify_binary "$BINARY" "$REQUIREMENT"; then + RESIDENT_SHA=$(macdoc_sha256_file "$BINARY") [ "$RESIDENT_SHA" = "$WANT_SHA" ] && RESIDENT_VERIFIED=true fi fi @@ -73,18 +72,21 @@ TMP=$(mktemp "$INSTALL_DIR/.${BINARY_NAME}.download.XXXXXX" 2>/dev/null) || soft trap 'rm -f "$TMP"' EXIT URL="https://github.com/$REPO/releases/download/v$WANT/$BINARY_NAME" -"$CURL_BIN" -fsSL --proto '=https' --tlsv1.2 --max-time 300 "$URL" -o "$TMP" 2>/dev/null \ +/usr/bin/curl -fsSL --proto '=https' --tlsv1.2 --max-time 300 "$URL" -o "$TMP" 2>/dev/null \ || soft_exit "download failed for v$WANT; resident binary was not executed. Manual: https://github.com/$REPO/releases" -EXPECTED=$("$CURL_BIN" -fsSL --proto '=https' --tlsv1.2 --max-time 30 "$URL.sha256" 2>/dev/null | /usr/bin/head -1 | /usr/bin/awk '{print $1}') +EXPECTED=$(/usr/bin/curl -fsSL --proto '=https' --tlsv1.2 --max-time 30 "$URL.sha256" 2>/dev/null | /usr/bin/head -1 | /usr/bin/awk '{print $1}') [[ "$EXPECTED" =~ ^[0-9a-fA-F]{64}$ ]] \ || soft_exit "missing/malformed .sha256 asset — refusing to install unverified binary" -[[ "$EXPECTED" == "$WANT_SHA" ]] \ - || soft_exit "release sha256 asset does not match pinned binary_sha256 — refusing to install" -[[ "$(/usr/bin/shasum -a 256 "$TMP" | /usr/bin/awk '{print $1}')" == "$WANT_SHA" ]] \ - || soft_exit "sha256 mismatch — refusing to install" -verify_binary "$TMP" \ - || soft_exit "code-signature verification failed (not Developer ID Team 6W377FS7BS) — refusing to install" +macdoc_verify_candidate "$TMP" "$EXPECTED" "$WANT_SHA" "$REQUIREMENT" +CANDIDATE_RC=$? +case "$CANDIDATE_RC" in + 0) ;; + 10) soft_exit "release sha256 asset does not match pinned binary_sha256 — refusing to install" ;; + 11) soft_exit "sha256 mismatch — refusing to install" ;; + 12) soft_exit "code-signature verification failed (not Developer ID Team 6W377FS7BS) — refusing to install" ;; + *) soft_exit "candidate verification failed unexpectedly — refusing to install" ;; +esac chmod +x "$TMP" || soft_exit "chmod failed" mv "$TMP" "$BINARY" || soft_exit "install mv failed" diff --git a/plugins/macdoc/tests/session-start-reverify.sh b/plugins/macdoc/tests/session-start-reverify.sh index 9c55244e..9e6c42a7 100644 --- a/plugins/macdoc/tests/session-start-reverify.sh +++ b/plugins/macdoc/tests/session-start-reverify.sh @@ -4,10 +4,11 @@ set -euo pipefail ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd) HOOK="$ROOT/plugins/macdoc/hooks/session-start.sh" +VERIFY_LIB="$ROOT/plugins/macdoc/hooks/session-start-verify.sh" TEST_ROOT=$(mktemp -d "${TMPDIR:-/tmp}/macdoc-session-start-test.XXXXXX") trap 'rm -rf "$TEST_ROOT"' EXIT -FAKE_PATH="$TEST_ROOT/fake-path" +FAKE_PATH="$TEST_ROOT/hostile-path" INSTALL_DIR="$TEST_ROOT/install" EVENT_LOG="$TEST_ROOT/events.log" HOOK_STDERR="$TEST_ROOT/hook.stderr" @@ -16,55 +17,18 @@ GUARD="$INSTALL_DIR/.macdoc.installed_version" UNSIGNED_CANDIDATE="$TEST_ROOT/unsigned-candidate" SIGNED_FIXTURE="${MACDOC_SIGNED_FIXTURE:-$HOME/bin/macdoc}" REQUIREMENT='=anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] exists and certificate leaf[field.1.2.840.113635.100.6.1.13] exists and certificate leaf[subject.OU] = "6W377FS7BS"' +PINNED_SHA=9fe09f26b6c8f97f13520ec2f618b8918b475de9ebc8a9980fd64897b39ec298 mkdir -p "$FAKE_PATH" "$INSTALL_DIR" -cat > "$FAKE_PATH/uname" <<'EOF' +for tool in uname awk codesign curl; do + cat > "$FAKE_PATH/$tool" <> "$EVENT_LOG" -echo arm64 +echo fake-$tool >> "\$EVENT_LOG" +exit 22 EOF - -# This deliberate bypass attempt must be ignored by production. Tests set the -# old override variable, but SessionStart must still use /usr/bin/codesign. -cat > "$FAKE_PATH/codesign" <<'EOF' -#!/bin/bash -echo fake-codesign >> "$EVENT_LOG" -exit 0 -EOF - -cat > "$FAKE_PATH/awk" <<'EOF' -#!/bin/bash -echo fake-awk >> "$EVENT_LOG" -exec /usr/bin/awk "$@" -EOF - -cat > "$FAKE_PATH/curl" <<'EOF' -#!/bin/bash -if [ "${FAKE_CURL_MODE:-fail}" != "success" ]; then - echo curl-download >> "$EVENT_LOG" - exit 22 -fi - -output="" -previous="" -for argument in "$@"; do - if [ "$previous" = "-o" ]; then output="$argument"; fi - previous="$argument" + chmod +x "$FAKE_PATH/$tool" done -if [ -n "$output" ]; then - echo curl-download >> "$EVENT_LOG" - cp "$DOWNLOAD_SOURCE" "$output" -else - echo curl-sha >> "$EVENT_LOG" - if [ "${FAKE_SHA_MODE:-actual}" = "wrong" ]; then - echo 0000000000000000000000000000000000000000000000000000000000000000 - else - shasum -a 256 "$DOWNLOAD_SOURCE" | awk '{print $1}' - fi -fi -EOF - cat > "$RESIDENT" <<'EOF' #!/bin/bash echo resident-executed >> "$EVENT_LOG" @@ -76,94 +40,72 @@ cat > "$UNSIGNED_CANDIDATE" <<'EOF' echo candidate-executed >> "$EVENT_LOG" echo 'macdoc 0.7.0' EOF - -chmod +x "$FAKE_PATH/uname" "$FAKE_PATH/codesign" "$FAKE_PATH/awk" "$FAKE_PATH/curl" "$RESIDENT" "$UNSIGNED_CANDIDATE" +chmod +x "$RESIDENT" "$UNSIGNED_CANDIDATE" run_hook() { : > "$EVENT_LOG" EVENT_LOG="$EVENT_LOG" \ - FAKE_CURL_MODE="$1" \ - DOWNLOAD_SOURCE="$2" \ - FAKE_SHA_MODE="$3" \ - MACDOC_CURL_BIN="$FAKE_PATH/curl" \ MACDOC_CODESIGN_BIN="$FAKE_PATH/codesign" \ + MACDOC_CURL_BIN="$FAKE_PATH/curl" \ MACDOC_INSTALL_DIR="$INSTALL_DIR" \ + HTTPS_PROXY="http://127.0.0.1:9" \ + ALL_PROXY="http://127.0.0.1:9" \ + NO_PROXY="" \ PATH="$FAKE_PATH:$PATH" \ bash "$HOOK" >/dev/null 2>"$HOOK_STDERR" } -assert_no_execution() { - if grep -q 'executed' "$EVENT_LOG"; then - echo "FAIL: SessionStart executed resident or candidate: $(tr '\n' ' ' < "$EVENT_LOG")" >&2 - exit 1 - fi -} - -assert_trust_tools_not_hijacked() { - if grep -q '^fake-\(uname\|awk\|codesign\)$' "$EVENT_LOG"; then - echo "FAIL: SessionStart resolved a trust-chain tool through hostile PATH: $(tr '\n' ' ' < "$EVENT_LOG")" >&2 +assert_no_hostile_tool_or_binary() { + if [ -s "$EVENT_LOG" ]; then + echo "FAIL: SessionStart executed a hostile PATH/env tool or binary: $(tr '\n' ' ' < "$EVENT_LOG")" >&2 exit 1 fi } -# A rejected resident must ignore a hostile verifier override, never execute, -# and force exactly one download attempt even if its sidecar claims WANT. +# A rejected resident must ignore hostile command overrides, never execute, +# and fail soft when the real download cannot connect. echo 0.7.0 > "$GUARD" -run_hook fail "$UNSIGNED_CANDIDATE" actual -assert_no_execution -assert_trust_tools_not_hijacked -[[ "$(grep -c '^curl-download$' "$EVENT_LOG")" -eq 1 ]] || { - echo "FAIL: rejected resident must force exactly one download attempt" >&2 - exit 1 -} - -# A mismatched release digest must be rejected before candidate installation. -rm -f "$GUARD" -run_hook success "$UNSIGNED_CANDIDATE" wrong -assert_no_execution -assert_trust_tools_not_hijacked -[[ ! -f "$GUARD" ]] -grep -qx curl-download "$EVENT_LOG" -grep -qx curl-sha "$EVENT_LOG" -grep -q 'resident-executed' "$RESIDENT" -grep -q 'release sha256 asset does not match pinned binary_sha256' "$HOOK_STDERR" || { - echo "FAIL: mismatched release digest did not trip the pinned-SHA gate" >&2 - exit 1 -} - -# Full valid-path coverage needs a real Team-signed fixture. Keep the hostile -# cases above mandatory; gate only the positive cases for CI machines without -# the released binary. +run_hook +assert_no_hostile_tool_or_binary +grep -q 'resident binary was not executed' "$HOOK_STDERR" + +# Candidate integrity functions are the same fixed-tool functions sourced by +# production. Test each negative gate independently. +# Test derives the checked-out plugin root. +# shellcheck disable=SC1090,SC1091 +. "$VERIFY_LIB" + +set +e +macdoc_verify_candidate "$UNSIGNED_CANDIDATE" 0000000000000000000000000000000000000000000000000000000000000000 "$PINNED_SHA" "$REQUIREMENT" +rc_asset_pin=$? +macdoc_verify_candidate "$UNSIGNED_CANDIDATE" "$PINNED_SHA" "$PINNED_SHA" "$REQUIREMENT" +rc_bytes=$? +unsigned_sha=$(/usr/bin/shasum -a 256 "$UNSIGNED_CANDIDATE" | /usr/bin/awk '{print $1}') +macdoc_verify_candidate "$UNSIGNED_CANDIDATE" "$unsigned_sha" "$unsigned_sha" "$REQUIREMENT" +rc_signature=$? +set -e + +[[ "$rc_asset_pin" -eq 10 ]] +[[ "$rc_bytes" -eq 11 ]] +[[ "$rc_signature" -eq 12 ]] + +# Full positive coverage needs a real Team-signed fixture. Mandatory hostile +# cases above still run on CI machines without one. if ! /usr/bin/codesign --verify --strict -R "$REQUIREMENT" "$SIGNED_FIXTURE" 2>/dev/null; then echo "SKIP: positive signed-fixture cases (set MACDOC_SIGNED_FIXTURE)" - echo "PASS: rejected resident/candidate are never executed" + echo "PASS: hostile resident/candidate paths were rejected" exit 0 fi -# A verified resident with a matching installer sidecar takes a zero-network -# fast path without executing the binary during SessionStart. +signed_sha=$(/usr/bin/shasum -a 256 "$SIGNED_FIXTURE" | /usr/bin/awk '{print $1}') +[[ "$signed_sha" = "$PINNED_SHA" ]] +macdoc_verify_candidate "$SIGNED_FIXTURE" "$PINNED_SHA" "$PINNED_SHA" "$REQUIREMENT" + +# Verified resident + matching sidecar is a zero-network, zero-execution path. cp "$SIGNED_FIXTURE" "$RESIDENT" chmod +x "$RESIDENT" echo 0.7.0 > "$GUARD" -run_hook fail "$SIGNED_FIXTURE" actual -assert_no_execution -assert_trust_tools_not_hijacked -[[ ! -s "$EVENT_LOG" ]] || { - echo "FAIL: verified matching resident should not hit test doubles: $(tr '\n' ' ' < "$EVENT_LOG")" >&2 - exit 1 -} +run_hook +assert_no_hostile_tool_or_binary -# A rejected resident can be replaced only by signed bytes whose release -# digest matches. The candidate is installed but never executed by the hook. -cp "$UNSIGNED_CANDIDATE" "$RESIDENT" -chmod +x "$RESIDENT" -rm -f "$GUARD" -run_hook success "$SIGNED_FIXTURE" actual -assert_no_execution -assert_trust_tools_not_hijacked -grep -qx curl-download "$EVENT_LOG" -grep -qx curl-sha "$EVENT_LOG" -cmp -s "$RESIDENT" "$SIGNED_FIXTURE" -[[ "$(cat "$GUARD")" = "0.7.0" ]] - -echo "PASS: SessionStart never executes resident binary and installs only a verified candidate" +echo "PASS: SessionStart uses fixed trust tools and verifies exact candidate bytes"