FreeHSM C

An open-source PKCS#11 v3.2 cryptographic module under Simorgh Labs

View the Project on GitHub afchine1337/freehsm

FreeHSM C — Operational User Guidance (CC EAL4+ AGD_OPE.1)

Not certified. FreeHSM is built to the requirements of FIPS 140-3 Level 1 and Common Criteria EAL4+, and documented with their methodologies. A certificate costs more than this project will ever have, and that cost is exactly the barrier that keeps public bodies, universities and developing countries away from auditable cryptography. What a certificate attests, discipline can make verifiable — by anyone, at no cost.

This document is an evaluation deliverable and is written, as the methodology requires, as though a certificate existed. Read “certified” throughout as “the configuration this module is built to”. It is published as a worked example, not as part of a submission.

TOE : FreeHSM Cryptographic Module (the version you built ; v1.0.0-FIPS in earlier drafts was a placeholder and never existed as a release) Audience : Security Officer (SO) and User operators of an installed TOE Prerequisites : the TOE has been installed and brought to the secure operational state per docs/AGD_PRE.md

This document tells operators which services they may call, what arguments are accepted, what each error means, and how to respond when things go wrong. Read it once before first use ; keep it within reach during operations.


1. Roles & responsibilities

Role Identifier Allowed services
Security Officer (CO) CKU_SO C_InitToken, C_InitPIN, C_SetPIN (own), audit review
User CKU_USER C_GenerateKey, C_GenerateKeyPair, C_Encrypt/Decrypt, C_Sign/Verify, C_Digest, C_DeriveKey, C_Wrap/Unwrap, C_SetPIN (own)

Anonymous (pre-login) services :

Service Purpose
C_Initialize Module bring-up (called once per process)
C_GetInfo Module identity
C_GetSlotList Enumerate slots
C_GetSlotInfo Slot status
C_GetTokenInfo Token status (incl. PIN counters)
C_OpenSession Open a session for login
C_CloseSession Close a session
C_GetMechanismList List dispatchable CKM_*
C_GetMechanismInfo Per-mechanism flags & key range

2. Common operator workflow

#include <pkcs11.h>

CK_FUNCTION_LIST_PTR p11;
C_GetFunctionList(&p11);          /* loaded from libfreehsm.so */
p11->C_Initialize(NULL);          /* triggers integrity check + KAT  */

CK_SESSION_HANDLE s;
p11->C_OpenSession(slot, CKF_RW_SESSION|CKF_SERIAL_SESSION,
                    NULL, NULL, &s);
p11->C_Login(s, CKU_USER, (CK_UTF8CHAR_PTR)pin, strlen(pin));

/* ... call C_GenerateKey, C_Encrypt, etc. ... */

p11->C_Logout(s);                  /* zeroizes DEK in this session   */
p11->C_CloseSession(s);
p11->C_Finalize(NULL);

After C_Finalize the process must not call any other C_* function until a new C_Initialize. Doing so returns CKR_CRYPTOKI_NOT_INITIALIZED (0x190).

3. Mechanism selection guidance

Pick mechanisms from the approved set listed in docs/MECHANISMS.md and docs/FIPS_140_3.md §4. The mapping below summarises the canonical choice per use case.

Use case Recommended mechanism
Symmetric encryption at rest CKM_AES_GCM (96-bit IV, 128-bit tag) — not CKM_AES_CBC_PAD, see §3.1
Symmetric encryption in transit CKM_AES_GCM or CKM_AES_CCM
Symmetric authentication CKM_SHA256_HMAC or CKM_AES_CMAC
Streaming MAC (large messages) CKM_KMAC128 / CKM_KMAC256
Asymmetric signature (classical) CKM_SHA384_RSA_PKCS_PSS or CKM_ECDSA_SHA384
Asymmetric signature (PQ) CKM_ML_DSA (parameter set ML-DSA-65)
Asymmetric signature (PQ, pre-hash) CKM_HASH_ML_DSA_<hash>, or CKM_HASH_ML_DSA when the caller hashes
Asymmetric signature (hybrid) Withdrawn. CKM_HYBRID_ED25519_ML_DSA_65 was de-advertised on 2026-09-24 (#17): the two components signed the bare message and the results were concatenated, so the Ed25519 half lifts out as a valid standalone signature — the non-separability failure draft-ietf-lamps-pq-composite-sigs §2.2 exists to prevent. CKM_COMPOSITE_MLDSA65_ED25519 replaces it and is available in the all-mechanisms profile only.
Key encapsulation (classical) CKM_ECDH1_DERIVE over P-256/384; X25519/X448 through the same mechanism with a CKK_EC_MONTGOMERY key, in the all-mechanisms profile with FHSM_INTEGRITY_ALLOW_UNSIGNED=1 only (the OpenSSL FIPS provider a signed module loads has neither curve). PKCS#11 defines no CKM_X25519_DERIVE.
Key encapsulation (PQ) CKM_ML_KEM (parameter set ML-KEM-768)
Key encapsulation (hybrid) Withdrawn. CKM_HYBRID_X25519_ML_KEM_768 was de-advertised on 2026-09-24 (#17); no standards body specifies this construction. Use CKM_ML_KEM and a classical exchange separately if you need both.
Password-based KEK derivation CKM_PKCS5_PBKD2 with ≥ 200 000 iter
Key derivation in a protocol CKM_HKDF_DERIVE (SHA-256+)

Any mechanism not in the approved list (e.g. CKM_MD5, CKM_DES3_CBC, CKM_RSA_PKCS) is rejected at dispatch in approved mode and returns FHSM_RV_FIPS_NOT_APPROVED (0x80000003).

3.1 Approved is not the same as safe for your use case

Two mechanisms in the approved set carry a caveat that FIPS approval does not express, because approval is about the algorithm and the caveat is about the protocol you build with it. Both are usable and neither is a defect in the module; the choice belongs to you.

CKM_AES_CBC_PAD is a padding oracle. Decryption tells the caller whether the PKCS#7 padding was well-formed — CKR_OK when it was, CKR_ENCRYPTED_DATA_INVALID when it was not. That single bit, repeated over chosen ciphertexts, recovers plaintext byte by byte (Vaudenay 2002; POODLE, CVE-2014-3566). It is inherent: CBC-PAD carries no authentication tag, so there is no way for C_Decrypt to distinguish “corrupted” from “not for you” without saying which, and returning garbage plaintext instead would be worse for every honest caller.

Measured on this module: 99.6% of corrupted ciphertexts are refused, and the 0.4% that decrypt are the ones whose random final block happens to carry valid padding — the theoretical rate for any correct implementation. The module is behaving as specified. The exposure is in the protocol.

Use CKM_AES_GCM or CKM_AES_KEY_WRAP wherever an attacker can submit ciphertexts of their choosing and observe whether decryption succeeded. CKM_AES_CBC_PAD is appropriate for data you decrypt for yourself — a file at rest, a backup — where no adversary drives the decryption.

CKM_AES_CBC and CKM_AES_CTR provide no integrity at all. No oracle, because there is no padding to check, but also no detection of tampering: a flipped ciphertext bit becomes a flipped plaintext bit under CTR, silently. Pair them with CKM_SHA256_HMAC over the ciphertext (encrypt-then-MAC, RFC 7366) or use CKM_AES_GCM, which does both in one pass.

See R2 in docs/PKCS11_CHECK_FINDINGS.md for the measurement, and tests/test_cbc_pad_oracle.c for the regression guard.

3.2 Through p11-kit server, fewer mechanisms reach the module

An application that reaches the token through p11-kit server and p11-kit-client.so sees only the mechanisms p11-kit’s RPC layer knows how to carry. The module is not involved: a call p11-kit does not carry is refused with CKR_MECHANISM_INVALID before it leaves the client, and the audit log records nothing.

What crosses depends on the p11-kit version (checked 2026-09-30):

p11-kit post-quantum mechanisms carried
0.26.5 and earlier (every release to date) none
master since 2026-09-08 (PR #745, commit 63c7bc4f) CKM_ML_KEM, CKM_ML_DSA, CKM_SLH_DSA and their key-pair generators, HashML-DSA and HashSLH-DSA
any version not CKM_COMPOSITE_MLDSA65_ED25519

So until a p11-kit release after 0.26.5, remote access is for RSA, ECDSA, AES and HMAC. The composite stays out even then: its code point is vendor-defined, and p11-kit carries mechanisms it names. A change that carries every mechanism taken without a parameter, the composite included, is proposed upstream as p11-kit PR #779 and is not merged. Issue #778, which reported the limit, was closed as completed on 2026-09-18 by #745.

Check what your installed p11-kit carries rather than relying on this table; docs/P11_KIT_REMOTING.md has the measurement and the setup.

4. Security advice (operator-actionable)

4.1 PIN management

4.2 Throttle handling (transient delay)

Between attempts, C_Login may return FHSM_RV_PIN_THROTTLED (0x80000004) with a wait time exposed via the pinStatus.throttle_*_remaining fields of C_GetTokenInfo. The client SHOULD :

  1. Display the wait time to the human operator.
  2. Sleep for the indicated number of milliseconds.
  3. Retry exactly once.

Repeatedly hammering through the throttle does not change the outcome. The failure count is persisted in the token file, so a restart does not reset it, and on load the delay is re-imposed from that count — restarting is not a way to skip a wait that was earned.

The deadline itself is not persisted, and this is a correction. It used to be, in the CLOCK_MONOTONIC domain, so that date -s could not shorten a cooldown. That reasoning was sound; its consequence was not examined. CLOCK_MONOTONIC restarts at boot and the file does not, so a deadline written after thirty days of uptime was still thirty days in the future when the next boot read it: a 500 ms cooldown became a 29.8-day refusal of the correct PIN, reported as PIN_THROTTLED with nothing to explain it. The same shape as the unseal defect in §”TPM sealing” below — a routine event locking out an operator who had done nothing wrong. The delay is now derived from the count instead of stored, capped at 60 000 ms as documented, and date -s still changes nothing.

If you are running a build from before this correction and a token reports PIN_THROTTLED for far longer than 60 seconds, that is this defect. The cooldown clears once the machine has been up as long as the boot that wrote it; upgrading and reloading the token clears it immediately.

4.2b Where the audit log lives

Do not put the audit log on tmpfs, and check rather than assume.

The log’s guarantee is that a line is durable before the operation it records returns — see docs/AUDIT_DURABILITY.md. On a volatile filesystem fdatasync returns immediately without a barrier, so the guarantee is silently absent: nothing errors, nothing is logged about it, and the log looks entirely normal until a power loss takes the last of it.

This is easy to do by accident. /tmp is tmpfs by default on current Debian, and FHSM_AUDIT_LOG can point the log somewhere other than the tokens directory. The symptom is a log that is implausibly fast:

make tests/bench_fsync_floor
./tests/bench_fsync_floor /var/lib/freehsm       # where the log will live

This is the shortest check and it involves no module: one append, one fdatasync, repeated. It names the filesystem it measured, prints the distribution, and exits non-zero if the barrier returned too fast to have reached stable storage. Use its exit status in a provisioning script; a message an operator has to read is a message an operator can miss.

  filesystem   ext4 on /dev/sdc (/sessions)
  min             2.058 ms
  p50             2.553 ms
  ...

versus, on tmpfs:

  NOT A DURABILITY MEASUREMENT.
  A barrier that returns in 0.000 ms did not reach stable storage -- this is
  tmpfs, where fdatasync is a no-op.

A durable barrier costs milliseconds on the storage measured so far. Microseconds mean no barrier happened. tests/bench_audit_rate measures the same thing through the module if you want the log’s own cost as well, but the floor tool is the one to run first — it fails for one reason only.

The same caution applies to a hypervisor that acknowledges barriers without honouring them; VirtualBox’s host I/O cache does exactly that, and no tool here can see through it. A hypervisor that lies produces a plausible millisecond figure and no warning of any kind. If the guarantee matters, the storage has to be trusted at a level below this module.

4.2c The log is a set of files, not one file

Each opening of the module creates its own log, audit.log.NNNNNN, because a hash chain has exactly one author: two processes sharing one file each resumed the chain from its tail and each believed itself the successor of the same line, which destroyed it. Two ordinary tools running at once were enough.

For you this means:

freehsm-audit verify /var/lib/freehsm/audit  <audit_key_hex>

A directory verifies every numbered log in it, and reports holes in the numbering. A hole means a file was removed: each remaining chain still verifies on its own, so the numbering is the only thing that shows the deletion. Missing numbers at the end leave no hole — that is the same end-of-log truncation described in §4.3, not a new weakness.

Two consequences to plan for:

4.3 Audit log review

The audit log is produced, and the chain is verifiable. The warning that stood here — that no log was written and the control could not be relied on — no longer applies. C_Initialize opens the log, the chain survives restarts, a failed write latches the module into ERROR, and both verifiers detect a modified, deleted, inserted or reordered record.

Three limits remain, and an operator should know them before relying on this:

  1. A log truncated at the end is not detected, and cannot be from the file alone: what remains is a shorter chain that verifies perfectly, which is indistinguishable from a log that stopped there. Mitigate by archiving (step 3 below) and by comparing the archived record count over time — a seq that goes backwards between two archives is the signal.
  2. A start-up integrity or KAT failure is not logged. The log is opened after the crypto layer, because chaining an entry needs HMAC. If the self-test fails, the primitives that would authenticate the entry are the ones that just failed. The module latches ERROR and refuses everything, so the condition is observable — but the reason will not be in the log. Read the process’s stderr and C_GetTokenInfo flags in that case.
  3. The chaining key does not defend against a live root. Sealed to the TPM it resists an attacker who takes the disk or changes the boot chain; it is unsealed into process memory to be used at all. See §4.4.

The audit log lives at {tokens_dir}/audit.log, or wherever FHSM_AUDIT_LOG points, HMAC-chained. The SO MUST :

  1. Periodically (recommended : weekly) verify the chain with the supplied verifier. Note the syntax: verify is a subcommand, and the 32-byte audit key is a required argument — the binary is freehsm-audit, and no freehsm-audit-verify exists.
    freehsm-audit verify /var/lib/freehsm/audit/slot0.audit.log <audit_key_hex_64>
    
  2. Investigate every login_fail, login_locked, login_throttled, and integrity_fail event.
  3. Archive monthly logs to immutable storage (WORM bucket, append-only volume) per the site’s retention policy.

A broken chain is a critical security event : the on-disk log has been tampered with by someone with write access to the audit directory. The verifier names the first line at fault and what it found — an altered record, a prev_hmac that does not follow, or a seq out of step. Take the system offline and follow the incident response procedure (SECURITY.md).

4.4 The audit key

The chain is authenticated by a 32-byte key, provisioned on first start and recovered afterwards. It is not derived from the token DEK: that would make the log writable only while logged in, and login_fail, login_locked and integrity_fail — the three events step 2 above tells you to investigate — would never be recorded.

Where it lives When
{tokens_dir}/audit.key.tpm, sealed FHSM_TPM_SEALING=1 and a TPM is present
{tokens_dir}/audit.key, mode 0600 otherwise

The module refuses to start rather than degrade quietly:

To verify a log you need that key. Read it with xxd -p -c 32 {tokens_dir}/audit.key on the host, or unseal it there. A log archived without its key cannot be verified later — archive the two separately, and never on the same medium.

4.5 Token backup

Token files are encrypted at rest under the operator PIN(s). Backups are therefore as safe as the PIN strength, but only if the backup retains the same access controls :

4.6 Logout discipline

After every operation sequence :

p11->C_Logout(s);
p11->C_CloseSession(s);

C_Logout zeroizes the DEK in process memory. C_CloseSession zeroizes the session-local key buffers. Failing to log out does not leak material on a process crash (the secure heap is process-local), but it does leave the audit trail with an unclosed session.

Since v2.3.0 the module enforces the login rules of PKCS#11 §5.6. The SO and the USER cannot hold the token at the same time: provisioning a token – C_Login as SO, C_InitPIN, then working as USER – needs a C_Logout between the two logins, or C_Login answers CKR_USER_ANOTHER_ALREADY_LOGGED_IN. Before v2.3.0 the second login silently took the token over. An SO login is refused while a read-only session is open (CKR_SESSION_READ_ONLY_EXISTS), and a read-only session cannot be opened while the SO is logged in (CKR_SESSION_READ_WRITE_SO_EXISTS). Closing the last session logs the token out, and the audit log records it as a logout.

5. Service reference (selected)

5.1 C_GenerateKey (AES-256)

CK_MECHANISM mech    = { CKM_AES_KEY_GEN, NULL, 0 };
CK_OBJECT_HANDLE key = 0;
CK_ULONG keylen      = 32;
CK_BBOOL true_       = CK_TRUE;
CK_ATTRIBUTE templ[] = {
    { CKA_VALUE_LEN, &keylen, sizeof(keylen) },
    { CKA_TOKEN,     &true_,  sizeof(true_)  },   /* persist */
    { CKA_ENCRYPT,   &true_,  sizeof(true_)  },
    { CKA_DECRYPT,   &true_,  sizeof(true_)  },
};
p11->C_GenerateKey(s, &mech, templ, 4, &key);

Returns CKR_OK and a non-zero key handle. The key material lives in the secure heap until the slot is logged out.

5.2 C_Encrypt (AES-GCM)

CK_GCM_PARAMS gcm = {
    .pIv      = iv,   .ulIvLen      = 12,
    .pAAD     = aad,  .ulAADLen     = sizeof(aad),
    .ulTagBits = 128,
};
CK_MECHANISM mech = { CKM_AES_GCM, &gcm, sizeof(gcm) };

p11->C_EncryptInit(s, &mech, key);
p11->C_Encrypt(s, plaintext, plen, ciphertext, &clen);

clen on entry is the output buffer capacity ; on exit it is the actual ciphertext length (plaintext length + 16 for the tag). For multi-part processing, use C_EncryptUpdate/C_EncryptFinal.

5.3 C_Sign (ECDSA-P-384 + SHA-384)

CK_MECHANISM mech = { CKM_ECDSA_SHA384, NULL, 0 };
p11->C_SignInit(s, &mech, priv_key);
p11->C_Sign(s, msg, msg_len, sig, &sig_len);

6. Error response

CK_RV (hex) Symbol Operator action
0x00 CKR_OK continue
0x05 CKR_GENERAL_ERROR check syslog ; if recurring, contact vendor
0x06 CKR_FUNCTION_FAILED retry once ; if persists, file a support ticket
0x60 CKR_KEY_HANDLE_INVALID handle expired (session closed?) ; restart sequence
0x70 CKR_MECHANISM_INVALID not in C_GetMechanismList ; pick approved mechanism
0xA0 CKR_PIN_INCORRECT retry with correct PIN ; mind the throttle
0xA4 CKR_PIN_LOCKED call SO to unlock with C_InitPIN
0xB3 CKR_SESSION_HANDLE_INVALID open a fresh session
0x101 CKR_USER_NOT_LOGGED_IN C_Login first
0x190 CKR_CRYPTOKI_NOT_INITIALIZED C_Initialize first ; do NOT call after C_Finalize
0x80000001 FHSM_RV_KAT_FAILED Critical : module halted ; do not reuse ; reinstall
0x80000002 FHSM_RV_INTEGRITY_FAILED Critical : binary tampered ; reinstall from verified bundle
0x80000003 FHSM_RV_FIPS_NOT_APPROVED switch to an approved mechanism
0x80000004 FHSM_RV_PIN_THROTTLED wait the indicated milliseconds, retry once
0x80000007 FHSM_RV_SECURE_HEAP_EXHAUSTED reduce key inventory or raise secure_heap_kb
0x80000008 FHSM_RV_RNG_FAILURE Critical : DRBG failed self-test ; restart the process
0x80000009 FHSM_RV_PROVIDER_UNAVAILABLE from C_Initialize only : a required OpenSSL provider would not load. The module prints the reason and OpenSSL’s error stack on stderr. After an OpenSSL upgrade, see AGD_PRE 3.3.1

Critical errors latch the module ERROR state. The only recovery is to restart the process (per FIPS 140-3 §7.10.5). The operator MUST also examine the audit log to determine the root cause before relaunching.

7. Routine maintenance

Frequency Task
Daily Confirm freehsm-bound-service is healthy ; check syslog for FHSM warnings
Weekly Verify each slot’s audit chain ; archive log if it exceeds rotation threshold
Monthly Rotate User PINs as per site policy ; review audit aggregates for anomalies
Quarterly Test make repro against a fresh checkout ; confirm digest still matches deployed binary
Yearly Re-run penetration-testing tooling ; update OE.OS / OE.OPENSSL pins if needed (triggers re-evaluation per ALC_CMC.md §8)

7bis. Runtime modes (v1.1.0)

The module selects between legacy (default) and FIPS strict mode at runtime. The choice is read from the FHSM_MODE environment variable first, then from the mode = directive in /etc/freehsm/freehsm.conf.

Mode Activation Behaviour
legacy (default) nothing, or FHSM_MODE=legacy All declared mechanisms callable. MD5 and SHA-1 (digest) route to fhsm_legacy_dispatch. DES, 3DES, RC4 currently return CKR_MECHANISM_INVALID (handlers reserved for future wiring).
fips (= strict) FHSM_MODE=fips or mode = fips in conf Every non-FIPS-approved mechanism returns CKR_MECHANISM_INVALID regardless of OpenSC’s negotiation table. Conforms to SP 800-131A Rev. 3.

For FIPS 140-3 evaluation runs the operator MUST set FHSM_MODE=fips (or write mode = fips in the conf) BEFORE any C_Initialize. The mode is cached on first lookup, so changing the variable after init has no effect.

Hardware-backed sealing (opt-in)

Variable Effect
FHSM_TPM_SEALING=1 At token init, the DEK is sealed to the host TPM 2.0 (PCRs 0-7). A companion {slot}.tok.tpm file is written next to the token. At login, the PBKDF2-unwrapped DEK is compared to the TPM-unsealed DEK. A mismatch, or an unseal that fails, refuses the login with CKR_DEVICE_ERROR — read the note below before enabling this.
FHSM_INTEGRITY_ALLOW_UNSIGNED=1 DEV-ONLY : bypasses every integrity-failure path (see §8 below) — this variable IS FORBIDDEN in production.

Before enabling: the module’s process needs TPM access

FHSM_TPM_SEALING=1 makes the module shell out to tpm2, which opens /dev/tpmrm0. On Debian and Ubuntu that node is crw-rw---- root tss, so the account running the PKCS#11 application must be in the tss group:

id -Gn | tr ' ' '\n' | grep -qx tss || sudo usermod -aG tss "$(id -un)"

Group membership is established at login, so an existing session will not pick it up; newgrp tss gives the current shell the new group without logging out.

Do not work around this by running the application as root. The token files would be created root-owned and the service would then need root for every subsequent operation, which is a larger concession than the one being avoided.

Without the group, every TPM call fails with a TCTI permission error and the module reports CKR_DEVICE_ERROR with tpm-unseal-failed in the audit log – the same signal as moved PCRs, so check the group before suspecting the PCRs.

Before enabling: the TPM must have a persistent primary key

Sealing binds to a persistent primary key at handle 0x81010001. The module does not create it, and if it is missing every seal fails. Nothing said so until the sealing path was first exercised against real hardware; this is that omission being corrected.

tpm2 readpublic -c 0x81010001            # present?  if not:
tpm2 createprimary -C o -g sha256 -G ecc -c /tmp/primary.ctx
tpm2 evictcontrol  -C o -c /tmp/primary.ctx 0x81010001

The handle is compile-time (TPM_PARENT_HANDLE in src/fhsm_tpm.c). An operator whose TPM already uses 0x81010001 for something else currently has no way to say so — that belongs in freehsm.conf and is not there yet.

scripts/validate_tpm_sealing.sh checks for the handle and offers to provision it, and exercises the whole sealing lifecycle against a real TPM including the PCR-change scenario below. Run it before trusting FHSM_TPM_SEALING with anything.

What a PCR change means for you (#109)

Sealing binds the DEK to PCRs 0 through 7. Those registers measure firmware and early boot: a BIOS/UEFI update, a Secure Boot key rotation, a bootloader or kernel change will move at least one of them. When that happens the TPM refuses to release the sealed DEK, and every login on that token fails until you act, even with the correct PIN.

This is the intended behaviour of measured boot, not a fault. What you get back is CKR_DEVICE_ERROR, deliberately distinct from CKR_PIN_INCORRECT:

Return Meaning What to do
CKR_PIN_INCORRECT / CKR_PIN_LOCKED The PIN was wrong. Normal PIN handling.
CKR_DEVICE_ERROR The PIN was right; the TPM side failed. See below.

Three causes produce CKR_DEVICE_ERROR, and the audit log distinguishes them:

Recovery from a firmware update. The DEK is still recoverable from the token file with the PIN — sealing is a second factor, not the only copy. Take a backup of {slot}.tok and {slot}.tok.tpm, then either roll the firmware change back so the PCRs return to their sealed values, or re-seal the token under the new measurements. Losing the .tpm file alone does not lose the token; losing the token file does.

A failed unseal does not consume a PIN attempt. Before v1.7.0 it did, and the throttle escalated with it, so a routine firmware update locked the token permanently for an operator who had done nothing wrong. That was a denial of service and it is fixed. A genuinely wrong PIN still counts, throttles and locks exactly as documented in §5.

Take a backup before enabling FHSM_TPM_SEALING on a token that matters.

Hardened DRBG

The fhsm_rng_bytes API now routes through fhsm_drbg_bytes which adds (a) multi-source entropy seeding, (b) SP 800-90B health tests (RCT, APT, CRNGT), (c) auto-reseed every 1 MiB of output or every hour. Any health-test alarm latches the module to FHSM_STATE_ERROR and forces a service restart. See RNG.md.

Pair-wise consistency check

Every C_GenerateKeyPair (RSA, EC, ML-KEM, ML-DSA, SLH-DSA) is followed by an automatic sign-verify (or encap-decap) round-trip with the freshly-generated key. Failure latches ERROR. Cost : ~5 ms for RSA-2048, sub-ms for EC, ~50 ms for SLH-DSA-128s.

8. Forbidden actions

The following actions invalidate the certified configuration and forfeit the FIPS / CC certificate :

A site that performs any of the above is operating outside the certified TOE and may not advertise FIPS 140-3 / CC EAL4+ compliance.