#!/usr/bin/env bash set -uo pipefail # CASAN H5 — Approval-identity MVP tests (C4 / V20). # # Proves that under CASAN_APPROVAL_STRICT=1 a high-risk approval is trusted ONLY # when a REGISTERED reviewer cryptographically signs THIS request and their role # is authorized — a plain env-var approver is no longer enough. Also proves the # default (non-strict) path is unchanged (backward compatible). # # Self-contained: generates ephemeral reviewer keypairs into a temp reviewers # dir and uses the committed reviewers.registry (pubkey filenames match). SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" S="$PROJECT_ROOT/.specify/scripts/bash" REG="$PROJECT_ROOT/.specify/level5/central-governance/reviewers.registry" WORK="$(mktemp -d)"; RV="$WORK/reviewers"; mkdir -p "$RV" trap 'rm -rf "$WORK"' EXIT PASS=0; FAIL=0 pass() { echo "PASS: $1"; PASS=$((PASS + 1)); } fail() { echo "FAIL: $1"; FAIL=$((FAIL + 1)); } expect_rc() { local want="$1" desc="$2"; shift 2 local got=0 { "$@" >/dev/null 2>&1; } || got=$? [[ "$got" -eq "$want" ]] && pass "$desc (rc=$got)" || fail "$desc (got rc=$got, want $want)" } # Ephemeral reviewer keypairs (filenames match reviewers.registry). for r in ops-owner tech-lead security-lead; do openssl genrsa -out "$WORK/$r.priv.pem" 2048 2>/dev/null openssl rsa -in "$WORK/$r.priv.pem" -pubout -out "$RV/$r.pub.pem" 2>/dev/null done openssl genrsa -out "$WORK/attacker.priv.pem" 2048 2>/dev/null REQ="$WORK/req.txt" printf 'deploy to production and run database migration\n' > "$REQ" OTHER="$WORK/other.txt" printf 'deploy a different unrelated change to production\n' > "$OTHER" sign() { bash "$S/approval-sign.sh" "$@" >/dev/null 2>&1; } # gc — run governance-check in STRICT mode for a high-risk deploy gc() { env CASAN_APPROVAL_STRICT=1 CASAN_REVIEWERS_FILE="$REG" CASAN_REVIEWERS_DIR="$RV" \ CASAN_ACTOR=alice CASAN_APPROVAL_DECISION=approve "$@" \ bash "$S/governance-check.sh" "$REQ" "$WORK/out.txt" deploy } echo "===== H5 approval-identity (CASAN_APPROVAL_STRICT=1) =====" # 1. Valid signed approval by an authorized role -> APPROVED sign deploy alice "$REQ" ops-owner "$WORK/ops-owner.priv.pem" "$WORK/ops.sig" OUT="$(gc CASAN_APPROVER=ops-owner CASAN_APPROVAL_SIG="$WORK/ops.sig" 2>/dev/null)"; RC=$? { [[ "$RC" -eq 0 ]] && printf '%s' "$OUT" | grep -q "human_approved_signed"; } \ && pass "valid signed approval by authorized reviewer -> APPROVED" \ || fail "valid signed approval rejected (rc=$RC out=$OUT)" # 2. Env-var approver but NO signature -> DENY (the core fix) expect_rc 2 "env-var approver without signature is denied" \ gc CASAN_APPROVER=ops-owner # 3. Registered reviewer, but role not authorized for this action -> DENY sign deploy alice "$REQ" tech-lead "$WORK/tech-lead.priv.pem" "$WORK/tech.sig" expect_rc 2 "reviewer whose role is not authorized for deploy is denied" \ gc CASAN_APPROVER=tech-lead CASAN_APPROVAL_SIG="$WORK/tech.sig" # 4. Forged signature (attacker key, claims to be ops-owner) -> DENY sign deploy alice "$REQ" ops-owner "$WORK/attacker.priv.pem" "$WORK/forged.sig" expect_rc 2 "forged signature (unregistered key) is denied" \ gc CASAN_APPROVER=ops-owner CASAN_APPROVAL_SIG="$WORK/forged.sig" # 5. Unregistered approver id -> DENY sign deploy alice "$REQ" ghost "$WORK/attacker.priv.pem" "$WORK/ghost.sig" expect_rc 2 "unregistered approver id is denied" \ gc CASAN_APPROVER=ghost CASAN_APPROVAL_SIG="$WORK/ghost.sig" # 6. Replay: a signature bound to a DIFFERENT request cannot approve this one sign deploy alice "$OTHER" ops-owner "$WORK/ops-owner.priv.pem" "$WORK/replay.sig" expect_rc 2 "signature bound to another request cannot be replayed" \ gc CASAN_APPROVER=ops-owner CASAN_APPROVAL_SIG="$WORK/replay.sig" # 7. Separation of duties still enforced even with a valid signature sign deploy ops-owner "$REQ" ops-owner "$WORK/ops-owner.priv.pem" "$WORK/sod.sig" expect_rc 2 "self-approval denied even with a valid signature (SoD)" \ env CASAN_APPROVAL_STRICT=1 CASAN_REVIEWERS_FILE="$REG" CASAN_REVIEWERS_DIR="$RV" \ CASAN_ACTOR=ops-owner CASAN_APPROVAL_DECISION=approve \ CASAN_APPROVER=ops-owner CASAN_APPROVAL_SIG="$WORK/sod.sig" \ bash "$S/governance-check.sh" "$REQ" "$WORK/out.txt" deploy # 8. Backward compatibility: default (non-strict) env approval still works expect_rc 0 "non-strict env approval unchanged (backward compatible)" \ env CASAN_ACTOR=alice CASAN_APPROVAL_DECISION=approve CASAN_APPROVER=bob \ bash "$S/governance-check.sh" "$REQ" "$WORK/out.txt" deploy echo "" echo "===== H5 APPROVAL-IDENTITY SUMMARY: PASS=$PASS FAIL=$FAIL =====" [[ "$FAIL" -eq 0 ]] || exit 1