9f030d1c07b87761e9aed5ca7214a5cac8ed3f18cde2c71d8032bcbfb1a3a953
#!/usr/bin/env python3"""server.py — the BLIND computation for `cohort_histogram`.This is the ONLY code that runs server-side. The kit shim`30_compute_encrypted.py` is its sole caller: it maps the argparse CLI(`--context/--inputs/--out`) onto the reserved ``compute`` function below. Theauthor writes only this pure, bytes-in/bytes-out function; all the argv/fileplumbing lives in the (kit-owned) shim.Trust boundary, made structural by the signature: compute(inputs: list[bytes], public_context: bytes) -> bytesThere is NO secret-context parameter — this function is incapable of receiving asecret key. It runs in the network-isolated sandbox (`--network none`, read-onlyfs, non-root, resource-limited) with the PUBLIC context and ciphertexts only, sothe server never sees a single plaintext bucket membership.The fold is written ONCE against an abstract evaluator ``E`` (the cleartext oraclein docs/simulation_mode.md swaps a plaintext evaluator into the same shape so itcannot drift from this encrypted path): aggregate(inputs, E): # E provides zero / add acc = <first input> for ct in rest(inputs): # cohort_histogram: pure coordinate-wise add acc = E.add(acc, ct) return accAdditive-only: the sentinel slot rides along, so ``sum_i (h_i || 1)`` yields theper-bucket count vector in the first B slots and the exact contributor count N inthe trailing sentinel slot. Because each ``h_i`` is one-hot, the first B slotsthemselves also sum to N — a redundancy stage 50 asserts.Determinism / verify-by-re-execution: BFV addition is deterministic, so the sameordered set of input ciphertexts always yields the same result ciphertext bytes(encryption is randomized, the *compute* is not). Re-running this stage on thesame inputs reproduces a bit-identical result digest."""from __future__ import annotationsfrom typing import Iterable, Protocolclass Evaluator(Protocol): """The abstract op interface both engines implement (see simulation_mode).""" def zero(self, length: int): ... def add(self, a, b): ...class BFVEvaluator: """The real (encrypted) evaluator: ops on TenSEAL BFV ciphertexts.""" def __init__(self, context) -> None: self.context = context def zero(self, length: int): import tenseal as ts return ts.bfv_vector(self.context, [0] * length) def add(self, a, b): return a + b def load(self, blob: bytes): import tenseal as ts return ts.bfv_vector_from(self.context, blob)def aggregate(inputs: Iterable, evaluator: Evaluator, length: int | None = None): """Fold ``inputs`` under ``evaluator.add`` into a single aggregate. Folds from the first input (no length needed). If ``inputs`` is empty a zero vector of ``length`` is returned (requires ``length``). """ iterator = iter(inputs) try: accumulator = next(iterator) except StopIteration: if length is None: raise ValueError("aggregate() needs a length when there are no inputs") return evaluator.zero(length) for item in iterator: accumulator = evaluator.add(accumulator, item) return accumulatordef compute(inputs: list[bytes], public_context: bytes) -> bytes: """RESERVED blind entrypoint — homomorphically sum the contributor ciphertexts. Deserialize the PUBLIC context and each ciphertext, fold them into one aggregate ciphertext, and return the serialized result. No secret key is present; defensively refuse a context that carries one. """ import tenseal as ts context = ts.context_from(public_context) if context.is_private(): # The server must never receive a secret key. raise ValueError("compute stage received a context holding a secret key") evaluator = BFVEvaluator(context) vectors = [evaluator.load(blob) for blob in inputs] return aggregate(vectors, evaluator).serialize()
Inside signed payload digest 9f030d1c07b8…b1a3a953. Change one byte here and the application becomes a different application.