Take the live-math cockpit down to the level of the Swiss Post crypto-primitives
class structure. The shuffle proof is no longer one line — you can now watch it
being constructed:
- Pedersen matrix commitment (CommitmentService analog): c_A = Comm(A; r),
c_{A,j} = h^{r_j} Π g_i^{A_ij}, emitted from CommitMatrix.
- All five Bayer-Groth sub-arguments, mirroring the *ArgumentService classes:
ShuffleArgument (composition + x,y,z challenges), ProductArgument,
HadamardArgument (entrywise product), ZeroArgument (bilinear star-map),
SingleValueProductArgument, MultiExponentiationArgument — each emits its
defining relation as LaTeX with live dimensions.
- Partial decryption + decryption proof (DecryptionProofService analog):
φ'_i = φ_i·γ_i^{-sk} with the ZK proof that log_g(pk) = log_γ(γ^sk).
New trace.KindArgument. Low-level Commit stays uninstrumented (called in
verification too — would flood the stream); CommitMatrix is the semantic step.
Test: a 6-voter ceremony (N=6 → 2×3 shuffle matrix, so m>1 and the full argument
tree runs) captures 345 live events across 8 kinds, and asserts all five named
sub-arguments appear.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The terminal systems-view half of the cockpit, fed by the SAME trace stream as
the browser math view — no double-maintenance.
- cockpit_tmux.go: spawns a tmux session with one pane per stakeholder (setup,
CC0–CC3, electoral board, voting server, verifier, and an aggregate voters
pane), each running `evote panelview`. The ceremony runs in the background,
appending paced NDJSON events to a shared temp file the panes tail. Guards for
missing tmux and for being run inside an existing tmux session.
- panelview.go: a hidden subcommand each pane runs — follows the shared event
file, filters to its stakeholder, and renders that party's operations as
colored ASCII/Unicode math with elided live values.
- Attribute setup-phase Schnorr challenges to the generating CC and verify-phase
challenges to the verifier (trace.SetContext), so every pane gets its ops.
Verified: panelview filters and renders correctly per role; a 9-pane tiled tmux
session builds with the real pane commands. (Interactive attach needs a TTY, so
the live attach is exercised when you run it.)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The differentiator. A single self-contained browser page (no libraries, offline)
renders each real cryptographic operation as typeset mathematics the instant it
runs, with the actual runtime values:
- E_1 = (γ, φ) = (g^r, pk^r·m), r ← Z_q (ElGamal ballot encryption)
- e = H((p,q,g), y, c, h_aux) mod q (Fiat-Shamir challenge)
- C' = { ReEnc_pk(C_π(i); ρ_i) } (Bayer-Groth verifiable shuffle)
- σ ← Ed25519.Sign_sk(SHA256(envelope)) (transport signature)
- s = a·B = b·A ∈ X25519, k = SHA256(…) (X25519 key agreement)
Math is rendered via a focused LaTeX→native-MathML converter written for exactly
the notation the instrumentation emits — so it works in any modern browser with
zero dependencies and nothing to ship. Unknown tokens fall back to literal text,
never crashing the view.
`evote cockpit` starts an HTTP server; on page connect it runs one full multi-
party ceremony, streaming every crypto event over SSE with configurable pacing
(--delay) so a human can follow along. A stakeholder sidebar highlights the
acting party; a phase timeline tracks setup→cards→voting→tally→verify; each op
shows its live values as expandable, copyable chips.
Verified in a real browser: all five operation kinds render correctly (96 sign,
36 challenge, 6 keyex, 2 encrypt, 5 shuffle in a 2-voter run), no console errors,
ceremony completes and verifies.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The running cryptography now narrates itself. Each headline operation emits a
trace.Event carrying its LaTeX notation plus the actual runtime values, the
instant it executes:
- Ed25519 signature on every inter-party message (envelope.Seal)
- X25519 ECDH key agreement for confidential card delivery (NewSecureChannel)
- ElGamal ballot encryption E1 = (g^r, pk^r·m) with the real r (castBallot)
- Fiat-Shamir challenge e = H(...) mod q (Schnorr proof)
- Bayer-Groth verifiable shuffle C' = {ReEnc_pk(C_π(i))} with N (mix-net)
Ceremony phases set trace phase/party context so events are attributed to the
acting stakeholder and phase. Instrumentation is behind the enabled-check, so
normal runs pay nothing.
Test: a full traced ceremony captures 184 live events across all five headline
kinds, each with non-empty LaTeX and live values, correctly phase/party-tagged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
The final stale-claim sweep caught index.html's embedded operations manual
(intro paragraph + comparison table still said ~6,500 lines / no-RSA row missing)
and the swe deck title. Update both, add the signatures/key-exchange row to the
index comparison table, and re-sync the root standalone decks. Repo-wide sweep
for stale claims is now clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The root presentation*.html were older, divergent copies that predated the
multi-party work. Sync each to its canonical web deck (demo/crypto/swe) so the
standalone browsable copies carry the same multi-party + Rust + cast-as-intended
content. manual.html: correct stale claims (LOC, Go 1.21→1.25 + Rust, 'no network
access'), and refresh the production-vs-PoC table (netdemo party separation,
Ed25519/X25519 no-RSA row, Rust deps).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Landing cards now mention the multi-party re-architecture, Rust transport layer,
and cast-as-intended, with corrected slide counts. Operations manual: replace
the 'no network / same machine' framing with the netdemo trust-boundary
explanation, update prerequisites (Go 1.25 + Rust, make build), and fix the
'no network access required' line (netdemo's signed transport runs in-process).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New Part IV½ shows the return-code cast-as-intended mechanism (E2 + plaintext-
equality proof, CC exponentiation + joint decryption) and the transport-security
layer (Ed25519/X25519, Ed25519 X.509 PKI, implemented in Rust) — so the deck's
existing "cast-as-intended" claim is now backed by the actual construction.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New section covers splitting the parties, the Rust transport-security layer
(Ed25519/X25519 over cgo, no RSA), the validated wire boundary, and cast-as-
intended return codes. Also corrects the error-handling slide (the netdemo mode
does cross a real trust boundary) and notes the Rust dependency on the deps
slide. Verified: deck serves 200 and the script balances.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
ARCHITECTURE: replace the "return codes simulated" scope limit with the
cast-as-intended design (E2 + plaintext-equality proof, CC exponentiation +
decryption, voter check, substitution-attack test) and document the remaining
honest limits (single-选option; return-code lookup lets the server infer the
selection — a privacy simplification that does not affect tally secrecy).
README: note that netdemo return codes are cast-as-intended.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The return code the voter checks is now genuinely computed by the CCs from the
submitted ciphertext, not looked up from the card:
- returncode_extract.go: after a ballot is accepted, the server asks each CC to
exponentiate E2 by its return-code key (product over CCs = Enc(vote^Σk)), then
each CC contributes a partial-decryption factor; the server recovers vote^Σk,
which equals the card base prime_sel^Σk, and looks up the short code.
- The server returns that code to the voter, who checks it against the card for
the chosen option; a mismatch aborts with a clear error.
Soundness test: a malicious client that encrypts option A for the tally (E1) but
option B in the return-code channel (E2) is REJECTED by the plaintext-equality
proof — so the code shown always reflects the tallied vote. This closes the
cast-as-intended gap (the old return codes were decorative, finding F16).
Card lCC now uses a fixed tau so extraction can recompute it without learning
the option up front.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The voter now also submits E2 = Enc(vote, returnCodesPK[0]) and a plaintext-
equality proof that E2 and the ballot's slot 0 encrypt the SAME vote. Every CC
verifies this proof during ballot verification. This is the soundness link that
makes the return code cast-as-intended: a client that encrypts one vote for the
tally and a different one for the return-code channel is rejected, so the code
the CCs compute from E2 necessarily reflects the tallied vote.
- transcript: publish the combined return-codes public key.
- voter stores returnCodePK from the (confidential) card delivery.
- wire: plaintext-equality proof DTO.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Changes the choice-code base from HashAndSquare(prime_i) to the encoding prime
itself (a G_q element). The card code becomes prime_i^{Σ_j k_j}, which is
algebraic and therefore recomputable by the CCs directly from the submitted
ciphertext at vote time — the prerequisite for a genuine cast-as-intended
return-code path. Setup and (upcoming) vote-time extraction use the same base,
keeping them consistent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The plaintext-equality primitive itself is sound: statement (γ1, γ2, φ1/φ2)
with witnesses (r0, r1) correctly proves two ciphertexts encrypt the same
plaintext under different keys. Finding F5 was a caller error in the old vote
path, not a defect here. This test pins the correct contract (accepts equal
plaintexts, rejects different plaintexts and mismatched aux) so the upcoming
cast-as-intended return-code path can rely on it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
README: add pkg/transport + pkg/party to the component table and project tree,
document `evote netdemo` alongside `demo`, point to ARCHITECTURE.md.
ARCHITECTURE: document the implemented message flow per phase; the design
decision (ciphertexts cross the signed transport, proofs go to the bulletin-board
transcript); which due-diligence findings were folded in (M4/F6/F7/F8/F12); and
honest scope limits (F5 not reimplemented; return codes derived, not homomorphic
over the ciphertext, per F16 — but with the F1 derivation mismatch fixed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New subcommand runs the entire election as separate parties over the
Rust-signed transport, printing per-phase progress and the verified message
count. --verbose logs every signed envelope (from -> to : type, signature OK).
--voters/--options are validated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The mix-net now runs across separate parties over the signed transport: the
server pads the ballot box and hands it to CC0; each CC shuffles + partially
decrypts and passes the (validated) ciphertexts to the next; the electoral
board performs the final shuffle + decryption. Ciphertext handoffs cross the
authenticated transport; each party posts its shuffle and decryption proofs to
the public transcript (the bulletin board).
- tally.go: RunTally orchestration + per-party handlers (server pad, CC shuffle,
EB final decrypt). Persists the padded mix input and per-stage partial
decrypts to the transcript (fixes F7/F8 in the multi-party setting).
- verify.go: RunVerify has the verifier independently re-check every CC Schnorr
proof and the whole shuffle chain from the transcript alone (no secrets).
- returncodes: DecodeVoteChecked returns an error instead of panicking on a
non-smooth plaintext (fixes F12), used on the tally path so a corrupt ballot
is counted as spoiled rather than crashing the tally.
Tests: the full ceremony (setup -> cards -> voting -> tally -> verify) produces
the correct tally over 124 verified transport messages; the verifier rejects a
transcript with swapped Schnorr proofs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Voters encrypt their selection under the election key, build the (sound)
exponentiation proof binding the ballot to their verification-card key, and
submit to the voting server. The server validates every group element on
receipt, routes the ballot to all four CCs for proof verification, and stores
it only on unanimous acceptance — persisting vcPK (finding F6) so the proof
statement is reconstructible by any party.
- voting.go: castBallot (voter), handleCastBallot (server), handleVerifyBallot
(CC). The CC re-derives the proof statement and verifies it; a malformed proof
or bad group element yields a clean reject, never a panic (the trust-boundary
hardening deferred from the due-diligence pass).
- wire.go: exponentiation-proof DTO.
Tests: 4 ballots flow end-to-end and are stored with vcPK; a ballot with a
zeroed proof is rejected by the CCs and never stored.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Setup now generates each voter's return-code card by collecting shares from all
four CCs over the bus (the GenEncLongCodeShares exchange) and distributes cards
and the mapping table over CONFIDENTIAL channels.
- codes.go: deriveReturnCodeKey is the SINGLE key-derivation function used by a
CC both when contributing to card assembly and (later) at vote-time extraction
— structurally preventing the setup/extraction derivation mismatch (F1). Each
CC computes its choice/confirm shares from its private return-code secret;
only the shares (validated as G_q members on decode) cross the bus.
- RunCards assembles cards, registers mapping-table entries, and delivers cards
to voters + the mapping table to the server via sendConfidential (X25519 ECDH
session key + AES-256-GCM, then Ed25519-signed) — exercising the secure
channel in the ceremony, not just in tests.
- returncodes: MappingTable Export/ImportMappingTable for transport.
Test confirms every voter receives its card confidentially with the right code
count and the server receives the full mapping table + public election keys.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
pkg/party models each endpoint of the system as a separate object holding only
its own private state, wired together through the transport bus.
- ceremony.go: NewCeremony bootstraps the Ed25519 root CA, enrolls all parties
(setup, 4 CCs, electoral board, voting server, verifier, N voters) with
CA-signed identity certs, registers each in the directory, and wires its
handler into the bus.
- parties.go: the six party types and a shared hello/ack handshake; Handshake()
proves the full sign -> route -> verify -> reply -> verify path for every
party before any election logic runs.
- state.go: per-party private state structs (nothing shared across parties).
- transcript.go: PublicTranscript, the append-only bulletin board a remote
verifier will consume (no secrets).
- phases.go: phase handlers reject unknown message types cleanly (the transport
boundary never panics on unexpected input) — filled in over the next commits.
Transport CA API simplified to own its serial counter (NewCA/Issue).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Foundation for the multi-party re-architecture: an authenticated, confidential
message transport between the separate parties of the system.
- identity.go: per-party Ed25519 + X25519 identities and an Ed25519 root CA.
Certificates are real X.509, but signed through a crypto.Signer shim whose
Sign() calls the Rust Ed25519 — so x509.CreateCertificate's signature bytes
are produced in Rust. Verification extracts TBS bytes and calls the Rust
verifier, never Go's x509 internals. No RSA anywhere.
- envelope.go: signed inter-party messages over an injective length-prefixed
encoding of all fields; sign/verify via Rust.
- channel.go: X25519 ECDH (Rust) → session key → AES-256-GCM confidential
payloads.
- bus.go: CA-anchored directory + message router that verifies every request
and reply signature before delivery (authenticity enforced at the boundary).
Tests cover cert-chain verification (incl. foreign-CA rejection), envelope
tamper rejection, forged-sender rejection at the bus, and secure-channel
round-trip with associated-data binding.
ARCHITECTURE.md documents the parties, the transport, and the message inventory.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
Proof-of-concept reimplementation of the Swiss Post e-voting
cryptographic protocol in Go. Single binary, 52 source files,
2 dependencies. Covers ElGamal encryption, Bayer-Groth verifiable
shuffles, zero-knowledge proofs, return codes, and a full
election ceremony demo.