An open-source PKCS#11 v3.2 cryptographic module under Simorgh Labs
2026-07-18
The headline of this release is not a feature. It is a defect we shipped, in every signed build, for months — and that our own tests never caught, because the one place it would have surfaced was switched off everywhere.
FIPS 140-3 §7.10.2 requires a cryptographic module to verify its own
integrity at start-up: hash the loaded binary, compare against a digest
embedded at build time, refuse to run on a mismatch. FreeHSM has that
check. It is wired into C_Initialize. It is documented. And until
v1.5.0 it never actually verified anything.
The embedded-digest slot was declared like this:
const uint8_t fhsm_module_integrity_digest[32] = { 0 };
with a comment, one line above, stating that a volatile qualifier
“prevents the compiler from constant-folding the digest into the code
generator.” The rationale was written. The keyword was not there.
The digest is patched into the binary after compilation by the signing
script. Without volatile, GCC is free to assume the slot still holds its
initialiser — and it folded the explicitly-initialised element [0] to a
literal zero. So byte 0 of every comparison came from the code generator
and bytes 1–31 from memory:
computed = 52fad82b6ccecf78306ca4249d37a173031b514276c318ab742755faa7a94e5a
embedded = 00fad82b6ccecf78306ca4249d37a173031b514276c318ab742755faa7a94e5a
^^
Every correctly signed build failed its own integrity check on that one
byte. Nobody noticed, because the entire toolchain — unit tests, the
external harness, CI — runs with FHSM_INTEGRITY_ALLOW_UNSIGNED=1, which
downgrades the failure to a warning. A security mechanism exercised only
from behind its own bypass is not exercised at all. That process gap, not
the missing keyword, is the real bug.
The fix is one qualifier. The lesson is the test we added alongside it:
make test-integrity now runs the check without the bypass, on a
freshly signed binary, and asserts three things — an unsigned binary must
fail, a signed binary must pass, and a signed-then-tampered binary must
fail. It is gating in CI. We confirmed it catches the original bug by
reverting the fix and watching it go red.
Worth noting: a v1.2.1 fix had already removed a different bypass in the
same function (an unconditional return CKR_OK). That turned “always
passes” into “always fails”, which the env var then masked. The check has
never actually gated anything until now.
The rest of v1.5.0 is the result of running FreeHSM against pkcs11-check, Denis Mingulov’s vendor-neutral PKCS#11 behavioural harness — 100k+ checks that know nothing about our implementation. Over the campaign, failures went from 361 to 13 and crashes from 7 to 0.
Several of the findings were not “a test is red” but “the module does the wrong thing and no counter was measuring it”:
0x108C, which is
CKM_AES_XCBC_MAC in the spec, and used 0x108A (the real
CKM_AES_CMAC) for GMAC. A caller asking for CMAC silently got a
different algorithm. Verified against the OASIS header, not a
recollection of it.CKA_PARAMETER_SET sat on
CKA_MODIFIABLE’s code point, so every ML-DSA / ML-KEM / SLH-DSA
key-gen silently used the default — asking for ML-DSA-87 gave you
ML-DSA-65, no error.C_CancelFunction was never
wired, shifting every later slot; a v3.0 caller dispatching through the
table invoked the wrong function and eventually segfaulted.CKA_LOCAL lied about provenance. It reported “generated on this
token” for keys that were imported.CKA_SENSITIVE=False produced a readable
copy. Closed the spec way, via CKA_UNWRAP_TEMPLATE on the wrapping
key.Correcting the AES-MAC code points is a breaking change. If your code
hard-codes 0x108C for AES-CMAC, move it to 0x108A. Callers using
0x108A (including OpenSC’s pkcs11-tool, which was right all along) are
unaffected and now get real CMAC. The binary name libfreehsm.so
and every other PKCS#11 identifier are unchanged.
Thirteen harness failures remain, and they should. Roughly half are design positions (the CBC-PAD padding oracle is inherent to the mode — the harness says so itself; GCM IV-reuse detection needs state a stateless software token does not keep) or gaps in the harness (two tests drive non-FIPS RSA PKCS#1 v1.5 encryption unconditionally, which a FIPS-strict build correctly does not offer). The full triage — fixable vs. design vs. report-upstream — is published in the repo, next to the security advisories we file against ourselves. Same logic as always: a green number bought by weakening the module is not a win.
This is also the first release under the new name. The repository is now
freehsm (was freehsm-c; old URLs redirect). FreeHSM is the Apache-2.0
PKCS#11 library; Simorgh PKI is the product being built on it; Simorgh
Labs is the org. Nothing consumer-facing changes: binary name, PKCS#11
identifiers, and the GPG signing key are all stable across the rename.
v1.5.0 is signed with GPG key 743A 6A59 04A1 4616 46A6 408D E485 6016
2DBB F28A 2 and available on GitHub, GitLab, and Codeberg.