diff --git a/pkg/party/codes.go b/pkg/party/codes.go index 21848b8..7b13733 100644 --- a/pkg/party/codes.go +++ b/pkg/party/codes.go @@ -163,8 +163,13 @@ func (p *VoterClient) handleVotingCard(env *transport.Envelope) (*transport.Enve if err != nil { return nil, err } + rcPK, err := d.ReturnCodePK.decode(group) + if err != nil { + return nil, err + } p.st.card = &d.Card p.st.electionPK = pk + p.st.returnCodePK = rcPK p.st.primes = make([]*big.Int, len(d.Primes)) for i, ps := range d.Primes { v, ok := new(big.Int).SetString(ps, 10) diff --git a/pkg/party/setup.go b/pkg/party/setup.go index dfe2d13..ecdce35 100644 --- a/pkg/party/setup.go +++ b/pkg/party/setup.go @@ -129,6 +129,7 @@ func (c *Ceremony) RunSetup() error { c.Transcript.ElectionID = cfg.ElectionID c.Transcript.EBPublicKey = ebPK c.Transcript.ElectionPK = c.Setup.st.electionPK + c.Transcript.ReturnCodePK = c.Setup.st.returnCodePK c.Transcript.Primes = make([]string, len(c.Setup.st.primes)) for i, p := range c.Setup.st.primes { c.Transcript.Primes[i] = p.String() diff --git a/pkg/party/state.go b/pkg/party/state.go index ff5e7ec..4ec21a7 100644 --- a/pkg/party/state.go +++ b/pkg/party/state.go @@ -52,11 +52,12 @@ type serverState struct { // voterState is a voter client's private card and ballot secrets. type voterState struct { - card *votingCard - electionPK elgamal.PublicKey - primes []*big.Int - vcSK emath.ZqElement - selected []int + card *votingCard + electionPK elgamal.PublicKey + returnCodePK elgamal.PublicKey + primes []*big.Int + vcSK emath.ZqElement + selected []int } // votingCard is a voter's credential bundle (private to the voter, produced by diff --git a/pkg/party/transcript.go b/pkg/party/transcript.go index c3284f5..c32ad35 100644 --- a/pkg/party/transcript.go +++ b/pkg/party/transcript.go @@ -18,6 +18,7 @@ type PublicTranscript struct { CCSchnorr [][]zkp.SchnorrProof // per-CC Schnorr proofs of key knowledge EBPublicKey elgamal.PublicKey // electoral board public key ElectionPK elgamal.PublicKey // combined election public key + ReturnCodePK elgamal.PublicKey // combined return-codes public key (CCs only, no EB) Primes []string // decimal encodings of the encoding primes // Tally artifacts. diff --git a/pkg/party/voting.go b/pkg/party/voting.go index cf1ad21..bb9f70b 100644 --- a/pkg/party/voting.go +++ b/pkg/party/voting.go @@ -21,16 +21,20 @@ const ( // 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. +// remote party can reconstruct the proof statement — finding F6), the +// exponentiation proof binding vcPK to the exponentiation, and the return-code +// ciphertext E2 with a plaintext-equality proof binding it to the ballot (so the +// return code is genuinely derived from the submitted vote — cast-as-intended). 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"` + 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"` + ReturnCodeCT wireCiphertext `json:"return_code_ct"` // E2: Enc(vote, returnCodesPK[0]) + EqProof wirePlaintextEquality `json:"eq_proof"` // proves E1[0] and E2 encrypt the same vote } // RunVoting has every voter encrypt its selection and submit a ballot; the @@ -75,7 +79,8 @@ func (p *VoterClient) castBallot(selected []int) (*transport.Envelope, error) { 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) + msgRandomness := emath.RandomZqElement(zq) + ct := elgamal.Encrypt(elgamal.NewMessage(emath.GqVectorOf(msgElems...)), msgRandomness, p.st.electionPK) // 2. Verification-card key pair. p.st.vcSK = emath.RandomZqElement(zq) @@ -94,6 +99,25 @@ func (p *VoterClient) castBallot(selected []int) (*transport.Envelope, error) { hash.HashableString{Value: p.st.card.VerificationCardID}, ) + // 5. Return-code ciphertext E2 and the plaintext-equality proof binding it + // to the ballot. E2 encrypts the same vote value (slot 0) under the + // return-codes key; the proof lets the CCs trust that the return code + // they compute from E2 reflects the actually-submitted vote. (Single + // selected option — the return-code channel is defined for one choice.) + rcPK0 := elgamal.PublicKey{Elements: emath.GqVectorOf(p.st.returnCodePK.Get(0))} + r2 := emath.RandomZqElement(zq) + e2 := elgamal.Encrypt(elgamal.NewMessage(emath.GqVectorOf(voteElem)), r2, rcPK0) + + // Single-component view of the ballot's slot 0 for the equality statement. + c1 := elgamal.NewCiphertext(ct.Gamma, emath.GqVectorOf(ct.GetPhi(0))) + eqProof := zkp.GenPlaintextEqualityProof( + c1, e2, + p.st.electionPK.Get(0), p.st.returnCodePK.Get(0), + msgRandomness, r2, 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, @@ -102,6 +126,8 @@ func (p *VoterClient) castBallot(selected []int) (*transport.Envelope, error) { ExponentiatedP: phi0Exp.Value().String(), VcPK: vcPK.Value().String(), ExpProof: encodeExponentiation(expProof), + ReturnCodeCT: encodeCiphertext(e2), + EqProof: encodePlaintextEquality(eqProof), } return p.cer.send(p.id, NameServer, MsgCastBallot, ballot) } @@ -206,5 +232,29 @@ func (p *ControlComponent) handleVerifyBallot(env *transport.Envelope) (*transpo if !ok { return reject("exponentiation proof INVALID") } + + // Verify the plaintext-equality proof binding E2 to the ballot's slot 0. + // This is what makes the return code cast-as-intended: if E2 encrypted a + // different vote than the ballot, this proof fails and the ballot is + // rejected, so the code computed from E2 must reflect the tallied vote. + e2, err := b.ReturnCodeCT.decode(group) + if err != nil { + return reject("bad return-code ciphertext") + } + eqProof, err := b.EqProof.decode(zq) + if err != nil { + return reject("bad equality proof encoding") + } + c1 := elgamal.NewCiphertext(ct.Gamma, emath.GqVectorOf(ct.GetPhi(0))) + eqOK := zkp.VerifyPlaintextEqualityProof( + c1, e2, + p.cer.Transcript.ElectionPK.Get(0), p.cer.Transcript.ReturnCodePK.Get(0), + eqProof, group, + hash.HashableString{Value: cfg.ElectionID}, + hash.HashableString{Value: b.VcID}, + ) + if !eqOK { + return reject("plaintext-equality proof INVALID") + } return reply(p.id, env.From, MsgBallotVerdict, env.Nonce, ballotVerdict{Accept: true}) } diff --git a/pkg/party/wire.go b/pkg/party/wire.go index e897793..2217691 100644 --- a/pkg/party/wire.go +++ b/pkg/party/wire.go @@ -138,6 +138,32 @@ func (w wireSchnorr) decodeExponentiation(zq *emath.ZqGroup) (zkp.Exponentiation return zkp.ExponentiationProof{E: e, Z: z}, nil } +// --- plaintext-equality proof (E scalar + Z vector of 2) --- + +type wirePlaintextEquality struct { + E string `json:"e"` + Z []string `json:"z"` +} + +func encodePlaintextEquality(p zkp.PlaintextEqualityProof) wirePlaintextEquality { + return wirePlaintextEquality{E: zqToStr(p.E), Z: zqVecToStrs(p.Z)} +} + +func (w wirePlaintextEquality) decode(zq *emath.ZqGroup) (zkp.PlaintextEqualityProof, error) { + e, err := strToZq(w.E, zq) + if err != nil { + return zkp.PlaintextEqualityProof{}, fmt.Errorf("pe E: %w", err) + } + z, err := strsToZqVec(w.Z, zq) + if err != nil { + return zkp.PlaintextEqualityProof{}, fmt.Errorf("pe Z: %w", err) + } + if z.Size() != 2 { + return zkp.PlaintextEqualityProof{}, fmt.Errorf("pe Z must have size 2, got %d", z.Size()) + } + return zkp.PlaintextEqualityProof{E: e, Z: z}, nil +} + // --- ciphertext --- type wireCiphertext struct {