Back home

Trust & integrity

Hash-chain integrity specification

Every AUSTRAC export bundle Korvos produces can be verified offline by an auditor with no internet access and no Korvos tooling — just Python 3.10 and a SHA-256 implementation. This page is the canonical specification. If the bundle ever diverges from what is documented here, the bundle is the bug, not the spec.

Hash
SHA-256
Chains
54 append-only tables
Verifier
Python 3.10, stdlib only
Offline?
Yes — no network calls

What the chain proves

Korvos cannot retroactively edit AML evidence even if the application layer were compromised. Every append-only table records each row’s SHA-256 over a payload of business fields plus the previous row’s hash. A single byte changed anywhere in the history breaks the chain at the next export, in a way the auditor can detect without trusting us.

The database itself enforces the contract: UPDATE, DELETE, and TRUNCATE are revoked on every chained table for every role including service. A BEFORE INSERT trigger computes the hash inside Postgres; the application has no ability to forge a row with a falsified hash because the trigger overwrites whatever the application sent. This page documents the algorithm an auditor can use to confirm what we say is true.

Algorithm

For each chained table, every row carries three integrity fields:sequence_no (monotonic per chain), prev_hash (the previous row’s record_hash, or 32 zero bytes at genesis), and record_hash (SHA-256 over prev_hash concatenated with a per-table payload). The export bundle includes the exact payload bytes that were hashed at INSERT time as __chain_payload_hex on each JSONL row, so offline verification has no dependency on the database that produced it.

genesis_prev_hash = bytes(32)   # 32 zero bytes

for each row in chain (ordered by sequence_no ascending):
    payload  = bytes.fromhex(row.__chain_payload_hex)
    expected = sha256( bytes.fromhex(row.prev_hash) || payload )
    assert expected.hex() == row.record_hash

    # The next row's prev_hash must equal this row's record_hash:
    next_prev_hash = row.record_hash

The payload itself is whatever the table’s {table}_chain_payload(row) function returns — a stable JSONB serialisation of the business-meaningful columns, wrapped with a __table and __version marker so payload-format changes re-epoch a chain rather than silently invalidating prior hashes. Schema versions are part of the audit story; the public spec at this URL is updated whenever a payload version bumps.

The verifier script

Every export bundle ships verify.py in the ZIP. The same source is published at this URL so an auditor can compare the in-bundle copy against the publicly- committed canonical copy — if they match, the script that ran against the bundle is the script Korvos publishes.

Download verify.pySingle-file Python 3.10+. No dependencies. ~250 lines.
#!/usr/bin/env python3
"""Korvos AUSTRAC export bundle integrity verifier.

Reproduces the integrity checks documented at:
    https://korvos.com.au/trust/hash-chain

Uses only the Python standard library (>= 3.10). Runs offline.

Usage:
    python3 verify.py path/to/korvos-austrac-export.zip

Exit codes:
    0  every check passed
    1  one or more file SHA-256s drifted from the manifest
    2  one or more rows failed the chain recomputation
    3  bundle is malformed or missing required files
    4  chain head in chain_verifications.json disagrees with the JSONL tail
"""
# Full source: /trust/hash-chain/verify.py

What it checks

  • Each chain file’s SHA-256 matches manifest.json
  • Every row’s record_hash equals sha256(prev_hash || payload)
  • Every chain head matches its JSONL tail
  • manifest.format_version is 1 (this spec)

What it doesn’t check

  • Whether the bundle is complete (rows could have been silently held back at export — verify against the manifest’s declared row counts)
  • Whether the payload semantics are correct for AML — the script verifies cryptographic integrity, not regulatory adequacy
  • Whether Korvos’s production database is honest — trust the hashes, not the producer

Chained tables

54 append-only tables carry a compute_hash_chain BEFORE INSERT trigger and a corresponding {table}_chain_payload function. The bundle exports every row from every chain within the requested date range. The list below is the canonical set (committed at src/lib/audit/hash-chained-tables.ts); the migration log under supabase/migrations/ is the operational record of which table acquired its trigger in which release.

  • aml_adverse_media_matches
  • aml_adverse_media_dispositions
  • aml_asset_freeze
  • aml_austrac_notifications
  • aml_auto_approval_policies
  • aml_beneficial_owners
  • aml_citec_extracts
  • aml_co_attestations
  • aml_compliance_officers
  • aml_ecdd_packs
  • aml_emergency_clearances
  • aml_enrolment
  • aml_events
  • aml_export_downloads
  • aml_face_match_attestations
  • aml_independent_reviews
  • aml_inferred_fact_confirmations
  • aml_kyb_records
  • aml_kyc_records
  • aml_monitoring_alerts
  • aml_monitoring_alert_resolutions
  • aml_pep_list_versions
  • aml_program_version_approvals
  • aml_program_versions
  • aml_programme_section_drafts
  • aml_reliance_arrangements
  • aml_risk_assessments
  • aml_sanctions_incidents
  • aml_sanctions_list_versions
  • aml_screening_diff_hits
  • aml_screening_results
  • aml_smr_access_log
  • aml_source_of_funds
  • aml_suspicious_matters
  • aml_threshold_transactions
  • aml_tipping_off_attestations
  • aml_tipping_off_incidents
  • aml_ttr_lodgements
  • aml_training_records
  • aml_verifier_runs
  • aml_verifier_overrides
  • audit_log
  • core_decision_cards
  • core_standing_delegations
  • core_trust_receipts
  • aml_austrac_exports
  • aml_fit_and_proper_assessments
  • aml_imported_programmes
  • aml_kyc_intake_audit_events
  • core_grace_period_reminders
  • credit_ledger
  • training_assessment_attempts
  • training_certificates
  • training_progress

Trust assumptions

The chain proves that no row in an exported bundle was altered after export. It does not prove that the producer never had the ability to falsify a row at INSERT time. Korvos is engineered against that risk by removing UPDATE and DELETE grants on every chained table at the PostgreSQL role layer, by storing the chain payload function as Postgres source code under supabase/migrations/, and by publishing every payload-version bump on this page so a silent change is detectable in code review even before it ships to production. The independent verifier framework re-derives generating-agent claims from sources before any artefact is sealed, which closes the falsification path for the artefacts that matter (programmes, SMRs, KYC records, screening outcomes). The whole story holds because each layer covers a different threat — application bug, database compromise, insider tampering — and you can audit any one of them in isolation.