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 RawKeyMaterial holding mlocked, zeroise-on-drop private key bytes,
  • lifecycle state (PreActivation, Active, Deactivated, Compromised, Destroyed) and CKA_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:

  • mlock on Unix (libc::mlock / munlock) and VirtualLock on Windows prevent paging to swap.
  • zeroize::Zeroize is called on drop, followed by munlock.
  • The Debug impl prints [REDACTED] for the key bytes so that a stray format!("{:?}", key) does not leak material into logs.

For operational procedures covering rotation, export, and disaster recovery, see Backup and Recovery.