# Verify an audit export

> Download a workspace's signed ledger and check it with a standalone verifier.

Every decision a workspace's agent or its people make, including a payment on Arc testnet, is appended to the workspace's ledger, hash-linked to the entry before it and signed with Ed25519. The Audit log page checks that chain for you. An export lets someone else check it, with their own copy and without Vestiarion: an auditor, your accountant, or a counterparty.

## 1. Download the ledger

Open **Audit log**. Next to the verification result, under **Download**, choose:

- **Signed JSON**: the whole chain, oldest entry first, with the public keys that vouch for it. This is the file that verifies.
- **CSV**: the same entries as a spreadsheet, one row per entry. It is for reading, not for checking: a spreadsheet may change the text it opens, and cells that would run as formulas are prefixed with `'`.

Every member of the workspace can export, viewers included. Each export is itself recorded in the ledger, as `ledger_exported`, with who exported it, the format, and how many entries it held.

## 2. What the file holds

The JSON file has the format `vestiarion-ledger-export/1`:

- `workspace`: its slug and name;
- `head`: the last entry in the file, by `seq` and `hash`;
- `keys`: every public key the workspace accepts signatures from, each with its key id;
- `verification`: what Vestiarion's own check said when the file was made;
- `entries`: every entry, exactly as stored: `seq`, `id`, `ts`, `actor`, `domain`, `action`, `summary`, `detail`, `body_hash`, `signature`, `prev_hash`, `hash` and `signing_key_id`.

An entry's `body_hash` is the SHA-256 of the canonical JSON of its `actor`, `domain`, `action`, `summary` and `detail` (keys sorted, no whitespace). Its `signature` is an Ed25519 signature over that hash. Its `hash` is the SHA-256 of `prev_hash`, `body_hash` and `signature` joined, and the first entry's `prev_hash` is 64 zeros.

## 3. Check it

Download the verifier, [verify-ledger-export.mjs](https://www.vestiarion.xyz/tools/verify-ledger-export.mjs). It is one file with no dependencies beyond Node.js 20 or later. Then run:

```bash
node verify-ledger-export.mjs vestiarion-your-workspace-ledger-392.json
```

It answers with one of three results:

- **VALID** (exit code 0): every entry's content matches its hash, every signature verifies, and every link holds, from the first entry to the head. It prints the head and the key ids it trusted.
- **BROKEN** (exit code 1): it names the first entry that fails and why: its content was changed, its signature does not verify, an entry is missing or out of order, or an entry is malformed, such as a missing field or a hash that is not hex.
- **NOT CHECKED** (exit code 2): it could not check the file at all. The file is not an export, an entry names a key the file does not hold, or an entry was signed by a key you did not pass with `--public-key`.

## 4. Compare the key id and the head

A file can only show that it agrees with itself. Someone who rewrote the entries could re-sign them with a key of their own and put that key in the file. So, before relying on a **VALID** result, compare the key id and head hash the verifier prints with the workspace's **Audit log** page. The key id is shown beside **Ledger signing public key**, and the head hash has a copy button. The page says: "Compare this key id and the head hash with the ones a verified export prints."

If you already hold the workspace's public key, pass it in, and only the key(s) you pass are trusted:

```bash
node verify-ledger-export.mjs vestiarion-your-workspace-ledger-392.json --public-key workspace-key.pem
```

If the workspace has rotated its signing key, an entry from before the rotation needs the retired key, not just the current one — the Audit log page lists every key it accepts. Pass every one of them: repeat `--public-key`, once per key,

```bash
node verify-ledger-export.mjs vestiarion-your-workspace-ledger-392.json --public-key current-key.pem --public-key retired-key.pem
```

or put all the public keys in one file, one after another, and pass that file once.

## 5. After a key rotation

An owner can replace a workspace's signing key: open **Settings** → **Ledger signing key** → **Rotate signing key**. They might do this because a key is suspected of leaking, or just as routine hygiene — either way, entries already written keep verifying, since each one carries the id of the key that signed it. Anyone else who opens that panel sees why they can't: "An owner of this workspace can rotate the key."

The old public key does not disappear. It stays listed under **Retired keys**, both on the **Ledger signing key** panel in Settings and on the **Audit log** page, next to the current key. The rotation itself is recorded in the ledger like any other decision, as `ledger_key_rotated`, naming both the retired and the new key id and who rotated.

For verifying an export from a workspace that has rotated its key, see [§4](#4-compare-the-key-id-and-the-head) above: pass every key the export's entries were signed with, the retired one and the current one, by repeating `--public-key` or listing them in one file.

If you operate a deployment that still sets `LEDGER_SIGNING_KEY` for the founding workspace, that environment copy becomes a retired private key once the workspace rotates: remove it from hosting. The app reads the signing key from the workspace itself, and `npm run org:adopt-env` refuses a retired key.
