SelectPdf for .NET 10 - Digital Signatures - C# / .NET 10 Sample

Digital signatures can be added to a PDF document using PdfDigitalSignatureElement objects. Adding a digital signature requires a PKCS#12 certificate (provided as a .pfx or a .p12 file).

Sample certificate file:
Certificate

The following sample shows how to add a digital signature to a pdf document using SelectPdf Library for .NET.

IMPORTANT: Please note that the certificate used is a self signed certificate generated for testing purposes only. The signature will appear as not valid in a pdf viewer. Using a valid certificate will result in a valid digital signature.

Signing With a Key Held Outside the Library

When the private key lives in a hardware security module (HSM), a smart card, a PKCS#11 token or a cloud key service, implement IPdfExternalSigner: it gives the signer's certificates and signs the request the library hands it. The library builds the whole signature and asks only for the signature value, so the key never leaves the device or service. When the signature cannot be produced while the document is saved (a remote signing service where the signer approves on another device), PrepareExternalSignature reserves an empty signature and returns the request, and CompleteExternalSignature writes the value into the document later. In this sample the key is read from the certificate file and stands in for the HSM or key service.

Two signers are ready-made. PdfCertificateStoreSigner signs with a certificate from the certificate store, found by its thumbprint; when the key is on a USB token or a smart card, the token's software asks for the PIN. PdfPkcs11Signer signs through the PKCS#11 library of a USB token, a smart card or an HSM. In a web application both use the certificate store and the devices of the machine and the account the application runs under.

Signing method:


Certificate thumbprint (certificate store):


PKCS#11 library path:

PKCS#11 PIN:

Token label (optional):

Key label (optional):

Signature Appearance

The visible signature shows the signer's name and the reason, location and date of the signature, over a faint background. SignatureBackgroundImage draws your logo there instead of the default flourish, and SignatureBackgroundOpacity sets how strongly it is drawn, from 0 (no background) to 1 (the image's own colors).

Background image (optional):


Background opacity (0 to 1):

Long-Term Validation (LTV)

A signature can be validated only as long as its certificate can be checked. With EnableLtv, the revocation data of the signing certificate and of its issuers (OCSP responses or certificate revocation lists, from the addresses the certificates name) is embedded in the document when it is saved, so the signature can still be validated after the certificate has expired, or offline. The sample certificate is self-signed and names no revocation source, so it needs no revocation data; upload a certificate issued by a certificate authority (a .pfx or .p12 file that contains its private key) to see the data embedded. Saving fails when the revocation data cannot be obtained.

Certificate (optional):


Certificate password:


Make the signature LTV-enabled

Validate Signatures

PdfSecurityManager.ValidateSignatures checks every signature of a pdf document: whether it is intact, whether its certificate chains to a trusted root, and whether content was added to the document after it. Upload a signed pdf document, or leave the field empty to validate a document signed by this sample (with the certificate and the LTV option chosen above). The sample certificate is self-signed, so it is trusted only when it is passed as a root. It also names no revocation source, so with the revocation check on its revocation status is Unknown; a certificate issued by a certificate authority reports Good or Revoked.

Signed PDF (optional):


Trust the sample certificate as a root
Check whether the certificate was revoked (the revocation data of an LTV-enabled document is used first, without going online)

Sample code · C#
using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Globalization;
using SelectPdf.Universal;

namespace SelectPdf.Samples.Pages
{
    public class DigitalSignaturesModel : PageModel
    {
        public void OnGet()
        {

        }

        // a certificate to sign with (optional; the sample certificate is used when none is uploaded)
        [BindProperty]
        public IFormFile FileCertificate { get; set; }

        [BindProperty]
        public string TxtCertificatePassword { get; set; }

        // how the document is signed: with the certificate file, with a certificate from the certificate
        // store, with a PKCS#11 device (USB token, smart card, HSM), with an external signer (a key held
        // outside the library), or in two steps (prepare, then complete)
        [BindProperty]
        public string DdlSigningMethod { get; set; } = "Certificate";

        public List<SelectListItem> SigningMethods { get; } = new List<SelectListItem>
        {
            new SelectListItem { Value = "Certificate", Text = "Certificate file" },
            new SelectListItem { Value = "Store", Text = "Certificate store (by thumbprint)" },
            new SelectListItem { Value = "Pkcs11", Text = "PKCS#11 device (USB token, smart card, HSM)" },
            new SelectListItem { Value = "External", Text = "External signer (HSM, smart card, key service)" },
            new SelectListItem { Value = "TwoSteps", Text = "Two steps (prepare, sign elsewhere, complete)" },
        };

        // the certificate store signer: the thumbprint of a certificate with a private key, in the store of
        // the account the application runs under (a token's software asks for its PIN)
        [BindProperty]
        public string TxtThumbprint { get; set; }

        // the PKCS#11 signer: the vendor's library, the PIN, and the token and key labels (both optional
        // when there is only one token and one key)
        [BindProperty]
        public string TxtPkcs11Library { get; set; }

        [BindProperty]
        public string TxtPkcs11Pin { get; set; }

        [BindProperty]
        public string TxtPkcs11Token { get; set; }

        [BindProperty]
        public string TxtPkcs11Key { get; set; }

        // the background of the visible signature: an image (optional; a faint flourish is drawn when
        // none is given) and how strongly it is drawn, from 0 (none) to 1
        [BindProperty]
        public IFormFile FileBackground { get; set; }

        [BindProperty]
        public string TxtBackgroundOpacity { get; set; } = "0.25";

        // embed the revocation data of the certificate chain (long-term validation)
        [BindProperty]
        public bool ChkEnableLtv { get; set; }

        // a signed pdf document to validate (optional)
        [BindProperty]
        public IFormFile FileSigned { get; set; }

        // trust the sample certificate as a root when validating
        [BindProperty]
        public bool ChkTrustSampleCertificate { get; set; } = true;

        // ask whether the signer's certificate has been revoked
        [BindProperty]
        public bool ChkCheckRevocation { get; set; }

        // the validation results shown on the page
        public PdfSignatureValidationResult[] ValidationResults { get; private set; }

        public string ValidatedDocument { get; private set; }

        public IActionResult OnPost()
        {
            byte[] pdf;
            try
            {
                // create a signed pdf document
                pdf = CreateSignedDocument();
            }
            catch (PdfDocumentException ex)
            {
                // for example: LTV was requested and the revocation data could not be obtained
                ViewData["Message"] = "An error occurred: " + ex.Message;
                return Page();
            }

            // return resulted pdf document
            FileResult fileResult = new FileContentResult(pdf, "application/pdf");
            fileResult.FileDownloadName = "Document.pdf";
            return fileResult;
        }

        public IActionResult OnPostValidate()
        {
            // the document to validate: the uploaded one, or a document signed by this sample
            byte[] pdf;
            if (FileSigned != null && FileSigned.Length > 0)
            {
                using (MemoryStream ms = new MemoryStream())
                {
                    FileSigned.CopyTo(ms);
                    pdf = ms.ToArray();
                }
                ValidatedDocument = Path.GetFileName(FileSigned.FileName);
            }
            else
            {
                try
                {
                    pdf = CreateSignedDocument();
                }
                catch (PdfDocumentException ex)
                {
                    ViewData["Message"] = "An error occurred: " + ex.Message;
                    return Page();
                }
                ValidatedDocument = "a document signed by this sample";
            }

            // load the signed document in a security manager
            PdfSecurityManager manager = new PdfSecurityManager();
            manager.Load(pdf);

            // ask the certificate's issuer whether it has been revoked; the revocation data a
            // LTV-enabled document carries is used first, without going online
            manager.RevocationCheckEnabled = ChkCheckRevocation;

            if (ChkTrustSampleCertificate)
            {
                // trust the sample certificate as a root, in addition to the system's trusted roots
                // (the same call trusts your organization's own certificate authority)
                using (PdfDigitalCertificatesCollection roots =
                    PdfDigitalCertificatesStore.GetCertificates(CertificateFile(), "selectpdf"))
                {
                    ValidationResults = manager.ValidateSignatures(roots);
                }
            }
            else
            {
                // trust only the system's trusted roots
                ValidationResults = manager.ValidateSignatures();
            }

            manager.Close();

            // show the results on the page
            return Page();
        }

        private static string CertificateFile()
        {
            return Path.Combine(Startup.ServerPath, "wwwroot", "files", "selectpdf.pfx");
        }

        // the certificates to sign with: the uploaded ones, or the sample certificate; the one that
        // can sign comes first. The caller disposes of them after the document is saved - a document
        // is signed when it is saved, and the collection holds the private key until it is disposed.
        private PdfDigitalCertificatesCollection SigningCertificates()
        {
            if (FileCertificate == null || FileCertificate.Length == 0)
            {
                // get the #PKCS12 certificate from file
                return PdfDigitalCertificatesStore.GetCertificates(CertificateFile(), "selectpdf");
            }

            // the certificate store reads a file, so keep the uploaded one in a temporary file
            string tempFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N") + ".pfx");
            try
            {
                using (FileStream fs = System.IO.File.Create(tempFile))
                {
                    FileCertificate.CopyTo(fs);
                }

                // the certificates that can sign come first; the rest of the file (the issuers)
                // is used to build the chain for long-term validation
                return PdfDigitalCertificatesStore.GetCertificates(tempFile, TxtCertificatePassword ?? "");
            }
            finally
            {
                System.IO.File.Delete(tempFile);
            }
        }

        // An external signer: it gives the certificates and signs the request with a key the library
        // never sees. In this sample the key comes from the certificate file; in an application it is a
        // key in an HSM, a smart card or a key service, whose SDK signs the digest.
        private class KeySigner : IPdfExternalSigner
        {
            private readonly AsymmetricAlgorithm key;

            public KeySigner(IList<byte[]> certificates, AsymmetricAlgorithm key)
            {
                Certificates = certificates;
                this.key = key;
            }

            // the signing certificate first, then its issuers, DER-encoded
            public IList<byte[]> Certificates { get; }

            public byte[] Sign(PdfExternalSigningRequest request)
            {
                return SignDigest(key, request.Digest, request.DigestAlgorithm);
            }
        }

        // what the HSM or key service does: sign a digest with the private key
        private static byte[] SignDigest(AsymmetricAlgorithm key, byte[] digest, PdfSignatureDigest algorithm)
        {
            RSA rsa = key as RSA;
            if (rsa != null)
                return rsa.SignHash(digest, new HashAlgorithmName(algorithm.ToString()),
                    RSASignaturePadding.Pkcs1);
            return ((ECDsa)key).SignHash(digest);
        }

        // the certificate file's contents, for the external signing methods: the certificates (DER) and
        // the private key that stands in for an HSM or key service. The loaded certificates hold the
        // key until they are disposed, so the caller releases them (ReleaseKey) once it has signed.
        private void ReadKeyAndCertificates(out List<byte[]> certificates, out AsymmetricAlgorithm key,
            out X509Certificate2Collection loaded)
        {
            byte[] packet;
            string password;
            if (FileCertificate == null || FileCertificate.Length == 0)
            {
                packet = System.IO.File.ReadAllBytes(CertificateFile());
                password = "selectpdf";
            }
            else
            {
                using (MemoryStream ms = new MemoryStream())
                {
                    FileCertificate.CopyTo(ms);
                    packet = ms.ToArray();
                }
                password = TxtCertificatePassword ?? "";
            }

            X509Certificate2Collection all = X509CertificateLoader.LoadPkcs12Collection(packet, password);
            loaded = all;
            X509Certificate2 signer = all.Cast<X509Certificate2>().First(c => c.HasPrivateKey);
            certificates = new List<byte[]> { signer.RawData };
            foreach (X509Certificate2 c in all)
                if (!c.HasPrivateKey) certificates.Add(c.RawData);
            key = (AsymmetricAlgorithm)signer.GetRSAPrivateKey() ?? signer.GetECDsaPrivateKey();
        }

        private static void ReleaseKey(AsymmetricAlgorithm key, X509Certificate2Collection loaded)
        {
            if (key != null) key.Dispose();
            if (loaded != null)
                foreach (X509Certificate2 c in loaded) c.Dispose();
        }

        // the uploaded background image, or null for the default flourish
        private byte[] BackgroundImage()
        {
            if (FileBackground == null || FileBackground.Length == 0)
                return null;
            using (MemoryStream ms = new MemoryStream())
            {
                FileBackground.CopyTo(ms);
                return ms.ToArray();
            }
        }

        private float BackgroundOpacity()
        {
            float opacity;
            if (!float.TryParse(TxtBackgroundOpacity, NumberStyles.Float, CultureInfo.InvariantCulture,
                out opacity))
                opacity = 0.25f;
            return Math.Max(0f, Math.Min(1f, opacity));
        }

        private byte[] CreateSignedDocument()
        {
            // create a new pdf document
            PdfDocument doc = new PdfDocument();

            // add a new page to the document
            PdfPage page = doc.AddPage();

            // get image path
            // the image will be used to display the digital signature over it
            string imgFile = Path.Combine(Startup.ServerPath, "wwwroot", "images", "logo.png");

            // define a rendering result object
            PdfRenderingResult result;

            // create image element from file path
            PdfImageElement img = new PdfImageElement(0, 0, imgFile);
            result = page.Add(img);

            if (DdlSigningMethod == "TwoSteps")
                return SignInTwoSteps(doc, page, result.PdfPageLastRectangle);

            // the certificates and signers, disposed of when this method returns - after the document is
            // saved, since the document is signed when it is saved
            using PdfDigitalCertificatesCollection certificatesFromFile =
                DdlSigningMethod == "Certificate" ? SigningCertificates() : null;
            using PdfCertificateStoreSigner storeSigner =
                DdlSigningMethod == "Store" ? new PdfCertificateStoreSigner(TxtThumbprint ?? "") : null;
            using PdfPkcs11Signer pkcs11Signer = DdlSigningMethod == "Pkcs11"
                ? new PdfPkcs11Signer(TxtPkcs11Library ?? "", TxtPkcs11Pin, TxtPkcs11Token, TxtPkcs11Key)
                : null;

            // create the digital signature object
            PdfDigitalSignatureElement signature;
            AsymmetricAlgorithm externalKey = null;
            X509Certificate2Collection externalCertificates = null;
            if (storeSigner != null)
            {
                // signed with a certificate from the certificate store
                signature = new PdfDigitalSignatureElement(result.PdfPageLastRectangle, storeSigner);
            }
            else if (pkcs11Signer != null)
            {
                // signed by a PKCS#11 device: the key never leaves it
                signature = new PdfDigitalSignatureElement(result.PdfPageLastRectangle, pkcs11Signer);
            }
            else if (DdlSigningMethod == "External")
            {
                // signed by an external signer: the key stays with the signer
                List<byte[]> certificates;
                ReadKeyAndCertificates(out certificates, out externalKey, out externalCertificates);
                signature = new PdfDigitalSignatureElement(result.PdfPageLastRectangle,
                    new KeySigner(certificates, externalKey));
            }
            else
            {
                // get the certificate
                PdfDigitalCertificate certificate = certificatesFromFile[0];
                signature = new PdfDigitalSignatureElement(result.PdfPageLastRectangle, certificate);
            }
            signature.Reason = "SelectPdf";
            signature.ContactInfo = "SelectPdf";
            signature.Location = "SelectPdf";

            // the background drawn behind the signature's text
            signature.SignatureBackgroundImage = BackgroundImage();
            signature.SignatureBackgroundOpacity = BackgroundOpacity();

            // long-term validation: when the document is saved, the revocation data (OCSP responses
            // or certificate revocation lists) of the certificate chain is obtained from the
            // addresses the certificates name and embedded in the document
            signature.EnableLtv = ChkEnableLtv;

            page.Add(signature);

            try
            {
                // save pdf document (it is signed now)
                return doc.Save();
            }
            finally
            {
                // close pdf document and release the stand-in key
                doc.Close();
                ReleaseKey(externalKey, externalCertificates);
            }
        }

        // Two steps: the document is prepared with an empty, reserved signature, the request is signed
        // elsewhere (a remote signing service, possibly much later), and the value is written into the
        // prepared document. Here both steps run in one request.
        private byte[] SignInTwoSteps(PdfDocument doc, PdfPage page,
            SelectPdf.Universal.Drawing.RectangleF bounds)
        {
            // the manager places the signature from the page corner, the element from inside the
            // page margins: move the rectangle by the margins so it still covers the logo
            bounds = new SelectPdf.Universal.Drawing.RectangleF(bounds.X + page.Margins.Left,
                bounds.Y + page.Margins.Top, bounds.Width, bounds.Height);

            byte[] unsigned = doc.Save();
            doc.Close();

            List<byte[]> certificates;
            AsymmetricAlgorithm key;
            X509Certificate2Collection loaded;
            ReadKeyAndCertificates(out certificates, out key, out loaded);

            // step one: reserve the signature and get the request to be signed
            PdfSecurityManager manager = new PdfSecurityManager();
            manager.Load(unsigned);
            manager.EnableLtv = ChkEnableLtv;   // not available in two steps: reported on the page
            manager.SignatureBackgroundImage = BackgroundImage();
            manager.SignatureBackgroundOpacity = BackgroundOpacity();
            PdfExternalSigningRequest request = manager.PrepareExternalSignature(certificates,
                "Signature1", "SelectPdf", "SelectPdf", "SelectPdf", 0, bounds);
            byte[] prepared = manager.Save();
            manager.Close();

            // elsewhere, later: the signing service signs the digest
            byte[] value = SignDigest(key, request.Digest, request.DigestAlgorithm);
            ReleaseKey(key, loaded);

            // step two: write the signature value into the prepared document
            return PdfSecurityManager.CompleteExternalSignature(prepared, null, "Signature1",
                certificates, request.DigestAlgorithm, value);
        }
    }
}