Digital signatures
editA signature proves that the signed bytes of a document did not change since it was signed, and binds them to the certificate of the signer. The types are in the Meziantou.Framework.Pdf.Signing namespace.
Sign a document
var document = new PdfDocument();
document.Pages.Add(PdfSize.A4).CreateContentBuilder()
.DrawText("Contract", PdfFont.GetStandardFont(PdfStandardFont.Helvetica), 24, 60, 700);
// The certificate has its private key: X509CertificateLoader.LoadPkcs12FromFile loads one from a .pfx file
var signed = PdfSigner.Sign(document, new PdfSignatureOptions(certificate) { Reason = "I approve this contract" });
var signature = PdfSignatureVerifier.Verify(signed).Single();
var isValid = signature.IsValid && signature.CoversWholeDocument;- PdfSigner.Sign returns the signed file. The signature is a PAdES baseline B-B signature (a CAdES detached signature, ETSI EN 319 142-1), in an invisible signature field on the first page.
- The signature is appended to the file as an incremental update, so the signatures the document already has stay valid. A document that was not read from a file is saved in full first; the changes made to a document read from a file are saved in the update of the signature.
- The certificate must have its private key (RSA or ECDSA):
X509CertificateLoader.LoadPkcs12FromFileloads one from a.pfxfile. Encrypted documents are signed with their keys.
| PdfSignatureOptions | Default | Sets |
|---|---|---|
Reason, Location, ContactInfo, SignerName | none | The information of the signature, shown by readers |
| SigningTime | Now | The time the signer claims; only a time stamp proves it |
| Certificates | Empty | The other certificates of the chain of the signer, embedded so that the signature can be validated |
| HashAlgorithm | SHA-256 | The digest algorithm: SHA-256, SHA-384 or SHA-512 |
| RsaSignaturePadding | PKCS #1 v1.5 | RSASignaturePadding.Pss for an RSASSA-PSS signature (RSA keys only) |
| FieldName | Signature1, Signature2... | The name of the signature field. An existing signature field that is not signed yet is signed; the name of any other field throws ArgumentException |
| Appearance | Invisible | The page and the rectangle of a visible signature, and its drawing or the options of the default layout |
| Certification, FieldLock | none | The changes a certification signature allows, and the fields the signature locks |
| TimestampAuthority, HttpClient | none | The time stamp authority of a B-T signature, and the client to call it |
| ReservedSize | Computed | The bytes reserved for the signature in the file |
Time stamp a signature
A time stamp from a trusted authority (RFC 3161) proves when the document was signed (PAdES B-T). It needs a request to the authority, so it is done by SignAsync; Sign throws ArgumentException when a time stamp is asked:
// PAdES B-T: the signature is stamped by an RFC 3161 time stamp authority
var options = new PdfSignatureOptions(certificate)
{
TimestampAuthority = new Uri("http://timestamp.example.com/"),
HttpClient = httpClient,
};
var signed = await PdfSigner.SignAsync(document, options, XunitCancellationToken);SignAsync throws HttpRequestException when the authority cannot be reached.
Show the signature on a page
By default the signature field is invisible. Set Appearance to show it in a rectangle of a page, given in the default user space of the page (points, from the bottom left of the page). The widget is printed with the page, and readers show it in the list of annotations of the page.
Without a drawing, the library draws a default layout: the name of the signer (SignerName, or the name of the certificate), the date (SigningTime, as yyyy-MM-dd HH:mm:ss and its offset), the reason and the location, with an optional image, such as a scan of a handwritten signature or a logo:
// Without a drawing, the library draws the name of the signer, the date, the reason and the location, and the image at their left
var options = new PdfSignatureOptions(certificate)
{
Reason = "I approve this contract",
Location = "Paris",
SigningTime = new DateTimeOffset(2026, 1, 2, 3, 4, 5, TimeSpan.Zero),
Appearance = new PdfSignatureAppearance(pageIndex: 0, PdfRectangle.FromSize(60, 60, 200, 50))
{
Image = image, // Such as PdfImage.Load("signature.png")
},
};
var signed = PdfSigner.Sign(document, options);| PdfSignatureAppearance | Default | Sets |
|---|---|---|
| Image | none | The image, scaled to fit the left 40% of the rectangle (the top 40% when the rectangle is taller than wide), keeping its proportions |
| Font | Helvetica | The font of the text. Helvetica shows the characters of WinAnsiEncoding: use an embedded font (PdfFont.Load) for the others, or Sign throws ArgumentException |
| FontSize | Fitted | The font size; by default the largest size, up to 10, at which the text fits in the rectangle |
To draw the appearance yourself, set Draw: it receives the content builder of the appearance, whose coordinates are those of the rectangle, from (0, 0) at its bottom left. A drawing that draws nothing gives an empty appearance.
// The widget is shown on the first page, at the bottom left; the drawing is in the coordinates of the rectangle
var options = new PdfSignatureOptions(certificate)
{
Reason = "I approve this contract",
Appearance = new PdfSignatureAppearance(pageIndex: 0, PdfRectangle.FromSize(60, 60, 200, 50))
{
Draw = builder => builder
.DrawText("Signed by Alice", PdfFont.GetStandardFont(PdfStandardFont.Helvetica), 12, 8, 30)
.DrawText("I approve this contract", PdfFont.GetStandardFont(PdfStandardFont.Helvetica), 9, 8, 12),
},
};
var signed = PdfSigner.Sign(document, options);Certify a document or lock fields
A certification signature is the first signature of a document: Certification sets the changes it allows after it (DocMDP). PdfCertificationLevel.NoChanges forbids any other signature, FormFilling allows filling in the form and signing, and FormFillingAndAnnotations also allows annotations. Validation data and document time stamps are allowed at every level. Sign throws InvalidOperationException when the certification of the document does not allow another signature, or when a certification is requested for a document that is already signed.
FieldLock locks fields (FieldMDP): new PdfFieldLock(PdfFieldLockAction.Include, ["Name"]) locks the field Name and its descendants; PdfFieldLockAction.All locks every field. When a signature field that is not signed yet has a lock dictionary, its lock is used.
Long-term validation
A signature can be validated years later only with the certificates and the revocation evidence of the time it was made. PdfSigner.AddValidationData writes them to the document security store (PAdES B-LT), with the certificates the signatures and their time stamps embed; PdfSigner.TimestampAsync then adds a document time stamp that protects them (PAdES B-LTA). Repeat both steps before the time stamp authority's certificate expires to extend the validity.
// PAdES B-LT: the certificates and the revocation evidence that validate the signatures, in the document security store
var validationData = new PdfValidationData();
validationData.Certificates.Add(authority.Certificate);
validationData.CertificateRevocationLists.Add(CertificateRevocationList);
var withValidationData = PdfSigner.AddValidationData(PdfDocument.Open(signedFile), validationData);
// PAdES B-LTA: a document time stamp protects the validation data
var timestamped = await PdfSigner.TimestampAsync(PdfDocument.Open(withValidationData), new PdfTimestampOptions(new Uri("http://timestamp.example.com/"))
{
HttpClient = httpClient,
}, XunitCancellationToken);- The data the store already has is not added again. This method writes no VRI dictionaries.
PdfSigner.AddValidationDataAsync gathers the validation data itself: it builds the chains of the signers, of the time stamp authorities and of the document time stamps from the certificates the signatures embed, those of the store and those of PdfValidationDataOptions, and finds the revocation evidence of each certificate of the chains that is not self-signed among the revocation lists and the OCSP responses of the options and of the store. With DownloadMissingData, it downloads what is missing with the HTTP client of the options: the issuers from the addresses of the certificates (caIssuers), then an OCSP response from the responder of each certificate, or else the revocation list of one of its distribution points. IncludeVri adds a VRI dictionary for each signature, which lists the certificates and the revocation evidence of its chains.
// PAdES B-LT: the chains of the signatures are completed, and the missing revocation evidence is downloaded
// from the addresses of the certificates (OCSP responders, revocation lists, issuers)
var options = new PdfValidationDataOptions
{
DownloadMissingData = true,
HttpClient = httpClient,
IncludeVri = true,
};
options.Certificates.Add(trustedRoot);
var withValidationData = await PdfSigner.AddValidationDataAsync(PdfDocument.Open(signedFile), options, XunitCancellationToken);- Nothing is downloaded unless DownloadMissingData is set; AddValidationData with PdfValidationDataOptions does the same work offline. Pass an
HttpClientconfigured with the proxy and the timeouts of the application. - Give the root certificates in Certificates: the chains end where no issuer is found.
- The library does not check the downloaded data: a revocation list or an OCSP response is stored when it comes from the issuer of the certificate, whatever its status. Validate the signatures with the trust policy of the application. When no address of a certificate answers, the method throws an
HttpRequestException. - The revocation evidence of the certificates of the OCSP responders is not downloaded: they usually have the
id-pkix-ocsp-nocheckextension (RFC 6960, 4.2.2.2.1). - PdfSignatureVerifier.GetValidationData reads the store back, for instance to build the chains offline with
X509ChainPolicy.ExtraStore.
Verify the signatures
PdfSignatureVerifier.Verify finds the signatures of a file and checks each of them: the bytes it covers are those that were signed, the cryptographic signature matches the certificate it embeds, and its time stamp is valid. It does not decide whether the signer is trusted: build the chain of the certificate with the trust anchors and the revocation policy of the application. Pass PdfReadOptions with the password to verify an encrypted document.
public static bool IsSignedByTrustedSigner(byte[] file, X509Certificate2 trustedRoot)
{
foreach (var signature in PdfSignatureVerifier.Verify(file))
{
// Intact and cryptographically valid, and nothing was appended to the file after it
if (!signature.IsValid || !signature.CoversWholeDocument || signature.Certificate is null)
continue;
// The library does not decide whether the signer is trusted: the application builds the chain
using var chain = new X509Chain();
chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
chain.ChainPolicy.CustomTrustStore.Add(trustedRoot);
chain.ChainPolicy.ExtraStore.AddRange(signature.Certificates);
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; // a private root without revocation lists
if (chain.Build(signature.Certificate))
return true;
}
return false;
}| PdfSignatureInfo | Tells |
|---|---|
| IsValid | The signature is intact, cryptographically valid, its time stamps are valid, its ESS signing certificate names the certificate of the signer, and no change after it breaks its certification or its field lock |
| IsDocumentIntact, IsSignatureValid, IsTimestampValid, IsSigningCertificateValid | Each of these checks; IsTimestampValid is null without a time stamp, and false when any of several time stamps is invalid; IsSigningCertificateValid is null without the attribute, except for ETSI.CAdES.detached, which requires it |
| Modifications, AreModificationsPermitted | The objects the revisions appended after the signature added or changed, with what each change is (a signature, validation data, a document time stamp, form filling, an annotation, or another change), and whether the certification and the field lock of the signature allow them |
| CertificationLevel, FieldLock | The certification and the field lock of the signature |
| CoversWholeDocument | Nothing was appended to the file after the signature. When it is false, the document was changed after it was signed: the signature covers an earlier revision only |
| Certificate, Certificates | The certificate of the signer, and the certificates the signature embeds |
| SigningTime, TimestampTime | The time the signer claims, and the time a time stamp proves |
Reason, Location, ContactInfo, SignerName, FieldName | The information of the signature |
| SubFilter, IsDocumentTimestamp | The format: ETSI.CAdES.detached, adbe.pkcs7.detached, adbe.pkcs7.sha1, or ETSI.RFC3161 for a document time stamp |
| Error | Why the signature could not be checked |
- The information of a signature (its name, reason, time and format) is read from the revision it signs: a later change of the signature dictionary is reported in Modifications, not returned as the information.
- A signature has one signer: CMS signed data with more signers has an Error.
- Modifications is a summary built from the structure of the document (the fields, the annotations of the pages, the document security store), not a proof that the pages look the same: review it, and treat a PdfModificationKind.Other change as a change of the content.
The signatures of other tools are verified as well, in the formats above, with or without signed attributes, and RSA signatures with PKCS #1 v1.5 or PSS padding.