TX Text Control .NET for Windows Forms

Certificates

Topic

A digital signature is only as trustworthy as the certificate behind it. An X.509 certificate binds a public key to a person or an organization, and the certificate authority that issued it vouches for that binding. TX Text Control takes the certificate as a .NET X509Certificate2 object, so your Windows Forms application loads it with the APIs of .NET: from a .pfx file, or from the Windows certificate store, where certificates on smart cards and USB tokens appear as well. Loading a certificate both ways, letting the user choose one and checking it before signing are shown below.

A test certificate is created in Create a Self-Signed Certificate for Testing in Getting Started.

What a Signing Certificate Needs

  • A private key. The signature is created with the private key, so the certificate must come with it. A .pfx file, also called PKCS#12, contains the certificate and the key.
  • A certificate authority that recipients trust. Signatures that other people must trust need a certificate from a certificate authority (CA). A self-signed certificate is its own issuer, which makes it suitable for development, tests and internal documents only.
  • A valid period. PDF readers check whether the certificate was valid when the document was signed, so sign only with a certificate that hasn't expired. A trusted timestamp keeps the signature verifiable after the certificate expires, as Timestamp Configuration explains.

Samples of Text Control, such as the blog article on self-signed certificates below, load signing certificates with X509KeyStorageFlags.Exportable, and the test certificate of Getting Started is created with an exportable key. The samples below do the same.

How TX Text Control Takes the Certificate

A DigitalSignature represents a certificate for signing a PDF or PDF/A document. It has two constructors and no other members:

ConstructorUsed for
DigitalSignature(X509Certificate2 x509, string timeServerURL)Signing the whole document with SaveSettings.DigitalSignature
DigitalSignature(X509Certificate2 x509, string timeServerURL, string signatureFieldName)Signing one signature field with SaveSettings.SignatureFields

The constructors take no file names, passwords or store locations: loading the certificate is up to your code. The certificate is used when the document is saved, and if it's invalid or has no private key, Save throws a FilterException with the Reason InvalidCertificate.

Form1.cs needs these using directives for the samples:

using System.Security.Cryptography.X509Certificates;
using TXTextControl;

Load the Certificate From a PFX File

In .NET 9 and higher, X509CertificateLoader loads a .pfx file:

X509Certificate2 certificate = X509CertificateLoader.LoadPkcs12FromFile(
    "certificate.pfx", "123", X509KeyStorageFlags.Exportable);

In a .NET 8 project and in the .NET Framework, X509CertificateLoader doesn't exist. Use the constructor with the same arguments, which .NET 9 and higher mark as obsolete:

X509Certificate2 certificate = new X509Certificate2(
    "certificate.pfx", "123", X509KeyStorageFlags.Exportable);

A .pfx file and its password give anyone who has them the power to sign in your name. Don't ship them with the application or write the password into the code; keep them where only the people and computers that sign can read them, as Certificate and Private Key Handling describes.

Let the User Choose a Certificate

Users who sign with their own certificate have it installed in their personal certificate store. Windows offers a dialog box for choosing one, which .NET opens with X509Certificate2UI.SelectFromCollection. Text Control's blog article Sign PDF Documents with an X.509 Certificate on a Hardware Device uses this dialog box, which also lists certificates on smart cards and USB tokens.

private X509Certificate2 SelectCertificate()
{
    using (X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
    {
        store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);

        // Offer only certificates that are valid today
        X509Certificate2Collection validCertificates = store.Certificates.Find(
            X509FindType.FindByTimeValid, DateTime.Now, false);

        // The Windows dialog box for choosing a certificate, owned by this form
        X509Certificate2Collection selected = X509Certificate2UI.SelectFromCollection(
            validCertificates,
            "Sign Document",
            "Select the certificate to sign the PDF document with.",
            X509SelectionFlag.SingleSelection,
            Handle);

        foreach (X509Certificate2 certificate in selected)
        {
            if (certificate.HasPrivateKey)
            {
                return certificate;
            }
        }
    }

    return null;
}

private void signWithMyCertificateButton_Click(object sender, EventArgs e)
{
    X509Certificate2 certificate = SelectCertificate();

    if (certificate == null)
    {
        MessageBox.Show(this, "No certificate with a private key was selected.");
        return;
    }

    SaveSettings saveSettings = new SaveSettings()
    {
        DigitalSignature = new DigitalSignature(certificate, "")
    };

    // Let the user choose where to save the signed PDF
    textControl1.Save(StreamType.AdobePDF, saveSettings);
}

The code works as follows:

  • X509Store opens the personal store of the current user, the store that Personal > Certificates shows in the Certificate Manager, certmgr.msc.
  • Find with X509FindType.FindByTimeValid leaves out certificates that have expired or aren't valid yet.
  • SelectFromCollection shows the dialog box. Handle makes the form its owner, so the dialog box stays in front of it. The result is empty when the user cancels.
  • HasPrivateKey makes sure the chosen certificate can sign. TextControl.Save then opens the Save As dialog box and signs the file.

The dialog box shows the name, the issuer and the validity period of each certificate:

The Windows Security dialog box Choose your certificate, showing a self-signed certificate with its issuer and validity period.

Find a Certificate by Its Thumbprint

An application that signs with one known certificate, such as a company certificate installed on the signing computer, finds it by its thumbprint, for example one read from the configuration of the application:

private X509Certificate2 FindCertificate(string thumbprint)
{
    using (X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
    {
        store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);

        // false: also find certificates without a trusted chain, such as test certificates
        X509Certificate2Collection found = store.Certificates.Find(
            X509FindType.FindByThumbprint, thumbprint, false);

        foreach (X509Certificate2 certificate in found)
        {
            if (certificate.HasPrivateKey)
            {
                return certificate;
            }
        }
    }

    return null;
}

With true as the last parameter of Find, only valid certificates are returned. A certificate that was installed for all users of the computer is in StoreLocation.LocalMachine instead of CurrentUser, and the user of the application needs access to its private key.

Check a Certificate Before Signing

A check before signing turns a wrong or expired certificate into a clear message instead of an exception at save time:

private string CheckCertificate(X509Certificate2 certificate)
{
    if (!certificate.HasPrivateKey)
    {
        return "The certificate has no private key and can't sign.";
    }

    if (certificate.NotAfter < DateTime.Now)
    {
        return $"The certificate expired on {certificate.NotAfter:d}.";
    }

    if (certificate.NotBefore > DateTime.Now)
    {
        return $"The certificate is valid from {certificate.NotBefore:d}.";
    }

    return null;
}

private void checkCertificateButton_Click(object sender, EventArgs e)
{
    X509Certificate2 certificate = X509CertificateLoader.LoadPkcs12FromFile(
        "certificate.pfx", "123", X509KeyStorageFlags.Exportable);

    string problem = CheckCertificate(certificate);

    MessageBox.Show(this,
        $"Subject: {certificate.Subject}\nIssuer: {certificate.Issuer}\n" +
        $"Valid until: {certificate.NotAfter:d}\n\n{problem ?? "The certificate can sign."}");
}

Subject names the owner of the certificate and Issuer the certificate authority that issued it; for a self-signed certificate, both are the same. Adobe Acrobat Reader shows the same details for each signature in its Signatures panel.

Certificates for Development

Besides the PowerShell commands of Getting Started, two blog articles describe other ways to create a test certificate:

  • How to Create Self-Signed Certificates on Windows to Sign PDFs in .NET C# creates one with PowerShell and explains the parameters.
  • Sign Documents with a Self-Signed Digital ID From Adobe Acrobat Reader in .NET C# creates a digital ID as a .pfx file in Acrobat Reader.

When the certificate from a CA arrives, only the file or the certificate the code loads changes.

Next Steps