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#
| Backend | Assurance | Configure with | Its post-quantum half | Install |
|---|---|---|---|---|
| File (default) | key in a file on the host | BITEXACT_SIGNING_KEY=/path/to/key | <name>.pq.key beside it, or BITEXACT_SIGNING_PQ_KEY | base wheel |
| PKCS#11 HSM | key never leaves hardware | BITEXACT_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 envelope | key encrypted at rest by KMS, decrypted once in memory | BITEXACT_SIGNING_KMS_CIPHERTEXT=/path/to/blob | BITEXACT_SIGNING_KMS_PQ_CIPHERTEXT: the .pq.key file, KMS-encrypted the same way | the [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.