#!/usr/bin/env bash set -uo pipefail # CASAN Plan-16 SEC-01 (H-01) — "unsigned = FAIL" in enforced mode. # # Proves that verify-audit-chain / verify-tool-audit / telemetry-integrity / # evidence-pack all treat a missing (or unverifiable) signature as a FAILURE when # strict enforcement is on, while keeping the permissive dev default unchanged. # Enforcement is triggered by either CASAN_VERIFY_STRICT=1 or CASAN_PROFILE=prod. # # Deterministic; no model/app/network. Hermetic: builds throwaway logs in a temp # workspace; the telemetry case backs up + restores the real head artifacts. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/../scripts/bash/casan-paths.sh" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" BASH_DIR="$CASAN_HARNESS_ROOT/scripts/bash" WORK="$(mktemp -d)" # Back up telemetry head artifacts (fixed-path; the sign step mutates them). L5_DIR="$CASAN_STATE_ROOT/logs/level5" TEL_BAK="$WORK/tel-bak"; mkdir -p "$TEL_BAK" for f in telemetry-manifest.json telemetry-head.txt telemetry-head.sig; do [[ -f "$L5_DIR/$f" ]] && cp -p "$L5_DIR/$f" "$TEL_BAK/$f" done restore_tel() { for f in telemetry-manifest.json telemetry-head.txt telemetry-head.sig; do if [[ -f "$TEL_BAK/$f" ]]; then cp -p "$TEL_BAK/$f" "$L5_DIR/$f"; else rm -f "$L5_DIR/$f"; fi done } trap 'restore_tel; rm -rf "$WORK"' EXIT PASS=0; FAIL=0 pass() { echo "PASS: $1"; PASS=$((PASS + 1)); } fail() { echo "FAIL: $1"; FAIL=$((FAIL + 1)); } # rc_of : run, print exit code, never abort. rc_of() { set +e; "$@" >/dev/null 2>&1; echo $?; set -e 2>/dev/null || true; } echo "===== Plan-16 SEC-01: unsigned = FAIL in enforced mode =====" # --------------------------------------------------------------------------- # 1) verify-audit-chain.sh — build a genuine unsigned chain (no head/sig files) # --------------------------------------------------------------------------- AUD_DIR="$WORK/audit"; mkdir -p "$AUD_DIR" python3 - "$AUD_DIR/audit.jsonl" <<'PY' import hashlib, json, sys path = sys.argv[1] prev = "" core = "|".join(["2026-07-06T00:00:00Z", "t1", "act", "a@x", "low", "ALLOW", "n/a", "", "ih", "oh", prev]) h = hashlib.sha256(core.encode()).hexdigest() rec = {"timestamp":"2026-07-06T00:00:00Z","trace_id":"t1","action":"act","actor":"a@x", "risk_level":"low","decision":"ALLOW","approval_status":"n/a","approver":"", "input_hash":"ih","output_hash":"oh","previous_record_hash":prev,"record_hash":h} open(path,"w").write(json.dumps(rec)+"\n") PY RC="$(rc_of bash "$BASH_DIR/verify-audit-chain.sh" "$AUD_DIR/audit.jsonl")" [[ "$RC" -eq 0 ]] && pass "audit-chain: unsigned OK in permissive mode (rc=0)" \ || fail "audit-chain: permissive mode should pass unsigned (rc=$RC)" RC="$(rc_of env CASAN_VERIFY_STRICT=1 bash "$BASH_DIR/verify-audit-chain.sh" "$AUD_DIR/audit.jsonl")" [[ "$RC" -ne 0 ]] && pass "audit-chain: unsigned FAILS with CASAN_VERIFY_STRICT=1 (rc=$RC)" \ || fail "audit-chain: strict flag did not fail unsigned (rc=$RC)" RC="$(rc_of env CASAN_PROFILE=prod bash "$BASH_DIR/verify-audit-chain.sh" "$AUD_DIR/audit.jsonl")" [[ "$RC" -ne 0 ]] && pass "audit-chain: unsigned FAILS with CASAN_PROFILE=prod (rc=$RC)" \ || fail "audit-chain: prod profile did not fail unsigned (rc=$RC)" # Attack: tamper a field, recompute record_hash so the chain is internally valid, # leave no signature. Strict must STILL fail (recompute does not save the forger). python3 - "$AUD_DIR/audit.jsonl" <<'PY' import hashlib, json, sys path = sys.argv[1] rec = json.loads(open(path).read().strip()) rec["decision"] = "DENY_BYPASSED" # forge the verdict prev = "" core = "|".join([rec["timestamp"],rec["trace_id"],rec["action"],rec["actor"],rec["risk_level"], rec["decision"],rec["approval_status"],rec["approver"],rec["input_hash"], rec["output_hash"],prev]) rec["record_hash"] = hashlib.sha256(core.encode()).hexdigest() # recompute → chain valid open(path,"w").write(json.dumps(rec)+"\n") PY RC="$(rc_of env CASAN_VERIFY_STRICT=1 bash "$BASH_DIR/verify-audit-chain.sh" "$AUD_DIR/audit.jsonl")" [[ "$RC" -ne 0 ]] && pass "audit-chain: tamper+recompute+no-sig still FAILS in strict (rc=$RC)" \ || fail "audit-chain: recomputed forged chain passed strict (rc=$RC)" # --------------------------------------------------------------------------- # 2) verify-tool-audit.sh — genuine unsigned tool-calls chain # --------------------------------------------------------------------------- TOOL_DIR="$WORK/tool"; mkdir -p "$TOOL_DIR" python3 - "$TOOL_DIR/tool-calls.jsonl" <<'PY' import hashlib, json, sys path = sys.argv[1] prev = "" rec = {"timestamp":"2026-07-06T00:00:00Z","tool":"bash","actor":"a@x","previous_record_hash":prev} core = json.dumps(rec, sort_keys=True, separators=(",", ":")) rec["record_hash"] = hashlib.sha256((prev + "|" + core).encode()).hexdigest() open(path,"w").write(json.dumps(rec)+"\n") PY RC="$(rc_of bash "$BASH_DIR/verify-tool-audit.sh" "$TOOL_DIR/tool-calls.jsonl")" [[ "$RC" -eq 0 ]] && pass "tool-audit: unsigned OK in permissive mode (rc=0)" \ || fail "tool-audit: permissive mode should pass unsigned (rc=$RC)" RC="$(rc_of env CASAN_VERIFY_STRICT=1 bash "$BASH_DIR/verify-tool-audit.sh" "$TOOL_DIR/tool-calls.jsonl")" [[ "$RC" -ne 0 ]] && pass "tool-audit: unsigned FAILS with CASAN_VERIFY_STRICT=1 (rc=$RC)" \ || fail "tool-audit: strict flag did not fail unsigned (rc=$RC)" # --------------------------------------------------------------------------- # 3) telemetry-integrity.sh — sign (unsigned, no key), then strict verify fails # --------------------------------------------------------------------------- # Force the keyless path so the head is written WITHOUT a signature. env CASAN_AUDIT_PRIV="$WORK/nonexistent.pem" bash "$BASH_DIR/telemetry-integrity.sh" sign >/dev/null 2>&1 RC="$(rc_of env CASAN_AUDIT_PUB="$WORK/nonexistent.pem" bash "$BASH_DIR/telemetry-integrity.sh" verify)" [[ "$RC" -eq 0 ]] && pass "telemetry: unsigned OK in permissive mode (rc=0)" \ || fail "telemetry: permissive mode should pass unsigned (rc=$RC)" RC="$(rc_of env CASAN_VERIFY_STRICT=1 CASAN_AUDIT_PUB="$WORK/nonexistent.pem" bash "$BASH_DIR/telemetry-integrity.sh" verify)" [[ "$RC" -ne 0 ]] && pass "telemetry: unsigned FAILS with CASAN_VERIFY_STRICT=1 (rc=$RC)" \ || fail "telemetry: strict flag did not fail unsigned (rc=$RC)" # --------------------------------------------------------------------------- # 4) evidence-pack.sh verify-pack — build a valid unsigned pack (no .sig) # --------------------------------------------------------------------------- PACK_DIR="$WORK/pack"; mkdir -p "$PACK_DIR" echo "hello evidence" > "$PACK_DIR/report.txt" python3 - "$PACK_DIR" <<'PY' import hashlib, json, os, sys d = sys.argv[1] files = {} for fn in os.listdir(d): p = os.path.join(d, fn) if os.path.isfile(p): files[fn] = hashlib.sha256(open(p,"rb").read()).hexdigest() canonical = json.dumps(files, sort_keys=True, separators=(",", ":")) head = hashlib.sha256(canonical.encode()).hexdigest() json.dump({"files": files, "manifest_head": head}, open(os.path.join(d,"artifact-manifest.json"),"w")) open(os.path.join(d,"manifest-head.txt"),"w").write(head) PY RC="$(rc_of bash "$BASH_DIR/evidence-pack.sh" verify-pack testrun --dir "$PACK_DIR")" [[ "$RC" -eq 0 ]] && pass "evidence-pack: unsigned OK in permissive mode (rc=0)" \ || fail "evidence-pack: permissive mode should pass unsigned (rc=$RC)" RC="$(rc_of env CASAN_VERIFY_STRICT=1 bash "$BASH_DIR/evidence-pack.sh" verify-pack testrun --dir "$PACK_DIR")" [[ "$RC" -ne 0 ]] && pass "evidence-pack: unsigned FAILS with CASAN_VERIFY_STRICT=1 (rc=$RC)" \ || fail "evidence-pack: strict flag did not fail unsigned (rc=$RC)" echo "" echo "===== SEC-01 SUMMARY: PASS=$PASS FAIL=$FAIL =====" [[ "$FAIL" -eq 0 ]] || exit 1