How Folio certificate verification works

A Folio Integrity Certificate is a claim about a document's edit history. This page is the recipe for checking that claim yourself — with a SHA-256 implementation and nothing else. No Folio account, no API key, and no need to take Folio's word for any step.

Every hash, root and proof below was computed when this page was built, by the same functions that issue and check real certificates — not transcribed from them. So a change to any construction changes this page in the same commit that changes the code. A spec that can drift from what it describes is worse than no spec at all: it is what makes an honest record read as tampered.

Step 1

The authorship hash chain

Folio saves a document's revisions. Each saved revision becomes one chain entry, ordered oldest first. An entry hashes its own content andthe hash of the entry before it, so the recorded history cannot be reordered, back-dated, or edited after the fact without breaking every link after the change. The first entry's predecessor is 64 zeros.

Worked example — an invented three-revision document. contentHash is the SHA-256 of the revision text with trailing whitespace stripped, and entryHash is the SHA-256 of exactly the preimage shown:

seq 0 · First draft

The tide gauge record begins in 1807.

contentHashd946fb40bad243a9e4b215200027ce443d045a7db22ff433d6c0b23f8a696763
prevHash0000000000000000000000000000000000000000000000000000000000000000
preimage{"contentHash":"d946fb40bad243a9e4b215200027ce443d045a7db22ff433d6c0b23f8a696763","meta":{"wordCount":7},"prevHash":"0000000000000000000000000000000000000000000000000000000000000000","seq":0,"timestamp":"2026-01-04T09:12:00.000Z"}
entryHash6b21692e0a5445cea65de3a3a605052f21b7af4e1909700cd1ab396a5520c4f8

seq 1 · Revision

The tide gauge record begins in 1807, two decades before the harbour was rebuilt.

contentHash04a4cd9c9598345ed63a3555292752951198c8d1e3613d3f994533ab40cf9467
prevHash6b21692e0a5445cea65de3a3a605052f21b7af4e1909700cd1ab396a5520c4f8
preimage{"contentHash":"04a4cd9c9598345ed63a3555292752951198c8d1e3613d3f994533ab40cf9467","meta":{"wordCount":14},"prevHash":"6b21692e0a5445cea65de3a3a605052f21b7af4e1909700cd1ab396a5520c4f8","seq":1,"timestamp":"2026-01-06T14:40:00.000Z"}
entryHash55d90ccae243d077e4a721d9218883721e080396ac3342aad1193060d1c5f1be

seq 2 · Final

The tide gauge record begins in 1807 — two decades before the harbour was rebuilt.

contentHash5ebd64864502252f664384d7dbc1c2136a20bdc07fce005b6f6c69beae180a74
prevHash55d90ccae243d077e4a721d9218883721e080396ac3342aad1193060d1c5f1be
preimage{"contentHash":"5ebd64864502252f664384d7dbc1c2136a20bdc07fce005b6f6c69beae180a74","meta":{"wordCount":14},"prevHash":"55d90ccae243d077e4a721d9218883721e080396ac3342aad1193060d1c5f1be","seq":2,"timestamp":"2026-01-09T11:05:00.000Z"}
entryHashdddd02fa652b0f1f125be1948c116a4a06b887dffd6023feb59f40e6a4550118

Chain verified at build time: every entry above rehashes to its recorded entryHash and links to the one before it.

One detail worth knowing before you reproduce this on a real document: a revision stored as structured JSON rather than plain text is canonicalized (keys sorted, recursively) before hashing, and a small set of derived-cacheattributes — values measured from the rendering rather than written by the author, such as an equation's captured layout — is stripped first. Without that strip, merely opening a document in a different browser would change its hashes and read as an edit nobody made.

Step 2

The Merkle root — the published fingerprint

The chain's entry hashes are folded into a single Merkle root. That root is what the certificate publishes and what /verify/<code> recomputes: change any revision and the root changes, so a match proves nothing in the history moved after the certificate was issued.

Leaves and internal nodes are hashed with different one-byte prefixes (00 for a leaf, 01 for a node, RFC 6962 style), and a node with no sibling on its level rides up unchanged rather than being paired with itself. Both details matter: without them, an odd-length leaf list and that same list with its last leaf repeated fold to the identical root, so an anchored digest would not pin down which set of leaves produced it.

leaves3 entry hashes, chain order
leaf tag00
node tag01
empty root0000000000000000000000000000000000000000000000000000000000000000 (no saved history)
root20f5e13cd48b10d6b7fff6488e4459681dc69383bd4ce978965a89b654e39c11

Step 3

The external anchor

Once a day, the roots issued since the last run are folded into one super root, which is timestamped externally with OpenTimestamps. That is the step that needs no trust in Folio at all: the timestamp attests the super root existed by a given time, and an inclusion proof shows a particular certificate's root was folded into it.

Continuing the example — the document's root sits at index 1 of a 3-leaf batch, so its proof carries one sibling on each side:

leaf20f5e13cd48b10d6b7fff6488e4459681dc69383bd4ce978965a89b654e39c11
step 1852ddc241a3acbd45f971b79f783908572e1b33acecd08ca661c09293cf192f1 (sibling on the left)
step 21346cec21324a5903daf5666440d0424c144b7df9f6ebec292cd0d25f1ffff02 (sibling on the right)
super root7db2e373735f1f8ba6cf3882186864a080b31286902ab4488d260f67fa53f1dc
schemev2

Inclusion proof verified at build time: folding those steps into the leaf reproduces the super root.

For a real certificate, GET /api/anchors/<code> returns exactly these values — root, inclusion steps, super root, the recorded scheme, and the raw .ots proof (?format=ots) for any OpenTimestamps client.

Reference

The two constructions, and which one to use

Folio's Merkle construction changed once, when the domain-separation tags above were introduced. Certificates issued under the earlier construction were not reissued and were not invalidated — they still verify, under the formula they were built with. Which one a record used is recorded on the record itself as merkle_scheme; where it is absent, the record predates the column and a verifier should try both and accept either.

These are the same recipe strings /api/anchors/<code> serves in its how_to_verify field, not a restatement of them:

v2 — current, used for every new proof

Recompute the super root from provenance_root: start with acc = sha256("00acc = sha256("01" + step.hash + acc) when step.side is "left", or acc = sha256("01" + acc + step.hash) when it is "right". All hashing is sha256 over the ASCII of the concatenated lowercase hex strings (the tag included), not over their decoded bytes. The result must equal super_root.

example root20f5e13cd48b10d6b7fff6488e4459681dc69383bd4ce978965a89b654e39c11

legacy — still verified, never issued

Recompute the super root from provenance_root: start with acc = provenance_root, then for each step of inclusion_proof set acc = sha256(step.hash + acc) when step.side is "left", or acc = sha256(acc + step.hash) when it is "right". All hashing is sha256 over the ASCII of the concatenated lowercase hex strings, not over their decoded bytes. The result must equal super_root.

example rootdbde8e19acce0eae440dbf374693b48bdd242ab2f9b92dfc29bcdbb8e5ddb263

The two roots above are the same three entry hashes folded two different ways. That they differ is the point: a proof checked against the wrong construction fails, and a failure is indistinguishable from tampering to anyone reading the result. Dispatch on the recorded scheme.

Reference

The attestation embedded in exported files

A .docx or .pdf exported from Folio carries a compact attestation in its own metadata — DOCX custom properties, the PDF Info dictionary — so the proof survives being detached from its download page. Current version 2; fields: anchor, code, issued, root, url, v.

Be clear about what this blob is.It is a pointer plus a fingerprint, not a signature. Folio holds no asymmetric signing key, so the blob alone does not prove “Folio vouches for this” — anyone can strip or forge it. What nobody can forge is the record it points at, and the anchor path above is checkable with no trust in Folio whatsoever.

Versions 1 and 2 are both readable by the current build. That is deliberate rather than incidental: v1attestations are already embedded in files out in the world, and rejecting them the day a field was added would turn every one of those documents into “no attestation” — the exact failure the embedding exists to prevent. v1 is v2 minus anchor.scheme, which is optional in v2 anyway, so an old blob parses with no shim. What the version number buys is the distinction: in a v2 blob a missing scheme means the record genuinely had none, while in a v1 blob it means only that the field did not exist yet.

Checked at build time: a v1-shaped blob round-trips through the current decoder and parses.

Honest scope

What a certificate does not prove

A verified certificate says: this edit history was recorded, in this order, and has not changed since the certificate was issued — and, once anchored, that its fingerprint existed by the anchored date, provable against a public timestamp rather than against Folio.

It does not say the text is correct, original, or unassisted. It does not identify the person at the keyboard. And it attests to the history Folio saw: writing done elsewhere and pasted in appears as it appears — one large addition, honestly recorded as such. Folio's process signal describes that shape; it does not accuse anyone of anything.

Checking a specific certificate? Open /verify/<code> for the human-readable record, or /api/anchors/<code> for the proof as JSON. Verifying a batch of them is what the certificate checker is for.