#!/usr/bin/env bash set -uo pipefail # CASAN Plan-09 — Evidence Pack (MVP). # # Packages a tamper-evident proof of a CASAN run: per-harness JSON reports, a # red-team / benign-FP result, an artifact manifest (sha256 of every file), a # human decision log, and an RSA signature over the manifest head. Verification # fails if any packed file changes. # # CLI mapping (future `casan` binary): # casan pack -> evidence-pack.sh pack # casan verify-pack -> evidence-pack.sh verify-pack # # A "Certified run" is only asserted when the required gates PASS and none was # silently skipped (see run-summary.json.certification_reasons). # # Usage: # evidence-pack.sh pack [--out ] # evidence-pack.sh verify-pack [--dir ] # Exit: 0 ok, 1 verify failed, 64 usage. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/casan-paths.sh" PROJECT_ROOT="$CASAN_APP_ROOT" # shellcheck source=casan-log.sh source "$SCRIPT_DIR/casan-log.sh" CMD="${1:-}"; RUN_ID="${2:-}" shift 2 2>/dev/null || true PACKS_ROOT="$PROJECT_ROOT/docs/output/casan/evidence-packs" PACK_DIR="" while [[ "$#" -gt 0 ]]; do case "$1" in --out|--dir) PACK_DIR="${2:-}"; shift 2 ;; *) shift ;; esac done [[ -z "$CMD" || -z "$RUN_ID" ]] && { echo "Usage: evidence-pack.sh {pack|verify-pack} [--out/--dir ]" >&2; exit 64; } [[ -z "$PACK_DIR" ]] && PACK_DIR="$PACKS_ROOT/$RUN_ID" AUDIT_PRIV="${CASAN_AUDIT_PRIV:-$CASAN_GOVERNANCE_ROOT/audit-private.pem}" AUDIT_PUB="${CASAN_AUDIT_PUB:-$CASAN_GOVERNANCE_ROOT/audit-public.pem}" run_status() { # -> prints "|" local out rc out="$("$@" 2>/dev/null | head -1)"; rc="${PIPESTATUS[0]}" printf '%s|%s' "${out:-none}" "$rc" } case "$CMD" in pack) mkdir -p "$PACK_DIR" casan_log info evidence-pack "packing run=$RUN_ID dir=$PACK_DIR" AUDIT_ST="$(run_status bash "$SCRIPT_DIR/verify-audit-chain.sh")" TOOL_ST="$(run_status bash "$SCRIPT_DIR/verify-tool-audit.sh")" TEL_ST="$(run_status bash "$SCRIPT_DIR/telemetry-integrity.sh" verify)" COST_RC=0; bash "$SCRIPT_DIR/cost-spike-detect.sh" >/dev/null 2>&1 || COST_RC=$? # Reuse an existing benign-FP report if present (fast); else leave unset so # the certification records the gate as skipped rather than fabricating it. FP_JSON="$PROJECT_ROOT/docs/output/casan/benign-fp-report.json" [[ -f "$FP_JSON" ]] || FP_JSON="" CERT_LINE="$(CASAN_EP_AUDIT="$AUDIT_ST" CASAN_EP_TOOLAUDIT="$TOOL_ST" \ CASAN_EP_TELEMETRY="$TEL_ST" CASAN_EP_COST_RC="$COST_RC" CASAN_EP_FP_JSON="$FP_JSON" \ python "$SCRIPT_DIR/evidence-pack-build.py" "$PROJECT_ROOT" "$RUN_ID" "$PACK_DIR")" # Safety: the human decision log must not leak secrets/PII (fail closed). if ! bash "$SCRIPT_DIR/data-exfil-guard.sh" "$PACK_DIR/decision-log.md" artifact >/dev/null 2>&1; then casan_log error evidence-pack "decision-log failed data-exfil guard — pack aborted" echo "EVIDENCE_PACK_ABORTED reason=decision_log_would_leak" >&2 exit 1 fi # Sign the manifest head (off-repo key in production; unsigned in keyless dev). HEAD_FILE="$PACK_DIR/manifest-head.txt" SIG_FILE="$PACK_DIR/evidence-pack.sig" if [[ -f "$AUDIT_PRIV" ]] && command -v openssl >/dev/null 2>&1; then openssl dgst -sha256 -sign "$AUDIT_PRIV" -out "$SIG_FILE" "$HEAD_FILE" ANCHOR="signed" else rm -f "$SIG_FILE"; ANCHOR="unsigned" fi CERTIFIED="${CERT_LINE#CERTIFIED|}"; CERTIFIED="${CERTIFIED%%|*}" echo "EVIDENCE_PACK_CREATED run=$RUN_ID dir=$PACK_DIR certified=$CERTIFIED anchor=$ANCHOR" [[ "$CERTIFIED" == "true" ]] && echo "CASAN_CERTIFIED_RUN run=$RUN_ID" || echo "CASAN_UNCERTIFIED_RUN run=$RUN_ID reason=${CERT_LINE##*|}" ;; verify-pack) [[ -d "$PACK_DIR" ]] || { echo "EVIDENCE_PACK_NOT_FOUND dir=$PACK_DIR" >&2; exit 1; } python "$SCRIPT_DIR/evidence-pack-verify.py" "$PACK_DIR"; VRC=$? [[ "$VRC" -ne 0 ]] && exit 1 # Signature check over the manifest head (catches a manifest rewrite). HEAD_FILE="$PACK_DIR/manifest-head.txt" SIG_FILE="$PACK_DIR/evidence-pack.sig" if [[ -f "$SIG_FILE" && -f "$AUDIT_PUB" ]] && command -v openssl >/dev/null 2>&1; then if openssl dgst -sha256 -verify "$AUDIT_PUB" -signature "$SIG_FILE" "$HEAD_FILE" >/dev/null 2>&1; then echo "EVIDENCE_PACK_VALID anchor=signed dir=$PACK_DIR" else echo "EVIDENCE_PACK_SIGNATURE_INVALID dir=$PACK_DIR" >&2 exit 1 fi else # SEC-01 (H-01): enforced mode treats a missing/unverifiable pack signature # as FAIL, otherwise deleting evidence-pack.sig after editing packed files # would still verify as a "valid unsigned" pack. if [[ "${CASAN_PROFILE:-}" == "prod" || "${CASAN_VERIFY_STRICT:-}" == "1" ]]; then MISSING="" [[ -f "$SIG_FILE" ]] || MISSING="$MISSING pack-sig" [[ -f "$AUDIT_PUB" ]] || MISSING="$MISSING pubkey" command -v openssl >/dev/null 2>&1 || MISSING="$MISSING openssl" echo "EVIDENCE_PACK_UNSIGNED_STRICT_FAIL dir=$PACK_DIR missing=${MISSING# }" >&2 exit 1 fi echo "EVIDENCE_PACK_VALID anchor=unsigned dir=$PACK_DIR" fi ;; *) echo "Usage: evidence-pack.sh {pack|verify-pack} [--out/--dir ]" >&2 exit 64 ;; esac