SoftHSM2 Development Setup
SoftHSM2 is a software token without tamper protection: it gives no security assurance, and the keys, PINs and test certificates it holds must never be used outside local development and tests. The procedure has not been tried on a Thales Luna HSM.
The keystore needs AES wrap and bind keys and EC provider signing keys in an HSM. During development, SoftHSM2 stands in for it. SoftHSM2 is a library, not a server: the keystore process loads libsofthsm2 and reads the token files from a directory. The scripts live in scripts/softhsm2 and are Python 3.9+ (standard library only). They run standard tools (softhsm2-util, pkcs11-tool, OpenSSL 3 and pkcs11-tools). Details, the list of negative tests and troubleshooting are in the scripts README.
Requirements
- SoftHSM2 2.7.0 on the development machine; 2.6.1 (from apt) in CI on Ubuntu 24.04.
setup.pyneeds only--show-slotsand--init-token. Version 2.6.1 lacks--show-config, so the scripts then search the standard install directories. - OpenSC
pkcs11-tool. - Python 3.9+ (standard library only) and a POSIX
sh(used bytest_setup.pyonly). - OpenSSL 3.x first on
PATH(macOS: the systemopensslis LibreSSL, Homebrew’sopenssl@3is keg-only). - For provisioning:
p11keygen,p11od,p11ls,p11reqandp11importcertfrom pkcs11-tools (selftest.pyalsop11setattr).
brew install softhsm opensc openssl@3 # macOS, library: /opt/homebrew/lib/softhsm/libsofthsm2.so
export PATH="$(brew --prefix openssl@3)/bin:$PATH"
sudo apt install softhsm2 opensc openssl # Ubuntu, library: /usr/lib/softhsm/libsofthsm2.so
Tokens and Objects
setup.py creates three empty tokens:
sk-custody: the custody token, PINKEYSTORE_HSM_PIN;sk-shared: provider keys and certificates only, PINKEYSTORE_HSM_SHARED_PIN;sk-recovery-test: a throw-away token for the recovery tests.
A key set is one generation <n>: a wrap-<n> and a bind-<n> key. provision.py then creates the objects:
| Label | Token | Type | Usage |
|---|---|---|---|
wrap-<n> |
sk-custody |
AES-256 | WRAP + UNWRAP only, with the unwrap template below |
bind-<n> |
sk-custody |
AES-256 | ENCRYPT + DECRYPT only (AES-GCM) |
ka-signing, wia-signing |
sk-shared |
EC P-256 key + 365-day TEST ONLY leaf certificate |
SIGN only |
provider-ca |
sk-shared |
certificate | SHA-256 equals pki/provider-ca.sha256 |
Every key is born in the HSM, private, sensitive, not extractable and not modifiable. The check also requires CKA_LOCAL, CKA_ALWAYS_SENSITIVE and CKA_NEVER_EXTRACTABLE, which rejects imported keys and keys that were once extractable. The unwrap template forces the attributes of every key unwrapped under wrap-<n>:
class=private_key, key_type=ec, token=false, private=true, sensitive=true,
extractable=false, sign=true, derive=false, modifiable=false
The development tokens and labels differ from the target names in Key Naming Conventions; the semantics are the same: sk-custody is keystore-<env>-user, sk-shared is keystore-<env>-eudi, wrap-<n>/bind-<n> are wrap-user-ecdsa-<n>/bind-user-ecdsa-<n>, and ka-signing/wia-signing are sign-eudi-ecdsa-ka/sign-eudi-ecdsa-wia.
Setting Up the Tokens
Set the PINs and the library path in .env (copy of .env.example; replace the <...> placeholders with your own throw-away values; never commit it), then run the setup script:
set -a; source .env; set +a
scripts/softhsm2/setup.py # --dir <path> changes the directory (default .softhsm/), --reset recreates everything
Environment variables:
| Variable | Description |
|---|---|
KEYSTORE_HSM_PIN |
PIN of sk-custody. |
KEYSTORE_HSM_SHARED_PIN |
PIN of sk-shared. |
KEYSTORE_HSM_SO_PIN |
Security officer PIN. |
KEYSTORE_HSM_LIBRARY |
Path of the PKCS#11 library, for example /opt/homebrew/lib/softhsm/libsofthsm2.so (macOS) or /usr/lib/softhsm/libsofthsm2.so (Ubuntu). |
SOFTHSM2_CONF |
The softhsm2.conf that setup.py prints; the application finds the tokens through it. |
KEYSTORE_TEST_TRUST_ANCHOR |
root.pem, the test trust anchor, outside the chain. |
KEYSTORE_HSM_PROVIDER_CA_SHA256 |
Content of provider-ca.sha256. |
The script is idempotent. It creates the empty tokens and writes a throw-away test PKI (EC P-256, made with OpenSSL) to <dir>/pki/:
root.pem: the test root, valid 3650 days. Its key is deleted right after it issues the intermediate.provider-ca.pem: the intermediate CA, valid 1825 days, so the 365-day provider certificates fit inside.provider-ca.key(mode0600): used byprovision.pyto issue the provider certificates, then deleted.provider-ca.sha256: the SHA-256 of the DER ofprovider-ca.pem; set it asKEYSTORE_HSM_PROVIDER_CA_SHA256.
Every subject contains TEST ONLY.
Because provision.py deletes provider-ca.key after issuing the provider certificates, run setup.py --reset (which recreates all three tokens) when a provider certificate expires, when you need a new one, or when your .softhsm/ predates the provider objects. The script works only in a new or empty directory or one it created before (marker .sk-softhsm). The PINs of tokens that already exist are not verified. Run scripts/softhsm2/setup.py --help for the remaining rules.
The script passes the PINs to softhsm2-util on its command line, so they are visible in the process list while it runs; use throw-away development PINs only. .softhsm/ is git-ignored.
The local run is source .env, provisioning of the key sets (below), then mvn spring-boot:run -Dspring-boot.run.profiles=dev.
Provisioning the Custody Key Sets
Once the tokens exist, scripts/softhsm2/provision.py creates the key sets with p11keygen and reads them back with p11od, both from the standard pkcs11-tools toolkit (Mastercard; brew install pkcs11-tools or build from source). There is no project-specific PKCS#11 program. Each key set is two AES-256 keys on sk-custody: wrap-<n>, a key-wrapping key whose CKA_UNWRAP_TEMPLATE forces the attributes of every key unwrapped under it, and bind-<n>, a data key for AES-GCM. Both are born in the HSM, private, sensitive, not extractable and not modifiable. OpenSC pkcs11-tool cannot create them: it sets neither CKA_UNWRAP_TEMPLATE nor CKA_MODIFIABLE=false.
When run against the default token sk-custody, the script also creates the provider objects on sk-shared: for each of ka-signing and wia-signing, an EC P-256 sign-only key and a 365-day TEST ONLY certificate, plus the provider-ca certificate. The x5c chain is the leaf and the intermediate. The provider prerequisites are checked before any custody key is created, but provisioning of the provider objects is not atomic: a failure can leave a partial set that the next run refuses, and cleanup removes provider-ca.key. Recover with setup.py --reset, which recreates all three tokens, and provision again. --check also proves that each private key belongs to its certificate and verifies each leaf against the provider-ca read back from the HSM. --token sk-recovery-test creates a key set only.
set -a; source .env; set +a
scripts/softhsm2/provision.py 1 2 # one or more generations; --dir <path> as for setup.py, --token <label> (default sk-custody)
scripts/softhsm2/provision.py --check 1 2 # creates nothing, only reads back and compares with the profile
scripts/softhsm2/provision.py --token sk-recovery-test 1 # key set only
The script refuses a label that already exists (PKCS#11 does not enforce unique labels) before it creates anything, reads every key back after creating it and exits non-zero when one differs from the profile. It uses KEYSTORE_HSM_PIN, KEYSTORE_HSM_SHARED_PIN and KEYSTORE_HSM_LIBRARY (and the softhsm2.conf of --dir, unless SOFTHSM2_CONF is set); the custody PIN reaches the pkcs11-tools programs through their :::exec: PIN source (see pkcs11-tools), so it is on no command line. A startup check of the same attributes in the keystore (refusing keys that differ, fail closed) is planned but not implemented yet; until then, verify the objects with provision.py --check. Run scripts/softhsm2/provision.py --help for the rest.
The sk-shared PIN is passed to pkcs11-tool --pin when the provider certificates are read back: it is visible in the process list, which is acceptable only for the throw-away development token. Provision one generation at a time and never run two provisioning commands on a token concurrently. A failure between the two keys of a generation leaves a half set: delete it before retrying.
Checking the Available Mechanisms
SoftHSM2 must offer the mechanisms the keystore uses. List them with p11slotinfo -l "$KEYSTORE_HSM_LIBRARY" -t sk-custody or pkcs11-tool --module "$KEYSTORE_HSM_LIBRARY" --list-mechanisms and compare with the table. A startup check of flags and key sizes in the keystore (fail closed) is planned but not implemented yet; until then, check the mechanisms by hand as described here. A mechanism counts as available only when C_GetMechanismList lists it and C_GetMechanismInfo reports the required flags.
Measured results. The 2.6.1 column was measured on Ubuntu 22.04; CI runs the same SoftHSM2 release (2.6.1) on Ubuntu 24.04. softhsm2-util reports no build options.
| Mechanism (number) | Required flags | SoftHSM2 2.7.0 (Homebrew, macOS, 2026-11-02) | SoftHSM2 2.6.1 (Ubuntu 22.04, apt 2.6.1-2ubuntu1, 2026-11-02) |
|---|---|---|---|
CKM_EC_KEY_PAIR_GEN (0x1040) |
generate key pair | yes | yes |
CKM_ECDSA (0x1041) |
sign | yes | yes |
CKM_AES_KEY_GEN (0x1080) |
generate | yes | yes |
CKM_AES_KEY_WRAP_PAD (0x210A) |
wrap, unwrap | yes | yes (listed as mechtype-0x210A) |
CKM_AES_GCM (0x1087) |
encrypt, decrypt | yes | yes |
token RNG (C_GenerateRandom) |
- | yes | yes |
Tests
scripts/softhsm2/test_setup.pytests the setup script. It runs in a temporary directory and does not touch.softhsm/; exit code 2 means a prerequisite is missing. It ignoresKEYSTORE_HSM_LIBRARYand uses only alibsofthsm2.*file reported bysofthsm2-util --show-config(2.7+) or found in the standard install directories, so it never loads another PKCS#11 module.scripts/softhsm2/selftest.pytests the provisioning script (temporary directory, same rules; needsp11keygenandp11odas well; exit code 2 means a prerequisite is missing).
Neither is part of mvn test; the Test with SoftHSM2 workflow runs them.
CI
The Test with SoftHSM2 workflow (.github/workflows/softhsm-test.yml) runs on Ubuntu 24.04. It installs softhsm2 (2.6.1) and opensc from apt and the pkcs11-tools 3.1.0 .deb from the official release, verified against a pinned SHA-256 before installation. It then provisions the tokens and key sets, runs selftest.py, test_setup.py and mvn -B verify.
- To bump pkcs11-tools, change the version, the file name and the hash together in the workflow: take the hash from the
digestof the release asset and recompute it withsha256sum. - OpenSSL 3.0 of Ubuntu 24.04 exits 0 from
openssl req -verifyeven when the signature does not verify, so the scripts require the printedverify OK. - The PINs are random throw-away values generated per run, masked in the log and passed only through the environment; the workflow uses no secrets, and the tokens are neither cached nor uploaded.