Docs: cast-as-intended return codes are now real

ARCHITECTURE: replace the "return codes simulated" scope limit with the
cast-as-intended design (E2 + plaintext-equality proof, CC exponentiation +
decryption, voter check, substitution-attack test) and document the remaining
honest limits (single-选option; return-code lookup lets the server infer the
selection — a privacy simplification that does not affect tally secrecy).
README: note that netdemo return codes are cast-as-intended.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
saymrwulf 2026-07-06 15:53:40 +02:00
parent 127ae61d11
commit 048b16e9fa
2 changed files with 46 additions and 15 deletions

View file

@ -71,8 +71,12 @@ Run it with `evote netdemo --verbose` to watch every signed envelope.
long-code-share-resp` (return-code shares from the CC's private secret). Setup
assembles cards and delivers them **confidentially**: `setup → voter:
voting-card` and `setup → server: mapping-table` (both X25519-encrypted).
- **Voting** — `voter → server: cast-ballot`; `server → CCj: verify-ballot`;
`CCj → server: ballot-verdict`. Stored on unanimous acceptance (vcPK persisted).
- **Voting** — `voter → server: cast-ballot` (ballot + E2 + equality proof);
`server → CCj: verify-ballot`; `CCj → server: ballot-verdict`. Stored on
unanimous acceptance (vcPK persisted). Then the return-code extraction:
`server → CCj: rc-exp-req` / `rc-dec-req`; `CCj →: rc-exp-resp` / `rc-dec-resp`.
The server returns the recovered code to the voter, who checks it against the
card (cast-as-intended — see below).
- **Tally** — `→ server: start-tally` (server pads the ballot box); then
`→ CCj: shuffle` / `CCj →: shuffled` chained through all CCs; `→ EB: final-mix`
/ `EB →: final-done`. Each party posts its shuffle + decryption proofs to the
@ -108,17 +112,41 @@ addressed in the party layer:
- **F12** (panic on malformed plaintext): tally uses `DecodeVoteChecked`, so a
ballot that does not decode is spoiled, not fatal; handlers reject malformed
messages cleanly rather than panicking.
- **F5 / F16** (return codes were decorative): the return-code path is now
genuinely **cast-as-intended** — see below.
### Honest scope limits
## Cast-as-intended return codes
- **F5** (the unsound plaintext-equality proof) is *not* reimplemented. The
multi-party ballot uses only the **sound exponentiation proof** binding the
ballot to `vcPK`; the return-code path that encrypts partial choice codes under
the return-codes key (E2 / plaintext-equality) is omitted.
- Consistent with the original system (finding F16), the **cast-as-intended**
return codes are derived deterministically from CC shares rather than
homomorphically over the submitted ciphertext. The return-code *mechanism*
(per-CC shares combined into a mapping-table lookup) is real and consistent
across card generation and would-be extraction (fixing the old derivation
mismatch, F1); the property that the shown code is computed *from the actual
ballot* remains simulated.
The return code the voter checks is computed by the control components **from the
submitted ciphertext**, so client-side vote substitution is detected:
1. The voter submits the ballot `E1` plus a second ciphertext `E2 =
Enc(vote, returnCodesPK)` and a **plaintext-equality proof** that `E1` and
`E2` encrypt the same vote. Every CC verifies this proof; a ballot whose two
ciphertexts disagree is rejected.
2. The card code for option *i* is the algebraic base `prime_i^{Σ_j k_j}`, where
`k_j` is CC *j*'s voter-specific return-code key. Setup builds it by combining
per-CC shares; the same `k_j` is used at vote time (one derivation — the old
F1 mismatch is gone by construction).
3. At vote time each CC exponentiates `E2` by its `k_j` (product over CCs =
`Enc(vote^{Σk})`), then contributes a partial-decryption factor. The server
recovers `vote^{Σk}` — which equals `prime_sel^{Σk}` — and looks up the short
code, returning it to the voter.
4. The voter checks the returned code against the card for the chosen option. A
mismatch means the tallied vote differs from the intended one.
**Soundness**: because the equality proof binds `E2` to `E1` (the tallied
ballot), the code is derived from the vote that is actually counted. Malware that
alters the vote cannot produce a matching card code without the CC secrets. A
unit test drives the substitution attack (`E1`=A, `E2`=B) and confirms the CCs
reject it.
### Remaining scope limits (honest)
- The return-code path is defined for a **single selected option** per voter (the
demo case); multi-selection return codes are not implemented.
- **Privacy**: the return-code lookup lets the server learn which mapping entry
matched, so in this PoC the return-code-computing parties can infer the
selection. The real system separates these roles to keep the vote secret from
any single party; that role separation is simplified here. Vote secrecy of the
**tally** is unaffected (it rests on the mix-net, not the return-code channel).

View file

@ -79,7 +79,10 @@ The `demo` command runs the whole protocol in one process. `netdemo` runs the
**multi-party** architecture: every party is a separate endpoint holding only its
own private state, and every message between them is Ed25519-signed (and, for
card/mapping-table delivery, X25519-encrypted) — all transport cryptography in
Rust. See [ARCHITECTURE.md](ARCHITECTURE.md).
Rust. The `netdemo` return codes are **cast-as-intended**: each voter's code is
recomputed by the control components from the submitted ciphertext (bound to the
ballot by a plaintext-equality proof), so a substituted vote is detected. See
[ARCHITECTURE.md](ARCHITECTURE.md).
If you build Go directly (`go build ./...`), run `make rust` first so the static library
at `rust/transportsec/target/release/libtransportsec.a` exists for cgo to link.