Skip to content
cybersecurity authentication

PKCS#11 (PKCS#11)

pkcs11 cryptoki hardware-token yubikey opensc authentication
Plain English

PKCS#11 is a standardized interface - think of it as a universal adapter - that lets software applications talk to hardware security devices without knowing the device’s internal details. When you tell SSH to use your YubiKey, SSH does not talk to the YubiKey directly. Instead, it calls through the PKCS#11 interface, which OpenSC implements. OpenSC knows how to talk to the YubiKey; SSH just needs to know the PKCS#11 standard. The result: SSH asks “please sign this challenge,” the message goes through PKCS#11 to OpenSC to the YubiKey, the YubiKey signs it inside the chip, and the signed result comes back. The private key never leaves the hardware.

Technical Definition

PKCS#11 (also called Cryptoki - Cryptographic Token Interface) is defined by the OASIS PKCS#11 Technical Committee (current version: 3.1). It specifies a C API for interacting with cryptographic tokens (smart cards, HSMs, USB tokens).

Core object model:

  • Slot: A physical interface that can hold a token (e.g., a USB port with a YubiKey inserted)
  • Token: The cryptographic device in the slot (the YubiKey itself)
  • Session: A stateful connection to a token, either read-only or read-write
  • Object: Data stored on the token: certificates, public keys, private keys, data

Key operation flow (signing):

  1. Application loads the PKCS#11 module (.so / .dylib / .dll) with C_Initialize
  2. Calls C_GetSlotList to find available tokens
  3. Opens a session with C_OpenSession
  4. Authenticates with C_Login (supplies PIN)
  5. Calls C_SignInit specifying the mechanism (e.g., CKM_ECDSA)
  6. Calls C_Sign with the data to sign
  7. Token performs the signing internally, returns the signature
  8. Application calls C_CloseSession / C_Finalize

The private key object on the token has CKA_SENSITIVE = true and CKA_EXTRACTABLE = false. The PKCS#11 specification guarantees these attributes cannot be changed, and C_GetAttributeValue will never return the private key bytes.

Common PKCS#11 providers:

ProviderPlatformDevices
OpenSC (opensc-pkcs11.so)Linux, macOSYubiKey PIV, most smart cards
ykcs11 (libykcs11.so)Linux, macOSYubiKey only (Yubico’s own)
SoftHSM2Linux, macOSSoftware HSM (testing)
Windows CNG PKCS#11WindowsPlatform keys, smart cards
AWS CloudHSMCloudManaged HSM

Using PKCS#11 with SSH:

# List available keys on the token
ssh-keygen -D /opt/homebrew/lib/pkcs11/opensc-pkcs11.so

# Use PKCS#11 provider for a specific host
# In ~/.ssh/config:
# PKCS11Provider /opt/homebrew/lib/pkcs11/opensc-pkcs11.so

# Load into SSH agent (Homebrew ssh-agent, not Apple's)
ssh-add -s /opt/homebrew/lib/pkcs11/opensc-pkcs11.so

macOS restriction:

Apple’s built-in ssh-agent rejects third-party PKCS#11 providers due to Library Validation (code signing enforcement). Only providers with an Apple code signature are allowed. Homebrew’s ssh-agent accepts a provider whitelist via the -P flag, which is why homelab Bastion workflows require spawning a separate agent.

In the Wild

PKCS#11 is the reason a YubiKey can work with SSH, Firefox, LibreOffice, OpenVPN, and dozens of other applications without any of those applications being specifically written for YubiKey. They all speak PKCS#11; OpenSC acts as the translator for the hardware. In enterprise environments, PKCS#11 modules are how corporate smart card readers integrate with email clients for S/MIME signing, and how VPN clients authenticate using client certificates stored on hardware tokens. For homelab operators, the main friction point is macOS’s Library Validation, which requires running a separate Homebrew SSH agent for Bastion agent forwarding. Once that pattern is in place (the bastion() shell function), the PKCS#11 layer is largely transparent - enter PIN, touch key, done.