mirror of
https://github.com/saymrwulf/swisspost-evoting-go-poc.git
synced 2026-07-30 20:18:16 +00:00
Ballot carries E2 + plaintext-equality proof, CC-verified
The voter now also submits E2 = Enc(vote, returnCodesPK[0]) and a plaintext- equality proof that E2 and the ballot's slot 0 encrypt the SAME vote. Every CC verifies this proof during ballot verification. This is the soundness link that makes the return code cast-as-intended: a client that encrypts one vote for the tally and a different one for the return-code channel is rejected, so the code the CCs compute from E2 necessarily reflects the tallied vote. - transcript: publish the combined return-codes public key. - voter stores returnCodePK from the (confidential) card delivery. - wire: plaintext-equality proof DTO. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
577205a2ff
commit
d2cfbf291f
6 changed files with 99 additions and 15 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue