#!/usr/bin/env bash set -euo pipefail # CASAN Level 5 signed central policy bundle. # Usage: # sign-policy-bundle.sh sign # sign-policy-bundle.sh verify MODE="${1:-}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/casan-paths.sh" PROJECT_ROOT="$CASAN_APP_ROOT" GOV_DIR="$CASAN_GOVERNANCE_ROOT" BUNDLE="$GOV_DIR/policy-bundle.yaml" MANIFEST="$GOV_DIR/policy-manifest.json" PRIVATE_KEY="$GOV_DIR/policy-private.pem" PUBLIC_KEY="$GOV_DIR/policy-public.pem" SIGNATURE="$GOV_DIR/policy-manifest.sig" mkdir -p "$GOV_DIR" if [[ "$MODE" != "sign" && "$MODE" != "verify" ]]; then echo "Usage: sign-policy-bundle.sh sign|verify" >&2 exit 64 fi if ! command -v openssl >/dev/null 2>&1; then echo "POLICY_SIGNING_UNAVAILABLE openssl not found" >&2 exit 1 fi generate_manifest() { python - "$PROJECT_ROOT" "$BUNDLE" "$MANIFEST" <<'PY' import hashlib import json import pathlib import re import sys from datetime import datetime, timezone root = pathlib.Path(sys.argv[1]) bundle = pathlib.Path(sys.argv[2]) manifest = pathlib.Path(sys.argv[3]) text = bundle.read_text(encoding="utf-8") paths = re.findall(r"^\s*path:\s*(.+?)\s*$", text, flags=re.MULTILINE) files = [] for raw in paths: rel = raw.strip().strip('"') path = root / rel if not path.exists(): raise SystemExit(f"missing policy file: {rel}") data = path.read_bytes() files.append({ "path": rel, "sha256": hashlib.sha256(data).hexdigest(), "bytes": len(data), }) payload = { "bundle_id": "casan-okr-harness-policy", "generated_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), "files": files, } manifest.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8") print(f"POLICY_MANIFEST_GENERATED files={len(files)} manifest={manifest}") PY } if [[ "$MODE" == "sign" ]]; then generate_manifest if [[ -n "${VAULT_ADDR:-}" && -n "${VAULT_TOKEN:-}" ]] && \ curl -sf "$VAULT_ADDR/v1/sys/health" >/dev/null 2>&1; then # ── KMS path: sign via HashiCorp Vault Transit (key never stored on disk) ── VAULT_KMS="$SCRIPT_DIR/vault-kms.sh" bash "$VAULT_KMS" enable-transit bash "$VAULT_KMS" sign "$MANIFEST" "$SIGNATURE" "casan-policy-key" bash "$VAULT_KMS" pubkey "$PUBLIC_KEY" "casan-policy-key" echo "POLICY_BUNDLE_SIGNED manifest=$MANIFEST signature=$SIGNATURE public_key=$PUBLIC_KEY key_backend=vault-kms" else # ── Fallback: local key file (dev / no Vault) ───────────────────────────── if [[ ! -f "$PRIVATE_KEY" ]]; then openssl genrsa -out "$PRIVATE_KEY" 2048 >/dev/null 2>&1 fi # Key-sync invariant: the on-disk public key must ALWAYS match the key that # signs (a prior Vault-signed run leaves the Vault pubkey here — verifying # a local-key signature against it would fail with an RSA padding error). openssl rsa -in "$PRIVATE_KEY" -pubout -out "$PUBLIC_KEY" >/dev/null 2>&1 openssl dgst -sha256 -sign "$PRIVATE_KEY" -out "$SIGNATURE" "$MANIFEST" echo "POLICY_BUNDLE_SIGNED manifest=$MANIFEST signature=$SIGNATURE public_key=$PUBLIC_KEY key_backend=local-file" fi exit 0 fi python - "$PROJECT_ROOT" "$MANIFEST" <<'PY' import hashlib import json import pathlib import sys root = pathlib.Path(sys.argv[1]) manifest = json.loads(pathlib.Path(sys.argv[2]).read_text(encoding="utf-8")) for item in manifest["files"]: data = (root / item["path"]).read_bytes() actual = hashlib.sha256(data).hexdigest() if actual != item["sha256"]: raise SystemExit(f"POLICY_HASH_MISMATCH path={item['path']} expected={item['sha256']} actual={actual}") print(f"POLICY_HASHES_VALID files={len(manifest['files'])}") PY openssl dgst -sha256 -verify "$PUBLIC_KEY" -signature "$SIGNATURE" "$MANIFEST" >/dev/null echo "POLICY_SIGNATURE_VALID manifest=$MANIFEST"