HSM Setup

This page describes how the keystore uses the HSM: the tokens, the resident keys, the format of a wrapped key and its lifecycle, and the HSMs for development and production.

Overview

The keystore talks to the HSM through PKCS#11, called with the Java Foreign Function & Memory (FFM) API. SoftHSM2 serves development and tests; production uses a Thales Luna HSM. ML-DSA keys need a PKCS#11 3.2 module.

Two kinds of keys protect the wrapped keys: the wrap key encrypts each private key (AES key wrap), and the bind key seals the result together with its owner and purpose (AES-GCM). An operator or the provisioning tool generates the wrap and bind keys and the attestation-signing keys inside the HSM. The keystore generates resident backend keys inside the HSM when a backend component asks for one. See Key Naming Conventions for the labels.

Tokens

The keys sit in three tokens, because their lifecycles differ. Token names follow keystore-<env>-<purpose>, where <env> is the environment (keystore.hsm.environment, for example dev or prod).

Token Holds Lifecycle
keystore-<env>-user The wrap and bind keys of the user domains (user-ecdsa, and user-mldsa with ML-DSA) Not replicated, so user keys can be restored only inside this HSM. On Luna: a Key Export partition.
keystore-<env>-eudi sign-eudi-ecdsa-ka, sign-eudi-ecdsa-wia, their certificates and the provider CA certificate May be shared between HSMs for availability. On Luna: a cloning partition.
keystore-<env>-backend The resident backend keys, and the wrap and bind keys of the wrapped backend domains Has its own PIN, session pool and login. Opened only when a backend domain is configured.

Resident Keys

Resident keys are persistent, non-extractable, sign-only objects generated inside the HSM:

  • the provider keys sign-eudi-ecdsa-ka and sign-eudi-ecdsa-wia on keystore-<env>-eudi, created at provisioning;
  • the resident backend keys on keystore-<env>-backend, created through the Backend Keys API.

The keystore finds them by label (backend keys also by keyId) and signs with them inside the HSM. Their private keys never leave it.

Wrapped Key Format

A wrapped key is a JWE in compact form (RFC 7516): header..iv.ciphertext.tag, with an empty encrypted-key segment. The encoded header is the additional authenticated data (AAD) of the AES-GCM seal, so a changed header or ciphertext fails the tag check.

The format is implemented in the keystore (WrappedKeyFormat, WrappedKeyPlaintextSerializer). The sample and the first table show the implemented format with the development labels (wrap-<n>, bind-<n>). The target design adds members and renames a purpose.

header      {"typ":"sk-wrapped-key+jwe","alg":"dir","enc":"A256GCM","kid":"1","iss":"secure-keystore"}
plaintext   {"iat":1793268000,"unit_id":"2a7d9c14-…","purpose":"CREDENTIAL_BINDING","wrapped_key":"<private key wrapped under wrap-1>"}
sealed      with bind-1 (AES-GCM), serialized as header . "" . IV . ciphertext . tag
Part Field Meaning
header typ, alg, enc Fixed: sk-wrapped-key+jwe, direct encryption (dir), AES-256-GCM (A256GCM).
header kid The key-set generation <n> of the wrap and bind keys, 1 to 65535.
header iss Fixed: secure-keystore.
plaintext iat Creation time in Unix seconds, up to 12 digits. The format checks it when a maximum age is configured (optional, off by default): an older key, or one more than 300 s in the future, is refused.
plaintext unit_id The unit the key belongs to. Must equal the request’s unit.
plaintext purpose The key purpose. Must match the endpoint.
plaintext wrapped_key The private key wrapped under wrap-<n> (RFC 5649), base64url.

The compact form has five segments joined by four dots:

# Segment Content
1 Protected header Base64url of the JSON header; its ASCII bytes are the AAD
2 Encrypted key Always empty (hence ..), because the content key is the bind key itself
3 IV 12 or 16 bytes (16 when the token generates it)
4 Ciphertext AES-256-GCM output without the tag
5 Tag The 16-byte GCM tag

Limits enforced by the implementation:

  • the compact form is at most 1366 characters;
  • the wrapped private key is at most 582 bytes;
  • the header is parsed strictly, so no member can be added or changed.

IV limit: at most 2^32 random-IV AES-GCM encryptions (NIST SP 800-38D §8.3) per bind key, across all keystore instances, HSMs and IV lengths. Rotate the key-set generation before that. Encryptions are not counted yet.

Target Design

The target design differs from the implemented format v1:

  • the purposes are CREDENTIAL, WALLET_INSTANCE and USER_SIGNING (CREDENTIAL_BINDING is renamed CREDENTIAL);
  • the labels are wrap-<domain>-<n> and bind-<domain>-<n>, for example wrap-user-ecdsa-1;
  • three members are added:
Part Field Meaning
header ver Format version, 1.
header kty User-signing keys only: ECDSA_P384, MLDSA_65 or MLDSA_87. EUDI keys (P-256) carry none. It selects the key set (user-ecdsa or user-mldsa), the unwrap template and the sign mechanism. Before a user-signing key signs, the keystore checks that the unwrapped key matches kty.
plaintext device_keys One thumbprint per device public key of the activation: the base64url SHA-256 of the key in its PowerAuth encoding. Read from PowerAuth Server, never from the request.

In the target design, an EUDI key is P-256 and a user-signing key is P-384 or ML-DSA. Each is wrapped and sealed under the wrap and bind keys of its family (user-ecdsa or user-mldsa).

Key Lifecycle

The lifecycle of a wrapped key, from the HSM’s point of view:

  1. Create. The HSM generates the key pair inside the token.
  2. Wrap and seal. The HSM wraps the private key under the domain’s wrap key and seals the result under its bind key, together with the owner, the purpose and, in the target design, the device-key thumbprints. The clear private key is destroyed.
  3. Store. The keystore returns the public key and the wrapped key, and keeps no per-user key. The app stores the wrapped key and sends it with each request that needs it.
  4. Open and check. The HSM opens the seal. A tampered key fails the tag check. The keystore then compares the owner with the request and the purpose with the endpoint: a key of another unit or purpose decrypts, but is refused here.
  5. Unwrap and sign. The HSM unwraps the key for this one operation and signs.
  6. Destroy. The unwrapped key is destroyed inside the HSM right after the signature.

Rotation is by generation <n>. A new generation of the wrap and bind keys is added and new wrapped keys use it. Older generations keep working until no wrapped key of that generation is in use; then they are retired. The generation is written into every wrapped user key as kid; see Key Naming Conventions. The flows that use these steps are in EUDI Wallet Flows and Backend Key Storage.

Development Setup with SoftHSM2

Development only: SoftHSM2 has no tamper protection.

SoftHSM2 stands in for the HSM during development and tests. Tokens, objects, scripts, provisioning, the mechanism table and CI are described in SoftHSM2 Development Setup.

Thales Luna Network HSM 7 (Not Verified)

Nothing in this repository has been run against a Luna HSM, and SoftHSM2 results are no evidence of Luna compatibility. Use a designated test partition only, never a production one.

  • Wrap mechanism. Luna offers AES key wrap as the vendor-defined CKM_AES_KWP (0x80000171, RFC 5649), not CKM_AES_KEY_WRAP_PAD. The keystore profile sets it; no tool substitutes a mechanism for you.
  • Attributes. The keys use only attributes that the Luna SDK reference documents. CKA_WRAP_TEMPLATE, CKA_ALLOWED_MECHANISMS, CKA_COPYABLE and CKA_DESTROYABLE are not documented there and are not used.
  • Tools. Whether p11keygen and p11od work on a Luna partition is not verified: p11keygen also sets CKA_ID, and p11od reads attributes that the Luna documentation does not name.
  • Unwrap template. Whether Luna enforces CKA_UNWRAP_TEMPLATE is not verified; the Luna PKCS#11 compliance table in the Thales SDK reference says nested templates are not supported. A read-back of the attributes of every unwrapped key is planned as a second check for the runtime; this version has no PKCS#11 unwrap path yet, so it is not available.
  • Key Export. Wrapping private keys needs a partition with Key Export enabled. Do not enable it or change partition policies without authorization.
  • FIPS mode. In FIPS mode, CKM_AES_GCM generates the IV in the HSM and appends it to the output.
Last updated on Nov 11, 2026 (21:37) Edit on Github Send Feedback

develop

Secure Keystore