Recursive AI

Sign a PDF and verify its signatures

All onepdf recipes

Sign a PDF in C# with a certificate from a .pfx file, verify the signature against your own root, and see how a change after signing is reported.

The sample uses signer.pfx, a certificate with its private key, and root.cer, the root certificate that issued it. Use your own, or make a test pair with .NET’s CertificateRequest as in Getting started.

C#
using System.Security.Cryptography.X509Certificates;
using OnePdf;
 
using X509Certificate2 signer = X509CertificateLoader.LoadPkcs12FromFile("signer.pfx", "test");
using X509Certificate2 root = X509CertificateLoader.LoadCertificateFromFile("root.cer");
 
// Sign: an invisible PAdES signature, written as an incremental update.
using (PdfDocument doc = PdfDocument.Open("report.pdf", new PdfOpenOptions { AllowEditing = true }))
{
    PdfSigningResult signed = doc.Sign("signed.pdf", PdfSigningCredential.FromCertificate(signer), new PdfSigningOptions
    {
        FieldName = "Approval",
        Reason = "Approved for release",
        Location = "Springfield",
    });
    Console.WriteLine($"signed field {signed.FieldName}, {signed.ContentsUsed:N0} of {signed.ContentsCapacity:N0} bytes reserved");
}
 
Verify("signed.pdf");
 
// Change the signed file: stamp a page and save the change as another incremental update.
using (PdfDocument doc = PdfDocument.Open("signed.pdf", new PdfOpenOptions { AllowEditing = true }))
{
    doc.AddTextStamp("COPY", doc.AddStandardFont("Helvetica"), 48, pageIndexes: [0]);
    using FileStream output = File.Create("changed.pdf");
    doc.SaveIncremental(output);
}
 
Verify("changed.pdf");
 
void Verify(string path)
{
    using PdfDocument doc = PdfDocument.Open(path);
    PdfSignatureReport report = doc.VerifySignatures(new PdfSignatureVerifyOptions { TrustAnchors = [root] });
    foreach (PdfSignatureVerification v in report.Signatures)
    {
        Console.WriteLine($"{path}: {v.Signature.FieldName} valid={v.IsValid} integrity={v.Integrity} signature={v.SignatureCheck} chain={v.Chain} coverage={v.Coverage}");
        foreach (PdfSignatureChange change in v.Changes.Where(c => !c.Allowed))
            Console.WriteLine($"  not allowed: object {change.ObjectNumber}, {change.Category}");
    }
}
Text
signed field Approval, 1,691 of 3,175 bytes reserved
signed.pdf: Approval valid=True integrity=Valid signature=Valid chain=Trusted coverage=WholeDocument
changed.pdf: Approval valid=False integrity=Valid signature=Valid chain=Trusted coverage=EarlierRevisionDisallowedChanges
  not allowed: object 7, PageContent
  not allowed: object 74, PageContent
  not allowed: object 75, PageContent
  not allowed: object 76, PageContent
  not allowed: object 77, PageContent
  not allowed: object 78, PageContent

The signature over the original bytes is still mathematically intact in changed.pdf, but the page content changed after it, so the signature is no longer valid for the document as it now reads.

Signing

  • Signing is always an incremental update. The original bytes stay an exact prefix of the output, so earlier signatures stay valid. The document must be opened with AllowEditing, and changes made before the call, such as a filled field, are part of what is signed.
  • The default format is PAdES baseline B-B (ETSI.CAdES.detached). Format = PdfSignatureFormat.Pkcs7Detached writes adbe.pkcs7.detached for older consumers.
  • Time-stamps. Set Timestamp to a callback that sends request.RequestDer to your time-stamp authority and returns its response, and the signature becomes PAdES B-T. AddDocumentTimestamp adds a document time-stamp.
  • Visible signatures. Set Rectangle (and PageIndex) for a visible signature with the signer’s name, the date and the reason. Leave it out for an invisible one.
  • Certification. Certify makes a certification signature that states which changes are allowed afterwards. Only the first signature of a document can certify.
  • Keys you can’t export. PdfSigningCredential.FromExternalSigner hands OnePDF’s digest to your code, for a hardware module or a cloud key service, and checks the signature value you return.
  • RSA (PKCS#1 v1.5 and PSS) and ECDSA keys are supported. Long-term validation data (PAdES B-LT and B-LTA) isn’t written.
  • To sign twice, sign, open the output with AllowEditing, and sign again.

Verifying

  • IsValid is true only when the byte range is sound, the digest matches, the signature verifies, the chain is trusted at the validation time, any time-stamp is valid, and nothing disallowed follows the signature.
  • Nothing is fetched. A chain is trusted only when it leads to a certificate in TrustAnchors, or to the system store with UseSystemTrustStore. Revocation is reported as not checked unless you pass a RevocationCheck function that answers from CRLs or OCSP responses you already have.
  • Changes lists every object written after the signature with a category (FormFieldValue, NewSignature, Annotation, PageContent, ValidationData, Other) and whether the document’s permissions allow it. Filling a form field after signing is allowed by default; changing page content isn’t.
  • Warnings explains anything that failed or couldn’t be checked, such as a weak algorithm.

The checks, the change rules and how known attacks on PDF signatures are reported are in Signatures.

A longer walk-through with PAdES B-T time stamps, visible signatures, HSM and cloud keys: Digitally sign a PDF in C#.