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 --versionOr 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/sealrThere 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 bandTwo 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 fullsealr 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
| Code | Result | Meaning |
|---|---|---|
0 | pass | no findings |
10 | pass with findings | at least one warning, no errors |
11 | fail | at least one error finding |
30 | usage error | unreadable bundle, unreadable --root or --console-key |
2 | bad arguments | a 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=SEQto make the true extent decidable.
7. Trust roots
The verifier picks a root in this order:
--root <file>— a root you obtained out of band. Use this when it matters.- The roots embedded in a release build of the verifier.
- 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:
| Vector | Expected | Must report |
|---|---|---|
valid.seal | pass | — |
bad-record-hash.seal | fail | record_hash_mismatch |
reordered.seal | fail | — |
gap.seal | fail | — |
forged-checkpoint.seal | fail | checkpoint_signature_invalid |
revoked-key.seal | fail | checkpoint_after_revocation |
bad-tsa-token.seal | fail | anchor_invalid |
fork.seal | fail | chain_fork_detected |
truncated-tail.seal | fail | truncation_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 $?"
doneThey 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