8691074bf4bbb434cbebb0c61acf6a40cd01987f0a9035a07c4cc484886fb43c
# Security notes — `carrier_count`Scoped to this bundle. The platform-wide threat model lives in`docs/manifesto.md`, `docs/requirements.md`, and `docs/simulation_mode.md` §5.Kerckhoffs applied to a product: **no guarantee rests on the secrecy — or thehonesty — of the server.** Don't trust, verify.`carrier_count` reuses the flagship's coordinate definition and additive circuitverbatim; only the client-side encoding differs (dosage thresholded to a carrierindicator *before* encryption). The leakage boundary is therefore the flagship's,with one narrowing: the server — and the released aggregate — see only carrier*indicators*, never the underlying dosage. Thresholding `2 -> 1` and `1 -> 1`happens locally, so whether a carrier is heterozygous or homozygous never leavesthe owner's machine in any form.## Trust classes (what may cross the wire)| class | example artifact | may leave the owner's machine? ||-------|------------------|-------------------------------|| RAW | `raw.json` dosage genotypes | **no** || ENCODED | `encoded.json` carrier-indicator vector | **no** || PRIVATE | `secret_context.tenseal` (secret key), `plain.json` | **no, ever** || ENCRYPTED | `cipher.bin`, `result.bin` | yes || PUBLIC | `public_context.tenseal` | yes |Only ENCRYPTED and PUBLIC are ever uploaded. `00_keygen.py` writes the secret keyto `secret_context.tenseal`, which is used **only** by `40_decrypt.py` on theresearcher's machine. There is no `/api/v1` endpoint that accepts a secret key.## Server holds no secret key`30_compute_encrypted.py` — the only server-side stage, a kit shim that runs`server.py`'s `compute` — loads the **public**context plus ciphertexts and homomorphically adds. It defensively refuses acontext that carries a secret key (`context.is_private()` → error). The servertherefore never sees a single plaintext carrier indicator; it operates onciphertext and returns ciphertext. Decryption happens only where the secret keylives: locally.## The append-1 sentinel is NOT a MACThe trailing sentinel slot decrypts to the exact contributor count N, anddropping one upload yields N−1 (test: `test_sentinel_tracks_dropped_upload`). Itcatches **mechanical corruption / miscounting** — it gives **no** guarantee thatcontributions are distinct, genuine, or non-Sybil. Call it what it is: anintegrity check, not authenticity.`carrier_count` admits one extra, free integrity check the flagship does not: acarrier count is a headcount, so every released `carrier_count[j]` must lie in`[0, N]`. `50_decode.py` asserts this; a value outside the range means corruptionor an out-of-domain contribution slipped past encoding. (It is not a strongerconstraint than the sentinel — a malicious over-1 contribution could still fallinside `[0, N]` — but it catches the common corruption mode for free.)## What FHE here does and does not hide- **Hides:** individual carrier vectors from the server (inputs are ciphertext), and — because thresholding is local — the underlying dosage entirely (hom vs het is never encoded or transmitted).- **Does not hide:** the released aggregate itself, and metadata (researcher identity, participant count/timing, ciphertext sizes, protocol choice).- **Differencing (K vs K+1):** the *statistic* leaks an individual if you can compute `A_{K+1} − A_K`. `aggregate_only` + `min_contributors ≥ 20` + `allowed_runs_per_project: 1` (cohort freeze + min-N + run cap) **mitigate** this; they are not a complete defense. Overlapping/Sybil differencing across separately frozen cohorts needs DP + cross-job query budgets (v2). Documented, not hand-waved — see `docs/simulation_mode.md` §5.- **Verify-by-re-execution is determinism, not zero-knowledge.** Re-running `30_compute_encrypted.py` on the same ciphertexts reproduces a bit-identical result digest; it proves the computation, it is not a ZK proof.## Exactness / parameter safetyBFV is exact in `Z_t`. The plaintext modulus must satisfy `t > max coordinatesum = N` (each contributor adds a 0/1 indicator per coordinate). The default`t = 1032193` (a 20-bit batching prime) stays exact for N up to ~1M — an evenwider margin than the flagship's `2N` ceiling, since carriers cap at 1 percoordinate. A real run at implausibly large N must raise `t` (or the simulationfeasibility sweep will report `infeasible-at-these-params` on overflow). Thesentinel sum is N, always ≪ t.
Packaged support file for application digest 8691074bf4bb…886fb43c. It ships in the archive for review, but is outside the signed payload digest.