swisspost-evoting-go-poc/pkg/party/codes.go
saymrwulf 127ae61d11 Cast-as-intended return codes: CC extraction + voter check
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>
2026-07-06 15:52:30 +02:00

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
}