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.”
seq 1 · Revision
“The tide gauge record begins in 1807, two decades before the harbour was rebuilt.”
seq 2 · Final
“The tide gauge record begins in 1807 — two decades before the harbour was rebuilt.”
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.
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:
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.
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.
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.