#!/usr/bin/env python3 """ Gwaalas Independent Trust Chain Verifier ========================================= Standalone Python 3 script (zero external dependencies) that fetches a Gwaalas trust chain verification payload and recomputes all SHA-256 event hashes from scratch to independently verify tamper-evidence. Usage: python3 verify_trust.py [--base-url http://localhost:4000] Examples: python3 verify_trust.py GW-VRFY01 python3 verify_trust.py http://localhost:4000/api/v1/verify/GW-VRFY01 python3 verify_trust.py /path/to/payload.json """ import argparse import hashlib import json import sys import urllib.request from typing import Any, Dict, List, Tuple def canonical_json(term: Any, is_root: bool = True) -> str: """ Computes canonical JSON string according to Gwaalas specification: 1. Drops 'hash' and 'prev_hash' at root level. 2. Sorts map keys recursively by UTF-8 byte order. 3. Emits compact JSON without whitespace. """ if isinstance(term, dict): d = dict(term) if is_root: d.pop("hash", None) d.pop("prev_hash", None) d.pop("index", None) d.pop("attestation", None) sorted_keys = sorted(d.keys(), key=lambda k: str(k).encode('utf-8')) pairs = [] for k in sorted_keys: key_str = json.dumps(str(k), ensure_ascii=False) val_str = canonical_json(d[k], is_root=False) pairs.append(f"{key_str}:{val_str}") return "{" + ",".join(pairs) + "}" elif isinstance(term, list): elements = [canonical_json(elem, is_root=False) for elem in term] return "[" + ",".join(elements) + "]" else: return json.dumps(term, ensure_ascii=False, separators=(',', ':')) def compute_event_hash(prev_hash: str, event: Dict[str, Any]) -> str: """Computes SHA-256 hex digest over prev_hash <> canonical_json(event).""" c_json = canonical_json(event, is_root=True) payload = (prev_hash + c_json).encode('utf-8') return hashlib.sha256(payload).hexdigest().lower() def compute_genesis_hash(trust_code: str) -> str: """Genesis prev_hash = SHA-256 hex digest of trust_code.""" return hashlib.sha256(trust_code.encode('utf-8')).hexdigest().lower() def verify_chain_data(payload: Dict[str, Any]) -> Tuple[bool, List[str]]: """ Verifies the event chain inside payload dict. Returns (is_valid, log_messages). """ logs = [] trust_code = payload.get("trust_code", "") event_chain = payload.get("event_chain", []) if not event_chain: logs.append("[FAIL] Empty event chain") return False, logs expected_prev_hash = compute_genesis_hash(trust_code) logs.append(f"Genesis prev_hash for trust_code '{trust_code}': {expected_prev_hash}") for idx, event in enumerate(event_chain): actual_prev = event.get("prev_hash", "") actual_hash = event.get("hash", "") event_type = event.get("type", "unknown") if actual_prev != expected_prev_hash: logs.append( f"[FAIL] Event #{idx} ({event_type}): prev_hash mismatch!\n" f" Expected: {expected_prev_hash}\n" f" Got: {actual_prev}" ) return False, logs computed_hash = compute_event_hash(expected_prev_hash, event) if computed_hash != actual_hash: logs.append( f"[FAIL] Event #{idx} ({event_type}): hash mismatch!\n" f" Computed: {computed_hash}\n" f" Stored: {actual_hash}" ) return False, logs logs.append( f"[PASS] Event #{idx} ({event_type}): hash verified ({computed_hash[:12]}...)" ) expected_prev_hash = actual_hash return True, logs def main(): parser = argparse.ArgumentParser( description="Gwaalas Independent SHA-256 Trust Chain Verifier" ) parser.add_argument( "target", help="Trust code (e.g. GW-VRFY01), full URL, or local JSON filepath", ) parser.add_argument( "--base-url", default="http://localhost:4000", help="Base URL for Gwaalas API (default: http://localhost:4000)", ) args = parser.parse_args() target = args.target.strip() if target.startswith("http://") or target.startswith("https://"): url = target elif target.endswith(".json") or "/" in target or "\\" in target: url = None else: url = f"{args.base_url.rstrip('/')}/api/v1/verify/{target}" print(f"=== Gwaalas Independent Trust Chain Verifier ===") if url: print(f"Fetching: {url}") req = urllib.request.Request(url, headers={"Accept": "application/json"}) try: with urllib.request.urlopen(req) as resp: data = json.loads(resp.read().decode('utf-8')) except Exception as e: print(f"ERROR: Failed to fetch API payload: {e}") sys.exit(1) else: print(f"Reading file: {target}") try: with open(target, 'r', encoding='utf-8') as f: data = json.load(f) except Exception as e: print(f"ERROR: Failed to read JSON file: {e}") sys.exit(1) print(f"Trust Code: {data.get('trust_code')}") print(f"Species: {data.get('batch_details', {}).get('species')}") print(f"Events: {len(data.get('event_chain', []))}") print("-" * 50) is_valid, logs = verify_chain_data(data) for line in logs: print(line) print("-" * 50) if is_valid: print("OVERALL VERDICT: VERIFIED ✓ (All SHA-256 hashes recomputed successfully)") sys.exit(0) else: print("OVERALL VERDICT: TAMPERED ✗ (Chain integrity failure detected!)") sys.exit(1) if __name__ == "__main__": main()