This C# sample signs a PDF document with a private key stored in a Hardware Security Module (HSM), such as Thales / Gemalto Luna, Utimaco, Entrust nShield or YubiHSM, or on a cryptographic token, through the PKCS#11 module of the device. The private key never leaves the HSM: SignLib prepares the data to sign and the HSM creates the signature value.

How it works

  1. The class Pkcs11ExternalSignature, included in the sample, implements IExternalSignature. It loads the PKCS#11 module (for example cryptoki.dll of the Luna client or eTPKCS11.dll of a SafeNet eToken) with the NuGet package Pkcs11Interop, opens a session on the slot, logs in with the PIN and finds the private key by its label (CKA_LABEL) or through the certificate stored on the device.
  2. The PDF signature gets only the public part of the certificate (a .cer file): the HSM does not even need to store the certificate.
  3. DigitalCertificate.UseExternalSignatureProvider binds the provider to the next signature; the library calls ApplySignature, which signs with RSA (PKCS#1 v1.5) or ECDSA inside the HSM.

C# code: sign a PDF with the HSM

/// <summary>
/// Signs a PDF file with the private key stored in an HSM.
/// </summary>
static void SignPdfWithHsm(string unsignedPdfFile, string signedPdfFile)
{
    PdfSignature pdfSignature = new PdfSignature(LibrarySerialNumber);

    pdfSignature.LoadPdfDocument(unsignedPdfFile);

    pdfSignature.SigningReason = "I approve this document";
    pdfSignature.SigningLocation = "Accounting department";
    pdfSignature.SignaturePosition = SignaturePosition.TopLeft;

    pdfSignature.HashAlgorithm = SignLib.HashAlgorithm.SHA256;

    Pkcs11ExternalSignature externalSignature = new Pkcs11ExternalSignature(Pkcs11LibraryPath, TokenPin, SlotIndex, PrivateKeyLabel);

    // The certificate is loaded from a file, not from the HSM: only the public part of the certificate is needed.
    externalSignature.SigningCertificate = new X509Certificate2(File.ReadAllBytes(SampleFile(CertificateFile)));

    // The library receives only the public part of the certificate: the private key stays in the HSM.
    pdfSignature.DigitalSignatureCertificate = externalSignature.SigningCertificate;

    // Bind the external signature provider to the next signature. The provider is released by the library at the end
    // of the signing operation, so it must be set again before every signature.
    DigitalCertificate.UseExternalSignatureProvider = externalSignature;

    File.WriteAllBytes(signedPdfFile, pdfSignature.ApplyDigitalSignature());

    Console.WriteLine("The PDF signature was created." + Environment.NewLine);
}

C# code: the PKCS#11 signature provider

The method that the library calls. The complete class (the slot, the login, the search of the private key, the RSA and ECDSA mechanisms, and the list of the certificates of the token) is in the project.

/// <summary>
/// Signs a message with the private key of the signing certificate: the token hashes the message (or the hash is
/// computed here, when the token cannot do it) and signs the hash.
/// </summary>
/// <param name="message">The data to sign (SignLib decides what has to be signed). It is not a hash.</param>
/// <param name="hashAlgorithm">The OID of the hash algorithm requested by SignLib.</param>
/// <returns>The signature value: PKCS#1 v1.5 for RSA, or the raw value (r || s) for ECDSA: SignLib converts it
/// to the form required by the signature format.</returns>
public byte[] ApplySignature(byte[] message, Oid hashAlgorithm)
{
    if (SigningCertificate == null)
        throw new InvalidOperationException("The signing certificate is not set.");

    try
    {
        Pkcs11InteropFactories factories = new Pkcs11InteropFactories();

        using (IPkcs11Library pkcs11 = factories.Pkcs11LibraryFactory.LoadPkcs11Library(factories, _pkcs11LibraryPath, AppType.MultiThreaded))
        {
            ISlot slot = GetSlot(pkcs11);

            using (ISession session = slot.OpenSession(SessionType.ReadOnly))
            {
                LoginUser(session);

                IObjectHandle privateKey = FindPrivateKey(factories, session);

                string keyAlgorithm = SigningCertificate.PublicKey.Oid.Value;

                if (keyAlgorithm == RsaKeyOid)
                    return SignWithRsaKey(factories, session, privateKey, message, hashAlgorithm.Value);

                if (keyAlgorithm == EcKeyOid)
                    return SignWithEcdsaKey(factories, session, privateKey, message, hashAlgorithm.Value);

                throw new NotSupportedException("The key algorithm of the certificate is not supported: " + keyAlgorithm);
            }
        }
    }
    catch (Exception ex)
    {
        throw new CryptographicException("PKCS#11 signature cannot be performed: " + ex.Message, ex);
    }
}

Set the path of the PKCS#11 module, the PIN, the slot, the key label and the certificate file in the constants of the sample. A 64-bit application needs the 64-bit PKCS#11 module. The same provider signs CAdES, XAdES and ASiC-E signatures, and the download also contains PKCS#11 samples for smart cards (PDF and CAdES). For keys in the cloud, see Azure Key Vault HSM.

Run the full sample. The complete Visual Studio project PDF Digital Signature with PKCS11 Driver for HSM is included in the library download, in the folder NET Sample Projects (.NET 8, .NET 9 and .NET 10) and in NET Framework Sample Projects (.NET Framework 4.6.2), with the test files and a test certificate. Without a serial number the library runs in demo mode. Download the library and the samples · User manual

Frequently asked questions

Can I create qualified electronic signatures with an HSM?

Yes, when the HSM is certified as a QSCD (qualified signature creation device) and the certificate is a qualified certificate. Add a time-stamp and the validation data (PAdES-LTA) for long-term validation.

Why PKCS#11 instead of the Windows certificate store?

Many HSMs and tokens do not publish their keys in the Windows store, or the application runs on Linux. PKCS#11 talks directly to the module of the device, with the same code on every platform.

Can several documents be signed in one session?

Yes. Set the provider before every signature; the provider of the sample opens a session for each signature, which you can change to keep the session open in a batch.