Sealrdocs
Guides

Verifying evidence

For auditors — get the open-source verifier, run it against a .seal bundle, read the report, and check the conformance vectors. Nothing from the vendor is required.

You have been handed a .seal evidence bundle. This page is how you check it without trusting Sealr and without trusting the party that produced it: no account, no network, no cooperation from anybody.

Everything the verifier needs is inside the bundle or supplied by you.

1. Get the verifier

sealr-verify is a standalone binary. Build it from source, which is the option that requires the least trust:

git clone https://github.com/sealr-labs/sealr
cd sealr
cargo build --release -p sealr-verify
./target/release/sealr-verify --version

Or take a release binary. Every release archive contains both sealr and sealr-verify for Linux (x86_64), macOS (x86_64 and arm64) and Windows (x86_64), and ships with a SHA256SUMS file plus GitHub build-provenance attestation:

sha256sum -c SHA256SUMS
gh attestation verify sealr-0.1.0-x86_64-unknown-linux-gnu.tar.gz -R sealr-labs/sealr

There is no self-update path in either binary. sealr version prints the version and tells you to verify the next release yourself.

If you would rather read the code than run it, the parts that decide anything are crates/verify, crates/bundle and crates/evidence.

2. Run it

sealr-verify bundle.seal                     # human-readable report
sealr-verify bundle.seal --json              # machine-readable report
sealr-verify bundle.seal --root root.pem     # pin a trust root you obtained out of band

Two further flags matter when you have side information:

# The console key that countersigned checkpoints, if you obtained it separately.
sealr-verify bundle.seal --console-key console.pem

# The authoritative extent of a stream, from GET /v1/streams/{id}/integrity (max_seq).
sealr-verify bundle.seal --expect-high-water 01J4T…=8421

--expect-high-water is the one that turns tail truncation into a hard failure; see the standing limits below for why a bundle alone cannot rule it out.

The holder of a stream can produce a bundle with:

sealr export -o bundle.seal --level full

sealr verify bundle.seal [--root root.pem] runs the same checks and returns the same exit codes, so someone who already has the recorder installed does not need a second binary. It accepts fewer flags than sealr-verify.

3. Exit codes

CodeResultMeaning
0passno findings
10pass with findingsat least one warning, no errors
11failat least one error finding
30usage errorunreadable bundle, unreadable --root or --console-key
2bad argumentsa malformed flag, including --expect-high-water without STREAM=SEQ

Findings carry a severity and a stable machine class (record_hash_mismatch, checkpoint_signature_invalid, anchor_invalid, chain_fork_detected, truncation_detected, …), so you can assert on classes in a script rather than on prose.

4. Reading the report

sealr-verify 0.1.0 — bundle 01J…
manifest hash (BLAKE3/JCS): 9f2c…

RESULT: PASS

Proven: 1284 records across 1 stream(s); 1284 attested by checkpoints; 1190 anchored.
Recorded span: 2026-07-01T09:12:03.221Z .. 2026-08-11T17:44:58.907Z
Earliest timestamp anchor: 2026-07-01T09:20:00.000Z

Findings: none.

Standing limits:
  - …
  • attested — records covered by at least one valid checkpoint.
  • anchored — records covered by a valid checkpoint that also carries a verified timestamp token. Anchored is the number that gives you proven time.
  • Standing limits are printed in every report, on pass and on fail. They are part of the output on purpose: a report that only said "PASS" would be read as proving more than it does.

5. What each check asserts

Container integrity. Every zip entry matches the manifest's BLAKE3-256 hash of its stored bytes, and any entry not listed in the manifest is an error. The manifest is written last and hashes everything before it.

Chain integrity. Every record's hash recomputes from its JCS form with record_hash removed; prev_hash links to the previous record; sequence numbers are strictly monotonic; the genesis rule holds. Any insertion, deletion or reordering fails.

Attestation. Checkpoints are Ed25519-signed Merkle roots. Signatures verify against the recorder's certificate, that certificate chains to the trust root, and each root recomputes from the records it claims to cover.

Countersignatures. The control plane's countersignature on each checkpoint verifies against the console key attested by the root. This is the anti-rollback property: a holder cannot quietly re-issue an earlier state.

Revocations. A checkpoint signed after its certificate's revocation time fails, and the report names the boundary.

Time anchors. RFC 3161 tokens verify offline against the signer certificates embedded in the tokens, including eIDAS qualified timestamps over daily roots. Each anchored checkpoint yields a proven "no later than" time. Whether a batch was qualified is reported as recorded, not independently proven.

Forks. Two validly signed checkpoints covering the same range with different roots are reported as chain_fork_detected.

Coverage honesty. Unattested tails, segments that ended without a clean shutdown, and coverage_gap records are reported rather than hidden.

6. What the verifier does not assert

These are the standing limits, quoted from what the tool itself prints:

  • Tamper-evident, not tamper-proof. The report proves integrity, ordering, timing and origin of the recorded stream. It cannot prove that unrecorded events did not happen.
  • Coverage is a deployment property. Only operations that passed through a recorder are in the stream. See Coverage for how bypasses are made visible from the other side.
  • Proven time comes from anchors. A record is proven to exist no later than its earliest anchor; "no earlier than" is bounded by the previous anchor and monotonic clock deltas.
  • Attribution quality is as recorded. Each record states whether its human principal was attributed, inferred or unattributed. The verifier does not independently prove identity.
  • Tail completeness is bounded, not proven. Removing trailing records together with the checkpoints attesting them leaves a shorter, self-consistent chain. Pass --expect-high-water STREAM=SEQ to make the true extent decidable.

7. Trust roots

The verifier picks a root in this order:

  1. --root <file> — a root you obtained out of band. Use this when it matters.
  2. The roots embedded in a release build of the verifier.
  3. The bundle's own pinned roots — last, and with an explicit warning, because a forged bundle could pin its own root.

Within the bundle, daily consolidated roots are recomputed from their batch roots, their console signature is checked against the attested console keys, and any qualified timestamp over a daily root is verified — all offline. A mismatch is daily_root_mismatch, a bad signature daily_root_signature_invalid.

Publishing that daily root series to a public, append-only location — so you could compare a bundle's roots against an independent record — is specified but not implemented. Today the daily roots are checked for internal consistency and signature, not against an external series.

8. Conformance vectors

testdata/vectors/ in the repository holds a valid bundle plus one bundle per fault class, with the expected verdicts in expected.json:

VectorExpectedMust report
valid.sealpass—
bad-record-hash.sealfailrecord_hash_mismatch
reordered.sealfail—
gap.sealfail—
forged-checkpoint.sealfailcheckpoint_signature_invalid
revoked-key.sealfailcheckpoint_after_revocation
bad-tsa-token.sealfailanchor_invalid
fork.sealfailchain_fork_detected
truncated-tail.sealfailtruncation_detected

Run the verifier against all of them before you trust it on a real bundle:

for v in testdata/vectors/*.seal; do
  sealr-verify "$v" --root testdata/vectors/root.pem >/dev/null
  echo "$v -> exit $?"
done

They are regenerable with seal-devtools vectors --out testdata/vectors, and CI runs the verifier against all of them on every change.

9. Selective disclosure of a payload

Records carry a salted commitment to the payload, never the payload:

commitment = BLAKE3(salt || payload_bytes)      alg: "blake3-salted"

with a 32-byte random salt. A party holding both the payload and its salt can prove to you that a specific payload matches a specific record: recompute BLAKE3(salt || payload) and compare it to that record's payload_commitment.commitment. Without the salt, the commitment reveals nothing practical even about a low-entropy payload.

That is what lets an operator hand you a fully verifiable bundle and disclose individual payloads only under an appropriate legal basis.

The recorder runs in commitment-only mode today: the salt is generated per record and discarded immediately after the commitment is computed. Bundles produced by the current recorder therefore contain commitments and no vault/ entries, and there is no salt anywhere to disclose with. Selective disclosure is specified and verifiable by hand; the vaulted-payload path that would retain salts is not implemented.

What is in the bundle

Useful when you want to look before you run anything. A .seal file is a zip:

records/<stream>.part-00000.ndjson.zst   record shards, one JSON record per line
records/index.json                       per-stream ranges
checkpoints/<stream>.json                signed Merkle roots + countersignatures
anchors/rfc3161/<batch>.tsr              RFC 3161 timestamp tokens
anchors/qts/<day>.der                    qualified timestamps over daily roots
anchors/proofs.json                      batches, daily roots, inclusion proofs
identity/certs.pem                       recorder + tenant certificates
identity/roots.json                      pinned roots (option 3 above)
identity/revocations.json                revocation entries, when present
VERIFY.md                                what to run, shipped inside the bundle
manifest.json                            written last; hashes every entry above

On this page