PKCS#11 HSM
This guide shows you how to store and use signing keys on a PKCS#11 token through the Enterprise Stack Key Management Service. Private keys are created on the token and never leave it. Enterprise stores only the public key plus a reference to the token object.
The same integration works with any PKCS#11 library. This page covers two common setups end to end under Step 2:
- SoftHSMv2 — a software token for local development and CI
- Thales Luna — a production HSM (network or PCIe)
What You Can Do
- Generate signing keys on a PKCS#11 token and use them across Enterprise Stack services (credential issuance, DID operations, and so on)
- Adopt keys an HSM operator already provisioned under a key ceremony, by alias
- Keep the token PIN out of key records — only a
resource-accessreference is stored
How It Works
Enterprise → crypto2 PKCS#11 provider → SunPKCS11 (JCA) → vendor .so → token
| |
+- stores: public key + { libraryPath, slot, alias, pinReference } +-- holds the private key
Two properties of PKCS#11 matter for deployment:
- The PIN is never stored with a key. The key record holds only a reference to a
resource-accessentry. The PIN is resolved from configuration when it is needed. - A token is node-local.
libraryPathis a path on the filesystem of the machine running Enterprise. In a multi-replica deployment every replica needs the same library and access to the same token.
PKCS#11 cannot run against the hosted Enterprise sandbox. You need a self-hosted Enterprise Stack deployment where the
vendor PKCS#11 library and token are available to the API process. Use your deployment's base URL in place of
https://{orgID}.enterprise-sandbox.waltid.dev in the examples below.
Prerequisites
Before you begin, ensure you have:
- Enterprise Stack running — A self-hosted instance with its config directory at
waltid-enterprise-api/config/. - PKCS#11 token and library — The vendor shared library installed on every host that runs the Enterprise API.
- Config access — Permission to edit
_features.confandresource-access.conf, then restart the API.
Step 1: Enable Resource Access
PKCS#11 profiles are loaded from resource-access.conf. That feature is off by default.
Add resource-access to enabledFeatures in
_features.conf:
enabledFeatures = [
# ... existing entries ...
resource-access
]
Restart the API after changing feature flags. On startup the log should list your profile ids:
ResourceAccessConfiguration loaded at startup: [..., softhsm, ...]
If the id is missing, the feature is not enabled or the config file was not picked up.
Step 2: Configure a PKCS#11 Profile
Add a named profile to
resource-access.conf. The id is what you pass
as configRef when creating the KMS, and as backend when generating keys.
| Field | Required | Description |
|---|---|---|
_type | Yes | "Pkcs11Access" in .conf files (HOCON class name) |
id | Yes | Profile name. Must match the key under resourceAccess. |
libraryPath | Yes | Absolute path to the vendor PKCS#11 shared library on the Enterprise host. |
slotId | One of | The token's CK_SLOT_ID. Prefer this — it is stable when other tokens appear. |
slotListIndex | One of | Position in the library's slot list. Convenient for SoftHSM when the slot ID is generated at init. |
credentials._type | Yes | "Pkcs11PinCredentials" |
credentials.pin | Yes | Token user PIN (not the SO PIN). |
providerConfigurationLines | No | Extra SunPKCS11 config lines for vendor-only requirements. |
Exactly one of slotId or slotListIndex must be set. Prefer slotId for production HSMs.
SoftHSMv2 is a software PKCS#11 token. Use it for local development and CI — not as a production root of trust.
1. Install and create a token
# Arch
sudo pacman -S --needed softhsm
# Debian/Ubuntu
sudo apt install softhsm2
export SOFTHSM2_CONF=/opt/waltid/softhsm/softhsm2.conf
mkdir -p /opt/waltid/softhsm/tokens
cat > "$SOFTHSM2_CONF" <<'EOF'
directories.tokendir = /opt/waltid/softhsm/tokens
objectstore.backend = file
log.level = ERROR
slots.removable = false
EOF
softhsm2-util --init-token --free \
--label enterprise-hsm \
--so-pin 12345678 \
--pin 123456
2. Find the slot
softhsm2-util --show-slots | grep -m1 '^Slot [0-9]'
# Slot 760482873
SoftHSM generates the slot ID when the token is initialized, so the value differs per machine. Note it down, or use
slotListIndex = 0 when this is the only token in the library.
3. Add the profile
resourceAccess = {
softhsm = {
_type = "Pkcs11Access"
id = "softhsm"
libraryPath = "/usr/lib/softhsm/libsofthsm2.so"
slotId = 760482873
credentials = {
_type = "Pkcs11PinCredentials"
pin = "123456"
}
}
}
Common library paths:
| Platform | Typical libraryPath |
|---|---|
| Arch / some Linux | /usr/lib/softhsm/libsofthsm2.so |
| Debian / Ubuntu | /usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so |
4. Export SOFTHSM2_CONF for the Enterprise process
The SoftHSM library — not Enterprise — reads this environment variable:
export SOFTHSM2_CONF=/opt/waltid/softhsm/softhsm2.conf
# systemd: Environment=SOFTHSM2_CONF=/opt/waltid/softhsm/softhsm2.conf
# docker: -e SOFTHSM2_CONF=... and mount the library + token directory
Step 3: Create a KMS Bound to the Token
Create a KMS service and point it at the profile with configRef. The value must match the profile id
(softhsm or luna in the examples above).
Endpoint: POST /v1/{target}/resource-api/services/create | API Reference
Example Request
curl -X 'POST' \
'https://{orgID}.enterprise-sandbox.waltid.dev/v1/{target}/resource-api/services/create' \
-H 'accept: application/json' \
-H 'Authorization: Bearer {yourToken}' \
-H 'Content-Type: application/json' \
-d '{
"type": "kms",
"configRef": "softhsm"
}'
Path Parameters
- orgID: String (required) - Your organization ID. When performing operations within an organization, use the
organization's Base URL. For example, if your organization is named
test, your default Base URL will betest.enterprise-sandbox.waltid.devwhen using the sandbox environment. - target: String (required) - The resource identifier indicating the organization + tenant + new KMS service
(
{organizationID}.{tenantID}.{newKmsServiceID}). Example:waltid.tenant1.hsmkms.
Header Parameters
- Authorization: String (required) - Bearer token for Enterprise Stack authentication. Format:
Bearer {token}.
Body Parameters
- type: String (required) - Service type. Must be
"kms". - configRef: String (required) - Profile
idfromresource-access.conf. Examples:"softhsm","luna".
Response Codes
201— Service created successfully.401— Invalid or missing authentication token.
With configRef set, later key-generation calls can omit backend — the KMS defaults to that profile. You can still
pass "backend": "softhsm" (or "luna") explicitly.
Supported Key Algorithms
Subject to what the token actually supports:
secp256r1(P-256)secp384r1(P-384)secp521r1(P-521)RSA(2048-bit)RSA3072(3072-bit)RSA4096(4096-bit)
Ed25519 and secp256k1 are not supported for PKCS#11 keys. Enterprise probes the token and refuses unsupported types
at generation time with a 400 response.
Keys are sign / verify only. RSA encryption and key wrapping are not offered through this integration.
Generate a Key on the Token
Endpoint: POST /v1/{target}/kms-service-api/keys/generate | API Reference
Example Request
curl -X 'POST' \
'https://{orgID}.enterprise-sandbox.waltid.dev/v1/{target}/kms-service-api/keys/generate' \
-H 'accept: application/json' \
-H 'Authorization: Bearer {yourToken}' \
-H 'Content-Type: application/json' \
-d '{
"backend": "softhsm",
"keyType": "secp256r1"
}'
For Thales Luna, use "backend": "luna" (matching your profile id).
Path Parameters
- orgID: String (required) - Your organization ID. When performing operations within an organization, use the
organization's Base URL. For example, if your organization is named
test, your default Base URL will betest.enterprise-sandbox.waltid.devwhen using the sandbox environment. - target: String (required) - The KMS service resource identifier
(
{organizationID}.{tenantID}.{kmsServiceID}). Example:waltid.tenant1.hsmkms.
Header Parameters
- Authorization: String (required) - Bearer token for Enterprise Stack authentication. Format:
Bearer {token}.
Body Parameters
- backend: String (optional) - PKCS#11 profile
idfromresource-access.conf. Omit if the KMS was created withconfigRef. - keyType: String (required) - Algorithm to generate. One of the supported algorithms.
Example Response
The response is a public JWK only — there is no private key material (d):
{
"_id": "waltid.tenant1.hsmkms.019fae71-d211-71eb-9e33-e27ffa96c35a",
"key": {
"type": "jwk",
"jwk": {
"kty": "EC",
"crv": "P-256",
"x": "...",
"y": "..."
}
},
"metadata": {
"resource-id": "softhsm"
}
}
Response Fields
- _id: String - Full key path. Use this as
{target}for sign, delete, and remove calls. - key: Object - Public key material only (
type: "jwk"). The private key remains on the token. - metadata.resource-id: String - The
resource-accessprofile id used for this key.
Response Codes
201— Key created on the token and stored in the KMS.400— Unsupported key type, or the token refused the request.401— Invalid or missing authentication token.
🎉 You've generated a signing key that lives on the PKCS#11 token.
Adopt an Existing Key by Alias
In production, HSM keys are often created by whoever administers the token — often under a key ceremony — and Enterprise
is given only an alias. Pass that alias in config instead of generating a new key:
Endpoint: POST /v1/{target}/kms-service-api/keys/generate | API Reference
Example Request
curl -X 'POST' \
'https://{orgID}.enterprise-sandbox.waltid.dev/v1/{target}/kms-service-api/keys/generate' \
-H 'accept: application/json' \
-H 'Authorization: Bearer {yourToken}' \
-H 'Content-Type: application/json' \
-d '{
"backend": "luna",
"keyType": "secp256r1",
"config": {
"alias": "issuer-signing-key"
}
}'
Path Parameters
- orgID: String (required) - Your organization ID. When performing operations within an organization, use the organization's Base URL.
- target: String (required) - The KMS service resource identifier
(
{organizationID}.{tenantID}.{kmsServiceID}). Example:waltid.tenant1.hsmkms.
Header Parameters
- Authorization: String (required) - Bearer token for Enterprise Stack authentication. Format:
Bearer {token}.
Body Parameters
- backend: String (optional) - PKCS#11 profile
idfromresource-access.conf. Omit if the KMS was created withconfigRef. - keyType: String (required) - Still required by the API shape; the key's real type is read from the token and must be usable for sign / verify.
- config: Object (required) - Adoption options.
- alias: String (required) - Alias of an existing private key with a certificate on the token.
Response Codes
201— Existing token key attached and stored in the KMS.400— Alias missing on the token, or the key has no certificate.401— Invalid or missing authentication token.
Nothing is generated. Reading the key from the token proves the alias exists and the configured PIN is correct.
SunPKCS11 addresses keys through a certificate. A key without a certificate on the token is not adoptable. When Enterprise generates a key itself, it creates a self-signed placeholder certificate for that reason.
Sign With a Token Key
Endpoint: POST /v1/{target}/kms-service-api/keys/sign | API Reference
Example Request
curl -X 'POST' \
'https://{orgID}.enterprise-sandbox.waltid.dev/v1/{target}/kms-service-api/keys/sign' \
-H 'accept: application/json' \
-H 'Authorization: Bearer {yourToken}' \
-H 'Content-Type: application/json' \
-d '{
"plaintext": "hello from the HSM"
}'
Path Parameters
- orgID: String (required) - Your organization ID. When performing operations within an organization, use the organization's Base URL.
- target: String (required) - Full key path returned as
_idfromkeys/generate. Example:waltid.tenant1.hsmkms.019fae71-d211-71eb-9e33-e27ffa96c35a.
Header Parameters
- Authorization: String (required) - Bearer token for Enterprise Stack authentication. Format:
Bearer {token}.
Body Parameters
- plaintext: String (required) - The message to sign.
Example Response
{
"signature": "MEYCIQ..."
}
Response Fields
- signature: String - Base64-encoded signature produced on the token.
Response Codes
200— Signature created successfully.401— Invalid or missing authentication token.
Delete vs Remove
| Endpoint | Effect on Enterprise record | Effect on token |
|---|---|---|
DELETE …/keys/delete | Removed | Key destroyed on the token |
DELETE …/keys/remove | Removed | Token left untouched |
Use keys/remove when you only want to drop the Enterprise reference (for example after migrating the record). Use
keys/delete only when the token object itself should be destroyed.
keys/delete destroys the private key on the HSM. That is irreversible for keys that cannot be restored from backup.
Operational Notes
- Multi-replica deployments. Every replica needs the vendor library at the same
libraryPathand access to the same token. Aresource-accessentry is shared config; what it points at is node-local hardware (or SoftHSM state), unlike a cloud KMS endpoint. - PIN rotation. Edit
resource-access.confand restart. Key records reference the profile; they do not copy the PIN. - Wrong PIN detection. PKCS#11 login state is per process per slot. Once a token is logged in, a later wrong PIN
may be accepted (
CKR_USER_ALREADY_LOGGED_IN). A wrong PIN is reliably rejected only on the first login to that token within a process. - Clean shutdown. Enterprise logs out of the token when it stops. A hard kill leaves the token logged in until the process is gone.
Troubleshooting
| Symptom | Cause |
|---|---|
Profile missing from ResourceAccessConfiguration loaded at startup | resource-access not in enabledFeatures, or config file not loaded |
PKCS11 resource-access reference not found: {id} | backend / configRef does not match an id in resource-access.conf |
PKCS11 library does not exist: … | Wrong libraryPath, or the file is not visible to the Enterprise process (common in containers) |
Initialization failed / CKR_SLOT_ID_INVALID | Wrong slotId / slotListIndex. Re-check with softhsm2-util --show-slots, LunaCM, or pkcs11-tool --list-slots |
CKR_TOKEN_NOT_PRESENT / empty token | Token never initialized, or SoftHSM's SOFTHSM2_CONF not set for the Enterprise process |
CKR_PIN_INCORRECT | Wrong user PIN in resource-access.conf (not the SO PIN) |
PKCS11 keys do not support Ed25519 | Expected — use secp256r1 / secp384r1 / secp521r1 or RSA |
PKCS11 alias does not exist: {alias} | Adopting a key that is not on the token, or that has no certificate |
PKCS11 token produced an extractable private key | The token ignored the non-extractability request. Enterprise refuses the key and rolls it back — do not use that token for key generation |
Next Steps
- Set up a KMS service — Setup.
- Configure resource-access profiles — Resource Access.
- Use AWS KMS — AWS KMS.
- Use Azure Key Vault — Azure Key Vault.
