Skip to content
vast-cow's blog
Go back

Building a “Software HSM” Workflow with SoftHSM2, OpenSC, and Python (Private Key Never Exported)

Edit page

This article explains a practical, end-to-end workflow for using SoftHSM2 as a software-backed HSM (PKCS#11 provider), managing it with OpenSC tools, and performing encryption/decryption from Python without exporting the private key.

1) What You Are Building (Roles and Responsibilities)

A clean mental model helps avoid common mistakes:

2) Core Concept: “Private Key Non-Export” and the Correct Crypto Pattern

A frequent misconception is “encrypt with private key and decrypt with public key.” That is not confidentiality encryption—conceptually it is closer to signing.

For confidentiality, the standard pattern is:

  1. Encrypt with the public key (anyone can do this).
  2. Decrypt with the private key (only the key owner can do this).

In a PKCS#11 workflow, step (2) is performed by calling C_Decrypt on the token’s private key object. The private key itself never leaves SoftHSM2.

For real-world payloads, you typically use hybrid encryption:

3) Prerequisites: Install Tools and Locate the PKCS#11 Module

Install (examples)

Locate SoftHSM2’s PKCS#11 module

You must know where the libsofthsm2.so (Linux) / .dylib (macOS) file is.

Examples:

In the examples below, we refer to the module path as:

4) Step 1 — SoftHSM2: Token Setup (Key Storage and Cryptographic Operations)

SoftHSM2 stores tokens as files under a configured token directory (tokendir). The configuration file is commonly:

You can override with SOFTHSM2_CONF if needed.

4.1 Inspect available slots

softhsm2-util --show-slots

4.2 Initialize a token (set SO PIN and User PIN)

softhsm2-util --init-token --slot 0 --label "demo-token"

You will be prompted for:

5) Step 2 — OpenSC: Manage and Validate with pkcs11-tool

OpenSC’s pkcs11-tool is the most practical way to generate keys and verify objects.

5.1 List tokens

pkcs11-tool --module "$PKCS11_LIB" -L

5.2 Generate an RSA key pair (private key stays inside the token)

pkcs11-tool --module "$PKCS11_LIB" \
  --login --pin <USER_PIN> \
  --keypairgen --key-type rsa:2048 \
  --id 01 --label "rsa-key-01"

Notes:

5.3 List objects (confirm key pair exists)

pkcs11-tool --module "$PKCS11_LIB" -O --login --pin <USER_PIN>

You should see entries such as:

5.4 Export the public key for testing/distribution (safe to export)

Export public key (DER):

pkcs11-tool --module "$PKCS11_LIB" \
  --read-object --type pubkey --id 01 \
  -o pubkey.der

Convert to PEM with OpenSSL:

openssl pkey -pubin -inform DER -in pubkey.der -out pubkey.pem

6) Step 3 — Python: Encrypt/Decrypt with a Non-Exported Private Key

6.1 Choose a Python PKCS#11 binding

Common options:

Regardless of library, the application flow is the same:

  1. Load the PKCS#11 module (libsofthsm2.so)
  2. Select token by label (demo-token)
  3. Open a session
  4. Login with User PIN
  5. Find the private key object by label or ID
  6. Call decrypt/sign using PKCS#11 mechanisms
  7. Logout and close session

7) Minimal Proof: Public-Key Encrypt → Token-Private-Key Decrypt

This is the simplest validation that you are actually using the token’s private key without exporting it.

7.1 Encrypt with OpenSSL using the exported public key

Example using RSA-OAEP with SHA-256 (plaintext must be small enough for RSA):

echo -n "hello" > plain.bin

openssl pkeyutl -encrypt -pubin -inkey pubkey.pem \
  -pkeyopt rsa_padding_mode:oaep \
  -pkeyopt rsa_oaep_md:sha256 \
  -pkeyopt rsa_mgf1_md:sha256 \
  -in plain.bin -out cipher.bin

7.2 Decrypt in Python via PKCS#11 (private key stays in token)

Your Python code will typically:

Pseudocode outline:

PKCS11_LIB = "/path/to/libsofthsm2.so"
TOKEN_LABEL = "demo-token"
USER_PIN = "1234"
KEY_LABEL = "rsa-key-01"

cipher = open("cipher.bin", "rb").read()

# 1) Load PKCS#11 module
# 2) Locate token by label
# 3) Open session, login(USER_PIN)
# 4) Find private key object by label or ID
# 5) Decrypt with RSA-OAEP-SHA256 mechanism
# 6) Output plaintext

Key requirement:

For any non-trivial data size, use hybrid encryption:

  1. Generate a random AES key.
  2. Encrypt data with AES-GCM (Python).
  3. Encrypt (wrap) AES key with RSA public key (OpenSSL or Python).
  4. Decrypt (unwrap) AES key with RSA private key inside SoftHSM2 via PKCS#11.
  5. Decrypt data with AES key (Python).

This makes HSM usage efficient and standard-compliant: the HSM performs a small private-key operation; symmetric crypto handles bulk data.

9) Operational Notes and Troubleshooting

SoftHSM2 security posture

SoftHSM2 is a software HSM. It can satisfy a “private key not exported” workflow at the API/object level, but it does not provide hardware tamper resistance. File permissions and host security are therefore critical.

Useful verification commands

If something fails, isolate where:

Common issues include:

Summary

By combining:

you can implement a reliable pipeline where the private key is never exported, yet your application can still perform necessary cryptographic operations securely via PKCS#11.


Edit page
Share this post:

Comments


Previous Post
Automated Data Extraction for the BUFFALO WSR-3200AX4S Admin UI
Next Post
Capturing DHCP-Assigned Network Parameters Using a Temporary macvlan in a Linux Network Namespace