swisspost-evoting-go-poc/pkg/party/setup.go
saymrwulf 6933835bac Distributed key generation over signed transport + validated wire layer
wire.go: the validated serialization boundary between parties. Crypto objects
(group elements, public keys, Schnorr proofs, ciphertexts) travel as decimal
DTOs; every decode routes through NewGqElement/NewZqElement so a peer cannot
inject a value outside G_q or Z_q — closing the small-subgroup / non-residue
hole (finding M4) at the trust boundary.

setup.go: RunSetup drives distributed key generation over the bus. Each CC
generates its ElGamal keypair + return-code secret PRIVATELY and returns only
its public key and Schnorr proofs; the setup component verifies every proof on
receipt before combining keys. The electoral board derives its own key and
returns only the public key. Combined election PK and setup artifacts are
published to the public transcript.

Test confirms the combined election key equals the product of the individually
generated CC and EB keys, and that private key material stays with each party
(never appears in the transcript).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 15:11:34 +02:00

230 lines
7.4 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.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)})
}