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

<!-- doc-audit: allow tests/test_smoke.tampered -- historical record of the
     v1.2.1 killer test; the artefact is now tests/test_integrity.tampered.
     See the note immediately below. -->
> **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-256 = (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 Sought** | **Level 1** (initial) ; Level 2 candidate after AVA_VAN.5 testing |

---

## 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 (32 octets of SHA-256). The boundary explicitly **excludes** :
- The host operating system kernel and user-land libraries (glibc, libdl, libpthread)
- The OpenSSL provider modules (`fips.so`, `default.so`) which have their own boundaries and CMVP cert
- The persistent token store on disk (treated as encrypted-at-rest data subject to §7.7 SSP transitions)
- The audit log files on disk (integrity-protected by HMAC chain)

### 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 :

* MD2, MD5, SHA-1 for signing (allowed for HMAC by SP 800-131A till 2030 ; we still allow HMAC-SHA-1 in verify path)
* DES, 3DES, RC4, RC5, RC2
* RIPEMD-128, RIPEMD-160
* RSA without padding (raw RSA)
* RSA-PKCS1-v1.5 signing with SHA-1 alone
* DSA (deprecated by SP 800-131A)
* SSL3 KDF family
* AES-ECB on > 16 bytes (FIPS forbids)

---

## 4. Software/Firmware Security (§7.5)

### 4.1 Source Code Hash

The build is reproducible per `dist-verify` :
- Reference digest : `dist/refs/v1.1.0.sha256` (signed at release time)
- Each build inside `Dockerfile.build` produces an ELF binary with the same SHA-256

### 4.2 Integrity Mechanism

The module embeds its own SHA-256 in a read-only ELF section `.fhsm_digest` (32 octets), patched by `make integrity` using OpenSSL FIPS provider. At every `C_Initialize`, `fhsm_integrity_verify()` recomputes the SHA-256 of the `.text` section 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 :
* **Crypto Officer (CO / SO)** : initial token init, user PIN init, key-pair generation, PIN policy.
* **User (USER)** : data ops (encrypt/decrypt/sign/verify), object create/destroy.

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

* **OS** : Debian 13.0 stable (kernel 6.1+)
* **C library** : glibc 2.40
* **Compiler** : gcc 14.2.0 (Debian 14.2.0-19)
* **OpenSSL** : 3.5.6 with FIPS provider activated
* **Provider load order** : `fips` (active), `base` (active)
* **Hardware** : x86_64 (Intel / AMD), required CPU features : AES-NI, AVX, CLMUL

### 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 :
* `fhsm_ct_memcmp` is used everywhere a secret is compared.
* Stack-allocated key material is cleared with `fhsm_zeroize` (calls `OPENSSL_cleanse` underneath).
* PIN verification path uses a throttle-before-PBKDF2 ordering to defeat timing attacks on the PBKDF2 derivation step.

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-256 (32 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

* **Establishment** (generation): Always via the FIPS DRBG (`fhsm_rng_bytes` → `RAND_bytes`).
* **Use** : In-memory only after C_Login ; DEK kept in secure heap arena.
* **Storage** : Persistent SSPs are wrapped under AES-GCM with the role's KEK ; the wrap CT includes a GCM tag and the token's serial as AAD.
* **Zeroization** : Triggered on `C_Logout`, `C_Finalize`, `fhsm_token_close`. The DEK buffer is overwritten with `OPENSSL_cleanse`.

---

## 9. Self-Tests (§7.10)

### 9.1 Pre-Operational Self-Test (§7.10.2)

Runs once at `C_Initialize` :
1. **Software integrity check** : SHA-256 of `.text` 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. Pre-certification status of the project means 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).

  * `C_CopyObject` enforces PKCS#11's one-way state transitions on the template : a copy cannot weaken `CKA_SENSITIVE` (false→true only) or `CKA_EXTRACTABLE` (true→false only) relative to the source. Violations return `CKR_TEMPLATE_INCONSISTENT`.
  * `C_SetAttributeValue` uses a whitelist approach : only `CKA_LABEL`, `CKA_ID`, and the two one-way state transitions on `CKA_SENSITIVE` / `CKA_EXTRACTABLE` are mutable. `CKA_CLASS` / `CKA_KEY_TYPE` / `CKA_VALUE` return `CKR_ATTRIBUTE_READ_ONLY` (immutable after creation per PKCS#11). All other attributes return `CKR_ATTRIBUTE_TYPE_INVALID`.
  * `C_WaitForSlotEvent` implements software-token semantics : `CKF_DONT_BLOCK` returns `CKR_NO_EVENT` immediately ; blocking mode returns `CKR_FUNCTION_NOT_SUPPORTED` rather than hang indefinitely (no hot-plug source in a software token).

**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_CloseAllSessions` (§C.6.6.2) into slot 14 ;
  * `C_SeedRandom` (§C.6.6.5) into slot 63, returning `CKR_RANDOM_SEED_NOT_SUPPORTED` unconditionally per NIST SP 800-90A §9.2 (DRBG seed input from caller is not approved) ;
  * `C_GetFunctionStatus` (§C.6.5.6) into slot 65, returning `CKR_FUNCTION_NOT_PARALLEL` per spec for non-parallel modules ;
  * `C_DigestKey` (§C.6.10.5) into slot 40, with `CKR_KEY_INDIGESTIBLE` gate on sensitive keys and asymmetric private keys to prevent key-material exfiltration via the digest output ;
  * `C_VerifyUpdate` (§C.6.13.6) into slot 50, multipart HMAC verify (mirrors `C_SignUpdate`) ;
  * `C_VerifyFinal` (§C.6.13.7) into slot 51, with constant-time MAC compare via `fhsm_ct_memcmp` to avoid timing side channels on signature validation.

`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 :

  * `C_GetOperationState` / `C_SetOperationState` (slots 16, 17) --- HSM-internal context switching, not applicable to a software token whose operation state is process-local.
  * `C_SignRecoverInit` / `C_SignRecover` (slots 46, 47), `C_VerifyRecoverInit` / `C_VerifyRecover` (slots 52, 53) --- RSA sign-with-message-recovery per ISO/IEC 9796-2, rarely used in modern applications.
  * `C_DigestEncryptUpdate` / `C_DecryptDigestUpdate` / `C_SignEncryptUpdate` / `C_DecryptVerifyUpdate` (slots 54-57) --- dual-operation streaming, niche use case.

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)

* **Pair-wise consistency** : after each `C_GenerateKeyPair`, sign+verify a known plaintext to confirm the new private key matches the public key (TODO — not yet wired).
* **Continuous RNG test** : every `RAND_bytes` call is checked against the previous block (FIPS 140-3 §7.10.4 "stuck DRBG").

### 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

* The RSA-PSS and RSA-OAEP KATs are FIPS 140-3 IG D.3 *consistency self-tests* (sign-then-verify and encrypt-then-decrypt round-trips on a fresh ephemeral keypair), not byte-deterministic vector matches. Two co-located bugs in the sign / verify or encrypt / decrypt code paths would not be surfaced by the round-trip alone ; the release-tier Wycheproof corpus (Section 13) provides the byte-deterministic safeguard against that residual risk.

* The PBKDF2 KAT vectors use iteration counts (c=1, c=2) below the production minimum (200 000) required by `FHSM_MODE=fips` at runtime. The low-iteration vectors validate algorithm mechanics ; the FIPS minimum is enforced separately at API entry under `FHSM_MODE=fips`.

* **AES-GMAC two-path self-consistency design** (v1.1.18) : the GMAC self-test computes the AES-256-GMAC tag of the existing TC14/TC15 inputs via TWO independent OpenSSL code paths — `EVP_MAC` "GMAC" and `EVP_CIPHER` AES-GCM tag-only with empty plaintext — and asserts the two 16-byte outputs match byte-for-byte. Mathematically the two paths are identical per NIST SP 800-38D §6.4. Any divergence catches a bug in either path. This is a *stronger* design than a fixed-value KAT because it exercises two implementations in parallel, and it obviates the need for a hardcoded expected tag (which would inevitably drift from upstream NIST publications and require periodic re-validation). The design is documented as a methodology in §13.6 and may be reused for future MAC additions.

---

## 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. Will be addressed at Level 2 candidate review (AVA_VAN.5).

Documented future work :
* Side-channel hardening audit and patches (see `docs/SIDE_CHANNEL.md`)
* TPM 2.0 sealing of the DEK (PCR-binding) for tamper-resistance at rest
* Multi-factor unlock via Shamir Secret Sharing quorum on the SO PIN

---

## 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** |

The post-quantum coverage makes FreeHSM C, at the time of writing, one of very few open-source PKCS#11 v3.2 modules with both NIST PQ primitives (KEM + signature) cross-validated against an external independent corpus. The Wycheproof `aes_gmac_test.json` corpus is wired through the `tests/wycheproof/adapters/aes_gmac.py` adapter added in v1.1.18 ; the corpus will be enabled in the full sweep once the test runner declaration is updated in a follow-up release.

### 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`. The matrix produces **24 PASS / 0 FAIL / 8 documented SKIP** in both default and FIPS-strict modes :

```
test-coverage-matrix         PASS = 24   FAIL = 0   SKIP = 8
test-fips-mode               PASS = 24   FAIL = 0   SKIP = 8
```

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-openssl-3.5`). 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, default + FIPS modes | 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 4616 46A6 408D E485 6016 2DBB F28A 2`) | 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 4616 46A6 408D E485 6016 2DBB F28A 2`. 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-openssl-3.5` container.
3. Patches the `.fhsm_digest` section with the post-link SHA-256 of the binary.
4. Builds both source and binary tarballs (`freehsm-c-X.Y.Z-FIPS-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-c`) and Codeberg (`codeberg.org/afchine1337/freehsm-c`) 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 pre-certification status, 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.** |
