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

11 KiB
Raw Blame History

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 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)
  • 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