An open-source PKCS#11 v3.2 cryptographic module under Simorgh Labs
This document audits FreeHSM C against the side-channel attack categories most commonly tested by FIPS 140-3 Level 2/3 evaluators and CC EAL4+ AVA_VAN.5 reviewers.
The module aims for Level 1 initially, where non-invasive testing is out of scope, but documents Level 2 candidacy gaps below.
We assume an attacker who can :
C_* calls with µs precisionEvery secret-vs-secret compare uses fhsm_ct_memcmp (implemented in src/fhsm_state.c). Locations audited :
| File | Function | Purpose |
|---|---|---|
src/fhsm_token.c |
fhsm_token_login |
AES-GCM tag check uses OpenSSL’s CT compare internally ; we don’t re-verify |
src/fhsm_pkcs11.c |
C_Verify (HMAC path) |
fhsm_ct_memcmp(mac, pSig, mac_len) |
src/fhsm_pkcs11.c |
C_Verify (CMAC path) |
fhsm_ct_memcmp(mac, pSig, 16) |
src/fhsm_integrity.c |
fhsm_integrity_verify |
fhsm_ct_memcmp(g_last_digest, fhsm_module_integrity_digest, 32) |
kat/fhsm_kat_vectors.c |
KAT comparisons | fhsm_ct_memcmp throughout |
The PIN throttle check happens before the PBKDF2 derivation in fhsm_token_login :
if (*fails >= FHSM_PIN_MAX_FAILED) return FHSM_RV_PIN_LOCKED;
if (now < *until) return FHSM_RV_PIN_THROTTLED;
/* ... then PBKDF2 derivation ~100 ms ... */
This prevents an attacker from observing whether PBKDF2 ran or not (which would leak whether the throttle was active).
All sensitive buffers are cleared on the failure path AND on success-exit :
| Buffer | Where cleared |
|---|---|
| KEK (PBKDF2 output) | After AES-GCM wrap/unwrap |
| DEK (in secure heap) | C_Logout, C_Finalize, fhsm_token_close |
| Stack-allocated key buffers | fhsm_zeroize (calls OPENSSL_cleanse) |
| ECDH shared secret | Right after fhsm_token_object_add returns |
| OAEP label copy | After EVP_PKEY_CTX_set0_rsa_oaep_label (ctx takes ownership) |
| ML-KEM shared secret in harnesses | After comparison |
Only lengths and event types are written to the audit log. No PIN, no DEK byte, no key value, no plaintext. Verified by safe_ascii filter in fhsm_audit_event.
fhsm_secure_heap arena (mlocked pages, allocated via OPENSSL_secure_malloc).-fstack-protector-strong, -D_FORTIFY_SOURCE=2, -fstack-clash-protection enabled in Makefile, and -fcf-protection=full (Intel CET) wherever the compiler accepts it – x86 only; the Makefile probes for it so that arm64, ppc64el, riscv64 and s390x build (PR #18).The following are documented as known limitations for future hardening :
The OpenSSL FIPS provider has its own side-channel posture, audited by Cryptosense and others. FreeHSM C inherits it. Any timing variation in AES-GCM, ECDSA, RSA-PSS comes from there.
PBKDF2 internally runs 200 000 HMAC-SHA-256 iterations. Each iteration takes a deterministic time on a fixed CPU. An attacker measuring total time can confirm that the iteration count was 200 000 (not 0 = wrong PIN), but cannot extract the PIN itself. No fix needed.
EVP_DigestSign for ML-DSA and SLH-DSA may not be fully constant-time in OpenSSL 3.5.6 (the implementations are very new). FIPS provider validation is in progress upstream. Future hardening : pin to a provider version with constant-time PQ.
Reading the token file from disk is not constant-time (cache effects, FS journaling). An attacker monitoring file I/O could learn that a login is happening but not the PIN. Mitigation : run the module under LD_PRELOAD=libnonblocking-fs.so or unmount /tmp before sensitive ops (operational guidance, not module-internal).
Not applicable for software module on a general-purpose CPU. If integrated into a dedicated HSM appliance, the appliance’s hardware boundary handles this separately.
To pass AVA_VAN.5 / Level 2 :
CT-PINcompare : Even the early-exit on locked PIN reveals timing. Add a CT-equivalent path that always runs the PBKDF2 derivation and then ignores the result for locked PINs. Cost : ~100 ms per “locked” auth attempt.
Cache attacks on AES : OpenSSL FIPS provider uses AES-NI (constant-time) on modern x86. Confirm the build flags forbid the lookup-table fallback (-DOPENSSL_NO_AES_NO_NI=0).
Spectre/Meltdown : ARM/Intel mitigations apply at kernel level. We don’t add LFENCE in our own code. If the host runs on a vulnerable CPU (Intel pre-CFL or AMD pre-Zen2), advise turning on retpoline / IBRS.
TPM 2.0 binding : Reduces the impact of a memory disclosure : the sealed DEK is useless without the TPM identity. See src/fhsm_tpm.c.
PCR pinning : Validate that the OS kernel + initrd + bootloader haven’t been tampered with by binding the TPM seal to PCR 0-7. An attacker who replaces the kernel won’t be able to unseal.
The “Constant-Time Checker” binsec or ctgrind can be applied to the compiled .so. We have not yet run those tools (planned for v1.2.0).
For a manual quick check :
# Build with debug
make CFLAGS_EXTRA="-O0 -ggdb"
# Use perf to measure timing of C_Login with wrong PIN
perf stat -e cpu-clock -r 100 ./test_login_timing wrong_pin
perf stat -e cpu-clock -r 100 ./test_login_timing correct_pin
# Compare distributions. Expectation : both should be ~100 ms ± 5%
# (dominated by PBKDF2). If significantly different (> 1 ms variance),
# a timing oracle exists.
Status : Level 1 self-attestation. Level 2 hardening tracked as Task #155+ (TBD).