mirror of
https://github.com/saymrwulf/swisspost-evoting-go-poc.git
synced 2026-09-04 20:23:55 +00:00
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>
209 lines
7.2 KiB
Go
209 lines
7.2 KiB
Go
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 under the election public key.
|
|
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)
|
|
}
|
|
ct := elgamal.Encrypt(elgamal.NewMessage(emath.GqVectorOf(voteElem)), emath.RandomZqElement(zq), singlePK(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)
|
|
}
|
|
|
|
// singlePK returns a single-element public key (index 0) for encrypting the vote
|
|
// product as a one-component message.
|
|
func singlePK(pk elgamal.PublicKey) elgamal.PublicKey {
|
|
return elgamal.PublicKey{Elements: emath.GqVectorOf(pk.Elements.Get(0))}
|
|
}
|
|
|
|
// 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})
|
|
}
|