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-access reference 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-access entry. The PIN is resolved from configuration when it is needed.
  • A token is node-local. libraryPath is 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.conf and resource-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:

_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.

FieldRequiredDescription
_typeYes"Pkcs11Access" in .conf files (HOCON class name)
idYesProfile name. Must match the key under resourceAccess.
libraryPathYesAbsolute path to the vendor PKCS#11 shared library on the Enterprise host.
slotIdOne ofThe token's CK_SLOT_ID. Prefer this — it is stable when other tokens appear.
slotListIndexOne ofPosition in the library's slot list. Convenient for SoftHSM when the slot ID is generated at init.
credentials._typeYes"Pkcs11PinCredentials"
credentials.pinYesToken user PIN (not the SO PIN).
providerConfigurationLinesNoExtra SunPKCS11 config lines for vendor-only requirements.

Exactly one of slotId or slotListIndex must be set. Prefer slotId for production HSMs.

SoftHSMv2
Thales Luna

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

resource-access.conf
resourceAccess = {
  softhsm = {
    _type = "Pkcs11Access"
    id = "softhsm"
    libraryPath = "/usr/lib/softhsm/libsofthsm2.so"
    slotId = 760482873
    credentials = {
      _type = "Pkcs11PinCredentials"
      pin = "123456"
    }
  }
}

Common library paths:

PlatformTypical 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).

CURL

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 be test.enterprise-sandbox.waltid.dev when 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 id from resource-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

CURL

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 be test.enterprise-sandbox.waltid.dev when 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 id from resource-access.conf. Omit if the KMS was created with configRef.
  • 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-access profile 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:

CURL

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 id from resource-access.conf. Omit if the KMS was created with configRef.
  • 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

CURL

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 _id from keys/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

EndpointEffect on Enterprise recordEffect on token
DELETE …/keys/deleteRemovedKey destroyed on the token
DELETE …/keys/removeRemovedToken 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 libraryPath and access to the same token. A resource-access entry is shared config; what it points at is node-local hardware (or SoftHSM state), unlike a cloud KMS endpoint.
  • PIN rotation. Edit resource-access.conf and 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

SymptomCause
Profile missing from ResourceAccessConfiguration loaded at startupresource-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_INVALIDWrong slotId / slotListIndex. Re-check with softhsm2-util --show-slots, LunaCM, or pkcs11-tool --list-slots
CKR_TOKEN_NOT_PRESENT / empty tokenToken never initialized, or SoftHSM's SOFTHSM2_CONF not set for the Enterprise process
CKR_PIN_INCORRECTWrong user PIN in resource-access.conf (not the SO PIN)
PKCS11 keys do not support Ed25519Expected — 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 keyThe token ignored the non-extractability request. Enterprise refuses the key and rolls it back — do not use that token for key generation

Next Steps

Last updated on August 18, 2026