What this guide does

This guide walks you through verifying that a SALT-sealed accounting archive is authentic and unmodified. It is written for non-engineers — auditors, lawyers, forensic accountants — and uses standard tools available on any modern computer.

The verification answers two questions:

  1. Is this archive unchanged since it was sealed?
  2. When was it sealed, and by whom?

If both answers come back clean, the archive is what it claims to be: a tamper-evident snapshot of accounting data captured at a specific time, signed by an independent authority.

Before you start

What you need

  • The SALT bundle: an encrypted file (.zip.enc) or unencrypted .zip delivered to you
  • The passphrase, if the bundle is encrypted (delivered to you separately by the engagement contact)
  • A computer running macOS, Linux, or Windows
  • Two free tools: openssl and sqlite3
  • If the bundle was encrypted with age: also install age (github.com/FiloSottile/age)

Installing the tools

macOS: Both tools are pre-installed. Open Terminal (Applications → Utilities → Terminal). No additional installation needed.

Linux: Both tools are pre-installed on most distributions. If not, your package manager has them: sudo apt install openssl sqlite3 on Debian/Ubuntu.

Windows: The simplest path is to install Git for Windows (git-scm.com/downloads), which includes a compatible terminal called Git Bash with both tools available. Open Git Bash to use the commands in this guide.

What you don't need

  • You do not need any Sealed Ledger software, account, or service
  • You do not need an internet connection (verification is offline)
  • You do not need the original source-system credentials

Step-by-step verification

Encrypted vs. unencrypted bundles

SALT bundles are typically delivered encrypted with a passphrase the user chose at delivery. Steps 0 and 1 below cover decryption. If your bundle is unencrypted (a .zip file you can unzip directly without a passphrase), skip to Step 2.

Step 0 of 6 — encrypted bundles only

Verify the outer seal (no passphrase needed)

Before decrypting, confirm the encrypted bundle hasn't been tampered with in transit or storage. The encrypted bundle is delivered alongside an outer-manifest.json file containing the SHA-256 hash and a TSA timestamp over the encrypted blob.

sha256sum bundle.zip.enc
cat outer-manifest.json
# Confirm the recorded sha256 matches the value sha256sum just printed.

# Verify the outer TSA timestamp signs that hash:
ENCHASH=$(sha256sum bundle.zip.enc | cut -d' ' -f1)
openssl ts -verify -digest "$ENCHASH" \
  -in outer-manifest.tsr \
  -CAfile freetsa-cacert.pem

You should see: Verification: OK

What this proves: The encrypted file you received is byte-for-byte identical to what Sealed Ledger delivered. This works without the passphrase, so a third party can verify provenance of the encrypted blob even without authorization to read its contents.
Step 1 of 6 — encrypted bundles only

Decrypt the bundle

Use whichever tool the bundle was encrypted with. The bundle's README.txt records which tool was used.

If encrypted with openssl:

openssl enc -d -aes-256-gcm -pbkdf2 -iter 600000 \
  -in bundle.zip.enc -out bundle.zip
# You will be prompted for the passphrase.

If encrypted with age:

age -d -o bundle.zip bundle.zip.enc
# You will be prompted for the passphrase.

The decrypted file is now a standard .zip bundle. Continue with Step 2.

What this proves: You hold the correct passphrase. Decryption with an authenticated cipher (AES-GCM or age's ChaCha20-Poly1305) will fail loudly if the file was tampered with, regardless of whether you also verified the outer seal.
Step 2 of 6

Unzip the bundle

Move the bundle's .zip file to a folder of your choice and unzip it.

cd /path/to/your/folder
unzip qbosnapshot-CompanyName-2026-XX-XX-bundle.zip
cd qbosnapshot-CompanyName-2026-XX-XX-bundle

Inside, you should find:

  • snapshot.db — the accounting database
  • manifest.json — file inventory with hashes
  • manifest.json.sha256 — separate hash record
  • verify.sh and verify.js — automated verifier scripts (optional convenience)
  • VERIFY.md — quick-reference for the verifier
  • README.txt — bundle overview
  • LIMITATIONS.txt — explicit disclosure of what the seal does and does not claim
  • freetsa-cacert.pem — public certificate of the Time Stamp Authority
Step 3 of 6

Verify the database file's hash

The bundle records the database's expected SHA-256 hash inside manifest.json. Compute the actual hash and compare:

sha256sum snapshot.db

Then look at the value recorded in the manifest:

cat manifest.json

Find the entry for snapshot.db and confirm the sha256 value matches what sha256sum just produced.

What this proves: The database file has not been modified since the bundle was created. Any change — even a single byte — would produce a different hash.
Step 4 of 6

Verify the time-stamp signature

The bundle includes a cryptographic timestamp from an external Time Stamp Authority (FreeTSA by default). The timestamp signs the database's hash, proving the bundle existed in its current form at a specific moment.

First, extract the embedded timestamp token:

sqlite3 snapshot.db "SELECT tsa_timestamp_token FROM _seal" | base64 -d > token.tsr

Then verify it against the database's hash, using the bundled TSA certificate:

DBHASH=$(sha256sum snapshot.db | cut -d' ' -f1)
openssl ts -verify -digest "$DBHASH" -in token.tsr -CAfile freetsa-cacert.pem

You should see: Verification: OK

To see the timestamp itself:

sqlite3 snapshot.db "SELECT tsa_timestamp_at, tsa_authority FROM _seal"
What this proves: The bundle existed in this exact form at the recorded timestamp, and the timestamp was issued by the named Time Stamp Authority — not by Sealed Ledger. The TSA is an independent third party.
Step 5 of 6

Review the structural integrity checks

At capture time, SALT runs structural validity checks against the data and records the results. Any reviewer can inspect them:

sqlite3 snapshot.db "SELECT check_name, status, severity, actual_value FROM _integrity_checks"

Look for any rows where status is not pass:

sqlite3 snapshot.db "SELECT check_name, status, severity, actual_value
FROM _integrity_checks
WHERE status != 'pass'"

Common checks include:

  • journal_debits_equal_credits.accrual — total debits match total credits on accrual basis
  • journal_debits_equal_credits.cash — same on cash basis
  • transaction_index_count_matches_typed_tables — unified transaction index covers every transaction record
  • linked_txns_targets_resolved — every linked-transaction reference points to a real record
  • raw_json_sha256_matches_recomputed — random sample of records have matching provenance hashes
What this proves: The data inside the archive is internally consistent. Failed checks at blocking severity indicate structural problems with the original capture; failed checks at lower severities are informational.
Step 6 of 6

Identify the archive

Confirm the archive matches the company and capture you expect:

sqlite3 snapshot.db "SELECT * FROM _company"
sqlite3 snapshot.db "SELECT * FROM _capture"

The _company table identifies the source-system company (name, country, EIN if present). The _capture table records when the capture started and completed, what mode it ran in, and how many entities were captured.

What this proves: This archive is the one you expected — for the right company, captured at the right time.

Verification using the bundled scripts (alternative)

For convenience, every SALT bundle includes a verifier that performs the steps above automatically. The bundled scripts handle decryption (if needed) and all integrity checks, producing a pass/fail summary:

./verify.sh
# or
node verify.js

The manual steps above are equivalent and let you inspect each check individually.

How to interpret the results

If all steps pass

The archive is what it claims to be:

  • The encrypted blob (if applicable) is the one Sealed Ledger delivered
  • The bundle is unchanged since the timestamp recorded in the inner seal
  • An independent Time Stamp Authority — not Sealed Ledger — issued the seal
  • The data is internally consistent
  • The company identification matches

What this does not prove: that the source accounting system itself was authentic and complete at capture time. SALT cannot verify the source; it can only attest to what the source returned. See the threat model for a full description of what is and is not within scope.

If Step 0 (outer seal) fails

The encrypted blob has been modified or replaced since Sealed Ledger delivered it. Do not proceed with decryption — request a fresh copy.

If Step 1 (decryption) fails

Either the passphrase is incorrect, or the ciphertext was modified. Authenticated encryption (AES-GCM, age) refuses to decrypt corrupted ciphertext rather than producing garbage output, which is the safe behavior. Verify the passphrase first; if it's correct and decryption still fails, treat it as evidence of tampering and request a fresh copy.

If Step 3 (database hash) fails

The database file inside the unzipped bundle has been modified since the bundle was created. The archive cannot be trusted in its current form. Possibilities:

  • Accidental modification (a tool opened the file and changed metadata)
  • File corruption during transfer or storage
  • Deliberate tampering

If a clean copy of the bundle exists elsewhere, retrieve it. If not, the archive's seal is broken and its evidentiary value is compromised.

If Step 4 (TSA verification) fails

Possibilities, in order of likelihood:

  • The database hash from Step 3 doesn't match what the TSA originally signed (the bundle was modified)
  • The TSA certificate is missing or wrong
  • The token itself was modified

If Step 3 passed but Step 4 fails, the timestamp record was tampered with after capture. The data may still be the original capture, but the timestamp claim is broken.

If Step 5 (integrity checks) shows failures

Failed integrity checks indicate problems with the data at capture time. They do not indicate post-capture tampering — that's what the seal steps cover. Failed structural checks may indicate:

  • The source accounting file has its own integrity problems (out-of-balance journals, dangling references)
  • The capture itself encountered errors that were recorded but not blocking
  • The source platform returned incomplete data

These are findings to investigate, not verification failures. The seal is still valid; the underlying data has issues that predate the seal.

Querying the archive

Once verified, the archive can be queried like any SQLite database. The complete schema is published at /schema.html. A few starting points:

-- See every transaction in chronological order
SELECT entity_type, txn_date, doc_number, total_cents
FROM transaction_index
ORDER BY txn_date;

-- Open invoices (unpaid AR) by aging
SELECT customer_name, doc_number, balance_cents, days_overdue
FROM open_ar_items
WHERE balance_cents > 0
ORDER BY days_overdue DESC;

-- Search transactions by keyword
SELECT * FROM search_transactions
WHERE search_transactions MATCH 'check OR refund'
LIMIT 50;

-- Source-system reports (e.g., Profit & Loss, Balance Sheet)
-- as the source platform produced them
SELECT report_name, basis, fy_end_year FROM report_snapshots;

For deeper exploration, any SQLite-compatible tool — DB Browser for SQLite, DBeaver, or your preferred tool — can browse the database visually.

This guide is published as a draft and may be revised. A printable PDF version will be made available alongside future revisions.