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:
| Property | Type | Default | Description |
|---|---|---|---|
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
| Property | Type | Description |
|---|---|---|
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:
| Exception | Cause |
|---|---|
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
SignWithCertificateoverload 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.