#!/usr/bin/env bash set -uo pipefail # CASAN H5 — Signed-approval verifier (Approval-identity MVP · C4 / V20). # # Problem: high-risk approval used to trust a plain env var (CASAN_APPROVER=bob) — # anyone who can set the env can "approve". This binds an approval to a REGISTERED # reviewer's cryptographic identity: the reviewer must SIGN this exact request with # their private key, AND their role must be authorized for the action class. # # The signed assertion is: casan-approval|v1|||| # — so a signature for one request/reviewer cannot be replayed for another. # # Usage: # approval-verify.sh # Registry (line format, no yaml dep): # reviewer # action # Env: CASAN_REVIEWERS_FILE (default governance/reviewers.registry) # CASAN_REVIEWERS_DIR (default governance/reviewers) — base dir for pubkey-file # Exit: 0 ok (prints "APPROVAL_OK role="), 3 deny (reason on stderr), 64 usage. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" GOV_DIR="$PROJECT_ROOT/.specify/level5/central-governance" REVIEWERS_FILE="${CASAN_REVIEWERS_FILE:-$GOV_DIR/reviewers.registry}" REVIEWERS_DIR="${CASAN_REVIEWERS_DIR:-$GOV_DIR/reviewers}" ACTION="${1:-}"; ACTOR="${2:-}"; INPUT_FILE="${3:-}"; APPROVER="${4:-}"; SIG_FILE="${5:-}" if [[ -z "$ACTION" || -z "$ACTOR" || -z "$INPUT_FILE" || -z "$APPROVER" || -z "$SIG_FILE" ]]; then echo "Usage: approval-verify.sh " >&2 exit 64 fi deny() { echo "APPROVAL_DENIED reason=$1 approver=$APPROVER action=$ACTION" >&2; exit 3; } [[ -f "$INPUT_FILE" ]] || deny "input_file_missing" [[ -f "$SIG_FILE" ]] || deny "signature_missing" [[ -f "$REVIEWERS_FILE" ]] || deny "reviewer_registry_missing" command -v openssl >/dev/null 2>&1 || deny "openssl_unavailable" hash_file() { if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | awk '{print $1}' else shasum -a 256 "$1" | awk '{print $1}'; fi } # Reviewer lookup (first matching registered reviewer). REV_LINE="$(awk -v id="$APPROVER" '$1=="reviewer" && $2==id {print $3" "$4; exit}' "$REVIEWERS_FILE")" [[ -n "$REV_LINE" ]] || deny "approver_not_registered" ROLE="${REV_LINE%% *}" PUB_REL="${REV_LINE##* }" # Role authorization for this action (fallback to the "default" action policy). ROLES="$(awk -v a="$ACTION" '$1=="action" && $2==a {print $3; exit}' "$REVIEWERS_FILE")" [[ -n "$ROLES" ]] || ROLES="$(awk '$1=="action" && $2=="default" {print $3; exit}' "$REVIEWERS_FILE")" case ",$ROLES," in *",$ROLE,"*) : ;; *) deny "approver_role_not_authorized(role=$ROLE action=$ACTION allowed=$ROLES)" ;; esac # Resolve pubkey path (absolute or relative to reviewers dir). PUB="$PUB_REL"; [[ "$PUB" = /* ]] || PUB="$REVIEWERS_DIR/$PUB_REL" [[ -f "$PUB" ]] || deny "approver_pubkey_missing($PUB)" # Rebuild the exact signed assertion and verify. INPUT_SHA="$(hash_file "$INPUT_FILE")" MSG="casan-approval|v1|$ACTION|$ACTOR|$INPUT_SHA|$APPROVER" TMP="$(mktemp)"; trap 'rm -f "$TMP"' EXIT printf '%s' "$MSG" > "$TMP" openssl dgst -sha256 -verify "$PUB" -signature "$SIG_FILE" "$TMP" >/dev/null 2>&1 \ || deny "approval_signature_invalid" echo "APPROVAL_OK role=$ROLE approver=$APPROVER action=$ACTION" exit 0