What Sealr proves
The exact claims a verified Sealr bundle supports, the threat each mechanism addresses, and the claims it deliberately does not make.
This is the page to read before you rely on Sealr for anything that matters. It states what a passing verification supports, what it does not, and where the boundary between the two sits.
The short form, which the verifier itself prints in every report:
Sealr evidence is tamper-evident, not tamper-proof. It proves the integrity, ordering, timing and origin of the stream it recorded. It cannot prove that unrecorded events did not happen.
What a passing verification proves
Run the verifier over a bundle:
sealr-verify evidence.sealExit code 0 means pass, 10 means pass with findings, 11 means fail. A pass supports five claims, each resting on a specific mechanism.
1. Integrity — no record was altered after it was written
Each record's record_hash is BLAKE3-256 over its canonical JCS encoding with the hash field
removed. The verifier recomputes every hash. A single changed byte anywhere in a record —
a different table name, a downgraded verdict, a shifted timestamp — produces
record_hash_mismatch and the report fails.
Addresses: silently editing history after the fact, whether by an attacker with disk access or by whoever is explaining an incident.
Does not address: a record that was never written. Integrity is a property of the stream, not of the world.
2. Ordering — records are in the sequence they were written
Each record's prev_hash is the previous record's hash, seq increments strictly by one
within a boot segment, and two clocks are recorded: ts_wall and a monotonic ts_mono_ns.
The verifier checks all of it. Deleting an interior record breaks the link and shows up as
link_broken or a sequence gap. Swapping two records breaks it too. A wall-clock regression
beyond two seconds without an adjacent clock_anomaly record is reported.
Addresses: removing an inconvenient operation from the middle of a session; reordering to make an approval appear before the action it approved; backdating with the system clock.
Does not address: removing records from the end of the stream. See tail completeness below.
3. Origin — the stream came from a recorder holding a specific identity key
Records are not individually signed. Integrity flows up to checkpoints, which are signed
Ed25519 with the recorder's identity key. That key's certificate chains to a tenant CA and a
root the verifier trusts (embedded, bundle-pinned with a warning, or supplied with --root).
Revoked certificates are honoured: checkpoints signed after the revocation's effective time are
invalid, and the verifier reports the boundary.
Addresses: a fabricated stream from someone who does not hold an enrolled recorder key.
Does not address: what the holder of that key does with it. Origin proof is exactly as
strong as key custody, and V1 ships a file-backed keystore — the private key is obfuscated with
a host-derived keystream and stored with restrictive permissions, which is not the same as a
TPM. sealr doctor reports the backend in use and flags the file fallback as a downgrade.
Hardware-backed backends (TPM 2.0, Secure Enclave, OS keychain) are specified and enumerated in
the code but not implemented in V1. Anyone who can read the key file on the recorder host
can sign records going forward; revocation bounds the damage after the fact, it does not
prevent it.
4. Timing — records existed no later than their earliest anchor
The Console batches checkpoint digests into a Merkle tree and obtains an RFC 3161 timestamp
token over the batch root. The verifier checks the inclusion proof, checks that the token's
message imprint binds that root, verifies the CMS signature against the certificate embedded in
the token, and reads genTime.
That gives a one-sided bound. A record covered by an anchored checkpoint provably existed no
later than the anchor time. "No earlier than" is bounded by the previous anchor plus the
monotonic clock deltas in between — not by ts_wall, which is whatever the recorder's clock
said.
Implementation status. The bundle format, the anchor structures and the offline RFC 3161
verifier are implemented, and golden test vectors exercise them. The hosted Console does not yet
submit checkpoints to a timestamp authority: TSA_URL exists in its configuration and no
service consumes it. Until anchoring is wired up, bundles carry no anchors, the report shows no
earliest anchor, and timing is not independently proven — the record's timestamps are
recorder-asserted. The daily consolidated root, the eIDAS qualified timestamp over it, and
public publication of that root are specified in the format and are not implemented.
5. That a verdict was rendered
The verdict block — decision, mode, risk class, policy version, matched rule ids, reason codes, guard name and version, evaluation time — lives inside the hashed record. A pass proves that the recorder recorded that decision, under that policy version, at that point in the stream, and that nobody changed it afterwards.
Addresses: the argument that a policy was not in force, or said something else, at the time.
Does not address: whether the operation was actually prevented. In enforce mode a BLOCK
verdict makes the surface return a denial and the tool does not run — but the proof that
nothing else ran through another path is a coverage question, not a cryptographic one. In
observe mode nothing is stopped at all, and the record says so: mode: observe.
What it does not prove
Unrecorded events did not happen
This is the one that matters most, so it is stated first and stated plainly. A hash chain
proves things about the records in it. It says nothing about an operation that never reached a
recorder. An engineer with a database URL and a psql client leaves no trace in the stream, and
the stream verifies perfectly without them.
Sealr does not claim bypass is impossible. It claims bypass is visible, and only where a second observer exists — see coverage gaps.
Coverage is complete
Coverage is a deployment property: it depends on which agents route through a recorder, which credentials live inside the instrumented path, and who installed what. No property of the evidence format can establish it. The verifier prints this as a standing limit in every report, including reports that pass.
The agent's intent matched its action
Guards read the operation, not the model. A guard sees DELETE FROM users WHERE created_at < $1; it has no opinion about whether the agent was asked to clean up stale rows or decided to on
its own. Sealr closes the gap between what an agent says it did and what it executed. It
does not close the gap between what it was asked to do and what it decided to do.
Related: a record proves an operation was submitted and what verdict it received. Outcome records link back to their request when the surface produces one, but the recorder is not the database — success or failure downstream is recorded as reported, not independently proven.
Tail completeness is bounded, not proven
Removing the trailing records of a stream together with the checkpoints that attest them leaves a shorter chain that is internally consistent. Chaining alone cannot detect this, because there is nothing left to point at what is missing.
Two things bound it:
- The checkpoint chain. Checkpoints link to their predecessors, so only a suffix can be dropped, never an interior slice.
- The Console countersignature. When the Console countersigns a checkpoint it asserts the highest sequence it had accepted for that stream, signed with its own key. A bundle whose records stop below a countersigned high-water mark is demonstrably incomplete, and a recorder-side attacker cannot forge a lower value.
Without an external attestation, the verifier reports tail_completeness_unproven as a warning
rather than staying silent. To make the true extent decidable, fetch the stream's high-water
mark from the Console and pin it:
sealr-verify evidence.seal --expect-high-water 01J8Z...=41822The value is max_seq from GET /v1/streams/{id}/integrity. The sealr verify subcommand
built into the recorder does not expose this flag; use the standalone sealr-verify binary when
you need it.
Who the human behind the operation was
Every record carries an attribution field with one of three values: attributed, inferred,
unattributed. That field records the quality of the attribution — whether the principal came
from an authenticated source or was guessed from the environment. The verifier reports
attribution as recorded. It does not independently prove that a named human was at the keyboard.
What the payload contained
Records carry a salted BLAKE3 commitment, never the payload. The commitment proves a payload
matches if you hold the payload and its salt. The V1 recorder runs in commitment-only mode:
the salt is generated per record and discarded immediately, so nothing on the recorder can later
open the commitment. Payload vaulting exists in the Ledger and bundle format; the recorder does
not populate payload_ref in V1.
The consequence is deliberate. A metadata-level bundle verifies fully — chain, signatures, anchors — without any payloads, and the commitment reveals nothing practical about a low-entropy payload to anyone who does not hold the salt.
Where a coverage gap appears instead
When the recorder cannot see something, the honest answer is a gap record rather than silence.
The record type coverage_gap exists for exactly this, and the git surface has a second
observer that produces them: the GitHub App sees every push that reaches the organization,
whatever laptop it came from. Each observation is reconciled against recorder records, and a
push with no matching record is reported — naming repository, ref, actor and time — raises a
coverage.gap webhook, and appears in the Exposure Report as unrecorded_pushes.
Other record types serve the same purpose within the recorder: guard_error when a guard
panicked, spool_shed when records were dropped under pressure, clock_anomaly when the wall
clock moved, lifecycle for segment starts and ends. A segment with no clean end is itself
evidence of a crash, and the verifier says so.
The design rule behind all of these: an unknown must cost a finding, never a silent pass. The verifier's three-way result exists so that "passed, with things you should read" is a distinct outcome from "passed".
The standing limits, as printed
Every verifier report ends with the same five statements, regardless of result. They are normative, not boilerplate:
- 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 this stream.
- Proven time comes from timestamp anchors: records are proven to exist no later than their earliest anchor; "no earlier than" is bounded by the previous anchor and monotonic clock deltas.
- Subject attribution quality is as recorded; it is not independently proven.
- Tail completeness is bounded, not proven: pass
--expect-high-waterto make the true extent decidable.
If a claim about Sealr does not survive contact with that list, it is not a claim Sealr makes.
Next
- Evidence format — the mechanisms above, in detail, with the wire format.
- Verify a bundle — running the verifier and reading its report.
- Coverage — closing the loop from the other side.
How it works
The path of one agent operation from interception to signed evidence, and what does and does not cross the boundary to the Console.
Evidence format
The .seal bundle in detail — records, the BLAKE3 hash chain, signed Merkle checkpoints, countersignatures, identity, time anchors and payload commitments.