Engineering

A pile of signatures is not a chain

One sentence on this site said: every action lands in a tamper-evident record that can be sealed on a Securosys HSM. An engineer at Securosys read it and asked two questions. The first was about a word. The second went straight through the middle of the feature, and they were right.

20 August 2026 · André

The two questions

Paraphrased, because they were asked in Swiss German over a chat window: the sentence sounds like the record is already tamper-evident before the HSM touches it. And: sealing normally means encrypting, not signing. So which is it? Do you sign the record with an HSM key, or is it a database encrypted under an HSM key?

The second one is a word, and the word was wrong. Here is what actually happens. The plugin hashes the content locally with SHA-256. Only the 32-byte digest leaves the machine. It goes to the Securosys Transaction Security Broker as POST /v1/synchronousSign with SHA256_WITH_RSA, and the signature comes back and is appended to attestations.jsonl. There is no encryption anywhere in that path, and no unsealing, because there is nothing to unseal. It is signing. So "sealed" goes, and "signed" stays.

Encrypting the log would also have been the wrong feature. An audit log you have to decrypt before anyone can audit it is worse than one anybody can read, and it would have killed the part that makes this useful: a receipt you can hand to a third party who verifies it in their own browser against a public key, with nothing of ours involved.

The first question was not a word

It was a defect, and the sentence was the only place it was visible.

Every receipt was signed on its own. Content in, digest out, digest signed, line appended. Each line was individually valid and nothing bound line N to line N-1. So you could open attestations.jsonl, delete the receipt for the thing you would rather nobody saw, save the file, and every remaining signature still verified. Reorder them and it still verified. The public verifier at xnaut.dev/attest would have shown a clean green tick for a log with a hole in it.

That is a set of individually valid records. It is not a record of what happened. And the failure mode is silence: nothing throws, nothing warns, the page says valid, and it is telling the truth about the only question it was asked.

Why we, of all people, had this bug

Two days before, we published Proofs, not promises, which argues this exact point at length for NautGate's routing log: chain every row into the one before it, then let the HSM sign the head, because a signature over an unchained pile proves nothing about the pile.

Then the attestation plugin shipped one directory over without the chain. We wrote the argument down and did not apply it to the next thing we built. That is worth saying out loud, because the interesting part is not that we made the mistake. It is that we had already written the correct answer, in public, and still could not see it from inside our own repository. It took someone outside reading one sentence.

What we changed

Every receipt now carries a seq and a prev. prev is the SHA-256 of the previous receipt's canonical JSON, sixty-four zeros at genesis. And the HSM no longer signs the bare content digest. It signs:

sha256( prev || content_digest )

Position and content together. Which means the newest signature commits to the entire history behind it, and there is no separate anchoring pass to run, forget to run, or run too late. Delete a receipt, move one, edit one in place, or cut the front off the file, and the walk breaks at the first receipt that no longer matches.

Four details in there are load-bearing, and three of them are the kind that would have quietly ruined it:

The verifier walks it now

The public page checked each signature and would have shown all green for a log with a receipt cut out of the middle. It now walks the chain as well: seq must equal its own index, and prev must be the hash of the receipt before it. The index check is what catches a truncated head, where a walk that only compared each row to the previous row would happily start halfway through a file and call it intact.

We know the check bites because we broke it on purpose, twice. Weakening the walk to compare only prev let a deleted receipt through. Leaving the content digest out of the link let an edited receipt through. Each one was caught by a different assertion. A test that still passes on deliberately broken code is worse than no test, because it is a claim.

What it still does not prove

The chain proves the log has not been edited since it was written. It does not prove something was written in the first place: an action that never reached the log leaves no gap to find. It does not say who is allowed to ask for a signature, which is a separate piece of work and the honest reason the next thing on our list is the credential, not more cryptography.

And unlike the NautGate log, which signs only the head of its chain, receipts are signed one by one. Each attestation is already a network round trip to the HSM, so the head-only optimisation buys nothing here, and per-receipt signatures mean a single receipt can be handed to somebody and verified entirely on its own.

So the sentence now reads: every action is hashed locally, signed on a Securosys HSM, and chained to the receipt before it. Remove one and the chain says so. Longer than the original. It has the advantage of being true.

Thanks to the engineer at Securosys who asked. Two questions about one sentence, and one of them was a hole in the product. That is the second time in a week that reading our own words back to us has been worth more than another week of writing code.