Verify a Proof
Step-by-step guide to verifying a Truestamp proof bundle offline by recomputing the leaf hash with domain separation, walking the Merkle inclusion path to the block root, and confirming that root against the public Stellar or Bitcoin commitment, via the verify web page, JSON:API, or GraphQL.
Overview
A Truestamp proof is a self-contained bundle that proves your data was submitted to Truestamp within a specific window and committed to a public blockchain. Verifying a proof means checking a chain of hashes for yourself: recompute the hash of your data, walk the Merkle inclusion path up to the block root, then confirm that same root was recorded on Stellar or Bitcoin. Every check is deterministic math on the bytes in the bundle.
Verification runs without contacting a Truestamp server. The optional network calls go to the public blockchains (a Stellar Horizon endpoint or a Bitcoin node) to confirm the commitment transaction actually exists, and, for an entropy subject, to the original randomness source to re-fetch the recorded value. Skip those and verification is fully offline, proving the internal cryptographic chain without any external calls at all.
One thing does need a Truestamp source, once. Confirming that the key which signed the bundle is really Truestamp’s means comparing it against the published keyring, and a keyring pulled fresh at verification time is no more trustworthy than the key already inside the bundle. Fetch the keyring once, out of band, pin it, and reuse your pinned copy. A verifier with no pinned keyring still runs every other check; it just reports the key-binding step as skipped rather than claiming a binding it never established.
You can verify a proof four ways: the public verify web page, the JSON:API, the GraphQL API, or the truestamp command-line client, which runs the same checks fully offline. The web page and APIs run the exact same pipeline as an independent verifier would, so they agree bit-for-bit. This guide walks through what each check does, how to run it, and how to read the result. For the meaning of every field in a bundle, see the proof bundle format.
Before you begin
You need a proof bundle. Proofs are generated on demand for a committed subject (an item, an entropy observation, or a block); there is nothing to download until the subject reaches its committed state. There are two ways to get one:
-
Download it in the app. The pages for an item, a block, a beacon, and an entropy observation each offer a proof download, as does the verify page itself once it has loaded a subject. Downloads arrive as
truestamp-<type>-<id>.jsonor, for the binary encoding,truestamp-<type>-<id>.cbor. -
Generate it over the API. Send an authenticated request to the generate endpoint, declaring the subject type (there is no auto-detection):
curl -X POST https://www.truestamp.com/api/json/proof/generate \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/vnd.api+json" \ -d '{"data": {"id": "01J...", "type": "item"}}'typeis one ofitem,entropy_nist,entropy_stellar,entropy_bitcoin,block, orbeacon;idis the subject’s identifier (a 26-character ULID for items, a UUID for everything else). An optionalformatargument set tocborreturns the Base64-encoded CBOR binary instead of the default JSON. The proof arrives under aresultkey; hand it onward exactly as produced, never reordered, truncated, or reformatted. The GraphQL equivalent is thegenerateProofmutation with the same arguments. Generation always requires an authenticated caller (an API key or OAuth bearer token) with access to the subject.
Bundles come in two interchangeable encodings that both verify identically:
- JSON: human-readable. Hash fields are hex strings, keys and signatures are base64, Merkle proofs are base64url. The verify web page and the APIs take the proof as a JSON object.
- CBOR: a compact binary form of the same bundle, distinguished by a self-describing tag prefix. Decode it to its JSON form before submitting it to a verification surface.
You also want to know what you expect the proof to be. Two facts help:
- The subject type. One of
item,entropy_nist,entropy_stellar,entropy_bitcoin,block, orbeacon. Declaring it lets the verifier reject a proof that was silently swapped for a different kind (a confused-deputy check). It is optional: the verifier reads the type from the signed bundle if you omit it. - Your original data hash (items only). If you kept the SHA-256 of the file you timestamped, you can supply it and have the verifier confirm the proof covers exactly that data.
To verify fully offline, plan to pass the offline flag (the skip_external argument). Without it, the verifier additionally reaches out to the public blockchain to confirm the commitment transaction on-chain, and, for an entropy subject, to the original randomness source.
Steps
Verification walks the cryptographic chain from your data outward to the public blockchain. These are the checks the pipeline performs in order. When you use the verify page or an API you do not run them by hand, but understanding them tells you exactly what a passing result means.
Before any of them run, the bundle is checked for shapes that cannot be verified at all: an unrecognized subject type code, an empty commitment list, a missing block, or a block-like proof that wrongly carries subject fields. Those are hard rejections that return an error and produce no report. An unrecognized t in particular is always a rejection and never a guess, because t is what selects the subject’s shape and the hash prefixes used to derive it. An unrecognized optional field inside a known structure is the opposite case and is simply ignored.
1. Compare against the file hash you hold
This step runs only for item subjects, and only when you supply the expected_hash argument. Truestamp normalizes the value and compares it in constant time against the hash recorded in the proof.
It has three distinct outcomes, and the report keeps them separate on purpose. A supplied hash that matches passes. A supplied hash that does not match fails. Supplying nothing, on an item whose proof records a file hash, produces a warning that your file hash was not independently confirmed, never a failure: the proof itself is still sound. Read expected_hash_provided before hash_matched, so “not provided” is never misread as “mismatch”.
For every other subject type the comparison does not apply, and passing expected_hash anyway produces a visible skip rather than a silent discard or a failure. Entropy and block-like payloads carry no file hash to compare against, so treating one as a mismatch would report a sound proof as forged over an argument that never applied to it.
2. Check the format version
The bundle’s v must equal 1, the only format version that has shipped. A different value fails this step, which is reported under the Structure group.
The same group carries a check on how the bundle spells its hashes. Ten fields are hex-encoded (s.mh, s.kid, b.ph, b.mr, b.mh, b.kid, and per commitment entry memo, op, tx, bmr), and all ten must be lowercase. Uppercase fails the step and names every field that got it wrong. The rule exists because hex decoders are usually case-insensitive, and a verifier that shrugs at case would accept many byte-different spellings of the same bundle under one signature: nothing is forged that way, since every spelling decodes to the same bytes, but the bundle stops being a single canonical document. A conforming bundle produces no row here at all. The check does not apply to pk, sig, ip, or ep, where case is part of the encoding, nor to rtx and txp, which may be either base64url or hex, nor to identifiers and subject data, whose spelling is already fixed by the hashes computed over them.
3. Decode the public key and derive its key id
Base64-decode the bundle’s pk; the result must be exactly 32 bytes. Then derive the signer’s key id as SHA-256(0x51 || pk) truncated to the first 4 bytes, written as 8 lowercase hex characters.
That derived id, not the bundle’s s.kid or b.kid, is what fills the key-id slot of the signature payload in step 8 and what gets cross-checked against the keyring in step 9. Under legitimate key rotation the stored s.kid and b.kid may differ from it, and a verifier must not require them to be equal. They are consumed verbatim only inside the composite preimages of steps 4 and 6. A pk that is not 32 bytes leaves the signature unverifiable, which is reported explicitly rather than passed over in silence.
4. Recompute the leaf hash with domain separation
Start from your subject data and rebuild the leaf hash the same way Truestamp did.
For an item, the claims map is canonicalized with JSON Canonicalization Scheme (JCS) and hashed with a one-byte domain prefix (0x11). That data hash, together with the item id, metadata hash, and the stored s.kid taken verbatim, is combined into a single composite subject hash under prefix 0x13. Entropy subjects follow the same shape with prefixes 0x21 and 0x23. Every SHA-256 in the format prepends a distinct one-byte prefix so a hash computed for one purpose can never collide with one computed for another. See hashing domain separation for the full prefix registry.
The metadata hash is an input the bundle carries but a verifier cannot recompute: the metadata map behind it never travels. Tampering is still caught, because the value feeds this composite and the signed payload.
An item proof also carries soft checks on the claims themselves, which warn and never fail: that a declared hash has the hex length and lowercase character set its declared hash_type implies, and that a claimed timestamp is neither after the submission time nor more than seven days before it.
For a block or beacon proof there is no separate subject: the block itself is the subject, so this step is skipped and the subject hash equals the block hash computed in step 6.
5. Walk the Merkle inclusion path to the block root
The bundle carries an inclusion proof: a short list of sibling hashes and left/right directions that lift your leaf up the block’s Merkle tree.
Hash the leaf with the leaf prefix (0x00), then for each step in the proof hash the running value together with the sibling under the internal-node prefix (0x01), placing the sibling on the left or right as the step directs. The value you end with is the derived Merkle root. It must exactly equal the mr (merkle root) field in the block. If it does, your data provably belongs to that block. This is standard RFC 6962 Merkle verification. See inclusion proofs for the walk in detail.
6. Derive the block hash
Combine the block fields (its id, previous-block hash, merkle root, metadata hash, and the stored b.kid taken verbatim) into a length-prefixed composite and hash under prefix 0x32. This is the block hash, the value that gets committed to the public chain. For a block or beacon proof, this hash is also the subject hash. All five fields must be present; the block hash is deliberately absent from the bundle so that a bundle cannot lie about it.
7. Walk the epoch proof to the committed root
Truestamp batches many blocks into an epoch and commits one epoch root per external transaction. Each external commitment in the bundle carries its own Merkle proof (the epoch proof) linking your block hash up to that epoch root.
Walk this proof exactly as in step 5, starting from the block hash. The derived root must equal the root that was actually placed on-chain. Which field holds that root is selected by the commitment’s own t code, never by guessing from which keys are present: the transaction memo when cx[].t is 40 (Stellar), the OP_RETURN payload when it is 41 (Bitcoin). Each commitment entry produces its own step result.
8. Verify the Ed25519 signature
The bundle carries one Ed25519 signature over a fixed-layout payload built from the version, subject-type code, the signing-key id derived in step 3, timestamp, subject hash, block hash, and the epoch roots. Rebuild that payload, hash it under prefix 0x61, and verify the signature against the bundle’s public key. Any tampering with the type code, the hashes, or the timestamp changes the payload and fails this check.
9. Bind the signing key to Truestamp
Step 8 establishes only that some key signed exactly this bundle. The public key rides inside the bundle and is self-consistent by construction, so a bundle forged with an attacker’s keypair satisfies every cryptographic step up to here. This step is the only one that turns “someone signed this” into “Truestamp signed this”.
Cross-check the derived key id and the public key against an independently obtained, pinned copy of Truestamp’s keyring at https://www.truestamp.com/.well-known/keyring.json. A verifier with no pinned keyring reports this step as skipped, never as passed, so a report can never be read as having established a binding it never attempted. Even a match establishes only that Truestamp published this key: the keyring carries no validity intervals and no revocation flag. Ed25519 signatures explains the keyring’s key ids and rotation lineage and how to pin keys.
10. Confirm the commitment on the public blockchain
This step reaches the outside world, and it is optional. Skip it (the offline flag) and the verifier reports it as skipped rather than failed.
-
Stellar: fetch the transaction from a public Horizon endpoint, confirm its memo type is a hash, and confirm the on-chain memo equals the epoch root from step 7. The ledger number is checked if the bundle provides one. Only a substantive disagreement with the chain fails this step. An unreachable, throttled, or erroring Horizon endpoint, and an entry with no transaction id to look up, are reported skipped; a definitive answer that the transaction does not exist is the one lookup outcome that fails.
-
Bitcoin: the checks run only over the fields the entry actually carries, and each optional field’s absence skips exactly the checks that consume it. With the raw transaction present, extract the OP_RETURN payload of the first output whose script begins with
0x6aand confirm it equals the epoch root, then recompute the transaction id and confirm it matches. With the txoutproof present, parse it, verify the partial Merkle tree to a root, and confirm the transaction is in the matched set. With the recorded block Merkle root present, cross-check it against the txoutproof’s own header.Every one of those checks compares bundle bytes against other bundle bytes. They establish internal consistency and nothing more, so they are reported as informational results rather than as a passing on-chain confirmation: a fabricated header over a one-transaction tree carrying any 32-byte OP_RETURN would satisfy all of them. Binding the commitment to Bitcoin requires an external confirmation point, either a networked lookup at the recorded height or an operator-pinned header set. Until one is available the Bitcoin commitment is reported unconfirmed (skipped), never passed, and Truestamp’s own verifier reports it that way today.
11. Determine the submission window
Extract the millisecond timestamp embedded in the subject id and the block id and confirm the subject was submitted at or before the block. Those ids are both minted by Truestamp, so that ordering is a Truestamp assertion: it fails the step when violated, but it is never presented as externally verified.
Of the submission window’s two edges, only the submitted-before edge is grounded outside Truestamp by the bundle, through the earliest commitment transaction carrying an epoch root that covers this block. Establishing the submitted-after edge needs the referenced previous block, which the bundle does not carry: only the subject’s metadata hash travels, and the previous-block reference lives inside the metadata map behind it. A verifier may fetch that block out of band as an optional extension. One that does not must report the submitted-after edge as not established from the bundle, never as established.
Block-like subjects produce no submission-window step.
12. Re-fetch the entropy source
This step applies only to entropy subjects (t of 30, 31, or 32), and it reaches the outside world. Re-fetch the value from the source that produced it and compare: a NIST pulse via the URL rebuilt from its chain index and pulse index, a Stellar ledger by sequence, a Bitcoin block by height. A mismatch fails the step. An unreachable source, local throttling, or the offline flag is reported skipped, never failed. The source check’s own report is merged into the proof report, under the Entropy Source group.
A pass here establishes exactly one thing: that the value exists in the source’s records. It does NOT establish that the value was fresh at the moment Truestamp captured it. A source that freezes, the documented case being NIST returning its last pulse indefinitely through a US budget lapse, yields a genuine, re-fetchable, stale value that still passes this step. This is why three independent sources are captured rather than one.
Verify
Pick whichever surface fits your situation. The three surfaces below run the identical pipeline described above; the command-line client runs the same checks offline.
Verify on the web
The public verify page at https://www.truestamp.com/verify is the no-tooling path. A shareable per-subject link takes the form https://www.truestamp.com/verify/<type>/<id>, where <type> is one of item, entropy_nist, entropy_stellar, entropy_bitcoin, block, or beacon. For entropy there is also a bare https://www.truestamp.com/verify/entropy/<id> convenience alias: it accepts any of the three entropy sources for that id, where the strict entropy_nist, entropy_stellar, and entropy_bitcoin types each require the id to be that specific source. The page shows each verification group with a pass, fail, warn, skip, or info status, plus the submitted and committed timestamps. Anyone can view a public item’s verification page this way; a private or unknown subject returns a generic not-found so nothing leaks to a visitor who should not see it. If you are signed in, you can still open the verify page for your own item, or your team’s, before it is made public, as a preview. A public item’s verification page can also be found by pasting its ID, its hash, or words from its name into the search page. For an item, you can also verify a local file: the page hashes the file in your browser and compares that hash against what the proof covers, so the file contents never leave your machine.
Verify with the JSON:API
Send the proof to the verify endpoint, with the action arguments directly under the data key. Pass skip_external: true for fully offline verification. Unlike the web page, the API requires an authenticated caller (an API key or OAuth bearer token).
curl -X POST https://www.truestamp.com/api/json/proof/verify \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/vnd.api+json" \
-d '{
"data": {
"proof": { "...": "your proof bundle here ..." },
"type": "item",
"skip_external": true
}
}'
The response carries the report object under a result key. Read passed for the overall verdict and steps for the per-group breakdown. Optional arguments: type asserts the expected subject type (a mismatch is rejected with subject_type_mismatch), and expected_hash (items only) confirms the proof covers a specific data hash. A well-formed request that returns passed: false is a successful HTTP response reporting a failed verification, which is different from a rejected malformed request.
Verify with GraphQL
Call the verifyProof mutation with the same arguments (proof, optional type, expected_hash, skip_external) and read the returned result object.
Reproducing the exact hashed bytes
Every hash in the chain is computed over a canonical byte encoding, and Truestamp shows you those exact bytes. The pages for an item, a block, a beacon, and an entropy observation each include a raw-contents disclosure: everything Truestamp records about that entity, rendered as a single canonical JSON object. The page displays the JSON prettified for reading, and its copy button copies the canonical form: JSON Canonicalization Scheme (RFC 8785 JCS) bytes, with sorted keys and no whitespace.
The canonical form is not just a display convenience. JCS is the same canonicalization Truestamp applies before hashing, so the hashed payloads appear inside the canonical view byte-for-byte. The claims object of an item’s raw contents is precisely the byte sequence that, prefixed with the claims domain byte (0x11) and run through SHA-256, must reproduce the stored claims hash; an entropy observation’s raw value relates to its stored data hash the same way under prefix 0x21. An external verifier can therefore recompute Truestamp’s hashes from the raw-contents view without guessing at key order, whitespace, or number formatting, which is what makes the leaf-hash step of the pipeline reproducible outside Truestamp.
Troubleshooting
Verification produces a per-group report. When something fails, the group name tells you where in the chain it broke.
- Structure fails. The Structure group carries the version check: it fails when the bundle declares the wrong format version. Other malformed shapes (an unrecognized subject type code, an empty commitment list, a missing block, or a block-like proof that wrongly carries subject fields) are rejected before any report is produced, as a malformed-request error rather than a failed step in the report. Either way, re-download the proof; a truncated or edited file often lands here.
- Hash Comparison fails. The
expected_hashyou supplied does not equal the hash the proof covers. Either the file you hold is not the file that was timestamped, or you hashed it differently. - Signing Key fails. The bundle’s public key is not 32 bytes, so the signature cannot be verified at all. That is reported explicitly, never quietly passed.
- Subject Data fails. The recomputed subject hash does not match, or the fields needed to derive it are missing. This group also carries the item soft checks, which warn rather than fail.
- Inclusion Proof fails. The recomputed Merkle root does not match the block’s merkle root. The subject data or the inclusion path was altered. If you edited the data before hashing, that alone breaks this step.
- Block Hash fails. The block fields do not recompute to the hash the epoch proof walks from, or one of the five required block fields is missing.
- Epoch Proof fails. The block hash does not walk up to the committed epoch root. The block fields or the epoch proof were altered.
- Proof Signature fails. The Ed25519 signature does not verify. Any change to the type code, hashes, or timestamp lands here. A beacon proof and a plain block proof over the same block have different signatures on purpose, so verifying one as the other fails this check.
- Key Binding is skipped. This is the normal result when no pinned keyring was supplied, and it is not a failure. It does mean the run established that some key signed the bundle, not that the key is Truestamp’s. Supply a pinned keyring to turn it into a real check.
- Stellar Commitment fails. The on-chain transaction did not confirm the epoch root, or Horizon answered definitively that the transaction does not exist. An unreachable, throttled, or erroring endpoint is reported skipped instead, because reachability is a property of the network and not of your bundle.
- Bitcoin Commitment fails. The bundle’s own Bitcoin bytes disagree with each other: the OP_RETURN payload, the recomputed txid, the partial Merkle tree, or the recorded block Merkle root did not line up. When those checks agree, they are reported as
infoand the commitment itself stays unconfirmed (skipped) until an external confirmation point exists. - Entropy Source fails. The value re-fetched from NIST, Stellar, or Bitcoin does not equal the value recorded in the proof. An unreachable source is reported skipped instead.
- Submission Window fails. The subject’s embedded timestamp is after the block’s, which should never happen for a genuine proof.
- Skipped steps are not failures. External checks skipped by the offline flag, the key-binding check with no pinned keyring, and steps that do not apply to this subject type all report as skipped. A verdict of passed with several skipped external steps is a valid offline result.
- Informational results are not checks. An
inforesult records something the run observed without verifying anything external, and it is excluded from the pass, fail, warn, and skip counts. The Bitcoin internal-consistency results use it precisely so they can never be read as an on-chain confirmation. - A hash warning on an item. If you did not supply your original data hash, the report warns that the claims hash was not independently confirmed. Supply
expected_hashto turn that into a real match check. Readexpected_hash_providedbefore interpretinghash_matched: when it is false, nothing was checked and a falsehash_matchedis not a mismatch. Supplyingexpected_hashfor a non-item subject is reported as a visible skip. - A type mismatch. If you declared a
typeand the bundle carries a different one, the request is rejected withsubject_type_mismatchand reports both the requested and actual types.
If verification still fails unexpectedly, see support and contact channels.
Citations
- RFC 6962: Certificate Transparency. Defines the Merkle leaf (
0x00) and internal-node (0x01) domain-separation and inclusion-proof structure used in the leaf-hash, inclusion-proof, and epoch-proof steps. - RFC 8785: JSON Canonicalization Scheme (JCS). The canonical byte encoding of subject data hashed in the leaf-hash step.