swisspost-evoting-go-poc/pkg/party/voting.go

211 lines
7.2 KiB
Go
Raw Normal View History

package party
import (
"fmt"
"github.com/user/evote/pkg/elgamal"
"github.com/user/evote/pkg/hash"
emath "github.com/user/evote/pkg/math"
"github.com/user/evote/pkg/returncodes"
"github.com/user/evote/pkg/transport"
"github.com/user/evote/pkg/zkp"
)
// Voting-phase message types.
const (
MsgCastBallot = "cast-ballot" // voter -> server
MsgBallotStored = "ballot-stored" // server -> voter
MsgVerifyBallot = "verify-ballot" // server -> CCj
MsgBallotVerdict = "ballot-verdict"
)
// wireBallot is a submitted ballot. It carries the encrypted vote, the
// exponentiated ciphertext, the verification-card public key (persisted so a
// remote party can reconstruct the proof statement — finding F6), and the
// exponentiation proof binding vcPK to the exponentiation.
type wireBallot struct {
VoterID string `json:"voter_id"`
VcID string `json:"vc_id"`
Ciphertext wireCiphertext `json:"ciphertext"`
ExponentiatedG string `json:"exp_gamma"`
ExponentiatedP string `json:"exp_phi0"`
VcPK string `json:"vc_pk"`
ExpProof wireSchnorr `json:"exp_proof"`
}
// RunVoting has every voter encrypt its selection and submit a ballot; the
// server routes each ballot to all CCs for proof verification and stores the
// accepted ballots. Selections are provided per voter (indexed by voter).
func (c *Ceremony) RunVoting(selections [][]int) error {
c.logf("\n--- VOTING PHASE (multi-party) ---")
for v, voter := range c.Voters {
sel := []int{0}
if v < len(selections) && len(selections[v]) > 0 {
sel = selections[v]
}
if _, err := voter.castBallot(sel); err != nil {
return fmt.Errorf("voter %d cast: %w", v, err)
}
}
c.logf(" voting: %d ballots submitted and accepted", len(c.Voters))
return nil
}
// castBallot (voter) encrypts the selection, builds the exponentiation proof,
// and submits the ballot to the server.
func (p *VoterClient) castBallot(selected []int) (*transport.Envelope, error) {
cfg := p.cer.Config
group := cfg.Group
zq := emath.ZqGroupFromGqGroup(group)
if p.st.card == nil {
return nil, fmt.Errorf("%s has no voting card", p.id.Name)
}
p.st.selected = selected
// 1. Encode + encrypt the vote. The message has NumOptions components (vote
// product in slot 0, identity elsewhere) so the ciphertext width matches
// the CC/EB key size used to decrypt during the mix-net.
product := returncodes.EncodeVote(selected, p.st.primes)
voteElem, err := emath.NewGqElement(product, group)
if err != nil {
return nil, fmt.Errorf("vote product not in group: %w", err)
}
msgElems := make([]emath.GqElement, cfg.NumOptions)
msgElems[0] = voteElem
for i := 1; i < cfg.NumOptions; i++ {
msgElems[i] = group.Identity()
}
ct := elgamal.Encrypt(elgamal.NewMessage(emath.GqVectorOf(msgElems...)), emath.RandomZqElement(zq), p.st.electionPK)
// 2. Verification-card key pair.
p.st.vcSK = emath.RandomZqElement(zq)
vcPK := group.Generator().Exponentiate(p.st.vcSK)
// 3. Exponentiated ciphertext components (E1_tilde).
gammaExp := ct.Gamma.Exponentiate(p.st.vcSK)
phi0Exp := ct.GetPhi(0).Exponentiate(p.st.vcSK)
// 4. Exponentiation proof: knowledge of vcSK s.t. (vcPK,gammaExp,phi0Exp) are
// (g,gamma,phi0) raised to the same vcSK. This binds the ballot to vcPK.
bases := emath.GqVectorOf(group.Generator(), ct.Gamma, ct.GetPhi(0))
exps := emath.GqVectorOf(vcPK, gammaExp, phi0Exp)
expProof := zkp.GenExponentiationProof(bases, p.st.vcSK, exps, group,
hash.HashableString{Value: cfg.ElectionID},
hash.HashableString{Value: p.st.card.VerificationCardID},
)
ballot := wireBallot{
VoterID: p.st.card.VoterID,
VcID: p.st.card.VerificationCardID,
Ciphertext: encodeCiphertext(ct),
ExponentiatedG: gammaExp.Value().String(),
ExponentiatedP: phi0Exp.Value().String(),
VcPK: vcPK.Value().String(),
ExpProof: encodeExponentiation(expProof),
}
return p.cer.send(p.id, NameServer, MsgCastBallot, ballot)
}
// handleCastBallot (server) validates a ballot's structure, asks every CC to
// verify its exponentiation proof, and stores it on unanimous acceptance.
func (p *VotingServer) handleCastBallot(env *transport.Envelope) (*transport.Envelope, error) {
var b wireBallot
if err := transport.UnmarshalPayload(env.Payload, &b); err != nil {
return nil, err
}
group := p.cer.Config.Group
// Decode + validate all group elements (rejects malformed ballots cleanly).
ct, err := b.Ciphertext.decode(group)
if err != nil {
return nil, fmt.Errorf("ballot ciphertext: %w", err)
}
vcPK, err := strToGq(b.VcPK, group)
if err != nil {
return nil, fmt.Errorf("ballot vcPK: %w", err)
}
if _, err := strToGq(b.ExponentiatedG, group); err != nil {
return nil, fmt.Errorf("ballot exp gamma: %w", err)
}
if _, err := strToGq(b.ExponentiatedP, group); err != nil {
return nil, fmt.Errorf("ballot exp phi0: %w", err)
}
// Route to every CC for proof verification; require unanimous acceptance.
for j := 0; j < p.cer.Config.NumCCs; j++ {
env2, err := p.cer.send(p.id, CCName(j), MsgVerifyBallot, b)
if err != nil {
return nil, fmt.Errorf("server->cc%d verify: %w", j, err)
}
var verdict ballotVerdict
if err := transport.UnmarshalPayload(env2.Payload, &verdict); err != nil {
return nil, err
}
if !verdict.Accept {
return nil, fmt.Errorf("cc%d rejected ballot %s: %s", j, b.VcID, verdict.Reason)
}
}
// Store the ballot (persisting vcPK, finding F6).
p.st.ballotBox = append(p.st.ballotBox, encryptedBallot{
VoterID: b.VoterID,
VerificationCardID: b.VcID,
Ciphertext: ct,
VcPK: vcPK,
})
return reply(p.id, env.From, MsgBallotStored, env.Nonce, ackPayload{Party: p.id.Name, OK: true})
}
type ballotVerdict struct {
Accept bool `json:"accept"`
Reason string `json:"reason"`
}
// handleVerifyBallot (CC) re-derives the exponentiation-proof statement and
// verifies it. A malformed proof yields a clean reject, never a panic.
func (p *ControlComponent) handleVerifyBallot(env *transport.Envelope) (*transport.Envelope, error) {
var b wireBallot
if err := transport.UnmarshalPayload(env.Payload, &b); err != nil {
return nil, err
}
cfg := p.cer.Config
group := cfg.Group
zq := emath.ZqGroupFromGqGroup(group)
reject := func(reason string) (*transport.Envelope, error) {
return reply(p.id, env.From, MsgBallotVerdict, env.Nonce, ballotVerdict{Accept: false, Reason: reason})
}
ct, err := b.Ciphertext.decode(group)
if err != nil {
return reject("bad ciphertext")
}
vcPK, err := strToGq(b.VcPK, group)
if err != nil {
return reject("bad vcPK")
}
gammaExp, err := strToGq(b.ExponentiatedG, group)
if err != nil {
return reject("bad exp gamma")
}
phi0Exp, err := strToGq(b.ExponentiatedP, group)
if err != nil {
return reject("bad exp phi0")
}
proof, err := b.ExpProof.decodeExponentiation(zq)
if err != nil {
return reject("bad proof encoding")
}
bases := emath.GqVectorOf(group.Generator(), ct.Gamma, ct.GetPhi(0))
exps := emath.GqVectorOf(vcPK, gammaExp, phi0Exp)
ok := zkp.VerifyExponentiationProof(bases, exps, proof, group,
hash.HashableString{Value: cfg.ElectionID},
hash.HashableString{Value: b.VcID},
)
if !ok {
return reject("exponentiation proof INVALID")
}
return reply(p.id, env.From, MsgBallotVerdict, env.Nonce, ballotVerdict{Accept: true})
}