swisspost-evoting-go-poc/pkg/party/voting_test.go
saymrwulf 23723fddd6 Tally + verification: full multi-party ceremony runs end-to-end
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>
2026-07-06 15:23:56 +02:00

87 lines
2.7 KiB
Go

package party
import (
"testing"
"github.com/user/evote/pkg/elgamal"
emath "github.com/user/evote/pkg/math"
"github.com/user/evote/pkg/returncodes"
"github.com/user/evote/pkg/transport"
)
func runToVoting(t *testing.T, voters, options int) *Ceremony {
t.Helper()
cfg := testConfig(t, voters, options)
c, err := NewCeremony(cfg, nil)
if err != nil {
t.Fatalf("NewCeremony: %v", err)
}
if err := c.RunSetup(); err != nil {
t.Fatalf("RunSetup: %v", err)
}
if err := c.RunCards(); err != nil {
t.Fatalf("RunCards: %v", err)
}
return c
}
// TestRunVotingBallotFlow submits ballots and confirms each is CC-verified and
// stored with its vcPK persisted.
func TestRunVotingBallotFlow(t *testing.T) {
c := runToVoting(t, 4, 3)
sel := [][]int{{0}, {1}, {1}, {2}}
if err := c.RunVoting(sel); err != nil {
t.Fatalf("RunVoting: %v", err)
}
if got := len(c.Server.st.ballotBox); got != 4 {
t.Fatalf("ballot box has %d ballots, want 4", got)
}
for i, b := range c.Server.st.ballotBox {
if b.VcPK.Value() == nil {
t.Fatalf("ballot %d did not persist vcPK", i)
}
}
}
// TestBallotWithBadProofRejected confirms a ballot whose exponentiation proof
// does not match is rejected by the CCs (and the server does not store it),
// without any panic.
func TestBallotWithBadProofRejected(t *testing.T) {
c := runToVoting(t, 1, 3)
voter := c.Voters[0]
// Build a valid ballot, then corrupt the proof before submitting.
if _, err := voter.castBallotTampered(t); err == nil {
t.Fatal("server accepted a ballot with a tampered proof")
}
if len(c.Server.st.ballotBox) != 0 {
t.Fatal("tampered ballot was stored")
}
}
// castBallotTampered submits a structurally valid ballot (real ciphertext,
// well-formed group elements) but with a zeroed exponentiation proof that cannot
// verify. Used only by the test above.
func (p *VoterClient) castBallotTampered(t *testing.T) (*transport.Envelope, error) {
t.Helper()
group := p.cer.Config.Group
zq := emath.ZqGroupFromGqGroup(group)
product := returncodes.EncodeVote([]int{0}, p.st.primes)
voteElem, _ := emath.NewGqElement(product, group)
msgElems := make([]emath.GqElement, p.cer.Config.NumOptions)
msgElems[0] = voteElem
for i := 1; i < len(msgElems); i++ {
msgElems[i] = group.Identity()
}
ct := elgamal.Encrypt(elgamal.NewMessage(emath.GqVectorOf(msgElems...)), emath.RandomZqElement(zq), p.st.electionPK)
g := group.Generator().Value().String()
return p.cer.send(p.id, NameServer, MsgCastBallot, wireBallot{
VoterID: p.st.card.VoterID,
VcID: p.st.card.VerificationCardID,
Ciphertext: encodeCiphertext(ct),
ExponentiatedG: g,
ExponentiatedP: g,
VcPK: g,
ExpProof: wireSchnorr{E: "0", Z: "0"},
})
}