Why this guide exists
Estimated reading time: 20 minutes
Estimated writing time: took us around 2 days, including the simulator code
There exist few protocols as obscure yet so relied-upon as PKCS#11. While cryptographic interfaces come and go, Lindy's law applies strongly to the case of PKCS#11 (Lindy's law: the future lifespan of a protocol/tool is directly proportional to the duration of its historical use). We're confident in predicting that PKCS#11 will still be widely used 20 years from now.
We wrote this guide for a couple of reasons. One of them, in a spirit of transparency, is that we sell
a software tool relating to PKCS#11 implementations. But the other reason is that no such guide
exists and the problems, gotchas, edge cases etc explored in this guide are the hard-earned lessons from
much time spent writing and debugging PKCS#11 issues. We certainly won't have been alone in the issues
encountered and, by providing a practical, easy-to-read guide, we hope to save other implementers and
maintainers time,
frustration and tears.
What is PKCS#11?
PKCS#11 or, to give its full name, Public Key Cryptography Standard number 11, is a standard which defines an API used to interact with Hardware Security Modules (HSMs) and smart cards. Fundamentally, it's a standard which governs C interfaces which can be used to interact with these devices; to create/modify/destroy cryptographic objects, to perform cryptographic operations etc. For this guide, we'll largely focus on HSMs rather than smart cards: however much of the content applies to both.
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ │
│ ╭─────────────────────────╮ │
│ │ Client Application │ │
│ ╰─────────────────────────╯ │
│ ──────────────────────────── PKCS#11 interface │
│ ╭─────────────────────────╮ │
│ │ PKCS#11 Module │ │
│ ╰─────────────────────────╯ │
│ ──────────────────────────── Proprietary, vendor-specific interface + protocol │
│ ╭─────────────────────────╮ │
│ │ HSM │ │
│ ╰─────────────────────────╯ │
│ │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
PKCS#11 is the de-facto standard for using such devices but, as we'll see the term `standard' when applied to PKCS#11 requires a certain relaxation of the common meaning of the word. However, before developing the answer to ``what is PKCS#11'' further, we need to ensure we've introduced HSMs first.
What is a HSM?
Cryptographers (and your author holds a Ph.D. in cryptography) spend a lot of time thinking about and testing the best designs for ciphers, signature schemes etc, making sure that we can prove that they're statistically safe and that an attacker would need to burn planet-scale electricity for a century to brute force a particular key. While this is all well and good and certainly essential, the real world has an unreasonable tendency to bypass the assumed attack models and undermine them in ways that are not in scope of the theoretical threat models (immortalized in the well-known XKCD comic concerning $5 wrench ``cryptanalysis'').Real-world use of cryptography introduces complications such as side channels (e.g. it might take me 1,000,000 years to break your key, but if I attach a probe to your CPU and analyze certain timing traces, I can crack it in 5 minutes) and, at a more mundane level, the problem of keys being copied, cloned or otherwise stolen. What use is a strong, quantum-resistant, side channel resistant ML-DSA key used for signing payment authorizations if Eve from accounts copies it onto a USB drive and uses her copy to sign a transfer for $10MM to her Cayman Islands account?
These real-world unpleasantries such as keys being cloned by corrupt employees or a rogue sysadmin attaching a probe to reconstruct a key bit by bit fall squarely outside the theoretical threat models and require more ``physical'' solutions such as access control and physical isolation.
Fundamentally, the need for HSMs arises from three requirements:
- The need to protect keys from outsiders
- The need to protect keys from corrupt insiders
- The need to know, with iron-clad assurance, how a key has been used and, by extension, how many copies of a key exist and where they are
It's easy to assume that the first requirement (protection against hoodie-wearing hackers) is paramount but, requirements 2 and 3 are the principal requirements. Protection against outsiders is ``table stakes'' even for keys which reside outside of hardware protections: instead, it's the insider threat and knowing where a key is and how it's being used that are the real causes leading to HSMs.
So, if protecting keys against insiders and probe-wielding admins is the goal, how does a HSM achieve this? The answer, put as simply as possible, is to make sure the keys are generated on-device and that they never leave. This is enforced by the HSM having an electric `fence' around it so that if our probe-wielding admin tries to reach through the fence, he'll touch the fence and the resulting "shock" will lead to the keys disappearing before he can reach them. This, of course, needs to be coupled with logical access controls that control access to the keys; record who's been using them and, crucially, ensure that there are no software routes by which the key itself can be extracted from the device (though this has many nuanced exceptions in practice).
That's what it really reduces to: robust access control and an electrified cage that makes the keys vanish if you try to reach through it (Of course, this is simplified - there are many other potential attack vectors such an plunging the device into liquid nitrogen and trying to get the keys out of RAM via a cold-boot attack, TEMPEST emission attacks and many others, but this simplified explanation suffices for a guide like this).
The different types of HSMs
HSMs come in a number of different flavours, but, for our purposes, we'll separate them into general-purpose and payment HSMs. Payment HSMs often share the same hardware as general-purpose HSMs, but use very different protocols. We won't go into further detail here, instead focusing on the general purpose HSMs which generally do support PKCS#11.Even then, PKCS#11 is not the only way of using HSMs. Usually, the manufacturer of the HSM (Thales, Entrust, Utimaco etc) will provide a PKCS#11 interface module but, between that module and the HSM itself, a proprietary protocol will be used. In some cases, PKCS#11 is bypassed and the proprietary protocol used directly for performance reasons, but PKCS#11 remains the principal method of use.
It's also worth explaining here that PKCS#11 isn't always `visible' Using a HSM from e.g. OpenSSL and Java JCE commonly involves the use of a C PKCS#11 module ``under the hood'' (Microsoft CNG is distinct).
╭───────────────────────────────────────────────────────────────────────────────────────────────╮
│ │
│ ╭──────────────────────────────────────────────────────────────────────────────────╮ │
│ │ Client Application │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────╯ │
│ ╭─────────────────────────╮ ╭─────────────────────────╮ ╭─────────────────────────╮ │
│ │ OpenSSL │ │ JCE │ │ Microsoft CNG │ │
│ ╰─────────────────────────╯ ╰─────────────────────────╯ ╰─────────────────────────╯ │
│ ╭──────────────────────────────────────────────────────╮ ╭─────────────────────────╮ │
│ │ PKCS#11 Module (usually) │ │ Provider │ │
│ ╰──────────────────────────────────────────────────────╯ ╰─────────────────────────╯ │
│ ──────── Proprietary, vendor-specific interface + protocol ──────── │
│ ╭──────────────────────────────────────────────────────────────────────────────────╮ │
│ │ HSM │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────╯ │
│ │
╰───────────────────────────────────────────────────────────────────────────────────────────────╯
A note on certifications and software ``HSMs''
While PKCS#11 is a ``standard'' at the interface layer, the hardware needs to meet certain standards to be properly called a HSM. These standards (FIPS 140-3 being the main one) are expensive to achieve and contribute to the high cost of HSMs. We won't go into further details.Finally, it's also worth mentioning common points of confusion. It's common for people to become confused as to what is an HSM and what isn't. The below are a few of the confusions we've seen in the past:
- What about a key manager? Is it an HSM?
- What about a key manager which uses a HSM as a root of trust?
- What about a confidential computing enclave, e.g. Intel SGX?
- I can grab SoftHSM from Github: it's got HSM in the name, right?
- What about putting a software HSM in an enclave?
- What about a cloud HSM?
- What about threshold cryptography: does that give equivalent protections to a HSM?
The answer to all these questions, with the exception of ``What about a cloud HSM?'', is no. Cynically, a potentially easier method of determining what is and what isn't a HSM with high throughput and active protection is to ask ``does it cost less than $30,000?'' (or per year if it's in the cloud). If so, it's very unlikely to be an enterprise HSM.
Sounds expensive and exciting. So what does an HSM look like?
A HSM generally looks like a regular PCI card or a standard 1U rack server. The electrified fence is typically a fine mesh embedded in epoxy. There have been some more exotic proposals such as parts of the HSM rotating at high speed to stop a physical attacker attaching a probe to the chips, but, in general, HSMs are not very photogenic. Here's an example of one from our QA lab:
So, what is PKCS#11?
With the background in place, PKCS#11 is the API which allows a client (external to the HSM) to manage and use objects which are contained inside the HSM. For example, to ask the HSM to generate a key; to ask it to use that key to encrypt some data; to sign a certificate; to derive another key etc. The API couples this cryptographic functionality with certain access control features, allowing some level of authentication and authorization to be performed by the HSM before performing operations.A brief history of PKCS#11
PKCS#11 dates back to 1994/5, originating from RSA Security, along with the other PKCS standards. v2.20 was released in 2004. The OASIS project adopted it in 2013 and formed the PKCS#11 technical committee, with v2.40 being released in 2015. The next big jump occurred in 2020 with the jump from 2.40 to 3.0, with 3.2 having been released recently in 2026.The core conventions, functions and patterns have been largely consistent from 1995 onwards, with the majority of the changes over 30 years being around algorithms, multi-threading and the like.
Versions of PKCS#11 and their uptake
The versions we've mentioned (2.20, 2.40 and 3.x) are the major ones, with the largest vendor (Thales) still using v2.20 (2004) for the PKCS#11 interface of their Luna HSMs (note: we're just using Thales as an example here: the other vendors are also at varying stages of adopting newer versions, and there are good reasons for vendor adoption being behind the standards).Why haven't they moved past the 2004 version of the standard? Two reasons exist: 1) They don't need to and 2) HSM vendors are very conservative.
To understand why not keeping up with the latest version of the standard is fine from the vendor perspective, it's necessary to understand that PKCS#11 is something of a ``barebones'' standard: it defines the structure, (some of) the roles, the patterns, (some of) the algorithms and the conventions to follow. Outside that, there's something of a free for all.
All the HSM vendors largely maintain compliance to the core standard, but they have all extended it in multiple ways. They've generally added algorithms, roles, crypto functions to suit their needs. Stepping up a level, this is certainly no bad thing: a standard which is overly restrictive would be of no use. Keeping the core structure common while allowing extensibility is probably the only way to maintain any commonality. It must also be borne in mind that the HSM world is fiercely competitive: the ability to effortlessly switch between HSM vendors isn't desirable for any of the vendors, yet having a common protocol certainly is, from a skills and certification standpoint.
In short, PKCS#11 is a ``standard'' with many flavors, proprietary extensions and other oddities, as we'll see below. It's probably fair to say that nobody loves or even likes PKCS#11, yet it bears the hallmarks (or scars) of practical realities and compromises and has certainly stood the test of time as a baseline.
As a quick illustration of the widely-extended nature of PKCS#11, v2.40 of the standard defines 68 functions. The current Thales Luna PKCS#11 module has 299 vendor-specific extensions. Entrust treats this somewhat differently, treating PKCS#11 as a wrapper around their nCore API, hence the equivalent vendor-specific functionality is available through nCore rather than through PKCS#11 extensions, but the extent of the custom functionality is similar to the Thales/Luna case. The Utimaco case (CryptoServer) is similar to the Entrust case, with PKCS#11 treated as a wrapper of their proprietary API.
Thus, in all these cases, the core PKCS#11 standard covers the common features, the patterns and the conventions, but every vendor has significant extensions beyond the core.
Different conventions among vendors - the non-standard standard
At the individual function/key level, the core standard leaves quite a bit of freedom to the implementer of the interface, the most frequent cause of issues being the particular combination of attributes needed to generate a particular type of key. In general, each vendor has their own requirements for which attributes need to be provided, making even simple code for generating a key often non-portable between modules. This will be covered in more detail in the following section.Vendor compliance to the standard
A final note is in order regarding vendor compliance to the standard. Here, the reader may be surprised to learn that even among the <100 core functions, full compliance of commercial modules doesn't hold as a rule. Indeed, there has been at least one attempt to sell commercial tools for checking compliance to the core standard.Common issues and solutions
This section forms the bulk of the (hopefully) useful content of this guide, running through common pitfalls, mistakes, misconceptions and their solutions.To begin with, we need to first define a few basic terms that, hopefully, are already familiar to the reader.
- Tokens and Slots - the use of the work ``Token'' in PKCS#11 is something of a historical artifact, stemming from the historical use cases of ``Tokens'' (think smartcards or replaceable storage) being plugged in and out of physical slots. In the HSM case, the picture can be a bit confusing, since there's generally only a single slot and token, but the PKCS#11 interface may tell you otherwise. Some vendors exposing only a single slot and token and others using the model to define multiple logical slots and partitions while the underlying hardware has only one physical slot/token. The model can be further complicated by vendors overloading the terms, e.g. by introducing ``partitions'' in the Thales/Gemalto Luna case, which largely map to logical tokens. One of the common points of confusion is the mapping between the two and whether the relationships between slots and tokens is dynamic or not.
- Sessions - a Session in PKCS#11 is a context in which requests take place. Logically, it can be thought of as loosely equivalent to a TLS session: it needs to be established before most sensitive operations take place; it has a finite duration; it has a state. Just like with TLS, there are certain operations which can be performed outside a session (e.g. querying API versions etc), but, as with the TLS analogue, a session target needs to be specified, authentication needs to take place etc. The main mental shift that needs to occur with PKCS#11 sessions is that they're stateful and that any objects, handles etc used in one session cannot be transferred or reused in another.
- Handle - a Handle in PKCS#11 can be thought of as a pointer, either a pointer to an ephemeral session or a pointer to an object (e.g. a key). One of the earliest points of confusion is that handles are maybe better thought of as pointer to pointers, i.e. while an application might report ``rsa-signing-key-49 can be found using handle 12'', a subsequent session could report ``rsa-signing-key-49 can be found using handle 812'', with handle 12 in this session actually pointing to an AES key instead. In short, a handle has only a limited window of validity, usually limited to the particular session. If the session changes, handles should never be reused and instead fresh searches for objects should be carried out within the context of the new session.
Introductions are now complete and we're ready to start getting into the details of what goes wrong with PKCS#11 in practice.
Issue 1: Mixing threads and sessions
Version 1.0 of the standard (1995) makes no mention of multi-threaded applications or mutexes for obvious reasons (i.e. multiprocessing CPUs weren't in widespread use). If your application is single threaded, the following won't apply. For multi-threaded applications, the golden rule is that a given session must never be accessed by multiple sessions concurrently. Sessions and threads should always be kept in a one-to-one correspondence, with attempts to share session state between sessions usually ending in disaster.Multiple reasons exist for this rule, but one reason is that it allows reasonable implementation models, balancing throughput with consistency. If this rule were not in place, then any module supporting multithreaded access would need to guarantee thread-safe management for all internal structures, temporary data stores etc, along with needing to migrate state between threads as needed. It would lead to increased complexity and reduced throughput on the module and HSM side, i.e. much better avoided by imposing reasonable restrictions on the session and thread model. The absence of this rule would also complicate the communication between the module and HSM. A further reason is that if multiple threads were allowed to use the same session, a large number of undefined cases would be possible such as one thread calling C_EncryptUpdate at the same time as another thread calling C_EncryptFinal.
A helpful mental picture here is to view a session as a stateful object which corresponds to a state machine on the HSM. Each session is inherently serial, passing messages back and forth to the HSM one by one. Multi-threaded access to this state machine is likely to violate the serial nature, leading to crashes.
Every time we call C_OpenSession, we're reminded of the serial nature of sessions since the CKF_SERIAL_SESSION flag must always be set in the flags passed to C_OpenSession.
Telling the module what to prepare for
Now, even though it's the responsibility of the application writer to confine sessions to individual threads, the PKCS#11 modules typically still need some level of internal thread safety to synchronize state which lives outside the session model. And PKCS#11 provides a method for us to inform the module what to expect and what tools it can use to safeguard internal state. When C\_Initialize is called, we can provide flags to the module, namely:- CKF_LIBRARY_CANT_CREATE_OS_THREADS
- CKF_OS_LOCKING_OK
Issue 1A: Thread affinity in the case of Go
While this golden rule works well in the case of C threads (i.e. real threads), in the case of higher level languages like Go the situation is more nuanced. In Go, the concept of ``goroutines'' largely replaces threads, with a goroutine being a thread-like abstraction where many goroutines are multiplexed onto a single thread. In most cases, this presents no issues, since the correspondence is many goroutines sharing a single thread, which still complies with the golden rule. In some cases, however, issues do arise, namely in the case of the Go runtime.In go, the runtime maintains a set of threads and goroutines are 'mapped' onto these threads, so, for example, you might have 4 threads but 40 goroutines mapped onto them. This doesn't immediately cause any problems with PKCS#11, since there's no possibility of the same session being used by two threads. What can cause issues, however, is the case of the go runtime moving goroutines between the underlying threads. In certain cases, if the underlying PKCS#11 module is using thread-local storage (TLS), it may store session-related information in TLS. Now, if a long-running session is moved to another thread by the go runtime, it may be the case that the underlying module is presented with TLS belonging to another session, leading to potential corruption, segmentation faults or crashes when it tries to use it. These cases can be very difficult to diagnose in practice and are relatively rare occurrences. But they can and do occur when using common go-to-C PKCS#11 wrappers.
To avoid these cases, it's necessary to lock the session to a given go runtime thread for the duration of the session, e.g.:
runtime.LockOSThread()
defer runtime.UnlockOSThread()
// Open session and perform session work
Rule - when using PKCS#11 via go, lock sessions to runtime threads to avoid potential thread-local storage issues in the underlying module
Issue 2: Choosing the right number of sessions
If your application is using a HSM only a few times a day/hour, maybe for acting as a root of trust, performing occassional signatures or unwrapping a root database key, then latency and throughput aren't going to be of much concern. In these cases, it's standard practice to open a session, login, perform the crypto operation, logout and close the session again. In these cases, you'll probably only have a single session active at any given time.On the other hand, if you're carrying out large numbers of operations, particularly if they're latency sensitive, you'll likely have a pool of sessions, potentially "warm", i.e. already logged in to a particular token. In these cases, the total throughput of your system simplistically becomes the number of sessions multiplied by the throughput per session per second. However, a number of other factors come into play in practice, meaning that actual throughput can be quite different from expectations.
The calculator below uses a model which takes into account network time, and HSM parallelism to approximate the realistic throughput achievable. Naturally, there are a number of approximations involved, but the model agrees well with performance figures in practice.
Now, bearing in mind that each session is serial, each session has a hard limit on the number of operations it can carry out per second. Suppose we've got some network latency between our module and the HSM so that each session can carry out e.g. 100 operations per second. But suppose we need to handle 1,000 operations per second. In this case, our only option is to increase the number of sessions open to the HSM and distribute the load across them. But how far can we go with this approach? Can we go up to 10 sessions? 100? 1000? Further?
In most cases, the rule of thumb is that, while there are limits on the number of sessions, optimising this comes down to 4 factors: 1) The network latency to the HSM; 2) The time the HSM will take to perform each operation; 3) RAM consumption on the client and 4) RAM consumption on the HSM
The limiting factors vary primarily depdending on network distance to the HSM. If the HSM is local (ie negligible ping time), the limiting factor is going to be CPU on either the client or HSM. In these cases, it's best to use a number of sessions which matches the core count of the client or the HSM, whichever is smaller. In contrast, if there is significant network distance to the HSM, i.e. more than a couple of milliseconds, any given session handler on the HSM is going to spend the majority of time waiting for traffic from the client. Thus if the HSM has 16 cores but network latency means that each session is idle 90% of the time, the number of sessions can generally be increased until the proportion of idle time becomes small.
On the other hand, suppose we have 16 cores and minimal latency to the HSM. If we try to open 1,000 sessions and flood them with crypto requests, the total throughput will generally be much lower than sticking with 16 sessions. A large amount of context switching will occur both on the client and the HSM, with relatively high memory consumption on both too.
Rule - Network round trip time to the HSM is the single most important factor
Rule - Number of sessions should be sized based on traffic profiles
Rule - Too many sessions can be detrimental
Issue 3: Changing token locations
One of the earliest PKCS#11 lessons is that of key and object handles being ephemeral, but the same behavior applies to token locations. While it's easy to understand why key handles change between sessions, it's an understandable assumption that, since tokens can correspond to physical objects, their indexes should be fixed. But this isn't so. While some vendors do maintain consistency here, in other cases you calling C_GetSlotList twice can give slot lists which can change on every call. On the first call you might see 4 slots (2, 9, 13 and 14) while on the second call you might see 6 slots (2, 8, 9, 11, 14, 16). Admittedly, this behavior can often indicate issues with the underlying module, but it should never be taken for granted that since a token was "in" slot 5 a few seconds ago, that it will still be "in" slot 5 if you look again...This rule particularly applies in the case of cloud HSM services.
Never assume that token to slot mappings are static. Always check
Issue 4: Compounding latency
Throughout this guide, we cannot emphasize enough the importance of fast communication between the client and (network) HSM. As in the calculator above, increasing the network distance has a serious impact on the throughput achievable in practice. But there are other, more subtle effects. It's easy to assume that every PKCS#11 function call results in a matching message being sent to the HSM but, in many cases, single PKCS#11 functions can trigger many network round trips to the HSM, obviously compounding delays. The worst cases we've seen have involved single PKCS#11 calls resulting in ~40 sequential round trips between client and network HSM. If the network distance is any more than a few milliseconds, the resulting delays become very noticeable and problematic.Issue 5: Clustering and HA
One area not covered by the PKCS#11 standard, but a very common source of issues in practice, is the problem of clustered HSMs, redundant groups, load-balancing and failover. These topics are handled by each vendor in different ways and their configuration, testing and resiliency is probably the biggest source of reliability headaches in practice. However, this guide is not an appropriate place to explore further.Issue 6: Object find loops
One of the more basic, yet still common source of PKCS#11 implementation errors is the incorrect use of loops for finding objects. In PKCS#11, finding a particular object involves preparing a search operation, executing it, and then cleaning up. These search operations work as follows:- A template is provided, specifying a number of filters for the search. C_FindObjectsInit is called (the preparation)
- The application then calls C_FindObjects for a first time, specifying the maximum number of objects we want to handle on THIS iteration (think of it as a page of objects and specifying the page size). The module will provide matching objects and tell you how many in the page
- The application looks at how many were returned and, if this is > 0, there MAY be more to come in another page
- The application calls C_FindObjects repeatedly until the module returns 0 indicating no more objects
- The application calls C_FindObjectsFinal (the cleanup)
Issue 7: Attribute requirements and default values
Another very common source of frustration is that different PKCS#11 modules have different constraints and requirements for the attributes provided when generating keys. Module A might be perfectly happy with CKA_* being provided while generating a key, but module B will return CKR_TEMPLATE_INCOMPLETE, CKR_ARGUMENTS_BAD or CKR_TEMPLATE_INCONSISTENT, often with no further details as to why it views it as incomplete or inconsistent.While vendor documentation can be of some assistance here, this remains a common source of portability issues. Unfortunately, there's not much alternative here beyond consulting the vendor documentation for the specific attributes that are required for the particular objects being created. Some middleware tools, do have some handling for sanitizing templates according to the different vendor requirements, but it unfortunately remains an area in which the vendors have little incentive to standardize, with legacy uses ruling out this possibility anyway.
Unfortunately, resolving "errors" of this class usually comes down to trial and error, aided in lucky cases by examining the HSM error logs for clues as to what's missing or extraneous.
Another common issue is when a template is under-specified, the HSM will apply default values for the missing attributes. However, the behavior here is often different between vendors and predicting it a priori would involve laborious consultation of vendor documentation.
To avoid template default-value surprises, it's best practice to specify template attributes as completely as possible, leaving few or no values for the HSM to decide.
Issue 8: Clashing labels, IDs etc
In the core standard, objects and keys are identified using the CKA_ID and CKA_LABEL attributes. Especially with a modern developer mindset, conditioned by UUIDs and uniqueness, it often surprises new PKCS#11 developers to learn that there are usually no constraints placed on uniqueness. In general, it's permitted to create 10 different objects all with the same CKA_ID and/or CKA_LABEL, for example leading to consternation when two symmetric keys with the same CKA_ID are created, one of them is used to encrypt data; a search is made for the key based on CKA_ID and the second key with clashing CKA_ID is used for attempted decryption...The reason that CKA_ID values are non-unique is that they're intended to allow grouping of objects, e.g. to a public and private key forming a keypair will share the same CKA_ID.
Some vendors have more unique identifiers (such as CKA_OUID in the Luna case), which can eliminate ambiguities.
Another common pitfall with locating objects is to specify a search template that unexpectedly matches more than one object and to not handle the case of more than one object being matched. Blindly relying on the first object matched being the one sought is clearly a recipe for confusion at best and decryption/verification failures at worst.
Issue 9: FIPS and IVs when using AEAD in <3.0 versions
When providing cryptographically-sensitive parameters such as initialization vectors, PKCS#11 (before v3.0) is agnostic as to whether these are generated by the client and provided to the HSM, or whether the HSM should generate these itself. In pretty much all cases, it's much better for these to be generated by the HSM itself, ensuring that uniqueness requirements are met. Using HSM-generated IVs is strongly encouraged by FIPS 140-3, meaning that attempts to provide an IV to a compliant (or FIPS-mode) HSM will sometimes (depending on vendor) result in its refusal to use them. This can lead to confusion, especially if a HSM can operate in non FIPS-compliant mode. In these cases, the implementer will often provide an IV and their code will run fine. Switch to FIPS-compliant mode and the HSM will now refuse to encrypt. Once the switch to FIPS mode is made and the HSM provides the IV, there can often be confusion as to how the HSM passes the IV back to the calling application, since there's no standard method of doing this. Some can prepend it to the ciphertext; some may append it, etc.With PKCS#11 v3.0 and C_MessageEncrypt*, the situation has improved, i.e. AEAD is no longer shoe-horned into a function not intended for AEAD. However, given the uneven state of v3.0+ compliance, AEAD handling remains arguably the most problematic area of portability.
Issue 10: Session types and object lifetimes
When opening a PKCS#11 session, the session flags must be provided, resulting in the session being either read-only or read-write. In a read-only session, cryptographic operations can be performed and objects read, but the session cannot create, modify, or destroy token objects. It can, however, create ephemeral session objects.In a read-write session, token objects can be freely created, modified, and destroyed.
Objects (keys, certificates) are either Token objects or Session objects. One of the most common issues that newcomers to PKCS#11 face is that they open a session, login, and generate a key. When they return later and try to find the key using a fresh session, the key cannot be located. The key was generated without CKA_TOKEN=true, resulting in an ephemeral Session object being created and automatically destroyed when the session was closed.
The interplay between session type and object type also extends to visibility. If a token object is created with CKA_PRIVATE=true, it is strictly invisible to unauthorized sessions and won't show up in search results until C_Login has been successfully performed. Newcomers frequently confuse a read-write session with a logged-in state; however, a session can be read-write while remaining completely unauthenticated.
In short, if you want your keys to survive beyond the lifetime of the session (generally the case for everything except ephemeral operations like TLS termination), ensure you're using a read-write session and the objects have CKA_TOKEN=true. If you want the key you're creating to only be visible to logged-in users, use CKA_PRIVATE=true; otherwise, any unauthenticated session will be able to locate it via C_FindObjects. Note that using a CKA_PRIVATE key object for cryptographic operations without logging in is strictly prohibited by the PKCS#11 specification.
A final note is in order regarding CKA_MODIFIABLE. When an application creates a key, it will generally locate it again in the future by querying a combination of CKA_LABEL and CKA_ID. If a token key is created with CKA_PRIVATE=false and CKA_MODIFIABLE=true (which is often the default), any other authenticated normal user application on the system can locate the key and modify its attributes. If a parallel process maliciously or accidentally alters the CKA_LABEL, your primary application will fail to locate its own key on the next startup. If an object does not need to change after creation, always explicitly set CKA_MODIFIABLE=false.
The situation is further complicated by different vendors using different default values. For example, if you create a key without specifying CKA_MODIFIABLE, vendor A might default to true, while vendor B might default to false. These ambiguities frequently cause unexpected errors and portability issues when moving code across different HSMs. To avoid this, never rely on token defaults.
Best practice dictates explicitly defining lifecycle templates for every object creation, minimizing ambiguity and reliance on default values. For sensitive keys, this usually means enforcing CKA_MODIFIABLE=false, CKA_TOKEN=true, and CKA_PRIVATE=true (CKA_SENSITIVE and CKA_EXTRACTABLE are dealt with in part 2).
Part 2 of this guide will cover:
Issue 11: Calling conventions and null pointers
Issue 12: String terminations
Issue 13: Operations being active
Issue 14: Underlying security and what PINs actually do
Issue 15: Failed logins...
Issue 16: Login once vs many
Issue 17: Permissions
Issue 18: Locking and mutexes
Issue 19: Callbacks
Issue 20: Putting PKCS#11 behind HTTP/REST
KeyMirror
At the beginning, we mentioned our two-fold motivation for creating this guide, the first motivation being that no such guide existed and it would hopefully allow implementers and maintainers to avoid common pitfalls. The second motivation was to illustrate the limits of PKCS#11 and to introduce the products and services that i2km, inc offers to aid in the safe, reliable use of HSMs. While we hope this guide has been helpful, PKCS#11 is just an interface. Even if an implementation, both on the client and module sides is perfect, there are always additional real-world factors in play such as network and device latency, throughput and capacity issues, device contention between sessions, application crashes leading to dangling sessions and more. PKCS#11 is purely functional, i.e. it gives no feedback as to the operational status of a HSM, which direction latencies are trending, or whether a particular session from a particular client is causing issues. For end-to-end monitoring, tracing and analysis, other tools are needed.After spending years working for the large HSM vendors, we realized the limitations of PKCS#11 and the stark discrepancy between available monitoring options and modern, cloud-native formats such as OpenTelemetry and OpenMetrics. Being able to instrument PKCS#11 applications to provide real-time monitoring of behaviour, throughput, latencies, and anomalous key usage patterns is only possible with extreme effort and expense, so expensive that very few organizations have attempted it. Instead, observability chains typically end at the PKCS#11 interface, with everything beyond that point being dark (maybe there are some audit logs which can be examined months later for forensic purposes). From this experience, we built KeyMirror: a monitoring layer which transparently observes PKCS#11 commands between a client application and the vendor module, then judiciously transforms these into modern telemetry and tracing, allowing PKCS#11 applications to feed directly into standard tools such as Prometheus, DataDog, Grafana etc.
With KeyMirror, legacy PKCS#11 applications do not have to be black boxes any longer. With KeyMirror you can monitor exactly what your latency profiles are; attribute traffic to individual clients; monitor failure rates; get early warning of HSM capacity issues and, in the event of application failures, reduce resolution times from hours and days down to minutes.
Deploying KeyMirror takes minutes, with live results being visible in 5-10 minutes in typical deployments. The analysis engine, beyond the telemetry, tracing and metrics, can also identify problematic, incorrect and misconfigured applications, allowing us to provide application-specific guidance on implementation errors, bottleneck resolution and the like.
KeyMirror is a vendor agnostic tool, yet provides support for vendor-specific extensions. We are technology parters of Thales, Entrust and Utimaco.
To learn more, email us at info@keymirror.io
