Knowledge Base

Browse the concepts behind Truestamp. Follow the links between concepts, or search across everything.

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, rehashing each carried witness, and walking the epoch proof to the root recorded on Stellar or Bitcoin.

Open resource

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’s Merkle root, derive the block hash from the block’s own fields, then walk one more Merkle proof to the epoch root that 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 reach outward instead: to a public Stellar Horizon endpoint, to confirm the commitment transaction actually exists, and to the public sources behind the bundle’s witnesses and behind an entropy subject, to re-fetch a recorded value from NIST, Stellar, or Bitcoin. A Bitcoin commitment has no equivalent lookup yet, so it is reported unconfirmed rather than confirmed on-chain. Skip those calls and verification is fully offline, proving the internal cryptographic chain without any external calls at all.

One thing does need a Truestamp source, once, unless the bundle carries a signing key event. 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. A bundle that carries a signing key event gives a second, chain-checkable path to the same question.

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. When they confirm against a public source, the answer may be one Truestamp retrieved earlier: a settled Stellar transaction or a published NIST pulse never changes, so its answer is reused rather than asked for again on every view, and each result names the source that answered and when it was retrieved. 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. Each format comes in two variants:

    • Complete carries every witness the subject’s fingerprint commits to, so the file stands alone and its submitted-after edge can be opened without fetching anything from Truestamp. This is the default and the one to keep. Downloads arrive as truestamp-<type>-<id>.json or truestamp-<type>-<id>.cbor.
    • Compact leaves the witness details out. It verifies under exactly the same signature, is smaller, and arrives as truestamp-<type>-<id>-compact.json or .cbor. A verifier reports each committed witness as committed but not carried.
  • 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"}}'

    type is one of item, entropy_nist, entropy_stellar, entropy_bitcoin, block, or beacon; id is the subject’s identifier (a 26-character ULID for items, a UUID for everything else). An optional format argument set to cbor returns the Base64-encoded CBOR binary instead of the default JSON. The proof arrives under a result key; hand it onward exactly as produced, never reordered, truncated, or reformatted. The GraphQL equivalent is the generateProof query, which takes the same arguments as flat field arguments and answers with a { result, errors } envelope, the bundle being the JSON string under result: query { generateProof(id: "01J...", type: "item") { result errors { code message } } }. A failure (an uncommitted subject, a rate-limit refusal) is reported in that field’s own errors, so it never nulls other fields in the same document. Generation always requires an authenticated caller (an API key or OAuth bearer token) with access to the subject; because generation reads and writes nothing, an OAuth token needs only the api:read scope.

    An optional witnesses array chooses which witness details ride along. Omit it to carry every witness, which is what a complete download does. Pass [] for the compact bundle, or a subset of block, entropy_nist, entropy_stellar, entropy_bitcoin, and signing_key_event to choose:

    -d '{"data": {"id": "01J...", "type": "item", "witnesses": ["block", "entropy_stellar"]}}'

    An unrecognized name is rejected as invalid_witness. Asking for a witness the subject’s metadata does not commit to simply omits it rather than failing, and for entropy and block-like subjects only signing_key_event applies.

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, or beacon. 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. Truestamp compares your declared type only with the type it has authenticated, which it has once the bundle’s key matched Truestamp’s keyring and the signature verified, because the type is part of what is signed. A bundle that fails either check gets its failing report, whatever type it claims.
  • 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 Stellar to confirm the commitment transaction on-chain, and to the public sources behind any carried witness and behind an entropy subject.

Steps

Verification walks the cryptographic chain from your data outward to the public blockchain and then back to the public records the submission was witnessed against. These are the checks the pipeline performs, numbered in dependency 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.

Truestamp’s own verifier (the verify page and the APIs) believes nothing in a bundle until two things hold. First the public key the bundle carries must be one of Truestamp’s: it is matched against the keyring the service derives from the key events in its own blocks, which is step 9 run early. Then the signature of step 8 must be valid, which needs the subject hash and block hash of steps 4 and 6 because those are what is signed. A bundle that fails either stops there. Every later step reports skipped with a message saying it was not checked, and nothing is looked up anywhere, because everything else in such a bundle is an unsigned claim and a bundle names whatever transaction, ledger, pulse or block it likes. A bundle that passes both is examined in full, and a public source is contacted only while no step has failed. The service cannot be given a different keyring by any caller.

An independent verifier is a different case. It is free to order the steps differently as long as each step’s inputs exist when it runs, and it may run with no keyring at all, which is what keeps a proof checkable if Truestamp cannot be reached. Step 9 says what such a run must report.

Before any of them run, the bundle is checked for shapes that cannot be verified at all. Those are hard rejections that return an error and produce no report:

  • a bundle nested more than 32 levels deep, the bundle object itself counting as the first level, is rejected as malformed before anything in it is canonicalized; no bundle Truestamp issues comes near that depth;
  • a bundle in the pre-publication short-key layout, recognizable by a top-level v or t key, is rejected as unsupported_layout; regenerate the proof;
  • an unrecognized subject type is rejected as invalid_subject_type, always as a rejection and never as a guess, because the type is what selects the subject’s shape and the hash prefixes used to derive it;
  • a missing subject or block metadata map is rejected as missing_metadata, because the metadata maps are the preimages every metadata hash is derived from;
  • a missing or empty commitment list is rejected as no_external_commitments, and a commitment entry missing its chain name, epoch proof, or epoch root is rejected as invalid_commitment_entry;
  • a missing block, a missing subject or inclusion proof on an item or entropy proof, and a block-like proof that wrongly carries subject fields are likewise rejected.

An unrecognized optional field inside a known structure is the opposite case and is simply ignored, and so is a witness name the verifier does not recognize.

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 four 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. Supplying a hash for an item whose claims are themselves the timestamped data, so the proof records no file hash at all, also produces a warning rather than a failure: there is nothing for your hash to mismatch, and the reference verifier reports the same. 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.

Truestamp’s own verifier (the verify page and the APIs) runs this comparison last, and only once the bundle’s key has matched Truestamp’s keyring and its signature has verified (steps 9 and 8). Until both hold, the hash recorded in the bundle is only what its builder wrote, and a match would vouch for your file against a document Truestamp never signed. So a bundle that fails either check gets no comparison at all: where a signed bundle would have reported this step, it is reported as skipped, reading “Not checked”, and the API reports expected_hash_provided: false and hash_matched: false, because no comparison ran.

2. Check the format version

The bundle’s version 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. Every hex-encoded field must be lowercase: the subject’s signing key id; each committed witness hash inside subject.metadata.witnesses; the previous block hash, Merkle root and signing key id of every block map the bundle carries, which means the containing block, every block_path entry, the head block carried as the block witness, and the signing key event’s block; and the epoch Merkle root, transaction hash, and Bitcoin block Merkle root of every commitment, including the commitments carried under the signing key event. 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 sweep does not reach public_key or signature (base64), inclusion_proof or epoch_proof (base64url), or raw_transaction and txoutproof, which may be either base64url or hex and so have no defined case rule. Identifiers and the hashed maps are bound by their own preimages and are not hex fields.

3. Decode the public key and derive its key id

Base64-decode the bundle’s public_key; the result must be exactly 32 bytes. Then derive the signer’s key id as SHA-256(0x51 || public_key) truncated to the first 4 bytes, written as 8 lowercase hex characters.

That derived id, not any signing_key_id stored in the bundle, is what fills the key-id slot of the signature payload in step 8 and what gets cross-checked in step 9. Under legitimate key rotation the stored subject and block key ids 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 public key 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). The metadata map is canonicalized and hashed the same way under 0x12, which the report records as the metadata hash being derived rather than read. Those two hashes, together with the item id and the stored subject signing key id taken verbatim, are combined into a single composite subject hash under prefix 0x13. Entropy subjects follow the same shape with prefixes 0x21, 0x22, 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.

Nothing here is opaque. The metadata map travels in the bundle, so the metadata hash is derived from bytes in hand rather than taken on trust, which is what makes the witnesses in step 10 checkable at all.

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 falls before the submission time and no 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 block’s merkle_root. If it does, your data provably belongs to that block. See inclusion proofs for the walk in detail.

6. Derive the block hash

Canonicalize and hash the block’s metadata map under prefix 0x33, then combine the block fields (its id, previous block hash, Merkle root, that derived metadata hash, and the stored block signing key id 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. The block hash is deliberately absent from the bundle so that a bundle cannot lie about it, and the same derivation runs for every block map the bundle carries.

7. Walk the epoch proof to the committed root, and confirm it on chain

Truestamp batches many blocks into an epoch and commits one epoch root per external transaction. Each 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 epoch_merkle_root, the value that was actually placed on-chain. There is one root field on both chains, so nothing is selected by guessing from which keys are present. Each commitment entry produces its own step result.

Confirming that the transaction really exists 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. 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 hash 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 0x6a and 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.

8. Verify the Ed25519 signature

The bundle carries one Ed25519 signature over a fixed-layout payload built from the version, the subject type’s registry code, the signing key id derived in step 3, the generation timestamp, the subject hash, the block hash, and the epoch roots in commitment order. Rebuild that payload, hash it under prefix 0x61, and verify the signature against the bundle’s public key. Any tampering with the type, 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”, and it has two independent paths.

The pinned keyring. 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. The copy has to come from Truestamp, independently of the proof. A keyring handed over by whoever supplied the proof proves nothing, because that party can list its own key in it. 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, and its result says in words that the signature verified against the public key the bundle carries but that key was not matched to one Truestamp published. Running that way is allowed on purpose: a proof must stay checkable by someone who cannot reach Truestamp. Truestamp’s own verify page and APIs are the opposite case. They always perform this step, first, against the keyring the service itself holds. There is no way to hand them a keyring, this step is never skipped there, and a bundle signed by any other key fails here with nothing after it examined. 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.

The signing key event. When the bundle carries one, the verifier confirms that the key event’s published key equals the bundle’s public key and that its key id equals the id derived in step 3, recomputes the key-event block’s hash from its carried fields, and walks each of that block’s own epoch proofs to their recorded roots, running the same chain confirmations as step 7. That moves the question off Truestamp’s web server and onto the public chains, and it stays checkable long after a keyring endpoint stops answering. A bundle with no key event reports this group as skipped.

10. Check the witnesses

A witness is a public record that existed before the submission and that the subject’s composite fingerprint commits to. The metadata map verified in step 4 names each witness by hash; a complete bundle also carries the underlying record. This step pairs them up. Only item subjects commit to witnesses, so a block or beacon proof, which carries no submission fingerprint at all, and an entropy proof, whose metadata commits to none, both report this group as a single skip.

For each name the metadata commits to, or that the bundle carries a detail for:

  • Carried and matching: rehash the carried detail and compare. The head block detail is rehashed as a block hash (step 6’s derivation, applied to the carried block map); each entropy detail is canonicalized and hashed under 0x21. A match passes; a mismatch fails as witness_hash_mismatch.
  • Committed but not carried: reported as a skip naming the witness. This is the normal result for every witness in a compact bundle, and it is not a failure.
  • Carried but not committed: fails. A detail nothing commits to is unbound data.
  • Unrecognized name: reported as a skip saying the verifier does not know the name, whether it appeared in the metadata or in the carried details. It can never fail a proof.

The head block witness also carries the ledger-position check. If the head block’s recomputed hash equals the containing block’s previous block hash, the head block is its direct predecessor and the link is confirmed from the bundle. Otherwise the bundle must carry a block_path, walked oldest first: each entry’s previous block hash must equal the hash of the entry before it, and the last entry’s hash must equal the containing block’s previous block hash. A path that does not link up, or a missing path where one is needed, fails as block_path_broken.

Finally, each entropy witness’s source-published time is compared against the submission time embedded in the subject id. A witness published after the submission cannot have been a record the submission was made against, so it is flagged with a warning.

11. Re-fetch the sources behind the entropy values

This step reaches the outside world and applies to two things: an entropy subject, and each carried entropy witness on any subject.

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 hash. A definitive disagreement with the source fails the step, for a witness exactly as for a subject. Everything short of a definitive answer is a skip: an unreachable source, local throttling, and the offline flag are reported skipped, never failed. All of these results appear under the Entropy Source group, and a witness row names the witness it came from. Only a witness whose source confirmed it in this run can carry the operative submitted-after edge in step 12.

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.

12. Determine the submission window

Three groups report the submission window, and they say different things on purpose.

Submission Window extracts the millisecond timestamp embedded in the subject id and the block id and confirms 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. Block-like subjects produce no such step.

Submitted After prints one informational row per carried witness, naming the time its own source published it and the basis for that time (a NIST pulse timestamp, a Stellar ledger close time, a Bitcoin block header time, or, for the head block, the mint time of its identifier, which is a Truestamp assertion rather than a public moment). Then it names the operative edge: the latest witness that was confirmed against its own source in this run, which passes. If nothing was confirmed, because the run was offline or a source was unreachable, the latest usable witness is reported informationally as what the edge rests on, unconfirmed. If no witness detail is carried at all, one informational row says so and names the witnesses the metadata commits to, so a reader can see exactly what is missing and go fetch it.

Submitted Before is established by the earliest commitment whose chain confirmed in this run. An unconfirmed commitment with an earlier timestamp is named as a candidate that would tighten the edge once confirmed, and never displaces a confirmed one. Without any confirmation the earliest timestamp is reported as a named candidate, not as established.

Reading a Bitcoin-derived after edge conservatively matters: a block header time may run up to two hours ahead of real time under Bitcoin’s consensus rules, so either subtract that tolerance or let a NIST or Stellar witness carry the edge. For what each edge establishes when Truestamp, a chain, or an entropy source is unreachable, see proof claims by availability.

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, and it names each witness behind the submitted-after edge. 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. If Truestamp cannot issue the subject’s proof at that moment, for example because it cannot sign it just then, the page says the proof is temporarily unavailable and to try again later, rather than reporting a failed verification. 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 with the type verification authenticated 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.

A request body to /api/json may be at most 528,384 bytes, and one to /gql the same. That leaves room for a proof of up to 256 KB (262,144 bytes) even when it is sent escaped inside a JSON string, as the GraphQL proof argument is, which can double its size. A larger body is refused with 413 and the code request_too_large; see the JSON:API reference for the details, including the 411 a body sent without its length can get.

Verify with GraphQL

Call the verifyProof query with the same arguments as flat field arguments (proof, optional type, expectedHash, skipExternal) and select result and errors: verifyProof(proof: $proof) { result errors { code message vars } }. The report is the JSON string under result; a refusal or an invalid bundle fills errors for that field alone. The proof argument is the JsonString scalar, so pass the bundle as a JSON-encoded string (typically through a variable) rather than as a GraphQL object literal. Verification is read-only on every surface, so an OAuth token needs only the api:read scope.

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; its metadata object relates to the metadata hash the same way under 0x12, and an entropy observation’s raw value to its data hash under 0x21. That last relationship is also what lets a holder resolve a witness by hand: paste a committed witness hash into the search page, open the block or observation it finds, and rehash its raw contents to confirm it is the record the fingerprint named.

Troubleshooting

Verification produces a per-group report. When something fails, the group name tells you where in the chain it broke.

  • The whole bundle is rejected. A bundle nested more than 32 levels deep, a pre-publication short-key bundle (unsupported_layout), an unrecognized subject type (invalid_subject_type), a missing metadata map (missing_metadata), a missing or empty commitment list (no_external_commitments), a malformed commitment entry (invalid_commitment_entry), a missing block, a missing subject or inclusion proof, and a block-like proof carrying subject fields are all refused before any report is produced, as a malformed-request error rather than a failed step. For unsupported_layout the fix is to regenerate the proof; for the others, re-download it, since a truncated or edited file often lands here.
  • Structure fails. The version is not 1, or one of the hex fields is not lowercase, in which case the message names every offending field.
  • Hash Comparison fails. The expected_hash you 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 subject hash could not be derived at all: the claims or entropy map, the metadata map, the id, or the signing key id is missing, or one of the maps cannot be canonicalized. This group reports the derivations, not a comparison, so a subject hash that derives cleanly but is wrong surfaces one step later, in Inclusion Proof. The 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 hash could not be derived: a required block field is missing or malformed, one of the hex fields is not lowercase, or the block metadata map cannot be canonicalized. A block hash that derives but does not match what was committed surfaces in Epoch Proof.
  • 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, 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.
  • Signing Key Event is skipped. The bundle carries no key event, which is normal for a compact bundle and for a bundle generated before the key-event block was committed. A failure here (signing_key_event_mismatch) means the carried event does not introduce the key that signed the bundle, or its block hash or epoch proofs do not recompute.
  • Witnesses fails. A carried witness does not rehash to the value the fingerprint commits to (witness_hash_mismatch), or the head block does not link to the containing block and no valid path was carried (block_path_broken). A skip in this group is normal: it means the subject commits to no witness, a committed witness was not carried, or the verifier does not recognize the name.
  • 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, or one of them cannot be parsed at all: 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 info and 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, either for an entropy subject or for a carried entropy witness. 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.
  • Submitted After is informational only. Every row in this group except the operative edge is info, and a compact bundle produces a single info row naming the witnesses it commits to but does not carry. That is not a failure; it is the report telling you what a complete bundle, or a lookup of those hashes, would add.
  • Skipped steps are not failures. External checks skipped by the offline flag, external checks withheld because an earlier check failed, every step Truestamp’s own verifier did not reach because the key was not Truestamp’s or the signature was invalid (in both cases the failure is reported by the step that failed, not by the skip), 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 info result 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 your file hash was not verified. Supply expected_hash to turn that into a real match check. Read expected_hash_provided before interpreting hash_matched: when it is false, nothing was checked and a false hash_matched is not a mismatch. Supplying expected_hash for a non-item subject is reported as a visible skip.
  • A type mismatch. If you declared a type and the bundle is a genuine Truestamp proof of a different type, the request is rejected with subject_type_mismatch and reports both the requested type and the authenticated one. A bundle whose key or signature fails is never answered this way: you get its report, with the failure on the step that failed.
  • Hash Comparison is skipped as “Not checked”. The bundle’s key did not match Truestamp’s keyring or its signature did not verify, so no comparison was made; the failure is reported by the step that failed.

If verification still fails unexpectedly, see support and contact channels.

Citations

  1. RFC 6962: Certificate Transparency. Source of the Merkle leaf (0x00) and internal-node (0x01) hashing used in the leaf-hash, inclusion-proof, and epoch-proof steps.
  2. RFC 8785: JSON Canonicalization Scheme (JCS). The canonical byte encoding of every hashed map: claims, metadata, and witness payloads.
  3. Bitcoin block header reference. The consensus rule that a header time may not exceed network-adjusted time by more than two hours, which is why a Bitcoin-derived submitted-after edge is read conservatively.