---
title: "Add a timestamp to a PDF signature in C#"
url: https://recursiveai.org/onecrypto/cookbook/timestamp-pdf-signature/
description: "Add an RFC 3161 timestamp to a PDF signature in C# (PAdES B-T) from a public time-stamp authority, freetsa.org, then check the token with openssl ts -verify. Runnable code with OneCrypto.Pdf 0.2.0-preview.1."
version: "0.2.0-preview.1"
---

# Add a timestamp to a PDF signature in C#

A signature time-stamp is an RFC 3161 token from a time-stamp authority that proves the signature existed at a given time, so it can be checked after the signing certificate expires. With `RecursiveAI.OneCrypto.Pdf`, set `Level = PadesLevel.Timestamped` and pass a function that sends the time-stamp request to an authority. The program below signs the IRS Form W-9 and gets the time-stamp from [freetsa.org](https://freetsa.org), a free public authority, over `RecursiveAI.OneCrypto.Online`.

```shell
dotnet new console -o PdfTimestamp && cd PdfTimestamp
dotnet add package RecursiveAI.OneCrypto.Pdf --version 0.2.0-preview.1
dotnet add package RecursiveAI.OneCrypto.Online --version 0.2.0-preview.1
curl -LO https://www.irs.gov/pub/irs-pdf/fw9.pdf
curl -O https://freetsa.org/files/cacert.pem -O https://freetsa.org/files/tsa.crt
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -days 365 \
  -subj "/CN=Ada Lovelace" -addext "basicConstraints=critical,CA:FALSE" \
  -addext "keyUsage=critical,digitalSignature,nonRepudiation" \
  -keyout signer-key.pem -out signer.pem
```

OneCrypto is a preview, so `dotnet add package` needs `--version` or `--prerelease`. The OpenSSL line makes a self-signed test certificate; use your own certificate and key for real documents. Put this in `Program.cs` and run `dotnet run`:

```csharp
using OnePdf;
using RecursiveAI.OneCrypto;
using RecursiveAI.OneCrypto.Online;
using RecursiveAI.OneCrypto.Pdf;
using RecursiveAI.OneCrypto.Validation;

using LoadedItems signerFile = Crypto.Load("signer.pem");
using LoadedItems keyFile = Crypto.Load("signer-key.pem");
Certificate signer = signerFile.Certificates[0];

// The time-stamp function: POST the request to the authority, return its reply
var fetcher = new Fetcher(new HttpTransport(new()), new FetchOptions());
PadesTimestampFunction freeTsa = async (request, ct) =>
{
    FetchResult r = await fetcher.FetchAsync(new FetchRequest
    {
        Uri = new Uri("https://freetsa.org/tsr"),
        Method = "POST",
        Body = request.Encoded,
        Headers = [new("Content-Type", "application/timestamp-query")],
    }, ct);
    return r.IsSuccess ? r.Body.ToArray() : throw new IOException(r.Reason);
};

using (PdfDocument doc = PdfDocument.Open("fw9.pdf",
    new PdfOpenOptions { AllowEditing = true }))
using (FileStream output = File.Create("fw9-timestamped.pdf"))
{
    PadesSignResult result = doc.SignPades(output, keyFile.PrivateKeys[0].Signing!,
        signer, [], new PadesSignOptions
        {
            Level = PadesLevel.Timestamped,
            Timestamp = freeTsa,
        });
    Console.WriteLine($"Signed {result.FieldName}: {result.Level}");
}

// Verify, trusting the test signer and freetsa.org's root
using LoadedItems tsaRootFile = Crypto.Load("cacert.pem");
Certificate tsaRoot = tsaRootFile.Certificates[0];
using PdfDocument signedDoc = PdfDocument.Open("fw9-timestamped.pdf");
PadesReport report = signedDoc.VerifyPades(new PadesVerifyOptions
{
    TrustAnchors = [TrustAnchor.FromCertificate(signer),
        TrustAnchor.FromNameAndKey(tsaRoot.Subject, tsaRoot.PublicKey)],
    ReferenceTime = DateTimeOffset.UtcNow,
});
PadesSignatureReport s = report.Signatures[0];
PadesTimestamp stamp = s.Timestamps[0];
Console.WriteLine($"Valid: {s.IsValid}, level: {s.Level}");
Console.WriteLine($"Time-stamp: {stamp.GenTime:u}, trusted: {stamp.IsTrusted}");
Console.WriteLine($"Proof of existence: {s.ProofOfExistence:u}");

// For openssl ts -verify: the token and the signature value it covers
File.WriteAllBytes("timestamp.tsr", stamp.Report!.Token!.Encoded.ToArray());
File.WriteAllBytes("signature.bin", s.Cms!.Signers[0].Signer.Signature.ToArray());
```

```text
Signed Signature1: Timestamped
Valid: True, level: Timestamped
Time-stamp: 2026-11-01 08:35:30Z, trusted: True
Proof of existence: 2026-11-01 08:35:30Z
```

The token sits inside the signature, as an unsigned attribute of its CMS, in the room the signature reserved. The verifier checks every RFC 3161 rule, that the token covers this signature’s value and that the authority chains to a trust anchor at the token’s time. Then it judges the signature at that time instead of the reference time: that is the proof of existence. The signing time written in the PDF is only a claim and is never used.

## Check it with OpenSSL

`openssl ts -verify` checks the same token against the signature value it stamps:

```shell
openssl ts -verify -data signature.bin -in timestamp.tsr -token_in \
  -CAfile cacert.pem -untrusted tsa.crt 2>/dev/null
openssl ts -reply -in timestamp.tsr -token_in -token_out -text 2>/dev/null \
  | grep -E "Policy|Hash Algorithm|Time stamp"
```

```text
Verification: OK
Policy OID: tsa_policy1
Hash Algorithm: sha256
Time stamp: Nov  1 08:35:30 2026 GMT
```

## Choosing the authority

- **freetsa.org** is free, allows commercial use and asks for at most one request per second per address. It runs on a best-effort basis with no service-level agreement: fine for trying this out, but use an authority with an agreement for production. Each signature makes one request.
- **Any RFC 3161 authority** works the same way: change the URL, and add its API key to `Headers` if it needs one. The fetcher refuses non-HTTP(S) addresses, applies a time limit and a size limit, and drops added headers when a redirect leaves the host.
- **An authority as a value.** The time-stamp function only turns request bytes into response bytes, so a recorded reply, an in-house authority or OneCrypto’s own `TimeStampAuthority` handler (used in [the LTV recipe](https://recursiveai.org/onecrypto/cookbook/pades-ltv-pdf-signature/)) plug in the same way.

## Limits

- **Not audited.** OneCrypto (cryptography, not cryptocurrency) has not been independently audited. It is a preview: 0.2.0-preview.1, and the API can change.
- **freetsa.org’s root as a trust anchor.** Its root certificate doesn’t mark basicConstraints critical, and by default OneCrypto’s path validator checks the form of an anchor certificate too, so it refuses this one. The program passes it as a name and key instead (`TrustAnchor.FromNameAndKey`), which is how RFC 5280 defines a trust anchor. The validator is a preview: “still a preview, so not yet recommended for trust decisions” ([Path validation](https://recursiveai.org/onecrypto/docs/validation/)).
- **A time-stamp is not long-term validation.** B-T proves when the signature existed. To keep it verifiable after the certificates expire, add the revocation data and a document time-stamp (B-LTA): [Sign a PDF with long-term validation](https://recursiveai.org/onecrypto/cookbook/pades-ltv-pdf-signature/).
- **Revocation stays unknown.** Nothing here fetches revocation data, so the chain check reports it as unknown, a warning, never as good.

How time-stamps are verified: [RFC 3161 time-stamps](https://recursiveai.org/onecrypto/docs/tsp/). The fetcher, its guards and the clients: [Online](https://recursiveai.org/onecrypto/docs/online/).
