PKCS#11 session state

Hi,

How to examine that i have already logged session and prevent for CKR_USER_ALREADY_LOGGED_IN ?

I have Scoped service that in constructor is part of this code :

            pkcs11Module = new PdfPkcs11Module(LibraryPath); // always new instance, and true (default = true to multithread) 
            token = pkcs11Module.Tokens.Single(t => t.TokenLabel == TokenLabel); 
            token.Login(TokenPin); //but i got error in second get instance service

Net.Pkcs11Interop has the session object to determine to is open session and close and login again ex.

Regards
Pawel

Hi Paul,

According to the PKCS #11 specification:

Since all sessions an application has with a token have a shared login state, C_Login only needs to be called for one of the sessions.

This means that the scoped service is not “natively” scoped since the login state is shared between multiple sessions with the token.

The safest solution is to have the singleton PdfPkcs11Module instance and use it and its descendant members under the lock from multiple scoped services / multiple threads.

Regards,
Stipo

Hi,

Im tested Itext8/9 a few months ago and i dont remember this problem - i can stress the HSM or SoftHSM2 with multiple connections (session) to more than 1 tokens (slots) concurent.
Slots has maybe diffrents pins.

Make one or more calls to C_OpenSession to obtain one or more sessions with the token.

PS. the token.logout before token.login is the same not working.

token
{Token Label: FakeToken01, Status: Logged in}
    DigitalIds: Count = 4
    Model: "SoftHSM v2"
    ModuleManufacturerId: "SoftHSM project"
    SerialNumber: "3404311a63e73b2c"
    TokenLabel: "FakeToken01"

how i can get the : Status: Logged in from code ?

P.

Hi Paul,

I don’t know how exactly iText handles this, but using the PKCS #11 device is inherently not thread-safe for all possible scenarios.

Imagine multiple threads, logging in to the same token, using the private key (that becomes accessible only after the successful login) to sign some data, and logging out from the session/token like in the following example:
Thread 1: log in →
Thread 2: log in already done, skip it →
Thread 1: retrieve private key handle →
Thread 2: retrieve private key handle →
Thread 1: sign data using private key handle →
Thread 1: log out →
Thread 2: sign data using private key handle (error happens here because private key handle is invalid after log out).

Even if you try to check the login status and log in again before signing data in Thread 2, the issue might still happen because PKCS #11 doesn’t have an atomic thread-safe operation: “Log in if needed, retrieve private key handle, and sign data with it.”

Some scenarios, such as signing the data (without concurrent login and logout) would probably work in multiple threads, even on the same token, but login and logout must be done non-concurrently as a part of the global initialization and global finalization (global in the context before and after concurrent usage).

PdfPkcs11Token.Login and PdfPkcs11Token.Logout methods use the private state on the PdfPkcs11Token instance to check if the login was already done or not and PdfPkcs11Token.ToString method uses this private state to output the log-in status. It is done like this because PdfPkcs11Token.DigitalIds must be reconstructed after each PdfPkcs11Token.Login and PdfPkcs11Token.Logout because digital IDs consist of X.509 certificate handles, public key handles, and private key handles. While X.509 certificate handles and public key handles are always retrievable regardless of the log-in status, private key handles are retrievable only while logged in.

Therefore, the solution with the global login to the PdfPkcs11Token instance, concurrent signing using that PdfPkcs11Token instance, and global logout from that PdfPkcs11Token instance would probably work even without the lock (global in the context before and after concurrent usage).

Regards,
Stipo

Thanks Stipo for reply!

ok, thats correct - once login to token, but this token could many concurents sessions. One sesion and One CryptoOperation in the same time…

Regards
Pawel

Hi Paul,

We extended the GemBox.Pdf API to support concurrent cryptographic operations on the same Cryptoki device/token.

After reviewing the PKCS #11 specification and the Pkcs11Interop code, we have concluded that the workflow should be like this:

  1. Create a singleton instance of the PdfPkcs11Module instance just once in your application/process for a particular PKCS#11 module (usually a .dll).
  2. Identify the PdfPkcs11Token that contains the private key that will sign PDF documents.
  3. Log in to that “global” token.
  4. Pass that “global” token to each concurrent signer. On a new thread, call the PdfPkcs11Token.Clone() method on the “global” token and use the returned “local” PdfPkcs11Token instance. Retrieve the digital ID from the “local” token. Sign the PDF document using the retrieved digital ID. Dispose the “local” token.
  5. Log out of the “global” token.
  6. Dispose the singleton PdfPkcs11Module instance.

Newly extended XML documentation (yet to be uploaded on the web) of PdfPkcs11Module, PdfPkcs11Token, PdfPkcs11Token.Login(String), and PdfPkcs11Token.Clone() explains why this workflow is required.

After our CI server produces artifacts with the new functionality, my colleague will send you a link to the latest version of GemBox.Pdf and will provide you with an example code on how to use the new functionality. The example code uses SoftHSM like the example PDF digital signature with PKCS#11 (Cryptoki) device in C# and VB.NET.

Regards,
Stipo

Hi Pawel,

Please try again with this NuGet package:

Install-Package GemBox.Pdf -Version 2025.7.103

Note that this is a hidden (unlisted) version. To install it, you’ll need to run the above command on the NuGet Package Manager Console (Tools → NuGet Package Manager → Package Manager Console).

Also, try running the following:

using System;
using System.IO;
using System.Linq;
using System.Threading;
using GemBox.Pdf;
using GemBox.Pdf.Forms;
using GemBox.Pdf.Security;

static class Program
{
    static void Main()
    {
        var desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
        if (!Directory.Exists(Path.Combine(desktop, "SignedFiles")))
            Directory.CreateDirectory(Path.Combine(desktop, "SignedFiles"));

        // Extract GemBoxPkcs11SoftwareModule from ZIP archive and setup environment variable with path to configuration file.
        // Required only for SoftHSM device used in this example. Not required for yours PKCS#11/Cryptoki device.
        var pkcs11SoftwareModuleDirectory = "GemBoxPkcs11SoftwareModule";
        if (!Directory.Exists(pkcs11SoftwareModuleDirectory))
            System.IO.Compression.ZipFile.ExtractToDirectory(Path.Combine(desktop, "GemBoxPkcs11SoftwareModule.zip"), pkcs11SoftwareModuleDirectory);
        Environment.SetEnvironmentVariable("SOFTHSM2_CONF", Path.Combine(pkcs11SoftwareModuleDirectory, "softhsm2.conf"));

        var errors = SignConcurrent(
            20,
            Path.Combine(desktop, "Reading.pdf"),
            Path.Combine(desktop, "SignedFiles"),
            "File",
            Path.Combine(pkcs11SoftwareModuleDirectory, IntPtr.Size == 8 ? "softhsm2-x64.dll" : "softhsm2.dll"),
            "GemBoxECDsaToken",
            "GemBoxECDsaPin",
            "GemBoxECDsa521",
            "GemBoxECDsa");

        if (errors == null)
            Console.WriteLine("No errors!");
        else
            Console.WriteLine("There were errors while doing concurrent signing.");
    }

    private static Exception[]? SignConcurrent(int signerCount, string inputPdfPath, string outputPdfDirectory, string outputPdfFilePrefix, string libraryPath, string tokenLabel, string pin, string digitalIdSubjectCommonName, string? intermediateCASubjectCommonName = null)
    {
        ConcurrentSigner[] signers;

        using (var pkcs11Module = new PdfPkcs11Module(libraryPath))
        {
            var token = pkcs11Module.Tokens.Single(t => t.TokenLabel == tokenLabel);

            token.Login(pin);

            signers = Enumerable.Range(0, signerCount).Select(i => new ConcurrentSigner(inputPdfPath, Path.Combine(outputPdfDirectory, outputPdfFilePrefix + i + ".pdf"), digitalIdSubjectCommonName, token, intermediateCASubjectCommonName)).ToArray();
            Array.ForEach(signers, s => s.Prepare());

            var threads = Array.ConvertAll(signers, s => new Thread(s.Sign) { IsBackground = true });

            Array.ForEach(threads, t => t.Start());

            Array.ForEach(threads, t => t.Join());

            token.Logout();
        }

        var errors = Array.ConvertAll(signers, s => s.Error);
        return Array.TrueForAll(errors, e => e == null) ? null : errors;
    }

    class ConcurrentSigner
    {
        private readonly string inPath, outPath, digitalIdSubjectCommonName;
        private readonly PdfPkcs11Token globalToken;
        private readonly string? intermediateCASubjectCommonName;

        private PdfDocument document;

        public Exception? Error { get; private set; }

        public ConcurrentSigner(string inPath, string outPath, string digitalIdSubjectCommonName, PdfPkcs11Token globalToken, string? intermediateCASubjectCommonName = null)
        {
            this.inPath = inPath;
            this.outPath = outPath;
            this.digitalIdSubjectCommonName = digitalIdSubjectCommonName;
            this.globalToken = globalToken;
            this.intermediateCASubjectCommonName = intermediateCASubjectCommonName;
        }

        public void Prepare()
        {
            this.document = PdfDocument.Load(this.inPath, new PdfLoadOptions() { ReadOnly = true });
        }

        public void Sign()
        {
            // Finish signing of a PDF file.
            using (this.document)
            using (var token = this.globalToken.Clone())
                try
                {
                    // Add a visible signature field to the first page of the PDF document.
                    var signatureField = this.document.Form.Fields.AddSignature(this.document.Pages[0], 300, 500, 250, 50);

                    // Get a digital ID from PKCS#11/Cryptoki device token.
                    var digitalId = token.DigitalIds.Single(id => id.Certificate.SubjectCommonName == digitalIdSubjectCommonName);

                    // Create a PDF signer that will create the digital signature.
                    var signer = new PdfSigner(digitalId);

                    if (this.intermediateCASubjectCommonName is not null)
                    {
                        // Adobe Acrobat Reader currently doesn't download certificate chain
                        // so we will also embed certificate of intermediate Certificate Authority in the signature.
                        // (see https://community.adobe.com/t5/acrobat/signature-validation-using-aia-extension-not-enabled-by-default/td-p/10729647)
                        var intermediateCA = token.DigitalIds.Single(id => id.Certificate.SubjectCommonName == this.intermediateCASubjectCommonName).Certificate;
                        signer.ValidationInfo = new PdfSignatureValidationInfo(new PdfCertificate[] { intermediateCA }, null, null);
                    }

                    // Initiate signing of a PDF file with the specified signer.
                    signatureField.Sign(signer);

                    this.document.Save(this.outPath);

                    this.Error = null;

                    Console.WriteLine($"Signed '{this.outPath}'.");
                }
                catch (Exception ex)
                {
                    this.Error = ex;

                    Console.WriteLine($"Error while signing '{this.outPath}'.");
                }

            this.document = null;
        }
    }
}

Does this solve your issue?

Regards,
Mario

Thanks a lot !

I will implemented new version of Yours library and i will check this solution !

Regards
Pawel

Thanks ! all works ok (on Windows)

I will test it on Linux Docker yet

Regards
Pawel