To verify a Praesidia audit bundle offline, build the open-source verifier, @praesidia/audit-verifier, and run its praesidia-verify command against the exported audit package on your own machine. It checks the signatures, the hash chain, the Merkle inclusion proofs, any Sigstore Rekor receipts and the platform key attestation, prints one line per proof and a final RESULT, and exits with a code that a script can act on. It never contacts Praesidia.
This walkthrough is for the auditor, the customer security team or the compliance lead who has to decide whether a bundle can be relied on. It uses the sample bundles that ship with the verifier, so you can run every step before you have a real export. Every command and every output block below comes from a real run of version 0.11.1 at commit cb81fb2 on 17 October 2026. Output blocks are excerpts of that output: lines may be left out, for example the title banner or the closing NOTE and WARNING, but none is changed, and ... marks a gap inside a block. For why an audit trail needs this kind of check at all, start with the AI agent audit trail guide.
What you need
- Node.js 22.12 or newer. The verifier has no third-party runtime dependencies: signature checks use Node's built-in crypto, and the Merkle-proof, canonical-JSON, Rekor-receipt and ZIP-parsing logic is in its own source.
- git and npm to fetch and build it. This is the only step that needs the network.
- A bundle. Use the samples for this walkthrough. For a real check, an audit package exported from your Praesidia organization (see verify a real export).
Get the verifier
The verifier is published as Apache-2.0 source at github.com/praesidia-ai/audit-verifier. Its package name is @praesidia/audit-verifier, but when we checked on 17 October 2026 it was not yet on the public npm registry, so it cannot be installed from there. Build it from source, or ask your Praesidia contact for a packaged build.
git clone https://github.com/praesidia-ai/audit-verifier.git
cd audit-verifier
git checkout cb81fb286b4809a2578ff2e76109cfee29d21e19 # 0.11.1, the version used below
npm ci
npm run build
The build writes the command to dist/cli.js. Run it as node dist/cli.js; once the package is installed from a registry, the same program is called praesidia-verify. node dist/cli.js verify <bundle> is an alias of the form used below.
Pinning a commit matters. The verifier carries a compiled-in copy of Praesidia's production platform key (see the release trust anchor section below), so the exact source you build is part of what you trust.
The sample bundles
The repository's samples/ folder holds three audit packages and one public key. They are signed with test keys whose private halves are public, so a sample that verifies proves that the verifier works and nothing about Praesidia.
| File | What it is |
|---|---|
audit-package.valid.zip |
An audit package holding a signed bundle with 4 hash-chained rows, one signed Merkle root and a platform attestation, plus its verification.txt receipt and the decision disclosures for one decision, approval and outcome |
audit-package.corrupted.zip |
The same package with one byte of the signed approval row flipped. The unsigned receipt was recomputed, so only the signatures can catch it |
audit-package.wrong-key.zip |
A well-formed package whose platform attestation was signed by a different key |
sample-platform-key.pem |
The public half of the sample platform key |
The samples carry no Rekor anchor, because only Sigstore can sign one. So every sample run below passes --no-rekor, and --platform-key to tell the verifier to trust the sample key instead of the production key it was built with.
Step 1: verify the valid sample
S=samples
node dist/cli.js $S/audit-package.valid.zip \
--platform-key $S/sample-platform-key.pem --no-rekor --summary
echo "exit=$?"
PASS signature
PASS hash chain
PASS decision receipt
PASS policy reference
PASS evidence integrity
NOT_PRESENT target receipt
RESULT: OK
NOTE: --no-rekor was passed — the external Rekor witness was NOT checked. Non-Rekor anchors were still evaluated when present.
WARNING: platform key supplied by caller — result is only as strong as the provenance of that key file. Obtain it from Praesidia's published trust-anchor document over a channel independent of this bundle, and pin it with --platform-key-fingerprint <sha256hex>.
exit=0
Read the two lines after RESULT before the result itself. The NOTE says Rekor was not checked, so this OK says nothing about external anchoring. The WARNING says you chose the platform key, so the result is only as strong as the place that key came from. The verifier prints both on every run that uses those options, so a skimmed OK cannot be mistaken for more than it is.
To bind the key you supplied to a fingerprint you got some other way, add --platform-key-fingerprint. The sample key's SPKI SHA-256 fingerprint is published in samples/README.md:
node dist/cli.js $S/audit-package.valid.zip --no-rekor \
--platform-key $S/sample-platform-key.pem \
--platform-key-fingerprint fb4b6246b3670bbafd1c0b84b3025c0be8f486d008395fba755657e583fc7139
A fingerprint that does not match stops the run before any check, with exit code 2:
error: cannot load platform key: platform key fingerprint mismatch: file hashes to fb4b6246b3670bbafd1c0b84b3025c0be8f486d008395fba755657e583fc7139, --platform-key-fingerprint expects 0000000000000000000000000000000000000000000000000000000000000000
Step 2: read the full report
Drop --summary to see the component checks behind the six proof lines. This is the valid sample. Left out: the two-line title banner, the six proof lines, the two-line evidence privacy summary before RESULT, and the closing NOTE and WARNING:
package: VALID evidence/audit-bundle.zip vs verification.txt (unsigned receipt)
unsigned side artifacts (not verified): none
org: 5a3b1e00-0000-4000-8000-00000000a011
from: 2026-09-01T00:00:00.000Z
to: 2026-09-01T01:00:00.000Z
rows: 4/4 (seen/declared)
roots: 1/1 (seen/declared)
proofs: 4
tenant key v1: ACTIVE sha256 179b77a9eacc88fb28293ac0eee057fe21d487f80a409625f135b0367554d3b9 (platform-attested 2026-09-01T01:00:06.000Z)
[VALID ] manifest 1 checked
[VALID ] row signatures 4 checked
[VALID ] chain integrity 3 checked
[VALID ] root signatures 1 checked
[VALID ] inclusion proofs 4 checked
[NOT_PRESENT] rekor receipts 0 checked (rekor_check_skipped_by_caller: --no-rekor was passed — Rekor receipts were NOT checked; non-Rekor anchors, if present, were still evaluated.)
[VALID ] platform attest. 1 checked
[VALID ] completeness 2 checked
[VALID ] key binding 1 checked
[VALID ] root coverage 1 checked
[NOT_PRESENT] integrity chkpts 0 checked
[UNSUPPORTED] action chain 0 checked
... (eight more action-evidence checks, all UNSUPPORTED)
[VALID ] decision receipts 2 checked (2 opened; 0 withheld (unsigned trailer); 0 commitment-signed decision rows in the bundle not opened)
[VALID ] policy reference 2 checked (1 distinct (policyId, policyVersion); 0 decisions name no policy; 0 pre-Decision-Record-v1 payloads carry none; policy text is not in the package and is NOT verified)
policy pol-outbound-email@4 (reference only; policy text not verified)
RESULT: OK
The header ties the run to one organization, one time window and one tenant signing key. rows: 4/4 and roots: 1/1 compare what is in the archive with the counts the signed manifest declares.
Each component has one of five statuses:
VALID: checked and passed.INVALID: checked and failed. Any INVALID component makes the resultFAIL.INCOMPLETE: the evidence present is not enough to decide.NOT_PRESENT: the check applies, but the bundle gave it nothing to check, as with Rekor receipts under--no-rekor.UNSUPPORTED: the bundle's format version carries no evidence of this kind. The sample uses an older manifest version with no protected-action events, so the nine action-evidence checks report UNSUPPORTED.
NOT_PRESENT and UNSUPPORTED never fail a run on their own, which is why you read the component lines and not only RESULT. A proof line is FAIL if any of its components is invalid, INCOMPLETE if any is incomplete, PASS if any is valid, and NOT_PRESENT otherwise.
What each check proves
The six proof lines group the components like this:
| Proof line | Components behind it |
|---|---|
| signature | manifest, row signatures, root signatures, platform attestation, key binding |
| hash chain | chain integrity, inclusion proofs, Rekor receipts, completeness, root coverage, integrity checkpoints |
| decision receipt | decision receipts |
| policy reference | policy reference |
| evidence integrity | the package's verification.txt receipt, plus the protected-action evidence checks |
| target receipt | target acknowledgement |
Signatures: who wrote the records
Under the default configuration, rows are signed when they are written; an operator can turn signing off. The verifier recomputes the canonical bytes of the rows in the bundle and checks their signatures against the tenant keys the rows name. The signed bytes include the hash of the previous row, so a row signature also fixes the row's position in the chain. The manifest, which declares the organization, the time window and the row and root counts, carries its own signature, and so does every Merkle root.
What it proves: no signed row, root or manifest field was changed after signing, and the signing keys belong to the bundle's own key set and were not revoked. A signature made under a revoked key is rejected even if it was made before the revocation, because the signing time is not itself signed.
Hash chain: nothing removed or reordered in the middle
Each row records the hash of the row before it. The verifier rebuilds the order from these links, not from the order of lines in the file, and fails on a broken link, an orphan row or two rows claiming the same predecessor. A bundle covers a date range, so the first row may point to history before the range; the verifier accepts exactly one such opening link.
What it proves: no row was inserted, deleted or reordered inside the exported range. On its own, a chain cannot show that its newest rows were cut off: a shortened chain is still a valid chain. The next two checks narrow that gap.
Merkle roots: what each period committed to
Praesidia groups rows into signed Merkle roots, one per rooted period. The verifier requires one RFC 6962-style inclusion proof per exported row and walks it up to a signed root, with the leaf and node prefixes the standard defines. Two count checks build on that:
- Completeness: the rows and roots in the archive must equal the counts in the signed manifest, which catches rows removed from the archive after export.
- Root coverage: for every root whose whole period falls inside the bundle's range, the rows and proofs present must equal the row count the root itself signed. A root's signed count does not shrink when rows are later deleted from the database, so a fully covered period with fewer rows than its root committed to fails, even when the manifest was honestly regenerated with the smaller count.
What it proves: each row belongs to the period its root committed to, and no fully covered period lost rows. A period that has not been rooted yet, or that sits only partly inside the range, is not covered this way.
Sigstore Rekor anchors: a witness the operator does not control
When external anchoring is enabled for an organization, each Merkle root is logged in the public Sigstore Rekor transparency log, and the receipt travels in the bundle. Unless you pass --no-rekor, the verifier checks every receipt offline: the signed entry timestamp against a Rekor public key compiled into the verifier, the inclusion proof against Rekor's signed checkpoint, and the logged entry against this root's exact hash and signature. Rekor's signed integration time is also compared, with 24 hours of tolerance, against the signing and anchoring times the bundle records for the root. Those recorded times are not themselves signed, and a root that records no anchoring time gets no upper bound.
What it proves: an entry for this exact root and signature is in the public Rekor log, its inclusion in the log is verified, and the root existed by the time Rekor logged it. Praesidia cannot rewrite that log, so if you keep a receipt or an earlier copy of the bundle, a later rewrite of the history up to that root no longer matches what you hold. The verifier checks only the receipts in the bundle you give it. It does not cover rows written after the last anchored root.
A root without a receipt fails closed. Run the valid sample without --no-rekor and the hash chain proof fails:
[INVALID ] rekor receipts 1/1 failed first=root-sample (no_external_witness: no root in this bundle carries an anchor receipt at all — this looks like external anchoring was never enabled for this org/deployment (or was off for this export), not a gap in existing coverage. Tenant/platform signatures are unaffected, but tamper cannot be bounded against an independent transparency log.)
The reason distinguishes a bundle in which no root is anchored, as here, from one in which only some roots lack a receipt (anchor_missing_for_partially_anchored_bundle). The second is a narrower and more serious finding, such as an anchoring outage or a deleted receipt.
Release trust anchor: who vouches for the keys
The tenant keys that sign rows are listed inside the bundle, so on their own they are the bundle's claim about itself. The platform attestation closes that loop: Praesidia's platform key, held in AWS KMS, signs a statement binding each tenant key to the organization, with its status and any revocation time. The verifier checks that attestation against one of three trust sources:
- The key compiled into the verifier. Since version 0.11.0 the source build embeds Praesidia's production platform key, SPKI SHA-256 fingerprint
3f7a069ab0e30bd7c1765c8fa9f7b8f0bc164bb5363500d111f5ec0deb4a0a6d. With no key option, a bundle attested by that key verifies and any other fails. That is why the samples need--platform-key. Without it, the valid sample fails withsignature: platform attestation does not verify under pinned key. - A trust-anchor document passed with
--trust-anchor <file>. Praesidia publishes it at/.well-known/praesidia-audit-keys.jsonon the API host (for the hosted service,https://api.praesidia.ai/.well-known/praesidia-audit-keys.json). The verifier never fetches it; you download it and pass the local file. On 17 October 2026 it listed one active key, with the same fingerprint as the compiled-in key. - A single key file passed with
--platform-key, ideally with--platform-key-fingerprint, as in step 1.
Options 2 and 3 make you the trust anchor, and the verifier prints a WARNING saying so. A build that embeds no key, run without either option, reports UNANCHORED with exit code 5. UNANCHORED does not count as a pass. Whichever source you use, confirm the fingerprint with Praesidia over a channel independent of the bundle and of the download: a key that arrives in the same email as the bundle proves nothing. For the full procedure, see docs/trust-anchor-verification.md in the verifier repository.
Decision receipts and policy references
Newer decision rows sign a commitment to their details rather than the details themselves, so that personal data can be erased later without breaking the chain. The package's unsigned evidence/decision-receipts.ndjson opens those commitments, and the verifier checks each opening against the signed row it names. The policy reference check then reports which policy and version each verified decision named. It verifies the reference only: the policy text is not in the package.
Evidence integrity and the package receipt
An audit package is an outer ZIP holding the signed bundle at evidence/audit-bundle.zip, an unsigned verification.txt receipt with that bundle's SHA-256 and byte count, and unsigned reports. The verifier checks the receipt against the inner bundle and lists everything else as not verified. Only the inner bundle's signatures authenticate anything. For bundles with protected-action evidence, this proof line also covers the action-event checks; they are described in the action evidence quickstart.
What a failure looks like
One flipped byte
The corrupted sample changes one byte of the signed approval row and recomputes the unsigned receipt, so the package checksum still matches. In the output below, the two-line title banner is left out and ... marks the other omitted lines:
node dist/cli.js $S/audit-package.corrupted.zip \
--platform-key $S/sample-platform-key.pem --no-rekor
FAIL signature
FAIL hash chain
INCOMPLETE decision receipt
INCOMPLETE policy reference
PASS evidence integrity
NOT_PRESENT target receipt
package: VALID evidence/audit-bundle.zip vs verification.txt (unsigned receipt)
...
[INVALID ] row signatures 1/4 failed first=row-approval (row signature does not verify)
[INVALID ] chain integrity 2/4 failed first=row-outcome (prev_row_hash does not chain to previous row)
[VALID ] root signatures 1 checked
[INVALID ] inclusion proofs 1/4 failed first=row-approval (inclusion proof does not verify against root)
...
RESULT: FAIL
Exit code 1. One changed byte fails three independent checks: the row's own signature, the next row's link to it, and its inclusion proof into the signed root. The package line still says VALID, which is the point of the sample: an unsigned checksum is easy to recompute, a signature is not. The decision disclosures still open correctly, but they ride on rows that no longer verify, so they are INCOMPLETE rather than PASS.
An attestation from the wrong key
node dist/cli.js $S/audit-package.wrong-key.zip \
--platform-key $S/sample-platform-key.pem --no-rekor --summary
FAIL signature
PASS hash chain
PASS decision receipt
PASS policy reference
PASS evidence integrity
NOT_PRESENT target receipt
RESULT: FAIL
Exit code 1. Every row, link and proof is intact; only the platform attestation fails, because it was not signed by the key you trust. Without that check, anyone could produce an internally consistent bundle under keys of their own.
The same thing happens if you pass Praesidia's real trust-anchor document with a sample bundle, which is a useful test that your trust anchor is doing its job:
[INVALID ] platform attest. 1/1 failed (trust_anchor_key_not_found: attestation is signed by platform key fb4b6246b3670bbafd1c0b84b3025c0be8f486d008395fba755657e583fc7139, which the supplied trust anchor does not list)
Exit codes
| Code | Status | Meaning |
|---|---|---|
| 0 | valid | Every signature, chain link and proof verified |
| 1 | invalid | A real verification failure |
| 2 | error | Unreadable or malformed bundle, missing file, bad option, or a fingerprint mismatch |
| 3 | incomplete | The evidence present is not enough to decide, for example rows exported before their hour was rooted, or legitimately redacted evidence |
| 5 | unanchored | No platform trust anchor at all, so the keys are the bundle's own claim. Never a pass |
--quiet prints only the status word (OK, FAIL, INCOMPLETE or UNANCHORED), and --json prints the full report as stable JSON, so the verifier can gate a pipeline or an evidence-collection job. Exit code 4 belongs to verify-set, which checks that several consecutive bundles chain into one another.
Verify a real Praesidia export
- Export an audit package. Organization owners and compliance officers can request one for a chosen period and download it when it is ready. It contains the signed bundle, the decision receipts,
verification.txtand unsigned reports. - Get the trust anchor through a separate channel. Download
/.well-known/praesidia-audit-keys.jsonfrom your API host yourself, and confirm the key fingerprint with your Praesidia contact. With a verifier built at version 0.11.1 or later you can also rely on the compiled-in production key, after confirming its fingerprint the same way. - Run the verifier on the package as a whole, so the receipt is checked too. Leave out
--no-rekorif your organization has external anchoring enabled:
curl -fsSo praesidia-audit-keys.json https://api.praesidia.ai/.well-known/praesidia-audit-keys.json
node dist/cli.js audit-package.zip --trust-anchor praesidia-audit-keys.json
The verification.txt in your package prints an equivalent command that fetches the verifier from the npm registry. That form works only once the package is published there; until then, run the built dist/cli.js as above.
- Record what you ran: the verifier commit, the trust-anchor fingerprint, the package SHA-256 and the full output. Those four items let someone else repeat your check and reach the same result.
What a pass does not prove
A RESULT: OK is a statement about the records in the bundle. It does not show:
- That every action was recorded. An action that never passed through Praesidia, or was taken while signing was turned off, leaves no row, and a missing row that was never written looks the same as no activity.
- That the newest records were not removed. Rows after the last anchored root, or a whole recent period deleted together with its root, can fall outside what the chain, the roots and the anchors cover. The verifier's own documentation sets out these residual cases in detail.
- That an approved action was the right one. A decision record shows which policy and version applied and that the record was not altered. It does not show who chose the arguments the agent proposed.
- Anything about the live system. It is a static check of one file, not of uptime, retention jobs or current behavior.
For the wider design questions behind these limits, see tamper-evident audit logs with cryptographic proofs. To see the chain and tail-truncation limits in a teaching format, run the signed audit log verification example.
Common questions
Do I need a Praesidia account to verify an audit bundle?
No. The verifier runs on your own machine, reads only the files you give it and never contacts Praesidia. You need the bundle, the verifier built from its public source or obtained from your Praesidia contact, and a platform key or trust-anchor document confirmed through a channel independent of the bundle.
Is the audit verifier on npm?
Not when this page was checked on 17 October 2026. The package is named @praesidia/audit-verifier and its command is praesidia-verify, but the public registry does not list it yet. Build it from the Apache-2.0 source on GitHub, or ask your Praesidia contact for a packaged build.
Why do the sample bundles need --platform-key and --no-rekor?
The samples are signed with test keys, not Praesidia's production key, so the verifier rejects their attestation unless you explicitly trust the sample key. They also carry no Rekor receipt, because only Sigstore can sign one, and a root without a receipt fails closed unless you pass --no-rekor. The verifier prints a WARNING and a NOTE whenever these options are used.
What is the difference between FAIL, INCOMPLETE and UNANCHORED?
FAIL (exit 1) means a check ran and the evidence did not verify. INCOMPLETE (exit 3) means the evidence present was not enough to decide, for example rows whose hour had not been rooted yet. UNANCHORED (exit 5) means the verifier had no trusted platform key at all, so the bundle's keys are only its own claim. None of the three is a pass.
Can I use the verifier output as SOC 2 or ISO/IEC 42001 audit evidence?
It can support it. The verifier shows that a set of decision records is authentic and intact, which is evidence that logging and change controls operated. Whether that satisfies a control is your auditor's judgement, and Praesidia's controls are mapped to these frameworks, not certified against them. See the SOC 2 and ISO/IEC 42001 mappings.
Further reading
- AI agent audit trail: the complete guide: what a trail should hold and how to judge one.
- Verify your audit trail offline: the summary of what the verifier checks.
- AI agent evidence and proof and AI audit evidence: the decision records the bundle is built from.
- Build an AI agent audit trail for SOC 2 evidence: the use case end to end.
- Tamper-evident log and AI agent audit trail, defined in the glossary.
- EU AI Act compliance for AI agents: the record-keeping articles this evidence supports.
Primary sources: audit-verifier source and samples · RFC 6962 · RFC 8032 · Sigstore Rekor.