Skip to content
BitExactdocs
Documentation
On this page

Recorder-key custody: file, HSM, or KMS#

The recorder key signs your runs' checkpoints — the tamper-evident head attestations a relying party pins with --expect-recorder-key. You choose where that key lives, at three levels of assurance. Whichever you pick, the signature is the same the open verifier checks, so the bundle format and verification never change.

A recorder key is hybrid: an Ed25519 key and its post-quantum half, an ML-DSA-65 key (ML-DSA-87 with --pq-alg ML-DSA-87, for CNSA 2.0 buyers). Every checkpoint, anchor and record carries both signatures. Ed25519 alone falls to a quantum computer, and every checkpoint carries the public key, so anyone holding a bundle and such a computer could recover the key and re-sign a doctored history; the ML-DSA half is what they cannot make. bitexact keygen writes both halves (<name>.key and <name>.pq.key, which holds the ML-DSA seed as 64 hex characters) and prints the trust-file entry that pairs them. That entry, and the pin that comes from it, is what binds the halves: a run signed with a PQ half the pin does not name fails. The parameter set is the deployment's BITEXACT_PQ_ALG (ML-DSA-65 unless set), which must be the one the key was made with. A key that signs Ed25519 alone (keygen --suite ed25519, or a backend configured without its half) works, and bitexact health and the inspector's Settings say signing_classical_only for as long as it is in use.

The three backends#

BackendAssuranceConfigure withIts post-quantum halfInstall
File (default)key in a file on the hostBITEXACT_SIGNING_KEY=/path/to/key<name>.pq.key beside it, or BITEXACT_SIGNING_PQ_KEYbase wheel
PKCS#11 HSMkey never leaves hardwareBITEXACT_SIGNING_PKCS11=1 + BITEXACT_HSM_*BITEXACT_HSM_PQ_LABEL: an ML-DSA key in the same token, signed with CKM_ML_DSA (PKCS#11 3.2)the [hsm] extra of the delivered wheel
AWS KMS envelopekey encrypted at rest by KMS, decrypted once in memoryBITEXACT_SIGNING_KMS_CIPHERTEXT=/path/to/blobBITEXACT_SIGNING_KMS_PQ_CIPHERTEXT: the .pq.key file, KMS-encrypted the same waythe [aws] extra of the delivered wheel

A misconfigured half - a file that is missing or malformed, a library without ML-DSA (the product requires cryptography 50.0.2 or later, built with it), a half that names another key - stops the recorder at startup, as a misconfigured key does.

Both HSM and KMS clients are lazily imported, so the base install stays dependency-free and air-gap-friendly — you opt in only for the backend you use.

Each checkpoint signature checks against the public key stored beside it, so a signature alone proves the chain is intact, not which recorder made it: anyone who can write to the store could rebuild a run and sign it with a key of their own. bitexact verify and the viewer therefore pin the recorder keys your license names in recorder_keys (or the key given to bitexact verify --expect-recorder-key): a run signed by any other key, or a sealed run with no signature by a pinned key, fails verification. With nothing pinned, bitexact verify says so in its verdict rather than calling the run recorder-signed.

PKCS#11 HSM — key never leaves hardware#

The strongest option, and it works with every enterprise HSM: AWS CloudHSM, Azure Managed HSM, Thales Luna, Entrust nShield, YubiHSM 2 (and SoftHSM2 for a dry-run). Generate an Ed25519 key in the HSM, then:

export BITEXACT_SIGNING_PKCS11=1
export BITEXACT_HSM_PKCS11_LIB=/path/to/pkcs11.so
export BITEXACT_HSM_SLOT=0
export BITEXACT_HSM_PIN=…            # from your secrets manager
export BITEXACT_HSM_KEY_LABEL=bitexact-recorder
pip install --no-index --find-links wheels --require-hashes -r requirements-hsm.lock   # in your platform bundle (installing.md)
pip install --no-index --no-deps bitexact_sdk-<version>-*.whl

The recorder now signs every checkpoint via C_Sign inside the HSM. Read the key's public half back with bitexact recorder-key - it works for whichever backend is configured, including a key generated inside the token that cannot be exported - and publish it (--expect-recorder-key) to your relying parties.

This path is exercised against a real PKCS#11 module: scripts/gate_hsm.sh provisions a SoftHSM2 token, generates an Ed25519 key inside it, and runs the tests marked hsm against it. SoftHSM2 is software, so that proves the interface, not a particular device: hardware timing, session limits and a vendor's own quirks still need the device you intend to deploy.

For the post-quantum half, generate an ML-DSA-65 (or -87) key pair in the same token under its own label (Thales Luna firmware 7.9 and later, and any PKCS#11 3.2 token, offer CKM_ML_DSA) and set BITEXACT_HSM_PQ_LABEL to it. The parameter set is read from the public key's size. That path is exercised against a real module too: SoftHSM2 has no ML-DSA, so a second stage of scripts/gate_hsm.sh provisions a kryoptic PKCS#11 3.2 token, generates the Ed25519 and ML-DSA-65 keys inside it, and signs a run's checkpoints with both halves, the private keys never leaving the token. kryoptic is software as well: validate the path on the device you deploy.

On AWS this means CloudHSM, not KMS. AWS/GCP KMS can't sign Ed25519 — but KMS envelope protection (below) gives you KMS involvement anyway.

AWS KMS envelope — key protected by KMS, no algorithm change#

Cheaper than a dedicated HSM, and it keeps ed25519. Generate a recorder key, encrypt it with a KMS key, and hand the recorder only the ciphertext:

bitexact keygen -o recorder.key        # note the trust-file entry it prints
for f in recorder.key recorder.pq.key; do
  aws kms encrypt --key-id <kms-key> --plaintext fileb://$f \
    --query CiphertextBlob --output text | base64 -d > $f.kms
done
shred -u recorder.key recorder.pq.key  # keep only the encrypted blobs

export BITEXACT_SIGNING_KMS_CIPHERTEXT=/path/to/recorder.key.kms
export BITEXACT_SIGNING_KMS_PQ_CIPHERTEXT=/path/to/recorder.pq.key.kms
pip install --no-index --find-links wheels --require-hashes -r requirements-aws.lock   # boto3; in your platform bundle
pip install --no-index --no-deps bitexact_sdk-<version>-*.whl   # standard AWS credential chain

At startup the recorder calls KMS Decrypt once (IAM-gated, CloudTrail-audited) and signs locally thereafter — no per-checkpoint network call. The key is never on disk in the clear; access is governed by your KMS key policy. bitexact recorder-key prints the public half at any time, which is what you publish to relying parties. (The same envelope pattern applies to GCP KMS / Azure Key Vault — ask if you need those wired.)

Assurance note, stated plainly. This backend protects the key at rest; unlike the PKCS#11 path, the key is in this process's memory while it signs. And unlike the PKCS#11 path it has never been exercised against real AWS — its tests drive an injected fake, so what is proven is the wiring, not the behavior of KMS. Treat it as the middle tier it is, and validate it in your own account before relying on it.

What stays constant#

No matter where the key lives, the checkpoint records, the bundle format, and the standalone verifier are identical — a relying party pins the same --expect-recorder-key (with --expect-recorder-pq-key, or a trust-file entry carrying pq_public_key) and never has to know or care that it came from an HSM. bitexact recorder-key --pq prints the post-quantum half.