Verifying an Existence Proof timestamp independently

This guide shows how to check an Existence Proof timestamp without trusting Existence Proof or its website. It uses only standard, freely available tools (OpenSSL, sha256sum, Python 3) and independent public services. It is written so that a technically competent person, such as an expert witness, can follow it step by step and record each result.

Each step says what it establishes. You can stop after step 5 for a basic check; steps 6 to 11 establish that the timestamp could not have been backdated, even by the operator.

What a timestamp proves

A valid timestamp proves that a file with exactly this SHA-256 hash existed no later than the time in the token (within the stated accuracy).

It does not prove who created the file, that its contents are true, or that it did not exist earlier. Any change to the file, even re-saving it, produces a different hash that the timestamp does not cover.

What you need

Commands below are for Linux, macOS (with OpenSSL 3) and WSL. Replace yourfile.pdf with your file and 1234 with your stamp's log index.

Contents of the evidence package

Unzip it into an empty folder:

mkdir evidence && cd evidence
unzip ../evidence.zip
File What it is
token.tsr The RFC 3161 timestamp token (DER). This is the core evidence.
tstinfo.der The signed timestamp data extracted from the token. This is exactly the entry in the public log.
certs/*.pem The certificates: the Existence Proof Root CA and the timestamping (TSA) certificate. Below, they are called root.pem and tsa.pem.
checkpoint.txt A signed checkpoint of the public log that includes this stamp.
inclusion-proof.json, stamp.json The proof that the stamp is in the log, and the full stamp record.
anchors/ Timestamps of log checkpoints by independent authorities (DigiCert, FreeTSA), once available.
VERIFY.md A copy of these instructions.

If you only have the token, get the certificates as described in step 4.

Step 1. Compute the file's hash

This establishes the fingerprint of the file you hold.

sha256sum yourfile.pdf                  # Linux, WSL
shasum -a 256 yourfile.pdf              # macOS
certutil -hashfile yourfile.pdf SHA256  # Windows Command Prompt

The result is 64 hexadecimal characters. Write it down. Different tools may print it in upper or lower case; that does not matter.

Step 2. Read the token

This shows what the timestamp says, in a standard tool's own words.

openssl ts -reply -in token.tsr -token_in -text

Example output (abridged):

TST info:
Version: 1
Policy OID: 2.25.203544903467926440216470024252890741783
Hash Algorithm: sha256
Message data:
    0000 - a9 48 90 4f 2f 0f 47 9b-8f 81 97 69 4b 30 18 4b   .H.O/.G....iK0.K
    0010 - 0d 2e d1 c1 cd 2a 1e c0-fb 85 d2 99 a1 92 a4 47   .....*.........G
Serial number: 0x04D2
Time stamp: Oct 26 18:30:00.123 2026 GMT
Accuracy: 0x01 seconds, unspecified millis, unspecified micros
Ordering: no
Nonce: unspecified
TSA: unspecified
Extensions:

Check:

Step 3. Check that the token matches your file

If you prefer a direct comparison:

sha256sum yourfile.pdf
openssl ts -reply -in token.tsr -token_in -text | sed -n '/Message data/,/Serial/p'

The two must show the same 32 bytes. Step 5 also checks this automatically.

Step 4. Confirm the root certificate is genuine

Everything that follows depends on the Existence Proof Root CA certificate. A forger could hand you a fake one, so check it against copies published independently, which an attacker would have to alter all at once (including historical archives) to deceive you.

If your evidence package lacks the certificates, download them:

curl -s https://existence-proof.com/.well-known/existence-proof-keys -o keys.json
jq -r .root_cert_pem keys.json > root.pem
jq -r .tsa_cert_pem keys.json > tsa.pem
jq -r .chain_pem keys.json > chain.pem

Compute the root certificate's fingerprint:

openssl x509 -in root.pem -noout -subject -fingerprint -sha256

The subject should name the Existence Proof Root CA. Compare the SHA-256 fingerprint (ignoring colons and letter case) with at least two of these independent copies:

  1. DNS. The fingerprint is published in a DNS TXT record:

    dig +short TXT _root-cert.existence-proof.com
    nslookup -type=TXT _root-cert.existence-proof.com   # Windows
    
  2. The Internet Archive. Open https://web.archive.org/web/2026*/existence-proof.com/.well-known/existence-proof-keys, choose the earliest snapshot (the first was taken on 2026-10-26 at 19:51:25 UTC: archived copy), and compute the fingerprint of the root_cert_pem it contains. The Internet Archive is independent of Existence Proof, and its snapshots cannot be changed after they are taken.

  3. A public GitHub gist at https://gist.github.com/ts-web/fd6530ab25de6c07031bda89534ab1d8, a copy of the keys published on GitHub with its own revision history.

  4. The website, at https://existence-proof.com/.well-known/existence-proof-keys (on its own, this is the weakest check, since it is the party under examination).

  5. Log entry #0 of the public log, written when the log was created.

If the copies disagree, stop: do not rely on the stamp until the discrepancy is explained.

The root certificate was itself timestamped by DigiCert and FreeTSA when it was created. If those tokens are supplied, check them as in step 10, using the SHA-256 of the root certificate in DER form (openssl x509 -in root.pem -outform DER | sha256sum) as the digest.

Step 5. Verify the token's signature

This establishes that the token was signed by a certificate issued by the root you checked in step 4, that the signed data has not been altered, and that it covers your file.

openssl ts -verify -in token.tsr -token_in -data yourfile.pdf -CAfile root.pem -untrusted tsa.pem

The expected output is:

Verification: OK

Notes:

After step 5 you have established that a party holding the Existence Proof signing key asserted that your file existed at the stated time. The remaining steps establish that this assertion was publicly recorded at that time and could not have been made later and backdated.

Step 6. Extract the log entry

Each stamp is recorded in the public log as its signed timestamp data (the DER-encoded TSTInfo). The evidence package includes it as tstinfo.der. To extract it from the token yourself and confirm they match:

openssl cms -verify -inform DER -in token.tsr -noverify -binary -out my-tstinfo.der
cmp my-tstinfo.der tstinfo.der && echo identical
openssl asn1parse -inform DER -in tstinfo.der

(-noverify skips certificate-chain checks only; step 5 did those. The CMS signature itself is still checked.)

The log identifies an entry by its leaf hash, the SHA-256 of a zero byte followed by the entry:

(printf '\000'; cat tstinfo.der) | sha256sum

Step 7. Verify that the entry is in the log

The inclusion proof is a list of hashes showing that the entry is part of the log summarised by a checkpoint, using the RFC 9162 (Certificate Transparency) algorithm. Save the following as verify_inclusion.py:

#!/usr/bin/env python3
"""Check that a timestamp's log entry is included in a signed checkpoint.

Usage: python3 verify_inclusion.py stamp.json tstinfo.der [checkpoint.txt]

stamp.json is the stamp record (from the evidence package, or from
https://existence-proof.com/v1/stamps/<index>). Uses only the Python
standard library. Implements RFC 9162 section 2.1.3.2.
"""
import base64, hashlib, json, sys

def sha256(b): return hashlib.sha256(b).digest()

def root_from_proof(index, size, leaf, proof):
    if not 0 <= index < size:
        raise ValueError("index is outside the tree")
    fn, sn, r = index, size - 1, leaf
    for p in proof:
        if sn == 0:
            raise ValueError("proof is too long")
        if fn % 2 == 1 or fn == sn:
            r = sha256(b"\x01" + p + r)
            if fn % 2 == 0:
                while fn % 2 == 0 and fn != 0:
                    fn //= 2; sn //= 2
        else:
            r = sha256(b"\x01" + r + p)
        fn //= 2; sn //= 2
    if sn != 0:
        raise ValueError("proof is too short")
    return r

stamp = json.load(open(sys.argv[1]))
entry = open(sys.argv[2], "rb").read()
checkpoint = open(sys.argv[3]).read() if len(sys.argv) > 3 else stamp["checkpoint"]

lines = checkpoint.split("\n")
origin, cp_size, cp_root = lines[0], int(lines[1]), base64.b64decode(lines[2])
index, size = stamp["index"], stamp["tree_size"]
proof = [bytes.fromhex(h) for h in stamp["inclusion_proof"]]
leaf = sha256(b"\x00" + entry)
computed = root_from_proof(index, size, leaf, proof)

print("log origin:          ", origin)
print("entry index:         ", index)
print("leaf hash:           ", leaf.hex())
print("tree size (proof):   ", size)
print("tree size (checkpt): ", cp_size)
print("root from proof:     ", computed.hex())
print("root in checkpoint:  ", cp_root.hex())
ok = size == cp_size and computed == cp_root
print("RESULT:", "OK - the entry is in the log" if ok else "FAILED")
sys.exit(0 if ok else 1)

Run it on the evidence package:

python3 verify_inclusion.py stamp.json tstinfo.der

The last line must read RESULT: OK - the entry is in the log. Check also that entry index equals the serial number from step 2, and that the log origin is existence-proof.com/v1.

You can repeat this against the log as it is today, which shows the entry has not been removed since:

curl -s https://existence-proof.com/v1/stamps/1234 -o stamp-latest.json
python3 verify_inclusion.py stamp-latest.json tstinfo.der

Step 8. Verify the checkpoint signature

A checkpoint is a short text file signed by the log's Ed25519 key, in the C2SP signed-note format:

existence-proof.com/v1
1235
<root hash, base64>
time 1793039400123
drand <chain hash> <round> <randomness>

— existence-proof.com/v1 <base64 key ID and signature>

The lines are: the log's name, the number of entries, the root hash, the time the checkpoint was signed (milliseconds since 1970, UTC), and the drand round included in it (step 9). The signature covers everything above the blank line.

Get the log's public key (checkpoint_vkey) from the keys file (step 4; compare it with the copies in the repository and the Internet Archive). It has the form existence-proof.com/v1+<key ID>+<base64 key>. Then:

VKEY=$(jq -r .checkpoint_vkey keys.json)

# 1. Convert the raw Ed25519 public key to PEM (the prefix is the fixed SPKI header).
echo "MCowBQYDK2VwAyEA$(echo "$VKEY" | cut -d+ -f3- | base64 -d | tail -c 32 | base64)" \
  | (echo '-----BEGIN PUBLIC KEY-----'; cat; echo '-----END PUBLIC KEY-----') > checkpoint-key.pem

# 2. The signed text: everything before the blank line, including its final newline.
awk '/^$/{exit} {print}' checkpoint.txt > checkpoint-body.txt

# 3. The signature: the last 64 bytes of the base64 value on the signature line.
grep '^— existence-proof.com/v1 ' checkpoint.txt | cut -d' ' -f3 | base64 -d | tail -c 64 > checkpoint-sig.bin

# 4. Verify (OpenSSL 3.0 or later).
openssl pkeyutl -verify -pubin -inkey checkpoint-key.pem -rawin -in checkpoint-body.txt -sigfile checkpoint-sig.bin

The expected output is Signature Verified Successfully. (On older macOS, use base64 -D instead of base64 -d.)

Optionally, check that the 4-byte key ID on the signature line matches the key: the first 8 hex digits of SHA-256("existence-proof.com/v1" || 0x0A || 0x01 || public key) must equal the key ID in VKEY, and the first 4 bytes of the decoded signature value.

After steps 7 and 8 you have established that the stamp is part of the public, append-only log, as summarised by a checkpoint signed by the log key.

Step 9. Check the drand round (not earlier than)

drand is a public randomness beacon operated by the League of Entropy, a group of independent organisations. Every round produces a random value that nobody can predict in advance. Each checkpoint contains the latest drand round, so the checkpoint cannot have been created before that round was published.

Take the chain hash, round and randomness from the checkpoint's drand line and fetch the same round from drand:

curl -s https://api.drand.sh/<chain hash>/public/<round>
curl -s https://api.drand.sh/<chain hash>/info

drand is also served by other independent relays (for example api2.drand.sh, api3.drand.sh and drand.cloudflare.com), which should return the same values. For a fully independent check of the round's signature, use the drand command-line client from the drand project.

Step 10. Check the external anchors (not later than)

Existence Proof regularly asks independent RFC 3161 timestamping authorities, DigiCert and FreeTSA, to timestamp its latest checkpoint. Each such anchor proves that the checkpoint, and every log entry it covers, existed no later than the anchor's time. Anchors are in the evidence package under anchors/ (once available; anchoring runs every 10 to 60 minutes), and are listed at https://existence-proof.com/v1/anchors.

For each anchor you need: the anchor token (.tsr), the exact checkpoint file it covers, and the authority's root certificate.

  1. Hash the anchored checkpoint. The anchor's message imprint is the SHA-256 of the checkpoint file, byte for byte:

    sha256sum anchored-checkpoint.txt
    
  2. Read the anchor token and confirm the message data equals that hash, and note the authority's time:

    openssl ts -reply -in anchor.tsr -token_in -text
    
  3. Get the authority's certificates. Extract the certificates embedded in the token:

    openssl pkcs7 -inform DER -in anchor.tsr -print_certs > anchor-certs.pem
    

    Get the authority's root certificate from the authority itself, not from Existence Proof:

    • FreeTSA: curl -s https://freetsa.org/files/cacert.pem -o freetsa-root.pem (and the TSA certificate, https://freetsa.org/files/tsa.crt). Compare its fingerprint with the one published on freetsa.org.
    • DigiCert: DigiCert's timestamping certificates chain to a DigiCert root (currently DigiCert Trusted Root G4). Download it from DigiCert's root certificate page (https://www.digicert.com/kb/digicert-root-certificates.htm), check the fingerprint listed there and in your operating system's trust store, and convert it to PEM if needed: openssl x509 -inform DER -in DigiCertTrustedRootG4.crt -out digicert-root.pem.
  4. Verify the anchor token:

    openssl ts -verify -in anchor.tsr -token_in -digest <SHA-256 from 1.> \
      -CAfile digicert-root.pem -untrusted anchor-certs.pem
    

    The expected output is Verification: OK. (For FreeTSA, use freetsa-root.pem and add tsa.crt to anchor-certs.pem if it is not embedded.)

  5. Connect the anchor to your stamp. The anchored checkpoint's tree size (line 2) must be greater than your stamp's log index, so the checkpoint covers your entry. Confirm the entry is in that checkpoint with an inclusion proof for that tree size (step 7, using the anchored checkpoint), or by rebuilding the log up to that size (step 11) and comparing root hashes.

The anchor's time is then an independent upper bound: your file existed no later than that time, whatever Existence Proof says.

Step 11. Rebuild the log yourself (optional, strongest)

The whole log is public, in the C2SP tlog-tiles format. Each tile holds 256 entries; each entry is a 2-byte big-endian length followed by the entry's TSTInfo DER. This script downloads the first SIZE entries, recomputes the RFC 6962 Merkle tree root, and prints it, so you can compare it with the root hash (line 3, base64) of any checkpoint of that size, including anchored ones. Save it as rebuild_root.py:

#!/usr/bin/env python3
"""Download the public log and recompute its root hash at a given size.

Usage: python3 rebuild_root.py SIZE [BASE_URL] [--show INDEX]

Fetches the entry tiles (C2SP tlog-tiles) for the first SIZE entries,
hashes every entry as an RFC 6962 leaf, and prints the Merkle tree root in
hex and base64, to compare with the root line of a checkpoint of that size.
With --show INDEX it also prints that entry's leaf hash and saves the entry
as entry-INDEX.der. Uses only the Python standard library.
"""
import base64, hashlib, sys, urllib.request

def sha256(b): return hashlib.sha256(b).digest()

def tile_path(n):
    s = f"{n:03d}"
    parts = []
    while True:
        parts.insert(0, s[-3:].rjust(3, "0"))
        n //= 1000
        if n == 0: break
        s = f"{n:03d}"
    return "/".join(["x" + p for p in parts[:-1]] + [parts[-1]])

def fetch(url):
    req = urllib.request.Request(url, headers={"user-agent": "rebuild_root.py"})
    with urllib.request.urlopen(req) as r:
        return r.read()

def mth(leaves):
    level = list(leaves)
    if not level: return sha256(b"")
    while len(level) > 1:
        nxt = [sha256(b"\x01" + level[i] + level[i + 1]) for i in range(0, len(level) - 1, 2)]
        if len(level) % 2: nxt.append(level[-1])
        level = nxt
    return level[0]

args = sys.argv[1:]
show = None
if "--show" in args:
    i = args.index("--show"); show = int(args[i + 1]); del args[i:i + 2]
size = int(args[0])
base = (args[1] if len(args) > 1 else "https://existence-proof.com").rstrip("/")

leaves = []
for t in range((size + 255) // 256):
    width = min(256, size - t * 256)
    path = f"{base}/tile/entries/{tile_path(t)}" + ("" if width == 256 else f".p/{width}")
    data = fetch(path)
    pos, k = 0, 0
    while k < width:
        n = int.from_bytes(data[pos:pos + 2], "big")
        entry = data[pos + 2:pos + 2 + n]
        if len(entry) != n: sys.exit(f"truncated tile {path}")
        idx = t * 256 + k
        leaf = sha256(b"\x00" + entry)
        if idx == show:
            open("entry-%d.der" % idx, "wb").write(entry)
            print("entry %d: leaf hash %s (saved entry-%d.der)" % (idx, leaf.hex(), idx))
        leaves.append(leaf)
        pos += 2 + n; k += 1

root = mth(leaves)
print("entries:     ", len(leaves))
print("root (hex):  ", root.hex())
print("root (base64):", base64.b64encode(root).decode())

For example, with an anchored checkpoint of 1300 entries and your stamp at index 1234:

python3 rebuild_root.py 1300 --show 1234
cmp entry-1234.der tstinfo.der && echo "my stamp is entry 1234"
sed -n 3p anchored-checkpoint.txt   # must equal "root (base64)"

Every entry is itself a signed timestamp's data, so you can inspect any of them with openssl asn1parse -inform DER -in entry-N.der.

Step 12. Putting it together: why the time cannot have been backdated

Suppose the operator issued a token today but wrote last month's date into it. Then:

So, for your stamp at index N with claimed time T:

  1. Find the first anchored checkpoint whose tree size is greater than N. Its anchor time must be at or after T (allowing for accuracy and the anchoring interval). It is an independent upper bound on when the entry was in the log.
  2. Optionally, find the last checkpoint whose tree size is N or less (it does not yet contain your entry). Its drand round (step 9) must have been published no later than T (allowing for accuracy). That checkpoint provably existed after its drand round, and your entry was appended after that checkpoint.

If both hold, the stated time is consistent with independent evidence from parties that the operator does not control.

Checklist for a report

Record, for each step, the command run, the tool versions (openssl version, python3 --version) and the output.

# Check Expected result
1 SHA-256 of the file 64 hex characters
2 Token contents Message data equals the file's hash; note time, accuracy, serial
4 Root certificate fingerprint Matches at least two independent published copies
5 openssl ts -verify Verification: OK
6 Log entry Extracted TSTInfo identical to tstinfo.der
7 Inclusion proof RESULT: OK, index equals serial number
8 Checkpoint signature Signature Verified Successfully
9 drand round Randomness matches drand; round time noted
10 External anchors Verification: OK; anchored tree size greater than index; anchor time at or after the stamp time
11 Log rebuild (optional) Recomputed root equals the checkpoint's root

Technical reference