# Simorgh Labs / FreeHSM — Roadmap

Authoritative, living roadmap. Provenance: the phased plan from the planning
session, reconciled against what has actually shipped, plus the commercial-HSM
design study (`docs/DESIGN_NOTES_COMMERCIAL_HSM.md`), the rebrand plan
(`../REBRAND_CHECKLIST.md`), the brand reference
(`docs/SIMORGH_LABS_BRAND_REFERENCE.md`), the pkcs11-check campaign
(`docs/PKCS11_CHECK_CAMPAIGN.md` + `PKCS11_CHECK_FINDINGS.md`), and the
certification checklist (`docs/CST_LAB_SUBMISSION_CHECKLIST.md`).

Legend: ✅ done · 🟡 partial · ⏳ pending · ♻ continuous

---

## Layering (keep these categories clean)

* **FreeHSM** — the HSM *library* (`libfreehsm-fips.so`), PKCS#11 v3.2,
  FIPS 140-3 / CC EAL4+ evidence target. Apache-2.0.
* **Simorgh PKI** — the *product*: FreeHSM + CA + signing tool + operator +
  IaC modules. The integrated crypto-engineering stack.
* Anti-pattern to avoid (study): do **not** blur HSM and KMS. KMS-like features
  (policy engine, cross-cloud key sync, FPE) are a **post-v2.0** tier.

---

## Status at a glance

Phases 1–3 are essentially **complete**. The remaining real work is **Phase 4
(v2.0 = PKI tool + signing tool)**, the **rebrand mechanics** (repo rename +
domains — content is already rebranded), and two small threads (#126, #116).

---

## Phase 1 — before v1.5.0 (critical)

| # | Task | Effort | Status |
|---|---|---|---|
| #118 | Audit primacy claim "first OSS PKI + PQC composite" | ~4h | ✅ `docs/PRIMACY_AUDIT_PQC_COMPOSITE.md` — claim revised (pivoted to "PKI + signing + composite") |
| — | Rebrand migration (repo `freehsm-c`→`freehsm`, buy domains, brand everywhere) | ~10–12h | 🟡 Content rebrand ✅ (README, TRADEMARK, blog, brand ref, checklist). **Pending: repo rename on the 3 remotes + `freehsm.org`/`simorgh.io` domains** (see `REBRAND_CHECKLIST.md` phases 1–2) |

## Phase 2 — v1.5.0 (target 2026-07-20)

| # | Task | Effort | Status |
|---|---|---|---|
| #113 | Design notes "lessons from Trustway / Safenet / CipherTrust" | ~3h | ✅ `docs/DESIGN_NOTES_COMMERCIAL_HSM.md` |
| #108 | Token-store format doc (`TOKEN_STORE_FORMAT.md`) | ~2–3h | ✅ shipped |
| #110 | `CKO_CERTIFICATE` object support | ~3–4h | ✅ shipped (parser + store) |
| #125 | Integrate Denis Mingulov's pkcs11-check into CI | ~3–5h | ✅ shipped — plus a full hardening campaign (**361→<50 failures, 7→0 crashes**) |

## Phase 3 — v1.6.0 (target 2026-08-10)

| # | Task | Effort | Status |
|---|---|---|---|
| #107 | Session keys (`CKA_TOKEN=FALSE`) vs token keys | ~4h | ✅ session-object lifecycle implemented during #125 |
| #109 | TPM 2.0 sealing backend (machine-bound vault) | ~8h | 🟡 code present (`src/fhsm_tpm.c`, `src/fhsm_token_tpm.c`) — verify/finish + document |
| #127 | Private-key values in the secure heap, not plain `malloc` | ~6h | ⏳ **swap-exposure gap** — see note below |
| #128 | **The shipped `freehsm.conf` is inert — and fails permissive** | ~4h | ⏳ **do before #127** — see note |

### #127 — secure storage of decrypted private-key material (noted 2026-07-21)

At rest the object blob is AES-256-GCM under a DEK that is PBKDF2-wrapped
(200 000 iters) by the PIN, and the live DEK sits in the OpenSSL secure heap
(`mmap`+`mlock`, swap-excluded, zeroized on logout — `src/fhsm_memory.c`).

The gap: once a token is loaded, each object's decrypted value (`o->value` in
`src/fhsm_token.c`, lines ~390/435/487/1165) is a plain `malloc`. It is
zeroized on free, but while live it is **pageable** — the DEK is protected from
swap, the key material it protects is not. A module that `mlock`s the vault key
and leaves the private keys it guards in swappable memory is protecting the safe
and setting the jewels beside it.

Fix options:
1. Route `o->value` for `FHSM_OBJF_SENSITIVE` objects through
   `fhsm_secure_malloc`. Clean, but the secure-heap arena is fixed size
   (`FHSM_SECURE_HEAP_BYTES`); with `FHSM_MAX_OBJECTS=256` x
   `FHSM_OBJ_VALUE_MAX=2 MiB` the arena must be sized or the per-object value
   capped. Needs a little sizing design, not a one-line swap.
2. Minimum: document the limitation in `SECURITY.md` so nobody discovers it in
   an audit -- what the module keeps out of swap and what it does not.

Not a correctness bug (encrypt/decrypt round-trips are exact); it is a gap
between what the module protects and what it implies it protects. Surfaced
during the #125 close-out while auditing private-object storage.

**Sizing measured 2026-07-24** (DER private keys, OpenSSL 3.5.6), against the
current `FHSM_SECURE_HEAP_BYTES` = 256 KiB arena, for a full token of 256
sensitive objects:

| all 256 objects are | total | vs arena |
|---|---|---|
| AES-256 (32 B) | 8 KB | 0.03x |
| EC P-521 (223 B) | 57 KB | 0.2x |
| RSA-2048 (1 191 B) | 305 KB | 1.2x |
| RSA-4096 (2 347 B) | 601 KB | 2.3x |
| ML-KEM-1024 (3 266 B) | 836 KB | 3.2x |
| ML-DSA-87 (4 962 B) | **1.21 MiB** | **4.8x** |

So the clean option is viable but the arena must grow to ~2 MiB.

**Decision taken 2026-07-24 — exhaustion is a hard failure.** When the arena is
full, loading a sensitive object returns `CKR_DEVICE_MEMORY`; there is no
silent fallback to `malloc`. If the module cannot keep a private key out of
swap, it does not load it and says so. Falling back quietly would rebuild
exactly the lie this ticket exists to remove — a guarantee that holds until it
quietly does not. The cost is that an over-full token is unusable until the
operator raises `secure_heap_kb`, which is a configuration error with a clear
message, not a silent downgrade. Note that #128 must land first: the knob that would configure
this (`secure_heap_kb`) is advertised in the shipped config and read by nothing.

### #128 — the shipped config file is inert, and its failure direction is permissive (noted 2026-07-24)

`make install` writes `/etc/freehsm/freehsm.conf` containing nine keys:

```
fips_strict, audit_mandatory, secure_heap_kb, pin_max_failed,
pin_throttle_base_ms, pin_throttle_max_ms, pbkdf2_iterations,
tokens_dir, audit_dir
```

**None of them is read by any code.** The only key any parser looks for is
`mode` (`src/fhsm_mode.c`, `conf_says_fips()`), and `mode` is not in the file we
ship.

Two consequences, one benign and one not:

* `secure_heap_kb` is exactly the knob #127 needs, advertised and inert.
* An operator who writes `fips_strict = true` intending to harden the module
  gets **legacy mode**: the parser wants `mode = fips`, does not find it, and
  `compute_mode_locked()` falls through to `g_is_fips = 0`. The config file
  promises a control nothing enforces, and it fails in the permissive
  direction.

**Impact measured 2026-07-24 — this is truth-in-advertising, not a bypass.**
In an interop build, `FHSM_MODE` changes nothing on the PKCS#11 surface:

| interop build | mechanisms | GetMechanismInfo(RSA_PKCS) | C_EncryptInit |
|---|---|---|---|
| `FHSM_MODE` absent | 71 | 0x0 | callable |
| `FHSM_MODE=fips` | 71 | 0x0 | callable |
| `FHSM_MODE=legacy` | 71 | 0x0 | callable |

Because the enforcement that matters is compile-time on both paths:
advertisement goes through `fhsm_mech_advertised()`, a pointer comparison
against `dispatch_reject_fips` fixed when the table is generated; and the crypto
API gates on `fhsm_build_fips_strict`. The runtime mode reaches only the
KAT/dispatch path, which the PKCS#11 entry points do not use.

So the FIPS enforcement is sound and no non-approved mechanism escapes through
a misread config. What is broken is narrower and still worth fixing: the file we
ship promises runtime controls that do not exist, and the one key a parser does
read (`mode`) is absent from it and affects far less than its name suggests. It
is the same claim-what-you-cannot-enforce shape as the rest of #125, moved down
to the configuration layer.

Every shipped key names a real concept, but all are compile-time constants or
env vars: `secure_heap_kb` -> `FHSM_SECURE_HEAP_BYTES`, `pin_max_failed` ->
`FHSM_PIN_MAX_FAILED`, `pbkdf2_iterations` -> a hardcoded 200 000,
`tokens_dir` -> the `FHSM_TOKENS_DIR` env var, `audit_mandatory` -> a constant a
comment describes as aspirational, `audit_dir` -> nothing at all.

Fix shape: a real config parser reading the keys we actually ship, or a shipped
file containing only keys that exist. Either is defensible; shipping nine dead
keys and reading a tenth that is absent is not. Whichever is chosen, the
`mode`/`fips_strict` naming has to converge on one spelling.

## Phase 4 — v2.0.0-beta (target 2026-09-01) — **the MVP focus now**

| # | Task | Effort | Status |
|---|---|---|---|
| #112 | PKI tool (`fhsm-ca`, `fhsm-csr`, cert lifecycle, OCSP) + PQC composite sigs | ~14h | ⏳ **core of the v2.0 MVP** |
| #123 | Signing tool `fhsm-sign` L1+L2 (raw + CMS/PKCS#7), PQC-ready | ~10h | ⏳ **MVP multiplier** |

## Continuous / parallel

| # | Task | Cadence | Status |
|---|---|---|---|
| #116 | PQC watch (NIST + ANSSI + IETF + industry) → `docs/PQC_VEILLE.md` | quarterly ~1h | ♻ doc created 2026-07-24 (first entry: Wycheproof SLH-DSA absent, ML-DSA present) |
| #126 | PR to Denis's pkcs11-check README mentioning FreeHSM | after #125 | ⏳ **actionable now** (#125 done) — relationship + SEO backlink |

## Deferred — post-v2.0 stable

| # | Task | When | Notes |
|---|---|---|---|
| #106 | Exhaustive CC ATE_FUN test book | v2.0 stable +6mo | ANSSI EAL4+ submission → Tier-3 evidence |
| #111-prep | **Thread-safety of the PKCS#11 layer (blocks #111)** | ~10h | ⏳ **hard prerequisite** — see note |
| #111 | Network HSM via REST API | v2.5+ | The big architectural leap — wait for MVP validation **and #111-prep** |

### #111-prep — the network boundary stresses things the single-process model hides (noted 2026-07-21)

PKCS#11's model is one process per application, so today each client has its own
copy of the module state and the absence of locking in `fhsm_pkcs11.c` is
harmless. A REST server inverts that: **one process serves N concurrent
clients**, and the module-level mutable state becomes shared. Do not start #111
until this is done and proven under ThreadSanitizer — exposing a
not-thread-safe core over a network is building the facade before the
foundations.

Consolidation points, in priority order:

1. **Locking — done 2026-07-24, and my reasoning here was wrong.** I had
   written that because `fhsm_pkcs11.c` contains no `pthread_mutex`,
   `g_op_*` / `g_slots` / `g_finds` were shared mutable state and "two
   concurrent requests are a data race, not a hypothesis". Absence of a mutex
   does not imply sharing: `op_slot()` returns `&table[hSession]` and `g_finds`
   is indexed the same way, so that state is **partitioned by session handle**,
   and handle allocation is serialised in `fhsm_session.c`. Eight threads x 40
   loops of concurrent digest, encrypt and find operations run clean under TSan.

   Two real races did exist, both lazy initialisation reached from
   `C_OpenSession` outside any lock: `fhsm_slot_table_init_once()` guarded on a
   bare flag instead of `pthread_once`, and `fhsm_slot_token()` did
   test-load-assign on `g_slots[slot].token` — two threads could both call
   `fhsm_token_load` and both assign, leaking one token and leaving two
   `fhsm_token_t` objects for the same file with independent object stores and
   login state. Fixed with `pthread_once` and a mutex; 12 runs clean afterwards
   against 5 warnings on two of three runs before.

   The method mattered more than the fix. The first test reported nothing,
   because main had completed the lazy init before the threads started and the
   window was already shut. Releasing them from a `pthread_barrier` made both
   races appear, and even then only intermittently. A race TSan does not observe
   is indistinguishable from no race — which is how this would have reached
   production and surfaced under load. `make TSAN=1` and
   `tests/test_concurrency` are in the tree.
2. **Session binding.** `CK_SESSION_HANDLE` is a process-local integer. Over the
   wire it must be bound to an authenticated client, or client A presents
   handle 3 and reads client B's session. The REST layer owns a
   client→sessions table, and the `C_CloseSession` teardown must also fire when
   a network connection drops, not only on an explicit call.
3. **Per-identity throttle.** The anti-brute-force throttle counts failures per
   token and survives restart (good), but not per source. A distributed
   attacker bypasses a global counter, and one noisy client can lock out a
   legitimate one. Needs a per-identity throttle at the REST layer on top of the
   token's.
4. **#127 is promoted from nice-to-have to prerequisite.** Decrypted private-key
   values in pageable `malloc` are a gap locally; on a server holding tokens
   open for many clients the swap-exposure window grows enormously. Ship #127
   before, or with, #111.
5. **Audit gains an actor.** `fhsm_audit.c` logs events but not who requested
   them. A network HSM needs client identity and request origin on every entry,
   or the log says "a signature happened" without saying by whom — the one
   question a network audit exists to answer.

---

## New design decisions to fold in (study Category B)

* **B1** Diátaxis documentation architecture.
* **B2** Portable key-blob format + `fhsm-export` / `fhsm-import` (sovereignty = freedom to migrate).
* **B3** Structured JSON logs (ECS/OTel) + Prometheus `/metrics`.
* **B4** Multi-source entropy pool (kernel DRBG + jitterentropy + TPM RNG + optional YubiKey/OpenTitan).
* **B5** K8s operator + Terraform provider + Ansible collection + Helm.
* **B6** SIEM plugins (Splunk/Datadog/Sentinel/QRadar) — Tier-4 monetization.
* **B7** Multilingual Tier-2 support (EN/FR/Persan/Arabic).
* **B8** Tier-2 SLA: 4h critical / 24h high / 5 business days medium-low.
* **B9** v3.x horizon: KMS + VM encryption + file encryption.

## Marketing claims (study Category C) — ready for the v2.0-beta landing

6 direct, defensible framings (verbatim in `docs/DESIGN_NOTES_COMMERCIAL_HSM.md`
§Category C): 20-year PKCS#11 lead, 23 signed releases + 3 GHSAs in 6 weeks vs
one version / 2 years, open token-store + migration tool, TPM tamper-resistance
at ~100× lower TCO, one year of incumbent maintenance ≈ a full EAL4+ evidence
package, "integrated crypto-engineering stack" vs "boxed HSM".

---

## The strongest insight — timing is the hidden strength

Bull/Atos Trustway has been in expert-flight for years and **no OSS project has
seriously taken over**. Each of these is individually a strong differentiator;
together they have **no OSS or commercial equivalent on the target geography**:

* 6-week release cadence vs their 2 years
* PKCS#11 v3.2 vs their v2.20
* integrated PKI + signing vs a boxed HSM
* DevOps-native vs zero cloud
* multilingual vs French-only
* **PQC composite** vs PQC-raw-only

The French OIV / defence / defence-industry market is actively looking for a
Proteccio alternative for exactly these reasons. The response arrives right as
they start searching. Entry window 2026–2027 (see GTM phasing).

## Go-to-market (study Category D)

| Phase | Timing | Action |
|---|---|---|
| 1 | v2.0-beta (2026-09) | Public announce, positioning vs Proteccio (claims C1–C6) |
| 2 | v2.0 stable (2026-12) | French OIV + defence procurement; Tier-3 evidence package |
| 3 | 2027-Q1 | Present at FIC 2027 (Lille) |
| 4 | 2027-Q2 | ANSSI CC EAL4+ QR evaluation kickoff |
| 5 | 2027-Q3/Q4 | MENA / Africa (GISEC, Black Hat MEA, Africa Cyber Defense Forum) |

## Honest scope boundary (study Category E)

Target the ~99% of the market that does **not** need FIPS 140-3 Level 3
*physical* security. Not matched (and that's fine): dedicated hardware TRNG,
physical tamper detection with auto-zeroization, Level 3 physical posture.
Compensated with TPM measured boot + integrity attestation + audit-log tamper
evidence; a physically compromised host is a lost host in our model.
