Shipmind Labs

We were handed an exported consent log and asked to confirm that nothing in it had been altered. Every hash lined up. Most of an afternoon went by before we admitted that this told us almost nothing.

The chain was internally consistent. The party who gave us the file also ran the system that produced it, though. Remove an entry, recompute the rest, and the log verifies perfectly. Tamper evident, not tamper proof, and we had been quietly reading the first as the second.

So in consentcore, in the proof module, we added a check that does not depend on the document at all.

Verification now takes a second argument, which is what you are checking the log against, defaulting to nothing. Inside it sit anchors: an index and a hash recorded earlier, somewhere the log holder does not control. The function that produces an anchor reads the last entry and returns that pair, or null for an empty log. We kept the null on purpose. An anchor that can never fail is the kind of thing people file away as a safeguard.

We also have a plain chain-head helper that returns the same hash on its own and falls back to a genesis value. Fine for display, useless as evidence, because it carries no position. A hash with nowhere to be is not a claim about anything.

The order of checks is fixed. The document comes first: wrong format, or entries that are not a list, and we stop. Then every entry, meaning index, link to the previous one, the hash of its preimage, timestamps moving forward. Only then the anchors. Comparing an anchor against something that is not a proof at all produces a confusing answer to the wrong question.

Anchors get treated as untrusted input too. A candidate that is not an object, an index that is not a non-negative integer, a hash that is not 64 lowercase hex characters: each one is reported as not being an anchor, with a null index, because a typo in your own records should not look like evidence of tampering in someone else's. If the anchor survives, we read the entry at that index. Missing, and we report the anchored index next to how many entries this document actually holds. Present but different, and we report it as not matching the anchor kept for it.

Nothing throws. Each finding is one problem in a list, so verification can report not-ok and still show you the rest. Entries after the anchored index are accepted, since an anchor is a floor and not a freeze. The log is supposed to keep growing.

The real cost landed outside the code: someone has to keep that anchor somewhere the log holder cannot write to, on a schedule. The check is cheap. The custody is the work.

If you keep audit logs you may one day need to defend, it is worth knowing where the external reference lives and who has write access to it.

Was this useful?

Building something similar?

or email hello@shipmindlabs.com