mirror of
https://github.com/saymrwulf/swisspost-evoting-go-poc.git
synced 2026-07-25 19:48:35 +00:00
The return code the voter checks is now genuinely computed by the CCs from the submitted ciphertext, not looked up from the card: - returncode_extract.go: after a ballot is accepted, the server asks each CC to exponentiate E2 by its return-code key (product over CCs = Enc(vote^Σk)), then each CC contributes a partial-decryption factor; the server recovers vote^Σk, which equals the card base prime_sel^Σk, and looks up the short code. - The server returns that code to the voter, who checks it against the card for the chosen option; a mismatch aborts with a clear error. Soundness test: a malicious client that encrypts option A for the tally (E1) but option B in the return-code channel (E2) is REJECTED by the plaintext-equality proof — so the code shown always reflects the tallied vote. This closes the cast-as-intended gap (the old return codes were decorative, finding F16). Card lCC now uses a fixed tau so extraction can recompute it without learning the option up front. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
301 lines
11 KiB
Go
301 lines
11 KiB
Go
package party
|
|
|
|
import (
|
|
"fmt"
|
|
"math/big"
|
|
|
|
"github.com/user/evote/pkg/hash"
|
|
"github.com/user/evote/pkg/kdf"
|
|
emath "github.com/user/evote/pkg/math"
|
|
"github.com/user/evote/pkg/returncodes"
|
|
"github.com/user/evote/pkg/transport"
|
|
)
|
|
|
|
// Return-code share generation (the GenEncLongCodeShares exchange). The setup
|
|
// component asks each CC for its per-voter shares; each CC computes them from
|
|
// its PRIVATE return-code secret using deriveReturnCodeKey — the SINGLE key
|
|
// derivation used both here (card assembly) and later at vote-time extraction,
|
|
// which is what keeps the two consistent (this is the structural fix for the
|
|
// old setup/extraction derivation mismatch, finding F1).
|
|
|
|
const (
|
|
MsgLongCodeShareReq = "long-code-share-req" // setup -> CCj
|
|
MsgLongCodeShareResp = "long-code-share-resp"
|
|
MsgVotingCard = "voting-card" // setup -> voter (confidential)
|
|
MsgMappingTable = "mapping-table" // setup -> server (confidential)
|
|
|
|
labelChoice = "VoterChoiceReturnCodeGeneration"
|
|
labelConfirm = "VoterVoteCastReturnCodeGeneration"
|
|
)
|
|
|
|
// deriveReturnCodeKey derives a CC's voter-specific return-code key from its
|
|
// secret. Used identically at setup and extraction time.
|
|
func deriveReturnCodeKey(secret emath.ZqElement, label, electionID, vcID string, group *emath.GqGroup) emath.ZqElement {
|
|
zq := emath.ZqGroupFromGqGroup(group)
|
|
info := kdf.BuildKDFInfo(label, electionID, vcID)
|
|
kVal := kdf.KDFToZq(hash.IntegerToByteArray(secret.Value()), info, group.Q())
|
|
k, _ := emath.NewZqElement(kVal, zq)
|
|
return k
|
|
}
|
|
|
|
type longCodeShareReq struct {
|
|
VcID string `json:"vc_id"`
|
|
ElectionID string `json:"election_id"`
|
|
Primes []string `json:"primes"` // decimal encodings of the encoding primes
|
|
}
|
|
|
|
type longCodeShareResp struct {
|
|
ChoiceShares []string `json:"choice_shares"` // hpCC_i^{k_choice} per option
|
|
ConfirmShare string `json:"confirm_share"` // hCK^{k_confirm}
|
|
}
|
|
|
|
// handleLongCodeShare computes this CC's return-code shares for one voter.
|
|
func (p *ControlComponent) handleLongCodeShare(env *transport.Envelope) (*transport.Envelope, error) {
|
|
var req longCodeShareReq
|
|
if err := transport.UnmarshalPayload(env.Payload, &req); err != nil {
|
|
return nil, err
|
|
}
|
|
group := p.cer.Config.Group
|
|
|
|
kChoice := deriveReturnCodeKey(p.st.returnCodeSecret, labelChoice, req.ElectionID, req.VcID, group)
|
|
choiceShares := make([]string, len(req.Primes))
|
|
for i, ps := range req.Primes {
|
|
prime, ok := new(big.Int).SetString(ps, 10)
|
|
if !ok {
|
|
return nil, fmt.Errorf("cc%d: invalid prime %q", p.index, ps)
|
|
}
|
|
// Base is the encoding prime itself (a G_q element), NOT a hash of it:
|
|
// this keeps the code base algebraic so the CCs can recompute prime^k
|
|
// homomorphically from the submitted ciphertext at vote time
|
|
// (cast-as-intended). The combined card code is prime_i^{Σ_j k_j}.
|
|
base, err := emath.NewGqElement(prime, group)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cc%d: prime %d not in group: %w", p.index, i, err)
|
|
}
|
|
choiceShares[i] = base.Exponentiate(kChoice).Value().String()
|
|
}
|
|
|
|
// Confirmation-code share, keyed off a per-voter confirmation key.
|
|
kConfirm := deriveReturnCodeKey(p.st.returnCodeSecret, labelConfirm, req.ElectionID, req.VcID, group)
|
|
ckElem := confirmationKeyElement(req.VcID, group)
|
|
hCK := hash.HashAndSquare(ckElem.Value(), group)
|
|
confirmShare := hCK.Exponentiate(kConfirm).Value().String()
|
|
|
|
return reply(p.id, env.From, MsgLongCodeShareResp, env.Nonce, longCodeShareResp{
|
|
ChoiceShares: choiceShares,
|
|
ConfirmShare: confirmShare,
|
|
})
|
|
}
|
|
|
|
// RunCards assembles each voter's return-code card (collecting shares from the
|
|
// CCs) and distributes cards to voters and the mapping table to the voting
|
|
// server — both over CONFIDENTIAL (X25519-encrypted, Ed25519-signed) channels.
|
|
func (c *Ceremony) RunCards() error {
|
|
cfg := c.Config
|
|
c.Setup.st.mappingTable = returncodes.NewMappingTable()
|
|
|
|
c.logf(" setup: generating %d voting cards from CC shares...", cfg.NumVoters)
|
|
for v := 0; v < cfg.NumVoters; v++ {
|
|
card, err := c.assembleVotingCard(v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Deliver the card plus the public election parameters the voter needs
|
|
// to encrypt, confidentially.
|
|
primeStrs := make([]string, len(c.Setup.st.primes))
|
|
for i, p := range c.Setup.st.primes {
|
|
primeStrs[i] = p.String()
|
|
}
|
|
delivery := cardDelivery{
|
|
Card: *card,
|
|
ElectionPK: encodePK(c.Setup.st.electionPK),
|
|
ReturnCodePK: encodePK(c.Setup.st.returnCodePK),
|
|
Primes: primeStrs,
|
|
}
|
|
if _, err := c.sendConfidential(c.Setup.id, VoterName(v), MsgVotingCard, delivery); err != nil {
|
|
return fmt.Errorf("deliver card to voter %d: %w", v, err)
|
|
}
|
|
}
|
|
|
|
// Hand the mapping table and public election data to the voting server.
|
|
rows := c.Setup.st.mappingTable.Export()
|
|
primeStrs := make([]string, len(c.Setup.st.primes))
|
|
for i, p := range c.Setup.st.primes {
|
|
primeStrs[i] = p.String()
|
|
}
|
|
payload := mappingTablePayload{
|
|
Rows: rows,
|
|
ElectionPK: encodePK(c.Setup.st.electionPK),
|
|
ReturnCodePK: encodePK(c.Setup.st.returnCodePK),
|
|
Primes: primeStrs,
|
|
}
|
|
if _, err := c.sendConfidential(c.Setup.id, NameServer, MsgMappingTable, payload); err != nil {
|
|
return fmt.Errorf("deliver mapping table to server: %w", err)
|
|
}
|
|
c.logf(" setup: %d cards delivered to voters; mapping table delivered to server (all confidential)", cfg.NumVoters)
|
|
return nil
|
|
}
|
|
|
|
type mappingTablePayload struct {
|
|
Rows []returncodes.MappingRow `json:"rows"`
|
|
ElectionPK wirePublicKey `json:"election_pk"`
|
|
ReturnCodePK wirePublicKey `json:"return_code_pk"`
|
|
Primes []string `json:"primes"`
|
|
}
|
|
|
|
// cardDelivery is the confidential payload a voter receives: its private card
|
|
// plus the public election parameters it needs to encrypt a ballot.
|
|
type cardDelivery struct {
|
|
Card votingCard `json:"card"`
|
|
ElectionPK wirePublicKey `json:"election_pk"`
|
|
ReturnCodePK wirePublicKey `json:"return_code_pk"`
|
|
Primes []string `json:"primes"`
|
|
}
|
|
|
|
// handleVotingCard (voter) receives and stores its confidential card + params.
|
|
func (p *VoterClient) handleVotingCard(env *transport.Envelope) (*transport.Envelope, error) {
|
|
var d cardDelivery
|
|
if err := p.cer.openConfidential(p.id, env, &d); err != nil {
|
|
return nil, err
|
|
}
|
|
group := p.cer.Config.Group
|
|
pk, err := d.ElectionPK.decode(group)
|
|
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)
|
|
if !ok {
|
|
return nil, fmt.Errorf("voter: invalid prime %q", ps)
|
|
}
|
|
p.st.primes[i] = v
|
|
}
|
|
return reply(p.id, env.From, MsgAck, env.Nonce, ackPayload{Party: p.id.Name, OK: true})
|
|
}
|
|
|
|
// handleMappingTable (server) receives and stores the confidential mapping table
|
|
// and public election parameters.
|
|
func (p *VotingServer) handleMappingTable(env *transport.Envelope) (*transport.Envelope, error) {
|
|
var payload mappingTablePayload
|
|
if err := p.cer.openConfidential(p.id, env, &payload); err != nil {
|
|
return nil, err
|
|
}
|
|
group := p.cer.Config.Group
|
|
p.st.mappingTable = returncodes.ImportMappingTable(payload.Rows)
|
|
pk, err := payload.ElectionPK.decode(group)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rcPK, err := payload.ReturnCodePK.decode(group)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
p.st.electionPK = pk
|
|
p.st.returnCodePK = rcPK
|
|
p.st.primes = make([]*big.Int, len(payload.Primes))
|
|
for i, ps := range payload.Primes {
|
|
v, ok := new(big.Int).SetString(ps, 10)
|
|
if !ok {
|
|
return nil, fmt.Errorf("server: invalid prime %q", ps)
|
|
}
|
|
p.st.primes[i] = v
|
|
}
|
|
return reply(p.id, env.From, MsgAck, env.Nonce, ackPayload{Party: p.id.Name, OK: true})
|
|
}
|
|
|
|
// confirmationKeyElement deterministically maps a voter id to a G_q element used
|
|
// as the confirmation key (as in the single-process version).
|
|
func confirmationKeyElement(vcID string, group *emath.GqGroup) emath.GqElement {
|
|
seed := hash.RecursiveHashToZq(group.Q(),
|
|
hash.HashableString{Value: "ConfirmationKey"},
|
|
hash.HashableString{Value: vcID},
|
|
)
|
|
plusOne := new(big.Int).Add(seed, big.NewInt(1))
|
|
elem, err := emath.GqElementFromSquareRoot(plusOne, group)
|
|
if err != nil {
|
|
// plusOne is in [1, q] by construction, so this cannot fail.
|
|
panic("confirmation key element: " + err.Error())
|
|
}
|
|
return elem
|
|
}
|
|
|
|
// assembleVotingCard collects return-code shares from all CCs for one voter,
|
|
// combines them into the long code values, registers the (value -> short code)
|
|
// entries in the mapping table, and returns the voter's card.
|
|
func (c *Ceremony) assembleVotingCard(voterIdx int) (*votingCard, error) {
|
|
cfg := c.Config
|
|
group := cfg.Group
|
|
vcID := fmt.Sprintf("vc-%04d", voterIdx)
|
|
|
|
primeStrs := make([]string, len(c.Setup.st.primes))
|
|
for i, p := range c.Setup.st.primes {
|
|
primeStrs[i] = p.String()
|
|
}
|
|
|
|
// Collect combined choice shares across all CCs.
|
|
choiceCombined := make([]emath.GqElement, cfg.NumOptions)
|
|
for i := range choiceCombined {
|
|
choiceCombined[i] = group.Identity()
|
|
}
|
|
confirmCombined := group.Identity()
|
|
|
|
for j := 0; j < cfg.NumCCs; j++ {
|
|
env, err := c.send(c.Setup.id, CCName(j), MsgLongCodeShareReq, longCodeShareReq{
|
|
VcID: vcID, ElectionID: cfg.ElectionID, Primes: primeStrs,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("voter %d cc%d share: %w", voterIdx, j, err)
|
|
}
|
|
var resp longCodeShareResp
|
|
if err := transport.UnmarshalPayload(env.Payload, &resp); err != nil {
|
|
return nil, err
|
|
}
|
|
if len(resp.ChoiceShares) != cfg.NumOptions {
|
|
return nil, fmt.Errorf("voter %d cc%d: %d shares, want %d", voterIdx, j, len(resp.ChoiceShares), cfg.NumOptions)
|
|
}
|
|
for i, ss := range resp.ChoiceShares {
|
|
share, err := strToGq(ss, group) // validated group membership
|
|
if err != nil {
|
|
return nil, fmt.Errorf("voter %d cc%d share %d: %w", voterIdx, j, i, err)
|
|
}
|
|
choiceCombined[i] = choiceCombined[i].Multiply(share)
|
|
}
|
|
cShare, err := strToGq(resp.ConfirmShare, group)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("voter %d cc%d confirm share: %w", voterIdx, j, err)
|
|
}
|
|
confirmCombined = confirmCombined.Multiply(cShare)
|
|
}
|
|
|
|
// Derive the short codes and register mapping-table entries. The lCC value
|
|
// uses a fixed tau (not the per-option prime) so that vote-time extraction
|
|
// can recompute it from vote^{Σk} without knowing which option was chosen —
|
|
// the per-option base prime_i^{Σk} already makes the codes distinct.
|
|
choiceCodes := make([]string, cfg.NumOptions)
|
|
for i := 0; i < cfg.NumOptions; i++ {
|
|
lCC := returncodes.ComputeLCCValue(choiceCombined[i], vcID, cfg.ElectionID, tauFixed)
|
|
short := fmt.Sprintf("CC-%02d", i)
|
|
choiceCodes[i] = short
|
|
c.Setup.st.mappingTable.Add(lCC, short)
|
|
}
|
|
lVCC := returncodes.ComputeLVCCValue(confirmCombined, vcID, cfg.ElectionID)
|
|
vcc := fmt.Sprintf("VCC-%04d", voterIdx)
|
|
c.Setup.st.mappingTable.Add(lVCC, vcc)
|
|
|
|
return &votingCard{
|
|
VoterID: fmt.Sprintf("voter-%04d", voterIdx),
|
|
VerificationCardID: vcID,
|
|
StartVotingKey: fmt.Sprintf("SVK-%04d", voterIdx),
|
|
ChoiceReturnCodes: choiceCodes,
|
|
VoteConfirmCode: vcc,
|
|
BallotCastingKey: fmt.Sprintf("BCK-%04d", voterIdx),
|
|
}, nil
|
|
}
|