Craton HSM

Your First Token

Your First Token

The Quickstart walks through the happy path in five minutes. This page goes one layer deeper: what a token actually is in Craton HSM, how the two PIN roles are used, where the data lives on disk, and the process-model pitfalls that surprise first-time PKCS#11 integrators.

The token and slot model

PKCS#11 models the world as a set of slots, each of which may contain a token. A token holds keys, certificates, and data objects and enforces authentication.

Craton HSM exposes a configurable number of slots (default: 1) through the slot_count field in craton_hsm.toml. Each slot:

  • Has a stable slot ID (0, 1, …) returned by C_GetSlotList.
  • Reports a label, model, manufacturer, and serial via C_GetTokenInfo.
  • Is either uninitialised (present but empty) or initialised (holds a token with PIN state and objects).
  • Stores its state under a subdirectory of token.storage_path so that slots do not share keys.

An uninitialised slot reports the flag CKF_TOKEN_INITIALIZED = false and will reject any session that tries to log in. Call C_InitToken (or craton-admin token init) to transition it into the initialised state.

SO PIN and user PIN

PKCS#11 defines two authenticated roles:

RolePKCS#11 constantWho is itWhat can it do
Security OfficerCKU_SOToken administratorInitialise the token, set and reset the user PIN, re-initialise the token (which wipes all objects).
UserCKU_USERApplication / key ownerOpen R/W sessions, generate and use keys, change the user PIN. Cannot reset a locked user PIN — only the SO can.

Both PINs are:

  • Stored as PBKDF2-HMAC-SHA256 hashes with 600 000 iterations (configurable down to a 100 000 floor).
  • Compared in constant time using subtle::ConstantTimeEq.
  • Subject to the same length policy (pin_min_length / pin_max_length in [security]).
  • Locked out after max_failed_logins consecutive wrong attempts (default: 10).

A locked user PIN is recovered by the SO with C_SetPIN while logged in as CKU_SO, or via craton-admin pin reset. A locked SO PIN cannot be recovered; the only remedy is to re-initialise the token, which destroys all token objects.

Operational rule of thumb: the SO PIN should live in an out-of-band escrow (password manager, KMS, paper in a safe). The user PIN is what day-to-day applications present.

Storage paths

Everything persistent is rooted in the directory named by token.storage_path (default: craton_hsm_store/, relative to the process working directory — absolute paths, UNC paths, and .. are rejected at load time).

Inside that directory Craton HSM manages:

  • A redb database file holding encrypted object records.
  • An advisory file lock that blocks a second Craton HSM process from opening the same store.
  • One subdirectory per slot when slot_count > 1.

The audit log is separate. By default it is written to audit.log_path = craton_hsm_audit.jsonl — one JSON object per line, each linked to the previous entry by a SHA-256 hash. Point this at a location that your log-shipping agent already tails; the format is compatible with any JSON-lines consumer.

Object records are encrypted with AES-256-GCM using a key derived from the user PIN via PBKDF2. Losing the user PIN therefore means losing access to the key material; this is intentional — the software HSM's at-rest protection is cryptographic, not just filesystem permissions.

Initialising the token

The quickstart showed the CLI form. Below is the underlying sequence in PKCS#11 vocabulary, which is useful when you integrate from C, Rust, Python, or Java.

  1. C_Initialize(NULL) — loads the module, runs the 17 Power-On Self-Tests, records the current PID.
  2. C_GetSlotList(tokenPresent=false, …) — returns the slots from config.
  3. C_InitToken(slotID, soPin, soPinLen, label) — creates an initialised token under the SO PIN. The label is padded/truncated to exactly 32 bytes per the spec; most tools handle the padding for you.
  4. Open a R/W SO session: C_OpenSession(slotID, CKF_RW_SESSION | CKF_SERIAL_SESSION, …) then C_Login(CKU_SO, soPin, …).
  5. C_InitPIN(session, userPin, userPinLen) — sets the initial user PIN.
  6. C_Logout(session) / C_CloseSession(session).

After step 5 the token is ready for applications to log in as CKU_USER.

Changing PINs

C_SetPIN changes the PIN of the currently logged-in role. There are two cases:

# User changes their own PIN (session must be logged in as CKU_USER).
pkcs11-tool --module $MODULE --login --pin 87654321 \
    --change-pin --new-pin 24681357
# SO resets a locked user PIN (session must be logged in as CKU_SO).
pkcs11-tool --module $MODULE --login --login-type so \
    --so-pin 12345678 --init-pin --pin NEW_USER_PIN

Both operations are synchronous and log a single audit record.

craton-admin pin change --user-type USER wraps the first flow and prompts for the old and new PIN interactively. craton-admin pin reset wraps the second.

Re-initialising

C_InitToken on an already-initialised slot wipes every object on that slot and sets a new SO PIN. Only the SO can perform it, and the call fails if any session is currently open on the slot.

craton-admin token init --label "Quickstart HSM"
# Prompts: "Slot 0 is already initialised. All objects will be destroyed. Continue? [y/N]"

There is no partial reset. If you need to rotate only a key, use C_DestroyObject (or craton-admin key delete) and generate a replacement.

Common pitfalls

Global PKCS#11 state

C_Initialize and C_Finalize are module-global. Loading libcraton_hsm.so twice in the same process — for example by two independent libraries both depending on a PKCS#11 consumer — ends up sharing a single initialised module. Consequences:

  • The second C_Initialize returns CKR_CRYPTOKI_ALREADY_INITIALIZED. This is not an error if both callers cooperate; PKCS#11 expects the application to reference-count initialisation.
  • C_Finalize tears down the module for everybody. Libraries should not call C_Finalize unless they own the lifecycle.
  • In test suites the global state is why cargo test must run with --test-threads=1.

Fork safety

Craton HSM records the PID at C_Initialize and compares it on every subsequent call. If the PID differs (the process has forked), the call returns CKR_CRYPTOKI_NOT_INITIALIZED and the child must re-initialise. This matches the guidance in the PKCS#11 specification and the behaviour of OpenSC's module.

Practical rules:

  • If you fork after opening sessions, the child must call C_Initialize again before doing anything else. Any session handles it inherited are invalid.
  • A common footgun is an application server that initialises PKCS#11 in the master and then forks workers. Move C_Initialize into the post-fork worker entry point.
  • On Windows there is no fork(2); the PID check provides defence-in-depth against process recycling in unusual hosts.

See Fork safety for the detailed model.

Multi-process access to the same store

Two Craton HSM library loads pointing at the same storage_path would race on the redb files. The module takes an exclusive advisory file lock at load time and the second loader fails. If you need concurrent access from multiple processes, use the gRPC daemon — it serialises requests behind a single module instance — or give each process its own store path.

PIN lockout during development

The default max_failed_logins = 10 is a production value. If you are scripting the CLI and mistype the PIN a few times, you can lock yourself out quickly. For development, lower pbkdf2_iterations to speed up login, but keep max_failed_logins generous. Never ship either relaxation to production.

Absolute paths in config

storage_path and log_path must be relative and must not contain .. or UNC segments. This is a defence-in-depth check: an absolute path would let a misconfigured daemon escape its data directory. Run the module from the directory that should contain its state, or set CRATON_HSM_CONFIG to a config file whose relative paths resolve correctly.

Next steps

  • Examples — concrete recipes for RSA, ECDSA, AES-GCM, and post-quantum ML-DSA-65.
  • Operator runbook — backup, restore, PIN rotation, audit export.
  • Security model — threat model, PIN protection, key lifecycle.