swisspost-evoting-go-poc/README.md
saymrwulf 9618e92c01 README: correct stale numbers and claims
Update file/LOC counts (52→~94 files, ~10.5K LOC), add the Rust transport-security
crate and Ed25519/X25519 (no RSA) to the comparison table, add the party-isolation
row and netdemo mode, and point the presentation links at the served web/ decks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 19:03:35 +02:00

205 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Swiss Post E-Voting — Go PoC
A ground-up reimplementation of the Swiss Post e-voting cryptographic protocol as a single Go binary, with a multi-party mode where the parties talk over a Rust-signed transport.
The Swiss Post system is Switzerland's official internet voting platform, used in binding federal elections. The production system spans **14 Java repositories, 500K+ lines of code, and requires 50GB of RAM**. This PoC distills the core cryptographic protocol into **~94 Go files (~10.5K lines) plus a small Rust crate for transport security**. Two runtime modes: `demo` (single process) and `netdemo` (each party a separate endpoint, communicating only over Ed25519-signed / X25519-encrypted messages implemented in Rust — see [ARCHITECTURE.md](ARCHITECTURE.md)).
## What This Implements
The full election lifecycle with end-to-end verifiability:
```
Setup 4 Control Components + Electoral Board generate keys
Voting cards with secret codes are produced
|
Vote Voter encrypts ballot client-side (ElGamal)
Server validates zero-knowledge proofs
Return codes confirm vote was recorded correctly
|
Tally 5 sequential verifiable shuffles (Bayer-Groth)
Each shuffle: permute -> re-encrypt -> partial decrypt
Final decryption by air-gapped Electoral Board
|
Verify Public audit: all proofs are independently checkable
No secrets required — anyone can verify the election
```
## Cryptographic Components
| Package | What It Does |
|---------|-------------|
| `pkg/math` | Quadratic residue groups (G_q), safe prime generation, group vectors/matrices |
| `pkg/elgamal` | ElGamal encryption, partial decryption, homomorphic ciphertext operations |
| `pkg/zkp` | Schnorr proofs, exponentiation proofs, decryption proofs, plaintext equality proofs |
| `pkg/mixnet` | Bayer-Groth verifiable shuffle with 6 sub-arguments (product, Hadamard, zero, SVP, multi-exponentiation, shuffle) |
| `pkg/hash` | SHA-256 hash-and-square for Fiat-Shamir transforms |
| `pkg/kdf` | HKDF key derivation for return code generation |
| `pkg/symmetric` | AES-GCM authenticated encryption |
| `pkg/returncodes` | Vote encoding as small primes, return code mapping tables |
| `pkg/protocol` | Single-process election orchestration (setup, vote, confirm, tally) |
| `pkg/verify` | Independent verification of all proofs |
| `pkg/transportsec` | Transport-layer security (Ed25519 signatures, X25519 ECDH) — **implemented in Rust**, linked via cgo (see below) |
| `pkg/transport` | Authenticated message bus: Ed25519 X.509 PKI, signed envelopes, X25519 secure channels |
| `pkg/party` | **Multi-party ceremony**: each endpoint (setup, 4 CCs, electoral board, voting server, voters, verifier) as a separate party communicating only over the signed transport |
## Transport Security in Rust
Channel security between parties uses **Ed25519** signatures and **X25519 (ECDH)** key
agreement — deliberately elliptic-curve, with **no RSA anywhere**. These primitives are
implemented in Rust (`rust/transportsec`, using `ed25519-dalek` and `x25519-dalek`),
compiled to a C-ABI static library, and called from Go through cgo (`pkg/transportsec`).
The Go side never implements or duplicates this cryptography. A cross-language conformance
test proves the Rust signatures are standard RFC 8032 Ed25519 (Go's `crypto/ed25519`
verifies them and vice-versa).
## Quick Start
Building requires a **Rust toolchain** (for the transport-security static library) in
addition to Go. The Makefile builds the Rust library first, then the Go binary:
```bash
make build # cargo build --release, then go build -o evote ./cmd/evote
make test # runs cargo test + go test ./...
# Run a complete election ceremony, single process (10 voters, 3 candidates)
./evote demo --voters 10 --options 3
# Run the SAME election as separate parties over Rust-signed transport
./evote netdemo --voters 10 --options 3
./evote netdemo --voters 3 --options 2 --verbose # log every signed message
# Serve presentations on local network (for iPad viewing)
./evote serve --port 8080
# Theatrical step-by-step terminal walkthrough
./evote present
```
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. 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.
## Demo Output
```
=== SWISS POST E-VOTING PROTOCOL PoC ===
Phase 1: SETUP
Generated safe prime group (q: 256 bits, p: 257 bits)
CC[0]: generated ElGamal keypair, Schnorr proof OK
CC[1]: generated ElGamal keypair, Schnorr proof OK
CC[2]: generated ElGamal keypair, Schnorr proof OK
CC[3]: generated ElGamal keypair, Schnorr proof OK
EB: generated ElGamal keypair, Schnorr proof OK
Combined election public key (product of all 5)
Generated 10 voting cards with return codes
Phase 2: VOTING
Voter 1: encrypted vote for option 2, proof verified
Voter 2: encrypted vote for option 0, proof verified
...
Phase 3: TALLY
Shuffle 1/5 (CC[0]): permute + re-encrypt + partial decrypt
Shuffle 2/5 (CC[1]): permute + re-encrypt + partial decrypt
...
Final decryption by Electoral Board
Results: Option 0: 4 votes, Option 1: 3 votes, Option 2: 3 votes
Phase 4: VERIFICATION
All 5 Schnorr key proofs: VALID
All 5 shuffle proofs: VALID
Vote count matches ballot box: VALID
Election integrity: VERIFIED
```
## Production vs. PoC
| Aspect | Production (Swiss Post) | This PoC |
|--------|------------------------|----------|
| Group size | 3072-bit safe prime | 256-bit safe prime |
| Codebase | 14 repos, 500K+ lines (Java) | ~94 Go files (~10.5K lines) + Rust crate |
| Infrastructure | Kubernetes, HSMs, air-gapped machines | Single binary, your laptop |
| Signatures / key exchange | RSASSA-PSS, RSA channels | Ed25519 / X25519 (Rust), no RSA |
| Dependencies | Spring Boot, Bouncy Castle, Angular, ... | Cobra + golang.org/x/crypto; ed25519-dalek + x25519-dalek (Rust) |
| Party isolation | Separate machines / operators | Separate in-process endpoints over a signed bus (`netdemo`) |
| Memory | 50GB+ RAM | ~50MB |
| Binary | N/A (Java services) | ~9.5MB (Go + linked Rust static lib) |
| Startup | Minutes (JVM + Spring) | Instant |
## Presentations
HTML slide decks are embedded in the binary and served via `./evote serve` (iPad-friendly);
the same files live under [cmd/evote/web/](cmd/evote/web/):
- **index.html** — Landing page and operations overview
- **demo.html** — Protocol walkthrough: how a cryptographic election works
- **crypto.html** — The mathematics (ElGamal, ZKPs, Bayer-Groth, Ed25519/X25519 transport, cast-as-intended)
- **swe.html** — Software engineering: building it in Go, plus the multi-party re-architecture and the Rust transport-security layer
Standalone copies (`presentation*.html`, `manual.html`) are kept at the repo root for direct browsing.
## Project Structure
```
cmd/evote/
main.go Cobra CLI root
demo.go Full election ceremony
serve.go HTTP server for presentations
present.go Theatrical terminal demo (772 lines)
web/ Embedded HTML presentations
pkg/
math/ Group theory (GQ, ZQ, vectors, matrices)
elgamal/ Encryption, decryption, key management
zkp/ Zero-knowledge proofs (4 types)
mixnet/ Verifiable shuffle (Bayer-Groth, 12 files)
hash/ Hash-and-square, Fiat-Shamir
kdf/ HKDF key derivation
symmetric/ AES-GCM
returncodes/ Vote encoding, return code mapping
protocol/ Single-process election orchestration
verify/ Independent proof verification
transportsec/ Rust FFI: Ed25519 sign/verify, X25519 ECDH
transport/ PKI, signed envelopes, secure channels, message bus
party/ Multi-party ceremony (one object per endpoint)
rust/transportsec/ Rust crate: ed25519-dalek + x25519-dalek, C ABI
```
## Security Hardening (Due-Diligence Pass)
A full correctness/security review of the codebase produced the following fixes,
each covered by a regression test:
| Area | Issue | Fix |
|------|-------|-----|
| `pkg/mixnet` | The multi-exponentiation verifier's `c_{B_m} = commit(0;0)` check was stubbed out (empty `if`), letting a malicious mixer prove a **non-permutation** shuffle | Check enforced; honest shuffles still verify |
| `pkg/zkp` | All four FiatShamir challenges were `hash mod q` — biased, and capped at 256 bits for production groups | Switched to spec-correct `RecursiveHashToZq` (oversample-then-reduce) |
| `pkg/protocol` | `VerifyTally` returned `true` unconditionally; Schnorr proofs were never actually verified | Real `zkp.VerifySchnorrProof` calls; returns the true aggregate result |
| `pkg/protocol` | Mix padding for N<2 was regenerated with fresh randomness at verify time shuffle 0 always failed | Padded input persisted as `event.MixInput` and reused by the verifier |
| `pkg/kdf` | `BuildKDFInfo` concatenated parts without separators (`("e1","23x")` == `("e12","3x")`) | Length-prefixed, injective encoding |
| `pkg/math` | `GqElementFromSquareRoot` rejected the valid root `q` (off-by-one), a latent panic; `RandomGqElement` skipped one element | Range corrected to `[1, q]` |
| `cmd/evote` | `--options=0` / `--voters=-1` panicked | Validated flags return clean errors |
| everywhere | `math/rand` used in the demo driver | Replaced with `crypto/rand` |
Trust-boundary hardening (validated deserialization, verifiers returning `false`
rather than panicking on malformed peer input) is handled in the multi-party
re-architecture, where inputs actually arrive over the wire.
## References
- [Swiss Post E-Voting System Specification (PDF)](https://gitlab.com/swisspost-evoting)
- Bayer, S. & Groth, J. (2012). *Efficient Zero-Knowledge Argument for Correctness of a Shuffle*
- Haines, T. & Groth, J. (2020). *Verifiable Shuffle of Large Ciphertexts*
- ElGamal, T. (1985). *A Public Key Cryptosystem and a Signature Scheme Based on Discrete Logarithms*
## License
MIT