#!/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)" source "$SCRIPT_DIR/../scripts/bash/casan-paths.sh" PROJECT_ROOT="$CASAN_APP_ROOT" S="$CASAN_HARNESS_ROOT/scripts/bash" REG="$CASAN_GOVERNANCE_ROOT/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 "===== H5 OIDC approval via mock IdP JWT (CASAN_APPROVAL_STRICT=1) =====" openssl genrsa -out "$WORK/idp.priv.pem" 2048 2>/dev/null openssl rsa -in "$WORK/idp.priv.pem" -pubout -out "$WORK/idp.pub.pem" 2>/dev/null openssl genrsa -out "$WORK/fake-idp.priv.pem" 2048 2>/dev/null mint_jwt() { python3 "$S/approval-jwt-mint.py" --key "$1" --sub "$2" --role "$3" \ --action deploy --actor alice --input "$REQ" --exp-offset "$4" } # 9. Valid IdP-signed JWT by an authorized role -> APPROVED OIDC_JWT="$(mint_jwt "$WORK/idp.priv.pem" oidc-ops ops 300)" OUT="$(gc CASAN_APPROVER=oidc-ops CASAN_APPROVAL_JWT="$OIDC_JWT" CASAN_IDP_PUBLIC_KEY="$WORK/idp.pub.pem" 2>/dev/null)"; RC=$? { [[ "$RC" -eq 0 ]] && printf '%s' "$OUT" | grep -q "human_approved_oidc"; } \ && pass "valid IdP JWT by authorized role -> APPROVED" \ || fail "valid IdP JWT rejected (rc=$RC out=$OUT)" # 10. Expired JWT -> DENY EXPIRED_JWT="$(mint_jwt "$WORK/idp.priv.pem" oidc-ops ops -60)" expect_rc 2 "expired IdP JWT is denied" \ gc CASAN_APPROVER=oidc-ops CASAN_APPROVAL_JWT="$EXPIRED_JWT" CASAN_IDP_PUBLIC_KEY="$WORK/idp.pub.pem" # 11. Role not authorized for deploy -> DENY WRONG_ROLE_JWT="$(mint_jwt "$WORK/idp.priv.pem" oidc-tech tech_lead 300)" expect_rc 2 "IdP JWT with unauthorized role is denied" \ gc CASAN_APPROVER=oidc-tech CASAN_APPROVAL_JWT="$WRONG_ROLE_JWT" CASAN_IDP_PUBLIC_KEY="$WORK/idp.pub.pem" # 12. JWT signed by a different key than the trusted IdP pubkey -> DENY FORGED_JWT="$(mint_jwt "$WORK/fake-idp.priv.pem" oidc-ops ops 300)" expect_rc 2 "JWT with forged IdP signature is denied" \ gc CASAN_APPROVER=oidc-ops CASAN_APPROVAL_JWT="$FORGED_JWT" CASAN_IDP_PUBLIC_KEY="$WORK/idp.pub.pem" echo "" echo "===== H5 APPROVAL-IDENTITY SUMMARY: PASS=$PASS FAIL=$FAIL =====" [[ "$FAIL" -eq 0 ]] || exit 1