Validate PDF digital signatures in C#
ValidateSignatures checks every digital signature of a PDF and returns one result each: whether the signed bytes are intact, whether the certificate chains to a trusted root, whether anything was added after signing, and a Status that sums it up.
Compiled and run: Linux ARM64 · Linux x64 · macOS ARM64 · Windows x64
Build it step by step.
-
1Install the NuGet packages
SelectPdf.Universalis the library. The native package carries the Chromium engine for one platform: add the one for the platform you run on.dotnet add package SelectPdf.Universal dotnet add package SelectPdf.Universal.Native.win-x64
dotnet add package SelectPdf.Universal dotnet add package SelectPdf.Universal.Native.linux-x64
dotnet add package SelectPdf.Universal dotnet add package SelectPdf.Universal.Native.linux-arm64
dotnet add package SelectPdf.Universal dotnet add package SelectPdf.Universal.Native.osx-arm64
-
2Create a console app
Any .NET 8 or .NET 10 project works. To follow along, create a console app, run the install commands above inside its folder, then replace
Program.cswith the code below.dotnet new console -n PdfSample cd PdfSample
-
3Import the namespaces
using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using SelectPdf.Universal; using SelectPdf.Universal.Drawing;
The
System.Security.Cryptographynamespaces only make the test certificates. Validation itself needsSelectPdf.Universal. -
4Make a test CA and a signer certificate
// 1. Test certificates: a certificate authority (CA) and a signer certificate it issued. // In production the signer's certificate comes from your CA or a public one. DateTimeOffset now = DateTimeOffset.UtcNow; using RSA caKey = RSA.Create(2048); var caRequest = new CertificateRequest("CN=Example Test CA", caKey, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); caRequest.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true)); using X509Certificate2 ca = caRequest.CreateSelfSigned(now.AddDays(-1), now.AddYears(5)); using RSA signerKey = RSA.Create(2048); var signerRequest = new CertificateRequest("CN=Jane Doe, O=Example Ltd", signerKey, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); using X509Certificate2 issued = signerRequest.Create(ca, now.AddDays(-1), now.AddYears(1), [1, 2, 3, 4]); using X509Certificate2 signer = issued.CopyWithPrivateKey(signerKey); File.WriteAllBytes("signer.pfx", signer.Export(X509ContentType.Pfx, "pfx-password")); // the CA certificate alone, without its private key: the root to trust using X509Certificate2 caCertificate = X509CertificateLoader.LoadCertificate(ca.RawData); File.WriteAllBytes("example-ca.pfx", caCertificate.Export(X509ContentType.Pfx, "ca-password"));To keep the sample self-contained, .NET creates a certificate authority and a certificate it issues to Jane Doe. The signer's certificate and key go to
signer.pfx; the CA's certificate alone, without its private key, goes toexample-ca.pfx. In production the PDFs you check are signed by others and the CA is your organization's or a public one. -
5Sign a PDF to check
// 2. A signed PDF to check. HtmlToPdf converter = new HtmlToPdf(); PdfDocument contract = converter.ConvertHtmlString("<h1>Service agreement</h1><p>Approved by:</p>"); contract.Save("contract.pdf"); contract.Close(); PdfSecurityManager signing = new PdfSecurityManager(); signing.Load("contract.pdf"); signing.Sign("signer.pfx", "pfx-password", "Signature1", "Approved", "London", "", 0, new RectangleF(40, 60, 260, 70)); signing.Save("contract-signed.pdf"); signing.Close();HtmlToPdfmakes a short contract andPdfSecurityManager.Signsigns it intocontract-signed.pdf, as in the signing guide. In your application this is the signed PDF you received. -
6Load the PDF and the roots to trust
// 3. Load the signed PDF and validate every signature, trusting the CA as a root. PdfSecurityManager manager = new PdfSecurityManager(); manager.Load("contract-signed.pdf"); using PdfDigitalCertificatesCollection roots = PdfDigitalCertificatesStore.GetCertificates("example-ca.pfx", "ca-password");Loadopens the signed PDF.PdfDigitalCertificatesStore.GetCertificatesreads the CA certificate. Certificates passed toValidateSignaturesare trusted as roots in addition to the system's trusted store. -
7Validate every signature
foreach (PdfSignatureValidationResult result in manager.ValidateSignatures(roots)) { Console.WriteLine($"{result.SignatureName}: {result.Status}"); Console.WriteLine($" signed by {result.SignerName} on {result.SigningTime}"); Console.WriteLine($" intact: {result.IsValid}, trusted: {result.IsTrusted}, " + $"covers the whole document: {result.CoversWholeDocument}"); foreach (string message in result.Messages) Console.WriteLine(" " + message); } manager.Close();ValidateSignaturesreturns onePdfSignatureValidationResultper signature.IsValidsays whether the signed bytes are intact,IsTrustedwhether the certificate chains to a trusted root,CoversWholeDocumentwhether anything was added after it, andStatussums them up.Messagesexplains any check that failed. Here the output isSignature1: Valid, signed by Jane Doe, intact, trusted, covering the whole document. -
8Run it
dotnet run
The program writes
contract-signed.pdfto the working folder. Without a license key, every page carries a trial watermark; setGlobalProperties.LicenseKeybefore the first conversion to remove it.
The complete Program.cs.
This is the exact file our samples pipeline compiled and ran on Linux ARM64 and Linux x64 and macOS ARM64 and Windows x64 to produce the output on this page.
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using SelectPdf.Universal;
using SelectPdf.Universal.Drawing;
// 1. Test certificates: a certificate authority (CA) and a signer certificate it issued.
// In production the signer's certificate comes from your CA or a public one.
DateTimeOffset now = DateTimeOffset.UtcNow;
using RSA caKey = RSA.Create(2048);
var caRequest = new CertificateRequest("CN=Example Test CA", caKey,
HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
caRequest.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true));
using X509Certificate2 ca = caRequest.CreateSelfSigned(now.AddDays(-1), now.AddYears(5));
using RSA signerKey = RSA.Create(2048);
var signerRequest = new CertificateRequest("CN=Jane Doe, O=Example Ltd", signerKey,
HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
using X509Certificate2 issued = signerRequest.Create(ca, now.AddDays(-1), now.AddYears(1), [1, 2, 3, 4]);
using X509Certificate2 signer = issued.CopyWithPrivateKey(signerKey);
File.WriteAllBytes("signer.pfx", signer.Export(X509ContentType.Pfx, "pfx-password"));
// the CA certificate alone, without its private key: the root to trust
using X509Certificate2 caCertificate = X509CertificateLoader.LoadCertificate(ca.RawData);
File.WriteAllBytes("example-ca.pfx", caCertificate.Export(X509ContentType.Pfx, "ca-password"));
// 2. A signed PDF to check.
HtmlToPdf converter = new HtmlToPdf();
PdfDocument contract = converter.ConvertHtmlString("<h1>Service agreement</h1><p>Approved by:</p>");
contract.Save("contract.pdf");
contract.Close();
PdfSecurityManager signing = new PdfSecurityManager();
signing.Load("contract.pdf");
signing.Sign("signer.pfx", "pfx-password", "Signature1", "Approved", "London", "",
0, new RectangleF(40, 60, 260, 70));
signing.Save("contract-signed.pdf");
signing.Close();
// 3. Load the signed PDF and validate every signature, trusting the CA as a root.
PdfSecurityManager manager = new PdfSecurityManager();
manager.Load("contract-signed.pdf");
using PdfDigitalCertificatesCollection roots =
PdfDigitalCertificatesStore.GetCertificates("example-ca.pfx", "ca-password");
foreach (PdfSignatureValidationResult result in manager.ValidateSignatures(roots))
{
Console.WriteLine($"{result.SignatureName}: {result.Status}");
Console.WriteLine($" signed by {result.SignerName} on {result.SigningTime}");
Console.WriteLine($" intact: {result.IsValid}, trusted: {result.IsTrusted}, " +
$"covers the whole document: {result.CoversWholeDocument}");
foreach (string message in result.Messages)
Console.WriteLine(" " + message);
}
manager.Close();
Open the project on GitHub → this program and the variations below, ready to dotnet run
The PDF it produces.
Download contract-signed.pdf ↓ the file this sample produced, unedited
Variations you will need next.
Each variation is a separate file in the same project, compiled and run with the program above.
Detect a file that was changed after signing
Changing any byte the signature covers, here the location recorded with it, breaks the signature: Status is Invalid, IsValid is false, and the message says the document was altered or corrupted since the signature was applied.
using System.Text;
using SelectPdf.Universal;
static class TamperedCopy
{
public static void Run()
{
// change bytes the signature covers: the location recorded with it
byte[] bytes = File.ReadAllBytes("contract-signed.pdf");
byte[] from = Encoding.ASCII.GetBytes("(London)");
byte[] to = Encoding.ASCII.GetBytes("(Paris!)");
int at = bytes.AsSpan().IndexOf(from);
to.CopyTo(bytes, at);
File.WriteAllBytes("contract-tampered.pdf", bytes);
PdfSecurityManager manager = new PdfSecurityManager();
manager.Load("contract-tampered.pdf");
using PdfDigitalCertificatesCollection roots =
PdfDigitalCertificatesStore.GetCertificates("example-ca.pfx", "ca-password");
foreach (PdfSignatureValidationResult result in manager.ValidateSignatures(roots))
{
Console.WriteLine($"{result.SignatureName}: {result.Status}, intact: {result.IsValid}");
foreach (string message in result.Messages)
Console.WriteLine(" " + message);
}
manager.Close();
}
}
Two signatures, one after the other
After a second signature, the first is still intact but no longer covers the whole document: its Status is ValidWithWarnings with the message that content was added after it. The second signature, the last one, is Valid.
using SelectPdf.Universal;
using SelectPdf.Universal.Drawing;
static class Countersigned
{
public static void Run()
{
PdfSecurityManager signing = new PdfSecurityManager();
signing.Load("contract-signed.pdf");
signing.Sign("signer.pfx", "pfx-password", "Signature2", "Reviewed", "London", "",
0, new RectangleF(320, 60, 260, 70));
signing.Save("contract-countersigned.pdf");
signing.Close();
PdfSecurityManager manager = new PdfSecurityManager();
manager.Load("contract-countersigned.pdf");
using PdfDigitalCertificatesCollection roots =
PdfDigitalCertificatesStore.GetCertificates("example-ca.pfx", "ca-password");
foreach (PdfSignatureValidationResult result in manager.ValidateSignatures(roots))
{
Console.WriteLine($"{result.SignatureName}: {result.Status}, covers the whole document: {result.CoversWholeDocument}");
foreach (string message in result.Messages)
Console.WriteLine(" " + message);
}
manager.Close();
}
}
Check whether the certificate was revoked
RevocationCheckEnabled = true asks the certificate's issuer, over OCSP or a revocation list, whether it was revoked. The test certificate names no revocation service, so RevocationStatus is Unknown and Status is Indeterminate. A certificate from a real CA names its services; an LTV-enabled signature carries the answer inside the PDF.
using SelectPdf.Universal;
static class RevocationCheck
{
public static void Run()
{
// ask the certificate's issuer whether it was revoked (OCSP or CRL, over the network)
PdfSecurityManager manager = new PdfSecurityManager();
manager.Load("contract-signed.pdf");
manager.RevocationCheckEnabled = true;
using PdfDigitalCertificatesCollection roots =
PdfDigitalCertificatesStore.GetCertificates("example-ca.pfx", "ca-password");
foreach (PdfSignatureValidationResult result in manager.ValidateSignatures(roots))
{
Console.WriteLine($"{result.SignatureName}: {result.Status}, revocation: {result.RevocationStatus}");
foreach (string message in result.Messages)
Console.WriteLine(" " + message);
}
manager.Close();
}
}
Common problems and fixes.
ValidWithWarnings: the certificate does not chain to a trusted root
Without the CA passed as a root, the same signature is ValidWithWarnings with the message "The signer's certificate does not chain to a trusted root." Pass your CA to ValidateSignatures(roots) or install it in the system's trusted store.
An empty result
ValidateSignatures returns an empty array when the document has no signatures.
The revocation check is slow or Unknown
It contacts the services the certificate names, over the network, and takes as long as they take to answer. With no network, or no service named, RevocationStatus is Unknown. Signatures made with EnableLtv carry the revocation data and are checked without going online.
The PDF is password-protected
Load it with Load(fileName, password); PdfSecurityManager also loads from a stream or a byte array.
Good to know.
- ValidateSignatures checks every signature of a loaded document and returns a PdfSignatureValidationResult for each. Source
- ValidateSignatures(roots) trusts the given certificates as roots, for example your organization's own certificate authority, in addition to the system's trusted store. Source
- Status is Valid, ValidWithWarnings, Indeterminate or Invalid; Invalid means the signed content was changed, the signature is damaged or the certificate is revoked. Source
- Each signature is checked against the bytes it was made over, so a signature followed by a later one, or by a form fill, is reported intact. Source
- RevocationCheckEnabled asks the issuer of each signer's certificate, through OCSP or a certificate revocation list, whether it was revoked; it is off by default and needs network access. Source
Questions, answered.
Short answers about this guide. The SelectPdf.Universal documentation covers every class and option.