End-to-end guide for running a Besu validator network with node-key operations delegated to an Utimaco GP HSM. Validated using the u.trust GP HSM Simulator v6.5.0.0 on Ubuntu 24.04 LTS (x86-64). Both signing (QBFT consensus) and ECDH key agreement (devp2p peer handshakes) are delegated to the HSM via the PKCS#11 R3 interface.
This guide uses the plugin's native-pkcs11 provider, which is the
default. It calls the Utimaco PKCS#11 R3 library (libcs_pkcs11_R3.so)
directly via a Java 25 Foreign Function & Memory (FFM) binding. The
library in turn connects to the HSM (or simulator) over TCP.
Simulator note: The u.trust GP HSM Simulator faithfully reproduces the PKCS#11 interface of real Utimaco hardware. Steps in this guide apply directly to physical Utimaco GP HSM hardware — replace the simulator address (
3001@127.0.0.1) with your device's IP address.
The deployment has three phases:
- Simulator setup. Install the SDK, start the
bl_sim5simulator daemon, and initialise a PKCS#11 token with a user PIN (one-time). - Generate keys on the HSM. Five
secp256k1keypairs (one per validator) generated on the simulator withpkcs11-tool. Private keys never exist in software. Their public points are extracted for the QBFT genesis. - Run Besu. Configure each validator with the plugin's HSM security module pointing at the initialised token; bring up the QBFT network.
- AWS Ubuntu 24.04 LTS instance (x86-64) with SSH access.
u.trust-GP-HSM-Simulator_v6.5.0.0.zipalready uploaded to the instance (e.g. at~).- Besu and the HSM plugin are downloaded directly on the instance via
curlin §8.
Unzip the archive and set a convenience variable pointing at the Linux software tree:
cd ~
unzip u.trust-GP-HSM-Simulator_v6.5.0.0.zip -d utimaco-sdkFind the extracted Linux directory (the exact nesting depends on how the archive was packaged):
ls ~/utimaco-sdk/Set SDK to the Software/Linux path you see:
# Adjust to match your actual extraction — for example:
SDK="$HOME/utimaco-sdk/u.trust-GP-HSM-Simulator_v6.5.0.0/Software/Linux"
# or:
# SDK="$HOME/utimaco-sdk/Software/Linux"
echo "$SDK" # should end in .../Software/LinuxCopy the files you need to $HOME/utimaco/:
mkdir -p $HOME/utimaco/key
cp -r "$SDK/Simulator/sim5_linux" $HOME/utimaco/
cp "$SDK/Crypto_APIs/PKCS11_R3/lib/libcs_pkcs11_R3.so" $HOME/utimaco/
cp "$SDK/Crypto_APIs/PKCS11_R3/sample/cs_pkcs11_R3.cfg" $HOME/utimaco/
cp "$SDK/Administration/p11tool2" $HOME/utimaco/
cp "$SDK/Administration/csadm" $HOME/utimaco/
cp "$SDK/Administration/key/ADMIN_SIM.key" $HOME/utimaco/key/
chmod +x $HOME/utimaco/p11tool2 \
$HOME/utimaco/csadm \
$HOME/utimaco/sim5_linux/bin/bl_sim5The simulator binary (bl_sim5) is a 32-bit ELF. Ubuntu 24.04 ships
only 64-bit libc by default, so you must enable the i386 architecture:
sudo dpkg --add-architecture i386
sudo apt-get update
sudo apt-get install -y libc6:i386 libstdc++6:i386Verify the binary runs:
$HOME/utimaco/sim5_linux/bin/bl_sim5 --help 2>&1 | head -5Expected output:
unknown option '--help'
Boot Loader, version 5.02.5.0 (May 5 2026) started
syntax:
bl_sim5 start SDK in Bootloader mode
bl_sim5 -o start SDK in SMOS mode
If you instead see cannot execute binary file: Exec format error, the
32-bit runtime packages were not installed correctly — re-run the
dpkg --add-architecture steps above.
sudo apt-get install -y opensc jq curl unzip tmuxThis plugin requires Java 25. Install Eclipse Temurin from Adoptium's APT repository:
wget -qO - https://packages.adoptium.net/artifactory/api/gpg/key/public \
| sudo gpg --dearmor -o /etc/apt/keyrings/adoptium.gpg
echo "deb [signed-by=/etc/apt/keyrings/adoptium.gpg] \
https://packages.adoptium.net/artifactory/deb \
$(awk -F= '/^VERSION_CODENAME/{print $2}' /etc/os-release) main" \
| sudo tee /etc/apt/sources.list.d/adoptium.list
sudo apt-get update && sudo apt-get install -y temurin-25-jdk
# If another JDK is already installed, activate Java 25 explicitly:
sudo update-alternatives --set java /usr/lib/jvm/temurin-25-jdk-amd64/bin/java
sudo update-alternatives --set javac /usr/lib/jvm/temurin-25-jdk-amd64/bin/javac
java --versionThe library reads a config file at the path given by the environment
variable CS_PKCS11_R3_CFG. The sample cs_pkcs11_R3.cfg already has
[HSMCluster] with Devices = 3001@127.0.0.1, which is correct for
the local simulator. The only change needed is to enable logging so you
can see library activity during testing — set Logging = 3 in the
[Global] section:
[Global]
Logging = 3 # change from 0 (NONE) to 3 (INFO)Export the variable for the current session (optionally persist it in
~/.bashrc for interactive testing):
export CS_PKCS11_R3_CFG=$HOME/utimaco/cs_pkcs11_R3.cfg
# optional — only for interactive testing convenience:
echo 'export CS_PKCS11_R3_CFG=$HOME/utimaco/cs_pkcs11_R3.cfg' >> ~/.bashrcProduction note: when Besu runs as a systemd service, environment variables set in
~/.bashrcare not inherited. SetCS_PKCS11_R3_CFGin the systemd unit file instead:[Service] Environment="CS_PKCS11_R3_CFG=/path/to/cs_pkcs11_R3.cfg"
Start bl_sim5 in a detached tmux session so it keeps running when you
detach:
tmux new -d -s simulator $HOME/utimaco/sim5_linux/bin/bl_sim5 -h -o
sleep 2Confirm it is listening on port 3001:
ss -tlnp | grep 3001Expected output: a LISTEN entry on 0.0.0.0:3001 or *:3001.
pkcs11-tool --module $HOME/utimaco/libcs_pkcs11_R3.so --list-slotsExpected output (abbreviated — logging lines are interleaved because
Logging = 3):
... | I: CryptoServer PKCS#11 Library R3 version 6.5.0.0 (May 5 2026)
... | I: Configfile: /home/<user>/utimaco/cs_pkcs11_R3.cfg
... | I: Device 3001@127.0.0.1
Available slots:
Slot 0 (0x0): ...
3001@127.0.0.1 - CLUSTER_0000 - SLOT_0000
token state: uninitialized
Slot 1 (0x1): ...
3001@127.0.0.1 - CLUSTER_0000 - SLOT_0001
token state: uninitialized
...
Slot 9 (0x9): ...
3001@127.0.0.1 - CLUSTER_0000 - SLOT_0009
token state: uninitialized
Ten slots (0–9) are shown, all uninitialized — this is correct before
token initialization. If the command hangs or returns a connection error,
confirm CS_PKCS11_R3_CFG is exported and the simulator is running
(ss -tlnp | grep 3001).
--moduleflag is required for everypkcs11-toolcall in this guide, and must be an absolute path (~is not expanded). Set a variable to avoid repetition:export PKCS11_MODULE=$HOME/utimaco/libcs_pkcs11_R3.so # then: pkcs11-tool --module "$PKCS11_MODULE" ...The commands below use
$HOMEinline for copy-paste clarity.
PKCS#11 tokens ship uninitialized. Initialization sets the token label, a Security Officer (SO) PIN, and a normal User PIN. The User PIN is what Besu reads at runtime.
Choose PINs now. Generate both SO and User PINs with a password manager and save them before proceeding. The SO PIN is needed only for administrative operations. The User PIN is the runtime credential Besu reads from a file.
Unlike SoftHSM2 or Luna, Utimaco's C_InitToken requires a device
administrator to authenticate before the call is permitted. The standard
pkcs11-tool --init-token therefore fails with CKR_USER_NOT_LOGGED_IN.
Use Utimaco's own p11tool2 instead — it accepts a Login= parameter
that passes the admin key alongside the InitToken= call.
Use
$HOMEinside parameter values. The shell expands~when it appears at the start of a command argument (~/utimaco/p11tool2) but not inside a parameter value string (Login=ADMIN,~/...). Use$HOMEor an absolute path for any path embedded in a parameter value.
~/utimaco/p11tool2 \
Login=ADMIN,$HOME/utimaco/key/ADMIN_SIM.key \
Label="besu-validators" \
InitToken=askLogin=ADMIN,<keyfile> authenticates as the device ADMIN user via
RSA signature using the simulator's pre-loaded admin key. InitToken=
then calls C_InitToken with the given SO PIN and sets the token label.
Using ask prompts for the PIN without it appearing in the shell or
command history, which is strongly recommended.
Alphanumeric characters and - are accepted. On success the command
returns to the shell after the ClusterSync log lines with no error
message — the absence of an error is the success indicator:
Enter SO PIN:
Repeat SO PIN:
... | I: [ClusterSync] State changing command started ...
... | I: [ClusterSync] The following devices are in sync:
... | I: Device 3001@127.0.0.1
... | I: [ClusterSync] The following devices are out of sync:
After InitToken, the SO_0000 user is created with an I[1]
(initial credentials) flag set. Utimaco requires the SO to change this
initial PIN before it can perform any privileged operation such as
creating the normal user. Attempting InitPIN without doing this first
results in CKR_PIN_TOO_WEAK.
Change the SO PIN via csadm:
~/utimaco/csadm dev=3001@127.0.0.1 \
"LogonPass=SO_0000,<so-pin-from-inittoken>" \
"ChangeUser=SO_0000,<new-so-pin>"Verify I[1] has cleared to I[0] before continuing:
~/utimaco/csadm dev=3001@127.0.0.1 \
LogonSign=ADMIN,$HOME/utimaco/key/ADMIN_SIM.key \
ListUserSO_0000 should now show I[0]. This is analogous to Luna's
role changepw -name co step.
Log in as SO with the new SO PIN and initialize the User PIN:
~/utimaco/p11tool2 Slot=0 LoginSO=ask InitPIN=<initial-user-pin>LoginSO=ask— prompts for the new SO PIN set in §4.2InitPIN=<initial-user-pin>— supply the initial User PIN as a plain value rather thanask;csadmhas no hidden-entry equivalent and requires this PIN directly in the next command, so specifying it here avoids having to retype an invisible value. Minimum length is 8 characters (enforced by the token, confirmed bypin min/max: 8/255in §4.5)
This creates USR_0000 with an I[1] (initial credentials) flag — the
same mandatory-change requirement as the SO.
~/utimaco/csadm dev=3001@127.0.0.1 \
"LogonPass=USR_0000,<initial-user-pin>" \
"ChangeUser=USR_0000,<new-user-pin>"Verify both SO and user show I[0]:
~/utimaco/csadm dev=3001@127.0.0.1 \
LogonSign=ADMIN,$HOME/utimaco/key/ADMIN_SIM.key \
ListUserExpected output:
ADMIN 22000000 RSA sign Z[0]I[0]
SO_0000 00000200 HMAC passwd Z[0]I[0]A[CXI_GROUP=SLOT_0000]L[besu-validators]
USR_0000 00000002 HMAC passwd I[0]A[CXI_GROUP=SLOT_0000]
The new User PIN (set in
ChangeUser) is the runtime credential. This is what Besu reads frompkcs11-password.txt— not the initial PIN set in §4.3.
p11tool2 ListSlots only shows slot IDs. Use pkcs11-tool here for
the richer token detail:
pkcs11-tool --module $HOME/utimaco/libcs_pkcs11_R3.so --list-slotsSlot 0 should now show a fully initialized token (abbreviated — logging lines omitted):
Slot 0 (0x0): 3001@127.0.0.1 - CLUSTER_0000 - SLOT_0000
token label : besu-validators
token manufacturer : Utimaco IS GmbH
token model : CryptoServer
token flags : login required, rng, token initialized, PIN initialized, other flags=0x260
hardware version : 5.2
firmware version : 6.5
serial num : SI003001_0000
pin min/max : 8/255
token initialized and PIN initialized confirm SO and User are both
set up. Slots 1–9 remain uninitialized — that is expected.
Before generating keys, confirm the simulator supports the mechanisms Besu requires. Capture the User PIN in a shell variable so it stays out of shell history:
read -s -p "User PIN: " UTIMACO_PIN; echoList EC-related mechanisms:
pkcs11-tool --module $HOME/utimaco/libcs_pkcs11_R3.so \
--token-label "besu-validators" \
--login --pin "$UTIMACO_PIN" \
--list-mechanisms | grep -Ei 'ecdsa|ec.*key.*pair|ecdh1-derive'All three mechanisms required by the plugin are present and hardware-accelerated:
ECDSA, keySize={112,571}, hw, sign, verify, EC F_P, EC F_2M, EC parameters, EC OID, EC uncompressed, EC compressed
ECDSA-KEY-PAIR-GEN, keySize={112,521}, hw, generate_key_pair, EC F_P, EC F_2M, EC parameters, EC OID, EC uncompressed, EC compressed
ECDH1-DERIVE, keySize={112,571}, hw, derive
The key size range {112,571} covers secp256k1 (256-bit). EC OID
support means curves are specified by OID — secp256k1 uses OID
1.3.132.0.10, which falls within the supported range.
Five secp256k1 keypairs, one per validator. Each is generated on
the simulator — the private key never exists in software.
for i in 1 2 3 4 5; do
pkcs11-tool --module $HOME/utimaco/libcs_pkcs11_R3.so \
--token-label "besu-validators" \
--login --pin "$UTIMACO_PIN" \
--keypairgen --key-type EC:secp256k1 \
--label "testValidator${i}" --id "0${i}" \
--usage-sign --usage-derive \
--sensitive --private
doneFlag rationale:
--key-type EC:secp256k1— Ethereum's curve (OID1.3.132.0.10)--label/--id—CKA_LABELandCKA_IDon both keys; the plugin looks the private key up by label--usage-sign→CKA_SIGN=trueon the private key (required for ECDSA signing) andCKA_VERIFY=trueon the public key--usage-derive→CKA_DERIVE=true(required for ECDH peer handshakes)--sensitive→CKA_SENSITIVE=true(key value cannot be read in plaintext viaC_GetAttributeValue)--private→CKA_PRIVATE=true(login required to access)
Verify all 10 objects (5 private + 5 public) are in place:
pkcs11-tool --module $HOME/utimaco/libcs_pkcs11_R3.so \
--token-label "besu-validators" \
--login --pin "$UTIMACO_PIN" \
--list-objects 2>/dev/null \
| grep -E '^(Private Key|Public Key| label:| ID:)'Expected: 5 Private Key Object and 5 Public Key Object blocks, each
with matching label: testValidatorN and ID: 0N. Objects may appear
out of order — that is normal:
Private Key Object; EC
label: testValidator1
ID: 01
Public Key Object; EC EC_POINT 256 bits
label: testValidator1
ID: 01
...
QBFT's genesis configuration needs the validators' raw 64-byte
uncompressed public keys (X ‖ Y, no 0x04 prefix). Read them from
the public-key objects and strip the DER OCTET STRING wrapper that
pkcs11-tool prints:
mkdir -p ~/utimaco-validators
cd ~/utimaco-validators
pkcs11-tool --module $HOME/utimaco/libcs_pkcs11_R3.so \
--token-label "besu-validators" \
--login --pin "$UTIMACO_PIN" \
--list-objects --type pubkey 2>/dev/null \
| awk '
/^[[:space:]]*EC_POINT:/ { ec=$2 }
/^[[:space:]]*label:[[:space:]]*testValidator[0-9]+/ { keys[$2]=ec }
END { for (i=1; i<=5; i++) print keys["testValidator" i] }
' \
| sed 's/^044104/0x/' \
| jq -R . | jq -s . > public_keys.json
cat public_keys.jsonYou should see five 0x-prefixed 64-byte hex strings, in validator
order.
If the
sedproduces no output or wrong output: The DER prefix on the EC_POINT attribute may differ from the expected044104. Runpkcs11-tool ... --list-objects --type pubkeyand examine the rawEC_POINT:hex for the leading bytes. The uncompressed-point marker04and the 64-byte length40(hex) must appear after the OCTET STRING DER envelope. Adjust thesedexpression to strip those leading bytes, and record the prefix in Compatibility Findings.
BESU_VERSION="26.6.1"
HSM_PLUGIN_VERSION="26.6.1"
WORK_DIR=$(pwd) # ~/utimaco-validators
curl -L -O https://github.com/besu-eth/besu/releases/download/${BESU_VERSION}/besu-${BESU_VERSION}.tar.gz
curl -L -O https://github.com/besu-eth/besu-hsm-plugin/releases/download/${HSM_PLUGIN_VERSION}/besu-hsm-plugin-${HSM_PLUGIN_VERSION}.zipCreate per-validator directories and unpack into each:
for i in 1 2 3 4 5; do
mkdir -p validator${i}/plugins validator${i}/data validator${i}/config
tar xzf besu-${BESU_VERSION}.tar.gz --strip-components=1 -C validator${i}
unzip -o -j besu-hsm-plugin-${HSM_PLUGIN_VERSION}.zip -d validator${i}/plugins
doneWrite the User PIN to a file each Besu process can read:
echo -n "$UTIMACO_PIN" > pkcs11-password.txt
chmod 600 pkcs11-password.txtIf code changes are needed: Build a custom plugin distribution locally (
./gradlew clean build -x test; output atbuild/distributions/besu-hsm-plugin-<version>.zip), upload it withscp, and use the uploaded zip in place of thecurldownload.
The plugin reads a minimal config file that tells it which library to
load and which slot to use. This is separate from
cs_pkcs11_R3.cfg — the library reads that file independently via
CS_PKCS11_R3_CFG.
cat > pkcs11-utimaco.cfg <<EOF
library = ${HOME}/utimaco/libcs_pkcs11_R3.so
slotListIndex = 0
EOFslotListIndex = 0 selects the first token-present slot returned by
C_GetSlotList. If you initialised more than one token on the simulator,
use the exact slot number from pkcs11-tool --list-slots instead:
slot = <N>.
Prerequisite:
CS_PKCS11_R3_CFGmust be set in any shell that starts Besu, otherwise the library cannot locate the simulator or HSM. Verify before continuing:echo "$CS_PKCS11_R3_CFG" # should print the path to cs_pkcs11_R3.cfg
VALIDATOR_KEYS=$(cat public_keys.json)
cat > qbftConfigFile.json <<EOF
{
"genesis": {
"config": {
"ecCurve": "secp256k1",
"chainId": 1337,
"berlinBlock": 0,
"londonBlock": 0,
"shanghaiTime": 0,
"qbft": {
"blockperiodseconds": 2,
"epochlength": 30000,
"requesttimeoutseconds": 4
}
},
"nonce": "0x0",
"timestamp": "0x0",
"gasLimit": "0x1fffffffffffff",
"difficulty": "0x1",
"mixHash": "0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365",
"coinbase": "0x0000000000000000000000000000000000000000",
"alloc": {
"fe3b557e8fb62b89f4916b721be55ceb828dbd73": {
"balance": "0xad78ebc5ac6200000"
}
}
},
"blockchain": {
"nodes": {
"generate": false,
"keys": ${VALIDATOR_KEYS}
}
}
}
EOF
./validator1/bin/besu operator generate-blockchain-config \
--config-file=qbftConfigFile.json \
--to=output
for i in 1 2 3 4 5; do
cp ./output/genesis.json ./validator${i}/data/
doneTesting secp256r1? Change
"ecCurve": "secp256k1"to"secp256r1"here, and match it inplugin-hsm-ec-curvein §§11–12.
Validator 1 is started first so its discovery URI can be captured for
the other validators. ADMIN is included in rpc-http-api so
admin_nodeInfo is available.
cat > validator1/config/config.toml <<EOF
genesis-file="${WORK_DIR}/validator1/data/genesis.json"
data-path="${WORK_DIR}/validator1/data"
security-module="hsm"
plugin-hsm-config-path="${WORK_DIR}/pkcs11-utimaco.cfg"
plugin-hsm-password-path="${WORK_DIR}/pkcs11-password.txt"
plugin-hsm-key-alias="testValidator1"
plugin-hsm-ec-curve="secp256k1"
p2p-port=30301
min-gas-price=0
rpc-http-enabled=true
rpc-http-port=8541
rpc-http-api=["ETH","NET","QBFT","ADMIN"]
rpc-http-host="0.0.0.0"
host-allowlist=["*"]
profile="ENTERPRISE"
EOFNotes on the HSM-plugin TOML fields:
- No
plugin-hsm-provider-type—native-pkcs11is the default. plugin-hsm-config-path— the minimal plugin config written in §9 (library+slotListIndex).plugin-hsm-password-path— file containing the User PIN (CKU_USERcredential in PKCS#11 terms).plugin-hsm-key-alias— matchesCKA_LABELof the keypair on the token. No certificate is required.- No
plugin-hsm-public-key-alias— that field is CloudHSM-JCE only.
Start validator 1 and wait for its RPC endpoint:
tmux new -d -s validator1 ./validator1/bin/besu --config-file=./validator1/config/config.toml
for _ in {1..60}; do
curl -s -o /dev/null -X POST http://localhost:8541 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"net_version","params":[],"id":1}' && break
sleep 1
doneCapture the bootnode URI:
VALIDATOR1_BOOTNODE=$(curl -s -X POST http://localhost:8541 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"admin_nodeInfo","params":[],"id":1}' \
| jq -r '.result.enode')
echo "$VALIDATOR1_BOOTNODE"Tip: attach to the tmux session with
tmux attach -t validator1(Ctrl+b, d to detach). Watch for "Ethereum main loop is up" before moving on.
for i in 2 3 4 5; do
P2P_PORT=$((30300 + i))
RPC_PORT=$((8540 + i))
cat > "validator${i}/config/config.toml" <<EOF
genesis-file="${WORK_DIR}/validator${i}/data/genesis.json"
data-path="${WORK_DIR}/validator${i}/data"
security-module="hsm"
plugin-hsm-config-path="${WORK_DIR}/pkcs11-utimaco.cfg"
plugin-hsm-password-path="${WORK_DIR}/pkcs11-password.txt"
plugin-hsm-key-alias="testValidator${i}"
plugin-hsm-ec-curve="secp256k1"
p2p-port=${P2P_PORT}
min-gas-price=0
bootnodes=["${VALIDATOR1_BOOTNODE}"]
rpc-http-enabled=true
rpc-http-port=${RPC_PORT}
rpc-http-api=["ETH","NET","QBFT","ADMIN"]
rpc-http-host="0.0.0.0"
host-allowlist=["*"]
profile="ENTERPRISE"
EOF
done
for i in 2 3 4 5; do
tmux new -d -s validator${i} ./validator${i}/bin/besu --config-file=./validator${i}/config/config.toml
donefor i in 1 2 3 4 5; do
PORT=$((8540 + i))
echo "=== validator${i} (port ${PORT}) ==="
curl -s -X POST http://localhost:${PORT} \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}'
echo
curl -s -X POST http://localhost:${PORT} \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"qbft_getValidatorsByBlockNumber","params":["latest"],"id":1}'
echo
doneExpected output — all five validators at the same (advancing) block number, validator list identical across all nodes:
=== validator1 (port 8541) ===
{"jsonrpc":"2.0","id":1,"result":"0x5"}
{"jsonrpc":"2.0","id":1,"result":["0x1e9eb3...","0x6aebb8...","0xba76c7...","0xceff2a...","0xf06372..."]}
=== validator2 (port 8542) ===
{"jsonrpc":"2.0","id":1,"result":"0x5"}
...
If eth_blockNumber advances and the validator list includes all five
addresses, the HSM-backed signing path is live and ECDH-backed peering
is working.
The provider opens one PKCS#11 session at validator startup and reuses
it for the lifetime of the process. On session-invalidating events
(simulator restart, token re-initialisation, etc.) the next sign or
ECDH call throws SecurityModuleException with the underlying PKCS#11
return code in the message. Restart the validator to recover.
The simulator persists its token state in the devices/ directory
inside sim5_linux. Restarting bl_sim5 after a crash or host reboot
restores the token and all keys automatically:
tmux kill-session -t simulator 2>/dev/null
tmux new -d -s simulator $HOME/utimaco/sim5_linux/bin/bl_sim5 -h -o
sleep 2
ss -tlnp | grep 3001 # confirm it is listening againEnsure CS_PKCS11_R3_CFG is set in any new shell before restarting
validators.
If you want to remove individual keys while keeping the token intact, do this before stopping the simulator:
for i in 1 2 3 4 5; do
pkcs11-tool --module $HOME/utimaco/libcs_pkcs11_R3.so \
--token-label "besu-validators" --login --pin "$UTIMACO_PIN" \
--delete-object --type privkey --label "testValidator${i}"
pkcs11-tool --module $HOME/utimaco/libcs_pkcs11_R3.so \
--token-label "besu-validators" --login --pin "$UTIMACO_PIN" \
--delete-object --type pubkey --label "testValidator${i}"
doneFor a full teardown, stop all processes first, then remove directories.
The simulator's token state (keys, users, token label) lives entirely in
sim5_linux/devices/ — deleting that directory is equivalent to wiping
the HSM, so there is no need to delete keys individually beforehand.
Stop all validators and the simulator:
for i in 1 2 3 4 5; do tmux kill-session -t "validator${i}" 2>/dev/null; done
tmux kill-session -t simulator 2>/dev/nullRemove working directories and the SDK (re-extractable from the zip):
rm -rf ~/utimaco-validators ~/utimaco ~/utimaco-sdk
pkcs11-password.txt(inside~/utimaco-validators) contains the User PIN. Confirm it is not copied elsewhere (shell history, scrollback, tmux scrollback) before treating cleanup as complete.
- DiscV5 is only supported on the
secp256k1curve. EIP-778 mandates secp256k1 for ENR identity. DiscV4 and devp2p handshakes work on both curves. - Per-JVM serialisation of HSM operations. Sign and ECDH calls are serialised at the Java level (one PKCS#11 session per process). For a QBFT validator this is fine.
pkcs11-toolcannot generateCKA_EXTRACTABLE=falsekeys. Keys are protected byCKA_SENSITIVE=truewhich blocks plaintext readback. For maximum security in production, regenerate keys with a tool that permits full attribute control.
When adapting this guide for a real Utimaco GP HSM deployment:
- Point
Devicesincs_pkcs11_R3.cfgat the physical HSM's IP address instead of3001@127.0.0.1. - Regenerate keys with
CKA_EXTRACTABLE=false. - Move
pkcs11-password.txtto a tightly-scoped location with mode0400, readable only by the Besu service user. - Enable TLS for the Utimaco client-to-HSM channel (refer to the Utimaco administration guide for mutual-TLS configuration).
- For HA deployments, configure multiple devices in
cs_pkcs11_R3.cfg:[HSMCluster] Devices = { 192.168.0.1 192.168.0.2 } FallbackInterval = 30
Validated on Utimaco u.trust GP HSM Simulator v6.5.0.0, Ubuntu 24.04 LTS (x86-64), Besu 26.6.1, plugin 26.6.1.
| Check | Result | Notes |
|---|---|---|
ECDSA-KEY-PAIR-GEN mechanism present |
✅ | hw, keySize={112,521} |
ECDSA mechanism present |
✅ | hw, sign/verify, keySize={112,571} |
ECDH1-DERIVE mechanism present |
✅ | hw, derive, keySize={112,571} |
secp256k1 key generation |
✅ | 256-bit EC keypairs generated successfully |
| EC_POINT DER prefix | ✅ 044104 |
Standard format; same as Luna; compatible with NativePkcs11Provider |
ECDSA signing via native-pkcs11 |
✅ | QBFT blocks produced; no errors |
CKM_ECDH1_DERIVE ECDH via native-pkcs11 |
✅ | All 5 validators peered via DiscV4; devp2p handshakes completed |
| Blocks produced (all 5 validators) | ✅ | All 5 validators at same block height; full validator set reported |
| DiscV4 peer discovery | ✅ | Validators discovered each other; consistent validator set across nodes |
sunpkcs11-jce fallback (if needed) |
N/A | native-pkcs11 works without fallback |
| Code changes required | No | Plugin works as-is with the Utimaco PKCS#11 R3 library |
- Plugin overview:
../../README.md - Architecture diagram:
../besu-hsm-plugin-architecture.png - Thales Luna guide for comparison:
../thales-luna/ - AWS CloudHSM guide:
../aws-CloudHSM/