---
title: "Verify offline"
---

# Verify offline

Use keystone-audit-verify to export the audit chain and prove it intact without network access, including the cold tier and an organisation's own segments.

`keystone-audit-verify` is a standalone verifier shipped with each release. It checks an exported audit chain cryptographically without connecting to any Govern server, so an air-gapped operator or an external auditor can run it on a machine with no network.

## Commands

| Command | What it does |
|---|---|
| `export` | Exports the audit chain, Merkle tree heads and public key from a live database |
| `verify` | Verifies an exported chain offline |
| `cold` | Verifies cold tier segments (Parquet objects) offline, alone or joined to a hot export |
| `version` | Prints the version |

### Exit codes

| Code | Meaning |
|---|---|
| 0 | Every verification passed |
| 1 | Fatal error: the verification could not run |
| 2 | Verification failed: the chain was tampered with or a signature is invalid |
| 3 | An agent export cannot be verified (pseudonymised entries) |

## Export and verify the hot tier

The export reads the instance-wide chain and the signing keys across organisations, so it connects as the `keystone_platform` role, not as the runtime role.

```bash
keystone-audit-verify export --db "$DATABASE_PLATFORM_URL" --from 2026-01-01 --to 2026-09-30

keystone-audit-verify verify --chain audit_chain.json --tree-heads audit_tree_heads.json \
  --public-key keystone_audit.pub --output report.json
```

### Export flags

| Flag | Meaning |
|---|---|
| `--db URL` | PostgreSQL URL of the `keystone_platform` role, or set `DATABASE_PLATFORM_URL` |
| `--org ORG_ID` | One organisation; empty for all |
| `--from DATE`, `--to DATE` | Window, `YYYY-MM-DD` |
| `--out-chain FILE` | Default `audit_chain.json` |
| `--out-tree-heads FILE` | Default `audit_tree_heads.json` |
| `--out-public-key FILE` | Default `keystone_audit.pub` |

### Verify flags

| Flag | Meaning |
|---|---|
| `--chain FILE` | Exported chain (required). A signed agent export is detected and verified as one |
| `--tree-heads FILE` | Merkle tree heads; enables Merkle verification |
| `--public-key FILE` | Ed25519 public key for Merkle and timestamp signatures |
| `--hmac-key KEY` | `NI_AUTH_AUDIT_HMAC_KEY`, to re-derive every entry hash |
| `--output FILE` | Write the report as JSON |
| `--format json\|text` | Output format (default text) |
| `--strict` | Fail if any optional check cannot be performed |

Without `--hmac-key` the verifier checks links, sequence numbers and signatures; with it, it also recomputes every entry hash. The HMAC key is a secret of the operator: give it to an auditor only under your own procedure.

The verifier checks continuity per organisation chain and lane, from each chain's genesis, which commits to the end of the ledger written before per-organisation chains existed.

## Verify the cold tier

When audit tiering is on, entries older than the hot window are in the audit bucket. Verify the whole ledger, both tiers, end to end:

```bash
aws s3 sync s3://<audit bucket>/audit/<cell> ./audit-bucket
keystone-audit-verify export --db "$DATABASE_PLATFORM_URL" --from 2026-01-01 --to 2100-01-01
keystone-audit-verify cold --dir ./audit-bucket --keys keystone-audit-keys.json \
  --hmac-key "$NI_AUTH_AUDIT_HMAC_KEY" --hot audit_chain.json
```

`keystone-audit-keys.json` is the key set published at `/.well-known/keystone-audit-keys.json`.

For every segment the verifier checks the manifest signature, the spine (SHA-256, id range, every chain link, first and last hashes) and every object (SHA-256, each row present in the spine at its id, one organisation per object, the objects together covering the spine), and with the HMAC key re-derives every entry hash. Across segments it checks sequence numbers, the manifest chain and the link from each segment to the next. A platform verification requires segment 1: a missing first segment fails.

With `--hot`, the hot export is checked against the segments: entries above the last segment must continue its chain link by link. A missing, reordered or altered entry, or a gap at the boundary between the tiers, fails. After tiering, the hot export has gaps where entries moved; verify it with `cold --hot`, not with `verify` alone.

### Cold flags

| Flag | Meaning |
|---|---|
| `--dir DIR` | The segments as stored in the bucket, or an organisation's download of its own segments (required) |
| `--keys FILE` | The audit signing key set |
| `--hmac-key KEY` | Re-derive every entry hash |
| `--org ORG_ID` | Verify one organisation's objects and their completeness |
| `--platform-entries` | Verify the platform entries' objects |
| `--hot FILE` | A hot tier export of every organisation |
| `--output FILE`, `--format json\|text` | Report |
| `--strict` | Fail without the key set or the HMAC key (and, for a platform verification, without `--hot`) |

## An organisation verifies its own entries

An organisation does not need the operator's HMAC key or anyone else's data to prove its own cold entries complete and in the chain. With `audit:read`:

```bash
# list the organisation's sealed segments
curl -H "Authorization: Bearer $TOKEN" https://<your Govern host>/api/v1/audit-logs/cold-segments

# download one segment as a tar: manifest, signature, spine and the organisation's object
curl -H "Authorization: Bearer $TOKEN" -o bundles/seg-12.tar \
  https://<your Govern host>/api/v1/audit-logs/cold-segments/12/bundle

keystone-audit-verify cold --dir ./bundles --keys keystone-audit-keys.json --org <organization id>
```

Each download is audited (`audit.cold_segment.downloaded`) before it is sent. Segments without the organisation's entries are reported as warnings; consecutive bundles must still link. The sequence as a whole is proven by the operator's platform verification.

Neither the bucket listing, nor a manifest, nor the spine names a tenant: an organisation finds its own object from its own id, and cannot tell which rows belong to the same other tenant or when they were written.
