WARDEN
Public verification · what is running, and what is not yet

Don't trust us.
Check the maths.

Warden's record is built so that you do not have to take our word for it. Every ledger event is hash-chained with SHA-256, which needs no key, and signed with an Ed25519 key whose fingerprint is fixed in this page. Anyone holding an export of a record can check every link and every signature against that fingerprint with the published tools, without any secret of ours. Events up to the latest time-stamped anchor (one whose token verifies) also carry an outside time-stamp (FreeTSA, RFC 3161), so they cannot be altered undetected after that time-stamp. Events newer than the latest anchor are not covered, and §06 lists every other limit. Both are on this page because a verification page that overstated itself would be self-defeating.

Chain SHA-256 · keyless Signatures Ed25519 · warden-2026-10 External time-stamp RFC 3161 · FreeTSA Signing key checking…
Read this first

Warden is pre-production. The signed ledger described here is running on Warden's own backend, but there is no live fire & rescue deployment: no real incident has ever been recorded, and any record in the ledger is a synthetic test. Everything below is stated so it can be checked, including what is not yet switched on.

00 What is actually deployed, today

The signed ledger is switched on, and the first external time-stamp was made on 1 November 2026. They prove different things, and this page keeps them apart.

Deployed Keyless chain, Ed25519 signatures

Every ledger event is hash-chained with SHA-256, which uses no key, and signed with Ed25519 under key warden-2026-10, whose fingerprint is fixed in §03. Each chain is bound to its incident: the first, signed event names that incident's id and reference. The keyed-HMAC secret the prototype used before the switch-over has been deleted, and the ledger was empty when the switch happened, so no old keyed rows exist.

Deployed External time-stamping, up to the latest anchor

A digest of the whole ledger, called an anchor (§05), is signed with our key and time-stamped by FreeTSA, an outside RFC 3161 time-stamp authority. The first anchor was made on 1 November 2026, and its token was checked with openssl before this page said so. Events up to the cutoff of an anchor whose token verifies, and whose time-stamp is minutes after that cutoff, cannot be altered undetected after that time-stamp, even by whoever holds the signing key. The limits sit beside that claim: events newer than the latest anchor, and any incident opened after it, are not covered until the next one; an anchor is attempted once a day, at 03:10 Coordinated Universal Time (UTC), with no retry, so the newest events usually wait up to about a day for one, and longer after a day with no anchor; FreeTSA is a free service with no service guarantee, and on a day when it does not answer, or its token fails the checks our anchor function runs before writing (§05), the scheduled run writes no anchor; checking takes both verifiers and a comparison of the record and ledger exports (§04), and openssl (§05); and someone with owner rights over the backend could still rebuild the ledger and anchor it again from scratch, which shows only against a copy of an earlier anchor kept outside Warden. That is why it is worth keeping each anchor's digest and token.

So the honest position is: the record is tamper-evident and independently verifiable from an export, with no known issues within the reviewed scope, and events up to the latest time-stamped anchor (one whose token verifies) cannot be altered undetected after that anchor's time-stamp (check it with both verifiers, the comparison of the record and ledger exports in §04, and openssl). The newest events are not covered until the next anchor. Read the algorithm, pin the fingerprint, run both verifiers, compare the record and ledger exports as §04 sets out, check the time-stamp token with openssl, keep your own copy of each anchor, and hold us to keeping the anchors coming before any real incident data exists.

One more limit, stated plainly: an export, today, means someone signed in to the console with a staff role reading records out of the ledger (in a deployment, the service's own staff). The console has no export button yet, so an outside checker depends on whoever holds the record to hand one over, and the public access key this page uses (Supabase's anon key) cannot read the ledger at all, by design. Current gaps are tracked in the open on the system status page.

01 The claim, stated precisely

Two different properties. They are often conflated; we keep them apart deliberately. Both are running; §05 adds a third, when, through anchoring, and §00 says what each one covers.

Integrity Nothing was altered

Every event is hashed, and every hash is chained to the one before it. Change a field, delete an event from the middle or reorder the record, and every link after it stops matching. This is verifiable by anyone holding an export of the record, with no secret key.

Authorship Warden wrote it

Each link is signed with a private key we hold and never publish. A signature that verifies against the public key whose fingerprint is fixed in §03 could only have been produced by the holder of that private key.

What signatures alone do not prove: that the record was not rewritten. Whoever holds the private key could regenerate a chain from scratch and every signature would still verify. That gap is narrowed by anchoring, not by signatures, and only for events an anchor already covers; §05 says what that does and does not prove. We say so plainly rather than claim more than the design delivers.

02 The algorithm, in full

Enough detail to reimplement it from scratch and get identical bytes. This is what the deployed ledger computes for every event.

canonical(e)  = JSON over exactly {seq, ts, event, meta, actor, gate}, where
                - every object's keys are sorted, at every depth, in
                  JavaScript's default order (UTF-16 code units);
                  arrays keep their order
                - there is no whitespace: separators are "," and ":"
                - each value is written as JSON.stringify writes it;
                  non-ASCII characters (e.g. "·") stay as raw characters

every hash and signature input below is the UTF-8 bytes of its string

content_hash  = SHA-256( canonical(e) )                              hex
chain_hash    = SHA-256( prev_hash || "::" || content_hash )         hex   ← no key needed
signature     = Ed25519( private_key,
                         "warden-ledger-v1|" || key_id || "|" || chain_hash )   base64

first event:  prev_hash = "VIGIL::GENESIS", event = "INCIDENT OPENED",
              meta.incident_id = the incident's id, meta.ref = its reference

ts is compared as the stored text, byte for byte (no re-formatting, no timezone normalisation), and must be exactly what JavaScript's toISOString() produces: UTC, milliseconds, a trailing Z. meta is hashed as canonical JSON, so key order in the database is irrelevant. The first event of every chain is INCIDENT OPENED, and its signed meta carries that incident's id and reference, so a complete, honestly signed chain from a different incident cannot pass as this one. If your recomputation disagrees with ours, the record is wrong or the tooling is; either way, that is the answer you want.

03 The signing key

Published here as a fixed fingerprint, so checking a signature does not depend on trusting us.

warden-2026-10Published · fixed in this page
Ed25519 · fingerprint = SHA-256 of the public key (SubjectPublicKeyInfo, DER)
fad4bf49fa38bc7217378baf6a2ed0fee6dc896a924e81481f646b640de4ad7e

This is the value to pin. It is written into this page's own source rather than fetched, so nobody who can only change our database can change it. To check a public key from an export against it without our tools, in Git Bash or any Linux shell:

printf %s '<public_key_spki_b64 from the export>' | base64 -d | sha256sum

Below, for convenience, is the key registry exactly as our database (hosted by Supabase in Ireland, eu-west-1) serves it. Your browser computes each entry's fingerprint and compares it with the value above; anything that does not match is marked not the published key. The list itself is never trusted.

Fetching the key registry…

Keys are append-only: a registered key can be retired but not deleted or altered, so signatures made years ago stay verifiable. If a key is ever compromised we rotate, retire the old id, and say so here.

04 Verify a record yourself

Two scripts, run offline: the chain check takes an export of one incident's record, and the anchor check takes an export of the whole ledger up to the anchor's cutoff. The ledger itself is not publicly readable, by design. Pass them the fingerprint from §03: a signature from any key you did not pin is reported as a possible forgery, and a run with no pin ends without establishing authorship.

01 Check a chain

Recomputes every content hash, every link and every Ed25519 signature, and checks that the first event names the incident the export says it is. A completely separate implementation from the one that wrote the record.

curl -O https://wardendfr.com/verify/verify-chain.mjs

# offline, against an export of the record (a JSON file):
node verify-chain.mjs --file export.json --pin fad4bf49fa38bc7217378baf6a2ed0fee6dc896a924e81481f646b640de4ad7e

02 Check the anchors

Recomputes each anchor's digest from an export of the whole ledger (every incident's events up to the anchor's cutoff, plus the anchors and keys), checks the anchor chain, and verifies each anchor's Ed25519 signatures against your pin. It does not check the time-stamp token itself: openssl does that (§05), including the token's time. Run it together with the chain check, and compare the two files yourself. Up to the cutoff_ts of the latest anchor whose token verifies, both files must hold the same events of the incident, none missing from either, matched on seq (the record export's incident.id is the ledger export's incident_id), each with the same ts and chain_hash; later events are not covered. An anchor fixes the chain hash of every event up to its cutoff and the chain check ties that hash to the event's content, but neither script compares the two files, so that comparison is the step that joins them.

curl -O https://wardendfr.com/verify/verify-anchors.mjs
node verify-anchors.mjs --file ledger-export.json --pin fad4bf49fa38bc7217378baf6a2ed0fee6dc896a924e81481f646b640de4ad7e

Both scripts are plain, readable Node — read them before you run them. They only ever read. Neither writes anything: offline, they read only the export file you give them and make no network connection.

Where an export comes from, today: someone signed in to the console with a staff role reads the records out of the ledger (in a deployment, the service's own staff). There is no export button in the console yet, and the public access key this site uses cannot read the ledger, by design.

05 Anchoring — why signatures aren't enough

A signature proves who wrote a record. It cannot prove when.

If we ever wanted to rewrite history, we could regenerate the whole chain and re-sign every event. Every signature would verify. That is a real hole, and cryptography alone does not close it — so a digest of the whole record is time-stamped by an authority we do not control, which signs the time at which it saw that digest. If an event up to the anchor's cutoff is later altered, removed or inserted, the digest recomputed from the record no longer matches the time-stamped one, and a new time-stamp cannot be backdated.

The first anchor was made on 1 November 2026. Its token was checked with openssl before we said so here. For the events an anchor covers, the hole above is closed from that anchor's time-stamp on, with the exception below: regenerating them would no longer match a time-stamp issued before the change. Events newer than the latest anchor, and any incident opened after it, are not covered until the next one; until then the holder of the signing key could still alter or remove them undetected. An anchor is attempted once a day, with no retry; on a day when FreeTSA does not answer, or its token fails our anchor function's check (below), the scheduled run writes no anchor and the newest events wait longer. The claim holds only for an anchor whose token verifies with openssl and whose time-stamp is minutes after its cutoff, checked with both verifiers and the comparison of the record and ledger exports (§04). The exception: someone with owner rights over the backend could rebuild the ledger and anchor it again from scratch, with a fresh, later time-stamp. That shows only against a copy of an earlier anchor kept outside Warden, so keep each anchor's digest and token when you see it: the anchor log is world-readable for that reason.

Sink 1 · live RFC 3161 time-stamp

Each anchor's digest is time-stamped by FreeTSA, an independent Time Stamp Authority (TSA) that we do not run. It is a free service with no service guarantee. The token is stored with the anchor in the world-readable anchor log. Check it with openssl ts -verify against FreeTSA's own certificates (download them from FreeTSA yourself), and read its time with openssl ts -reply -text: it must be minutes after the anchor's cutoff. The exact commands are in the header of verify-anchors.mjs. Before anything is written, our anchor function checks that FreeTSA granted a token over this exact digest, stamped within 15 minutes after the cutoff. It does not check FreeTSA's signature: openssl is that check.

Sink 2 · not switched on A receipt to your DPO

Not switched on: there is no fire service to send it to. In a deployment, a copy of each anchor would be emailed to the service's own data protection officer (DPO). They would hold it, in their mailbox, outside our reach. In a dispute that copy is often the more practical evidence.

Fetching the latest published anchor…

06 What this does not prove

The limits, stated by us before anyone else has to find them.

Scope The record, not the world

Verification proves the record is internally consistent, correctly signed and, for events up to the latest time-stamped anchor, that it still matches what was anchored. It cannot prove that what a commander typed was true — only that it has not changed since.

Custody Key storage

The private key is held as a server-side secret that the backend's own functions can read, not in a hardware security module (HSM); anything with the right to deploy code to that backend could read it too. Anchoring bounds the damage a compromise could do: a stolen key could not alter events an anchor already covers without detection, but could forge or alter events newer than the latest anchor (the limits below apply). An HSM would narrow it further, and that step has not been taken.

The record is tamper-evident, which is not the same as impossible to alter: alteration is detectable by anyone who checks an export of the record against the pinned fingerprint. Limits remain. Events written after the latest anchor (usually up to about a day's worth, more after a day with no anchor) are not covered until the next one: until then, whoever holds the signing key could alter them, or remove the most recent events of an incident or a whole incident opened since that anchor, without detection. An anchor is attempted once a day, with no retry: on a day when FreeTSA does not answer, or its token fails our anchor function's check, the scheduled run writes no anchor, and FreeTSA is a free service with no service guarantee. An anchor's claim needs both verifiers (verify-chain for each incident's content, verify-anchors for the anchors), the comparison in §04 showing that each event up to the anchor's cutoff has the same chain_hash and ts in the record export and the ledger export, and openssl for the token's signature and time. The anchor check needs an export of the whole ledger, which today only someone signed in with a staff role can produce. And someone with owner rights over the backend could still rebuild the ledger and anchor it again from scratch, with a later time-stamp; that is detectable only against a copy of an earlier anchor kept outside Warden. So keep your own copy of each anchor's digest and token when it is published: the anchor log is world-readable for that reason. That is why the tools to check are on this page rather than behind a sales call.

Found a problem?

If you can break the chain, forge a signature, or make a record verify when it shouldn't, we want to know before a fire service does. Responsible disclosure →