#!/usr/bin/env bash set -euo pipefail # Verify CASAN H5 append-only hash-chain audit log. # Usage: # verify-audit-chain.sh [audit-jsonl] SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" AUDIT_LOG="${1:-$PROJECT_ROOT/.specify/logs/audit/audit.jsonl}" if [[ ! -f "$AUDIT_LOG" ]]; then echo "AUDIT_CHAIN_MISSING file=$AUDIT_LOG" >&2 exit 1 fi COMPUTED_HEAD="$(python3 - "$AUDIT_LOG" <<'PY' import hashlib import json import sys path = sys.argv[1] previous = "" count = 0 with open(path, encoding="utf-8") as f: for line_no, line in enumerate(f, 1): if not line.strip(): continue record = json.loads(line) expected_previous = record.get("previous_record_hash", "") if expected_previous != previous: raise SystemExit( f"AUDIT_CHAIN_BROKEN line={line_no} expected_previous={previous} actual_previous={expected_previous}" ) core = "|".join( [ record.get("timestamp", ""), record.get("trace_id", ""), record.get("action", ""), record.get("actor", ""), record.get("risk_level", ""), record.get("decision", ""), record.get("approval_status", ""), record.get("approver", ""), record.get("input_hash", ""), record.get("output_hash", ""), expected_previous, ] ) expected_hash = hashlib.sha256(core.encode()).hexdigest() actual_hash = record.get("record_hash", "") if expected_hash != actual_hash: raise SystemExit( f"AUDIT_HASH_MISMATCH line={line_no} expected={expected_hash} actual={actual_hash}" ) previous = actual_hash count += 1 # Emit count and head on stderr (human) and the head on stdout (captured). sys.stderr.write(f"AUDIT_CHAIN_INTEGRITY_OK records={count}\n") sys.stdout.write(previous) PY )" # --- External anchor verification --- # Recomputing a forged chain yields a different head; the stored head signature # was produced with a private key the forger does not have, so it will not match. AUDIT_DIR="$(dirname "$AUDIT_LOG")" HEAD_FILE="$AUDIT_DIR/audit-head.txt" HEAD_SIG="$AUDIT_DIR/audit-head.sig" AUDIT_PUB="$PROJECT_ROOT/.specify/level5/central-governance/audit-public.pem" if [[ -f "$HEAD_FILE" && -f "$HEAD_SIG" && -f "$AUDIT_PUB" ]] && command -v openssl >/dev/null 2>&1; then STORED_HEAD="$(cat "$HEAD_FILE")" if [[ "$STORED_HEAD" != "$COMPUTED_HEAD" ]]; then echo "AUDIT_HEAD_MISMATCH computed=$COMPUTED_HEAD stored=$STORED_HEAD" >&2 exit 1 fi if ! openssl dgst -sha256 -verify "$AUDIT_PUB" -signature "$HEAD_SIG" "$HEAD_FILE" >/dev/null 2>&1; then echo "AUDIT_HEAD_SIGNATURE_INVALID head=$COMPUTED_HEAD" >&2 exit 1 fi echo "AUDIT_CHAIN_VALID anchor=signed last_hash=$COMPUTED_HEAD" else echo "AUDIT_CHAIN_VALID anchor=unsigned last_hash=$COMPUTED_HEAD" fi