Craton HSM
Storage
Storage
Craton HSM separates object storage from object persistence. The authoritative object map is always in memory; persistence is an optional overlay that writes encrypted copies of token objects to disk. This page describes the on-disk layout, the key-derivation scheme, and the durability properties relevant to operators.
In-Memory Object Store
The primary store is an ObjectStore holding
DashMap<CK_OBJECT_HANDLE, Arc<RwLock<StoredObject>>>
This structure is authoritative: every read, attribute update, or
handle resolution goes through it. An upper bound of 10,000 objects is
enforced to prevent resource exhaustion, and CKA_VALUE bodies are
capped at 8 KiB to cover the largest supported key types (4096-bit RSA)
with headroom.
Session objects (CKA_TOKEN = false) live only in this map and are
discarded when their owning session closes. Token objects
(CKA_TOKEN = true) are additionally written through to the encrypted
persistent store when it is configured.
Encrypted Persistent Store
Source: src/store/encrypted_store.rs. Enabled by setting a
persist.path value in craton_hsm.toml.
On-Disk Layout
<persist.path> redb database file
<persist.path>.lock zero-byte exclusive lock file (fs2)
The database uses a single redb table named objects:
- Key: UTF-8 string object identifier.
- Value:
nonce (12 bytes) || ciphertext-- AES-256-GCM output concatenated after its 12-byte nonce.
The on-disk file has restrictive permissions applied immediately after
creation (0600 on Unix; owner-only DACL on Windows). Craton treats
failure to tighten permissions as fatal and refuses to open a
world-readable database.
Record Encryption
Every object record is independently encrypted with AES-256-GCM. The
nonce is drawn from the same HMAC_DRBG that the rest of the HSM uses
(see Overview), not directly from
OsRng, so nonce generation participates in the DRBG's continuous
health tests and prediction-resistance guarantees.
AES-GCM nonce reuse is tracked per key: the core maintains per-key
counters in a LazyLock<DashMap<[u8; 32], AtomicU64>> keyed by the
SHA-256 hash of the key material, and rejects operations that would
exceed 2^31 invocations under the same key.
Key Derivation
The AES-256-GCM encryption key is derived from the user PIN with PBKDF2-HMAC-SHA256 at 1,000,000 iterations (OWASP 2024 guidance), using a 32-byte salt that is persisted alongside the database. There is no separate master KEK: the PIN-derived key is the only encryption key.
Consequences:
- A correct user PIN is required to open the persistent store.
- Changing the user PIN requires re-encrypting every record.
- Token re-initialisation (
C_InitToken) destroys all persisted objects -- there is no recovery path.
Process Locking
Before opening the redb file, EncryptedStore acquires an exclusive
fs2 lock on the sibling .lock file. A second process attempting to
open the same database receives CKR_GENERAL_ERROR. The lock file is
not deleted on shutdown: removing it would create a TOCTOU window where
another process could acquire a lock that is then deleted out from under
it. An empty .lock file is harmless.
For multi-process access to the same token, use the
gRPC daemon, which serialises all operations against a
single in-process HsmCore.
Write Durability
Writes to the redb database are committed inside an explicit
begin_write / commit transaction. redb's commit path issues an
fsync before returning, so a successful PKCS#11 object-creation call
implies the record is durable on disk. The audit log uses its own
std::sync::Mutex-serialised append-only file and is fsynced before
the originating PKCS#11 function returns.
Craton does not implement write batching for object creation: each
C_CreateObject or C_GenerateKey* that produces a token object
results in one transactional redb commit.
Attribute Encoding
Persisted records contain the full StoredObject, serialised through
serde. Each object carries:
- its PKCS#11 object class (
CKA_CLASS), - a
HashMap<CK_ATTRIBUTE_TYPE, AttributeValue>covering all set attributes, - optional
RawKeyMaterialholding mlocked, zeroise-on-drop private key bytes, - lifecycle state (
PreActivation,Active,Deactivated,Compromised,Destroyed) andCKA_START_DATE/CKA_END_DATE.
Public attributes (label, class, key type) and private attributes (key material, sensitive values) are stored in the same record. The whole record is opaque on disk -- there is no distinction between public and private attributes at rest.
Backup and Restore
Source: src/store/backup.rs. Exposed through
craton-hsm-admin backup --output ... and
craton-hsm-admin restore --input ....
Backup File Format
magic "RHBK" 4 bytes
version 1 4 bytes (u32 LE)
pbkdf2_salt 32 bytes
aes_gcm_nonce 12 bytes
ciphertext AES-256-GCM of JSON payload (remaining bytes)
The JSON payload carries a BackupPayload:
version u32
created RFC 3339 timestamp
created_epoch u64 seconds since UNIX epoch
backup_id random UUID
token_serial 16-char token serial
object_count usize
objects [StoredObject, ...]
token_serial binds a backup to its source token and is checked on
restore to prevent cross-token reimport. created_epoch is checked
against a 30-day staleness window by default.
Passphrase
The backup passphrase is independent of the user PIN. It must be at least 16 characters. The AES-256-GCM key is derived with the same PBKDF2-HMAC-SHA256 KDF (1,000,000 iterations) as the persistent store, but using the per-backup salt written into the file header.
Operational Considerations
- The unencrypted payload exists only in memory during backup creation and restore; buffers are zeroised on drop.
- A backup captures only token objects. Session objects are by definition transient and are not backed up.
- Restoring into a token that already contains objects merges by
object-class and label collision rules defined in
backup.rs; see Backup and Recovery for operational procedure.
Fork Safety
Craton records the process PID during C_Initialize. If a child
process inherits the library state across fork(2) and calls any
PKCS#11 function, the call returns CKR_CRYPTOKI_NOT_INITIALIZED and
the child must re-initialise. Persistent storage survives the parent
process, but the child cannot reuse the parent's in-memory state or
file locks.
Memory Hardening
Secret material held in memory is protected by RawKeyMaterial:
mlockon Unix (libc::mlock/munlock) andVirtualLockon Windows prevent paging to swap.zeroize::Zeroizeis called on drop, followed bymunlock.- The
Debugimpl prints[REDACTED]for the key bytes so that a strayformat!("{:?}", key)does not leak material into logs.
For operational procedures covering rotation, export, and disaster recovery, see Backup and Recovery.