Add a Digital Signature to Generated PDF Document

EVO HTML to PDF Converter allows you to add digital signatures to the generated PDF document. In order to add digital signatures you need a certificate with private and public keys. These certificates are usually stored in a .pfx or a .p12 file in PKCS#12 format and they can be password protected. A digital signature is represented by a EvoPdf.NextPdfDigitalSignature object. An object of this type is exposed by the HtmlToPdfConverterDigitalSignature property of the HTML to PDF Converter class.

Optionally, the digital signature can have an appearance in a page of the generated PDF document. An appearance can contain an image and a text.

The digital signature has some properties like reason, location and contact info, that will be displayed in the signatures panel in Adobe Reader and also in the signature appearance in PDF if a custom signature text was not explicitly set.

The digital signatures created by EVO HTML to PDF Converter are PAdES signatures, the European standard for advanced electronic signatures in PDF documents. The signature level is selected with the PdfDigitalSignatureProfile property

  • PdfDigitalSignatureProfilePadesBaselineB. A basic signature that proves the integrity and the authenticity of the signed document. The signing time is declared by the signer

  • PdfDigitalSignatureProfilePadesBaselineT. Adds a trusted timestamp obtained from a timestamp server, which proves when the document was signed. Requires the PdfDigitalSignatureTimestampServerUrl property to be set with the URL of a timestamp server

  • PdfDigitalSignatureProfilePadesBaselineLT. Long-term validation. The document also carries the validation data of the certificates, so the signature can be verified without contacting any external service, even after the certificates expire. Signing requires network access, a timestamp server and a certificate issued by an authority that publishes revocation information

  • PdfDigitalSignatureProfilePadesBaselineLTA. Long-term validation with archive timestamp. Adds a document timestamp that seals the whole document, including the validation data, for archiving over long periods. The document timestamp can be renewed later with the PDF editor

The default value of the Profile property is PdfDigitalSignatureProfileAuto, which produces a PAdES B-T signature when a timestamp server URL is set and a PAdES B-B signature otherwise. The LT and LTA levels cannot be combined with document encryption. The signature uses the SHA-256 hash algorithm by default; SHA-384 and SHA-512 are selected with the PdfDigitalSignatureDigestAlgorithm property. When the signing certificate does not come with the certificates of its issuers, they can be given in the PdfDigitalSignatureTrustedCaCertificates property

Signature fields. The signature creates a signature field named by the PdfDigitalSignatureFieldName property, or with a predefined name when the property is not set. When the document already contains an empty signature field with that name, for example a form prepared with a field for each signer, the signature is placed in that field, on its page and in its rectangle.

Certification signatures. The signatures are approval signatures by default. The PdfDigitalSignatureCertificationLevel property turns the signature into a certification signature, which declares the changes the document accepts after signing: PdfCertificationLevelNoChanges, PdfCertificationLevelFormFillingAndSigning or PdfCertificationLevelFormFillingSigningAndAnnotations. PDF viewers show a certified document with the author's permissions and report any change outside them. A certification signature must be the first signature of the document.

Certificate objects. The private key does not have to be in a PFX file. A certificate from the Windows certificate store, a smartcard or a USB token is given as an X509Certificate2 object to the PdfDigitalSignaturePdfDigitalSignature(X509Certificate2) constructor. The private key stays in its store. RSA and ECDSA certificates are supported with all the signature levels. On Windows servers where the application runs without a user profile, such as IIS application pools and Azure App Service, load the PFX file into the X509Certificate2 object with the MachineKeySet key storage flag, as the demo application does; the PFX file option described above needs no such setting.

External signing. When the key is held in a hardware security module or a cloud key service such as Azure Key Vault or AWS KMS, implement the EvoPdf.NextIExternalDigitalSignature interface and create the signature with the PdfDigitalSignaturePdfDigitalSignature(IExternalDigitalSignature, Byte) constructor, which takes the implementation and the signer certificate chain. The implementation receives the data to sign and returns the signature value produced by the key service. All the signature levels are available with external signing.

The same digital signature object and the same signature levels are available when creating PDF documents from scratch with the PdfDocumentDigitalSignature property and when signing existing PDF documents with the PdfEditorDigitalSignature property, including documents that already carry signatures, which stay valid. The PDF editor can also add the long-term validation data to the signatures of an existing document, as described in the Digitally Sign Existing PDF topic. Digital signatures can be combined with the PDF/A and PDF/UA standards: the signed document keeps its conformance.

Code Sample - Add A Digital Signature to Generated PDF Document

C#
using System;
using System.ComponentModel.DataAnnotations;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Hosting;
using EvoPdf_Next_AspNetDemo.Models;
using EvoPdf_Next_AspNetDemo.Models.HTML_to_PDF;

// Use EVO PDF Namespace
using EvoPdf.Next;

namespace EvoPdf_Next_AspNetDemo.Controllers.HTML_to_PDF
{
    public class PDF_Digital_SignaturesController : Controller
    {
        private readonly IWebHostEnvironment m_hostingEnvironment;
        public PDF_Digital_SignaturesController(IWebHostEnvironment hostingEnvironment)
        {
            m_hostingEnvironment = hostingEnvironment;
        }

        public ActionResult Index()
        {
            var model = SetViewModel();
            return View(model);
        }

        [HttpPost]
        public ActionResult ConvertHtmlToPdf(PDF_Digital_Signatures_ViewModel model)
        {
            if (!ModelState.IsValid)
            {
                var errorMessage = ModelStateHelper.GetModelErrors(ModelState);
                throw new ValidationException(errorMessage);
            }

            // Set the license key received after purchase to use the library in licensed mode; leave it commented for demo mode
            // Licensing.LicenseKey = "your-license-key";

            // Create a HTML to PDF converter object with default settings
            HtmlToPdfConverter htmlToPdfConverter = new HtmlToPdfConverter();

            string certificateFilePath = m_hostingEnvironment.ContentRootPath + "/wwwroot" + "/DemoAppFiles/Input/Certificates/evopdf.pfx";

            if (model.KeySource == "Certificate")
            {
                // An X509Certificate2 with access to its private key: a certificate from the Windows certificate
                // store, a smartcard or a USB token. The private key stays in its store and the certificate
                // chain is built from the system stores. The sample loads the PFX as a certificate object
                X509Certificate2 certificate = LoadCertificate(certificateFilePath, "evopdf");
                htmlToPdfConverter.DigitalSignature = new PdfDigitalSignature(certificate);
            }
            else if (model.KeySource == "External")
            {
                // External signing: the library prepares the signature and asks an IExternalDigitalSignature
                // implementation for the signature value, so the private key can stay in a hardware security
                // module or a cloud key service. The signer certificate chain is given separately.
                // This sample implementation signs with the key of the PFX certificate to show the interface
                X509Certificate2 certificate = LoadCertificate(certificateFilePath, "evopdf");
                htmlToPdfConverter.DigitalSignature = new PdfDigitalSignature(new CertificateKeySignature(certificate), new[] { certificate.RawData });
            }
            else
            {
                htmlToPdfConverter.DigitalSignature = new PdfDigitalSignature(certificateFilePath, "evopdf");
            }

            // optionally set digital signature field name in PDF document
            htmlToPdfConverter.DigitalSignature.FieldName = "EvoPdf Signature Field";

            // set the digital signature information that will be displayed in the signatures panel in Adobe Reader
            // and also in the signature appearance in PDF if a custom signature text was not explicitly set
            htmlToPdfConverter.DigitalSignature.Reason = model.SignatureReason;
            htmlToPdfConverter.DigitalSignature.Location = model.SignatureLocation;
            htmlToPdfConverter.DigitalSignature.ContactInfo = model.SignatureContact;

            // The PAdES signature level. Auto produces PAdES B-T when a timestamp server is set and
            // PAdES B-B otherwise
            htmlToPdfConverter.DigitalSignature.Profile = model.Profile;

            // An approval signature by default; a certification signature declares the
            // changes the document accepts after signing: none, form filling and signing, or also annotations
            htmlToPdfConverter.DigitalSignature.CertificationLevel = model.CertificationLevel;

            // The timestamp server, for example http://timestamp.digicert.com
            if (!string.IsNullOrWhiteSpace(model.TimestampServerUrl))
                htmlToPdfConverter.DigitalSignature.TimestampServerUrl = model.TimestampServerUrl.Trim();

            // enable signature appearance in PDF page
            htmlToPdfConverter.DigitalSignature.AppearanceEnabled = model.EnableAppearance;

            if (htmlToPdfConverter.DigitalSignature.AppearanceEnabled)
            {
                // set the digital signature appearance position in PDF document
                if (model.DisplayOnLastPage)
                {
                    // set the appearance to be displayed at the bottom of the last page
                    htmlToPdfConverter.DigitalSignature.Appearance.DisplayOnLastPage = true;
                    htmlToPdfConverter.DigitalSignature.Appearance.BoundsRectangle =
                            new PdfRectangle(0, htmlToPdfConverter.PdfDocumentOptions.PdfPageSize.Height - 50, 200, 50);

                    // optionally reserve space for signature appearance at the bottom of the PDF page
                    htmlToPdfConverter.PdfDocumentOptions.BottomMargin = 50;
                }
                else
                {
                    // set the appearance to be displayed at the top of the first page
                    htmlToPdfConverter.DigitalSignature.Appearance.PageNumber = 1;
                    htmlToPdfConverter.DigitalSignature.Appearance.BoundsRectangle = new PdfRectangle(0, 0, 200, 50);

                    // optionally reserve space for signature appearance at the top of the PDF page
                    htmlToPdfConverter.PdfDocumentOptions.TopMargin = 50;
                }

                // set the signature text in appearance or leave it null or empty to display the default signature information
                if (model.AddSignatureText && !string.IsNullOrEmpty(model.SignatureText))
                    htmlToPdfConverter.DigitalSignature.Appearance.Text = model.SignatureText;

                // set the signature image in appearance
                if (model.AddSignatureImage)
                {
                    string imageFilePath = m_hostingEnvironment.ContentRootPath + "/wwwroot" + "/DemoAppFiles/Input/Images/evologo.png";
                    htmlToPdfConverter.DigitalSignature.Appearance.SetImage(imageFilePath, true);
                }
            }

            byte[] outPdfBuffer = null;

            if (model.HtmlPageSource == "Html")
            {
                string htmlWithForm = model.HtmlString;
                string baseUrl = model.BaseUrl;

                outPdfBuffer = htmlToPdfConverter.ConvertHtml(htmlWithForm, baseUrl);
            }
            else
            {
                string url = model.Url;

                outPdfBuffer = htmlToPdfConverter.ConvertUrl(url);
            }

            // Send the PDF file to browser
            FileResult fileResult = new FileContentResult(outPdfBuffer, "application/pdf");
            fileResult.FileDownloadName = "Digital_Signatures.pdf";

            return fileResult;
        }

        // Loads the sample PFX as a certificate object. On Windows the private key goes to the machine key
        // store, which needs no user profile, so the code runs in IIS application pools and in Azure App Service;
        // macOS does not support in-memory keys and uses its default key storage
        private static X509Certificate2 LoadCertificate(string certificateFilePath, string password)
        {
            X509KeyStorageFlags keyStorageFlags = OperatingSystem.IsWindows() ? X509KeyStorageFlags.MachineKeySet :
                OperatingSystem.IsMacOS() ? X509KeyStorageFlags.DefaultKeySet : X509KeyStorageFlags.EphemeralKeySet;
#if NET9_0_OR_GREATER
            return X509CertificateLoader.LoadPkcs12FromFile(certificateFilePath, password, keyStorageFlags);
#else
            return new X509Certificate2(certificateFilePath, password, keyStorageFlags);
#endif
        }

        private PDF_Digital_Signatures_ViewModel SetViewModel()
        {
            var model = new PDF_Digital_Signatures_ViewModel();

            var contentRootPath = m_hostingEnvironment.ContentRootPath + "/wwwroot";

            HttpRequest request = ControllerContext.HttpContext.Request;
            UriBuilder uriBuilder = new UriBuilder();
            uriBuilder.Scheme = request.Scheme;
            uriBuilder.Host = request.Host.Host;
            if (request.Host.Port != null)
                uriBuilder.Port = (int)request.Host.Port;
            uriBuilder.Path = request.PathBase.ToString() + request.Path.ToString();
            uriBuilder.Query = request.QueryString.ToString();

            string currentPageUrl = uriBuilder.Uri.AbsoluteUri;
            string rootUrl = currentPageUrl.Substring(0, currentPageUrl.Length - "PDF_Digital_Signatures".Length);

            model.Url = "http://www.evopdf.com";
            model.HtmlString = "Enter the <b>HTML String to Convert</b> and optionally set a <b>Base URL</b> if the HTML string references external resources by relative URLs";
            model.BaseUrl = rootUrl;

            model.SignatureReason = "My Signature Reason";
            model.SignatureLocation = "My Signature Location";
            model.SignatureContact = "My Contact Information";
            model.EnableAppearance = true;
            model.DisplayOnLastPage = false;
            model.AddSignatureText = true;
            model.SignatureText = "Signed by EVO PDF Software";
            model.AddSignatureImage = true;

            return model;
        }
    }

    // Sample IExternalDigitalSignature implementation: produces the RSA signature value with the
    // private key of an X509Certificate2. A production implementation forwards the message to the device or
    // service that holds the key and returns the signature value it produces
    public class CertificateKeySignature : IExternalDigitalSignature
    {
        private readonly X509Certificate2 m_certificate;

        public CertificateKeySignature(X509Certificate2 certificate)
        {
            m_certificate = certificate;
        }

        public string GetSignatureAlgorithmName()
        {
            return "RSA";
        }

        public byte[] Sign(byte[] message)
        {
            using (RSA rsa = m_certificate.GetRSAPrivateKey())
            {
                return rsa.SignData(message, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
            }
        }
    }
}

See Also