swisspost-evoting-go-poc/pkg/party/setup.go
saymrwulf d2cfbf291f 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>
2026-07-06 15:47:53 +02:00

231 lines
7.5 KiB
Go

package party
import (
"fmt"
"math/big"
"github.com/user/evote/pkg/elgamal"
"github.com/user/evote/pkg/hash"
emath "github.com/user/evote/pkg/math"
"github.com/user/evote/pkg/transport"
"github.com/user/evote/pkg/zkp"
)
// Setup-phase message types.
const (
MsgGenCCKeys = "gen-cc-keys" // setup -> CCj: generate election keys
MsgCCKeys = "cc-keys" // CCj -> setup: PK + Schnorr proofs
MsgGenEBKey = "gen-eb-key" // setup -> EB: generate board key
MsgEBKey = "eb-key" // EB -> setup: PK
)
// --- payloads ---
type genCCKeysReq struct {
ElectionID string `json:"election_id"`
NumOptions int `json:"num_options"`
CCIndex int `json:"cc_index"`
}
type ccKeysResp struct {
PK wirePublicKey `json:"pk"`
Schnorr []wireSchnorr `json:"schnorr"`
}
type genEBKeyReq struct {
ElectionID string `json:"election_id"`
NumOptions int `json:"num_options"`
}
type ebKeyResp struct {
PK wirePublicKey `json:"pk"`
}
// RunSetup executes the distributed key-generation part of the setup phase over
// the transport. Each CC and the EB generate their own key material privately;
// only public keys and proofs cross the bus. The setup component verifies every
// CC's Schnorr proof on receipt, combines the keys, and publishes the setup
// artifacts to the public transcript.
func (c *Ceremony) RunSetup() error {
cfg := c.Config
group := cfg.Group
zq := emath.ZqGroupFromGqGroup(group)
c.logf("\n--- SETUP PHASE (multi-party) ---")
// 1. Encoding primes (public, computed by the setup component).
raw := emath.SmallPrimes(cfg.NumOptions)
c.Setup.st.primes = make([]*big.Int, cfg.NumOptions)
for i, rp := range raw {
sq := new(big.Int).Exp(rp, big.NewInt(2), group.P())
if !group.IsGroupMember(sq) {
return fmt.Errorf("encoding prime %d not a group member", i)
}
c.Setup.st.primes[i] = sq
}
// 2. Ask each CC to generate its election keys; verify the returned proofs.
ccPKs := make([]elgamal.PublicKey, cfg.NumCCs)
c.Transcript.CCElectionPKs = make([]elgamal.PublicKey, cfg.NumCCs)
c.Transcript.CCSchnorr = make([][]zkp.SchnorrProof, cfg.NumCCs)
for j := 0; j < cfg.NumCCs; j++ {
env, err := c.send(c.Setup.id, CCName(j), MsgGenCCKeys, genCCKeysReq{
ElectionID: cfg.ElectionID, NumOptions: cfg.NumOptions, CCIndex: j,
})
if err != nil {
return fmt.Errorf("setup->cc%d keygen: %w", j, err)
}
var resp ccKeysResp
if err := transport.UnmarshalPayload(env.Payload, &resp); err != nil {
return err
}
pk, err := resp.PK.decode(group)
if err != nil {
return fmt.Errorf("cc%d pk: %w", j, err)
}
proofs := make([]zkp.SchnorrProof, len(resp.Schnorr))
for i, ws := range resp.Schnorr {
p, err := ws.decode(zq)
if err != nil {
return fmt.Errorf("cc%d schnorr %d: %w", j, i, err)
}
proofs[i] = p
}
// Verify each CC's proof of knowledge of its secret key (the setup
// component does not trust the CC's word).
if err := verifyCCSchnorr(cfg.ElectionID, group, j, pk, proofs); err != nil {
return err
}
c.logf(" setup: cc%d keys received, %d Schnorr proofs VALID", j, len(proofs))
ccPKs[j] = pk
c.Transcript.CCElectionPKs[j] = pk
c.Transcript.CCSchnorr[j] = proofs
}
// 3. Ask the electoral board to generate its key.
env, err := c.send(c.Setup.id, NameEB, MsgGenEBKey, genEBKeyReq{
ElectionID: cfg.ElectionID, NumOptions: cfg.NumOptions,
})
if err != nil {
return fmt.Errorf("setup->eb keygen: %w", err)
}
var ebResp ebKeyResp
if err := transport.UnmarshalPayload(env.Payload, &ebResp); err != nil {
return err
}
ebPK, err := ebResp.PK.decode(group)
if err != nil {
return fmt.Errorf("eb pk: %w", err)
}
c.logf(" setup: electoral board key received")
// 4. Combine keys.
c.Setup.st.returnCodePK = elgamal.CombinePublicKeys(ccPKs...)
c.Setup.st.electionPK = elgamal.CombinePublicKeys(append(append([]elgamal.PublicKey{}, ccPKs...), ebPK)...)
// 5. Publish setup artifacts to the transcript.
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()
}
c.logf(" setup: election public key assembled and published to transcript")
return nil
}
// verifyCCSchnorr checks that every key element's Schnorr proof is valid for the
// stated CC index (using the same aux binding as generation).
func verifyCCSchnorr(electionID string, group *emath.GqGroup, j int, pk elgamal.PublicKey, proofs []zkp.SchnorrProof) error {
if len(proofs) != pk.Elements.Size() {
return fmt.Errorf("cc%d: %d proofs for %d keys", j, len(proofs), pk.Elements.Size())
}
for i := 0; i < pk.Elements.Size(); i++ {
aux := []hash.Hashable{
hash.HashableBigInt{Value: big.NewInt(int64(i))},
hash.HashableString{Value: electionID},
hash.HashableBigInt{Value: big.NewInt(int64(j))},
}
if !zkp.VerifySchnorrProof(proofs[i], pk.Elements.Get(i), group, aux...) {
return fmt.Errorf("cc%d key %d: Schnorr proof INVALID", j, i)
}
}
return nil
}
// --- CC handler: generate election keys ---
func (p *ControlComponent) handleGenCCKeys(env *transport.Envelope) (*transport.Envelope, error) {
var req genCCKeysReq
if err := transport.UnmarshalPayload(env.Payload, &req); err != nil {
return nil, err
}
cfg := p.cer.Config
group := cfg.Group
zq := emath.ZqGroupFromGqGroup(group)
kp := elgamal.GenKeyPair(group, req.NumOptions)
proofs := make([]zkp.SchnorrProof, req.NumOptions)
wireProofs := make([]wireSchnorr, req.NumOptions)
for i := 0; i < req.NumOptions; i++ {
aux := []hash.Hashable{
hash.HashableBigInt{Value: big.NewInt(int64(i))},
hash.HashableString{Value: req.ElectionID},
hash.HashableBigInt{Value: big.NewInt(int64(req.CCIndex))},
}
proofs[i] = zkp.GenSchnorrProof(kp.SK.Get(i), kp.PK.Get(i), group, aux...)
wireProofs[i] = encodeSchnorr(proofs[i])
}
// Store private key material; only the PK and proofs leave the CC.
p.st.keyPair = kp
p.st.returnCodeSecret = emath.RandomZqElement(zq)
p.st.schnorrProofs = proofs
return reply(p.id, env.From, MsgCCKeys, env.Nonce, ccKeysResp{
PK: encodePK(kp.PK),
Schnorr: wireProofs,
})
}
// --- EB handler: generate board key ---
func (p *ElectoralBoard) handleGenEBKey(env *transport.Envelope) (*transport.Envelope, error) {
var req genEBKeyReq
if err := transport.UnmarshalPayload(env.Payload, &req); err != nil {
return nil, err
}
cfg := p.cer.Config
group := cfg.Group
zq := emath.ZqGroupFromGqGroup(group)
// EB key is derived from board passwords (as in the single-process version),
// but the secret never leaves the EB.
p.st.passwords = []string{"password1", "password2"}
g := group.Generator()
skElems := make([]emath.ZqElement, req.NumOptions)
pkElems := make([]emath.GqElement, req.NumOptions)
for i := 0; i < req.NumOptions; i++ {
args := []hash.Hashable{
hash.HashableString{Value: "ElectoralBoardSecretKey"},
hash.HashableString{Value: req.ElectionID},
hash.HashableBigInt{Value: big.NewInt(int64(i))},
}
for _, pw := range p.st.passwords {
args = append(args, hash.HashableString{Value: pw})
}
skVal := hash.RecursiveHashToZq(group.Q(), args...)
skElems[i], _ = emath.NewZqElement(skVal, zq)
pkElems[i] = g.Exponentiate(skElems[i])
}
p.st.keyPair = elgamal.KeyPair{
SK: elgamal.PrivateKey{Elements: emath.ZqVectorOf(skElems...)},
PK: elgamal.PublicKey{Elements: emath.GqVectorOf(pkElems...)},
}
return reply(p.id, env.From, MsgEBKey, env.Nonce, ebKeyResp{PK: encodePK(p.st.keyPair.PK)})
}