Digital Signing

Sign PDF documents with PFX certificates or X509Certificate2 objects, and verify existing signatures programmatically. FolioPDF produces PAdES-B-B compliant signatures using SHA-256 and the adbe.pkcs7.detached format.

Overview

FolioPDF's signing pipeline is powered by a custom PDFium + OpenSSL orchestrator that handles the entire process natively: PFX parsing, signature field placement, byte-range computation, PKCS#7 detached signature generation, and /Contents patching. Everything runs in-memory with no temporary files.

The signature uses /SubFilter adbe.pkcs7.detached — the widely supported Adobe PDF signature format understood by Acrobat, Chrome, Foxit, and pdf.js. SHA-256 is the digest algorithm.

PAdES conformance level: FolioPDF currently produces PAdES-B-B (Basic) signatures. This means a CMS signature with no embedded timestamp, no OCSP/CRL validation data, and no archive timestamps. PAdES-B-T (timestamps), B-LT (long-term validation), and B-LTA (archive) are planned for a future release.

Sign with PFX File

The simplest way to sign a PDF is with a PFX (PKCS#12) file containing the certificate and private key:

using FolioPDF.Toolkit.Pdfium;

// Sign in-memory bytes
byte[] pdfBytes = File.ReadAllBytes("contract.pdf");
byte[] pfxBytes = File.ReadAllBytes("signer.pfx");

byte[] signed = PdfSigner.SignWithPfx(pdfBytes, pfxBytes, "pfx-password");
File.WriteAllBytes("contract-signed.pdf", signed);

File-Based Convenience

For file-to-file workflows, use SignFileWithPfx to skip the manual byte reading:

PdfSigner.SignFileWithPfx(
    inputPath:   "contract.pdf",
    outputPath:  "contract-signed.pdf",
    pfxPath:     "signer.pfx",
    pfxPassword: "pfx-password");

Sign with X509Certificate2

When you already hold a certificate in memory — loaded from a certificate store, created with CertificateRequest.CreateSelfSigned, or returned from a cloud KMS wrapper — use the SignWithCertificate overload:

using System.Security.Cryptography.X509Certificates;
using FolioPDF.Toolkit.Pdfium;

// Load from a PFX file
var cert = new X509Certificate2("signer.pfx", "pfx-password",
    X509KeyStorageFlags.Exportable);

byte[] pdfBytes = File.ReadAllBytes("invoice.pdf");
byte[] signed = PdfSigner.SignWithCertificate(pdfBytes, cert);
File.WriteAllBytes("invoice-signed.pdf", signed);

Self-Signed Certificate (Testing)

For development and testing, generate a self-signed certificate on the fly:

using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using FolioPDF.Toolkit.Pdfium;

// Create a self-signed test certificate
using var rsa = RSA.Create(2048);
var request = new CertificateRequest(
    "CN=Test Signer, O=Acme Corp", rsa,
    HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);

using var cert = request.CreateSelfSigned(
    DateTimeOffset.Now, DateTimeOffset.Now.AddYears(1));

byte[] pdfBytes = File.ReadAllBytes("report.pdf");
byte[] signed = PdfSigner.SignWithCertificate(pdfBytes, cert,
    new PdfSigningOptions
    {
        Reason = "I approve this document",
        SignerName = "Jane Smith"
    });

File.WriteAllBytes("report-signed.pdf", signed);

File-Based Certificate Signing

var cert = new X509Certificate2("signer.pfx", "password",
    X509KeyStorageFlags.Exportable);

PdfSigner.SignFileWithCertificate(
    inputPath:   "contract.pdf",
    outputPath:  "contract-signed.pdf",
    certificate: cert,
    options: new PdfSigningOptions { Reason = "Final approval" });

Signing Options

The PdfSigningOptions class controls signature placement and metadata:

PropertyTypeDefaultDescription
PageIndex int 0 Zero-based page index for the visible signature widget. The signature covers the entire document — this only controls where the widget annotation is placed.
Reason string? null Human-readable reason stored in the /Reason key (e.g. "I approve this document"). Displayed in Acrobat's signature properties panel.
SignerName string? null Display name stored in the /Name key. When null, viewers fall back to the certificate's subject DN.
ContentsPlaceholderSize int 0 (native default: 16 KB) Size of the /Contents hex placeholder in bytes. The default of 16,384 bytes is sufficient for RSA-2048 + SHA-256 with a typical certificate chain. Increase for RSA-4096 or long intermediate chains.

Example with All Options

var options = new PdfSigningOptions
{
    PageIndex = 2,                      // signature widget on page 3
    Reason = "Approved for release",
    SignerName = "Dr. Sarah Chen",
    ContentsPlaceholderSize = 32768     // 32 KB for RSA-4096
};

byte[] signed = PdfSigner.SignWithPfx(pdfBytes, pfxBytes, "password", options);

Verify Signatures

FolioPDF can read and cryptographically verify existing signatures in any PDF. Verification uses OpenSSL's PKCS7_verify under the hood — no managed NuGet dependencies required.

Read Signature Metadata

using FolioPDF.Toolkit.Pdfium;

using var doc = PdfiumDocument.Load(File.ReadAllBytes("signed-contract.pdf"));

// Check if the document has signatures
Console.WriteLine($"Signature count: {doc.SignatureCount}");
Console.WriteLine($"Has signatures: {doc.HasSignatures}");

// Enumerate all signatures
foreach (var sig in doc.GetSignatures())
{
    Console.WriteLine($"SubFilter:    {sig.SubFilter}");
    Console.WriteLine($"Reason:       {sig.Reason}");
    Console.WriteLine($"Signing time: {sig.SigningTime}");
    Console.WriteLine($"ByteRange:    [{sig.ByteRangeStart1}, {sig.ByteRangeLength1}, " +
                      $"{sig.ByteRangeStart2}, {sig.ByteRangeLength2}]");
    Console.WriteLine();
}

Cryptographic Verification

byte[] pdfBytes = File.ReadAllBytes("signed-contract.pdf");
using var doc = PdfiumDocument.Load(pdfBytes);

foreach (var sig in doc.GetSignatures())
{
    var result = PdfiumSignatureVerifier.Verify(sig, pdfBytes);

    Console.WriteLine($"Valid:                {result.IsValid}");
    Console.WriteLine($"Covers entire doc:    {result.CoversEntireDocument}");
    Console.WriteLine($"Signing time:         {result.SigningTime}");

    if (result.SignerCertificate is { } cert)
    {
        Console.WriteLine($"Signer:               {cert.Subject}");
        Console.WriteLine($"Issuer:               {cert.Issuer}");
        Console.WriteLine($"Serial:               {cert.SerialNumber}");
        Console.WriteLine($"Valid from:           {cert.NotBefore}");
        Console.WriteLine($"Valid until:          {cert.NotAfter}");
    }

    if (!result.IsValid)
    {
        Console.WriteLine($"Failure reason:       {result.FailureReason}");
    }
}

Verification Result Properties

PropertyTypeDescription
IsValid bool True when the PKCS#7 signature cryptographically validates against the covered bytes. False on any failure (tampering, wrong key, malformed PKCS#7, missing ByteRange).
SignerCertificate X509Certificate2? The certificate used to sign, parsed from the PKCS#7 SignerInfo. Null if the blob could not be decoded.
CoversEntireDocument bool Whether the /ByteRange covers the entire document body. False indicates a partial-document or incremental-update signature.
SigningTime string? Raw PDF date string from the /M entry (e.g. D:20260407103000+02'00'). Returned verbatim because real-world PDFs sometimes have malformed dates.
FailureReason string Human-readable explanation when IsValid is false. Empty string on success.

Sign and Verify Round Trip

A complete example that signs a generated PDF and immediately verifies the result:

using FolioPDF.Fluent;
using FolioPDF.Helpers;
using FolioPDF.Toolkit.Pdfium;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

// 1. Generate a PDF
byte[] pdfBytes = Document.Create(doc =>
{
    doc.Page(page =>
    {
        page.Size(PageSizes.A4);
        page.Margin(50);
        page.Content().Text("Purchase Order #12345").FontSize(20).Bold();
    });
}).GeneratePdf();

// 2. Create a test certificate
using var rsa = RSA.Create(2048);
var req = new CertificateRequest("CN=Finance Dept", rsa,
    HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
using var cert = req.CreateSelfSigned(
    DateTimeOffset.Now, DateTimeOffset.Now.AddYears(1));

// 3. Sign the PDF
byte[] signedPdf = PdfSigner.SignWithCertificate(pdfBytes, cert,
    new PdfSigningOptions { Reason = "Purchase approved" });

// 4. Verify the signature
using var doc2 = PdfiumDocument.Load(signedPdf);
var sigs = doc2.GetSignatures();
Console.WriteLine($"Found {sigs.Count} signature(s)");

foreach (var sig in sigs)
{
    var result = PdfiumSignatureVerifier.Verify(sig, signedPdf);
    Console.WriteLine($"Valid: {result.IsValid}, Signer: {result.SignerCertificate?.Subject}");
}

Trust Chain Validation

FolioPDF's verifier intentionally does not enforce trust chain validation against the OS root store. This is by design — many workflows use self-signed certificates for dev/test signing. If you need trust enforcement, build an X509Chain against the returned certificate:

var result = PdfiumSignatureVerifier.Verify(sig, pdfBytes);

if (result.IsValid && result.SignerCertificate is { } signerCert)
{
    using var chain = new X509Chain();
    chain.ChainPolicy.RevocationMode = X509RevocationMode.Online;
    chain.ChainPolicy.RevocationFlag = X509RevocationFlag.EntireChain;

    bool trusted = chain.Build(signerCert);
    if (!trusted)
    {
        foreach (var status in chain.ChainStatus)
            Console.WriteLine($"Chain error: {status.StatusInformation}");
    }
}

Error Handling

Signing operations throw clear, actionable exceptions:

ExceptionCause
ArgumentNullException Null input PDF, PFX data, or certificate
ArgumentException Empty input PDF, empty PFX data, or certificate without a private key
FileNotFoundException Input PDF or PFX file path does not exist (file-based overloads)
NotSupportedException Certificate private key is non-exportable (HSM, smartcard, or cert store without Exportable flag)
PdfiumException Native pipeline failure: corrupt PFX, wrong password, invalid PDF, signature too large for placeholder

Limitations

  • HSM/smartcard keys are not yet supported. The SignWithCertificate overload exports the certificate to PFX internally, which requires an exportable private key. A split signing API for HSM-backed keys is planned.
  • No timestamp server (TSA) support yet. Signatures are PAdES-B-B only. PAdES-B-T with RFC 3161 timestamps is planned.
  • No OCSP/CRL embedding for long-term validation (PAdES-B-LT/B-LTA).
  • Visible signature appearance is a basic widget. Custom visual appearances (logos, handwriting images) are not yet supported.
  • Multiple signatures require incremental saves, which are planned but not yet available.

Thread Safety

All PdfSigner and PdfiumSignatureVerifier methods route through PDFium's process-global lock. Calls from multiple .NET threads are safe — they serialize internally. No external synchronization is needed.