Digital signatures
Changed in 1.20.0: Certification signatures, Verifying signatures.
Sign the PDF with a certificate (a .pfx file or one from the certificate store). Adobe Reader then shows who signed
it and whether the file was changed afterwards. The key can also stay in an HSM, smart card or cloud key vault (external
signers), and the signatures of any PDF can be
verified and made verifiable for years.
var cert = X509CertificateLoader.LoadPkcs12FromFile("signer.pfx", "pfx-password");
var options = new PdfOptions
{
Signature = new PdfSignatureOptions(cert)
{
Reason = "Approved",
Location = "Copenhagen",
// Optional: a visible box (points from the top-left corner; Page = -1 is the last page).
Appearance = new PdfSignatureAppearance { Page = -1, X = 340, Y = 720, Width = 220, Height = 70 },
// Optional: a trusted RFC 3161 timestamp (PAdES B-T). The only option that uses the network.
TimestampUrl = "http://timestamp.digicert.com",
},
};
// Or sign any existing PDF; earlier signatures stay valid.
byte[] signed = PdfSigner.Sign(File.ReadAllBytes("in.pdf"), new PdfSignatureOptions(cert));
// A contract template with empty signature fields: sign into one by name, at its place on the page.
byte[] countersigned = PdfSigner.Sign(contract, new PdfSignatureOptions(cert) { FieldName = "Buyer" });
Signing into an existing field
When FieldName names an empty signature field the PDF already has ("Parties.Buyer" for a nested one), the signature
goes into it and the box fills the field's rectangle; signing a field of another type or one that is already signed
throws PdfSignatureException. Any other name adds a new field.
Formats and compatibility
Signatures are PAdES baseline (ETSI.CAdES.detached) by default, or classic adbe.pkcs7.detached with
Format = PdfSignatureFormat.Pkcs7Detached; RSA and ECDSA keys with SHA-256/384/512. The signature is appended as an
incremental update, and it works together with Security (encrypted files) and Conformance (PDF/A: the visible box is
drawn as outlines, so it needs no unembedded font). The CMS structure is built with .NET's own System.Formats.Asn1 and
System.Security.Cryptography; no extra package is needed. Signing adds a few milliseconds (the RSA or ECDSA operation
and re-reading the file); conversions without a signature are unaffected.
External signers: HSMs, smart cards and key vaults
When the private key cannot leave its hardware security module, smart card, PKCS#11 token or cloud key vault, pass a
PdfExternalSigner with the certificate. PdfPecker builds the PDF and the CMS structure, and asks your callback only
for the raw signature value. The certificate needs only its public part. The callback gets a PdfSignatureRequest with
Digest for "sign hash" APIs (Azure Key Vault SignAsync, AWS KMS Sign with MessageType.DIGEST,
RSA.SignHash) and Data for APIs that hash themselves; both give the same signature.
// The certificate without its private key; the key stays in Azure Key Vault.
var cert = X509CertificateLoader.LoadCertificateFromFile("signer.cer");
var keyVault = new CryptographyClient(keyId, new DefaultAzureCredential());
var signer = new PdfExternalSigner(PdfSignatureAlgorithm.RsaPkcs1, async (request, ct) =>
(await keyVault.SignAsync(SignatureAlgorithm.RS256, request.Digest.ToArray(), ct)).Signature);
byte[] signed = await PdfSigner.SignAsync(pdf, new PdfSignatureOptions(cert, signer) { Reason = "Approved" });
// A synchronous callback, here with a key .NET can use directly (such as one from a PKCS#11 wrapper).
var local = new PdfExternalSigner(PdfSignatureAlgorithm.RsaPss,
request => rsa.SignHash(request.Digest.ToArray(), request.HashAlgorithm, RSASignaturePadding.Pss));
byte[] signed2 = PdfSigner.Sign(pdf, new PdfSignatureOptions(cert, local));
PdfSignatureAlgorithm is RsaPkcs1 (RS256 and the like), RsaPss (PS256, with a salt as long as the hash) or
Ecdsa (ES256, DER-encoded or raw r||s); the hash is PdfSignatureOptions.HashAlgorithm, SHA-256 by default. The
returned signature is checked against the certificate before it goes into the PDF, so a wrong key or padding throws
PdfSignatureException instead of producing a broken file. The callback is normally called once per signature (twice
in the rare case the signature does not fit the space reserved for it). PdfSigner.SignAsync awaits an asynchronous
callback and the timestamp server instead of blocking a thread; PdfSigner.Sign and signing during HtmlToPdf.Convert
with PdfOptions.Signature wait for it. Everything else, visible boxes, timestamps and existing fields, works as with a
certificate's own key.
Certification signatures New in 1.20.0
The first signature of a document can certify it: it states what may change afterwards, and Acrobat shows a certified
document with a blue ribbon and flags changes the level does not allow. PdfCertificationLevel.NoChanges allows none,
not even another signature; FormFilling allows filling in fields and signing; FormFillingAndAnnotations also allows
adding, changing and deleting comments.
// The issuer certifies the contract; the parties may still fill in its fields and sign.
byte[] certified = PdfSigner.Sign(contract, new PdfSignatureOptions(issuerCert)
{
Certification = PdfCertificationLevel.FormFilling,
});
byte[] signed = PdfSigner.Sign(certified, new PdfSignatureOptions(buyerCert) { FieldName = "Buyer" });
Signing refuses what would break a certification: certifying a document that is already signed, any signature on a
NoChanges document, and a new visible signature box on a certified one (an invisible signature or a field the
document was certified with is fine). The verifier reports CertificationLevel and the DisallowedChanges made after
a certification, and a certification with disallowed changes is not Valid.
Verifying signatures Changed in 1.20.0
PdfSignatureVerifier checks the signatures and document timestamps of any PDF, oldest first, and reports what it
finds rather than throwing for a broken signature:
foreach (PdfSignatureVerification s in PdfSignatureVerifier.Verify(pdf))
{
Console.WriteLine($"{s.FieldName}: signed by {s.SignerName} at {s.TimestampTime ?? s.SigningTime}");
Console.WriteLine($" valid: {s.Valid}, intact: {s.IsIntact}, covers the whole file: {s.CoversWholeDocument}");
foreach (string problem in s.Problems) Console.WriteLine(" " + problem);
}
// Trust only your own CA instead of the operating system's roots, and check revocation online.
var results = PdfSignatureVerifier.VerifyFile("contract.pdf", new PdfSignatureVerificationOptions
{
TrustedRoots = new X509Certificate2Collection(companyRootCert),
RevocationMode = X509RevocationMode.Online,
});
IsIntact means the signed bytes are unchanged (DigestMatches), the byte range is well formed (ByteRangeValid), the
signature value verifies with the signer's certificate (SignatureValid) and an embedded timestamp, if any, is sound
(TimestampValid). Valid adds that the certificate chains to a trusted root (ChainTrusted, with ChainErrors
saying why not). Chains are built from the certificates in the
signature, the PDF's Document Security Store and ExtraCertificates. Verification is offline by default:
RevocationMode is NoCheck unless you set it, and VerificationTime checks the chain at another time.
Trusted timestamps
The chain is checked at the time of the signature's timestamp only when TimestampTrusted is true: the timestamp
authority's certificate chains to the same trusted roots as the signer's and carries the timestamping key usage.
Anyone can make a token that states any time, so an untrusted timestamp no longer lets an expired signer certificate
pass; the chain is then checked now, and the untrusted timestamp is listed in Problems even when the signature is
valid without it. The signing time the signer claims is never used. A signer certificate whose key usage allows neither
digital signatures nor non-repudiation is not trusted, and a document timestamp's certificate must carry the
timestamping usage. Add your timestamp authority's root to TrustedRoots when you trust only your own CA.
A signature followed by later changes stays valid, because that is how a second signature or validation data is added:
UpdatesAfterSigning counts the incremental updates after it, and CoversWholeDocument is false. What those updates
change is not analysed (see Limitations), so check
CoversWholeDocument on the last signature when nothing may follow it. PAdES, adbe.pkcs7.detached,
adbe.pkcs7.sha1 and document timestamps (ETSI.RFC3161, IsDocumentTimestamp) are read, from PDFs made by any tool.
Long-term validation
A signature can normally be checked only while its certificates are valid and their CA answers. PdfSigner.AddValidationData
makes it checkable for years: it adds a Document Security Store with every certificate in the chains of the PDF's
signatures and timestamps, and the OCSP responses and CRLs that show none was revoked (PAdES B-LT). With TimestampUrl a
document timestamp follows, which seals the signatures and the validation data with a trusted time (PAdES B-LTA).
Both are incremental updates, so the signatures stay valid.
byte[] signed = PdfSigner.Sign(pdf, new PdfSignatureOptions(cert) { TimestampUrl = "http://timestamp.digicert.com" });
byte[] ltv = PdfSigner.AddValidationData(signed, new PdfLtvOptions
{
FetchRevocationData = true, // ask each certificate's OCSP responder, else its CRL
TimestampUrl = "http://timestamp.digicert.com", // PAdES B-LTA
});
// Offline: only the chains, completed with your own certificates and revocation data.
byte[] offline = PdfSigner.AddValidationData(signed, new PdfLtvOptions
{
Certificates = [intermediateCert, rootCert],
OcspResponses = [ocspResponseDer],
});
Without FetchRevocationData or TimestampUrl nothing goes over the network; OCSP and CRL requests use only the
http and https addresses in the certificates, through HttpClient if you set one. Those addresses come from the
PDF, so set UrlFilter to keep requests away from internal hosts: it is asked about every URL, redirect
targets included, and returning false skips it.
var options = new PdfLtvOptions
{
FetchRevocationData = true,
UrlFilter = url => url.Host.EndsWith(".digicert.com", StringComparison.OrdinalIgnoreCase),
};
Whatever the filter says, one call contacts at most 32 distinct URLs (64 requests), follows at most 3 redirects per CRL
download (never from https to http), bounds slow responses and stops fetching after two minutes in total. An
HttpClient whose handler follows redirects itself does so without asking the filter. To make the document timestamp itself
long-term verifiable, call AddValidationData again later without a TimestampUrl. A PDF without signatures throws PdfSignatureException.
Sign and AddValidationData take a CancellationToken, and AddValidationDataAsync awaits the OCSP, CRL and
timestamp requests instead of blocking a thread. HtmlToPdf.ConvertAsync signs asynchronously with the caller's token.
Cancelling throws OperationCanceledException, and an unreadable PDF throws InvalidDataException.