Three narrow vertical beams of pale gold light descend from beyond the top edge of a deep navy field and converge on a single small bright point near the bottom center. The beams are of three visibly different lengths. The field is otherwise empty.

I sent the same 32 bytes to three timestamp authorities. All three signed it. Two of them told me who they were and one of them did not, and the one that did not is the one you have probably already used.

That is not a bug and it is not a slight. RFC 3161 makes the authority’s own name an optional field and says the real identification is the signing certificate. I knew that. I had read it. What I had not done was look at two tokens side by side and see the field present in one and the literal string unspecified in the other.

This post is what came back. It is not a tutorial on openssl ts, and it does not make your timestamps more correct. What it does is show you where the proof actually breaks, which in two runs across three authorities turned out to be nowhere near the protocol.

What these are for, if you have only met them through code signing

A timestamp authority is a third party whose entire job is to sign one statement: that it saw a particular fingerprint at a particular moment. It does not receive your file. It cannot read it. It takes no position on whether anything in it is true.

Most of us meet these through code signing, where they solve a problem that has nothing to do with documents. A signature made with a certificate that later expires has to stay distinguishable from one applied after the expiry, and a trusted timestamp is how that distinction survives. That is why the free public endpoints exist at all, and it is why the terms of use on the commercial ones are a promise to code-signing customers rather than to you.

The part worth noticing is that the same infrastructure answers a different question perfectly well, and mostly nobody points it at one:

Anything whose priority date you might have to assert. A design, a manuscript, a source tree you have not pushed yet, a proposal you sent to a prospect. Any place “I had this before they did” is a sentence you might one day need to say.

Evidence you collect yourself, before anybody asks for it. Screen captures, exports from a system somebody else runs, photographs of a condition. This is the weakest category of date in existence, because you produced it and you hold it. It is also the one most likely to matter.

Your own artifacts in an investigation. Collection notes, a hash list, a working copy of something volatile. A timestamp does not replace a chain of custody and it is not trying to. It fixes one end of it to something you do not run.

A policy or configuration you may need to show was in force on a date. The runbook, the retention schedule, the baseline you swear the estate matched in March.

None of that requires an account, a vendor relationship, or a line item. It takes about a minute per file.

The request is 69 bytes

Here is the whole thing:

openssl ts -query -data agreement.pdf -sha256 -cert -out request.tsq

That produced a 69-byte file. Reading it back:

openssl ts -query -in request.tsq -text
Version: 1
Hash Algorithm: sha256
Message data:
    0000 - 8b a3 6a 14 e7 fd ef da-cf 1a 0e 1d 81 73 1d 21
    0010 - 86 4a 53 d2 67 34 23 89-ce e6 ef d7 7d 08 cf 90
Policy OID: unspecified
Nonce: 0x62058CDB56DB8BF1
Certificate required: yes

A version, an algorithm, 32 bytes of hash, a nonce, and a flag asking the authority to include its signing certificate in the reply. No filename. No size. No metadata. Nothing that says what the bytes were.

That property is the whole reason this method survives contact with material you are not allowed to hand around. The authority is not trusted with the document because the authority never sees the document.

What came back

Three endpoints, one identical request, all HTTP 200:

Authority Response
https://freetsa.org/tsr 4,642 bytes
http://timestamp.digicert.com 6,006 bytes
http://timestamp.sectigo.com 6,634 bytes

And here is the part I actually wanted to see, read out of each with openssl ts -reply -in <file> -text:

Field FreeTSA DigiCert Sectigo
Policy OID tsa_policy1 2.16.840.1.114412.7.1 1.3.6.1.4.1.6449.2.1.1
Serial number 0x082EAE5F 0x4DB751C3...16D31B81 0x70C7AFCF...3576FA51BBA0
Time stamp Sep 16 20:25:15 2026 GMT same second same second
Message imprint matches matches matches
Accuracy unspecified unspecified unspecified
Ordering yes no no
TSA DirName:/O=Free TSA/... unspecified DirName:/C=GB/O=Sectigo Limited/...

Every one of them returned a policy identifier and a serial number, because TSTInfo requires both. DigiCert returned no TSA field at all, because TSTInfo does not require it, and the RFC is explicit that when the field is present it is a hint rather than identification. The binding identification is the signing certificate in the CMS structure, every time, for all three.

I ran the whole thing twice, the second time with a fresh nonce. Same pattern both times.

Two smaller things fell out of that table.

Nobody returned an accuracy. The field is optional and all three declined it. So the genTime you get carries no stated bound on how far off it might be. In practice these are NTP-disciplined and the error is irrelevant to anything you would use a document timestamp for, but if you were planning to argue about sub-second ordering between two tokens from different authorities, that is the field you were going to need and it is not there.

The serial numbers are shaped differently. FreeTSA returned a 32-bit value and the commercial pair returned something long and random-looking. I am not going to tell you FreeTSA uses a sequential counter, because two tokens are two observations and that is not a finding. It is a thing I noticed and would want to check before relying on.

What is in the other four and a half kilobytes

The request was 69 bytes. The smallest reply was 4,642. That gap is almost entirely certificates, and it is what the -cert flag bought:

Authority Reply Token Certificates embedded
FreeTSA 4,642 B 4,633 B 2
DigiCert 6,006 B 5,997 B 3
Sectigo 6,634 B 6,625 B 3

The reply is a thin wrapper around a CMS SignedData token, nine bytes of envelope in every case. Pull the token out with -token_out and you can read the chain that came with it:

openssl ts -reply -in freetsa.tsr -token_out -out freetsa.tk
openssl pkcs7 -inform DER -in freetsa.tk -print_certs -noout

Ask for the certificates and you get a token that carries the material needed to check itself, minus a root. Skip -cert and you get a smaller token and a future problem, because the signing certificate you need in year six is the one that was current in year one.

The nonce is the other field worth understanding. It is a random value you generate, the authority copies it back verbatim, and it is what ties this reply to your request rather than to a reply somebody replayed at you. openssl ts -verify checks it for you when you pass -queryfile. If you verify with -data instead, against the document, the nonce is not checked, which is a perfectly reasonable thing to do in year seven when the request file is long gone and is worth knowing you are doing.

And there is a clock in here that nobody mentions. The signing certificates expire:

Authority Signing certificate notAfter
FreeTSA 2 February 2040
DigiCert 4 November 2037
Sectigo 24 June 2037

Do not take those from me. They are in the token you already have:

openssl pkcs7 -inform DER -in freetsa.tk -print_certs   | awk '/BEGIN CERT/,/END CERT/' | openssl x509 -noout -enddate

Eleven to fourteen years out, as of September 2026; these certificates rotate, so check your own token rather than this table. That is a long time and it is not forever, and it is the reason long-term formats re-timestamp: before the old signature stops being checkable, you take a fresh timestamp over the document together with the old token and its validation material, which carries the proof forward. If you are stamping things you expect to matter in 2040, put a calendar entry somewhere around 2036.

Verification fails, and the error message is about the wrong thing

This is where I lost the most time, and it has nothing to do with timestamps.

openssl ts -verify -in freetsa.tsr -queryfile request.tsq
Verify error:self-signed certificate in certificate chain
Verification: FAILED

Fair enough: FreeTSA’s root is not in my trust store. So I tried DigiCert, whose root has been in every trust store on earth for years:

Verify error:unable to get local issuer certificate
Verification: FAILED

That is the interesting one. The issuer is local. It is sitting in /etc/ssl/certs/ca-certificates.crt along with everything else. The ts -verify subcommand does not consult the system trust store at all, and it will not go looking. Name the bundle you already have and it passes:

openssl ts -verify -in digicert.tsr -queryfile request.tsq \
  -CAfile /etc/ssl/certs/ca-certificates.crt
Verification: OK

Four cases, and the useful reading is the pair in the middle:

Case Result
FreeTSA, no -CAfile FAILED, self-signed certificate in certificate chain
FreeTSA, its published chain Verification: OK
DigiCert, no -CAfile FAILED, unable to get local issuer certificate
DigiCert, the system bundle Verification: OK

The rule most write-ups give you is “keep the authority’s chain.” That is right for FreeTSA and beside the point for DigiCert.

The actual rule is in the man page, and it is not a quirk of my build. openssl-ts(1) says plainly that at least one of -CAfile, -CApath or -CAstore must be specified. There is no default and there was never meant to be one. So you always name a trust source, and which one depends on whether the root is already yours: the authority’s published chain for FreeTSA, and for DigiCert either the system bundle by path or -CAstore if your build has it wired to the OS store.

The error strings above are from OpenSSL 3.0.13 and I have not checked 3.5 or LibreSSL. The requirement is documented; only the exact wording of the failure is something I measured.

The binding is the fragile part, and it breaks on a save

The token commits to the exact bytes you hashed. I knew that too. Here is what it costs.

File What I did Size Against the token
agreement.pdf nothing 763 Verification: OK
agreement-save.pdf wrote a Title tag with exiftool 4,048 FAILED, message imprint mismatch
agreement-saveas.pdf qpdf --linearize 1,313 FAILED, message imprint mismatch

Two different mechanisms, one consequence. exiftool appends an incremental update and qpdf rewrites the file from scratch, which is the same split as Acrobat’s Save and Save As. Both change the bytes, so both change the hash, so both detach the token.

The detail I did not expect is that the incremental update produced a file more than three times the size of the full rewrite, because appending keeps everything it replaced. Which is its own forensic point, and a different post.

I should be plain about the substitution: I did not use Acrobat. I reproduced the two shapes with exiftool and qpdf on Linux. If the specific claim you need is about Acrobat’s behavior, go get it from Acrobat.

What this does not do

It does not prove who wrote the file, who held it, or that a word of it is true. It proves that a hash matching those exact bytes was submitted to that authority no later than that instant, and that the token has not been altered since. Everything else you might want to conclude is somewhere else.

It also does not reach backward. A file you timestamp today says nothing about last year.

And the failure that will actually get you is none of the above. It is that the token is a separate file. The package that has to survive to year seven is four things: the file unmodified since you hashed it, the response, the authority’s chain as published at the time, and a note of what you did and when. Three of those four are the ones people lose.

Where this sits

Six years ago I wrote about interactively exploring NTFS timestamps, which was about reading dates that a machine you control wrote down. This is the other end of the same problem: getting a date from a machine nobody in the argument controls. What changed in between is not the protocol, which has been stable since 2001. What changed is that the infrastructure is free, unauthenticated, and sitting there.

If you want the version of this written for the person holding the document rather than the person running the commands, I covered that separately at how to prove a document existed on a given date.

What to run

Pick a directory that matters. A contracts folder, a collection notes folder, the source tree for the thing you are about to talk about publicly.

for f in *.pdf; do
  openssl ts -query -data "$f" -sha256 -cert -out "$f.tsq"
  curl -sS -H "Content-Type: application/timestamp-query" \
    --data-binary "@$f.tsq" https://freetsa.org/tsr -o "$f.tsr"
done

Then download the chain once, keep it beside the tokens, and write yourself the note. The whole exercise is a minute a file and the tokens are four kilobytes each.

Then open one of them and look inside. That part took me longer than the timestamping did, and it is the only reason this post exists.