|
|
Message-ID: <71220217-8161-4009-80d2-b31c4726de51@gmail.com> Date: Sun, 28 Jun 2026 19:46:22 +0200 From: Afchine Mad <afchine.mad@...il.com> To: oss-security@...ts.openwall.com Subject: [Security advisory] FreeHSM C v1.1.0 - v1.2.1 : raw CKM_ECDSA signatures not externally verifiable (fixed in v1.2.2 ; v1.3.0 extends boot KAT regression guard to 6/7 surfaces) -----BEGIN PGP SIGNED MESSAGE----- Hash: SHA512 FreeHSM C v1.2.2 (security patch) and v1.3.0 (function-list completion + export-roundtrip extension) are out. Both ship in response to the first external responsible-disclosure to the project, by Denis Mingulov via pkcs11-check (2026-06-26). Source https://github.com/afchine1337/freehsm-c Mirrors https://gitlab.com/afchine.mad/freehsm-c https://codeberg.org/afchine1337/freehsm-c GPG key 743A 6A59 04A1 4616 46A6 408D E485 6016 2DBB F28A 2 [E] sub 9813 876A 34BA DD4A 0A50 915E 7EAC 4BA5 5574 DBE8 GHSAs GHSA-xpxx-66pp-pf99 Denis disclosure (Findings 1+2) HIGH GHSA-6jx9-gh48-5qf6 v1.2.1 integrity self-test HIGH GHSA-wgv9-m9cv-4647 v1.1.0 maintainer key leak MEDIUM (retrospective ; 14h exposure window 2026-06-12, mitigation complete same day, advisory published 2026-06-28) License Apache-2.0 Target FIPS 140-3 Level 1 / CC EAL4+ candidate Stack C11, OpenSSL 3.5 FIPS provider, PKCS#11 v3.2 == Reporter == Denis Mingulov via pkcs11-check (2026-06-26). First external responsible-disclosure to the project. Crediting follows the reporter's preferred wording (see Acknowledgements). == Headline finding (Finding 1, HIGH) == In every signed release of FreeHSM C between v1.1.0 (2026-06-12) and v1.2.1 (2026-06-21) inclusive --- 20 releases over a 15-day window --- the raw CKM_ECDSA and CKM_RSA_PKCS sign paths produced signatures that no third-party verifier could check. The defect was in src/fhsm_pkcs11.c::sign_asymmetric : the raw mechanisms routed through EVP_DigestSignInit_ex(ctx, &pctx, mdname = NULL, ...), and on the OpenSSL 3.x default provider's ECDSA digest_sign function, mdname = NULL triggers an internal default digest (observed as SHA-256 on the 3.5.x default provider). The module silently signed SHA-256(input) instead of input. The module's own C_Verify path applied the same default digest symmetrically (mdname = NULL on verify), so module-internal sign + verify cycles cancelled the double-hash out and appeared internally consistent. Wycheproof CI exercises verification only against pre-computed signatures and never exercised the module's sign path externally, so the gap was invisible to internal testing. Any third-party verifier expecting raw ECDSA on the supplied digest (the standard contract for CKM_ECDSA) rejected every signature. CVSS v3.1 (Finding 1) : Vector CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H Base 7.5 HIGH on correctness / interoperability CWE-347 (Improper Verification of Cryptographic Signature) The defect is NOT exploitable in confidentiality or integrity : the private key is not leaked, the signature is mathematically valid for the (wrong) double-hashed digest, no key material is exposed. == Second finding (Finding 2, Medium compliance) == C_CreateObject, C_GetSessionInfo, C_GetObjectSize, C_CopyObject, and C_SetAttributeValue were exported as ELF symbols but their slots in the v2.40 CK_FUNCTION_LIST pfn[] array were not assigned in C_GetFunctionList. Normal PKCS#11 consumers (anything calling fl->C_*) received CKR_FUNCTION_NOT_SUPPORTED (0x54). Internal test harnesses and the Wycheproof Python adapters bypass the function-list dispatch and call the symbols via dlsym, so the gap was invisible to CI. CWE-693 (Protection Mechanism Failure). v1.2.2 wires 3 of the 5 slots : 15 C_GetSessionInfo implemented + wired in v1.2.2 20 C_CreateObject wired in v1.2.2 (implemented since v1.0) 23 C_GetObjectSize implemented + wired in v1.2.2 v1.3.0 (released ~4h after v1.2.2 the same day) closes the remaining 2 slots and adds a 6th outside Denis's report scope : 21 C_CopyObject implemented + wired in v1.3.0 25 C_SetAttributeValue implemented + wired in v1.3.0 66 C_WaitForSlotEvent wired in v1.3.0 (last unwired v2.40 slot) PKCS#11 v2.40 dispatch coverage : 51/67 wired in v1.3.0 (was 47/67 in v1.2.2, 44/67 in v1.2.1 pre-report). All Denis- flagged slots resolved. == Killer test (Finding 1) == M=./libfreehsm-fips.so PIN=12345678 pkcs11-tool --module $M --login --pin $PIN \ --keypairgen --key-type EC:prime256v1 --id 01 printf hello > msg.txt openssl dgst -sha256 -binary msg.txt > digest.bin pkcs11-tool --module $M --login --pin $PIN --sign \ --mechanism ECDSA --id 01 -i digest.bin -o sig.raw # Module self-verify : PASS (uses the same buggy default digest) pkcs11-tool --module $M --login --pin $PIN --verify \ --mechanism ECDSA --id 01 -i digest.bin \ --signature-file sig.raw # Export pubkey + verify externally with openssl : pkcs11-tool --module $M --login --pin $PIN --read-object \ --type pubkey --id 01 -o pub.der openssl pkey -pubin -inform DER -in pub.der -out pub.pem R=$(head -c32 sig.raw | od -An -tx1 | tr -d ' \n') S=$(tail -c32 sig.raw | od -An -tx1 | tr -d ' \n') printf 'asn1=SEQUENCE:sig\n[sig]\nr=INTEGER:0x%s\ns=INTEGER:0x%s\n' \ "$R" "$S" > sig.cnf openssl asn1parse -genconf sig.cnf -out sig.der # Pre-v1.2.2 : Verification failure (exit 1) # v1.2.2+ : Verified OK (exit 0) openssl dgst -sha256 -verify pub.pem -signature sig.der msg.txt Reproduces on P-256, P-384, P-521 (every run). == Affected releases == v1.1.0 - v1.2.1 (every signed release between 2026-06-12 and 2026-06-21). Origin commit : 0c0f5df. 20 GPG-signed releases over a 15-day window are affected by Finding 1. The same 20 releases are also affected by Finding 2 (function-list gap). == Fixed in == v1.2.2 (commit e10586f, 2026-06-27 22:42 CEST) 06af928 raw CKM_ECDSA / RSA-PKCS use EVP_PKEY_sign 7bab917 wire C_GetSessionInfo + C_CreateObject + C_GetObjectSize into the function list 28071e9 ECDSA-P256/384/521 export-roundtrip boot KAT regression guard cbb1259 encryption subkey + SECURITY.md update (operational precondition that enabled the encrypted-channel disclosure of Denis's remaining findings) v1.3.0 (commit e6d5d21, 2026-06-27 22:56 CEST) Function-list completion : C_CopyObject (slot 21) C_SetAttributeValue (slot 25) C_WaitForSlotEvent (slot 66) [outside Denis's report] Boot KAT export-roundtrip extended to 6/7 surfaces : RSA-2048-PSS-SHA256-export-roundtrip RSA-2048-OAEP-SHA256-export-roundtrip Ed25519-export-roundtrip ML-DSA-65-export-roundtrip ML-KEM-768-export-roundtrip ECDH-P256/P384/P521-export-roundtrip (SLH-DSA excluded by design, documented gap) Total boot KAT count : 62 (was 54 in v1.2.2, 51 in v1.2.1). == Recommended action == Any deployment running v1.1.0 - v1.2.1 should upgrade to v1.3.0 (or v1.2.2 minimum). The upgrade is a drop-in .so replacement ; PKCS#11 wire format is unchanged ; token store + PIN files are forward-compatible. Signatures previously produced by the module via raw CKM_ECDSA between v1.1.0 and v1.2.1 cannot be retroactively repaired ; only signatures produced by v1.2.2+ are interoperable. If the original messages are still accessible, re-sign with v1.3.0 ; otherwise the v1.1.0 - v1.2.1 raw-ECDSA signatures are not recoverable. Stopgap workaround for Finding 1 (until upgrade is possible) : use the explicit pre-hash mechanism variants (CKM_ECDSA_SHA256, CKM_ECDSA_SHA384, CKM_ECDSA_SHA512) instead of raw CKM_ECDSA. The pre-hash variants route through the correct (hashed) sign path and produce signatures that verify externally without modification. Stopgap workaround for Finding 2 : call the affected functions via dlsym directly. The symbols are exported with default visibility ; the function-list dispatch returns CKR_FUNCTION_NOT_SUPPORTED in v1.1.0 - v1.2.1 but the symbols themselves work. == Disclosure decision == No CVE is requested for any of the three GHSAs. Same model as v1.2.1 : informational GitHub Security Advisory, pre-certification status, no known production deployments. A CVE will be requested retroactively if a third-party deployment is later identified that was running an affected version. == Discovery + correction protocol extension == The five-step protocol established in v1.2.1 (Security Target sec.13.8) is extended in v1.2.2 (sec.13.8.1) with two operational expectations on the maintainer side that surfaced during the disclosure : a. Encrypted-channel availability is a precondition for any third-party report. The disclosure exposed that the maintainer's published GPG key was sign-only ; the reporter could not encrypt the rest of his findings until the channel was restored. Commit cbb1259 added a cv25519 encryption subkey and published it to keys.openpgp.org with the maintainer's email verified. b. Reproduce before challenging. The Finding 1 fix took less than 90 minutes from git-clone of the reporter's reproduction script to a tested local fix. Internal CI greenness is not a counter-argument to a reproducible external report. The extension is sub-procedural and applies before step 1 of the existing protocol. It is now standing practice for any future external-reporter case. == Boot KAT regression guard extension (v1.3.0) == The ECDSA export-roundtrip pattern introduced in v1.2.2 has been extended in v1.3.0 to every external cryptographic surface the module exposes (6 of 7 ; SLH-DSA excluded by design on runtime-budget grounds and documented as a known gap). Each vector generates a fresh keypair, exercises the operation with the original references as control, serializes the public key via i2d_PUBKEY, reloads via d2i_PUBKEY (mimicking exactly what an external verifier does), and exercises the operation again with the reloaded peer. If a future OpenSSL provider upgrade re-introduces the silent- default-digest behaviour on any signing surface, or if i2d_PUBKEY / d2i_PUBKEY ever stops producing byte-stable round-trips, the relevant boot KAT fails at C_Initialize and the module refuses to start. The class of bug that hid in v1.1.0 - v1.2.1 for 15 days cannot recur silently on any v1.3.0+ build. == Generalisable lesson == If you maintain a cryptographic module with an external-API contract that depends on a third-party verifier matching your output : write a boot KAT that exercises the external-roundtrip property today. The v1.3.0 export-roundtrip pattern in kat/cavp_extended.c (see helpers run_ecdsa_export_roundtrip, run_rsa_pss_export_roundtrip, run_rsa_oaep_export_roundtrip, run_evp_digestsign_export_roundtrip, run_mlkem_export_roundtrip, run_ecdh_export_roundtrip) is the template. ~80 lines per surface, ~5 ms boot-time cost per vector. Cheapest insurance against the class of bug that hides behind a green CI for three weeks until an external reporter catches it. == Acknowledgements == Denis Mingulov for the careful report, the responsible-disclosure framing despite the encrypted-channel friction, the clean reproduction script, and the noise-aware framing on pkcs11-check raw output. First external contribution to FreeHSM C and a meaningful milestone for the project's maturation. - -- Afchine Madjlessi Simorgh Labs, Open Source Cryptography and Digital Trust afchine.mad@...il.com PGP 743A 6A59 04A1 4616 46A6 408D E485 6016 2DBB F28A 2 [E] 9813 876A 34BA DD4A 0A50 915E 7EAC 4BA5 5574 DBE8 -----BEGIN PGP SIGNATURE----- iHUEARYKAB0WIQR0OmpZBKFGGmRkCN5IVgFi278oogUCakFB/AAKCRBIVgFi278o oiVnAQCT52tccEGzGg/Cs9GyZ7QJ/qKzz7TXT2xMPt1Qx5O2dgEAxDPW9x3DFbTH B8YN32lMHz3eZ/+JD1lygzPyHG80HQs= =NaNo -----END PGP SIGNATURE-----
Powered by blists - more mailing lists
Please check out the Open Source Software Security Wiki, which is counterpart to this mailing list.
Confused about mailing lists and their use? Read about mailing lists on Wikipedia and check out these guidelines on proper formatting of your messages.