Most developers think timestamping means sending a request to a Time Stamping Authority and storing the response.
Something like this:
POST /tsa
Content-Type: application/timestamp-query
<binary timestamp request>
But that is not verification.
That is only asking for a timestamp.
If you want to use a timestamp as evidence, you need to verify what came back.
And in RFC 3161, that means dealing with CMS SignedData.
The problem
An RFC 3161 timestamp response is not just a date.
It is a cryptographic structure.
Inside the response, there is a timestamp token. That token is usually a CMS SignedData object containing TSTInfo, signer information, signed attributes, certificates, and a signature.
If you want to treat the timestamp as evidence, you need to answer several questions:
- Does the timestamp token refer to the payload I actually submitted?
- Does the message imprint match my payload digest?
- Which certificate signed the timestamp token?
- Is the CMS signature valid?
- Was the signer certificate valid for timestamping?
- Can this verification be repeated later by another party?
Most basic integrations stop too early.
They parse the response, extract the generated time, store the token, and assume that is enough.
For a normal application timestamp, that may be acceptable.
For an evidence system, it is not.
What I am building
I am building AURORA, a cryptographic evidence layer for AI decisions.
The goal is not to decide whether an AI decision was fair, legal, or correct.
The goal is narrower:
preserve verifiable evidence of what was recorded.
A sealed AI decision record can include:
- SHA-256 record hash
- RSA digital signature
- RFC 3161 timestamp token
- public verification URL
- downloadable evidence bundle
The timestamp layer forced me to go deeper into RFC 3161 and CMS verification than I originally expected.
The code snippets below are simplified for explanation, but the verification boundaries are the same.
Layer 1: Parsing the timestamp response
The first layer is relatively straightforward.
You parse the timestamp response, extract the timestamp token, and read the embedded TSTInfo structure.
Conceptually:
tsr = tsp.TimeStampResp.load(token_der)
timestamp_token = tsr["time_stamp_token"]
signed_data = timestamp_token["content"]
tst_info = tsp.TSTInfo.load(
signed_data["encap_content_info"]["content"].parsed.dump()
)
Then you check the message imprint.
message_imprint = tst_info["message_imprint"]
hashed_message = message_imprint["hashed_message"].native
if hashed_message != expected_digest:
raise ValueError("Timestamp imprint does not match payload digest")
This confirms that the timestamp token refers to the digest you expected.
That matters because the TSA does not timestamp your original JSON, PDF, or database record directly.
It timestamps a digest.
If the digest in TSTInfo does not match your payload digest, the token is not evidence for your record.
But Layer 1 is still not enough.
It proves that the token contains the expected imprint.
It does not fully prove that the CMS signature is valid.
Layer 2: Verifying CMS SignedData
The harder part is verifying the CMS SignedData structure itself.
This is where the integration becomes less like an API call and more like applied cryptography.
A timestamp token is a signed object.
To verify it properly, you need to inspect the signer, the signed attributes, the encapsulated content, and the certificate usage.
1. Do not assume certs[0] is the signer
One easy mistake is assuming that the first certificate inside the CMS certificate set is the signer certificate.
That is unsafe.
CMS does not guarantee that certs[0] is the signer.
The signer must be selected using the SignerInfo identifier.
Conceptually:
def select_signer_certificate(certs, signer_info):
signer_id = signer_info["sid"]
for cert in certs:
if cert_matches_signer_id(cert, signer_id):
return cert
raise ValueError("Signer certificate not found")
The signer can be identified by issuer and serial number, or by subject key identifier.
If you choose the wrong certificate, verification fails.
Worse, you may build an implementation that appears to work only because one provider happens to order certificates in a convenient way.
That is not a verification strategy.
That is an accident.
2. Verify the message-digest signed attribute
CMS signed attributes usually include a message-digest attribute.
This digest must match the digest of the encapsulated TSTInfo content.
Conceptually:
expected_digest = extract_message_digest_attr(signed_attrs)
actual_digest = sha256(tst_info_der).digest()
if expected_digest != actual_digest:
raise ValueError("CMS message-digest does not match TSTInfo")
This step is easy to misunderstand.
The signature is not simply over your original payload.
In this structure, the signature is over the signed attributes.
Those signed attributes include a digest of the encapsulated content.
So the chain is:
original payload
→ payload digest
→ TSTInfo message imprint
→ TSTInfo DER
→ CMS signedAttrs message-digest
→ CMS signature
Each boundary matters.
If one layer is skipped, the evidence chain becomes weaker.
3. Handle the signed attributes encoding correctly
This was one of the most annoying parts.
CMS signedAttrs are encoded with an IMPLICIT context-specific tag on the wire.
But for signature verification, the signed attributes need to be verified as a SET OF attributes.
In simplified form:
signed_attrs_for_verification = b"\x31" + signed_attrs_wire[1:]
That small encoding detail can break verification for hours.
The data may look logically correct.
The parsed object may look correct.
The certificate may be correct.
The digest may be correct.
But if the exact bytes passed into signature verification do not match the expected DER encoding, the signature fails.
This is one of the places where “I parsed the object” and “I verified the evidence” become very different things.
4. Verify the CMS signature
Once the signer certificate is selected and the signed attributes are encoded correctly, the signature can be verified using the signer certificate’s public key.
Conceptually:
public_key.verify(
signature_bytes,
signed_attrs_for_verification,
padding.PKCS1v15(),
hash_algorithm,
)
If this succeeds, the CMS signature over the signed attributes is valid.
If it fails, the timestamp token should not be treated as verified evidence.
At this stage, you are no longer merely trusting that the TSA returned something that looks valid.
You are checking the cryptographic structure yourself.
5. Check certificate usage
A valid signature is still not the entire story.
The certificate should also be valid for timestamping.
For RFC 3161 timestamping, that usually means checking for the id-kp-timeStamping extended key usage.
Conceptually:
eku = cert.extensions.get_extension_for_class(
x509.ExtendedKeyUsage
).value
if ExtendedKeyUsageOID.TIME_STAMPING not in eku:
raise ValueError("Certificate is not valid for timestamping")
Without this check, you may verify a signature from a certificate that was not intended to issue timestamp tokens.
For an evidence layer, that distinction matters.
The question is not only:
“Was this signed?”
The better question is:
“Was this signed by the right kind of certificate, for the right kind of purpose, over the right content?”
Why this matters for AI decision evidence
AURORA is built around AI decision records.
A record might describe a loan decision, an insurance claim decision, a hiring review, a moderation outcome, a risk score, or another system-generated decision event.
For internal analytics, a normal database log may be enough.
For audit evidence, it is weaker.
A database timestamp says:
“This is what the database currently says.”
A cryptographic timestamp says:
“This digest existed at this time, according to an external timestamp authority.”
A verified cryptographic timestamp says:
“We checked that the timestamp token, imprint, signature, signer certificate, and evidence chain are internally consistent.”
That is a stronger evidence boundary.
What AURORA does not claim
AURORA does not determine whether an AI decision was fair.
It does not determine whether a decision was ethical.
It does not replace legal review.
It does not guarantee regulatory compliance.
Those are separate questions.
AURORA focuses on evidence preservation.
It tries to answer a narrower but important question:
“What exactly was recorded, and can that record be verified later?”
Key takeaways
The main lessons from implementing this layer:
- A timestamp response is not the same as a verified timestamp.
- RFC 3161 timestamp tokens contain CMS SignedData.
- The message imprint must match the payload digest.
- The CMS
message-digestsigned attribute must match TSTInfo. - The signer certificate should be selected by SignerInfo, not by array position.
- signedAttrs encoding details matter.
- The signer certificate should be valid for timestamping.
- Evidence systems should expose verification metadata, not hide it.
The result
AURORA’s verification layer is designed to verify RFC 3161 timestamp evidence instead of blindly trusting that a TSA response is valid.
That timestamp layer sits alongside:
- SHA-256 record hashing
- RSA signatures
- public verification pages
- downloadable evidence bundles
The broader goal is simple:
AI decision records should be verifiable later.
Not just logged.
Verified.
Project: https://aurora-audit.com
Sample PDF: https://aurora-audit.com/sample-audit.pdf
Have you implemented RFC 3161, CMS SignedData, or timestamp verification before?
I would be interested in hearing what edge cases you ran into.
Top comments (1)
Additional note:
I intentionally separated “timestamp received” from “timestamp verified” in this post.
For normal application logging, storing the TSA response may be enough.
For evidence systems, I think the verification boundary needs to be explicit: imprint check, signedAttrs digest, signer certificate selection, signature verification, and timestamping EKU.
That distinction is what pushed me to build this layer into AURORA.