mirror of
https://github.com/saymrwulf/swisspost-evoting-go-poc.git
synced 2026-09-03 20:13:43 +00:00
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>
205 lines
11 KiB
Markdown
205 lines
11 KiB
Markdown
# 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 Fiat–Shamir 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
|