Digital signatures
Signatures are always added as an incremental update: the original bytes stay untouched, so existing signatures remain valid. The CMS container is assembled from the digest of the signed byte ranges (the document is never copied for signing); certificates, keys and time-stamps use the .NET cryptography APIs.
Signing with a certificate file
using MiPdf.Signatures;
var signer = CertificateSigner.FromPfx("signer.pfx", "password"); // key + chain from PKCS#12
byte[] signed = PdfSigner.Sign(File.ReadAllBytes("contract.pdf"), signer, new PdfSignatureOptions
{
Reason = "Approval",
Location = "Vienna",
ContactInfo = "office@example.com",
// visible signature on page 1; omit Rectangle for an invisible signature
PageNumber = 1,
Rectangle = new PdfRectangle(350, 50, 550, 110),
// RFC 3161 signature time-stamp -> PAdES B-T
TimestampClient = new HttpTimestampClient("http://timestamp.digicert.com"),
});
File.WriteAllBytes("contract-signed.pdf", signed);
Overloads accept an X509Certificate2 with private key (from the Windows certificate store, a smart card
CSP/KSP, ...) or a PdfDocument.
Large files are signed from file to file without loading them; the output may be the input file:
PdfSigningResult result = PdfSigner.SignFile("archive.pdf", "archive-signed.pdf", signer, options);
| Option | Description |
|---|---|
Format |
PAdES (default, ETSI.CAdES.detached, with signing-certificate-v2) or Pkcs7Detached (adbe.pkcs7.detached, with signing-time and signing-certificate-v2) |
DigestAlgorithm |
SHA-256 (default), SHA-384, SHA-512 |
RsaPadding |
RSA keys: RSASignaturePadding.Pkcs1 (default, PKCS#1 v1.5) or RSASignaturePadding.Pss (RSASSA-PSS, MGF1 with the digest algorithm, salt length = digest length) |
FieldName |
existing empty signature field to sign, or name of the new field (default Signature1, Signature2, ...) |
TimestampClient |
time-stamp authority (B-T level) |
Certification |
certify the document (DocMDP): NoChanges, FormFilling, FormFillingAndAnnotations |
FieldLock |
form fields locked by this signature (FieldMDP): PdfFieldLock.All(), Include(..), Exclude(..) |
Appearance |
text template ({name}, {date}, {reason}, {location}, {contact}), image (left, background or image only), font, colors |
Password |
password of an encrypted document |
SignatureSize |
reserved container size (default: estimated automatically) |
Key types
| Key | Supported |
|---|---|
| RSA | PKCS#1 v1.5 and RSASSA-PSS, any key size |
| ECDSA | NIST P-256, P-384, P-521 and Brainpool P256r1, P384r1, P512r1 (tested); any other curve the platform supports. Use a digest that matches the curve size (e.g. P-384 with SHA-384). |
Certificates issued with RSASSA-PSS are supported for chain building, LTV and validation as well.
HSM, smart cards, cloud signing – IExternalSigner
When the private key cannot be exported, implement IExternalSigner; only the hash is passed to it:
public sealed class MyHsmSigner : IExternalSigner
{
public X509Certificate2 Certificate { get; } // signer certificate (public)
public IReadOnlyList<X509Certificate2> CertificateChain { get; } // intermediates to embed
public byte[] SignHash(byte[] hash, HashAlgorithmName alg)
=> hsm.Sign(hash, alg); // RSA: PKCS#1 v1.5 signature; ECDSA: IEEE P1363 (r||s)
// optional, only needed for RsaPadding = RSASignaturePadding.Pss
public byte[] SignHash(byte[] hash, HashAlgorithmName alg, RSASignaturePadding padding)
=> hsm.Sign(hash, alg, padding);
}
byte[] signed = PdfSigner.Sign(pdf, new MyHsmSigner(...), options);
Signers that do not implement the padding overload are rejected with NotSupportedException when PSS is
requested; they are not asked to sign.
Deferred (two-step) signing with a signing service
For services that return a complete CMS/PKCS#7 container (e.g. remote/mobile signature services):
// step 1: prepare (store PreparedPdf if step 2 runs later or in another process)
var prepared = PdfSigner.PrepareExternalSignature(pdf, new PdfSignatureOptions { FieldName = "sig1" }, certificate);
byte[] digest = prepared.GetDocumentDigest(); // SHA-256 of the signed byte ranges
// ... send digest to the service, receive a detached CMS signature container ...
byte[] signed = PdfSigner.CompleteExternalSignature(prepared, cmsFromService);
// or later: PdfSigner.CompleteExternalSignature(storedPreparedPdf, "sig1", cmsFromService);
Deferred signing with a raw signature value
For services that only sign a hash and return the raw signature value (possibly in a later HTTP round trip), build the signed attributes yourself and assemble the CMS afterwards:
var prepared = PdfSigner.PrepareExternalSignature(pdf, options, signerCertificate, containerSize: 65536);
byte[] attributes = CmsUtil.CreateSignedAttributes(prepared.GetDocumentDigest(), signerCertificate,
additionalAttributes: biometricAttributes); // store PreparedPdf + attributes between the steps
byte[] hashToSign = SHA256.HashData(attributes);
// ... later: signature value from the service (RSA PKCS#1 v1.5 or PSS, or ECDSA r||s / DER)
byte[] cms = CmsUtil.AssembleSignedData(attributes, signatureValue, signerCertificate, chain);
// RSASSA-PSS value: CmsUtil.AssembleSignedData(..., rsaPadding: RSASignaturePadding.Pss)
cms = CmsUtil.AddSignatureTimestamp(cms, tsa); // optional: B-T
byte[] signed = PdfSigner.CompleteExternalSignature(prepared, cms);
CmsUtil.AddSignatureTimestamp also works for containers created by other software (remote signing services,
smart card middleware, ...): the signed parts are copied byte by byte. CmsUtil.GetSignedAttribute/GetUnsignedAttribute
read attributes of any container.
Custom signed attributes (e.g. biometric data)
var w = new AsnWriter(AsnEncodingRules.DER);
w.WriteOctetString(encryptedBiometricBlob);
// use an OID from your own arc; 1.3.6.1.4.1.32473 is the example enterprise number of RFC 5612
options.AdditionalSignedAttributes.Add(new AsnEncodedData("1.3.6.1.4.1.32473.1.1", w.Encode()));
// reading: PdfSignatureValidator.Validate(pdf)[0].GetSignedAttribute("1.3.6.1.4.1.32473.1.1")
The reserved signature size is enlarged automatically.
Custom entries in the signature dictionary
Some applications store data directly in the signature dictionary (inside the signed byte range):
options.SignatureDictionaryEntries["Biometrics"] = new PdfString(encryptedBase64);
options.SignatureDictionaryEntries["MyAppData"] = new PdfString("100.5;200");
options.SignatureCreator = "MyApp"; // /Prop_Build /App /Name
// reading
string? bio = PdfSignatureValidator.Validate(pdf)[0].GetSignatureDictionaryText("Biometrics");
Field locks (FieldMDP)
A signature can lock form fields: the lock (/Lock on the signature field) and a FieldMDP reference in the signature dictionary are covered by the signature, so a later change of a locked field makes the signature invalid in Adobe Acrobat instead of being shown as permitted form filling. Without a lock, approval signatures only protect the signed revision, and filling any field afterwards is an allowed change.
// lock the fields of this signer when signing
var options = new PdfSignatureOptions { FieldName = "Employee", FieldLock = PdfFieldLock.Include("name", "date") };
byte[] signed = PdfSigner.Sign(pdf, signer, options);
// or let the form author define the lock in advance: whoever signs the field applies it
form.AddSignatureField(page, "Manager", rect).Lock = PdfFieldLock.All();
// validation: locked fields changed after signing
var result = PdfSignatureValidator.Validate(signed)[0];
Console.WriteLine($"{result.FieldLock}: changed {string.Join(", ", result.ModifiedLockedFields)}");
Field names are fully qualified ("address.street"); unknown names are rejected when signing. FieldLock can be
combined with Certification (both references are written, DocMDP first) and works with deferred signing.
PdfFieldLock.All() and Exclude(..) also lock fields that are added later.
Time-stamp failover and optional time-stamps
var tsa = FailoverTimestampClient.FromUrls(new[] { "http://tsa.belgium.be/connect", "http://timestamp.digicert.com" });
var result = PdfSigner.SignDetailed(pdf, signer, new PdfSignatureOptions { TimestampClient = tsa, TimestampOptional = true });
Console.WriteLine($"{result.Level} {string.Join("; ", result.Warnings)}"); // BT, or BB if all TSAs failed
Handwritten signatures and custom appearances
options.Appearance = new PdfSignatureAppearance
{
Text = "", // no text
Draw = (canvas, width, height) => InkRenderer.Draw(canvas, strokes, // pressure-sensitive vector ink
minWidth: 0.35, maxWidth: 2.4, topLeftHeight: height), // screen coordinates (top-left origin)
};
// image above a text strip:
options.Appearance = new PdfSignatureAppearance { Image = png, ImagePosition = SignatureImagePosition.Top, Text = "Signed by {name}\n{date}" };
On rotated pages (/Rotate) the appearance is drawn upright as seen in viewers (KeepUpright, default true); an
existing signature field with /MK /R is drawn with that rotation. Rectangle stays in unrotated page coordinates,
so on a page rotated by 90 or 270 degrees the visible width of the signature is the rectangle's height. The Draw
callback receives the upright width and height.
Long-term validation: PAdES B-LT and B-LTA
var lt = PdfLtv.AddValidationData(signedPdf); // B-LT: certificates, OCSP responses, CRLs in /DSS
var lta = PdfLtv.MakeLta(signedPdf, tsa); // B-LTA: + document time-stamp (+ its validation data)
Console.WriteLine($"{lta.Level} complete={lta.IsComplete} {string.Join("; ", lta.Warnings)}");
- Works for all signatures in the document, including signatures created by other software (
FieldNamesrestricts it). - Missing issuer certificates are downloaded via AIA (caIssuers); OCSP is preferred, CRLs are the fallback
(
IncludeCrlsWithOcsp,UseOcsp,UseCrls). Delegated OCSP responder certificates are embedded as well. HttpRevocationSourcecaches CRLs and issuers (reuse the instance), limits CRL size (MaxCrlSize, default 5 MB). ImplementIRevocationSourcefor custom caching, proxies or offline data.- Failures never break the document: they are listed in
Warnings,IsCompletetells whether every certificate is covered. /VRIentries are optional (IncludeVri). The PAdES extension (/Extensions /ESIC) is declared.- Verified against DigiCert (OCSP responses checked with
openssl ocsp, CRLs withopenssl crl).
Document time-stamps
byte[] stamped = PdfSigner.AddDocumentTimestamp(signedPdf, new HttpTimestampClient("http://timestamp.digicert.com"));
Verifying signatures
var options = new SignatureValidationOptions { CheckCertificateChain = true }; // optional
foreach (var r in PdfSignatureValidator.Validate(File.ReadAllBytes("signed.pdf"), options))
{
Console.WriteLine($"{r.FieldName}: valid={r.IsIntegrityValid}, signer={r.SignerName}, " +
$"whole document={r.CoversWholeDocument}, timestamp={r.TimestampTime}, trusted={r.IsCertificateTrusted}");
}
PdfSignatureValidator.Validate("signed.pdf") reads the file on demand and hashes the signed byte ranges from it, for
files of any size.
| Property | Meaning |
|---|---|
IsIntegrityValid |
signed bytes unchanged, message digest and signature value correct (RSA PKCS#1 v1.5 and PSS, ECDSA), byte range excludes exactly the container |
CoversWholeDocument |
no incremental updates after this signature; otherwise GetSignedRevision returns the signed version |
TimestampTime |
verified signature time-stamp or document time-stamp |
IsCertificateTrusted |
chain built against the system store or TrustedRoots (when CheckCertificateChain is set) |
CertificationLevel |
DocMDP level of a certification signature |
FieldLock, ModifiedLockedFields |
FieldMDP lock of the signature and the locked fields whose value was changed or that were removed after signing |
Level |
PAdES level (B-B, B-T, B-LT, B-LTA) derived from time-stamps, DSS and later document time-stamps |
Revocation, RevocationDetails |
revocation status at the time of signing from the OCSP responses and CRLs stored in the document (see below) |
Cms, Certificates, GetSignedAttribute(oid) |
raw container, embedded certificates, attribute values (e.g. biometric data) |
Revocation status from the stored validation data
foreach (var r in PdfSignatureValidator.Validate(File.ReadAllBytes("signed-lt.pdf")))
{
Console.WriteLine($"{r.FieldName}: {r.Revocation}"); // Good, Revoked, Unknown – null without data
foreach (var c in r.RevocationDetails)
Console.WriteLine($" {c.Certificate.Subject}: {c.Status} via {c.Source}, issued {c.IssuedAt}, fresh={c.IsFresh}");
}
The validator evaluates the OCSP responses and CRLs of the Document Security Store and of the
adbe-revocationInfoArchival signature attribute – offline, without contacting any service:
- Every certificate of the signer's chain is checked up to a self-signed root or a root in
TrustedRoots. For document time-stamps, the chain of the time-stamping authority is checked. - Only validly signed revocation data counts: CRLs signed by the issuer, OCSP responses signed by the issuer or by
a responder certificate the issuer authorised (extended key usage OCSP signing). Everything else is ignored, and
the certificate is
Unknown. - The decisive time is the verified signature time-stamp, otherwise the earliest later document time-stamp,
otherwise the time of validation – the claimed signing time (
/M) is not trusted. A revocation after that time keeps the statusGood(RevocationTimeshows when it happened); a revocation at or before it givesRevoked. IsFreshtells whether the evidence was issued after the time of signing, i.e. whether it proves the status at that time. Right after signing this is usually false: OCSP responders and CRLs publish data that is hours or days old. For archiving, add the validation data again after a grace period (for example a day later withPdfLtv.AddValidationDataorMakeLta).IsIntegrityValidis not affected; a signature is valid ifIsIntegrityValid,IsCertificateTrustedand aRevocationother thanRevokedall hold. Online checking through the operating system is still available withSignatureValidationOptions.RevocationModetogether withCheckCertificateChain.- Tested with real DigiCert OCSP responses and CRLs (time-stamping chain with three certificates).
Apart from the values of locked fields (ModifiedLockedFields), whether changes after a signature are permitted
(DocMDP analysis, other object changes) is not evaluated; the validator reports that later revisions exist.
Standards and verification
- PAdES baseline B-B / B-T (ETSI EN 319 142-1):
ETSI.CAdES.detached, signing-certificate-v2, no signing-time attribute, claimed time in/M, signature time-stamp as unsigned attribute. - Verified independently with OpenSSL (
openssl cms -verifyover the extracted byte ranges). - Validated with EU DSS: RSA PKCS#1 v1.5 and RSASSA-PSS (SHA-256/512, also deferred and with
a PSS-issued certificate), ECDSA P-256/P-384/P-521 and Brainpool P256r1/P384r1/P512r1 (incl. raw deferred
signature values), multiple
signatures with certification, PKCS#7, ink appearances are cryptographically intact and valid; DSS classifies
the output as PAdES-BASELINE-B / -T / -LT / -LTA and accepts the embedded OCSP responses and CRLs. For B-LT,
every certificate of the chain needs revocation data – private CAs must publish a CRL/OCSP or provide it via
a custom
IRevocationSource. - PAdES B-LT / B-LTA: DSS with certificates, OCSP responses and CRLs, document time-stamps.
- Field locks: EU DSS reads the /SigFieldLock and FieldMDP dictionaries (action and field list) and accepts them; Adobe Acrobat keeps the signature valid when an unlocked field is filled and reports it as invalid when a locked field is changed.
- Not yet supported: /P in field locks (PDF 2.0), legacy
adbe.x509.rsa_sha1creation, full evaluation of permitted changes after signing (DocMDP).