swisspost-evoting-go-poc/README.md

197 lines
9.6 KiB
Markdown
Raw Normal View History

# 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` | Single-process election orchestration (setup, vote, confirm, tally) |
| `pkg/verify` | Independent verification of all proofs |
Due-diligence hardening + Rust transport-security layer Correctness/security review of the whole PoC, with fixes and regression tests. Cryptographic soundness: - mixnet: enforce the multi-exponentiation c_{B_m}=commit(0;0) check that was stubbed out with an empty if — without it a malicious mixer can prove a non-permutation shuffle. - zkp: derive all four Fiat-Shamir challenges via RecursiveHashToZq instead of a biased `hash mod q` (which also capped the challenge space at 256 bits for production-sized groups). Verification honesty: - protocol: VerifyTally now actually calls zkp.VerifySchnorrProof and returns the true aggregate result instead of an unconditional true. - protocol: persist the padded mix input (event.MixInput) so the verifier checks shuffle 0 against the same padding the tally used (fixes false INVALID for N<2). Other correctness: - kdf: length-prefix BuildKDFInfo parts so the info encoding is injective. - math: GqElementFromSquareRoot accepts the valid root q (off-by-one that could panic in HashAndSquare); RandomGqElement samples the full canonical range. - cmd: validate demo --voters/--options instead of panicking on degenerate values. - protocol: use crypto/rand in the demo driver (drop the last math/rand import). Transport security (new): pkg/transportsec exposes Ed25519 signatures and X25519 ECDH — implemented in Rust (rust/transportsec: ed25519-dalek, x25519-dalek), linked into Go via cgo. No RSA. Cross-language conformance test proves the Rust Ed25519 signatures interoperate with Go's crypto/ed25519. Makefile builds the Rust static lib before the Go binary. Tests: added unit/round-trip/tamper coverage for math, hash, elgamal, zkp, mixnet, kdf, returncodes, protocol (end-to-end), and the Rust FFI bridge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 12:42:34 +00:00
| `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 |
Due-diligence hardening + Rust transport-security layer Correctness/security review of the whole PoC, with fixes and regression tests. Cryptographic soundness: - mixnet: enforce the multi-exponentiation c_{B_m}=commit(0;0) check that was stubbed out with an empty if — without it a malicious mixer can prove a non-permutation shuffle. - zkp: derive all four Fiat-Shamir challenges via RecursiveHashToZq instead of a biased `hash mod q` (which also capped the challenge space at 256 bits for production-sized groups). Verification honesty: - protocol: VerifyTally now actually calls zkp.VerifySchnorrProof and returns the true aggregate result instead of an unconditional true. - protocol: persist the padded mix input (event.MixInput) so the verifier checks shuffle 0 against the same padding the tally used (fixes false INVALID for N<2). Other correctness: - kdf: length-prefix BuildKDFInfo parts so the info encoding is injective. - math: GqElementFromSquareRoot accepts the valid root q (off-by-one that could panic in HashAndSquare); RandomGqElement samples the full canonical range. - cmd: validate demo --voters/--options instead of panicking on degenerate values. - protocol: use crypto/rand in the demo driver (drop the last math/rand import). Transport security (new): pkg/transportsec exposes Ed25519 signatures and X25519 ECDH — implemented in Rust (rust/transportsec: ed25519-dalek, x25519-dalek), linked into Go via cgo. No RSA. Cross-language conformance test proves the Rust Ed25519 signatures interoperate with Go's crypto/ed25519. Makefile builds the Rust static lib before the Go binary. Tests: added unit/round-trip/tamper coverage for math, hash, elgamal, zkp, mixnet, kdf, returncodes, protocol (end-to-end), and the Rust FFI bridge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 12:42:34 +00:00
## 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
Due-diligence hardening + Rust transport-security layer Correctness/security review of the whole PoC, with fixes and regression tests. Cryptographic soundness: - mixnet: enforce the multi-exponentiation c_{B_m}=commit(0;0) check that was stubbed out with an empty if — without it a malicious mixer can prove a non-permutation shuffle. - zkp: derive all four Fiat-Shamir challenges via RecursiveHashToZq instead of a biased `hash mod q` (which also capped the challenge space at 256 bits for production-sized groups). Verification honesty: - protocol: VerifyTally now actually calls zkp.VerifySchnorrProof and returns the true aggregate result instead of an unconditional true. - protocol: persist the padded mix input (event.MixInput) so the verifier checks shuffle 0 against the same padding the tally used (fixes false INVALID for N<2). Other correctness: - kdf: length-prefix BuildKDFInfo parts so the info encoding is injective. - math: GqElementFromSquareRoot accepts the valid root q (off-by-one that could panic in HashAndSquare); RandomGqElement samples the full canonical range. - cmd: validate demo --voters/--options instead of panicking on degenerate values. - protocol: use crypto/rand in the demo driver (drop the last math/rand import). Transport security (new): pkg/transportsec exposes Ed25519 signatures and X25519 ECDH — implemented in Rust (rust/transportsec: ed25519-dalek, x25519-dalek), linked into Go via cgo. No RSA. Cross-language conformance test proves the Rust Ed25519 signatures interoperate with Go's crypto/ed25519. Makefile builds the Rust static lib before the Go binary. Tests: added unit/round-trip/tamper coverage for math, hash, elgamal, zkp, mixnet, kdf, returncodes, protocol (end-to-end), and the Rust FFI bridge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 12:42:34 +00:00
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
Due-diligence hardening + Rust transport-security layer Correctness/security review of the whole PoC, with fixes and regression tests. Cryptographic soundness: - mixnet: enforce the multi-exponentiation c_{B_m}=commit(0;0) check that was stubbed out with an empty if — without it a malicious mixer can prove a non-permutation shuffle. - zkp: derive all four Fiat-Shamir challenges via RecursiveHashToZq instead of a biased `hash mod q` (which also capped the challenge space at 256 bits for production-sized groups). Verification honesty: - protocol: VerifyTally now actually calls zkp.VerifySchnorrProof and returns the true aggregate result instead of an unconditional true. - protocol: persist the padded mix input (event.MixInput) so the verifier checks shuffle 0 against the same padding the tally used (fixes false INVALID for N<2). Other correctness: - kdf: length-prefix BuildKDFInfo parts so the info encoding is injective. - math: GqElementFromSquareRoot accepts the valid root q (off-by-one that could panic in HashAndSquare); RandomGqElement samples the full canonical range. - cmd: validate demo --voters/--options instead of panicking on degenerate values. - protocol: use crypto/rand in the demo driver (drop the last math/rand import). Transport security (new): pkg/transportsec exposes Ed25519 signatures and X25519 ECDH — implemented in Rust (rust/transportsec: ed25519-dalek, x25519-dalek), linked into Go via cgo. No RSA. Cross-language conformance test proves the Rust Ed25519 signatures interoperate with Go's crypto/ed25519. Makefile builds the Rust static lib before the Go binary. Tests: added unit/round-trip/tamper coverage for math, hash, elgamal, zkp, mixnet, kdf, returncodes, protocol (end-to-end), and the Rust FFI bridge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 12:42:34 +00:00
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. See [ARCHITECTURE.md](ARCHITECTURE.md).
Due-diligence hardening + Rust transport-security layer Correctness/security review of the whole PoC, with fixes and regression tests. Cryptographic soundness: - mixnet: enforce the multi-exponentiation c_{B_m}=commit(0;0) check that was stubbed out with an empty if — without it a malicious mixer can prove a non-permutation shuffle. - zkp: derive all four Fiat-Shamir challenges via RecursiveHashToZq instead of a biased `hash mod q` (which also capped the challenge space at 256 bits for production-sized groups). Verification honesty: - protocol: VerifyTally now actually calls zkp.VerifySchnorrProof and returns the true aggregate result instead of an unconditional true. - protocol: persist the padded mix input (event.MixInput) so the verifier checks shuffle 0 against the same padding the tally used (fixes false INVALID for N<2). Other correctness: - kdf: length-prefix BuildKDFInfo parts so the info encoding is injective. - math: GqElementFromSquareRoot accepts the valid root q (off-by-one that could panic in HashAndSquare); RandomGqElement samples the full canonical range. - cmd: validate demo --voters/--options instead of panicking on degenerate values. - protocol: use crypto/rand in the demo driver (drop the last math/rand import). Transport security (new): pkg/transportsec exposes Ed25519 signatures and X25519 ECDH — implemented in Rust (rust/transportsec: ed25519-dalek, x25519-dalek), linked into Go via cgo. No RSA. Cross-language conformance test proves the Rust Ed25519 signatures interoperate with Go's crypto/ed25519. Makefile builds the Rust static lib before the Go binary. Tests: added unit/round-trip/tamper coverage for math, hash, elgamal, zkp, mixnet, kdf, returncodes, protocol (end-to-end), and the Rust FFI bridge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 12:42:34 +00:00
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/ 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
```
Due-diligence hardening + Rust transport-security layer Correctness/security review of the whole PoC, with fixes and regression tests. Cryptographic soundness: - mixnet: enforce the multi-exponentiation c_{B_m}=commit(0;0) check that was stubbed out with an empty if — without it a malicious mixer can prove a non-permutation shuffle. - zkp: derive all four Fiat-Shamir challenges via RecursiveHashToZq instead of a biased `hash mod q` (which also capped the challenge space at 256 bits for production-sized groups). Verification honesty: - protocol: VerifyTally now actually calls zkp.VerifySchnorrProof and returns the true aggregate result instead of an unconditional true. - protocol: persist the padded mix input (event.MixInput) so the verifier checks shuffle 0 against the same padding the tally used (fixes false INVALID for N<2). Other correctness: - kdf: length-prefix BuildKDFInfo parts so the info encoding is injective. - math: GqElementFromSquareRoot accepts the valid root q (off-by-one that could panic in HashAndSquare); RandomGqElement samples the full canonical range. - cmd: validate demo --voters/--options instead of panicking on degenerate values. - protocol: use crypto/rand in the demo driver (drop the last math/rand import). Transport security (new): pkg/transportsec exposes Ed25519 signatures and X25519 ECDH — implemented in Rust (rust/transportsec: ed25519-dalek, x25519-dalek), linked into Go via cgo. No RSA. Cross-language conformance test proves the Rust Ed25519 signatures interoperate with Go's crypto/ed25519. Makefile builds the Rust static lib before the Go binary. Tests: added unit/round-trip/tamper coverage for math, hash, elgamal, zkp, mixnet, kdf, returncodes, protocol (end-to-end), and the Rust FFI bridge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 12:42:34 +00:00
## 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