The Blind Machine

Cohort Histogram

1.0.0 Ed25519 signed

Application digest — content address of the signed/ payload

9f030d1c07b87761e9aed5ca7214a5cac8ed3f18cde2c71d8032bcbfb1a3a953
cohort_histogram / SECURITY.md

76 lines · 4.21 KB · sha256:9bc1f000277c…7a4e7856

Security notes — cohort_histogram

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 the
honesty — of the server.
Don’t trust, verify.

Trust classes (what may cross the wire)

class example artifact may leave the owner’s machine?
RAW raw.json bucket index no
ENCODED encoded.json one-hot 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 key
to secret_context.tenseal, which is used only by 40_decrypt.py on the
researcher’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 a
context that carries a secret key (context.is_private() → error). The server
therefore never sees a single plaintext bucket membership; it operates on
ciphertext and returns ciphertext. Decryption happens only where the secret key
lives: locally.

The append-1 sentinel is NOT a MAC

The trailing sentinel slot decrypts to the exact contributor count N, and
dropping one upload yields N−1 (test: test_sentinel_tracks_dropped_upload).
Because contributions are one-hot, the per-bucket counts must also total N,
so 50_decode.py asserts sum(counts) == N and rejects any aggregate that
fails it (test: test_decode_rejects_non_one_hot_aggregate). Both checks catch
mechanical corruption / miscounting — they give no guarantee that
contributions are distinct, genuine, or non-Sybil. Call them what they are:
integrity checks, not authenticity. A malicious client could still submit a valid
one-hot vote for the wrong bucket; nothing here proves a contributor’s raw value
was truthfully bucketed.

What FHE here does and does not hide

  • Hides: each contributor’s individual bucket membership from the server
    (inputs are ciphertext).
  • Does not hide: the released histogram itself, and metadata (researcher
    identity, participant count/timing, ciphertext sizes, protocol choice, the
    published bucket definition).
  • Small-bucket / rare-category leakage: a bucket whose count is 1 pinpoints
    that a single contributor is in that category. The released statistic is the
    histogram, so a rare bucket is inherently identifying — a stronger version of
    the flagship’s differencing concern. aggregate_only + min_contributors ≥ 25
    (higher than the flagship) + allowed_runs_per_project: 1 mitigate this;
    they are not a complete defense. Per-bucket suppression / k-anonymity thresholds
    and DP noise on the counts are the v2 answer (documented, not hand-waved).
  • Differencing (K vs K+1): the histogram leaks an individual if you can
    compute H_{K+1} − H_K (the differenced vector is that contributor’s one-hot).
    Same cohort-freeze + min-N + run-cap mitigation as the flagship; overlapping /
    Sybil differencing across separately frozen cohorts needs DP + cross-job query
    budgets (v2). 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 safety

BFV is exact in Z_t. The plaintext modulus must satisfy t > max bucket count.
Because every contribution is one-hot, a single bucket holds at most N, so the
largest coordinate value is N — a wider margin than the flagship’s 2·N
envelope. The default t = 1032193 (a 20-bit batching prime) stays exact for N
up to ~1M; a real run at larger N must raise t (or the simulation feasibility
sweep will report infeasible-at-these-params on overflow). The sentinel sum is
N, always ≪ t, and equals sum(counts) by construction.

Packaged support file for application digest 9f030d1c07b8…b1a3a953. It ships in the archive for review, but is outside the signed payload digest.

Keyboard shortcuts

  • Cmd/Ctrl+K
    Focus global search
  • ?
    Open keyboard shortcuts

Send feedback

We'll only use this to respond to your feedback.