FreeHSM C

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

View the Project on GitHub afchine1337/freehsm

FreeHSM C — FIPS 140-3 Security Target (Draft v0.10)

On tests/test_smoke.tampered, named in §9.1.1, §13.8 and the revision log below. Nothing in the tree produces it and it is not in the repository; the tamper demonstration now lives in make test-integrity, which signs tests/test_integrity, flips one byte and requires the refusal.

The artefact itself is real and was located on 2026-09-02 in an off-repository archive dated 2026-08-15. Its own mtime is 2026-06-21 20:07, the day of the v1.2.1 fix; 1 580 544 bytes; sha256 8f6327dd0007361b581a21472c3a16c66f3c34c0924dc11f2557be3f293bf31e; and its .fhsm_digest is non-zero (707e11ee5855957d27e410c8c2104a50a72abedd2934cbf437a92220c557e8ef), so it was signed and then modified — the mechanism §9.1.1 describes.

What is not re-verifiable is the “1 byte flipped in .text” figure: the untampered signed binary of that day did not survive, so there is nothing to difference it against. Existence, date and signedness are established; the size of the edit rests on the record made at the time.

The three mentions keep the old name, because they are records of June 2026 and the revision log says of itself that its rows are “preserved verbatim as historical record”. Checked 2026-09-02.

Status : Written to the FIPS 140-3 Security Policy structure, never submitted. FreeHSM is not evaluated by a CST lab and will not be — see the mission note in ROADMAP.md. Published as a worked example of what a Security Policy for a software cryptographic module contains.

Standard : NIST FIPS 140-3 Security Requirements for Cryptographic Modules, issued March 2019, effective September 2020.

Implementation Guidance : NIST IG 9.5 (cryptographic module testing) ; FIPS 140-3 IG ; OASIS PKCS #11 v3.2.


1. Module identification

Field Value
Manufacturer (full legal name) Simorgh Labs, Open Source Cryptography and Digital Trust
Manufacturer (PKCS#11 32-octet field) Simorgh Labs (CK_INFO.manufacturerID, CK_SLOT_INFO.manufacturerID, CK_TOKEN_INFO.manufacturerID)
Module Name FreeHSM C
Library Description (PKCS#11) FreeHSM C (FIPS 140-3) (CK_INFO.libraryDescription)
Token Model (PKCS#11) FreeHSM-C-v1 (CK_TOKEN_INFO.model ; stable across the v1 major series)
Version 1.4.0-FIPS
Module Type Software (libfreehsm.so, ELF-64 shared object)
Module Embodiment Multi-chip standalone (GPC host)
Module Boundary The single .so file at SHA-384 = (see .fhsm_digest section, patched by make integrity)
Operational Environment (OE) Debian 13 / Linux kernel ≥ 6.1 / glibc ≥ 2.40 / OpenSSL 3.5.6 FIPS provider
Approved Cryptographic Algorithms See §3 below
Tested Configuration x86_64 Debian 13.0 stable, FIPS-mode enabled in /etc/ssl/openssl.cnf
Security Level Addressed Level 1 ; Level 2 requirements noted where they differ

2. Cryptographic Module Specification (§7.2)

2.1 Cryptographic Module Description

FreeHSM C is a software-only PKCS#11 v3.2 cryptographic module providing key generation, key storage, encryption, decryption, signature, and verification services to host applications. The module wraps the OpenSSL 3.5.6 FIPS provider for all approved cryptographic primitives. It does not implement any primitive in its own code ; all FIPS-relevant computations are delegated to the FIPS-validated OpenSSL FIPS provider (CMVP cert #4282 or successor).

2.2 Cryptographic Boundary

The cryptographic boundary is the entire address range of the libfreehsm.so executable code, plus the embedded read-only .fhsm_digest section (48 octets of SHA-384). The boundary explicitly excludes :

2.3 Module Interfaces (§7.3)

Logical Interface Physical Realization
Data Input pData, pInput, pTemplate parameters of C_* entry points
Data Output pSignature, pCiphertext, pPlaintext return buffers
Control Input pMechanism, flags, attribute templates
Control Output CK_RV return codes mapped to PKCS#11 CKR_*
Status Output C_GetInfo, C_GetTokenInfo, fhsm_kat_results, audit log entries
Power Input N/A (software module ; host-supplied)

All interfaces use the OASIS PKCS#11 v3.2 calling convention. The CK_FUNCTION_LIST (v2.40) and CK_FUNCTION_LIST_3_0 are the only externally visible symbol tables.


3. Approved Cryptographic Functions (§7.4)

Category Function Standard CAVP Cert (TBD by lab)
AES AES-128/192/256 CBC, CBC-PAD, CTR, GCM, KW (RFC 3394), KWP (RFC 5649), CMAC, GMAC FIPS 197, SP 800-38A, SP 800-38D §6.4 (GMAC), SP 800-38F, SP 800-38B A-AES-####
Hash SHA-256, SHA-384, SHA-512 FIPS 180-4 A-SHS-####
HMAC HMAC-SHA-256/384/512 FIPS 198-1 A-HMAC-####
PBKDF2 PBKDF2-HMAC-SHA-256 (≥ 200 000 iter, ≥ 14-byte password, ≥ 16-byte salt) SP 800-132 A-PBKDF-####
DRBG CTR_DRBG-AES-256, no derivation function SP 800-90A Rev. 1 A-DRBG-####
RSA RSA-2048/3072/4096 key generation, PKCS#1 v1.5 sign, PSS sign, OAEP encrypt/decrypt FIPS 186-5 (sig), SP 800-56B (key transport) A-RSA-####
ECDSA P-256, P-384, P-521 key generation + sign/verify with SHA-256/384/512 FIPS 186-5 A-ECDSA-####
ECDH P-256, P-384, P-521 (CKM_ECDH1_DERIVE, CKM_ECDH1_COFACTOR_DERIVE ; cofactor = 1) SP 800-56A Rev. 3 A-KAS-####
ML-KEM ML-KEM-512/768/1024 key generation, encapsulate, decapsulate FIPS 203 (Aug 2024) A-MLKEM-####
ML-DSA ML-DSA-44/65/87 key generation, sign, verify FIPS 204 (Aug 2024) A-MLDSA-####
SLH-DSA SLH-DSA-SHA2-128s/128f/192s/192f/256s/256f key generation, sign, verify FIPS 205 (Aug 2024) A-SLHDSA-####

Non-Approved (Rejected in FIPS Mode)

The module advertises these mechanisms in C_GetMechanismList for compatibility, but C_*Init will return CKR_MECHANISM_INVALID when the FIPS provider is loaded :


4. Software/Firmware Security (§7.5)

4.1 Source Code Hash

The build is reproducible per dist-verify :

4.2 Integrity Mechanism

The module embeds its own SHA-384 in a read-only ELF section .fhsm_digest (48 octets), patched by make integrity using OpenSSL FIPS provider. At every C_Initialize, fhsm_integrity_verify() recomputes the SHA-384 over the whole file with the digest area zeroed — not the .text section alone — and compares it in constant time. On mismatch, the module enters ERROR state and refuses all service requests.

4.3 Authorization

Two roles per token :

PINs are bound to a per-role PBKDF2 derivation. Failed login attempts increment a counter ; after 5 failures the role is locked. Throttle is exponential (500 ms → 60 s).


5. Operating Environment (§7.6)

5.1 Tested Configuration

5.2 Restrictions

The user must :

  1. Set fips_strict=true in /opt/freehsm/etc/freehsm.conf
  2. Configure OpenSSL openssl.cnf to load only the fips and base providers (no default)
  3. Run with non-zero .fhsm_digest (signed module) — FHSM_INTEGRITY_ALLOW_UNSIGNED MUST be unset

6. Physical Security (§7.7)

Not applicable. Software-only module, no physical embodiment to protect.

If the host hardware (HSM appliance) is provided, the physical security testing must cover that hardware separately under FIPS 140-3 §7.7 IG 7.7.


7. Non-Invasive Security (§7.8)

The module relies on the OpenSSL FIPS provider’s hardening for side-channel attacks (constant-time AES, masked DRBG, etc.). FreeHSM C’s own code is audited for :

Documented residual side-channels are in docs/SIDE_CHANNEL.md.


8. Sensitive Security Parameter Management (§7.9)

SSP Type Persistent Wrap Role Required
DEK (Token Data Encryption Key) AES-256 GCM key Yes (file) PBKDF2-wrapped under each role’s PIN USER login required to use
SO Wrap KEK AES-256, ephemeral No Derived from SO PIN by PBKDF2 SO login required to derive
USER Wrap KEK AES-256, ephemeral No Derived from USER PIN by PBKDF2 USER login required to derive
Audit MAC Key HMAC-SHA-256, 256-bit Yes (separate file) Plaintext (root-only access) Audit-write only
Module Integrity Digest SHA-384 (48 octets) Yes (in .fhsm_digest) None (read-only ELF section) None ; checked at boot
Generated RSA / ECDSA / ML-DSA / SLH-DSA Private Keys Per-algorithm Yes (via objects_blob, AES-GCM(DEK)) Wrapped under DEK USER login required

SSP Transitions


9. Self-Tests (§7.10)

9.1 Pre-Operational Self-Test (§7.10.2)

Runs once at C_Initialize :

  1. Software integrity check : SHA-384 of the whole file, digest area zeroed, vs .fhsm_digest.
  2. FIPS provider load : OSSL_PROVIDER_load("fips") non-NULL.
  3. CSP zeroization : fhsm_secure_heap_init allocates the secure arena and locks pages with mlock.

9.1.1 v1.2.1 fix : enforcement of the software integrity check

A defect introduced in commit 0c0f5df (the initial open-source release v1.1.0, 2026-06-12) caused src/fhsm_integrity.c::do_verify to return FHSM_RV_OK on every reachable path of the comparison block, including when the embedded digest mismatched the computed digest and the development bypass env var FHSM_INTEGRITY_ALLOW_UNSIGNED was not set. The intent (per the surrounding comment and per the operator documentation) was to return FHSM_RV_INTEGRITY_FAILED in that case ; the implementation fell through to a final return FHSM_RV_OK regardless.

The net effect was that the software integrity self-test described above was not actually enforced in any signed production build between v1.1.0 and v1.2.0 inclusive (19 GPG-signed releases over a 9-day window). A tampered libfreehsm.so would have passed the check silently. The defect did not affect the boot KAT (which has its own enforcement path) nor any cryptographic primitive ; it affected only the §7.10.2 software / firmware integrity self-test itself.

Fixed in v1.2.1 (commit 63e1b35, 2026-06-21). The comparison block now returns FHSM_RV_INTEGRITY_FAILED in both the all-zero (unsigned) and the mismatched-digest paths when the bypass env var is unset. Two adjacent defects in the same translation unit (a use-after-free on the find_section_offset failure path, and a locate_self regression for binaries that statically link fhsm_integrity.o) were fixed in the same commit ; see CHANGELOG entry for v1.2.1 for the full breakdown.

A killer test validates the fix : after re-signing the binary with the v1.2.1 build, the tests/test_smoke harness loads the signed binary correctly and the tests/test_smoke.tampered variant (1 byte flipped in .text) is rejected with C_Initialize returning 0x80000002 = FHSM_RV_INTEGRITY_FAILED. The same procedure on a v1.2.0 binary returns OK silently, confirming the defect’s behaviour on the affected window.

Discovery was triggered by an investigation of a dev-environment integrity quirk on the maintainer’s VM ; the visibility of the underlying defect was enabled by the v1.2.0 ALC_DVS-grade decomposition of C_CreateObject (see §13.5 and §13.8), which sharpened the focus on the integrity surface and made the dev-env quirk worth chasing. The project holds no certificate, so no CVE is requested ; full disclosure is in CHANGELOG, this Security Target, and the GitHub Security Advisory in informational mode.

9.1.2 v1.2.2 fix : raw CKM_ECDSA / CKM_RSA_PKCS sign+verify

A second critical defect was disclosed externally by Denis Mingulov (via the pkcs11-check testing harness, 2026-06-26) and fixed in v1.2.2. The defect was in src/fhsm_pkcs11.c::sign_asymmetric and verify_asymmetric : the raw mechanisms CKM_ECDSA (bare) and CKM_RSA_PKCS (bare) routed through EVP_DigestSignInit_ex(ctx, &pctx, mdname = NULL, ...), expecting OpenSSL to treat the input as a pre-computed digest. 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) before signing : the module silently signed SHA-256(input) instead of input.

The module’s own C_Verify path applied the same default digest symmetrically, so module-internal sign + verify cycles were internally consistent (both apply the same wrong hash, which cancels in the round trip). The Wycheproof CI corpus exercises verification only against pre-computed signatures and never exercised the module’s sign path externally. The defect was therefore invisible to internal testing.

Any third-party verifier expecting raw ECDSA on the supplied digest (the standard contract for CKM_ECDSA) rejected every signature produced by the affected releases. The cryptographic surface of FreeHSM C was effectively non-interoperable for ECDSA signing across all 19 GPG-signed releases between v1.1.0 and v1.2.1.

Fixed in v1.2.2 (commits 06af928 for the sign / verify branch separation and 28071e9 for the boot-KAT regression guard, both 2026-06-27). Raw mode now routes through EVP_PKEY_sign / EVP_PKEY_verify on a freshly-allocated EVP_PKEY_CTX — the canonical OpenSSL 3.x API for signing a pre-computed digest. Hashed mechanisms ( CKM_ECDSA_SHAxxx, CKM_SHAxxx_RSA_PKCS ) continue through the existing EVP_DigestSign / EVP_DigestVerify path unchanged.

The fix is paired with three new boot KAT vectors (ECDSA-P256/P384/P521-export-roundtrip) that enforce the methodology the fix is built on : sign a digest via EVP_PKEY_sign, serialize the public key via i2d_PUBKEY, reload via d2i_PUBKEY (mimicking exactly what an external verifier does), and verify via EVP_PKEY_verify. If a future OpenSSL provider upgrade re-introduces the silent-default-digest behaviour for EVP_PKEY_sign, or if i2d_PUBKEY / d2i_PUBKEY ever stop producing byte-stable round-trips for EC keys, these boot KATs fail at C_Initialize and the module refuses to start. Total boot KAT count grows from 51 (v1.2.1) to 54 (v1.2.2).

Severity (CVSS v3.1) : AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H base 7.5, HIGH on correctness / interoperability. Not exploitable in confidentiality or integrity sense : the private key is never leaked, the signature is mathematically valid for the (wrong) double-hashed digest, no key material is exposed.

Discovery was triggered externally by a third-party reporter (Denis Mingulov) using pkcs11-check ; the report was initially blocked by the maintainer’s published GPG key being sign-only with no encryption capability. The operational gap was fixed in v1.2.2 by adding a cv25519 encryption subkey to the maintainer’s primary key, publishing to keys.openpgp.org with the maintainer’s email verified, and updating SECURITY.md. The discovery + correction protocol in §13.8 is extended for the v1.2.2 release to incorporate the external-reporter case as a recognised path. No CVE is requested ; the disclosure model matches the v1.2.1 informational GHSA pattern.

9.1.3 v1.3.0 enhancements (2026-06-27) — functionality-additive only

Functionality-additive only release. No defect resolved ; closes the two narrative threads opened by v1.2.2 :

Function-list completion. The five PKCS#11 dispatch slots flagged by Denis Mingulov’s v1.2.2 report as “exported but unwired” are now fully resolved : three were wired in v1.2.2 (C_GetSessionInfo, C_CreateObject, C_GetObjectSize), the remaining two (C_CopyObject §C.6.7.3, C_SetAttributeValue §C.6.7.5) are implemented and wired in v1.3.0, plus a sixth (C_WaitForSlotEvent §C.6.5.4) closing the last unwired slot in the PKCS#11 v2.40 function list. Total v2.40 dispatch coverage : 51/67 (was 47/67 in v1.2.2, 44/67 in v1.2.1 pre-Denis).

Export-roundtrip boot KAT extension. The methodology codified in v1.2.2 for ECDSA (sign with original, serialize+reload public key via i2d_PUBKEY/d2i_PUBKEY, verify with reloaded key, byte-compare) is extended to six more cryptographic surfaces : RSA-2048-PSS-SHA256, RSA-2048-OAEP-SHA256, Ed25519, ML-DSA-65, ML-KEM-768, ECDH (P-256 / P-384 / P-521). Total boot KAT count grows from 54 (v1.2.2) to 62 (v1.3.0). Coverage of external-roundtrip surfaces : 6/7. SLH-DSA is excluded by design : a full sign+verify-with-reloaded-pubkey would approximately double the existing ~33ms SLH-DSA-SHA2-128f selftest cost ; the exclusion is documented as a known gap, addressable when the boot KAT runtime budget allows.

No change to the security functions inventory (FCS_COP, FCS_CKM, FCS_RNG, FIA_, FAU_, FDP_, FMT_, FPT_, FTP_). Under CC §APE_REQ.2 unchanged-SFR rule, CMVP/CC re-validation is considered a clarification, not a re-evaluation. Token-store on-disk format unchanged ; existing v1.2.2 token files load unchanged.

Denis Mingulov’s two remaining findings (memory-safety + key-handling) are pending via the encrypted channel and will be addressed in a v1.3.1 or v1.4.0 release as appropriate.

9.1.4 v1.4.0 enhancements (2026-06-28) — v2.40 dispatch near-completion

Functionality-additive only release. No defect resolved ; closes most of the remaining gap between FreeHSM C and a strict OASIS PKCS#11 v2.40 implementation. Six additional dispatch slots wired :

C_CancelFunction is implemented + exported as an ELF symbol but cannot be wired in the v2.40 dispatch due to the 67-slot table collision with slot 66 (C_WaitForSlotEvent, wired in v1.3.0). Documented as a roadmap item for v1.5.x.

Total v2.40 dispatch coverage : 57 / 67 (85 %). The 10 remaining unwired slots are intentional design decisions :

No change to the security functions inventory (FCS_COP, FCS_CKM, FCS_RNG, FIA_, FAU_, FDP_, FMT_, FPT_, FTP_). Under CC §APE_REQ.2 unchanged-SFR rule, CMVP/CC re-validation is considered a clarification, not a re-evaluation. Token-store on-disk format unchanged ; existing v1.3.0 token files load unchanged.

Denis Mingulov’s two remaining findings (memory-safety + key-handling) are still pending via the encrypted channel and will be addressed in a future v1.4.x or v1.5.0 release as appropriate.

9.2 Conditional Self-Tests (§7.10.3)

9.3 Known Answer Tests (KAT)

51 cryptographic self-tests run on first C_Initialize, covering every FIPS 140-3 §C.B mandatory algorithm category, including all three NIST post-quantum primitives (ML-KEM, ML-DSA, SLH-DSA) and AES-GMAC (added in v1.1.18 with a novel two-path self-consistency design, see §9.4). Each vector cites a public standards-track document so any independent reviewer can re-derive the expected value.

Family Algorithm Vector ID Source
Symmetric encryption AES-256-GCM SP800-38D-TC14 NIST SP 800-38D Annex B
  AES-256-GCM (AAD) SP800-38D-TC15 NIST SP 800-38D Annex B
  AES-256-CBC SP800-38A-F.2.5 NIST SP 800-38A Annex F.2.5
  AES-256-CTR SP800-38A-F.5.5 NIST SP 800-38A Annex F.5.5
MAC HMAC-SHA-256 RFC 4231 TC1, TC2, TC3, TC6 IETF RFC 4231
  AES-256-CMAC SP800-38B-D.3 Ex 7, 8, 9 NIST SP 800-38B Annex D.3
  AES-256-GMAC SP800-38D-self-consistency NIST SP 800-38D §6.4 + two-path equivalence (§9.4)
Hash (SHA-2) SHA-256 ”” + “abc” FIPS 180-4 Annex B.1
  SHA-384 ”” + “abc” FIPS 180-4 Annex D.1
  SHA-512 ”” + “abc” FIPS 180-4 Annex C.1
Hash (SHA-3) SHA3-256 ”” + “abc” FIPS 202 Annex A.1
  SHA3-384 ”” + “abc” FIPS 202 Annex A.2
  SHA3-512 ”” + “abc” FIPS 202 Annex A.3
Signature ECDSA-P256-SHA256 RFC 6979 §A.2.5 IETF RFC 6979
  ECDSA-P384-SHA384 RFC 6979 §A.2.6 IETF RFC 6979
  ECDSA-P521-SHA512 RFC 6979 §A.2.7 IETF RFC 6979
  RSA-2048-PSS-SHA256 sign-verify round-trip NIST SP 800-89 §7 + FIPS 140-3 IG D.3
Asym encryption RSA-2048-OAEP-SHA256 encrypt-decrypt round-trip NIST SP 800-89 §7 + FIPS 140-3 IG D.3
KDF HKDF-SHA-256 RFC 5869 §A.1, A.2 IETF RFC 5869
  PBKDF2-HMAC-SHA-1 RFC 6070 §2 TC1, TC2 IETF RFC 6070
DRBG CTR_DRBG-AES-256 stuck-DRBG Internal continuous test (SP 800-90A)
PQ KEM ML-KEM-768 encaps-decaps round-trip FIPS 203 + FIPS 140-3 IG D.3
PQ signature (lattice) ML-DSA-65 sign-verify round-trip FIPS 204 + FIPS 140-3 IG D.3
PQ signature (hash-based) SLH-DSA-SHA2-128f sign-verify round-trip FIPS 205 + FIPS 140-3 IG D.3

Failure of any single KAT latches the module into the FIPS 140-3 §7.10.2 ERROR state and returns FHSM_RV_KAT_FAILED = 0x80000001. No cryptographic operation can complete after a latched failure ; the module must be reloaded by re-invoking C_Initialize on a corrected build.

Boot-time KAT execution measured at ~155 ms on the Debian 13 reference platform (Section 5.1), of which ~100 ms is the RSA-2048 ephemeral keypair generation used by the PSS and OAEP round-trip self-tests, ~20 ms for the three post-quantum keygen + round-trip self-tests combined (ML-KEM-768, ML-DSA-65, SLH-DSA-SHA2-128f), and ~8 µs for the new AES-GMAC self-consistency test. The “fast” variant of SLH-DSA (128f) was chosen over the “small signature” (128s) variant explicitly to bound the boot-time cost ; production code paths exercise both variants through the C_Sign / C_Verify dispatch at runtime.

9.4 Trade-offs documented


10. Lifecycle Assurance (§7.11)

Item Status
Vendor-supplied configuration management Makefile, Dockerfile.build, dist/refs/*.sha256
Delivery and operation docs/AGD_PRE.md + docs/AGD_PRE.fr.md
Lifecycle/development docs/ARCHITECTURE.md, source code, git history
Guidance documentation docs/AGD_OPE.md (administrator + user guide)
Reproducible build make repro + make dist-verify against dist/refs/v1.1.0.sha256

11. Operational Modes

Mode Trigger Behavior
Approved (FIPS) fips_strict=true in conf + OpenSSL FIPS provider loaded + signed .fhsm_digest All non-FIPS mechanisms return CKR_MECHANISM_INVALID
Error KAT failure, integrity mismatch, RNG failure All C_* return FHSM_RV_FUNCTION_FAILED. Only C_Finalize permitted to clean up.

The module has no “non-approved” mode in shipping builds. Development builds may set FHSM_INTEGRITY_ALLOW_UNSIGNED to skip integrity (developer only ; flagged as non-validated build).


12. Mitigation of Other Attacks (§7.12)

Out of scope at Security Level 1 ; it is a Level 2 requirement (AVA_VAN.5).

Documented future work :


13. External Validation (Continuous, Non-FIPS Evidence)

FIPS 140-3 §7.10.2 governs the boot KAT (Section 9.3). FreeHSM C additionally maintains a continuous external validation surface that runs on every push to main and on every signed release tag. This surface is not part of the validation scope per se, but provides standing evidence for the lab and end-users that the module produces bit-for-bit correct outputs against an independent reference implementation.

13.1 Google Wycheproof corpus

The release tier validates the module against the open Google Wycheproof crypto test corpus, pinned at the immutable commit SHA 6d7cccd0fcb1917368579adeeac10fe802f1b521 (recorded in tests/wycheproof/VECTORS_SHA for reproducibility). The full sweep covers nine PKCS#11 v3.2 algorithm families with 6 978 vectors at zero violations as of v1.1.12 :

Family Match Violation Skip Coverage
ECDSA (P-256/384/521 verify, raw r||s) 3 098 0 18 794 Wycheproof DER + P1363 ; P1363 deferred via --schema filter
EdDSA (Ed25519, Ed448) 236 0 0 Complete
RSA-PSS (SHA-256/384/512, 2 048+) 1 083 0 1 323 Skips = parameter combinations rejected by OpenSSL 3.5 default provider hardening
RSA-OAEP (SHA-1/256/384/512) 788 0 420 Skips = “Constructed” EDGE_CASE rejected by OpenSSL malleability defence + unsupported MGF
AES-GCM (128/192/256) 310 0 6 Skips = ivBits > 512 (above OpenSSL limit)
AES-CMAC (128/192/256) 306 0 5 Skips = non-standard key lengths
HMAC (SHA-256/384/512) 522 0 0 Complete
ML-KEM (FIPS 203, post-quantum) 21 0 0 All three NIST parameter sets (512/768/1024) ; semi-expanded decaps corpus
ML-DSA (FIPS 204, post-quantum) 614 0 15 All three NIST parameter sets (44/65/87) ; 15 skips = ctx length > 255 (FIPS 204 §5.2.1 spec violation, unreachable through CK_ML_DSA_PARAMS)
TOTAL 6 978 0 20 583 9 families

Both of the module’s NIST post-quantum primitives, ML-KEM (FIPS 203) and ML-DSA (FIPS 204), are cross-validated against an external, independent corpus: the Wycheproof rows above. AES-GMAC is part of the full sweep as well: the runner discovers every adapter under tests/wycheproof/adapters/, and tests/wycheproof/results/full.json records aes_gmac beside the other families.

Until 2026-10-02 this paragraph called the module “one of very few” open-source PKCS#11 v3.2 modules with this coverage, which is a comparison with other projects rather than a statement about this one; docs/PRIMACY_AUDIT_PQC_COMPOSITE.md is why it was removed. It also said the GMAC corpus would be enabled in a later release; it already had been.

13.2 Coverage matrix self-test

A second tier runs on every push, inside a separate pinned Debian 13 container (ghcr.io/<owner>/freehsm-c-test:debian13-pkcs11-tools), exercising 32 PKCS#11 v3.2 function × mechanism × error path assertions through OpenSC’s pkcs11-tool against the freshly built .so. As of v1.1.18 the matrix produced 24 PASS / 0 FAIL / 8 documented SKIP in both the permissive and the strict runtime mode :

test-coverage-matrix         PASS = 24   FAIL = 0   SKIP = 8
test-fips-mode               PASS = 24   FAIL = 0   SKIP = 8    (now test-strict-mode)

What these two jobs do not show. Both run with the integrity self-test bypassed and OPENSSL_CONF=/dev/null, so the OpenSSL FIPS provider is never loaded: every EVP fetch is served by the default provider. “Strict” is the runtime policy — non-approved mechanisms refused at call time — not the provider. Until 2026-09-30 the second job was named test-fips-mode and this section called it a “FIPS-strict” run, which invited the reading that the provider had been exercised. It had not been, in any CI job.

The run that does load it is test-fips-provider (since 2026-09-30): the signed module, no bypass, KATs enforced, Debian’s openssl-provider-fips activated in the system configuration, and scripts/run_fips_tests.sh, which counts nothing until tests/probe_fips_loaded has shown the provider is in the process. The pkcs11-check harness also runs against the signed module with the provider loaded, in both build profiles (pkcs11-check-signed).

The 8 skips are all due to tooling gaps in the harness layer (e.g., OpenSC’s pkcs11-tool does not propose SHA-3 mechanisms, MD5 is intentionally absent from g_mech_list per FIPS 140-3 §C.A removal), not module behaviour.

Note on the ECDH derive step. Between v1.1.15 and v1.1.18 the matrix’s ECDH derive assertion (C_DeriveKey / CKM_ECDH1_DERIVE) was reported as intermittently failing in CI (about one failure per five runs on the same commit). Investigation in v1.1.18 traced the flakiness to three environmental causes in the harness, not to a bug in the module’s C_DeriveKey implementation : (i) stale 0-byte content in /tmp/cov-z.bin carried over from a previous matrix run on the same CI runner ; (ii) the script ignored pkcs11-tool’s exit code and inferred success only from the output file size ; (iii) the CI container occasionally swapped under load, delaying the write completion past the [ -s file ] check. The matrix script was hardened to clean the output file before each derive, check both exit code and file size, and retry once on failure. The module’s C_DeriveKey code is unchanged.

13.3 Reproducible build

Every release tag triggers a deterministic build inside a pinned Docker image (ghcr.io/<owner>/freehsm-c-build:debian13-20261006). The resulting libfreehsm.so is bit-identical (same SHA-256) across independent builds, verified continuously by the reproducibility CI job (cross-build + compare). A GPG-signed source + binary tarball is published per tag.

13.4 Public attestation summary

Attestation Tier Status as of v1.1.18
51 boot KAT vectors (Section 9.3) Per-invocation Mandatory per FIPS 140-3 §7.10.2
6 978 Wycheproof vectors, 9 families Per push to main 100 % match, 0 violation
32 matrix assertions, permissive + strict runtime modes (no FIPS provider, §13.2) Per push to main 24/0/8
37.9 million fuzz inputs across 3 harnesses Per push (5 min) / nightly (1 h) 0 crash, 0 leak, 12 invariants checked (§13.5)
Reproducible build (sha256 bit-identical) Per release tag Verified by dist-verify
GPG signed releases (Ed25519 fingerprint 743A 6A59 04A1 461A 6464 08DE 4856 0162 DBBF 28A2) Per release tag 18 consecutive releases since v1.1.0 (§13.7)

This surface is the operational complement to the boot KAT : it answers the questions “does the module match a third-party reference ?”, “is the binary the one I think it is ?”, and (from §13.5) “is the module memory-safe under adversarial input ?” that no §7.10.2 self-test can answer in isolation.

13.5 Structured fuzzing (libFuzzer + ASAN + UBSAN)

Introduced in v1.1.14 and expanded in v1.2.0. Four sanitizer-instrumented (-fsanitize=fuzzer,address,undefined) libFuzzer harnesses cover the PKCS#11 v3.2 parser surfaces that receive untrusted input from a calling application :

Harness Target Surface Coverage tier
fuzz_ecdsa_raw fhsm_ecdsa_der_to_raw, fhsm_ecdsa_raw_to_der DER ECDSA-Sig-Value ↔ raw r‖s wire format (PKCS#11 v3.2 §6.13) Integration (production TU)
fuzz_pq_params fhsm_parse_pq_params CK_ML_DSA_PARAMS / CK_SLH_DSA_PARAMS 24-byte struct decoder (PKCS#11 v3.2 §6.18/§6.19) Integration (production TU)
fuzz_attr_template fhsm_find_attr, fhsm_strip_octet_string_inline CK_ATTRIBUTE[] template lookup + DER OCTET STRING wrapper stripper Unit (primitives in include/fhsm_attr_utils.h)
fuzz_create_attrs fhsm_parse_create_attrs C_CreateObject template parser : (CKO, CKK) dispatch into VERBATIM / EC pub / Ed25519 pub / Ed448 pub / RSA pub paths (PKCS#11 v3.2 §5.6.4) Integration (production TU, v1.2.0+)

Each harness checks memory safety (sanitizer-flagged out-of-bounds reads, undefined behavior, leaks) plus structural invariants that must hold for every input. The 23 invariants currently checked :

  1. ECDSA round-trip closure : der_to_raw(raw_to_der(r‖s)) == r‖s.
  2. ECDSA length-vs-buffer accounting on truncated DER.
  3. PQ params no-context invariant : *out_have == 0 ⇒ *out_ctx_len == 0.
  4. PQ params bounds clamp : *out_ctx_len ≤ out_ctx_cap.
  5. PQ params hedge-variant pass-through.
  6. PQ params NULL rejection.
  7. PQ params short-input rejection.
  8. find_attr index range : return value ∈ {-1, 0, …, count-1}.
  9. find_attr type-match on found index.
  10. find_attr empty-template returns -1 for any type.
  11. OCTET STRING accept-on-pointer-in-range : data ≤ out ≤ data + size.
  12. OCTET STRING length accounting : (out - data) + out_len == size.
  13. fhsm_parse_create_attrs path enum in range [INVALID..RSA_PUB].
  14. fhsm_parse_create_attrs returns OK ⇒ path ≠ INVALID (dispatch decision was made).
  15. fhsm_parse_create_attrs label NUL-terminated within sizeof(label).
  16. fhsm_parse_create_attrs id pointer/length consistency : id_data == NULL ⇒ id_len == 0.
  17. fhsm_parse_create_attrs class is supported : cko ∈ {PUBLIC, PRIVATE, SECRET}.
  18. fhsm_parse_create_attrs VERBATIM path carries a non-NULL CKA_VALUE.
  19. fhsm_parse_create_attrs EC public path carries one of three curve strings + non-NULL point + cko == PUBLIC.
  20. fhsm_parse_create_attrs Ed25519 / Ed448 paths carry ec_group == NULL (curve in path enum) + non-NULL point + cko == PUBLIC.
  21. fhsm_parse_create_attrs RSA public path carries both modulus and exponent (non-NULL, non-zero length) + cko == PUBLIC.
  22. fhsm_parse_create_attrs returns error ⇒ path == INVALID (parser memset()s output at entry).
  23. fhsm_parse_create_attrs truncation probe : re-parse after shrinking the last record’s ulValueLen by one byte must not crash or violate invariants 13–22.

ALC_DVS rationale for the v1.2.0 expansion. The v1.1.x design of fuzz_attr_template exercised an inline mirror of fhsm_find_attr and fhsm_strip_octet_string_inline declared in include/fhsm_attr_utils.h, but the surrounding dispatch logic in C_CreateObject (the (CKO, CKK) → path enum mapping, curve OID lookup, Ed25519/Ed448 disambiguation, RSA modulus/exponent extraction) was embedded in src/fhsm_pkcs11.c interleaved with OpenSSL EVP construction, so the harness could only exercise a mirror of that logic in a separate harness body. Mirrors are an ALC_DVS hazard : the mirror code can drift from the production code in subtle ways (a missed branch, a slightly different bounds check), at which point the fuzzer reports “no findings” against the mirror while the production code retains the same vulnerability. The v1.2.0 decomposition (src/fhsm_create_attrs.c) removes this hazard for the C_CreateObject parser surface : fuzz_create_attrs now links the production TU directly, so the bytes libFuzzer mutates exercise the same instructions that run when an untrusted PKCS#11 caller invokes C_CreateObject in production. The remaining two mirror surfaces (fhsm_find_attr, fhsm_strip_octet_string_inline) are still exercised by fuzz_attr_template as a unit-level guardrail and remain on the maintenance ledger in fuzz/README.md until the last non-C_CreateObject call sites are migrated to the production helpers.

Continuous validation tier. On every push to main, each harness runs for 5 minutes (fail-on-crash, upload reproducer as 90-day-retention artifact). Nightly at 02:00 UTC, each harness runs for 1 hour (upload evolving corpus + crashes). Developer smoke runs : the original three harnesses measured 37.9 million inputs validated in 90 s total in v1.1.14 ; the new fuzz_create_attrs harness measured 18.7 million inputs in 60 s on the same developer machine (306 k exec/s, 714 new units discovered, 0 crash / 0 ASAN report / 0 UBSAN report / 518 MB peak RSS), confirming the parser TU is hot-loop-stable under adversarial input. Zero violation of any of the 23 invariants has been observed since the harnesses were introduced.

Crash policy (ALC_DVS alignment). Every crash reported by the fuzz CI is treated as a security finding : triage in a private GitHub issue → confirm the bug is in the target helper → fix in src/fhsm_*.c + add a regression test → backport to supported release branches → CVE if the surface is reachable from an untrusted caller. The full procedure is in fuzz/README.md ; the file is maintained as evidence for the CC EAL4+ ALC_DVS expectation that every reported crash leads to a tracked corrective action.

Seed corpora for the original three harnesses are committed to git under fuzz/corpus/<harness>/ so CI runs start from the same baseline as developer machines. One regression seed (regression_harness_oob_1byte) was added in v1.1.14 from a 1-byte OOB found in the harness itself during initial validation — kept as a permanent guardrail. The fuzz_create_attrs corpus starts empty in v1.2.0 (libFuzzer bootstraps from zero, reaching coverage saturation within the first nightly run) ; interesting inputs discovered by the nightly run will be promoted into the committed seed corpus as a rolling baseline.

13.6 Cross-validation methodology (KAT integrity)

Established as a methodology between v1.1.14 and v1.1.18. Four KAT bugs that had been silently passing under FHSM_KAT_ALLOW_FAIL=1 in CI were uncovered and corrected by applying a uniform protocol :

  1. Cross-validation across three independent codebases. For each disputed KAT, the canonical value is recomputed via :
    • OpenSSL EVP through the C source under test ;
    • Python cryptography (cffi binding to OpenSSL — same libcrypto, different binding layer) ;
    • a third codebase independent of OpenSSL (pycryptodome’s autonomous AES-GCM, python-ecdsa with RFC 6979 deterministic signing, etc.).
  2. Verdict from the agreement of the three. If all three implementations agree on a value that differs from what the KAT data claimed, the KAT data is replaced with the agreed value. If only two agree, the divergent third is investigated upstream (potential implementation bug).
  3. Reproducible cross-validation scripts are kept under disabled/verify_<vector>.py so the audit trail is replayable indefinitely.

This methodology resolved four bugs in the v1.1.13 → v1.1.18 cascade — see Section 9.4 for the cumulative trade-offs and disabled/verify_aes_gcm_tc14.py, disabled/verify_ecdsa_p521_rfc6979.py for two worked examples. The methodology generalises to any future KAT addition and is documented as a developer expectation in CONTRIBUTING.md.

The self-consistency design used for AES-GMAC (§9.4) is a variant of the same idea applied in-process : two OpenSSL code paths that should be mathematically equivalent are compared at boot, eliminating the need for a hardcoded expected value entirely.

13.7 Release track record (ALC_DEL / ALC_CMC)

23 consecutive GPG-signed releases since v1.1.0, every one Ed25519-signed by key 743A 6A59 04A1 461A 6464 08DE 4856 0162 DBBF 28A2. Each release tag triggers an automated workflow (release.yml) that :

  1. Verifies the tag’s GPG signature against the canonical fingerprint.
  2. Builds libfreehsm.so reproducibly in the pinned freehsm-c-build:debian13-20261006 container.
  3. Patches the .fhsm_digest section with the post-link SHA-384 of the binary.
  4. Builds both source and binary tarballs (freehsm-c-X.Y.Z-src.tar.xz and -bin.tar.xz).
  5. Detached-signs both tarballs with the release GPG key.
  6. Publishes a GitHub Release with the signed assets attached.

The mirror workflow (mirror.yml) cross-publishes the same tag and assets to GitLab (gitlab.com/afchine.mad/freehsm) and Codeberg (codeberg.org/afchine1337/freehsm) within seconds. This provides three independent hosts of every signed artifact, mitigating single-point-of-failure risk on the supply chain (ALC_DEL coverage).

The unbroken signing chain across 23 releases — including the v1.1.13 → v1.1.18 patch cascade that fixed four latent KAT data bugs, the v1.2.0 structural decomposition of C_CreateObject, the v1.2.1 security patch on the integrity self-test, the v1.2.2 external-reporter-driven raw ECDSA fix plus boot-KAT extension, the v1.3.0 function-list completion + export-roundtrip extension, and the v1.4.0 v2.40 dispatch near-completion (85 % coverage) — is itself ALC_CMC evidence : every change that reached a shipping release passed through the signed-release pipeline, and the inventory of changes is verifiable in the CHANGELOG.md ledger plus the git commit history (each tagged commit is itself GPG-signed by the same fingerprint).

13.8 Discovery + correction protocol (ALC_DVS / ALC_FLR)

The v1.2.1 release codifies a working protocol for the discovery and correction of latent defects in the cryptographic module’s own evidence-bearing surface. The protocol generalises the practice established between v1.1.13 and v1.1.18 for KAT data and extends it to the §7.10.2 self-test path.

The five-step protocol :

  1. Symptom triage in dev : An apparent dev-environment quirk is investigated as a potential surface symptom of a latent defect, not dismissed as ergonomic friction. The maintainer’s standing rule is that any boot-time failure that disappears under FHSM_INTEGRITY_ALLOW_UNSIGNED deserves a one-hour minimum investigation before being filed as a side issue.

  2. Read both sides : For any disagreement between two pieces of code that should agree (sign/verify, mirror/prod, compute/compare), both sides are read in full before any patch is written. The reading explicitly looks for : the source-of-truth definition, return-code paths, fall-through cases, and silently-bypassed branches.

  3. Cross-check the contract : The expected contract is reconstructed independently of the existing implementation (from the standards documents, the operator-facing comments, the audit log). The reconstructed contract is compared against what the implementation actually does. Drift is identified as either harness-too-strict or production-too-permissive ; both cases are corrected at the boundary that owns the contract.

  4. Killer-test artifact : For every defect that has the structure “a malicious input would have been accepted”, a reproducible artifact is produced that demonstrates the difference between the buggy and the fixed behaviour. For v1.2.1, the artifact is tests/test_smoke.tampered (1 byte flipped in .text, runs OK against pre-v1.2.1, returns 0x80000002 against v1.2.1).

  5. Scope the temporal impact via git blame : The commit that introduced the defect is identified. The set of affected releases is enumerated. The disclosure decision (CVE vs informational advisory) is made based on the absence of any certificate, the deployment landscape (none known), and the project’s transparency-first hygiene.

This protocol applies to any future evidence-bearing surface defect : the KAT runner (covered by the v1.1.13 → v1.1.18 cross-validation methodology, §13.6), the integrity self-test (covered by the v1.2.1 fix), the structured fuzzing harnesses (covered by the v1.2.0 ALC_DVS-grade decomposition, §13.5), the audit log (not yet a surface that has produced a defect, but eligible). The protocol is documented as a developer expectation in SECURITY.md and CONTRIBUTING.md.

13.8.1 External-reporter extension (added v1.2.2)

The five-step protocol was extended in v1.2.2 to recognise the external-reporter case — the first time the project received a security report from a third party (Denis Mingulov, pkcs11-check, 2026-06-26). The extension adds two operational expectations on the maintainer side :

a. Encrypted-channel availability is a precondition. Any third-party reporter must be able to reach the maintainer over an encrypted channel before the technical triage begins. The v1.2.2 disclosure exposed that the maintainer’s published GPG key was sign + certify + authenticate only, with no encryption capability ; the reporter sent the headline finding in clear (because it was already non-exploitable) and held the memory-safety / key-handling findings until an encryption-capable key was published. Operational fix : add a cv25519 encryption subkey to the existing primary, publish to keys.openpgp.org with the maintainer’s email verified, update SECURITY.md, document the gap honestly. The fix is logged in the v1.2.2 release commit history so future reporters can verify that the encryption capability has been continuously available from 2026-06-26 onward.

b. Reproduce before challenging. Take the reporter’s evidence seriously even when the report seems to contradict CI-green status. The v1.2.2 raw ECDSA fix took less than 90 minutes from git clone of the reporter’s reproduction script to a tested local fix, because the maintainer reproduced the report locally before opening the source code. Internal CI greenness is not a counter-argument to a reproducible external report ; CI tests what it tests, and the reporter typically tests something CI does not.

The five-step protocol of §13.8 generalises unchanged ; the extension is sub-procedural and applies before step 1 (the triage) begins. The pattern will be reinforced in future external-reporter cases as the project matures.


Document Revision History

Version Date Author Changes
0.1 2026-04 A.M. Initial outline
0.2 2026-05 A.M. §3 algorithm table draft
0.3 2026-06-10 A.M. First complete draft : §1-§12 all sections populated, ready for internal review before lab submission
0.4 2026-06-18 A.M. §9.3 expanded from 7 to 32 KAT vectors with explicit standards-track citations (NIST SP 800-38A/B/D, FIPS 180-4, FIPS 202, RFC 4231, RFC 5869, RFC 6070, RFC 6979, NIST SP 800-89). §9.4 added documenting the FIPS 140-3 IG D.3 trade-off for RSA consistency self-tests. §13 added documenting the external Wycheproof + matrix + reproducibility evidence surface (6 978 vectors clean across 9 families, including both NIST post-quantum primitives).
0.5 2026-06-20 A.M. §3 adds GMAC to the approved AES function list (NIST SP 800-38D §6.4). §9.3 KAT count 35 → 51 vectors with the addition of (a) ML-KEM-768 / ML-DSA-65 / SLH-DSA-SHA2-128f consistency self-tests (v1.1.13), (b) AES-256-GMAC two-path self-consistency self-test (v1.1.18). §9.4 trade-off bullet added documenting the AES-GMAC two-path self-consistency design pattern (EVP_MAC vs EVP_CIPHER cross-check, eliminating the need for a hardcoded expected tag). §13.5 NEW : Structured fuzzing — three sanitizer-instrumented libFuzzer harnesses covering the PKCS#11 v3.2 parser surfaces, with 12 structural invariants checked across 37.9 M smoke inputs, CI integration on every push + nightly, and a documented crash policy aligned with CC EAL4+ ALC_DVS expectations. §13.6 NEW : Cross-validation methodology — three-codebase triangulation protocol used between v1.1.14 and v1.1.18 to identify and correct four latent KAT data bugs that had been silently passing under FHSM_KAT_ALLOW_FAIL=1 in CI ; methodology documented as a developer expectation. §13.7 NEW : Release track record — 18 consecutive GPG-signed releases since v1.1.0 as ALC_DEL / ALC_CMC evidence, with three-mirror redundancy (GitHub + GitLab + Codeberg). §13.4 attestation summary updated from v1.1.12 to v1.1.18 figures.
0.6 2026-06-20 A.M. §1 Module identification : Version field 1.1.18-FIPS → 1.2.0-FIPS. §13.5 expanded from three harnesses to four with the addition of fuzz_create_attrs, attacking fhsm_parse_create_attrs — the pure parser extracted from the C_CreateObject monolith in v1.2.0 (src/fhsm_create_attrs.c). Invariant count grows 12 → 23 with the addition of P1–P10 + E1 + truncation probe specifying the structural contract on the parser’s typed output struct. New ALC_DVS rationale paragraph explaining why the v1.2.0 decomposition is a security-evidence-grade improvement : the production code is now linked directly into the libFuzzer binary, eliminating the mirror-vs-prod drift hazard on the C_CreateObject surface. Local smoke run numbers added : 18.7 M executions in 60 s on the new harness, 0 violation across all 23 invariants, 0 ASAN / 0 UBSAN report. §13.7 release counter 18 → 19 with the addition of v1.2.0 to the signed-release ledger. The two preceding rows are preserved verbatim as historical record.
0.7 2026-06-21 A.M. §1 Module identification : Version field 1.2.0-FIPS → 1.2.1-FIPS. §9.1.1 NEW : v1.2.1 security fix on the §7.10.2 software / firmware integrity self-test, documenting the defect that caused do_verify to silently return OK on every reachable comparison path in v1.1.0 through v1.2.0, and the v1.2.1 fix (commit 63e1b35) that returns FHSM_RV_INTEGRITY_FAILED on unsigned + no-env and on mismatch + no-env. Includes the killer-test artifact (tests/test_smoke.tampered, 1 byte flipped, rejected with 0x80000002 in v1.2.1, accepted silently in v1.2.0). §13.7 release counter 19 → 20 with the addition of v1.2.1 to the signed-release ledger and explicit mention of the integrity-fix patch cascade. §13.8 NEW : Discovery + correction protocol (ALC_DVS / ALC_FLR), codifying the five-step practice used to find and fix the v1.2.1 defect — symptom triage in dev, read both sides, cross-check the contract, killer-test artifact, scope the temporal impact. The protocol generalises the cross-validation methodology of §13.6 from KAT data to evidence-bearing surfaces in general. Disclosure decision documented : no CVE requested (pre-certification status, no known production deployments), informational GitHub Security Advisory published.
0.8 2026-06-27 A.M. §1 Module identification : Version field 1.2.1-FIPS → 1.2.2-FIPS. §9.1.2 NEW : v1.2.2 security fix on the raw CKM_ECDSA / CKM_RSA_PKCS sign+verify path, documenting the defect that caused the module to silently sign SHA-256(input) instead of input (EVP_DigestSignInit_ex with mdname=NULL on OpenSSL 3.x ECDSA digest_sign applies an internal default digest), affecting every signed release v1.1.0 through v1.2.1. The fix routes raw mode through EVP_PKEY_sign / EVP_PKEY_verify and is paired with three new boot KAT vectors (ECDSA-P256 / P384 / P521 export-roundtrip self-consistency, KAT count 51 → 54) that enforce the methodology as a regression guard. The fix and the boot KAT both ship in v1.2.2 commits 06af928 and 28071e9 respectively. §13.7 release counter 20 → 21 with the addition of v1.2.2 and explicit mention of the raw ECDSA fix plus boot-KAT extension. §13.8.1 NEW : External-reporter extension to the Discovery + correction protocol, recognising the v1.2.2 case as the first time the project received a security report from a third party (Denis Mingulov, pkcs11-check). Adds two operational expectations : encrypted-channel availability is a precondition (the v1.2.2 disclosure exposed that the maintainer’s published GPG key was sign-only, fixed by adding a cv25519 encryption subkey published with email verification on keys.openpgp.org), and reproduce before challenging (the v1.2.2 fix took less than 90 minutes from reproduction-script clone to tested local fix). Disclosure decision documented : no CVE requested, informational GitHub Security Advisory published, reporter credited as Denis Mingulov.