Time as evidence, revisited: a key incident and a log that can be audited from outside
Continues and corrects the entry of 20 August 2026, now under the name Sigelith. A signing key shared with a development mirror refuted the assumption that a valid signature identifies our roots, so trust moved to a log anyone can download and recompute (RFC 9162) and to copies and anchors held by others; plus per-entry time brackets from Bitcoin and a bank instead of our clock, backups that carry their own evidence, and a review of what a client showed as verified — each with its boundaries.
Koch Laboratory — time as evidence, revisited: a key incident and a log that can be audited from outside
The entry of 2026-08-20 (“Time as an agreed unit and as evidence”) described a time service whose statement is meant to hold for someone who trusts neither us nor our server. Six weeks later it is clear that one of its claims was weaker than it looked: for more than three months the same signing key was configured on two machines. This entry describes what followed from that, what was built in response, and which statements of the previous entry need correcting. The previous entry stays unchanged; the corrections are here, with a date.
Name. Since 27 September 2026 the whole proof infrastructure — the log, the checkpoints, the PDF proof documents and the verification API — is called Sigelith and runs at sigelith.org. Pages at beattime.live redirect (301) to the same address at sigelith.org; the API, the proof files and everything a program reads answer at both addresses without a redirect. The format identifiers inside signed bytes stay unchanged (beattime-proof-v1, beattime-entry-v1, beattime-checkpoint-v1) — renaming them inside signed data would invalidate every proof already issued. The name BeatTime now refers only to the two Android apps: the clock and the watch face. The clock remains an Internet Time clock — the day divided into 1000 parts, anchored in UTC — and a moment is written as @beat, for example @523.
What is research here and what is not. Most of what is described is integration of published standards: Merkle trees with inclusion and consistency proofs (RFC 9162), canonical JSON (RFC 8785), Ed25519 signatures (RFC 8032), OpenTimestamps, authenticated time synchronisation with NTS (RFC 8915). None of these mechanisms is ours. The research content is in the boundaries — what exactly a signature covers, what an anchor attests, what a copy does not attest — and in what failed.
Method. As in the rest of the laboratory: problem → state of the art → falsifiable criterion → method → result (confirmed, refuted or partial, with boundary conditions) → next step. A negative result is a result. Figures about the log come from its public data and anyone can check them without asking us; figures from tests are marked as such.
1. Direction 1 — a negative result: a valid signature did not show whose root it was
Problem. The entry of 2026-08-20 treated the Ed25519 signature over the weekly Merkle root as confirmation that the root came from us. From 15 June to 21 September 2026, however, the same private key was configured on two machines: the production service and a development mirror. The mirror runs the same scheduled jobs, so it closed weeks of its own, signed their roots with the shared key and anchored them in Bitcoin through OpenTimestamps. Two such roots came into being, for weeks 2026-W25 and 2026-W30. The error was found by our own security review in September 2026.
State of the art (published). In time-stamping under RFC 3161 and ETSI EN 319 421, compromise of a time-stamping unit’s key is handled by revocation and notification — trust stays with the provider. In Certificate Transparency (RFC 6962, RFC 9162), a log that signs two inconsistent states loses trust as a whole. Both answers assume that the signature identifies the issuer.
Criterion (falsifiable). The hypothesis of the entry of 2026-08-20: a valid signature with our key identifies a root as ours. A single validly signed root that is not ours refutes it. Replacement criterion: the authoritative root must be determinable without trusting the signature, from data held outside our infrastructure.
Method. Key rotation on 21 September 2026 and re-signing of every published root with the new key — the roots do not change, and their existence in time is attested by Bitcoin and the bank, so re-signing moves nothing in time. The retired key is refused by the signing code itself; each machine pins the public key it is allowed to sign with, so a misconfigured machine ends up with no signature rather than a foreign one; the mirror no longer anchors anything. The key history is public and nothing is ever removed from it. The first public note said only that the key had been rotated “after a security review”; a few days later we published the full account with the list of rejected roots (sigelith.org/spec/#incident-2026-09), because a rejected root that we do not name ourselves looks credible to anyone who is shown it with a valid signature. The page gives three checks that can be run without trusting us — including the decisive one for W25: the mirror’s root for that week was frozen on the Monday of the same week, and a weekly root cannot be final before its week has ended.
Result. Hypothesis refuted: two validly signed roots (2026-W25, 2026-W30) are not ours; we publish them ourselves, by name, as rejected. No hash in the log changed, no stamp was lost or backdated — what was weakened is the claim resting on the signature alone. Replacement criterion met: the authoritative root of each week is the one recomputed from that week’s published entries (copies held by third parties, direction 2), and for weeks with a bank anchor also the one written in the MROOT reference line of the transfer. Neither rejected root passes this test. Trust has moved from the signature to copies held by others and to the anchors: the signature is now one witness among several, not the identity of the log. Boundary conditions: an OpenTimestamps anchor attests time, not authorship — the mirror used the same public calendars as production; a rotation today requires changing the key list in three places (the service, the desktop client, the public log repository) and a new client release, and a client without that change shows every new proof as signed by a foreign key.
Next step. One key currently signs weekly roots, checkpoints and stamp receipts. Open question: should checkpoints carry signatures of independent witnesses, as some transparency logs do, so that the operator’s key alone — compromised or misconfigured — is not enough to present a convincing alternative history?
Status: negative result (hypothesis refuted); correction implemented and described publicly.
2. Direction 2 — a log that can be audited from outside
Problem. Until 25 September 2026 anyone could check their own entry — the inclusion path to the weekly root — but nobody could audit us: download the whole log, recompute it from scratch and show that nothing had been cut out or rewritten. After direction 1 this is not a cosmetic gap: if the signature is not enough, something held by others is needed.
State of the art (published). Certificate Transparency (RFC 6962, RFC 9162): a Merkle tree over the whole log, inclusion and consistency proofs, signed tree states; transparency logs built on that pattern (among them the Go module checksum database and Sigstore Rekor), and witnesses that cosign checkpoints. The pattern is mature; we are not building a new one.
Criterion (falsifiable). A person with no access to our infrastructure downloads the whole log, recomputes the hash chain and every root, checks that each later checkpoint extends the earlier one (an RFC 9162 consistency proof), and compares the checkpoints with copies held by others. The criterion is refuted by any change to the published history — an entry removed, rewritten or reordered, two different checkpoints with the same number — that this procedure does not detect.
Method. One global tree over all entries since the first, in the shape of the RFC 9162 Merkle Tree Hash. A leaf binds the number, hash and time of an entry (beattime-entry-v1|number|hash|time), so a single path proves “this hash was entry no. N at moment T”, not merely “this hash is somewhere in the tree”. The weekly trees, their signatures and the transfer reference lines stay unchanged; the global tree complements them, it does not replace them. A checkpoint is a signed tree state in canonical JSON: issued daily and right after each week closes, chained to the hash of the previous checkpoint file, naming a Bitcoin block (depth 3; two independent block explorers must agree) and itself anchored in Bitcoin through OpenTimestamps. Checkpoint no. 1 (25 September) covers the whole log since the first entry — 171 entries. The whole log can be downloaded (entry pages and weekly dumps), and consistency proofs between checkpoints are publicly available (sigelith.org/checkpoints/). Copies held by third parties are made automatically: OpenTimestamps/Bitcoin — every checkpoint, daily; the MROOT reference line of a bank transfer — every weekly root, weekly; GitHub (immutable releases, github.com/DeiFlagellum/sigelith-log) and the Internet Archive — weekly; Zenodo — quarterly, a complete snapshot under CC0. Since 28 September every response to a stamp request carries a signed receipt (number, hash, time, chain link): a log that later lacks such an entry is convicted by its own signature — the role an SCT plays in Certificate Transparency. Since version 2.2 the desktop client (now Sigelith Desktop, called BeatStamp before version 3.0) checks every checkpoint in the background — file bytes, signature, chain, consistency — compares it byte for byte with the copies at GitHub, the Internet Archive and Zenodo, and checks the block with an independent explorer; a checkpoint with a known number and different content is kept as evidence. In its default mode it keeps a copy of the whole log and computes paths itself; the server learns at most which week was asked about.
Result. Confirmed within the criterion. Inclusion and consistency proofs were checked exhaustively before going live for every tree size from 1 to 64 (test): 2080 of 2080 genuine proofs of each kind accepted, 8190 of 8190 forgeries rejected (among them a wrong path, a wrong index, a rewritten entry in history); generation follows the recursive definition, verification the iterative algorithm — both forms in RFC 9162 must agree. A second implementation, written from the text of the specification alone and deliberately without importing any production code, reproduced production on its first run on 25 September: 171 entries, 13 weekly dumps, global root and weekly roots matching, weekly roots equal to the MROOT reference lines. At the time of writing this was repeated with a further, separately written implementation: the roots at 171 and 211 entries equal the roots of checkpoints no. 1 and no. 8, the consistency proof between them is valid, and so is the signature of checkpoint no. 1. Boundary conditions: (a) an exhaustive test on small trees is an argument for a correct implementation, not a proof; (b) the second implementation is independent in code, not in person — it was written by the same author; there has been no third-party audit so far; (c) the copies at GitHub, the Internet Archive and Zenodo are passive — they store but check nothing before accepting; checking is done by clients and by anyone who downloads them, and there are no cosigning witnesses; (d) the history before 25 September is pinned as a whole only by checkpoint no. 1, before that only by the weekly anchors; (e) the server code is not public and does not need to be, because verification does not use it: the formats are described in the specification and in the public log repository; (f) local verification is done by the desktop client; the browser pages and the Android app compute a file’s hash locally but take the log part from the server’s response and point to the independent copies for comparison.
Next step. A standalone verification script for third parties that takes the checkpoint from an independent copy rather than from us — the only item of this direction’s plan not yet done.
Status: implemented since 25 September 2026 (first weekly release: 2026-W39; first quarterly version: 2026-Q3).
3. Direction 3 — a time bracket for every entry from external anchors instead of our clock
Problem. The time of an entry is a reading of our server’s clock. The entry of 2026-08-20 relied on weekly granularity (“no later than”) and on publishing the clock’s own error, but both remain our word. The most serious accusation against any time-stamping service is that the operator backdated an entry at someone’s request. The answer to it must not depend on the operator’s clock.
State of the art (published). RFC 3161: the time comes from the authority’s clock and is worth as much as trust in the authority; linked time-stamping (Haber and Stornetta, 1991) — order without trust, but no absolute time; OpenTimestamps — an upper bound (“existed no later than”) from Bitcoin; Roughtime — authenticated rough time with proof of server misbehaviour; NTS (RFC 8915) — authenticated clock synchronisation. The lower bound comes from a known technique: a value that could not have been known before a given moment — here the hash of a Bitcoin block — proves that whatever contains it was created later.
Criterion (falsifiable). For every entry both bounds can be derived without our clock, from data held outside our infrastructure, and the recorded time of the entry lies between them. A single entry whose recorded time lies outside its own bracket refutes the criterion.
Method. Every entry has three times. Recorded — the server clock, to the microsecond; this is our only word. Not earlier than — the Bitcoin block named in the last checkpoint issued before the entry: the entry lies outside that checkpoint’s tree, so it was appended later, and the checkpoint could not have been created before its block was mined. Not later than — the earliest anchor that covers the entry: the OpenTimestamps attestation of the first checkpoint containing it, the attestation of the weekly root, or the booking of the transfer carrying that root. The API and the PDF proof document give all three, together with the entry’s path to the first checkpoint containing it — the document plus a checkpoint taken from an independent copy suffice to check without us. Independently of this, the host clock now follows four time servers of three independent operators, authenticated with NTS; a source that disagrees with the others is outvoted. The service also measures its own offset against an external server every five minutes and publishes the result — but the bracket from the anchors does not need that measurement.
Result. Confirmed for entries since the first checkpoint (25 September 2026); partial for the log as a whole. A check at the time of writing covered all 41 entries made since then: in every case the recorded time lies within the bracket; 40 entries have both bounds (the newest is waiting for the anchor of the next checkpoint); bracket widths range from about 14 to 25.5 hours, median about 25. Example: entry no. 191 (the announcement of the name Sigelith), recorded on 27 September at 17:17:27 UTC — not earlier than 26 September, 23:43 UTC (block 968756), not later than 28 September, 00:35 UTC (block 968908). Boundary conditions: (a) block times come from block headers, which miners set to within about two hours — the bounds are accurate to hours, not seconds; (b) the bracket concerns the moment of recording in the log, not the creation of the document; (c) the 171 entries made before the first checkpoint have only an upper bound, and a lower bound cannot be added retroactively; (d) a bank booking carries a date without a time of day; (e) the lower bound rests on the log’s order being immutable, which the consistency proofs against copies held by others check (direction 2).
Next step. The bank anchor is publicly visible today as the MROOT reference line and a reference number; checking it independently requires a statement or a confirmation from the bank. The specification allows for publishing redacted statements; none has been published so far. Next step: establish whether such a statement gives a third party anything the reference number alone does not.
Status: implemented; full bracket for entries since 25 September 2026, upper bound only for earlier ones.
4. Direction 4 — a backup that carries its own evidence
Problem. A backup program compares files with its own table of contents, which lies next to them. Whoever replaces the files — ransomware, an intrusion, a failing drive, someone with access to the disk — can replace the table of contents as well, and the check then passes. Moreover, showing that a particular file was in a given day’s backup usually requires disclosing the whole backup, or at least its file list.
State of the art (published). restic (check --read-data) and Borg (check --verify-data) check data against the repository’s authenticated index — the protection rests on a key the user keeps; immutable media and storage (WORM, object lock) protect against change but by themselves give no evidence to a third party; salted hashes for selective disclosure (e.g. SD-JWT); domain separation of leaves and nodes as in RFC 6962.
Criterion (falsifiable). (1) One entry in the public log per backup version, with no file names and no file hashes; (2) for any single file, a proof that discloses no other file of the version; (3) an audit that detects a replaced file even when the table of contents next to the files has been replaced too. The criterion is refuted by a replacement that the audit does not detect while the log entry is intact.
Method. Sigelith Backup — a program for versioned, optionally encrypted backups to an external drive, with public code (github.com/DeiFlagellum/sigelith-backup) — gives every version a statement: the hash of the version’s file list (path, size and SHA-256 of every file) and the root of a Merkle tree over the version’s files. A leaf is the SHA-256 of a domain byte, a salt, the file hash, the size and the hash of the path; the salt is an HMAC-SHA256 of a random per-version seed and the path. Only the hash of the statement goes to the log — one entry per version. The proof for a single file (sigelith-file-proof-v1) consists of the path in the tree, the statement and the embedded proof of the log entry; it may or may not disclose the file’s path. The audit checks the statement offline (the path in the weekly tree and the signature, using a key built into the program rather than one supplied by the server), then reads every file back from the drive — after decryption with a check of the GCM authentication tag and after reassembling chunks — and compares it with the hash covered by the statement. The reference is the entry in the public log, not the file list lying next to the files. “Last untouched” is the newest version that matches its entry — the one to restore from after an attack. The live backup tops up today’s version with changes; a version that has already been given its statement is never topped up, and with stamping switched on the first top-up of a new day closes yesterday’s version and only then stamps it.
Result. Confirmed within the criterion (tests): a replaced file is detected even after the table of contents was destroyed or “corrected”; a modified file list covered by the statement yields no false “untouched”; in an encrypted backup a single changed bit of ciphertext is detected; the path to the root was checked for trees of 1 to 1000 leaves; a file proof discloses neither other files nor the seed. The same file-proof format is checked by three implementations on a shared test vector: the program itself, the desktop client and the verification page in the browser. Boundary conditions, stated plainly: (a) the audit binds the files to some entry in the public log, but does not prove that it is the first entry for this version — anyone can stamp, so whoever has write access to the drive can rebuild a version and give it a new entry; the later date of that entry would give it away, and the program shows that date in its timestamp view, but the audit does not yet compare it with the version’s date; (b) a missing network connection is not a backup error — the entry is sent with the next backup, so an honest entry may also be later than the version’s date, which weakens the distinction in (a); (c) with stamping switched on, an encrypted drive holds the version’s file list (paths, sizes and SHA-256 of every file’s plaintext) and the statement unencrypted, next to the encrypted files — paths are visible anyway as file names, but the plaintext hashes let anyone holding the drive test whether a file they know is in the backup; (d) a file proof attests that a file with exactly these bytes was part of a version stamped at a given moment — not when the file was created or who wrote it; (e) the live backup has evidence to the day: the current day’s changes have no entry until that day’s version is closed.
Next step. Two open decisions: whether the audit should itself compare the entry’s date with the version’s date — and how to tell late stamping from a rebuilt version — and whether the file list of an encrypted backup should be encrypted: the cost is checking the statement without the passphrase, the gain a drive that does not reveal plaintext hashes.
Status: implemented (Sigelith Backup 3.0, public code); boundaries (a)–(e) apply.
5. Direction 5 — a client review: what it showed as verified and what it did not check
Problem. A verifier that shows as verified something it did not check is worse than no verifier: it turns the issuer’s claim into a green tick at the recipient’s end. The desktop client is where proofs are checked without asking the server — so it was the first that had to be reviewed from this angle. State of the art (published). A known class of errors: the interface attributes a signature’s validity to data the signature does not cover — described, among others, for e-mail clients with OpenPGP and S/MIME (“Johnny, you are fired!”, USENIX Security 2019) and for PDF signatures; pinning a single key without history — the same failure mechanism that led browsers to drop HTTP Public Key Pinning (RFC 7469). Criterion (falsifiable). Every value shown as verified is covered by a check the client actually performed; every other value is marked as a declaration. A single value shown as verified without such cover refutes it. Method. A review of the client code against the format specification — what exactly each signature and each path covers — and tests with manipulation cases. Result. Criterion refuted for the versions before 2.1. The classes of error found, all fixed in version 2.1 (September 2026): (1) the client had a single key hard-coded — the retired one; after the rotation it would have rejected every current proof as signed by a foreign key while accepting the mirror’s roots; trust now comes from a built-in key history with dates, and a signature by a retired key yields at most the level “registered”, with a prompt to refresh; (2) a proof file with a shifted date was accepted as “verified offline” — the signature covers the week and the root, the leaf of the weekly tree only the hash, so the exact time in the file was an unsigned declaration; it must now lie within the signed week, and structurally the gap is closed by the leaf of the global tree, which binds the time (direction 2); (3) the PDF report called a rejected proof confirmed; (4) bank anchors were counted without checking that they carry the same root; (5) some fields from the server response reached interface labels without escaping of special characters. Boundary conditions: the review was internal, not an external audit; it covers the desktop client (github.com/DeiFlagellum/sigelith-desktop) — the only one that verifies the log part locally. Next step. Add the manipulation cases from this review to the shared test vectors, so that every verifier — in the backup program, in the desktop client and in the browser — is tested against the same set. Status: fixes implemented; internal review.
Corrections to the entry of 2026-08-20
The entry of 2026-08-20 stays unchanged. Below are its statements that are outdated or were not accurate.
The stamp of that entry itself. The entry of 2026-08-20 was stamped on 20 August 2026 at 14:52 UTC, in week 2026-W34 — within the period of the incident in direction 1. The root of that week was originally signed with the retired key; it is now re-signed with the new one. The date of the entry does not rest on that signature but on the anchors: the booking of the transfer carrying the W34 root (25 August) and the Bitcoin attestation of that root through OpenTimestamps (26 August); checkpoint no. 1 also covers it. There is no lower bound from anchors — the entry is older than the first checkpoint. The stamp file published next to that entry records the state at the moment of stamping (week open, provisional root); the final root, the path to it and the anchors come from the log, for example from the dump of week 2026-W34 in the quarterly copy at Zenodo.
“The application layers (Android, wear, extension, SDK) are under construction”. Outdated: the Android app and the watch face, the desktop client, the browser extension and the SDK packages are published.
Direction 2: “a periodic task measures the host clock’s drift against NTP”. Imprecise. The measurement is a single unauthenticated SNTP query to a single external server; the result is the difference from that server, not an error against UTC, and it remains our statement — a health signal of the service, not evidence. The host clock now follows four servers authenticated with NTS, and the proof of time does not rest on our clock (direction 3).
Direction 3: the Ed25519 signature as part of the result “confirmed”. Inaccurate for the period from 15 June to 21 September 2026: the same private key was on the development mirror, so a valid signature did not identify a root as ours (direction 1). Moreover, the log could be checked entry by entry but not as a whole — direction 2 closes that gap.
Direction 4: “at least two independent anchoring channels … each verifiable on its own” — confirmed. Needs qualifying. (a) An anchor attests time, not authorship: the development mirror anchored its roots through the same public OpenTimestamps calendars. (b) The banking channel is independent of us in the recording, but a third party publicly sees only the MROOT reference line and the reference number; checking it independently requires a statement or a confirmation from the bank. (c) For weeks 2026-W32, W33 and W34 the Bitcoin attestation of the roots arrived only on 26 August — 16.5, 9.5 and 2.5 days after the week closed; for those weeks the upper bound is set by the bank booking, on the day of closing or one day later. The second channel turned out not to be redundant. (d) The manual step of the banking channel failed once: for 2026-W30 a second record of the same reference was entered with a value that is not the week’s root. The record stays visible, marked as not matching; the form now fills in the root itself and checks it against the week.
Direction 6: co-stamping protocol v2. A replay gap: the first party’s start signature was not bound to a single session, so the public data of a completed session sufficed to open a new one in that party’s name; the log could then contain a co-stamp that the holder of that key never performed. Since 21 September a salt from a completed session is refused.
“Next research step: … (C2PA)”. Not taken; work on C2PA has not started. A different step was taken: a log that can be audited from outside (direction 2).
Status of the direction. The proof infrastructure runs under the name Sigelith. The key incident refuted the assumption that the signature identifies our roots; trust now rests on a log anyone can download and recompute, on copies held by GitHub, the Internet Archive and Zenodo, and on anchors in Bitcoin and at a bank. Every entry since 25 September 2026 has a time bracket independent of our clock; backups carry their own evidence with openly stated boundaries; the desktop client shows as verified only what it has verified. Next steps: a standalone verification script for third parties and the decisions from direction 4. Time capsules — envelopes closed until a chosen moment in the future — and the proof-of-delivery protocol will be described in a separate entry.
Proof of time
This text was hashed and stamped on publication. Verifying it takes both files: the stamp proves that one exact byte string existed at that moment, and only the source below is that byte string. Any later edit changes the hash and breaks the match — which is the point.
SHA-256 of the stamped text: 90ee5eb66f12eaaa998a1eca1e9f5688ce04a7e322e5398c397ca7ac4882adc2
- Sigelith anchor portfolio-sigelith.public.md.beattime.json
- OpenTimestamps proof portfolio-sigelith.public.md.ots
- Stamped source text portfolio-sigelith.public.md
Verify with: ots verify <proof> --file <source>