PKCS #11 is an OASIS standard C programming interface, called Cryptoki, that lets an application use keys held inside a hardware security module, smart card or USB token without the private key ever leaving the device. The application loads a vendor supplied library, picks a slot and token, opens a session, logs in with a PIN and asks for operations by mechanism.
The current version is PKCS #11 3.2, approved as an OASIS Standard on 3 June 2026. Most real problems with PKCS #11 are not in the cryptography. They come from key attributes set the wrong way, PINs left in scripts and untrusted libraries loaded into trusted processes. This guide walks through each concept, the attribute combinations that turn a non exportable key into an exportable one, and a hands on lab you can run with a software token.
Key takeaways
- PKCS #11 defines a C API (Cryptoki) plus a large catalogue of mechanisms. Every HSM, smart card middleware and software token that claims support ships a shared library (a "module") that implements it.
- The core objects are the module, slots, tokens, sessions, two login roles (Security Officer and normal user), objects with attributes, and mechanisms. Learn those seven words and the rest of the API reads naturally.
- Key protection is expressed through attributes.
CKA_SENSITIVEandCKA_EXTRACTABLEdecide whether key material can leave the token;CKA_WRAP,CKA_UNWRAP,CKA_TRUSTED,CKA_WRAP_WITH_TRUSTED,CKA_WRAP_TEMPLATEandCKA_UNWRAP_TEMPLATEdecide how it may leave if it can. - Research presented at ACM CCS 2010 tested 17 commercial tokens with an automated model checking tool: 9 were vulnerable to key extraction attacks through the API itself, and the other 8 had severely restricted functionality. Most of those problems were attribute policy problems.
- RFC 7512 (April 2015) defines
pkcs11:URIs so tools can name a token or key the same way. p11-kit registers modules once for the whole system, and OpenSC supplies both a smart card module and thepkcs11-toolutility. - OpenSSL 3 reaches PKCS #11 through a provider, OpenSSH through
PKCS11Providerandssh-agent, and code signing tools through the same URIs. - A software token is fine for learning and CI tests, but it is not evidence that a production HSM behaves the same way.
What is PKCS #11 and where did it come from?
PKCS #11 is one of the Public-Key Cryptography Standards, a numbered family of documents that a commercial cryptography lab began publishing in the early 1990s. The current specification still cites siblings from that era; its reference list includes PKCS #3 version 1.4 from November 1993. Number 11 in the family described a "cryptographic token interface", a portable way for software to talk to devices that hold keys.
The goal has not changed since. The OASIS 3.2 text describes Cryptoki as an API "for devices that hold cryptographic information and perform cryptographic functions", built on two aims: technology independence (any kind of device) and resource sharing (many applications using many devices). Applications see a common logical view called a cryptographic token, whatever the hardware underneath.
Stewardship moved to OASIS in 2013. The OASIS PKCS 11 Technical Committee page records that the PKCS #11 version 2.30 input specification was contributed on 4 March 2013. Since then the committee has published every new version as an OASIS Standard.
Which versions exist?
| Date | Milestone | What changed |
|---|---|---|
| November 1993 | PKCS #3 v1.4 published (cited in the PKCS #11 references) | Shows the PKCS family was active in the early 1990s |
| 4 March 2013 | PKCS #11 v2.30 contributed to the OASIS TC | Becomes the principal input for OASIS work |
| April 2015 | v2.40 approved as OASIS Standard | Base, Current Mechanisms, Historical Mechanisms and Profiles documents; normative header files; backwards compatible to v2.20 |
| April 2015 | RFC 7512 published by the IETF | Standard pkcs11: URI scheme for tokens, objects and libraries |
| 13 May 2016 | v2.40 Errata 01 OASIS Standard | Corrections to 2.40 |
| 15 June 2020 | v3.0 approved as OASIS Standard | Base, Profiles, Current Mechanisms and Historical Mechanisms; adds C_GetInterface, C_LoginUser, C_SessionCancel and message based encryption functions |
| 23 July 2023 | v3.1 approved as OASIS Standard | Consolidated single specification document plus a separate Profiles document |
| 3 June 2026 | v3.2 approved as OASIS Standard | Post quantum mechanisms (ML-DSA, ML-KEM, SLH-DSA), XMSS, C_EncapsulateKey, C_DecapsulateKey, authenticated wrapping, asynchronous functions |
Many deployed modules still implement 2.40, because firmware and middleware move slowly. From 3.0, C_GetInterface lets a module offer the 2.x and 3.x function lists side by side, so a well written application asks for the version it needs and falls back cleanly.
What is new in 3.2?
Comparing the 3.1 and 3.2 texts shows the additions that matter most for planning. Version 3.2 defines key objects, key generation and signature mechanisms for ML-DSA and SLH-DSA, key objects and key encapsulation for ML-KEM, stateful hash based signatures (XMSS and XMSS MT), new functions C_EncapsulateKey and C_DecapsulateKey for key encapsulation mechanisms, C_WrapKeyAuthenticated and C_UnwrapKeyAuthenticated for authenticated key wrapping, and asynchronous function management (C_AsyncComplete, C_AsyncGetID, C_AsyncJoin). Its revision history notes that the ML-DSA, ML-KEM and SLH-DSA sections were updated in October 2024 to match the final FIPS 203, 204 and 205 documents.
A new specification version does not mean your device supports it. The mechanisms a token actually offers are whatever C_GetMechanismList returns on that slot, and that list is what you should plan against.
The core concepts: module, slot, token, session, roles, objects, mechanisms
PKCS #11 reads naturally once you hold seven ideas.
The library module
An application does not link against a device. It loads a shared library, usually with dlopen on Linux or LoadLibrary on Windows, and calls C_GetFunctionList (2.x) or C_GetInterface (3.x) to get a table of function pointers. Every vendor, open source project and software token ships its own module. That design is what gives PKCS #11 its portability, and it is also its largest trust decision: the module runs inside your process with your process's privileges. A malicious or buggy module can read anything the application can.
C_Initialize starts the library and C_Finalize ends it. The initialisation arguments let an application tell the module whether it may create operating system threads and which locking functions to use. Several libraries in one process calling C_Initialize and C_Finalize on the same module at different times is a classic source of crashes, which is one of the problems p11-kit set out to solve (more on that below).
Slots
A slot is a logical reader. For a smart card it maps closely to a physical card reader. For a network HSM it is usually a partition or a virtual token. C_GetSlotList returns the slots, optionally only those with a token present, and C_GetSlotInfo describes each one. Slot IDs are not stable across reboots or module upgrades on many products, so never hard code a slot number in production. Select by token label or serial number instead, which is exactly what a PKCS #11 URI does.
Tokens
A token is the device or partition that holds objects and performs cryptography. C_GetTokenInfo returns its label, manufacturer, model, serial number, flags (for example whether login is required, whether a protected authentication path such as a PIN pad exists, whether the token is initialised) and PIN length limits. C_InitToken initialises a token and sets the Security Officer PIN; on real HSMs this step is usually a ceremony, not a script.
Sessions
A session is a logical connection between an application and a token. C_OpenSession opens one, read only by default or read/write with the CKF_RW_SESSION flag. Sessions have states defined in the specification: CKS_RO_PUBLIC_SESSION, CKS_RO_USER_FUNCTIONS, CKS_RW_PUBLIC_SESSION, CKS_RW_USER_FUNCTIONS and CKS_RW_SO_FUNCTIONS. Login is not per session: the 3.2 text says a successful C_Login moves each of the application's sessions with that token into the matching logged in state.
Some rules surprise newcomers. The Security Officer cannot log in while a read only session exists (CKR_SESSION_READ_ONLY_EXISTS), and a read only session cannot be opened while a read/write SO session exists. Objects created as session objects (CKA_TOKEN false) vanish when the session closes, which is useful for temporary keys and confusing when a test seems to "lose" a key.
User roles: SO and user
PKCS #11 defines user types CKU_SO (the Security Officer), CKU_USER (the normal user) and CKU_CONTEXT_SPECIFIC. The SO initialises the token and sets the normal user's PIN with C_InitPIN; the normal user does the cryptographic work. Version 3.0 added C_LoginUser, which accepts a username as well as a PIN for tokens that support more than one user identity.
CKU_CONTEXT_SPECIFIC exists for keys that carry CKA_ALWAYS_AUTHENTICATE set to true. The 3.2 text says that for such a key "the user has to supply the PIN for each use (sign or decrypt)". Qualified signature cards often use this so a signature cannot be produced silently after a single login.
Objects and attributes
Everything on a token is an object: data objects, certificates, public keys, private keys, secret keys, domain parameters, mechanism objects and hardware features. Each object is a bag of attributes, each identified by a CKA_ constant. CKA_CLASS says what kind of object it is, CKA_KEY_TYPE the key algorithm, CKA_LABEL and CKA_ID how humans and software find it, CKA_TOKEN whether it persists, and CKA_PRIVATE whether it is visible only after login.
Applications create objects with C_CreateObject, C_GenerateKey, C_GenerateKeyPair, C_UnwrapKey or C_DeriveKey, passing a template (an array of attributes). They search with C_FindObjectsInit, C_FindObjects and C_FindObjectsFinal, read attributes with C_GetAttributeValue, and change those that are modifiable with C_SetAttributeValue. Security in PKCS #11 is almost entirely about what goes into those templates.
Mechanisms
A mechanism is a named cryptographic procedure: CKM_RSA_PKCS_PSS, CKM_ECDSA, CKM_SHA256_RSA_PKCS, CKM_AES_GCM, CKM_AES_KEY_WRAP, and in 3.2 CKM_ML_DSA and friends. C_GetMechanismList returns what a slot supports and C_GetMechanismInfo returns key size limits and flags saying which functions (encrypt, sign, wrap, derive, generate) the mechanism can be used with. The specification's mechanism tables mark each mechanism's allowed functions, and a token may support fewer.
Operations follow an init, update, final pattern: C_SignInit with a mechanism and key handle, then C_Sign for single part data or C_SignUpdate and C_SignFinal for streams. Version 3.0 added message based functions (C_MessageEncryptInit and related calls) so AEAD modes such as GCM can process many messages under one key without re-initialising, with per message parameters like the IV.
A minimal call sequence
The flow for signing with an existing key looks like this, written as pseudocode rather than a complete program:
C_GetInterface(...) /* or C_GetFunctionList on 2.x modules */
C_Initialize(&init_args)
C_GetSlotList(CK_TRUE, slots, &n) /* only slots with a token present */
/* pick the slot whose C_GetTokenInfo label matches, never slot 0 by habit */
C_OpenSession(slot, CKF_SERIAL_SESSION, NULL, NULL, &hSession)
C_Login(hSession, CKU_USER, pin, pinLen)
C_FindObjectsInit(hSession, {CKA_CLASS=CKO_PRIVATE_KEY, CKA_LABEL="release-signing"})
C_FindObjects(hSession, &hKey, 1, &found)
C_FindObjectsFinal(hSession)
C_SignInit(hSession, {CKM_ECDSA_SHA256}, hKey)
C_Sign(hSession, data, dataLen, sig, &sigLen)
C_Logout(hSession)
C_CloseSession(hSession)
C_Finalize(NULL)
Every call returns a CK_RV code; anything other than CKR_OK is a failure to handle, not to retry blindly.
Which key attributes matter for security?
Most PKCS #11 failures in the field are attribute failures. The table below lists the attributes that decide whether a key can leave the token and in what form, with the rule the 3.2 specification attaches to each.
| Attribute | Applies to | Meaning in the spec | Change rules |
|---|---|---|---|
CKA_SENSITIVE | Private and secret keys | Key value cannot be revealed in plaintext outside the token | Once set to true it cannot be changed (it becomes read only) |
CKA_EXTRACTABLE | Private and secret keys | Key "is extractable and can be wrapped" | Once set to false it cannot be changed |
CKA_ALWAYS_SENSITIVE | Private and secret keys | True if the key has always had CKA_SENSITIVE true | Set by the token, read only |
CKA_NEVER_EXTRACTABLE | Private and secret keys | True if the key has never had CKA_EXTRACTABLE true | Set by the token, read only |
CKA_WRAP | Secret and public keys | Key can be used to wrap (encrypt) other keys | Modifiable on many tokens unless the object is read only |
CKA_UNWRAP | Secret and private keys | Key can be used to unwrap (decrypt and import) other keys | As above |
CKA_TRUSTED | Certificates, wrapping keys | Key or certificate can be trusted for the purpose it was created | Can only be set to true by the SO |
CKA_WRAP_WITH_TRUSTED | Private and secret keys | Key can only be wrapped with a wrapping key whose CKA_TRUSTED is true; default false | Once set to true it cannot be changed |
CKA_WRAP_TEMPLATE | Wrapping keys | Attribute template that any key being wrapped must match | Set at creation |
CKA_UNWRAP_TEMPLATE | Unwrapping keys | Template applied to any key unwrapped with this key, before the caller's own template | Set at creation |
CKA_ALLOWED_MECHANISMS | Keys | List of mechanisms the key may be used with | Narrows a key to, for example, one signature scheme |
CKA_ALWAYS_AUTHENTICATE | Private keys | PIN required for each use | Default false |
CKA_PRIVATE | All objects | Object visible only after login | Can usually be changed during C_CopyObject |
CKA_MODIFIABLE, CKA_COPYABLE, CKA_DESTROYABLE | All objects | Whether attributes can change, the object can be copied, or deleted | Set at creation |
Three words in that table do a lot of work: sensitive, extractable and trusted.
Sensitive versus extractable
These two are easy to confuse. CKA_SENSITIVE stops the token from returning the raw key value through C_GetAttributeValue. It does not stop the key being exported in encrypted form. CKA_EXTRACTABLE controls whether the key can be wrapped at all. A key with CKA_SENSITIVE true and CKA_EXTRACTABLE true is a key you cannot read directly but can export wrapped under another key, and whoever holds that other key can then decrypt it.
For a long lived signing key that should never exist outside the device, you want CKA_SENSITIVE true, CKA_EXTRACTABLE false, and you want the token to report CKA_ALWAYS_SENSITIVE true and CKA_NEVER_EXTRACTABLE true. Those last two are the ones an auditor should check, because they prove history: the key was generated on the token with those protections from the start, rather than being imported or loosened and tightened later. Pair them with CKA_LOCAL (true when the key was generated on the token rather than imported) and you have a short, defensible attestation of where the key came from.
The specification makes the protections one way on purpose. Once CKA_SENSITIVE is true it cannot be set back to false, and once CKA_EXTRACTABLE is false it cannot be set back to true. If a token lets you reverse either, that is a conformance bug worth reporting.
Wrapping keys and trust
Wrapping is how keys legitimately move between tokens: for backup, for cloning a key into a second HSM in a cluster, or for delivering a session key to a peer. C_WrapKey encrypts a key under a wrapping key that has CKA_WRAP set, and C_UnwrapKey imports a wrapped key using an unwrapping key that has CKA_UNWRAP set. The specification returns CKR_KEY_UNEXTRACTABLE when the target key has CKA_EXTRACTABLE false.
The trust attributes let an organisation say "keys may leave, but only to places we approve". The SO marks approved wrapping keys with CKA_TRUSTED; only the SO can set that flag. High value keys get CKA_WRAP_WITH_TRUSTED true, after which they can only be wrapped by those approved keys. CKA_WRAP_TEMPLATE narrows it further: a wrapping key can be told to wrap only keys whose attributes match a template, for example only AES keys with a particular CKA_ID prefix.
On the import side, CKA_UNWRAP_TEMPLATE is the defensive control people forget. It forces attributes onto every key that comes in through that unwrapping key, so an imported key can be made sensitive, non extractable and restricted to one purpose regardless of what the caller asked for. The 3.2 text says the caller's template is "applied after this template as if the object has already been created", which means the caller cannot loosen what the unwrap template fixed as read only.
Which combinations are dangerous?
The dangerous patterns share one shape: a single key, or a key the caller controls, is allowed to play two roles that together turn ciphertext back into plaintext.
| Combination | Why it is risky | Safer policy |
|---|---|---|
One secret key with both CKA_WRAP and CKA_DECRYPT | A sensitive key can be wrapped under it, and the same key can then be asked to decrypt the wrapped blob as if it were ordinary data | Give wrapping keys only wrap and unwrap roles; never encrypt or decrypt |
One key with both CKA_UNWRAP and CKA_ENCRYPT | The mirror image: data encrypted under the key can be unwrapped into a key object whose value the caller chose | Same separation of roles |
Signing or decryption keys with CKA_EXTRACTABLE true | Any wrapping key the session can reach can export them | Generate with CKA_EXTRACTABLE false; check CKA_NEVER_EXTRACTABLE |
Users can import a wrapping key with C_CreateObject or C_UnwrapKey | A caller supplied wrapping key whose value the caller already knows can be used to export extractable keys | Restrict object creation by policy; require CKA_WRAP_WITH_TRUSTED on valuable keys |
CKA_SENSITIVE false on secret keys | C_GetAttributeValue can return the raw value | Default every private and secret key to sensitive |
Unwrapping keys with no CKA_UNWRAP_TEMPLATE | Imported keys can arrive extractable, non sensitive, or with extra roles | Set an unwrap template that forces sensitive true, extractable false and a single role |
CKA_MODIFIABLE true on production keys | A logged in user can add roles later | Create production keys with CKA_MODIFIABLE false where the token supports it |
The first row is the textbook case and worth understanding at concept level. If one key K may both wrap and decrypt, then the API will happily wrap a sensitive key S under K, producing ciphertext, and will happily decrypt that ciphertext under K as data, producing S in the clear. No single call broke a rule. The policy did, by giving one key two roles that compose into an export. That is why the fix is a policy, not a patch.
What did key wrapping attack research show?
In October 2010, Matteo Bortolozzo, Matteo Centenaro, Riccardo Focardi and Graham Steel presented "Attacking and Fixing PKCS#11 Security Tokens" at the 17th ACM Conference on Computer and Communications Security (CCS 2010). Their tool, Tookan (TOOl for cryptoKi ANalysis), worked in three stages, according to the project page at Ca' Foscari University of Venice:
- It reverse engineered a real token through the PKCS #11 API to learn what it actually allowed: which attributes could be set, which functions worked with which keys, whether known keys could be imported.
- It built a formal model of that behaviour for the SATMC model checker.
- It replayed any attack trace the model checker found directly against the token, so every reported attack was confirmed on the device.
The authors report testing 17 commercially available tokens. Nine were vulnerable to attack. The other eight had "severely restricted functionality", meaning they avoided the attacks mainly by not supporting the operations involved. The project page also notes that one attack the model checker found had not appeared in the literature before.
The attacks were API level logic, not side channels or hardware tampering. The recurring ingredients were the role conflicts in the table above, the ability to import a key whose value the caller already knew and then use it as a wrapping key, and attribute changes that let a key gain a role after it was created. The project page puts the import risk plainly: letting known keys onto a device is "generally a dangerous thing", because an imported wrapping key can then be asked to wrap a sensitive internal key.
The defensive lessons have aged well:
- Separate key roles. A key used for wrapping should not encrypt or decrypt data, and a key used for data should not wrap. Many current HSM policies enforce this by key class or partition.
- Make protections sticky. Sensitive cannot be turned off and non extractable cannot be turned back on. Add
CKA_MODIFIABLEfalse so roles cannot be added later. - Control what may wrap what.
CKA_TRUSTED(SO only),CKA_WRAP_WITH_TRUSTEDandCKA_WRAP_TEMPLATEmean a valuable key can only leave under an approved key. - Control what may come in. Restrict
C_CreateObjectandC_UnwrapKeyfor secret keys, and setCKA_UNWRAP_TEMPLATEon every unwrapping key. - Test the policy, not the documentation. The method itself, probing the real device and checking the behaviour against a model, is still the right way to evaluate a token.
Restricted functionality is also a legitimate defence: a signing partition configured not to wrap at all is simpler to reason about than one with an elaborate trust policy.
PKCS #11 URIs, p11-kit and OpenSC
Before 2015 every tool had its own way to name a key on a token: a slot number here, a key ID in hex there, a label in a config file somewhere else. RFC 7512, published by the IETF in April 2015, fixed that with the pkcs11: URI scheme.
How a PKCS #11 URI works
A URI has a path, separated by semicolons, and an optional query, separated by ampersands. Path attributes identify the library, slot, token and object: library-manufacturer, library-description, library-version, slot-manufacturer, slot-description, slot-id, manufacturer, model, serial, token, object, type and id. Query attributes carry pin-source, pin-value, module-name and module-path. An empty attribute set matches everything, so pkcs11: alone means "any token, any object".
Some examples in the RFC's own style:
pkcs11:token=release;object=release-signing;type=private
pkcs11:token=release;id=%01;type=private?pin-source=file:/etc/pki/release.pin
pkcs11:object=web-tls;type=cert?module-name=softhsm2
id values are percent encoded bytes, which is why %01 appears so often in examples. type is one of public, private, cert, secret-key and data.
Two security notes come straight from the RFC. If a URI contains both pin-source and pin-value, it "SHOULD be refused as invalid". And use of pin-value "may have security-related consequences", so the RFC sends readers to its security considerations before using it at all. A URI is a string; strings end up in shell history, process listings, logs and tickets. Prefer pin-source pointing to a file with tight permissions, a protected authentication path on the token, or an interactive prompt.
What p11-kit does
p11-kit, part of the p11-glue project, "provides a way to load and enumerate PKCS#11 modules" and "a standard configuration setup for installing PKCS#11 modules in such a way that they're discoverable." Each module gets a small .module file in a system or user directory. The file names the module path and options such as critical (if yes, failure to load it aborts loading of all modules) and managed. A blank module: line in a user config disables a module that the system config enabled.
The project also addresses a problem that bites real applications: several libraries in one process each initialising and finalising the same module. p11-kit coordinates that for applications that link to it, and for those that do not it ships a proxy compatibility module that loads the configured modules and exposes each of their slots as its own.
Install a module once and many tools see it; p11-kit list-modules shows what is registered.
What OpenSC does
OpenSC provides PKCS #11 support for a wide range of smart cards and USB tokens through its opensc-pkcs11.so module, and ships command line tools for working with any PKCS #11 module. The most useful is pkcs11-tool, which can list slots and mechanisms, initialise tokens and PINs, generate keys and key pairs, set attributes such as --sensitive, --private, --always-auth and --extractable, sign, verify, wrap and run self tests. Its manual page carries a warning that applies to every tool in this space: passing a PIN on the command line with --pin lets other users on the machine read it, and the tool accepts env:VARIABLE as a safer alternative.
How code signing, TLS servers and SSH use PKCS #11
PKCS #11 sits underneath a lot of everyday infrastructure. The common integration points look like this.
OpenSSL 3 providers
OpenSSL 3 replaced the old engine interface with providers. The pkcs11-provider project is an OpenSSL 3.x provider that its README says "targets PKCS#11 version 3.2 but is backwards compatible to version 3.1, 3.0 and 2.40". Once configured, any OpenSSL command or library call that accepts a key URI can use pkcs11: URIs, because OpenSSL 3's store API resolves them through the provider. That covers openssl pkey, openssl req, openssl dgst, openssl storeutl and applications built on the same APIs.
Configuration lives in openssl.cnf: a [pkcs11_sect] with the provider path, pkcs11-module-path pointing at the PKCS #11 module, optionally pkcs11-module-token-pin = file:/path, and activate = 1. The default provider must also be activated, or ordinary commands stop working. The PKCS11_PROVIDER_MODULE environment variable overrides the module path, which is handy when different services on one host use different tokens.
For older applications that can only read PEM files, the provider documentation describes a wrapper PEM that contains a PKCS #11 URI, with a clear warning: "storing PINs within these PEM files is not secure".
TLS servers
A TLS server with a PKCS #11 backed key holds a session open to the token and sends a signing request for each handshake. So the PIN must be available at service start (its storage is the real secret), token latency becomes handshake latency, and a token outage is a TLS outage. High availability usually means more than one device holding the key, which brings you straight back to wrapping policy.
SSH
OpenSSH supports PKCS #11 directly. The ssh_config option PKCS11Provider "specifies which PKCS#11 provider to use or none to indicate that no provider should be used (the default)", taking a path to the shared library. You can also pass -I /path/to/module.so to ssh, list the public keys a token holds with ssh-keygen -D /path/to/module.so, and load them into an agent with ssh-add -s /path/to/module.so.
The agent is where module trust matters most. ssh-agent takes a -P allowed_providers option, a pattern list of library paths that ssh-add -s may load; the manual gives the default as /usr/lib/*,/usr/local/lib/*. Keep that allowlist narrow, ideally the one module you use. An agent that will load any library path it is given is an agent that will run any code it is given.
Code signing
Code signing tools in most ecosystems accept a PKCS #11 key, either directly through a URI or through OpenSSL. The pattern is the same everywhere: the build host has the module and a URI, the signing key lives on the token with CKA_EXTRACTABLE false, and the signing step is the only step that holds the PIN. The firmware signing guide walks through one concrete flow end to end; the HSM selection guide covers validation levels and form factors, which this article does not repeat.
PKCS #11 compared with KMIP, platform key stores and cloud KMS APIs
PKCS #11 is not the only way to reach keys. The table compares it with the other three common interfaces by scope, deployment and key export.
| Dimension | PKCS #11 (Cryptoki) | KMIP | Platform key stores | Cloud KMS APIs |
|---|---|---|---|---|
| What it is | C API loaded in process from a vendor module | Wire protocol between a client and a key server | Operating system APIs for keys and certificates | HTTPS APIs run by a cloud provider |
| Standards body | OASIS PKCS 11 TC | OASIS KMIP TC | Each operating system | Each provider |
| Main job | Use keys: sign, decrypt, wrap, derive inside a device | Manage keys: create, register, get, rotate, revoke, destroy across many systems | Store and use keys for local apps and the OS | Store and use keys for cloud workloads |
| Where crypto happens | Inside the token or HSM | Usually on the client after fetching a key, or on the server for some operations | OS process, TPM or secure enclave depending on platform | Provider's HSM backed service |
| Typical clients | TLS servers, CAs, code signing, smart card login, SSH | Storage arrays, databases, backup systems | Desktop and mobile apps, browsers | Cloud services and applications |
| Authentication | PIN or password per token, plus SO role | Mutual TLS client certificates, sometimes username and password | OS user account and platform policy | Cloud identity and access policies |
| Key export control | Attributes: sensitive, extractable, wrap with trusted, templates | Attributes and server policy (for example Sensitive and Extractable) | Platform flags for exportable or non exportable keys | Usually non exportable; import and export by explicit features |
| Biggest trust decision | Which module you load into your process | Which client certificates the server accepts | Which apps can access the store | Which identities hold which permissions |
Many deployments use more than one. Choose by the job: PKCS #11 when an application must use a key it can never see, KMIP when many systems need keys delivered and rotated centrally.
Step by step: SoftHSMv2, a non extractable key and an OpenSSL signature
This lab uses SoftHSMv2, a software token. SoftHSM "started as part of the OpenDNSSEC project" so that people could use PKCS #11 without buying a device, and today it is a standalone project. It implements the PKCS #11 interface on top of OpenSSL or Botan and stores tokens as files. That makes it ideal for learning and for continuous integration, and unsuitable as a production key store. Run this lab on a test machine or in a disposable container.
The commands assume a Debian or Ubuntu host. Package names and paths differ on other distributions; each step shows how to find the right path rather than assuming it.
Step 1: Install SoftHSMv2, OpenSC and the OpenSSL provider
sudo apt-get update
sudo apt-get install -y softhsm2 opensc pkcs11-provider openssl p11-kit
# Find the installed module paths instead of guessing them
dpkg -L softhsm2 | grep 'libsofthsm2.so$'
dpkg -L pkcs11-provider | grep 'pkcs11.so$'
Record both paths. The rest of the lab uses shell variables for them.
export SOFTHSM_MODULE="$(dpkg -L softhsm2 | grep 'libsofthsm2.so$' | head -n1)"
export OSSL_P11_PROVIDER="$(dpkg -L pkcs11-provider | grep 'pkcs11.so$' | head -n1)"
echo "$SOFTHSM_MODULE"; echo "$OSSL_P11_PROVIDER"
Step 2: Give SoftHSM a private token directory
SoftHSM reads its configuration from the file named in SOFTHSM2_CONF, with directories.tokendir saying where tokens live and objectstore.backend choosing the file or database store. A private directory keeps the lab away from any system tokens.
mkdir -p "$HOME/p11lab/tokens"
chmod 700 "$HOME/p11lab" "$HOME/p11lab/tokens"
cat > "$HOME/p11lab/softhsm2.conf" <<'EOF'
directories.tokendir = /REPLACE/p11lab/tokens
objectstore.backend = file
objectstore.umask = 0077
log.level = INFO
EOF
sed -i "s#/REPLACE#$HOME#" "$HOME/p11lab/softhsm2.conf"
export SOFTHSM2_CONF="$HOME/p11lab/softhsm2.conf"
softhsm2-util --show-slots
You should see one uninitialised slot. Note that it is uninitialised; real devices have the same state before their first ceremony.
Step 3: Initialise a token without putting PINs in history
softhsm2-util --init-token --free initialises the first free slot. If you omit --so-pin and --pin, it prompts for them. Prompting is the habit to practise.
softhsm2-util --init-token --free --label release
# Enter the SO PIN and the user PIN when prompted.
softhsm2-util --show-slots
SoftHSM reassigns the initialised token to a new slot ID, which is a live demonstration of why scripts should select tokens by label, not slot number.
For the remaining steps, put the user PIN in a file only you can read. This mirrors the pin-source pattern you would use for a service, and it is still a secret: treat the file as one.
umask 077
read -rs -p "User PIN: " P11PIN; echo
printf '%s' "$P11PIN" > "$HOME/p11lab/user.pin"
export P11PIN # pkcs11-tool reads it with --pin env:P11PIN
Step 4: Generate a non extractable signing key pair
pkcs11-tool sets attributes from flags. Ask for an EC P-256 key pair for signing only, mark the private key sensitive and private, and do not pass --extractable.
pkcs11-tool --module "$SOFTHSM_MODULE" --token-label release \
--login --pin env:P11PIN \
--keypairgen --key-type EC:prime256v1 \
--label release-signing --id 01 \
--usage-sign --sensitive --private
--usage-sign sets CKA_SIGN on the private key and CKA_VERIFY on the public key. Nothing in the command grants decrypt, wrap or unwrap, so the key has one role.
Step 5: Audit the attributes
List the objects and read the attributes that matter. The listing in current OpenSC releases shows usage and access flags for each key; what you are looking for on the private key is sign only usage, sensitive, always sensitive, never extractable and local.
pkcs11-tool --module "$SOFTHSM_MODULE" --token-label release \
--login --pin env:P11PIN --list-objects --type privkey
A good result reads, in substance: CKA_SIGN true, CKA_DECRYPT false, CKA_UNWRAP false, CKA_SENSITIVE true, CKA_EXTRACTABLE false, CKA_ALWAYS_SENSITIVE true, CKA_NEVER_EXTRACTABLE true, CKA_LOCAL true. If the listing on your version does not show a field, read it with any PKCS #11 aware tool that calls C_GetAttributeValue, or write a ten line program against the module. Do not assume a default.
Then prove the negative. Ask the token to export the key and confirm it refuses:
pkcs11-tool --module "$SOFTHSM_MODULE" --token-label release \
--login --pin env:P11PIN --read-object --type privkey --id 01 \
-o /dev/null; echo "exit code: $?"
A non zero exit and an error mentioning a sensitive or unextractable attribute is the outcome you want.
Step 6: Point OpenSSL at the token through the provider
Write a dedicated OpenSSL config for the lab rather than editing the system one.
cat > "$HOME/p11lab/openssl-p11.cnf" <<EOF
openssl_conf = openssl_init
[openssl_init]
providers = provider_sect
[provider_sect]
default = default_sect
pkcs11 = pkcs11_sect
[default_sect]
activate = 1
[pkcs11_sect]
module = $OSSL_P11_PROVIDER
pkcs11-module-path = $SOFTHSM_MODULE
pkcs11-module-token-pin = file:$HOME/p11lab/user.pin
activate = 1
EOF
export OPENSSL_CONF="$HOME/p11lab/openssl-p11.cnf"
# List what OpenSSL can see on the token, with URIs
openssl storeutl -keys -text 'pkcs11:token=release'
Step 7: Sign a file and verify it
Export the public key (public keys are meant to leave), sign with the private key by URI, and verify with plain OpenSSL.
echo "release 1.4.2" > "$HOME/p11lab/release.txt"
openssl pkey -in 'pkcs11:token=release;object=release-signing;type=public' \
-pubin -pubout -out "$HOME/p11lab/release-signing.pub"
openssl dgst -sha256 \
-sign 'pkcs11:token=release;object=release-signing;type=private' \
-out "$HOME/p11lab/release.txt.sig" "$HOME/p11lab/release.txt"
OPENSSL_CONF=/dev/null openssl dgst -sha256 \
-verify "$HOME/p11lab/release-signing.pub" \
-signature "$HOME/p11lab/release.txt.sig" "$HOME/p11lab/release.txt"
The last command runs without the provider to show that verification needs only the public key. Verified OK means the signature came from the key on the token.
Step 8: Use the same key over SSH (optional)
The same token can back SSH authentication. These commands only read public keys and load the module into a local agent with an allowlist.
ssh-keygen -D "$SOFTHSM_MODULE" # prints the public key(s) in OpenSSH format
eval "$(ssh-agent -P "$SOFTHSM_MODULE")"
ssh-add -s "$SOFTHSM_MODULE" # prompts for the user PIN
ssh-add -L
Step 9: Clean up
Warning: the next command deletes the token and every key on it. Check the token label and the SOFTHSM2_CONF path first, so you only delete the lab token.
echo "$SOFTHSM2_CONF"
softhsm2-util --show-slots | grep -A3 'Label:.*release'
softhsm2-util --delete-token --token release
shred -u "$HOME/p11lab/user.pin" 2>/dev/null || rm -f "$HOME/p11lab/user.pin"
unset P11PIN OPENSSL_CONF SOFTHSM2_CONF
What changes on a production HSM
Production devices add their own policy on top of PKCS #11: partitions that refuse extractable private keys, multi person SO approval, their own role models and a subset of mechanisms, with different error codes for the same refusal. Rerun steps 4, 5 and 7 against the real device and record what it actually did.
Common mistakes
PINs in scripts, URIs and command lines. --pin 123456 lands in shell history and is visible in the process list. pin-value in a URI lands in logs and config repositories. Use prompts, env: with care, pin-source files with strict permissions, or a protected authentication path. Rotate the user PIN when people leave, and treat the PIN file on a server as the secret it is.
Extractable signing keys. Keys created by import, or created with a tool whose default you never checked, can be extractable. A signing key that can be wrapped can be copied to anywhere a wrapping key can reach. Generate on the token, check CKA_NEVER_EXTRACTABLE and CKA_LOCAL, and record both in your key inventory.
One key, two roles. A secret key with wrap and decrypt, or unwrap and encrypt, recreates the oldest PKCS #11 attack pattern. Give wrapping keys only wrapping roles.
No unwrap template. Without CKA_UNWRAP_TEMPLATE, an imported key takes whatever attributes the caller asks for.
Loading untrusted modules. A PKCS #11 module is native code that runs in your process. Load modules only from your package manager or the device's signed installer, keep ssh-agent -P and similar allowlists narrow, and do not let configuration from untrusted sources set module-path. RFC 7512 calls out module-path as a query attribute consumers may accept; accepting it from an untrusted URI means loading arbitrary libraries.
Hard coded slot numbers. Slots move. SoftHSM moves a token to a new slot on initialisation; real devices renumber after upgrades. Select by token label or serial.
Testing on SoftHSM and assuming production matches. A software token is a fine stand in for API shape. It does not have the device's validation boundary, its policy layer, its mechanism list, its error behaviour or its performance. Run an acceptance test against the real device before go live.
No inventory of what talks to the token. The key may be safe; the application holding the session and PIN is ordinary software with ordinary dependencies, and it is often the easier target.
FAQ
What is the difference between PKCS #11 and Cryptoki?
They are the same thing seen from two sides. PKCS #11 is the name of the standard; Cryptoki ("cryptographic token interface") is the name of the API it defines. Function names start with C_, attributes with CKA_, mechanisms with CKM_ and return codes with CKR_.
What is the latest version of PKCS #11?
Version 3.2, approved as an OASIS Standard on 3 June 2026. Version 3.1 became an OASIS Standard on 23 July 2023 and 3.0 on 15 June 2020. Many devices in service still implement 2.40, so check the version your module reports through C_GetInfo and the interfaces it offers through C_GetInterfaceList.
Does PKCS #11 support post quantum algorithms?
The 3.2 specification defines mechanisms for ML-DSA, ML-KEM and SLH-DSA, aligned to FIPS 203, 204 and 205, plus XMSS and new key encapsulation functions. Whether your token supports them depends on its firmware and module; check C_GetMechanismList on the actual slot.
What does CKA_SENSITIVE mean?
It means the key's value cannot be read in plaintext through the API. It does not stop the key being exported in wrapped (encrypted) form; that is what CKA_EXTRACTABLE false does. For keys that should never leave, set both and check that the token reports CKA_ALWAYS_SENSITIVE and CKA_NEVER_EXTRACTABLE as true.
What is the wrap and decrypt attack in PKCS #11?
It is a policy flaw rather than a broken algorithm. If one key may both wrap other keys and decrypt data, a sensitive key can be wrapped under it and the result decrypted under the same key, revealing the sensitive key. Separating roles and using trusted wrapping keys prevents it. The pattern was among those studied in the CCS 2010 Tookan research.
How do I use a PKCS #11 key with OpenSSL 3?
Install an OpenSSL 3 provider for PKCS #11, add a provider section to openssl.cnf with pkcs11-module-path set to your module, activate both the default and PKCS #11 providers, and pass pkcs11: URIs wherever a key file would go. openssl storeutl -keys -text pkcs11: lists the keys and their URIs.
How do I use a smart card or HSM key with SSH?
Set PKCS11Provider in ssh_config to the module path, or pass -I to ssh. To use an agent, run ssh-add -s with the module path and start ssh-agent with -P set to an allowlist of module paths.
Is SoftHSM safe to use in production?
SoftHSM was built so that people could use PKCS #11 without hardware and stores keys in files protected by the token PINs. It is excellent for development and testing. It does not provide the physical protection, validation or policy enforcement of a hardware device, so do not treat a SoftHSM token as an HSM in production risk decisions.
Why software supply chain hygiene still matters for PKCS #11
A PKCS #11 key is only as safe as the software that holds the session and the PIN: the TLS server, the signing tool, the OpenSSL provider, the agent and every library they pull in. Those are ordinary open source dependencies and container images, rebuilt and redeployed like any other, and a vulnerable one sitting next to a token is the easiest route to misusing the key without ever extracting it.
Fix it without waiting for upstream
Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.
1. Autonomous remediation: pull requests, not tickets
When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation covers eight categories, including dependencies, licenses, security posture and findings. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.
2. Gold Open Source: the safe version in seconds, free
gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds.
3. Gold Registry: start from components that are already fixed
Gold Registry provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs and a completed license check, with signed build provenance (signed with Safeguard's key and verifiable with cosign, SLSA Build Level 3 as the design target) and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.
When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. Ask which packages it covers before you plan around it.
| Your situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | Safeguard policies and gates in CI |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch |
| You want to start clean on new projects | Pull hardened components and images from Gold Registry |
One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.
See how Safeguard would help
To be plain about the fit: Safeguard is not an HSM, does not implement PKCS #11, does not store or use your keys and does not audit token attributes or PIN handling. Where it helps is the software around the token. It scans repositories, container images and SBOM files for dependencies, matches them against vulnerability data, and shows which projects contain a given component and version, including the OpenSSL builds, PKCS #11 providers, SSH clients and signing tools inside what you build. Gold Open Source at gold.safeguard.sh gives free CVE intelligence with affected and fixed versions for those components.
The first demo is a real CLI session. Asked to fix a lodash CVE, the assistant reads the finding and advisory, asks before any command that changes something, edits package.json to lodash 4.17.21, runs npm install and npm test with your approval, prints a summary and pushes nothing. The same flow applies when a signing service or TLS front end that talks to your token pins an outdated cryptography library.
Click through it at your own pace, or open it full screen.
The second demo sets up the local runner on your own machine, approves it with its device code, then scans from the terminal. Signing infrastructure usually lives in internal repositories and build images that never leave a restricted network, and the runner scans them where they are built.
Click through it at your own pace, or open it full screen.
The third demo connects Claude to the Safeguard MCP server with OAuth and lets you choose which tools Claude can use. It is a quick way to ask which of your projects carry a given OpenSSL or PKCS #11 provider version on the day an advisory lands, with tool permissions you control.
Click through it at your own pace, or open it full screen.
To try the CLI, download Safeguard and follow the CLI documentation.
Quick check
A private key has CKA_SENSITIVE true and CKA_EXTRACTABLE true. Can it leave the token?
Yes, in wrapped form. Sensitive only stops the raw value being read through the API. Because the key is extractable, any wrapping key the session can use (subject to CKA_WRAP_WITH_TRUSTED and wrap templates) can export it encrypted.
An application can set CKA_EXTRACTABLE back to true on a key it created as non extractable. Is that allowed by the specification?
No. The specification says CKA_EXTRACTABLE cannot be changed once set to false, and the token tracks history in CKA_NEVER_EXTRACTABLE. A token that allows the change is not behaving as the standard requires.
Who can set CKA_TRUSTED to true on a wrapping key?
Only the Security Officer. That is what makes CKA_WRAP_WITH_TRUSTED meaningful: a normal user cannot promote a key they control into an approved wrapping key.
Sources
- OASIS PKCS 11 Technical Committee
- PKCS #11 Specification Version 3.2, OASIS Standard, 3 June 2026
- PKCS #11 Specification Version 3.1, OASIS Standard, 23 July 2023
- PKCS #11 Base Specification Version 3.0, OASIS Standard, 15 June 2020
- PKCS #11 Base Specification Version 2.40 Plus Errata 01
- RFC 7512: The PKCS #11 URI Scheme
- Tookan project page, Ca' Foscari University of Venice (Bortolozzo, Centenaro, Focardi, Steel, ACM CCS 2010)
- SoftHSMv2 README
- softhsm2-util manual page
- softhsm2.conf manual page
- OpenSC README
- pkcs11-tool manual page (OpenSC)
- p11-kit project page
- p11-kit pkcs11.conf manual
- pkcs11-provider README
- pkcs11-provider HOWTO
- provider-pkcs11(7) manual
- OpenSSH ssh_config(5) manual
- OpenSSH ssh-agent(1) manual
Last reviewed: 25 October 2026. PKCS #11 3.2 is the current OASIS Standard; check the OASIS PKCS 11 TC page for errata and later versions.