swisspost-evoting-go-poc/pkg/party/voting_test.go
saymrwulf a443e44875 Voting phase: ballot submission with cross-party proof verification
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>
2026-07-06 15:19:07 +02:00

82 lines
2.5 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)
ct := elgamal.Encrypt(elgamal.NewMessage(emath.GqVectorOf(voteElem)), emath.RandomZqElement(zq), singlePK(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"},
})
}