wire.go: the validated serialization boundary between parties. Crypto objects (group elements, public keys, Schnorr proofs, ciphertexts) travel as decimal DTOs; every decode routes through NewGqElement/NewZqElement so a peer cannot inject a value outside G_q or Z_q — closing the small-subgroup / non-residue hole (finding M4) at the trust boundary. setup.go: RunSetup drives distributed key generation over the bus. Each CC generates its ElGamal keypair + return-code secret PRIVATELY and returns only its public key and Schnorr proofs; the setup component verifies every proof on receipt before combining keys. The electoral board derives its own key and returns only the public key. Combined election PK and setup artifacts are published to the public transcript. Test confirms the combined election key equals the product of the individually generated CC and EB keys, and that private key material stays with each party (never appears in the transcript). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|---|---|---|
| cmd/evote | ||
| pkg | ||
| rust/transportsec | ||
| .gitignore | ||
| ARCHITECTURE.md | ||
| go.mod | ||
| go.sum | ||
| Makefile | ||
| manual.html | ||
| presentation-crypto.html | ||
| presentation-swe.html | ||
| presentation.html | ||
| README.md | ||
Swiss Post E-Voting — Go PoC
A ground-up reimplementation of the Swiss Post e-voting cryptographic protocol as a single Go binary.
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 52 Go files with 2 dependencies.
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 |
Full 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) |
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:
make build # cargo build --release, then go build -o evote ./cmd/evote
make test # runs cargo test + go test ./...
# Run a complete election ceremony (10 voters, 3 candidates)
./evote demo --voters 10 --options 3
# Serve presentations on local network (for iPad viewing)
./evote serve --port 8080
# Theatrical step-by-step terminal walkthrough
./evote present
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) | 52 files, 15K lines (Go) |
| Infrastructure | Kubernetes, HSMs, air-gapped machines | Single binary, your laptop |
| Dependencies | Spring Boot, Bouncy Castle, Angular, ... | Cobra + stdlib crypto |
| Memory | 50GB+ RAM | ~50MB |
| Binary | N/A (Java services) | 9.5MB static binary |
| Startup | Minutes (JVM + Spring) | Instant |
Presentations
Three HTML slide decks are included, viewable in any browser or served to iPad via ./evote serve:
- presentation.html — Protocol overview: how a cryptographic election works
- presentation-crypto.html — Deep dive into the mathematics (ElGamal, ZKPs, Bayer-Groth)
- presentation-swe.html — Software engineering perspective: building a government election system in Go
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/ Election orchestration
verify/ Independent proof verification
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)
- 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