The foundation for the "watch the mathematics execute" cockpit. Each meaningful crypto operation will emit one Event carrying its LaTeX notation plus the real runtime values, tagged with party + phase. One stream feeds both a terminal view (ASCII/Unicode) and a browser view (typeset KaTeX) — no double-maintenance. - Off by default with near-zero cost: Emit/EmitFunc return immediately when no sink is attached (atomic fast path), so instrumentation is free in normal runs. - Sinks: ChanSink (buffered, drops rather than stalling the ceremony), SliceSink (tests). Short() elides ~77-digit values for compact display. Tests: disabled-is-cheap, monotonic sequence + context stamping, elision. 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, 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).
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:
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.
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/:
- 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)
- 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