mirror of
https://github.com/saymrwulf/swisspost-evoting-go-poc.git
synced 2026-07-24 19:43:51 +00:00
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>
231 lines
7.5 KiB
Go
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)})
|
|
}
|