#!/usr/bin/env bash # Shared tamper-evident appender for the central tool-call audit log # (.specify/logs/audit/tool-calls.jsonl). # # Every record is chained: record_hash = SHA-256(previous_record_hash | core), # where core is the canonical (sorted-key) JSON of the record minus record_hash. # After each append the chain head is signed with the audit RSA key, so a # re-forged chain (recomputed hashes) cannot produce a valid head signature # without the private key. Used by both agent-metrics.sh and tool-registry-gate.sh # so the combined audit is tamper-evident regardless of which writer appended. # # Production note: the private key must live off-repo (KMS/HSM); it is local # here only for self-contained demonstration. append_tool_audit() { local record_json="$1" local project_root="$2" local audit_dir="$CASAN_STATE_ROOT/logs/audit" local log="$audit_dir/tool-calls.jsonl" mkdir -p "$audit_dir" local head head="$(python - "$log" "$record_json" <<'PY' import hashlib, json, sys log, rec_json = sys.argv[1], sys.argv[2] rec = json.loads(rec_json) prev = "" try: with open(log, encoding="utf-8") as f: lines = [l for l in f if l.strip()] if lines: prev = json.loads(lines[-1]).get("record_hash", "") except FileNotFoundError: pass rec.pop("record_hash", None) rec["previous_record_hash"] = prev core = json.dumps(rec, sort_keys=True, separators=(",", ":")) rec["record_hash"] = hashlib.sha256((prev + "|" + core).encode()).hexdigest() with open(log, "a", encoding="utf-8") as f: f.write(json.dumps(rec) + "\n") sys.stdout.write(rec["record_hash"]) PY )" command -v openssl >/dev/null 2>&1 || return 0 # Private signing key lives OFF-REPO (default ~/.casan/audit-keys); only the # public key is committed, for verification. Production: replace with KMS/HSM. local pub_dir="$CASAN_GOVERNANCE_ROOT" local priv_dir="${CASAN_AUDIT_KEY_DIR:-$HOME/.casan/audit-keys}" local priv="$priv_dir/audit-private.pem" pub="$pub_dir/audit-public.pem" mkdir -p "$pub_dir" "$priv_dir" if [[ ! -f "$priv" ]]; then openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out "$priv" 2>/dev/null chmod 600 "$priv" fi # Always re-export the matching public key so a persisted off-repo private key # never drifts out of sync with a freshly checked-out audit-public.pem on CI # runners (see governance-check.sh for the full rationale). openssl rsa -in "$priv" -pubout -out "$pub" 2>/dev/null || true printf '%s' "$head" > "$audit_dir/tool-calls-head.txt" openssl dgst -sha256 -sign "$priv" -out "$audit_dir/tool-calls-head.sig" "$audit_dir/tool-calls-head.txt" 2>/dev/null || true }