---
title: "Digitally sign a PDF in C#"
url: https://recursiveai.org/onepdf/articles/sign-pdf-csharp/
description: "Sign a PDF in C# with a .pfx certificate: PAdES B-B and B-T, visible or certification signatures, HSM and cloud keys, and verification that catches changes."
date: 2026-10-26
author: OnePDF team
---

# Digitally sign a PDF in C#

Sign a PDF in C# with a .pfx certificate: PAdES B-B and B-T, visible or certification signatures, HSM and cloud keys, and verification that catches changes.

October 26, 2026 · OnePDF team

To digitally sign a PDF in C#, open it with OnePDF for editing, load your certificate and call `doc.Sign`. The result is a PAdES baseline signature (B-B), written as an incremental update, so the original bytes stay untouched and earlier signatures stay valid. Add a time-stamp callback and it becomes PAdES B-T. The same library verifies signatures, against trust anchors you choose, without touching the network, and reports exactly what changed after signing. It uses .NET’s own cryptography, with no BouncyCastle and no native binaries, and is licensed MIT OR Apache-2.0. Every sample was compiled and run against OnePDF 1.11.0 on .NET 10, and each signed file was checked with poppler’s `pdfsig`.

## Before you start

*Terminal · NuGet*

```
dotnet new console -o SignPdf
cd SignPdf
dotnet add package RecursiveAI.OnePdf --version 1.11.0
```

OnePDF targets .NET 10. The samples sign `contract.pdf`, a one-page agreement, but any PDF works. You need a certificate with its private key, usually a `.pfx` (PKCS #12) file from your certificate authority. To follow along without one, make a test pair. This writes a root certificate and a signer certificate it issued, with the key usage a document-signing certificate needs:

*MakeCerts.cs · .NET 10*

```
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
 
// Test certificates only: a root and a signer it issued. Use your real certificate in production.
using RSA rootKey = RSA.Create(3072);
var rootRequest = new CertificateRequest("CN=Example Test Root", rootKey, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
rootRequest.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true));
rootRequest.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.KeyCertSign | X509KeyUsageFlags.CrlSign, true));
using X509Certificate2 root = rootRequest.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddYears(5));
 
using RSA signerKey = RSA.Create(3072);
var signerRequest = new CertificateRequest("CN=Jane Doe, O=Example Co.", signerKey, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
signerRequest.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.NonRepudiation, true));
using X509Certificate2 issued = signerRequest.Create(root, DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddYears(1), [1, 2, 3, 4]);
using X509Certificate2 signer = issued.CopyWithPrivateKey(signerKey);
 
File.WriteAllBytes("root.cer", root.Export(X509ContentType.Cert));
File.WriteAllBytes("signer.pfx", signer.Export(X509ContentType.Pkcs12, "test"));
Console.WriteLine($"signer: {signer.Subject}, issued by {signer.Issuer}");
```

*Output · Console*

```
signer: CN=Jane Doe, O=Example Co., issued by CN=Example Test Root
```

A PDF viewer won’t trust these, because it doesn’t know the root. That’s expected for a test pair: the verification below names the root as trusted explicitly.

## Sign a PDF with a .pfx certificate

*Program.cs · OnePDF 1.11.0 · .NET 10*

```
using System.Security.Cryptography.X509Certificates;
using OnePdf;
 
using X509Certificate2 signer = X509CertificateLoader.LoadPkcs12FromFile("signer.pfx", "test");
 
using (PdfDocument doc = PdfDocument.Open("contract.pdf", new PdfOpenOptions { AllowEditing = true }))
{
    PdfSigningResult result = doc.Sign("contract-signed.pdf", PdfSigningCredential.FromCertificate(signer), new PdfSigningOptions
    {
        FieldName = "Approval",
        Reason = "I approve this contract",
        Location = "Springfield",
    });
    Console.WriteLine($"signed {result.FieldName}: {result.SignedLength:N0} bytes covered");
}
 
// Verify against the root you trust. Nothing is downloaded.
using X509Certificate2 root = X509CertificateLoader.LoadCertificateFromFile("root.cer");
using PdfDocument signed = PdfDocument.Open("contract-signed.pdf");
PdfSignatureReport report = signed.VerifySignatures(new PdfSignatureVerifyOptions { TrustAnchors = [root] });
foreach (PdfSignatureVerification v in report.Signatures)
    Console.WriteLine($"{v.Signature.FieldName}: valid={v.IsValid}, chain={v.Chain}, coverage={v.Coverage}, format={v.Signature.SubFilter}");
```

*Output · Console*

```
signed Approval: 22,277 bytes covered
Approval: valid=True, chain=Trusted, coverage=WholeDocument, format=ETSI.CAdES.detached
```

`pdfsig contract-signed.pdf` reports the same: signer “Jane Doe”, type `ETSI.CAdES.detached`, “Total document signed” and “Signature is Valid.”

- **The document must be opened with `AllowEditing`.** Changes you make before `Sign`, such as a filled form field, are part of what gets signed.
- **`Sign` writes nothing unless the whole signature succeeded.** It takes a path or a `Stream`, so you can sign straight into a `MemoryStream` in a web app.
- **`FieldName`** names an existing unsigned signature field, or a new field to create. Without `Rectangle` the signature is invisible.
- **The format is PAdES baseline B-B** (`ETSI.CAdES.detached`) by default. `Format = PdfSignatureFormat.Pkcs7Detached` writes the older `adbe.pkcs7.detached` for consumers that need it.
- **Keys:** RSA (PKCS #1 v1.5 and PSS) and ECDSA, with SHA-256, SHA-384 or SHA-512.

## Add a visible signature and certify the document

A visible signature needs a page and a rectangle, in points from the lower left corner. By default the box shows “Digitally signed by”, the signer’s name, the date and the reason. An image, such as a logo or a scanned handwritten signature, goes on the left. A certification signature also states which changes are allowed afterwards:

*Program.cs · OnePDF 1.11.0 · .NET 10*

```
using System.Security.Cryptography.X509Certificates;
using OnePdf;
 
using X509Certificate2 signer = X509CertificateLoader.LoadPkcs12FromFile("signer.pfx", "test");
using PdfDocument doc = PdfDocument.Open("contract.pdf", new PdfOpenOptions { AllowEditing = true });
 
PdfCreatedImage logo = doc.AddPngImage(File.ReadAllBytes("logo.png"));
doc.Sign("contract-certified.pdf", PdfSigningCredential.FromCertificate(signer), new PdfSigningOptions
{
    FieldName = "Author",
    Reason = "Final version",
    PageIndex = 0,
    Rectangle = new PdfRectangle(54, 54, 294, 114),          // left, bottom, right, top in points
    Appearance = new PdfSignatureAppearance { Image = logo, Border = true },
    Certify = PdfCertificationLevel.FormFilling,             // later: form filling and more signatures only
});
Console.WriteLine("certified with a visible signature on page 1");
```

*Output · Console*

```
certified with a visible signature on page 1
```

The bottom of page 1 of `contract-certified.pdf`, rendered with poppler:

The three certification levels are `NoChanges`, `FormFilling` (fill fields and add signatures) and `FormFillingAndAnnotations`. Only the first signature of a document can certify it. Set `Appearance.Lines` to show your own text instead of the default lines.

## Add a trusted time-stamp (PAdES B-T)

The signing time in a B-B signature is the signer’s own claim. A time-stamp from a time-stamping authority (TSA) proves the signature existed at that moment. OnePDF never opens a network connection itself. You supply a callback that posts its RFC 3161 request to your TSA and returns the response. Here it uses DigiCert’s public time-stamp server:

*Program.cs · OnePDF 1.11.0 · .NET 10*

```
using System.Net.Http.Headers;
using System.Security.Cryptography.X509Certificates;
using OnePdf;
 
using var http = new HttpClient();
using X509Certificate2 signer = X509CertificateLoader.LoadPkcs12FromFile("signer.pfx", "test");
using PdfDocument doc = PdfDocument.Open("contract.pdf", new PdfOpenOptions { AllowEditing = true });
 
doc.Sign("contract-timestamped.pdf", PdfSigningCredential.FromCertificate(signer), new PdfSigningOptions
{
    FieldName = "Approval",
    Reason = "I approve this contract",
    Timestamp = request =>                                    // makes the signature PAdES B-T
    {
        var body = new ByteArrayContent(request.RequestDer.ToArray());
        body.Headers.ContentType = new MediaTypeHeaderValue("application/timestamp-query");
        using HttpResponseMessage response = http.Send(new HttpRequestMessage(HttpMethod.Post, "http://timestamp.digicert.com") { Content = body });
        response.EnsureSuccessStatusCode();
        return response.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult();
    },
});
 
using X509Certificate2 root = X509CertificateLoader.LoadCertificateFromFile("root.cer");
using PdfDocument signed = PdfDocument.Open("contract-timestamped.pdf");
var options = new PdfSignatureVerifyOptions { TrustAnchors = [root], UseSystemTrustStore = true };   // the system store holds the TSA's root
foreach (PdfSignatureVerification v in signed.VerifySignatures(options).Signatures)
    Console.WriteLine($"{v.Signature.FieldName}: valid={v.IsValid}, timestamp valid={v.Timestamp?.IsValid}, time={v.Timestamp?.Time:u}");
```

*Output · Console*

```
Approval: valid=True, timestamp valid=True, time=2026-10-27 04:45:38Z
```

OnePDF checks the response before embedding it: the status, that the imprint matches what it sent, and the nonce. The token goes into the signature as the `signatureTimeStampToken` attribute, which makes it PAdES B-T. Verification checks the authority’s signature and its chain, here through the system trust store, which holds DigiCert’s root. To time-stamp the document as a whole, for example after several signatures, use `AddDocumentTimestamp`.

## Sign with a key in an HSM, a smart card or a cloud key service

Production signing keys often can’t be exported. `PdfSigningCredential.FromExternalSigner` takes the public certificate and a function that signs a digest. OnePDF builds everything else, hands your function the digest and the algorithm, and checks the signature value you return against the certificate before embedding it:

*Program.cs · OnePDF 1.11.0 · .NET 10*

```
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using OnePdf;
 
// The certificate is public. The key stays in your HSM or cloud key service; here a local RSA key stands in for it.
using X509Certificate2 withKey = X509CertificateLoader.LoadPkcs12FromFile("signer.pfx", "test");
using X509Certificate2 certificate = X509CertificateLoader.LoadCertificate(withKey.RawData);   // no private key
using RSA remoteKey = withKey.GetRSAPrivateKey()!;
 
PdfExternalSigner signWithRemoteKey = request =>
{
    // Send request.Hash to the key service and return the raw signature. It is already a digest: don't hash it again.
    RSASignaturePadding padding = request.RsaPadding == PdfRsaPadding.Pss ? RSASignaturePadding.Pss : RSASignaturePadding.Pkcs1;
    return remoteKey.SignHash(request.Hash.ToArray(), request.HashAlgorithm, padding);
};
 
using PdfDocument doc = PdfDocument.Open("contract.pdf", new PdfOpenOptions { AllowEditing = true });
PdfSigningResult result = doc.Sign("contract-hsm.pdf",
    PdfSigningCredential.FromExternalSigner(certificate, signWithRemoteKey),
    new PdfSigningOptions { FieldName = "Approval" });
Console.WriteLine($"signed {result.FieldName} with an external key");
```

*Output · Console*

```
signed Approval with an external key
```

In this sample a local RSA key stands in for the remote one, so it runs anywhere. In a real system, the body of `signWithRemoteKey` calls your key service’s “sign digest” operation. The digest is final, so the service must not hash it again. For ECDSA keys, return the signature as DER or IEEE P1363. Both are accepted.

## Verify signatures and detect changes

The first sample verified a signature. Verification checks the byte range, the digest, the signature value, the certificate chain at the validation time, any time-stamp, and every change made after the signature. Here someone stamps “PAID” on the signed contract and saves it as an incremental update, so the original signed bytes are still in the file:

*Program.cs · OnePDF 1.11.0 · .NET 10*

```
using System.Security.Cryptography.X509Certificates;
using OnePdf;
 
// Change a signed file: stamp page 1 and save the change as an incremental update.
using (PdfDocument doc = PdfDocument.Open("contract-signed.pdf", new PdfOpenOptions { AllowEditing = true }))
{
    doc.AddTextStamp("PAID", doc.AddStandardFont("Helvetica"), 48, pageIndexes: [0]);
    using FileStream output = File.Create("contract-changed.pdf");
    doc.SaveIncremental(output);
}
 
using X509Certificate2 root = X509CertificateLoader.LoadCertificateFromFile("root.cer");
using PdfDocument changed = PdfDocument.Open("contract-changed.pdf");
foreach (PdfSignatureVerification v in changed.VerifySignatures(new PdfSignatureVerifyOptions { TrustAnchors = [root] }).Signatures)
{
    Console.WriteLine($"{v.Signature.FieldName}: valid={v.IsValid}, integrity={v.Integrity}, coverage={v.Coverage}");
    foreach (var group in v.Changes.Where(c => !c.Allowed).GroupBy(c => c.Category))
        Console.WriteLine($"  not allowed after signing: {group.Count()} × {group.Key}");
}
```

*Output · Console*

```
Approval: valid=False, integrity=Valid, coverage=EarlierRevisionDisallowedChanges
  not allowed after signing: 6 × PageContent
```

The signature itself is still mathematically intact (`integrity=Valid`), but page content changed after it, so the document as it now reads is not what was signed, and `IsValid` is false. `pdfsig` agrees: it still calls the signature valid but no longer reports “Total document signed”. Filling a form field after signing is allowed by default, and certification levels set what else is. Each change comes with its object number, a category and the reason it is or isn’t allowed.

Verification is built to fail safe:

- **Nothing is fetched.** No certificate downloads and no OCSP or CRL requests. A chain is trusted only when it leads to one of your `TrustAnchors`, or to the system store with `UseSystemTrustStore`.
- **Revocation** is reported as not checked unless you pass a `RevocationCheck` function that answers from CRLs or OCSP responses you already hold.
- **The published attacks** on PDF signature validation, such as universal signature forgery, incremental saving, signature wrapping and shadow attacks, each have a crafted test file. The [signatures docs](https://recursiveai.org/onepdf/docs/signatures/) list which are detected and which are allowed by the document’s own permission level and reported as advisory.

## What OnePDF doesn’t do: B-LT and B-LTA

OnePDF 1.11.0 writes PAdES B-B and B-T, and document time-stamps. It doesn’t write long-term validation data: the `/DSS` dictionary with certificates, CRLs and OCSP responses that PAdES B-LT adds, and the archive time-stamp of B-LTA. If your signatures must stay verifiable after the signing certificate expires or its CA goes offline, you need a library that writes those levels. iText, for example, documents `PdfPadesSigner` with methods for all four baseline levels ([iText knowledge base](https://kb.itextpdf.com/itext/pades-signing-high-level-api)). It is AGPL-3.0 or commercial ([iText license](https://recursiveai.org/onepdf/articles/itext-license/)), and its signing needs one of its two BouncyCastle adapter packages ([Bouncy Castle changes](https://kb.itextpdf.com/itext/bouncy-castle-changes)).

Also not supported: public-key (certificate) encryption, several signers in one CMS container, and Ed25519 or Ed448 keys.

## Signing in the right order

- **Fill, flatten or convert first, sign last.** Every later change is checked against the signature. A PDF/A save rewrites the whole file and drops existing signatures, so save as PDF/A first and sign afterwards ([create PDF/A](https://recursiveai.org/onepdf/articles/create-pdf-a-csharp/)). Flattening a form also rewrites pages ([flatten a PDF form](https://recursiveai.org/onepdf/articles/flatten-pdf-csharp/)).
- **Several signatures:** sign, open the output with `AllowEditing`, and sign again. Each signature is its own incremental update.
- **Space:** OnePDF reserves room for the signature from the certificates, the key size and any time-stamp. If a large chain doesn’t fit, the error carries the size needed. Set `ContentsReserve` and sign again.

## Where next

- [Sign a PDF and verify its signatures](https://recursiveai.org/onepdf/cookbook/sign-and-verify/), the short cookbook recipe.
- [Signatures](https://recursiveai.org/onepdf/docs/signatures/): every check, the change rules, attack handling and signing options.
- [Fill PDF form fields in C#](https://recursiveai.org/onepdf/articles/fill-pdf-form-csharp/), the usual step before signing.
- [Choosing a C# PDF library](https://recursiveai.org/onepdf/articles/csharp-pdf-library/) for licenses and features compared.
