RFC 3161 timestamps explained, with openssl and curl

RFC 3161 is the Internet standard for trusted timestamps: a way for a third party, the Time-Stamping Authority (TSA), to sign a statement that a given piece of data existed at a given moment. It dates from 2001, it is what PDF signatures, code signing and EU qualified electronic time-stamps use under the hood, and it is small enough to try end to end with openssl and curl in two minutes. This post does exactly that, then looks at what the token contains, how to verify it, and what makes a timestamp qualified in the EU.

The idea in one paragraph

You hash your data and send only the hash to the TSA. The TSA adds the current time from a trusted clock, signs the hash and the time together, and sends the signed result back. Later, anyone can check that signature against the TSA’s certificate and compare the hash with the data in front of them. If both match, the data existed at that time and has not changed since — and the TSA never saw the data itself.

Try it: a timestamp with openssl and curl

Any RFC 3161 TSA works. For a test, SK ID Solutions runs a public demo service at http://demo.sk.ee/tsa/ (test certificates, no legal value), so set TSA_URL to that:

# 1. Build a request: openssl hashes the file and keeps only the digest
openssl ts -query -data contract.pdf -sha256 -cert -out request.tsq

# 2. Send it to a TSA over plain HTTP (RFC 3161 §3.4)
curl -s -H "Content-Type: application/timestamp-query" \
     --data-binary @request.tsq "$TSA_URL" -o response.tsr

# 3. Look inside the reply
openssl ts -reply -in response.tsr -text

The reply, as we received it this morning:

Status info:
Status: Granted.
Status description: Operation Okay

TST info:
Version: 1
Policy OID: 0.4.0.2023.1.1
Hash Algorithm: sha256
Message data:
    0000 - 58 91 b5 b5 22 d5 df 08-6d 0f f0 b1 10 fb d9 d2
    0010 - 1b b4 fc 71 63 af 34 d0-82 86 a2 e8 46 f6 be 03
Serial number: 0x2A701CCA05B1011D
Time stamp: Oct 14 06:25:55 2026 GMT
Accuracy: 0x01 seconds, unspecified millis, unspecified micros
Ordering: no
Nonce: 0x7E65135E3E5E2255
TSA: DirName:/CN=DEMO SK TIMESTAMPING UNIT 2025E/organizationIdentifier=NTREE-10747013/O=SK ID Solutions AS/C=EE
Extensions:
qcStatements: ...

Three lines matter most. Message data is the SHA-256 of the file, exactly as sent. Time stamp is the time the TSA vouches for. Policy OID says which rules the TSA operated under. Nothing in the reply contains the file.

What is in the request

A TimeStampReq is a small DER-encoded ASN.1 structure, sent as application/timestamp-query:

FieldMeaning
versionAlways 1
messageImprintThe hash algorithm and the hash of your data — the only thing about your data the TSA sees
reqPolicyOptional: the TSA policy you want the stamp issued under
nonceOptional random number, echoed back so you can match the reply to the request
certReqAsk the TSA to include its signing certificate in the reply (you almost always want this)

What is in the reply — and the token inside it

The reply (TimeStampResp, application/timestamp-reply, usually saved as .tsr) has two parts: a status (granted, rejection, …) and, if granted, the TimeStampToken. The token is a CMS SignedData structure — the same container S/MIME uses — whose signed content is a TSTInfo:

TSTInfo fieldMeaning
policyThe policy the TSA followed — 0.4.0.2023.1.1 is the ETSI baseline time-stamp policy
messageImprintYour hash, copied unchanged from the request
serialNumberUnique per TSA; together with the TSA name it identifies the stamp
genTimeThe time, in UTC, that the TSA vouches for
accuracyHow far genTime may be from true UTC
nonceYour nonce, if you sent one
tsaThe TSA's name
extensionsFor EU qualified stamps: a qcStatements extension (ETSI EN 319 422)

The token is the part worth keeping: it is self-contained evidence. Many systems store only the token and drop the status wrapper. That is why openssl has a -token_in flag — without it, openssl expects a full reply and fails on a bare token with wrong tag … TS_STATUS_INFO, which trips up almost everyone once.

RFC 5816 updated the standard in 2010 so the signer certificate can be referenced with SHA-256 instead of SHA-1 (ESSCertIDv2). Every current TSA uses it; you only notice it when an old verifier refuses a modern token.

Verify it

# tsa-chain.pem = the TSA's issuing CA + root, downloaded from the provider itself
openssl ts -verify -data contract.pdf -in response.tsr -CAfile tsa-chain.pem
# Verification: OK

openssl re-hashes the file, compares the digest with the token’s message imprint and checks the TSA’s signature up to the root you supply. Two rules keep this honest:

  • Take the root from the provider, never from the token. The token can carry certificates, but whoever made the token chose them. Download the chain from the TSA operator’s website and compare the root’s fingerprint.
  • Check the time against the certificate. The TSA certificate must have been valid — and not revoked — at genTime. openssl checks the chain; it does not fetch revocation data for you.

RFC 3161 vs. a qualified electronic time-stamp

RFC 3161 defines the format and the protocol. It says nothing about how good the TSA’s clock is, how its keys are protected or whether a court must believe it. Anyone can run a TSA — openssl even ships one.

In the EU, the eIDAS Regulation adds a legal layer on top. A qualified electronic time-stamp is issued by a qualified trust service provider that is supervised and audited, and it “shall enjoy the presumption of the accuracy of the date and the time it indicates and the integrity of the data to which the date and time are bound” (Article 41(2)) — in every member state. Technically it is still an RFC 3161 token, profiled by ETSI EN 319 422. To tell one apart:

  1. The issuing TSA is listed on the EU Trusted List as a qualified time-stamping service (service type QTST) and was granted at genTime.
  2. The token’s TSTInfo carries the qcStatements extension with the EU-qualified statement from EN 319 422 — the qcStatements line in the output above. (The demo service includes it too, so the Trusted List check is the one that decides.)
  3. The token is signed with a key and algorithm that are still considered secure.

The European Commission’s free DSS validator runs all three checks against the live Trusted Lists.

What gets hard when you run this yourself

  • Contracts and cost per stamp. Qualified TSAs sell stamps individually or in packages, typically behind a contract, an IP allow-list or credentials. One stamp per document adds up quickly.
  • Certificates expire. A TSA certificate lasts a few years. To keep a token verifiable for decades you have to collect the certificates and revocation data while they are available and re-stamp the evidence before the old one stops being trustworthy — RFC 4998 evidence records exist for exactly this. Our ETSI TS 119 511 explainer covers the service side.
  • Availability. If your one TSA is down, your stamping is down. You want a second qualified provider as a fallback.

The same thing as an API: TrustBeat

TrustBeat Document Anchoring does the above for you over REST/JSON. You send a SHA-256 hash; every ten minutes all submitted hashes are combined into a Merkle tree, and its root gets one qualified RFC 3161 time-stamp from SK ID Solutions (EuroCert as fallback). Your proof contains that token plus the short Merkle path from your hash to the root. You can try it without an account:

# No account: 3 hashes per IP per day
curl -s https://api.trustbeat.eu/v1/public/anchor \
     -H "Content-Type: application/json" \
     -d "{\"hash\": \"$(sha256sum contract.pdf | cut -d' ' -f1)\"}"
# → 202 {"id": "...", "status": "pending", ...}

# Within ~10 minutes:
curl -s https://api.trustbeat.eu/v1/public/proof/<id> -o proof.json

The proof’s token field is a standard RFC 3161 TimeStampToken, so the openssl check above still works — with -token_in, and with the Merkle root as the digest, because that is what was stamped:

# "token" is a bare TimeStampToken (base64 DER) — note -token_in
python3 -c "import base64,json; open('token.der','wb').write(base64.b64decode(json.load(open('proof.json'))['token']))"
ROOT=$(python3 -c "import json; print(json.load(open('proof.json'))['merkle_root'])")
openssl ts -verify -token_in -in token.der -digest "$ROOT" -CAfile tsa-chain.pem

Manual verification walks through every step, including recomputing the Merkle path from your file, and Inspect token decodes any RFC 3161 token in the browser. Document anchors are also kept verifiable over time: each gets an RFC 4998 evidence record, which we renew before the certificates behind it expire.

The short version

  • RFC 3161 = send a hash, get back a TSA-signed (hash, time) pair: the token.
  • Keep the TimeStampToken; with openssl, a bare token needs -token_in.
  • Verify against a root you fetched from the TSA operator, not one from the token.
  • Qualified is a legal status on top of RFC 3161: an EU Trusted List entry plus the EN 319 422 profile, with a presumption of accuracy across the EU.
  • Tokens outlive certificates only if someone renews the evidence.

If you would rather call one endpoint than manage TSA contracts, fallbacks and renewal, start free — 100 anchors a month, no card.