You need to show, later, that a file existed by a certain date. Three families of tool do this: RFC 3161 timestamping authorities, the OpenTimestamps protocol, and hosted anchoring services such as ProofLedger. They differ in what you hold afterwards, who you trust, and what can lapse. This post spends most of its time on RFC 3161, because its token has a lifetime that catches people out, then sets the other two against it.
RFC 3161 timestamping authorities
RFC 3161 is the IETF standard for timestamp tokens issued by a Time Stamping Authority (TSA). Certificate authorities such as DigiCert, Sectigo and GlobalSign operate TSAs, mostly for code signing and signed PDFs, and FreeTSA runs a public one you can use from the command line.
The exchange is a request and a response. Your client builds a TimeStampReq holding the file's hash (the message imprint) and usually a nonce, and posts it to the TSA. The TSA signs the hash together with its own clock reading and returns a TimeStampResp containing a TimeStampToken: a CMS signed structure whose TSTInfo carries the hash, the generation time, a serial number and the TSA's policy identifier. With OpenSSL:
openssl ts -query -data report.pdf -sha256 -cert -out report.tsq
curl -s -H "Content-Type: application/timestamp-query" --data-binary @report.tsq https://freetsa.org/tsr -o report.tsr
openssl ts -reply -in report.tsr -text
openssl ts -verify -in report.tsr -data report.pdf -CAfile tsa-chain.pem
Where the record lives: in the .tsr file you keep. There is no public ledger. The token is a signature, and the TSA does not need to remember issuing it.
Who you trust: the TSA's clock, its key handling, and the certificate chain from its signing certificate up to a root your verifier accepts. The check is local once you have the chain, which is a real advantage: a third party needs the file, the token and the chain, and nothing else.
Why the token has a lifetime
The signing certificate expires some years after issue, and can be revoked before that. Ordinary verification evaluates the chain as of now, so once the signing certificate has expired the check fails unless the verifier is told to evaluate the signature as of the time it was made and has the revocation information that was current then. That is the long-term validation problem, and the standards answer it in two parts:
- Capture validation material at signing time: the chain, and the OCSP responses or CRLs that show the certificate was good then. The PAdES and CAdES long-term profiles embed this inside the signed document.
- Re-timestamp before the clock runs out. An archive timestamp from a current TSA covers the earlier token plus its validation material, so the chain of trust is renewed under a live certificate. Repeat before each one expires.
If nobody does that, a token can still be correct and yet impossible to validate with ordinary tooling once its chain has aged out. For a signed PDF in a document system that handles renewals, this is routine. For a loose .tsr sitting in a folder, it is a maintenance task someone has to own, and the question "is this still verifiable" has a different answer each year.
OpenTimestamps
OpenTimestamps is an open protocol with an open-source client. ots stamp sends the file's hash to public calendar servers, which aggregate hashes into a Merkle tree and commit the root in a Bitcoin transaction. The client writes a .ots receipt next to the file. The receipt starts as a pending attestation; after the calendar's transaction confirms, ots upgrade fetches the path from your hash to the committed root, and ots verify recomputes the path and checks it against Bitcoin block data.
Where the record lives: the Bitcoin transaction plus your .ots receipt. Who you trust: the chain, and the calendar until the upgrade has happened. What can lapse: nothing expires, but the receipt is the only link between your hash and the on-chain root, and a receipt that was never upgraded cannot be completed if its calendar is gone. Verification needs the client and a source of Bitcoin block data.
Hosted anchoring: ProofLedger
A hosted service does the chain write for you behind an API. ProofLedger takes a SHA-256 hash and anchors it in its own Polygon transaction, usually within a minute. The file never leaves your machine.
curl -X POST https://proofledger.io/api/v1/proof \
-H "Authorization: Bearer sk_your_key" \
-H "Content-Type: application/json" \
-d '{"sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", "filename": "report.pdf", "external_id": "report-7781"}'
The 201 response carries the proof id, polygon_status (PENDING until the transaction is recorded), a certificate_url and a verification_url. Poll GET /api/v1/proof/{id} until polygon_status is ANCHORED and chain_tx is set, or take the proof.polygon.anchored webhook event. Then save the bundle:
curl -H "Authorization: Bearer sk_your_key" \
"https://proofledger.io/api/v1/proof/42/bundle?download=1" -o report.pdf.proof.json
Where the record lives: the Polygon transaction, plus the bundle, a JSON document with the hash, the chain, the transaction id and the anchor time. ProofLedger's certificate page and public verify endpoint are convenience views of that transaction, not the record.
Who you trust: the service, between your request and the transaction being recorded. After that, the transaction is public and the check runs without the service.
What can lapse: no certificate, so nothing expires on a schedule. What you must keep is the file and the bundle. Each hash is in its own transaction, so the bundle's merkle_path is empty and there is no tree to recompute. (Before 15 October 2026 ProofLedger also anchored some proofs on Bitcoin in batches; those bundles do carry a path, and every proof since is Polygon only.)
Checking each one yourself
RFC 3161: openssl ts -verify with the file, the token and the chain, plus whatever validation material you captured if the certificate has since expired.
OpenTimestamps: ots verify with the file, the upgraded receipt and Bitcoin block data.
ProofLedger: the open-source verify-proof tool, offline, then a block explorer.
pip install verify-proof
verify-proof verify report.pdf --proof report.pdf.proof.json
from verify_proof import hash_file, load_proof, verify_proof
file_hash = hash_file("report.pdf")
bundle = load_proof("report.pdf.proof.json")
result = verify_proof(file_hash, bundle)
print(result["verified"]) # True when the file matches the bundle and the bundle names a transaction
print(result["blockchain"]) # "polygon"
print(result["tx_id"]) # open this on a block explorer to confirm the anchor
print(result["anchored_at"])
verify-proof recomputes the file's hash and compares it with the bundle; it does not read the chain. The anchor time is what the bundle says until you open tx_id on a Polygon block explorer and see the hash and the block time there. Anyone with only the hash can also ask the public endpoint, GET /api/v1/verify?hash=ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad, which needs no key and is limited to 120 requests an hour per IP.
What every one of them proves
The same claim, three ways: a file with this exact hash existed by the recorded time, and the file you hold matches it. None of them says who made the file, whether its contents are true, or who owns it. Whether a court accepts any of this is for the court and the lawyers.
Choosing
Take RFC 3161 when the timestamp belongs inside a signed document or a code-signing flow, your tooling already validates certificate chains, and something in your process owns the renewal. Take OpenTimestamps when you want no vendor and no account and will run the client, including the upgrade step. Take a hosted anchor when you want an API call from your own code, a public record per hash with no certificate lifetime, and a bundle a third party can check with an open tool.
To check a ProofLedger proof without ProofLedger: run verify-proof verify on the file and its bundle, then open the bundle's tx_id on any Polygon block explorer. The API is documented at https://proofledger.io/api.html.