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); } } }