mirror of
https://github.com/saymrwulf/swisspost-evoting-go-poc.git
synced 2026-11-03 04:41:16 +00:00
Add CHANGELOG (five story arcs) + blog-post draft for the cockpit
CHANGELOG.md tells the project's evolution in five arcs — original PoC, due-diligence hardening, multi-party Rust-signed transport, cast-as-intended return codes, live crypto cockpit — with commit references, so the repo (and its mirrors) surface the story, not just the current state. docs/blog-draft-live-crypto-cockpit.md is a ready-to-publish draft for the offsite blog (Hugo front matter, teaser, house-style tone), covering the three developments since the February post with the cockpit as the headline. It lives in docs/ purely for versioning; the blog itself is external. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
a1e4ec6ce7
commit
3a2e24363f
2 changed files with 216 additions and 0 deletions
89
CHANGELOG.md
Normal file
89
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
# Changelog
|
||||
|
||||
The project's story in five arcs, newest first. Hashes reference this repo's history.
|
||||
|
||||
## Arc 5 — The Live Crypto Cockpit (2026-07-07)
|
||||
|
||||
*The cryptography narrates itself.* A surface-agnostic event stream (`pkg/trace`)
|
||||
lets every meaningful cryptographic operation emit its notation as LaTeX plus the
|
||||
**real runtime values**, the instant it executes — at zero cost when tracing is off.
|
||||
|
||||
- `evote cockpit` — browser view: each operation rendered as typeset mathematics
|
||||
(native MathML, no libraries, offline) with live values, a stakeholder sidebar,
|
||||
and a phase timeline. Auto-opens the browser; Replay button when done.
|
||||
(`1e0b286`, `a1e4ec6`)
|
||||
- `evote cockpit --tmux` — terminal view: one tmux pane per stakeholder, each
|
||||
streaming its own party's operations as colored ASCII math. Same event stream,
|
||||
two surfaces. (`b38b990`)
|
||||
- **Deep instrumentation to the Swiss Post class granularity** (`c246508`):
|
||||
Pedersen matrix commitments (`CommitmentService` analog), all five Bayer-Groth
|
||||
sub-arguments (`ShuffleArgument`, `ProductArgument`, `HadamardArgument`,
|
||||
`ZeroArgument`, `SingleValueProductArgument`, `MultiExponentiationArgument`),
|
||||
and partial decryption with proofs (`DecryptionProofService` analog). The
|
||||
shuffle proof is watched being *constructed*, not just referenced.
|
||||
- Verified in a real browser: a 6-voter run (2×3 shuffle matrix, so the full
|
||||
argument tree fires) renders ~345 live operations across 8 kinds, no errors.
|
||||
|
||||
## Arc 4 — Cast-as-Intended Return Codes (2026-07-06 → 07)
|
||||
|
||||
*The return code is computed from the ballot that is actually counted.*
|
||||
|
||||
- The ballot now carries `E2 = Enc(vote, returnCodesPK)` plus a **plaintext-
|
||||
equality proof** binding E2 to the tallied ciphertext; every control component
|
||||
verifies it. (`d2cfbf2`)
|
||||
- Return-code extraction: CCs exponentiate E2 by their per-voter keys and
|
||||
jointly decrypt, recovering `vote^Σk = prime_sel^Σk` → mapping-table lookup →
|
||||
code returned to the voter, who checks it against the card. (`127ae61`)
|
||||
- Soundness test: a malicious client encrypting option A for the tally but
|
||||
option B for the return-code channel is **rejected** — vote substitution is
|
||||
caught. Closes findings F5/F16; the F1 derivation mismatch is gone by
|
||||
construction (single key-derivation function).
|
||||
|
||||
## Arc 3 — Multi-Party Architecture over Rust-Signed Transport (2026-07-06)
|
||||
|
||||
*From one process simulating everyone to separate endpoints that trust nothing
|
||||
unsigned.*
|
||||
|
||||
- `rust/transportsec`: Ed25519 signatures + X25519 ECDH (ed25519-dalek,
|
||||
x25519-dalek), compiled to a C-ABI static library, called from Go via cgo.
|
||||
**No RSA anywhere** — including the X.509 CA, whose certificates are signed
|
||||
through a Rust-backed `crypto.Signer`. Cross-language test proves RFC 8032
|
||||
interop with Go's stdlib. (`ec4be74`, `f7cbe4f`)
|
||||
- `pkg/transport`: CA-anchored identity directory, signed envelopes over an
|
||||
injective encoding, X25519 secure channels, a bus that verifies every message
|
||||
before delivery. (`f7cbe4f`)
|
||||
- `pkg/party`: setup component, four control components, electoral board, voting
|
||||
server, voters, and verifier as separate endpoints holding only their own
|
||||
state; distributed key generation, confidential card delivery, CC-verified
|
||||
ballots, the CC→CC→EB mix-net cascade, and independent verification from the
|
||||
public transcript alone. (`313db92` → `23723fd`)
|
||||
- `evote netdemo` runs it end-to-end; `--verbose` logs every signed envelope.
|
||||
(`b1f5606`)
|
||||
|
||||
## Arc 2 — Due-Diligence Hardening (2026-07-06)
|
||||
|
||||
*A full correctness/security review of the original PoC, with fixes and
|
||||
regression tests.* (`ec4be74`)
|
||||
|
||||
- **Critical**: the multi-exponentiation verifier's `c_{B_m} = commit(0;0)` check
|
||||
was stubbed out — a malicious mixer could prove a non-permutation "shuffle".
|
||||
Now enforced.
|
||||
- Fiat-Shamir challenges switched from biased `hash mod q` to the spec-correct
|
||||
oversample-then-reduce (`RecursiveHashToZq`).
|
||||
- `VerifyTally` actually verifies (it used to return `true` unconditionally);
|
||||
mix padding persisted so small-N verification checks the real input.
|
||||
- KDF domain separation made injective; `[1, q]` square-root off-by-one fixed;
|
||||
CLI flag validation; `crypto/rand` everywhere.
|
||||
- Test suite added across math, hash, elgamal, zkp, mixnet, kdf, returncodes,
|
||||
protocol, and the Rust FFI bridge.
|
||||
|
||||
## Arc 1 — The Original PoC (2026-02-13)
|
||||
|
||||
*The cryptographic core of Switzerland's internet voting system, distilled into
|
||||
a single Go binary.* (`e8b6f30`, `37072a5`)
|
||||
|
||||
- ElGamal over G_q (safe prime p = 2q+1), Schnorr / exponentiation / decryption /
|
||||
plaintext-equality proofs, the Bayer-Groth verifiable shuffle with its full
|
||||
sub-argument tree, return-code mapping tables, Argon2id + HKDF + AES-GCM.
|
||||
- `evote demo` (full ceremony), `evote present` (terminal walkthrough),
|
||||
`evote serve` (embedded, iPad-optimized lecture decks).
|
||||
127
docs/blog-draft-live-crypto-cockpit.md
Normal file
127
docs/blog-draft-live-crypto-cockpit.md
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
---
|
||||
title: "The cryptography narrates itself: a live-math cockpit for the Swiss Post e-voting PoC"
|
||||
date: 2026-07-07
|
||||
tags: ["e-voting", "zero-knowledge", "bayer-groth", "go", "rust", "mathml", "pedagogy"]
|
||||
draft: false
|
||||
---
|
||||
|
||||
<!--
|
||||
DRAFT for blog.zkdefi.org — written to follow the February post
|
||||
"A 52-file Go re-implementation of Swiss Post's e-voting protocol".
|
||||
Copy into the blog's content/posts/ directory (Hugo) and adjust front matter
|
||||
to taste. Teaser suggestion for the listing:
|
||||
|
||||
The Go re-implementation of Swiss Post's e-voting protocol grew up: it now
|
||||
runs as mutually distrusting parties over Rust-signed transport, its return
|
||||
codes are genuinely cast-as-intended, and — the headline — every cryptographic
|
||||
operation renders itself as typeset mathematics with live values, the instant
|
||||
it executes.
|
||||
-->
|
||||
|
||||
Back in February I wrote about distilling Swiss Post's internet voting protocol
|
||||
— 14 Java repositories, 500K+ lines — into a small Go binary. That post ended
|
||||
where most reimplementations end: the ceremony runs, the proofs verify, the
|
||||
tally is right. Since then the project has grown in three directions, and the
|
||||
third one is the reason for this post.
|
||||
|
||||
## First: it became honest about trust
|
||||
|
||||
The original PoC was faithful to the *protocol* but not to the *trust model* —
|
||||
every party (the setup component, four control components, the electoral board,
|
||||
the voting server, the voters, the verifier) lived as fields on one shared Go
|
||||
struct. Anyone could read anyone's secret key; nothing stopped them but
|
||||
politeness.
|
||||
|
||||
Now each party is a separate endpoint holding only its own private state, and
|
||||
everything that crosses between them travels as an **Ed25519-signed envelope**
|
||||
over a message bus that verifies every signature before delivery. Voting cards
|
||||
are delivered confidentially under **X25519-derived session keys**. Identities
|
||||
are real X.509 certificates from a root CA — and there is **no RSA anywhere**,
|
||||
including the CA: certificate signatures are produced through a Go
|
||||
`crypto.Signer` shim that forwards to Rust.
|
||||
|
||||
That's the second point worth pausing on: all transport cryptography lives in a
|
||||
small **Rust** crate (`ed25519-dalek`, `x25519-dalek`) behind a five-function C
|
||||
ABI, linked into Go via cgo. The election mathematics — ElGamal over a
|
||||
safe-prime group, the zero-knowledge proofs, the Bayer-Groth shuffle — remains
|
||||
hand-built Go, because understanding every line of the *protocol* is the point.
|
||||
But hand-rolling Ed25519 would be reckless; audited constant-time
|
||||
implementations are exactly what one should depend on. A cross-language test
|
||||
keeps both sides honest: Go's `crypto/ed25519` must verify Rust's signatures
|
||||
and vice versa, from the same seed.
|
||||
|
||||
## Second: the return codes stopped being decorative
|
||||
|
||||
Swiss voters check short **return codes** against a printed card to detect
|
||||
malware that swaps their vote after encryption. In the original PoC (and, to be
|
||||
fair, in an early reading of the real system's structure) the displayed code
|
||||
came *from the card* — cast-as-intended theater rather than cast-as-intended.
|
||||
|
||||
Now the ballot carries a second ciphertext `E2 = Enc(vote, returnCodesPK)` and
|
||||
a **plaintext-equality proof** that `E2` and the tallied ballot encrypt the
|
||||
same vote. Every control component verifies that proof before accepting the
|
||||
ballot. Then the CCs exponentiate `E2` by their per-voter secret keys and
|
||||
jointly decrypt, recovering `vote^Σk` — which equals the card's precomputed
|
||||
base `prime_selected^Σk` — and the resulting short code goes back to the voter.
|
||||
A unit test drives the actual attack (encrypt option A for the tally, option B
|
||||
for the return-code channel) and watches the equality proof reject it. The code
|
||||
you check is now derived from the vote that is actually counted.
|
||||
|
||||
## Third, the headline: watch the mathematics execute
|
||||
|
||||
Here is the thing I have not seen elsewhere. Run
|
||||
|
||||
```
|
||||
./evote cockpit
|
||||
```
|
||||
|
||||
and your browser opens on a page where **every cryptographic operation renders
|
||||
itself as typeset mathematics with its real runtime values, the instant the
|
||||
code executes**. Not an animation of the protocol. A readout of it.
|
||||
|
||||
When a voter's client encrypts, you see
|
||||
|
||||
> *E₁ = (γ, φ) = (g^r, pk^r·m), r ←$ ℤ_q*
|
||||
|
||||
with the actual 77-digit `r` and the actual encoded vote (a squared prime — you
|
||||
watch `m₀ = 25` scroll past and know someone picked option 3). When a control
|
||||
component builds its shuffle proof, the **Bayer-Groth argument tree assembles
|
||||
itself in construction order**: the Pedersen commitment to the permutation
|
||||
matrix, then the Product, Hadamard, Zero, Single-Value-Product and
|
||||
Multi-Exponentiation arguments, each with its defining relation —
|
||||
|
||||
> *∑ᵢ aᵢ ∗_y bᵢ = 0*, *b = a₁ ∘ a₂ ∘ ⋯ ∘ aₘ*, *C = (∏∏ Cᵢⱼ^{Aᵢⱼ})·ReEnc_pk(1;ρ)*
|
||||
|
||||
— and its live dimensions and values. Then the partial decryptions with their
|
||||
proofs, every Fiat-Shamir challenge, every Ed25519 transport signature. A
|
||||
sidebar highlights which stakeholder is acting; a timeline tracks
|
||||
setup → cards → voting → tally → verify. Prefer a terminal?
|
||||
`./evote cockpit --tmux` opens one pane per stakeholder, each narrating its own
|
||||
operations in ASCII math from the same event stream.
|
||||
|
||||
Two implementation notes for the curious. The math is **native browser MathML**
|
||||
— no KaTeX, no fonts, no network; a two-hundred-line LaTeX→MathML converter
|
||||
handles exactly the notation the instrumentation emits, and unknown tokens fall
|
||||
back to literal text so the view can never crash. And the instrumentation
|
||||
itself costs nothing when off: emitters sit behind an atomic check and build
|
||||
their event (including formatting those 77-digit values) only if a sink is
|
||||
subscribed, so `demo` and `netdemo` runs are untouched.
|
||||
|
||||
The pedagogical claim, made precisely: the sub-argument tree that the February
|
||||
post could only *diagram* is now something you *watch happen*, with the same
|
||||
granularity as the production system's class structure — `CommitmentService`,
|
||||
the five `*ArgumentService` classes, `DecryptionProofService` — because the Go
|
||||
packages mirror those classes one-to-one. The tree is not a picture of the
|
||||
code. It is the code, narrating itself.
|
||||
|
||||
## Honest limits
|
||||
|
||||
It remains a PoC: a 256-bit demo group (production uses 3072), one OS process
|
||||
(the parties are isolation-by-architecture, not by kernel), single-choice
|
||||
ballots on the return-code path, and the return-code lookup lets the server
|
||||
learn which entry matched — a privacy simplification the real system avoids by
|
||||
separating roles. The mix-net's tally secrecy is untouched by that caveat.
|
||||
|
||||
Code, tests, lecture decks and an operations manual:
|
||||
[github.com/saymrwulf/swisspost-evoting-go-poc](https://github.com/saymrwulf/swisspost-evoting-go-poc)
|
||||
(mirrored on this site). Build with `make build`; the cockpit is one command.
|
||||
Loading…
Reference in a new issue